查询优化器在成本上押注
查询规划器根据预计工作量选择执行路径,并不会先把所有候选方案运行一遍、计时后再保留冠军。PostgreSQL 用统计信息和成本模型比较扫描、连接、排序等操作,因此选定的计划始终是建立在当前假设上的预测。诊断预测为何偏离实际,需要找出失效的具体假设,而不是把顺序扫描或嵌套循环当成天生有问题的节点。计划好不好,要放在完整查询的输入和输出要求中判断。 成本描述的是一个模型 PostgreSQL 的成本综合页面访问、元组处理、运算符计算等估计工作量,单位并不是毫秒。只需要拿到少量记录时,启动成本可能特别重要;必须产生完整结果时,总成本的意义更大。一个扫描如果已经提供了后面需要的顺序,就可能省掉排序,因此单独看来最便宜的扫描,不一定是整条查询最合适的输入。只比较一个节点会漏掉它给后续处理带来的影响。 规划搜索也有边界。数据库会考虑不同路径和连接顺序,而不是实际执行全部候选。连接关系很多时,PostgreSQL 可以启用遗传查询优化器来控制规划开销。可用索引、合法变换、配置与搜索选择共同限制了候选集合,所以不能把最后的选择描述为“对所有想象得到的实现都保证数学最优”。这种边界并不表示规划器不可信,只是说明它需要接受真实测量的检验。 坏掉的样子 统计刚刷新也可能估错。普通统计分别描述每一列,在组合两个过滤条件时可能假设它们相互独立,而实际两列总是一起变化。中间结果被低估后,反复查找看起来就比实际便宜,后续连接顺序也可能受到影响。如果模型本身没有表达这两列的关系,重复运行相同的分析不会凭空补上这部分知识。统计陈旧与统计种类不足,不能混成一种诊断。 扩展统计也只解决特定问题。函数依赖可以帮助估计与常量比较且彼此兼容的等值条件,但它不保存具体值之间的完整对应关系,也不是范围条件或任意跨列比较的一般证明。即使已经收集到很强的依赖,一组实际上互不兼容的常量仍可能被严重高估。因此验证中既需要兼容的正向情况,也需要一个不成立的组合来展示限制。 快速验证 在 PostgreSQL 18 的 psql 中执行下面的临时表实验。两列始终保存相同的值,一共有一百种不同值,每种重复一百次。所有修改都放在事务中,最后回滚会一并移除表和相应统计对象。 \set ON_ERROR_STOP on BEGIN; CREATE TEMP TABLE dimensions (a integer,b integer); INSERT INTO dimensions SELECT g%100,g%100 FROM generate_series(1,10000) AS g; ANALYZE dimensions; EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT * FROM dimensions WHERE a=7 AND b=7; CREATE STATISTICS pg_temp.dimensions_dependency (dependencies) ON a,b FROM dimensions; ANALYZE dimensions; EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT * FROM dimensions WHERE a=7 AND b=7; EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT * FROM dimensions WHERE a=7 AND b=8; DO $$ BEGIN IF (SELECT count(*) FROM dimensions WHERE a=7 AND b=7) <> 100 OR (SELECT count(*) FROM dimensions WHERE a=7 AND b=8) <> 0 THEN RAISE EXCEPTION 'wrong correlated population'; END IF; END $$; ROLLBACK; 在建立扩展统计之前,PostgreSQL 18.6 对 a=7 AND b=7 估计一行,实际返回一百行。两个条件单独都选中百分之一的数据;把选择率相乘,就得到一万行中的万分之一,也就是一行。这套算术在条件独立时有意义,而夹具故意让两列完全相关,所以假设与数据并不相符。 ...