Wrap auth calls in (select …) and index the policy column

(select auth.uid()) is evaluated once per statement instead of per row; a btree index whose first column is the policy column turns the check into a lookup.

RLS & Auth

· Chapter

23

·

3

min read

The answer. A policy is a WHERE clause Postgres adds to every query, and it is planned like one. Written as auth.uid() = owner, the function is called for each candidate row; written as (select auth.uid()) = owner, the planner evaluates it once as an init-plan and compares a constant. The advisor flags the slow form (auth_rls_initplan). The second half is the same as any query: the column the policy compares needs a btree index with that column first, or every request walks the table. Beyond those two, prefer a security definer helper for membership checks so nested tables do not run their own policies per row, name the role on the policy, and add explicit filters (.eq("tenant", t)) to queries even where RLS would filter anyway, because an explicit filter is something the planner can use an index for.

The pattern.

-- slow: per-row function call, no usable index
create policy p on public.tickets for select to authenticated using (auth.uid() = owner);
-- fast: init-plan + index
create policy p on public.tickets for select to authenticated using ((select auth.uid()) = owner);
create index tickets_owner_idx on public.tickets (owner);
const { data } = await supabase.from("tickets").select("id, title").eq("tenant", tenant).explain({ analyze: true });
// look for "Index Scan" on tickets_tenant_idx, and for InitPlan 1 (returns $0) instead of a Filter with auth.uid()

Watch out.

  • A column counts as indexed only when it is the first column of a btree; (created_at, tenant) does nothing for a tenant check.
  • Diagnose in a test environment by temporarily rewriting a policy to true or <original>; if the query gets fast, RLS was the bottleneck.
  • .explain() needs the pg_stat_statements-style explain setting enabled on the project; keep it off in production except while investigating.

Related: first-table-enable-rls-write-one-policy-query-it · find-slow-queries-explain-analyze-advisors-inspect-db · a-policy-that-reads-another-table-runs-that-tables-policies