Chris van Eijk:Supabase RLS 实测——(select auth.uid()) 何时真正有效

Chris van Eijk:Supabase RLS 实测——(select auth.uid()) 何时真正有效

💡 原文英文,约1700词,阅读约需7分钟。
📝

内容提要

实测 Supabase RLS 中 auth.uid() 与 (select auth.uid()) 的差异:在百万行表且无索引时,count 查询从 923ms 降至 42ms,原因是包装后只执行一次;有索引时两者均为 0.1–0.3ms。查询自带 user_id 过滤时也很快。建议为策略列建索引,仍用 (select …) 包装 auth.uid() 和 auth.jwt(),并将角色检查改用 to 子句。

🔎

延伸解读

索引才是性能分水岭

实测显示,在 user_id 上建索引后,无论是否用 (select auth.uid()) 包装,所有查询都降到 0.1–0.3ms。无索引时包装才带来 923ms 到 42ms 的差距。这说明优化 RLS 策略时,优先为策略比较的列建索引,比纠结函数写法更有效。索引让条件变成 Index Cond,只计算一次,而非逐行求值。

包装生效的边界条件

包装只在条件无法利用索引、退化为逐行 Filter 时明显有效。此时内联的 auth.uid() 会每行执行 current_setting、jsonb 解析和类型转换,百万行就是百万次。包装后生成 InitPlan,只执行一次。若查询自身已带 user_id 过滤,规划器可能将其合并为 One-Time Filter,效果与包装相当,但策略仍需为忘记过滤的查询兜底。

角色检查应改用 to 子句

额外用 auth.jwt() ->> 'role' = 'authenticated' 做角色检查是最昂贵的模式,无索引时达 1592ms,比仅 uid 检查的 923ms 更慢。包装两个函数后降到 42ms 左右。文章建议将角色检查改为策略的 to 子句,因为策略本已限定 to authenticated,叠加角色检查只增加成本。

实测的局限与适用提醒

结论基于 PostgreSQL 17.11/18.6 的一次性容器、默认配置和特定数据分布(每用户 100 行)。文章未覆盖 PostgREST 实际事务设置、托管平台、联表策略,以及索引存在但规划器仍选顺序扫描的情况。因此,索引加包装是稳妥起点,但生产环境仍需结合自身查询模式验证执行计划。

❓

Q&A

Supabase RLS 里 auth.uid() 和 (select auth.uid()) 到底有什么区别?

auth.uid() 是内联的 SQL 函数,在策略中会被展开成表达式;用 (select auth.uid()) 包装后,PostgreSQL 会生成 InitPlan,使函数体只执行一次,而不是每行执行一次。在无索引的百万行表上,count 查询从 923ms 降到 42ms。

为什么 (select auth.uid()) 只在没有索引的时候才明显提速?

有索引时,内联表达式会成为索引条件(Index Cond),每次扫描只计算一次,所以两种写法都只要 0.1–0.3ms。无索引时条件变成顺序扫描上的 Filter,每行都要计算,包装后变成 InitPlan 只算一次,因此差距巨大。

在 Supabase RLS 策略里,给 user_id 建索引能带来多大提升?

建索引是最大的优化杠杆:在百万行表上,无论用 auth.uid() 还是 (select auth.uid()),所有查询都从数百毫秒降到 0.1–0.3ms。

查询里自己带上 user_id 过滤条件,还需要用 (select auth.uid()) 吗?

查询自带 user_id 过滤时,规划器会把它和策略合并成 One-Time Filter,只计算一次函数,耗时约 40ms,与包装效果相当。但不能依赖每个查询都这么做,策略是为那些忘记过滤的查询兜底的,所以仍建议包装。

用 auth.jwt() 做角色检查为什么慢?该怎么改?

同时检查 auth.jwt() ->> 'role' = 'authenticated' 的策略最慢,无包装时达 1592ms,因为角色检查在 RLS 下仍逐行执行。建议把 auth.jwt() 也用 (select …) 包装,或者干脆去掉角色检查,改用策略的 to 子句指定角色。

根据实测,Supabase RLS 策略的最佳实践是什么?

首先为策略比较的列(如 user_id、tenant_id)建索引;其次仍然用 (select …) 包装 auth.uid() 和 auth.jwt(),有索引时无额外开销,无索引时能兜底;最后把角色检查写成 to 子句,而不是在策略里用 auth.jwt() 判断。

🏷️

标签

➡️

继续阅读