Fix Supabase linter performance findings
- add covering indexes for flagged foreign keys - optimize RLS policies to use select auth.uid() - consolidate posts update policies into one permissive rule - document linter findings and decisions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# Database performance notes (Supabase linter)
|
||||
|
||||
This project had two performance-related linter groups:
|
||||
|
||||
1. **RLS auth function evaluation**
|
||||
2. **Unindexed foreign keys**
|
||||
|
||||
## What was changed
|
||||
|
||||
1. Added migration `infrastructure/migrations/002_perf_linter_fixes.sql`.
|
||||
2. Updated `infrastructure/migrations/001_schema.sql` to keep fresh setups aligned with the same fixes.
|
||||
|
||||
### RLS changes
|
||||
|
||||
The affected policies now use:
|
||||
|
||||
- `(select auth.uid())` instead of `auth.uid()`
|
||||
|
||||
This avoids repeated per-row evaluation in RLS plans for these policies:
|
||||
|
||||
- `profiles_update`
|
||||
- `posts_select`
|
||||
- `posts_insert`
|
||||
- `posts_update_own`
|
||||
- `resonances_insert`
|
||||
- `resonances_delete`
|
||||
- `whispers_select`
|
||||
- `whispers_insert`
|
||||
- `follows_insert`
|
||||
- `follows_delete`
|
||||
- `reports_insert`
|
||||
- `reports_select_own`
|
||||
|
||||
Also, duplicate permissive update policies on `public.posts` were consolidated by removing `posts_softdelete` and keeping a single `posts_update_own`.
|
||||
|
||||
### FK index changes
|
||||
|
||||
Added covering indexes:
|
||||
|
||||
- `idx_follows_following_id` on `public.follows(following_id)`
|
||||
- `idx_reports_reporter_id` on `public.reports(reporter_id)`
|
||||
- `idx_resonances_user_id` on `public.resonances(user_id)`
|
||||
- `idx_whispers_from_user_id` on `public.whispers(from_user_id)`
|
||||
- `idx_whispers_post_id` on `public.whispers(post_id)`
|
||||
|
||||
## About "unused index" findings
|
||||
|
||||
`unused_index` is based on `pg_stat_user_indexes.idx_scan`, which can read as zero after restarts or during low traffic.
|
||||
In this environment, PostgreSQL was recently restarted, so these were not dropped automatically.
|
||||
|
||||
Suggested review query:
|
||||
|
||||
```sql
|
||||
select
|
||||
schemaname,
|
||||
relname as table_name,
|
||||
indexrelname as index_name,
|
||||
idx_scan
|
||||
from pg_stat_user_indexes
|
||||
where schemaname = 'public'
|
||||
order by idx_scan asc, indexrelname;
|
||||
```
|
||||
|
||||
Drop an index only after observing sustained zero usage over representative production traffic.
|
||||
@@ -87,8 +87,13 @@ CREATE INDEX IF NOT EXISTS idx_posts_created_at ON public.posts(created_at DESC)
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_review ON public.posts(review_status)
|
||||
WHERE review_status != 'ok';
|
||||
CREATE INDEX IF NOT EXISTS idx_resonances_post ON public.resonances(post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resonances_user_id ON public.resonances(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_follows_follower ON public.follows(follower_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_follows_following_id ON public.follows(following_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_whispers_to ON public.whispers(to_user_id, read_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_whispers_from_user_id ON public.whispers(from_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_whispers_post_id ON public.whispers(post_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reports_reporter_id ON public.reports(reporter_id);
|
||||
|
||||
-- ── View: Feed Posts ────────────────────────────────────────────────────────
|
||||
-- Wird in der iOS-App direkt abgefragt — joins alles zusammen
|
||||
@@ -195,7 +200,7 @@ ALTER TABLE public.reports ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- profiles: jeder darf lesen, nur eigenes darf geändert werden
|
||||
CREATE POLICY "profiles_select" ON public.profiles FOR SELECT USING (true);
|
||||
CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE USING (auth.uid() = id);
|
||||
CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE USING ((select auth.uid()) = id);
|
||||
|
||||
-- posts: Feed-Sichtbarkeit (über View geregelt), eigene Verwaltung
|
||||
CREATE POLICY "posts_select" ON public.posts FOR SELECT
|
||||
@@ -204,34 +209,31 @@ CREATE POLICY "posts_select" ON public.posts FOR SELECT
|
||||
AND review_status IN ('ok', 'approved')
|
||||
AND (
|
||||
NOT is_anonymous
|
||||
OR user_id = auth.uid()
|
||||
OR user_id = (select auth.uid())
|
||||
)
|
||||
);
|
||||
CREATE POLICY "posts_insert" ON public.posts FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
WITH CHECK ((select auth.uid()) = user_id);
|
||||
CREATE POLICY "posts_update_own" ON public.posts FOR UPDATE
|
||||
USING (auth.uid() = user_id AND deleted_at IS NULL);
|
||||
-- Soft-Delete: nur eigene Posts dürfen als gelöscht markiert werden
|
||||
CREATE POLICY "posts_softdelete" ON public.posts FOR UPDATE
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (deleted_at IS NOT NULL);
|
||||
USING ((select auth.uid()) = user_id)
|
||||
WITH CHECK ((select auth.uid()) = user_id);
|
||||
|
||||
-- resonances
|
||||
CREATE POLICY "resonances_select" ON public.resonances FOR SELECT USING (true);
|
||||
CREATE POLICY "resonances_insert" ON public.resonances FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||
CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING (auth.uid() = user_id);
|
||||
CREATE POLICY "resonances_insert" ON public.resonances FOR INSERT WITH CHECK ((select auth.uid()) = user_id);
|
||||
CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING ((select auth.uid()) = user_id);
|
||||
|
||||
-- whispers: nur Absender und Empfänger
|
||||
CREATE POLICY "whispers_select" ON public.whispers FOR SELECT
|
||||
USING (auth.uid() = from_user_id OR auth.uid() = to_user_id);
|
||||
USING ((select auth.uid()) = from_user_id OR (select auth.uid()) = to_user_id);
|
||||
CREATE POLICY "whispers_insert" ON public.whispers FOR INSERT
|
||||
WITH CHECK (auth.uid() = from_user_id);
|
||||
WITH CHECK ((select auth.uid()) = from_user_id);
|
||||
|
||||
-- follows
|
||||
CREATE POLICY "follows_select" ON public.follows FOR SELECT USING (true);
|
||||
CREATE POLICY "follows_insert" ON public.follows FOR INSERT WITH CHECK (auth.uid() = follower_id);
|
||||
CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING (auth.uid() = follower_id);
|
||||
CREATE POLICY "follows_insert" ON public.follows FOR INSERT WITH CHECK ((select auth.uid()) = follower_id);
|
||||
CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING ((select auth.uid()) = follower_id);
|
||||
|
||||
-- reports: nur eigene einsehen
|
||||
CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK (auth.uid() = reporter_id);
|
||||
CREATE POLICY "reports_select_own" ON public.reports FOR SELECT USING (auth.uid() = reporter_id);
|
||||
CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK ((select auth.uid()) = reporter_id);
|
||||
CREATE POLICY "reports_select_own" ON public.reports FOR SELECT USING ((select auth.uid()) = reporter_id);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- Performance follow-up for DB linter findings:
|
||||
-- 1) Wrap auth.uid() in RLS policies with SELECT to avoid per-row re-evaluation.
|
||||
-- 2) Ensure every FK in follows/reports/resonances/whispers has a covering index.
|
||||
-- 3) Merge duplicate permissive UPDATE policies on posts.
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER POLICY profiles_update ON public.profiles
|
||||
USING ((select auth.uid()) = id);
|
||||
|
||||
ALTER POLICY posts_select ON public.posts
|
||||
USING (
|
||||
deleted_at IS NULL
|
||||
AND review_status IN ('ok', 'approved')
|
||||
AND (
|
||||
NOT is_anonymous
|
||||
OR user_id = (select auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
ALTER POLICY posts_insert ON public.posts
|
||||
WITH CHECK ((select auth.uid()) = user_id);
|
||||
|
||||
DROP POLICY IF EXISTS posts_softdelete ON public.posts;
|
||||
ALTER POLICY posts_update_own ON public.posts
|
||||
USING ((select auth.uid()) = user_id)
|
||||
WITH CHECK ((select auth.uid()) = user_id);
|
||||
|
||||
ALTER POLICY resonances_insert ON public.resonances
|
||||
WITH CHECK ((select auth.uid()) = user_id);
|
||||
|
||||
ALTER POLICY resonances_delete ON public.resonances
|
||||
USING ((select auth.uid()) = user_id);
|
||||
|
||||
ALTER POLICY whispers_select ON public.whispers
|
||||
USING (
|
||||
(select auth.uid()) = from_user_id
|
||||
OR (select auth.uid()) = to_user_id
|
||||
);
|
||||
|
||||
ALTER POLICY whispers_insert ON public.whispers
|
||||
WITH CHECK ((select auth.uid()) = from_user_id);
|
||||
|
||||
ALTER POLICY follows_insert ON public.follows
|
||||
WITH CHECK ((select auth.uid()) = follower_id);
|
||||
|
||||
ALTER POLICY follows_delete ON public.follows
|
||||
USING ((select auth.uid()) = follower_id);
|
||||
|
||||
ALTER POLICY reports_insert ON public.reports
|
||||
WITH CHECK ((select auth.uid()) = reporter_id);
|
||||
|
||||
ALTER POLICY reports_select_own ON public.reports
|
||||
USING ((select auth.uid()) = reporter_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_follows_following_id ON public.follows (following_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reports_reporter_id ON public.reports (reporter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resonances_user_id ON public.resonances (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_whispers_from_user_id ON public.whispers (from_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_whispers_post_id ON public.whispers (post_id);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user