当前位置: 首页 > news >正文

DeepSeek总结的plan_cache_mode 的隐藏行为

原文地址:https://richyen.com/postgres/2026/03/30/plan_cache_mode.html

plan_cache_mode 的隐藏行为

2026年3月30日


引言

大多数 PostgreSQL 用户使用预备语句来提升性能并防止 SQL 注入。很少有人知道,查询规划器会在恰好执行五次之后,悄无声息地更改预备语句的执行计划。

这种行为常常让工程师们感到惊讶,因为一个查询计划可能会突然转变——有时甚至是戏剧性的变化,尽管查询本身并未改变。原因在于规划器处理自定义计划与通用计划的方式,而这由参数plan_cache_mode控制。


自定义计划 vs 通用计划

当预备语句带有参数执行时,规划器有两种选择:

  • 自定义计划:使用实际的参数值生成。它可能针对该次特定执行是最优的,但每次都需要规划开销。
  • 通用计划:在不知道具体参数值的情况下规划一次。它被重用于所有后续执行,以节省规划开销。

默认情况下,plan_cache_mode设置为auto。在此模式下,规划器在前五次执行时使用自定义计划。在第六次执行时,它会比较这些自定义计划的平均成本与通用计划的估计成本。如果通用计划被认为“更便宜”或相等,规划器将在该会话中永久切换到通用计划。


用 pgbench 演示

一如既往,pgbench 是进行简单演示时的首选模式。撰写本文时,我使用的是最新版本的 Postgres 18。出于本文的目的,添加一个具有高度偏斜值的列更容易触发切换。因此,我们添加一个具有极端偏斜的标记列:'N'占 0.1% 的行,'Y'占其余 99.9% 的行:

### 在 bash 中:pgbench-i-s10-Upostgres postgres### 在 psql 中:ALTER TABLE pgbench_accounts ADD COLUMN flag CHAR(1)NOT NULL DEFAULT'Y';UPDATE pgbench_accounts SET flag='N'WHERE aid<=1000;CREATE INDEX idx_accounts_flag ON pgbench_accounts(flag);ANALYZE pgbench_accounts;SELECT flag, count(*)FROM pgbench_accounts GROUP BY flag;
flag | count ------+-------- N | 1000 Y | 999000

在触发自动切换之前,让我们直接强制使用每种模式,看看规划器为同一个语句生成什么计划。

-- 自定义计划:规划器看到字面值 'Y',在列统计信息中查找-- (MCV 频率 ≈ 0.999),并为 999,033 行选择顺序扫描。SETplan_cache_mode=force_custom_plan;PREPAREflag_lookup(char)ASSELECTaid,abalanceFROMpgbench_accountsWHEREflag=$1;EXPLAINEXECUTEflag_lookup('Y');
QUERY PLAN ------------------------------------------------------------------------- Seq Scan on pgbench_accounts (cost=0.00..28910.00 rows=999033 width=8) Filter: (flag = 'Y'::bpchar) <-- 字面值 'Y' 表示自定义计划
DEALLOCATEflag_lookup;-- 通用计划:规划器没有值可以查找。由于 ndistinct = 2-- (只有 'Y' 和 'N' 存在),它估计选择性为 1/ndistinct = 50%,-- 即 500,000 行。在此估计下,更便宜的路径是索引扫描。SETplan_cache_mode=force_generic_plan;PREPAREflag_lookup(char)ASSELECTaid,abalanceFROMpgbench_accountsWHEREflag=$1;EXPLAINEXECUTEflag_lookup('Y');
QUERY PLAN -------------------------------------------------------------------------------------------- Index Scan using idx_accounts_flag on pgbench_accounts (cost=0.42..19322.07 rows=500000) Index Cond: (flag = $1) <-- 注意占位符 $1,而不是字面值 'Y'/'N'

成本数字揭示了选择索引扫描而非顺序扫描的原因:19,322 < 28,910。


自动切换的实际效果

plan_cache_mode重置回auto后,我们使用常用值'Y'执行该语句五次。每次运行都会生成一个自定义的顺序扫描计划,成本约为 28,910。五次执行之后,规划器比较:

  • 平均自定义计划成本:~28,910
  • 通用计划成本:~19,322

由于 19,322 ≤ 28,910,从第 6 次执行开始选择通用计划。

DEALLOCATEflag_lookup;SETplan_cache_mode=auto;PREPAREflag_lookup(char)ASSELECTaid,abalanceFROMpgbench_accountsWHEREflag=$1;-- 执行 1-5 次:自定义计划,每次都解析字面值 'Y'EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');

每次显示:

QUERY PLAN -------------------------------- Seq Scan on pgbench_accounts Filter: (flag = 'Y'::bpchar)

在第六次执行时:

EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');
QUERY PLAN -------------------------------------------------------- Index Scan using idx_accounts_flag on pgbench_accounts Index Cond: (flag = $1)

策略在第六次调用时从顺序扫描转变为索引扫描——尽管查询和数据完全相同。$1占位符确认了现在使用的是通用计划。


它会切换回来吗?

