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)
|
CREATE INDEX IF NOT EXISTS idx_posts_review ON public.posts(review_status)
|
||||||
WHERE review_status != 'ok';
|
WHERE review_status != 'ok';
|
||||||
CREATE INDEX IF NOT EXISTS idx_resonances_post ON public.resonances(post_id);
|
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_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_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 ────────────────────────────────────────────────────────
|
-- ── View: Feed Posts ────────────────────────────────────────────────────────
|
||||||
-- Wird in der iOS-App direkt abgefragt — joins alles zusammen
|
-- 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
|
-- profiles: jeder darf lesen, nur eigenes darf geändert werden
|
||||||
CREATE POLICY "profiles_select" ON public.profiles FOR SELECT USING (true);
|
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
|
-- posts: Feed-Sichtbarkeit (über View geregelt), eigene Verwaltung
|
||||||
CREATE POLICY "posts_select" ON public.posts FOR SELECT
|
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 review_status IN ('ok', 'approved')
|
||||||
AND (
|
AND (
|
||||||
NOT is_anonymous
|
NOT is_anonymous
|
||||||
OR user_id = auth.uid()
|
OR user_id = (select auth.uid())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
CREATE POLICY "posts_insert" ON public.posts FOR INSERT
|
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
|
CREATE POLICY "posts_update_own" ON public.posts FOR UPDATE
|
||||||
USING (auth.uid() = user_id AND deleted_at IS NULL);
|
USING ((select auth.uid()) = user_id)
|
||||||
-- Soft-Delete: nur eigene Posts dürfen als gelöscht markiert werden
|
WITH CHECK ((select auth.uid()) = user_id);
|
||||||
CREATE POLICY "posts_softdelete" ON public.posts FOR UPDATE
|
|
||||||
USING (auth.uid() = user_id)
|
|
||||||
WITH CHECK (deleted_at IS NOT NULL);
|
|
||||||
|
|
||||||
-- resonances
|
-- resonances
|
||||||
CREATE POLICY "resonances_select" ON public.resonances FOR SELECT USING (true);
|
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_insert" ON public.resonances FOR INSERT WITH CHECK ((select auth.uid()) = user_id);
|
||||||
CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING (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
|
-- whispers: nur Absender und Empfänger
|
||||||
CREATE POLICY "whispers_select" ON public.whispers FOR SELECT
|
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
|
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
|
-- follows
|
||||||
CREATE POLICY "follows_select" ON public.follows FOR SELECT USING (true);
|
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_insert" ON public.follows FOR INSERT WITH CHECK ((select auth.uid()) = follower_id);
|
||||||
CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING (auth.uid() = follower_id);
|
CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING ((select auth.uid()) = follower_id);
|
||||||
|
|
||||||
-- reports: nur eigene einsehen
|
-- reports: nur eigene einsehen
|
||||||
CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK (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 (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