Dinesh Kumar:PostgreSQL 中 SQL 与 PL/pgSQL 的对比:执行计划缓存

Dinesh Kumar:PostgreSQL 中 SQL 与 PL/pgSQL 的对比:执行计划缓存

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

内容提要

PostgreSQL 16 中,无法内联的 SQL 函数(如调用 regexp_matches)每次调用都会重新生成执行计划,而 PL/pgSQL 函数会缓存计划。某每小时任务循环调用该函数 1000 万次,SQL 版因反复规划耗时约 47 秒;改用 PL/pgSQL 后仅规划 6 次,降至约 24 秒;改用 regexp_match 使函数可内联后进一步降至约 12 秒。PostgreSQL 18 已修复此问题。

🔎

延伸解读

调用方式决定计划缓存是否生效

文章指出,SQL 函数在 PostgreSQL 16 中是否被反复规划,取决于循环里的调用写法。用 SELECT ... INTO、PERFORM 或 EXECUTE 调用时,每一行都会启动新语句,函数计划随之重建;而用普通赋值 v_code := get_error_code(...) 调用,计划能保留到整个循环结束。因此,同样的函数换一种调用方式,规划次数可能从千万次降到一次,排查性能问题时不能只看函数语言。

内联是更优解,但受函数体限制

文章强调,若 SQL 函数能被内联,函数调用会直接展开到外层查询中,根本不存在重复规划的问题。本例最初无法内联,是因为函数体调用了返回多行的 regexp_matches;换成返回单值的 regexp_match 后,EXPLAIN 显示函数已被展开,CPU 时间进一步降到约 12 秒。这说明在改写函数前,应先确认它能否被内联,而不是默认改用 PL/pgSQL。

用 pg_stat_statements 验证规划次数

文章没有停留在理论解释,而是给出可复现的验证方法:开启 pg_stat_statements.track = all 和 track_planning = on,重置计数后运行任务,再查询 calls 与 plans 两列。实测 SQL 版本执行 1000 万次、规划 1000 万次,PL/pgSQL 版本同样执行 1000 万次却只规划 6 次。对生产环境而言,这种对比能直接定位 CPU 消耗是否来自重复规划,而非查询本身。

版本差异决定修复策略

文章明确,该问题已在 PostgreSQL 18 中修复,SQL 函数也开始使用计划缓存;但许多生产系统仍运行 16 或 17,因此仍需应对。对这类系统,可选的缓解手段包括改用 PL/pgSQL、调整调用写法,或改写函数使其可内联。文章给出的数据均来自 PostgreSQL 16.15 默认配置下的负载测试,迁移到其他版本或配置时,实际收益需要重新测量。

❓

Q&A

为什么在 PostgreSQL 16 中,把 SQL 函数改成 PL/pgSQL 后 CPU 时间减半?

因为 SQL 函数无法内联时,每次调用都会重新解析和规划查询,而 PL/pgSQL 函数会缓存执行计划。在每小时调用 1000 万次的场景中,SQL 版每次调用都生成新计划,CPU 时间约 47 秒;PL/pgSQL 版只规划 6 次,CPU 时间降至约 24 秒。

PostgreSQL 16 中 SQL 函数为什么没有利用计划缓存?

在 PostgreSQL 18 之前,SQL 函数没有计划缓存,其计划只在调用它的语句执行期间保留。如果函数被逐行调用(如 SELECT ... INTO),每行都是一条新语句,因此每次都会重新规划。

PL/pgSQL 函数是如何缓存执行计划的?

PL/pgSQL 函数在会话中第一次执行查询时,会将计划保存到计划缓存。前五次调用使用实际参数值规划,第六次调用时生成一个通用计划,如果该计划不更昂贵,后续调用都使用它。因此无论循环多少行,总共只规划六次。

除了改用 PL/pgSQL,还有什么方法可以避免 SQL 函数重复规划?

可以改写函数使其可内联。例如将 regexp_matches 替换为 regexp_match(返回单行而非集合),这样 PostgreSQL 会将函数体内联到调用查询中,消除函数计划。测试中该版本 CPU 时间降至约 12 秒,优于 PL/pgSQL 版本。

如何验证 SQL 函数是否被重复规划?

使用 pg_stat_statements 扩展,设置 pg_stat_statements.track = all 和 track_planning = on,然后重置计数器并运行作业,查询 pg_stat_statements 中的 calls 和 plans 字段。例如 SQL 版显示执行 1000 万次、规划 1000 万次,而 PL/pgSQL 版仅规划 6 次。

PostgreSQL 18 对 SQL 函数的计划缓存做了哪些改进?

PostgreSQL 18 修复了 SQL 函数计划缓存的问题,使其也能使用计划缓存,从而避免每次调用都重新规划。该改进在 PostgreSQL 18 的发布说明中提及。

🏷️

标签

➡️

继续阅读