Demo, all content is generated
Question

Query got 20x slower after I added RLS, is that normal?

Solved · 1486 views · asked by rosa_m · edited

My events table has ~200k rows. Before RLS, loading a user's events took 40ms. After adding this policy it takes 800ms+:

create policy "own events" on events for select using (auth.uid() = user_id);

Same query, same user. Is RLS just slow?

What I’ve tried

Ran explain in the SQL editor, but there it's fast because I'm postgres. Added .eq('user_id', user.id) in the client, helped a bit.

Comment
Do you have an index on user_id? hannah_reyes · edited

3 answers

Marked as helpful by the asker
hannah_reyes · edited

RLS isn't slow, this policy is. Two fixes:

  1. Wrap the function in a subselect so Postgres evaluates it once per query instead of once per row:
drop policy "own events" on events;
create policy "own events" on events for select to authenticated
  using ((select auth.uid()) = user_id);
  1. Index the column the policy filters on:
create index on events (user_id);

Also adding to authenticated means the policy isn't even evaluated for anonymous requests. And keep the .eq('user_id', ...) in the client: the planner can use it directly, the policy is a safety net.

To see the real plan as your user, run in the SQL editor:

set local role authenticated;
set local request.jwt.claims = '{"sub":"<user-uuid>"}';
explain analyze select * from events;
Comment
From 800ms to 12ms. The index was missing completely. rosa_m · edited
The Performance Advisor flags the missing subselect as auth_rls_initplan, handy to scan all your policies at once. lena_ops · edited
sarah_k_dev · edited

Same trick applies to auth.jwt() if you read claims in policies.

Comment
I do read a role claim in another policy, fixed that one too rosa_m · edited
elif_y · edited

Also check you don't have two permissive select policies on the table. They're OR-ed together, so both get evaluated, and the slow one still runs even if the fast one would allow the row.

Comment