Chris van Eijk:实测 PostgreSQL 行级安全策略的性能开销

Chris van Eijk:实测 PostgreSQL 行级安全策略的性能开销

💡 原文英文,约2000词,阅读约需8分钟。
📝

内容提要

实测 PostgreSQL 17 行级安全策略的性能开销:简单等值策略几乎无成本;调用 PL/pgSQL 等不可内联函数的策略会退化为全表扫描,耗时近 2 秒;成员子查询策略每查询约 80 毫秒。lower()、LIKE 等非 leakproof 函数会阻止索引使用,使大租户查询从 0.2 毫秒增至 45 毫秒。建议将辅助函数声明为 STABLE PARALLEL SAFE,使用生成列或 leakproof 操作符,并提前解析租户归属。

🔎

延伸解读

策略函数内联与否决定性能

策略中获取租户ID的函数能否被内联是关键。SQL函数通常可内联,即使声明为VOLATILE也很快;而PL/pgSQL函数无法内联,若声明为VOLATILE(默认),则每行调用一次,导致索引失效,查询退化为全表扫描,耗时近2秒。声明为STABLE可避免此问题,因为函数只计算一次。因此,无论使用何种语言,都应将辅助函数声明为STABLE。

成员子查询策略的高昂代价

使用IN (SELECT ... FROM memberships)的策略会导致每查询约80毫秒的开销,因为规划器无法使用索引,改为逐行检查成员列表。改写为= ANY (ARRAY(SELECT ...))可修复计数查询,但可能使大租户的最新50条查询变慢(158毫秒)。最佳实践是在请求开始时解析成员关系,将租户ID放入设置中,策略直接比较单个值,避免每行子查询。

非leakproof函数导致索引失效

lower()、LIKE等非leakproof函数会阻止索引使用,因为PostgreSQL必须在应用用户条件前先执行安全策略条件。这导致大租户查询从0.2毫秒增至45毫秒,而小租户几乎无感。解决方案包括:使用生成列存储lower(customer_email)并索引,或使用leakproof操作符如~>=~和~<~进行前缀搜索。切勿将lower()标记为leakproof,除非你完全了解其影响。

并行计划与连接池的潜在问题

声明函数为PARALLEL SAFE可恢复并行计划,提升大租户计数性能。但并行计划在共享连接上可能带来问题:psycopg等驱动会缓存计划,导致后续查询使用第一个租户的计划,使中位租户查询从0.2毫秒增至7.3毫秒。在连接池(如PgBouncer)后问题更严重。因此,需权衡并行收益与计划缓存风险,或使用强制重新规划。

❓

Q&A

PostgreSQL 行级安全策略最简单的写法性能开销大吗?

不大。在 200 万行发票表上,使用 tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid 的策略,查询速度与没有 RLS 且显式 WHERE tenant_id = … 时几乎相同:最大租户计数 11.0 ms 对 11.0 ms,中位租户最新 50 条 0.30 ms 对 0.34 ms。

为什么我的 RLS 策略会导致全表扫描,查询耗时近 2 秒?

很可能因为策略中调用了 PL/pgSQL 函数且未声明为 STABLE。PL/pgSQL 函数无法被内联,默认 VOLATILE 时会对每一行调用一次,导致索引无法使用,查询退化为对 200 万行的顺序扫描,耗时约 1.9 秒。将函数声明为 STABLE 或用 (select …) 包裹可修复。

RLS 策略中使用成员子查询(如 tenant_id in (select … from memberships))性能如何?

性能很差,每次查询约 80 毫秒。在 200 万行表上,最大租户计数 83–85 ms,中位租户计数 79–81 ms,而简单策略仅 0.15–0.30 ms。因为规划器无法用索引查找单个值,只能逐行检查成员列表。建议在请求开始时解析租户归属,将租户 ID 放入设置中,策略直接比较单个值。

为什么 RLS 下使用 lower() 或 LIKE 查询会变慢,如何解决?

因为 lower() 和 LIKE 不是 leakproof 函数,PostgreSQL 会先应用策略条件再评估这些函数,导致索引只能用于租户过滤,然后对租户所有行逐行计算 lower()。最大租户查询从 0.2 ms 增至 34–45 ms。解决方案:使用存储生成列(如 customer_email_lower)并建立索引,或对前缀搜索使用 leakproof 操作符 ~>=~ 和 ~<~ 手写范围。

如何检查数据库中哪些 RLS 策略可能因函数易变性或 leakproof 规则而变慢?

两个检查:1) 查询 pg_policies 和 pg_proc,找出策略表达式中调用了 VOLATILE 函数的策略(注意同名函数可能误报);2) 以应用角色(而非表所有者)对最慢的查询运行 EXPLAIN,如果某个条件在所有者下是 Index Cond 而在应用角色下是 Filter,就是 leakproof 规则导致的问题。

优化 RLS 策略性能有哪些最佳实践?

1) 将辅助函数声明为 STABLE PARALLEL SAFE,避免依赖内联并保留并行计划;2) 避免在策略中使用成员子查询,改为在请求开始时解析租户归属并放入设置;3) 对于 lower() 等非 leakproof 函数,使用存储生成列或 leakproof 操作符;4) 注意 SECURITY DEFINER 和 SET search_path 会阻止 SQL 函数内联,应避免或配合 STABLE 使用。

🏷️

标签

➡️

继续阅读