一个查询昨天还跑3毫秒,今天变成150毫秒。没人改代码,没人动配置,数据库自己"叛变"了。这种叫查询计划回退(Query Plan Regression)的故障,是PostgreSQL运维里最阴魂不散的问题——没有日志,没有告警,只有账单上的CPU费用在无声尖叫。
Philip McClarence在Gumroad做了8年数据库工程,处理过上百起这类事故。他说这类问题的诡异之处在于:你所有监控都显示"正常",唯独业务在报警。pg_stat_statements能告诉你查询变慢了,但它不会告诉你执行计划从索引扫描变成了哈希连接。
计划回退的5种隐形触发器
PostgreSQL的查询规划器像个过度自信的会计,每次ANALYZE更新统计信息后都会重新算账。McClarence列出的触发场景,每个都让DBA想摔键盘:
统计信息漂移:批量导入后ANALYZE自动运行,某张表的行数估计变了5%,规划器判定哈希连接更便宜。实际运行时间从3ms飙到150ms,正好50倍。
表体积过阈值:小表用位图扫描(Bitmap Scan)很香,但数据量涨到某个临界点,规划器突然改主意要用顺序扫描。这个"临界点"不会写进任何文档,你得用EXPLAIN猜。
索引膨胀:索引物理页变多,规划器的成本模型却还按创建时的页数估算。等它发现真相,已经选错执行策略了。
数据分布偏斜:某列原本是均匀分布,突然来个用量异常的大客户,直方图(Histogram)失效,选择性估计崩盘。
参数嗅探:预处理语句(Prepared Statement)第一次执行的参数决定了计划形状,后续参数不匹配时不会重新规划。
最扎心的是:这些变化都发生在数据库内部,应用层完全无感知。你的APM工具显示"数据库响应时间上升",但往下挖全是黑盒。
现有工具的盲区
McClarence试过用pg_stat_statements抓异常。这个扩展能追踪查询的聚合性能——总时间、调用次数、平均耗时——但对执行计划本身一无所知。你可以看到某条查询的平均耗时从3ms变成6ms,标准差(stddev_exec_time)突然变大,这暗示计划不稳定。但具体是哪一步变了?不知道。
他写过一个查询来筛查嫌疑对象:
SELECT substring(query, 1, 80) AS query_preview,
mean_exec_time,
stddev_exec_time
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC;
高stddev_exec_time relative to mean_exec_time说明同一条查询有时5ms有时500ms,规划器可能在根据参数选择不同计划。但这只是间接证据,你还得手动EXPLAIN ANALYZE去抓现行。
PostgreSQL 12+的pg_stat_statements增加了plan列,但默认关闭,且只存哈希值不存完整计划。想对比计划变化?自己建表存历史。
McClarence的防御工事
他在Gumroad搭了一套"计划档案"系统。核心思路不复杂:定期抓取活跃查询的执行计划,存进历史表,diff算法自动标红变化。
具体实现用了几个组件:
计划采集器:用auto_explain.log_min_duration=0强制记录所有计划,但只保留采样(比如1%),避免日志爆炸。或者用PL/pgSQL写定时任务,对高频查询主动EXPLAIN。
标准化存储:执行计划里常有具体参数值(如WHERE id=12345),直接存会生成无数"不同"的计划。McClarence的做法是正则替换参数为占位符,把结构相同的计划归为一类。
变化检测:对比同一查询模板的历史计划,节点类型变化(Index Scan→Seq Scan)、连接算法变化(Nested Loop→Hash Join)、代价估计变化超过阈值,都触发告警。
回滚机制:发现计划劣化后,可以用pg_hint_plan强制指定执行策略,或者对特定查询启用plan_cache_mode='force_custom_plan'绕过预处理语句的计划复用。
这套系统上线第一年,Gumroad提前拦截了17起潜在的生产事故。其中一起是某张表的统计信息在凌晨ANALYZE后突变,导致核心订单查询从索引扫描变成顺序扫描。告警在业务受影响前30分钟触发,DBA用pg_hint_plan打了补丁,等白天再根治。
社区还在争论的解法
PostgreSQL社区对计划稳定性有过激烈讨论。Oracle有SQL Plan Management,SQL Server有Query Store,MySQL 8.0也有Optimizer Trace。PostgreSQL的立场更"Unix哲学":提供钩子,让用户自己搭。
pg_plan_advisor和pg_query_plan是社区的两个实验性扩展,前者试图自动识别并锁定优质计划,后者提供更细粒度的计划监控。但都还没到生产就绪。
McClarence的观点很直接:在核心引擎改进之前,每个跑PostgreSQL的团队都得自己造轮子。他的开源项目query_plan_monitor(GitHub 400+ star)把Gumroad的内部工具泛化了,支持自动计划归档、Slack告警、与pg_stat_statements联动。
但工具只是半场。他更强调流程:谁对计划变化负责?变化发生后多久必须响应?强制指定执行策略的临时补丁,生命周期怎么管理?这些问题没有标准答案,但团队得先意识到"计划回退"是个值得专门立项的风险域。
去年Gumroad的一次复盘会上,有个后端工程师问了个扎心的问题:如果PostgreSQL的规划器已经"智能"到能根据统计信息动态调整,为什么我们还需要人工监控计划变化?McClarence的回答被写进了团队手册——"智能"不等于"正确",成本模型算的是估计值,不是真理。
你的团队上次检查执行计划变化是什么时候?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.