ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PostgreSQL 执行计划实战(第 9 篇):SQL 和索引没变,计划为什么突然慢一百倍

PostgreSQL 执行计划实战(第 9 篇):SQL 和索引没变,计划为什么突然慢一百倍 同一条订单查询昨天只循环内表几百次今天却循环几十万次。SQL、索引和参数没改变化只是活动订单从均匀分布变成“省份与城市强相关”。优化器先估算每个节点会输出多少行再比较路径成本。第一处基数误差会沿 Nested Loop、排序和 Hash 容量继续放大强制换 Join 只能遮住症状。用一百组相关值制造稳定的百倍误差在隔离测试库执行DROPTABLEIFEXISTScustomer_geo;CREATETABLEcustomer_geo(idbigintPRIMARYKEY,provincetextNOTNULL,citytextNOTNULL,payloadtextNOTNULL);INSERTINTOcustomer_geoSELECTg,P||lpad((g%100)::text,2,0),C||lpad((g%100)::text,2,0),repeat(x,100)FROMgenerate_series(1,1000000)ASg;ANALYZEcustomer_geo;每个省份约占 1%每个城市也约占 1%但P42永远与C42同时出现。查询EXPLAIN(ANALYZE,BUFFERS,SETTINGS)SELECTidFROMcustomer_geoWHEREprovinceP42ANDcityC42;在只有单列统计时优化器可能近似按独立条件相乘1% × 1% 0.01%估算约 100 行实际约 10000 行形成约百倍低估。具体数字受 ANALYZE 随机采样影响判断重点是rows方向和数量级。这一步证明单列统计无法表达列间相关性它不能证明实际慢 SQL 一定由相关性造成也不能证明某种 Join 必然被选择。扩展统计补上单表多列关系CREATESTATISTICScustomer_geo_corr(dependencies,mcv,ndistinct)ONprovince,cityFROMcustomer_geo;ANALYZEcustomer_geo;EXPLAIN(ANALYZE,BUFFERS,SETTINGS)SELECTidFROMcustomer_geoWHEREprovinceP42ANDcityC42;dependencies描述列间函数依赖适合相关等值条件mcv记录多列常见值组合能表达“组合特别常见或根本不存在”ndistinct改善多列 distinct 估算常影响 GROUP BY。创建统计对象本身不会立即产生数据必须再次ANALYZE。扩展统计也不创建索引不改变数据访问能力它改变的是估算依据。检查统计对象SELECTstatistics_name,attnames,kindsFROMpg_stats_extWHEREstatistics_namecustomer_geo_corr;SELECTstatistics_name,most_common_vals,most_common_freqsFROMpg_stats_extWHEREstatistics_namecustomer_geo_corr;扩展统计目前主要用于单表条件和聚合估算并不普遍解决表与表之间的 join selectivity。若误差首次出现在 join 节点不能机械创建两张表各自的扩展统计后宣称修复。函数依赖统计还有一个容易误用的边界它主要帮助等值条件和IN常量条件不是任意范围谓词的通用相关性模型并且它是统计推断不像唯一约束那样强制数据正确。若province只是“通常决定 city”新增异常组合后依赖强度会变化下一次 ANALYZE 才会反映新分布。为什么昨天快、今天慢SQL 文本不变优化器输入仍可能变化变化它影响什么应查证据数据分布变化选择率、热点和相关性pg_stats、业务分布快照统计陈旧planner 仍看旧世界last_analyze、变更量采样不足MCV/直方图漏掉热点statistics target、重复 ANALYZE 对照表/索引尺寸变化seq/random I/O 成本relation size、计划成本参数倾斜custom plan 的选择率不同同会话同语句不同参数generic plan计划看不到本次值pg_prepared_statementswork_mem/并行变化Hash、Sort 和 Gather 成本SETTINGS、temp、workers缓存与存储变化实际 I/O 时间BUFFERS、I/O timing、系统指标因此“计划突然变了”与“计划没变但执行突然慢”也要分开。前者先比较 plan shape 和估算输入后者还要查缓存、锁、I/O、数据量与循环次数。读 EXPLAIN 的顺序从叶子找第一处分叉第一处 estimated rows 与 actual rows 明显分叉 ↓ 该节点的过滤条件、参数与统计对象 ↓ 误差是否被上层 loops 乘法放大 ↓ Buffers、temp、I/O、Memory、workers ↓ 最后才讨论 Join、索引或参数例如Index Scan 估 10 行实际 10000 行 Nested Loop loops 估 10实际 10000 内表每次只读 3 个 buffer 最终仍变成约 30000 次 buffer 访问Nested Loop 不一定选错在“外表只有 10 行”这个错误前提下它可能是成本最低的合理决策。根因是提供给 Join 搜索的行数世界错了。EXPLAIN ANALYZE不是无风险只读EXPLAIN(ANALYZE,BUFFERS,WAL,SETTINGS)SELECT...;ANALYZE会真实执行语句。对 SELECT 也可能消耗大量 CPU/I/O、持有锁和污染缓存对 UPDATE/DELETE 则会真的修改数据除非放入可回滚事务且没有不可回滚副作用。生产优先取得已有慢计划、在副本或测试库复现并设置超时与观测范围。只看估算可用EXPLAIN(COSTS,VERBOSE,SETTINGS)SELECT...;它安全但没有 actual rows无法验证基数假设。单列 statistics target 什么时候有用热点值没进入 MCV、直方图过粗时可以只提高关键列ALTERTABLEcustomer_geoALTERCOLUMNprovinceSETSTATISTICS1000;ANALYZEcustomer_geo;更高 target 增加 ANALYZE 时间、统计存储和规划开销。它能提高单列分布精度却仍不能自动表达province与city的关系相关性问题应使用扩展统计。两种手段面对不同信息缺口。为什么关闭 Nested Loop 只能做对照BEGIN;SETLOCALenable_nestloopoff;EXPLAIN(ANALYZE,BUFFERS,SETTINGS)SELECT...;ROLLBACK;若替代计划更快只证明当前成本模型没有选中它不能证明 Nested Loop 是根因。全局关闭会伤害真正的小外表、索引点查和低延迟 OLTP。同理把random_page_cost改到极端值可能改变计划却是在重写整个实例的成本世界。除非有存储基准和完整工作负载验证不应把它当单 SQL 修复。生产修复顺序保存慢时计划、参数、设置、统计更新时间与业务影响。找第一处 rows 分叉确认它发生在 base relation、表达式、聚合还是 join。统计陈旧则先 ANALYZE热点采样不足才调 target多列相关才建扩展统计。用同一数据快照比较修复前后估算、Buffers、时间和计划稳定性。小范围部署统计或 SQL 变更观察同 workload 的 p95/p99 与规划 CPU。修复业务分布变化后的统计维护和告警而不是只固定一个计划。统计更新可能使其他 SQL 重新规划。灰度要覆盖共享这些列和表的查询停止条件包括其他关键 SQL 尾延迟恶化、规划 CPU 异常或新计划抖动。删除统计对象可以回退其影响但不能瞬间恢复旧计划缓存和缓存热度。证据边界证据能证明不能证明rows 首次分叉找到估算误差起点它一定是全部耗时来源扩展统计后估算接近多列信息改善该条件估算join 估算和所有参数都改善禁用 Nested Loop 更快存在更快替代路径应全局禁用 Nested LoopANALYZE 后计划恢复统计陈旧或采样是重要因素数据未来不会再次漂移单次执行快当前缓存与参数下表现好高并发和全参数域稳定面试表达主线PostgreSQL 计划由基数估算驱动。排障先从叶子找第一处 estimated/actual rows 分叉再看误差如何被 loops 放大单列热点用 statistics target多列相关用扩展统计参数倾斜检查 custom/generic plan。禁用 Join 只能作为替代路径对照。实验清理DROPSTATISTICSIFEXISTScustomer_geo_corr;DROPTABLEIFEXISTScustomer_geo;官方资料PostgreSQL 18Using EXPLAINPostgreSQL 18Planner StatisticsPostgreSQL 18CREATE STATISTICSPostgreSQL 18pg_stat_statementsPostgreSQL 18.6 源码标签 REL_18_6
返回列表