内容提要
本文讨论 PostgreSQL 17 中“stack depth limit exceeded”错误的成因与修复。作者实测四种情形:自更新触发器、自调用函数、通过函数读取自身表的行级安全策略、超长表达式。前三种属无限递归,调高 max_stack_depth 只能延后报错;超长表达式有界,提高上限可放行。修复方法:触发器改用 BEFORE 修改行、函数加基准条件、策略避免自引用;扁平 OR/AND 链和 IN 列表不易触发。
延伸解读
调高 max_stack_depth 并非万能解
文章实测表明,对于自更新触发器、自调用函数、通过函数读取自身表的行级安全策略这三类无限递归,将 max_stack_depth 从 2MB 提高到 7MB 后,报错深度虽增加约 3.5 倍,但最终仍会触发同一错误。因此,遇到“stack depth limit exceeded”时,应先判断是否存在递归循环,而非直接调整参数。只有对超长但有界的表达式,提高上限才可能放行。
触发器递归的修复:优先用 BEFORE 触发器
AFTER UPDATE 触发器若再次更新同一行,会反复触发自身。文章对比了三种修复:改用 BEFORE 触发器直接修改 NEW 并返回、用 WHEN 条件限制触发、在函数内用 pg_trigger_depth() 守卫。作者推荐 BEFORE 方案,因为它只写入一次,而守卫和 WHEN 仍会执行第二次 UPDATE。这提示读者在设计审计或时间戳触发器时,应优先考虑 BEFORE 方式避免递归。
行级安全策略自引用的隐蔽性与出路
策略条件直接内联子查询读取自身表时,PostgreSQL 能提前检测并报“infinite recursion detected in policy”。但若将子查询封装进函数,循环变得隐蔽,直到栈耗尽才报错。文章还指出,SECURITY DEFINER 函数在 FORCE ROW LEVEL SECURITY 下仍受策略约束,无法打破循环;需要函数属主不受策略约束(如关闭 FORCE、BYPASSRLS 或超级用户)才能避免。设计 RLS 时应警惕策略与函数间的间接自引用。
超长表达式的边界与安全形状
生成式 SQL 可能因超长表达式触发栈深度限制,例如 4093 个 1+1+…+1 项即失败。文章发现,扁平 OR/AND 链和 IN 列表不易触发该错误,30 万项 OR 链和 100 万值 IN 列表均正常运行;而嵌套括号或嵌套 OR 会触发解析器的“memory exhausted”,且提高 max_stack_depth 无效。若报表构建器生成超长过滤条件,应优先采用扁平结构和 IN 列表。
Q&A
PostgreSQL 报错 stack depth limit exceeded 是什么意思?
这是 PostgreSQL 后端进程在执行时检查自身进程栈使用量,当函数调用、触发器或表达式嵌套层级超过 max_stack_depth 设置时抛出的错误,SQLSTATE 为 54001。该设置用于防止失控递归导致后端进程崩溃。
哪些情况会导致 PostgreSQL 出现栈深度超限错误?
文章实测四种成因:1) AFTER UPDATE 触发器更新自身表;2) 函数无基准条件自调用;3) 行级安全策略通过函数读取自身表;4) 超长表达式(如 1+1+…+1 链)。前三种是无限递归,第四种是有界嵌套。
调高 max_stack_depth 能解决栈深度超限吗?
对于无限递归(触发器、递归函数、自引用策略),调高 max_stack_depth 只能延后报错,不能根治;对于有界的长表达式,提高上限可以让其通过。例如 4,093 项的加法链在 2MB 时失败,在 7MB 时成功。
如何修复因触发器更新自身表导致的栈深度超限?
推荐改用 BEFORE UPDATE 触发器,直接设置 new.updated_at := now() 并返回 new,避免第二次 UPDATE。也可在触发器上加 WHEN (old.n IS DISTINCT FROM new.n) 条件,或在函数开头用 pg_trigger_depth() > 1 时提前返回。
行级安全策略通过函数读取自身表导致递归,怎么解决?
需要打破循环:让函数所有者不受该策略约束,例如使用 BYPASSRLS 角色、超级用户,或关闭 FORCE ROW LEVEL SECURITY 并使用策略不适用的所有者。仅用 SECURITY DEFINER 不够,因为函数以表所有者身份运行,在 FORCE 下仍受策略约束。
生成很长的 SQL 表达式时,哪种写法容易触发栈深度超限或内存耗尽?
扁平的 OR/AND 链和 IN 列表不易触发,可支持数十万项;而连续的 + 或 || 链、嵌套的 OR/AND、多层嵌套括号容易触发。嵌套括号和嵌套 OR 会报 memory exhausted(SQLSTATE 42601),来自解析器栈,调 max_stack_depth 无效。
普通用户能否修改 max_stack_depth?有什么限制?
普通角色执行 SET max_stack_depth 会报权限拒绝。超级用户可通过 ALTER ROLE app SET max_stack_depth = '4MB' 设置,从下次登录生效;或使用 GRANT SET ON PARAMETER max_stack_depth TO app 授权。上限受操作系统栈限制,如容器 ulimit -s 为 8,192 kB 时,PostgreSQL 拒绝超过 7,680 kB 的值。