从第 6 次执行开始,每个查询——无论参数值是什么——都使用那个通用的索引扫描。对于'N'(1000 行),索引扫描恰好是高效的。对于'Y'(999,000 行),通过随机索引查找来扫描近 100 万行的表,比顺序扫描要差得多。

-- 第 7+ 次执行:无论值如何,都使用通用计划EXPLAIN(COSTSOFF)EXECUTEflag_lookup('Y');-- 999,000 行通过索引扫描(糟糕!)EXPLAIN(COSTSOFF)EXECUTEflag_lookup('N');-- 1,000 行通过索引扫描(偶然可以)

两者都显示:

QUERY PLAN -------------------------------------------------------- Index Scan using idx_accounts_flag on pgbench_accounts Index Cond: (flag = $1)

通用计划会一直保持,直到执行DEALLOCATE flag_lookup或会话结束。对于频繁执行的预备语句来说,这无疑是需要注意的一点,因为它对我合作过的一些客户造成了显著的影响。


幕后:C 逻辑

只是为了强调数字 5 并不是由任何花哨的逻辑决定的,我们可以在源代码中找到它。在src/backend/utils/cache/plancache.c中(大约第 1200 行),函数choose_custom_plan明确说明了这一点:

staticboolchoose_custom_plan(CachedPlanSource*plansource){/* ... 检查 force_custom / force_generic 的设置 ... *//* 如果我们还没有执行 5 次自定义计划,继续执行 */if(plansource->num_custom_plans<5)returntrue;/* 否则,将 generic_cost 与平均 custom_cost 进行比较。 * 如果通用计划更便宜(或相等),我们就切换! */if(plansource->generic_cost<=plansource->total_custom_cost/plansource->num_custom_plans)returnfalse;returntrue;}

最后的思考

查询规划器的自动计划缓存通常是英雄,节省了 CPU 周期。但是,当你拥有高度偏斜的数据或易变的临时对象时,这种“第六次运行切换”可能会对客户端/应用程序性能产生负面影响。

如果你在预备语句中看到无法解释的性能回退,你可能想检查它是否被调用了超过 5 次,或者尝试SET plan_cache_mode = force_custom_plan作为排查步骤。这会强制每次执行都生成一个全新的自定义计划,确保规划器总是能看到实际的参数值,并能选择正确的策略。

祝你好运!

http://www.cnnetsun.cn/news/1608554.html

相关文章:

  • 在Windows 10上运行Android应用:Windows Subsystem for Android完整指南
  • 别光写控制台!给C++飞机订票系统加个简易图形界面(基于EasyX库)
  • Ostrakon-VL-8B环境部署教程:8-bit Retro UI免配置启动
  • TDOA三维定位实现——基于加权最小二乘法的MATLAB例程
  • 【Altium Designer2025】EDA软件新特性解析:从PCB设计到FPGA开发的全面升级
  • Redis RDB文件全解析指南:从数据提取到存储优化
  • 用C语言手把手实现Clock页面置换算法(附完整代码和避坑指南)
  • 3分钟轻松安装:BetterNCM Installer网易云插件管理器终极指南
  • 程序员副业图谱:从入门到变现,全维度实战指南(2026最新版)
  • 英飞凌TC3xx芯片功能安全开发避坑指南:手把手教你集成safeTpackage(含多核启动时序)
  • Linux桌面自动化引擎:xdotool从入门到专家的全栈实践指南
  • 攻克开源软件中文路径支持难题:5个步骤实现Calibre完美兼容
  • Vision Transformer——打破CNN垄断的视觉革命先锋
  • OrigamiSimulator:让数字折纸创作触手可及的WebGL工具指南
  • 扩散模型之(十八)ControlNet 原理与指南
  • Pixel Aurora Engine基础教程:Streamlit前端交互逻辑与后端diffusers集成
  • TouchGal完整指南:一站式Galgame文化社区的终极解决方案
  • 3步实战:Redoc CLI终极指南,让API文档自动化成为现实
  • ACM LaTeX模板中CCSXML填写的3个常见错误及解决方法(附最新官方指南)
  • HunyuanVideo-Foley 赋能短视频创作:AI自动生成背景音效与BGM
  • 告别玄学调参!手把手教你用TL431+PC817搞定反激电源反馈环路(附动态补偿设计)
  • YY/T0681.15与ASTM D4169 DC13包装运输测试标准俩者区别在于
  • Orange在法国铁路连接质量测试中表现领先
  • 别再手动整理会议纪要了!用FunASR搭个带权限管理的内部转写工具(支持热词定制)
  • 告别Sobel和Canny!用Python实现光照不敏感的相位一致性特征提取(附完整代码)
  • 256K上下文颠覆智能编程:Qwen3-Coder重构全栈开发效率范式
  • 别再到处找教程了!Visual Studio 2022 + GLFW + GLAD 配置 OpenGL 开发环境(Win10 保姆级指南)
  • LFM2.5-1.2B-Thinking-GGUF实战:低资源环境下的高效文本生成体验
  • 华为eNSP实战:从零搭建一个能跑通OSPF、FTP、HTTP的小型企业网(附Wireshark抓包分析)
  • 12306Bypass分流抢票软件 抢票神器