统计信息对执行计划的影响
前面几节讲了统计信息「是什么」与「怎么收集」。本节回答最后一个问题:统计信息到底是怎么变成执行计划的。
PostgreSQL 的规划过程可以概括成一条链路:统计信息 → 选择性估算 → 行数(基数)估算 → 成本估算 → 计划选择。这条链路上任何一环失真,最终都可能选出一个「估算很便宜、实际很昂贵」的计划。理解这条链路,是把「调统计」和「调计划」两件事接起来的关键。
本节讲清估算与成本的换算关系、估算偏差如何具体地扭曲计划选择,以及在统计信息之外还有哪些调节手段(成本常量、规划器开关)以及它们的正确用法。
前置知识
阅读本节前,建议先了解:扩展统计信息(CREATE STATISTICS)
基础概念
规划的五步链路
① 统计信息 pg_statistic / pg_statistic_ext_data / pg_class
│ (列分布、n_distinct、MCV、直方图、reltuples、relpages)
▼
② 选择性估算 每个 WHERE 条件命中多少比例?
│ selectivity ∈ [0, 1]
▼
③ 基数估算 行数 = 表行数 × 选择性
│ rows(EXPLAIN 里那个数字)
▼
④ 成本估算 I/O 成本 + CPU 成本,按代价常量换算
│ 用任意单位,只有相对值有意义
▼
⑤ 计划选择 枚举候选路径,选总成本最低的关键点:规划器是"成本驱动"而非"时间驱动"的。它不知道真实耗时,只能用「页读取次数 × seq_page_cost」「处理行数 × cpu_tuple_cost」这类公式算出抽象的代价数字,再比大小。
选择性(selectivity)
选择性是「命中行数占表总行数的比例」,取值 0~1。
| 条件形态 | 估算依据 | 典型失真 |
|---|---|---|
col = 常量 | MCV 命中则用 MCV 频率;否则摊平 | 值高度偏斜且不在 MCV 中时 |
col < 常量 / BETWEEN | histogram_bounds 分桶 | 分布不规则、统计目标太低 |
col IS NULL | null_frac | 基本不失真 |
a = 1 AND b = 2 | 各条件选择性相乘 | 列相关时严重低估 |
a = 1 OR b = 2 | 容斥公式 | 列相关时容易高估 |
| 无统计的表达式 | 固定默认值(等值 0.5%) | 缺失表达式统计时 |
IN (...) | 逐值累加 | 列表很长时可近似 |
从选择性到行数
rows = 表的总行数 × 选择性
表的总行数取自 pg_class.reltuples(可能略有滞后),
规划器会按当前物理表大小做缩放修正。EXPLAIN 输出里 rows= 后面那个数字,就是这一步算出来的估算基数。
从行数到成本
成本由两大类构成:
I/O 成本:读取数据页
- 顺序读一页 → seq_page_cost 默认 1.0
- 随机读一页 → random_page_cost 默认 4.0
CPU 成本:处理数据
- 处理一行 → cpu_tuple_cost 默认 0.01
- 处理一个索引项 → cpu_index_tuple_cost 默认 0.005
- 执行一次操作/函数 → cpu_operator_cost 默认 0.0025
并行相关:
- 启动并行 worker → parallel_setup_cost 默认 1000
- 跨进程传一行 → parallel_tuple_cost 默认 0.1一个重要性质:这些常量只有相对值有意义。把全部常量同比例放大或缩小,规划器的选择完全不变。
为什么「低估行数」特别危险
低估行数 → 规划器以为结果集很小
→ 认为「用索引逐条取」很划算
→ 选择 Index Scan / Nested Loop
→ 实际行数很大,随机 I/O 爆炸,慢几个数量级
高估行数 → 规划器以为结果集很大
→ 选择 Seq Scan / Hash Join,或预先分配大内存
→ 若实际很小,浪费一次全表扫描
→ 更糟:Hash Join 的内存估算过大,造成 work_mem 溢出落盘方向上,低估比高估危险得多:高估最多是"多干活",低估会让规划器选一个本质上不适合当前数据量的算法。
成本常量与统计信息的职责分工
统计信息 → 决定 rows(估算的行数)
成本常量 → 决定「每行、每页值多少成本」
两者相乘 → 总成本
调错了对象,问题不会解决:
行数估错 → 调成本常量治标不治本,必须修统计
硬件特性变了 → 调统计没用,必须调成本常量比如数据库基本全在内存里,随机读与顺序读几乎无差别,这种情况该调 random_page_cost;而如果只是列相关性导致行数估错,调 random_page_cost 最多是"碰巧对了这次",换个查询又会错。
术语表
| 术语 | 含义 |
|---|---|
| 选择性(selectivity) | 条件命中的行数比例 |
| 基数(cardinality) | 结果集的行数估算值 |
| 成本(cost) | 规划器用任意单位计算的抽象代价 |
| 代价常量(cost constant) | 把页数、行数换算成成本的系数 |
| 规划器开关(planner method config) | enable_* 系列,禁用某类计划 |
| 误估(misestimate) | 估算基数与实际行数偏离 |
语法与用法
观察估算与实际
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 100; Index Scan using orders_customer_id_idx on orders
(cost=0.43..16.85 rows=12 width=120)
(actual time=0.031..0.082 rows=15 loops=1)对比要点:
| 位置 | 含义 |
|---|---|
节点行第一对括号 cost= | 估算的启动成本..总成本 |
节点行第一对括号 rows= | 估算输出行数 |
节点行第一对括号 width= | 估算平均行宽(字节) |
第二对括号 actual time= | 实际首行时间..总时间 |
第二对括号 rows= | 实际输出行数 |
loops= | 该节点被执行的次数(Nested Loop 内层会很大) |
估算与实际差一个数量级以内通常可接受;差 10 倍以上就值得查统计。
只看计划的形状,不跑查询
EXPLAIN SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id;不跑 EXPLAIN ANALYZE 时只做规划、不执行,适合观察估算(因为若用 ANALYZE,估算可能被平均掉或被实际值掩盖)。
加上成本与内存细节
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT * FROM orders WHERE status = 'paid';| 选项 | 作用 |
|---|---|
BUFFERS | 显示共享缓冲命中/读取次数,判断 I/O 假设是否符合实际 |
VERBOSE | 显示更多节点细节(输出列、schema 限定名) |
SETTINGS | 显示影响本计划选择的非默认参数,排查成本常量改动很关键 |
WAL | 显示该语句产生的 WAL 记录数 |
TIMING OFF | 关闭逐节点计时,降低计时开销对结果的影响 |
调整成本常量
-- 会话级临时调整(推荐调试用)
SET random_page_cost = 1.1;
-- 恢复默认
RESET random_page_cost;
-- 查看当前值
SHOW random_page_cost;# postgresql.conf(全局持久化)
seq_page_cost = 1.0
random_page_cost = 1.1
cpu_tuple_cost = 0.01
cpu_index_tuple_cost = 0.005
cpu_operator_cost = 0.0025
effective_cache_size = 16GB临时禁用某类计划
-- 会话级:暂时禁用顺序扫描,验证"走索引到底会不会更快"
SET enable_seqscan = off;
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'paid';
RESET enable_seqscan;注意:off 不是"禁止",而是"给它加一个极大的惩罚成本"。如果确实没有别的路径可用,规划器仍会选它。
相关运行参数
| 参数 | 默认值 | 作用 |
|---|---|---|
seq_page_cost | 1.0 | 顺序读一页的成本 |
random_page_cost | 4.0 | 随机读一页的成本 |
cpu_tuple_cost | 0.01 | 处理一行的成本 |
cpu_index_tuple_cost | 0.005 | 处理一个索引项的成本 |
cpu_operator_cost | 0.0025 | 执行一次操作/函数的成本 |
parallel_setup_cost | 1000 | 启动并行 worker 的成本 |
parallel_tuple_cost | 0.1 | 跨进程传递一行的成本 |
min_parallel_table_scan_size | 8MB | 触发并行表扫描的最小数据量 |
min_parallel_index_scan_size | 512kB | 触发并行索引扫描的最小数据量 |
effective_cache_size | 4GB | 规划器假设的可用缓存总量(仅用于估算) |
default_statistics_target | 100 | 默认统计目标(范围 1~10000) |
规划器开关一览
| 开关 | 默认 | 禁用的计划类型 |
|---|---|---|
enable_seqscan | on | 顺序扫描 |
enable_indexscan | on | 索引扫描与仅索引扫描 |
enable_indexonlyscan | on | 仅索引扫描(需 indexscan 也开着) |
enable_bitmapscan | on | 位图扫描 |
enable_tidscan | on | TID 扫描 |
enable_sort | on | 显式排序 |
enable_incremental_sort | on | 增量排序 |
enable_hashagg | on | 哈希聚合 |
enable_hashjoin | on | 哈希连接 |
enable_mergejoin | on | 归并连接 |
enable_nestloop | on | 嵌套循环连接 |
enable_material | on | 物化节点 |
enable_memoize | on | 嵌套循环内层的缓存 |
enable_gathermerge | on | Gather Merge |
enable_parallel_append | on | 并行 Append |
enable_parallel_hash | on | 并行哈希连接 |
enable_async_append | on | 异步感知的 Append |
enable_partition_pruning | on | 分区裁剪 |
enable_partitionwise_join | off | 分区对分区连接 |
enable_partitionwise_aggregate | off | 分区内聚合 |
enable_presorted_aggregate | on | 预排序聚合输入 |
enable_group_by_reordering | on | 按子节点顺序输出分组键 |
enable_distinct_reordering | on | 重排 DISTINCT 键 |
enable_self_join_elimination | on | 自连接消除 |
注意 enable_partitionwise_join 与 enable_partitionwise_aggregate 默认是 off——因为开启后规划耗时与内存占用会随分区数线性增长。
详细说明
选择性估算的具体算法
规划器对三类条件走不同的估算路径:
范围条件(<, >, BETWEEN)
→ 用 histogram_bounds 定位值落在第几个桶
→ 在桶内按「均匀分布」线性插值
→ 选择性 = (桶序号 + 桶内相对位置) / 桶数
等值条件(=)
→ 优先查 MCV 列表,命中则直接用该值的频率
→ 未命中则摊平: (1 - sum(mcv_freqs)) / (n_distinct - num_mcv)
再扣掉 null_frac
IS NULL
→ 直接用 null_frac直方图插值的具体过程
官方 row-estimation-examples.html 给出的完整推导(针对 unique1 < 1000 的关系):
表 tenk1:10000 行
unique1 的直方图(10 个桶,11 个边界)
bucket[2].min = 993
bucket[2].max = 1997
num_buckets = 10
公式:
selectivity = (1 + (1000 - bucket[2].min) / (bucket[2].max - bucket[2].min)) / num_buckets
= (1 + (1000 - 993) / (1997 - 993)) / 10
= 0.100697
rows = 10000 × 0.100697 = 1007拆开看:
(1 + ...)中的1表示「前面整桶都命中了」(桶 1 完全在 1000 以内);- 分式部分表示桶 2 里命中的比例;
- 除以桶数把「桶数」换算成「比例」。
MCV 与直方图的加权合并
等值条件如果同时有 MCV 命中与直方图底座,会被加权合并(官方 tenk1 的 stringu1 = 'xxx' 类例子):
selectivity = 0.01833333 + 0.298387 × 0.96966667 = 0.307669
rows = 10000 × 0.307669 = 3077含义:MCV 部分贡献固定频率 0.01833333,剩余 0.96966667 的部分按直方图摊平,再被该条件的摊平比例 0.298387 缩放。
低估如何导致 Nested Loop
这是最经典的一种计划扭曲:
真实情况:orders 表按 customer_id 过滤后返回 50000 行
customers 表有 100 万行,customer_id 上有索引
若统计失真,估算只返回 10 行:
规划器认为「对每一行去 customers 表里索引查一次」只要 10 次随机 I/O
→ 选择 Nested Loop + Index Scan
实际执行:
50000 次索引查找 → 50000 次随机 I/O
→ 比 Hash Join 慢几十倍
若估算正确(50000 行):
规划器算下来「建哈希表扫一遍 customers」更便宜
→ 选择 Hash Join高估如何导致内存问题
Hash Join 的内存需求取决于输入行数估算:
work_mem 不足时 → 分批落盘(Batches > 1)
→ 额外的临时文件 I/O
若估算行数虚高:
规划器可能认为需要给哈希表配很大的内存
但 work_mem 是硬上限,超了就落盘
或者规划器干脆选择了看似"更省内存"的归并连接EXPLAIN (ANALYZE) 里若看到 Batches: 8 或 Memory Usage 很大,就要检查行数估算。
统计信息滞后导致的"突然变慢"
场景:批量导入 2000 万行后,立刻执行查询
此时 autovacuum 还没跑,reltuples 与列统计都还是旧值
→ 规划器以为表只有 10 万行
→ 选出的计划在大表上完全失效表现为「同一查询几分钟前还很快,现在突然慢」。解法是加载数据后立即手工 ANALYZE,不等 autovacuum。
成本常量的调整逻辑
random_page_cost 相对 seq_page_cost 决定「索引扫描 vs 顺序扫描」的倾向:
调低 random_page_cost → 更倾向索引扫描
调高 → 更倾向顺序扫描
effective_cache_size → 更高则更倾向用索引(假设缓存命中率高)
→ 只影响估算,不分配任何内存官方提示:把它设得比 seq_page_cost 还小在物理上不合理,但如果数据库几乎全在内存里,让两者相等是合理的。SSD 上通常建议把 random_page_cost 降到 1.1 左右。
并行计划的成本门槛
并行不是"免费的":
启动并行 worker → parallel_setup_cost(默认 1000)
跨进程传每一行 → parallel_tuple_cost(默认 0.1)
因此小结果集上并行一定亏——
只有当「串行成本 × 加速比」的收益超过上面两项固定开销时,
规划器才考虑并行。
min_parallel_table_scan_size(8MB)
min_parallel_index_scan_size(512kB)
→ 表/索引太小连并行都不考虑分区表的两个默认关闭项
enable_partitionwise_join 默认 off
enable_partitionwise_aggregate 默认 off
原因(官方明确说明):
开启后,受 work_mem 限制的节点数量会随分区数线性增长
→ 查询执行期间总内存占用可能大幅上升
→ 规划本身的内存与 CPU 开销也显著上升分区数很多时开启要谨慎评估内存。
GEQO:连接数过多时的兜底
当查询连接了大量表,穷举搜索空间爆炸,规划器改用遗传算法(GEQO)做启发式搜索,牺牲部分计划质量换取可接受的规划时间。这是规划时间本身成为瓶颈时的自动兜底机制,通常不需要手工干预。
估算偏差的三种归因
遇到估算不准时,按这个顺序排查:
① 统计信息过期?
→ 手工 ANALYZE 后复测,若恢复说明是滞后问题
(批量导入、大批量更新后最常见)
② 统计目标太低?
→ 提高该列统计目标(SET STATISTICS)后复测
(数据分布不规则、MCV 之外还有大量中间值)
③ 多列相关?
→ 同一张表多个条件的估算严重偏离
→ 建扩展统计(CREATE STATISTICS)这三种归因对应三套不同的处理手段,先判断再动手。
实战示例
场景一:定位一次"低估 1500 倍"导致的错误计划
-- 步骤 1:看估算与实际
EXPLAIN (ANALYZE, BUFFERS) SELECT *
FROM orders
WHERE customer_id = 42 AND status = 'paid'; Nested Loop (cost=0.86..1024.55 rows=5 width=120)
(actual time=0.052..4521.882 rows=6120 loops=1)
-> Index Scan using orders_customer_id_idx on orders
(cost=0.43..412.10 rows=5 width=120)
(actual time=0.040..3802.115 rows=6120 loops=1)
Index Cond: (customer_id = 42)
Filter: (status = 'paid'::text)
Rows Removed by Filter: 18375
-> ...
Planning Time: 0.612 ms
Execution Time: 4522.941 ms症状:rows=5(估算)vs rows=6120(实际),低估 1200 多倍。执行时间 4.5 秒。
-- 步骤 2:看条件 —— 两个条件在同一张表上,怀疑列相关
SELECT attname, n_distinct, null_frac, correlation
FROM pg_stats
WHERE schemaname = 'public' AND tablename = 'orders'
AND attname IN ('customer_id', 'status');若 customer_id 有 5 万个不同值,status 只有 3 个值,且两者相关(某些客户几乎只有 paid 订单),那就是典型的独立性假设失真。
-- 步骤 3:建扩展统计
CREATE STATISTICS orders_cust_status (dependencies, mcv)
ON customer_id, status FROM orders;
ANALYZE orders;
-- 步骤 4:复测
EXPLAIN (ANALYZE, BUFFERS) SELECT *
FROM orders
WHERE customer_id = 42 AND status = 'paid'; Bitmap Heap Scan on orders (cost=120.55..2840.10 rows=5900 width=120)
(actual time=0.121..190.442 rows=6120 loops=1)
Recheck Cond: (customer_id = 42)
Filter: (status = 'paid'::text)
...
Planning Time: 0.688 ms
Execution Time: 195.873 ms估算从 5 变为 5900,接近实际的 6120;计划从 Nested Loop 变成了 Bitmap Heap Scan,执行时间从 4522ms 降到 195ms。
场景二:区分"统计滞后"与"成本常量不当"
-- 步骤 1:用 SETTINGS 看是否有非默认参数在起作用
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT * FROM events WHERE created_at > now() - interval '1 day'; Seq Scan on events (cost=0.00..24410.00 rows=1000 width=64)
(actual time=0.014..310.221 rows=48211 loops=1)
Filter: (created_at > (now() - '1 day'::interval))
Rows Removed by Filter: 951789
Settings: random_page_cost = '4', seq_page_cost = '1',
effective_cache_size = '4GB'
Planning Time: 0.402 ms
Execution Time: 312.559 msSettings 显示成本常量全是默认值,说明不是常量调过的问题。
-- 步骤 2:既然是默认少量,看 reltuples 是否滞后
SELECT relname, reltuples::bigint, relpages
FROM pg_class WHERE relname = 'events'; relname | reltuples | relpages
---------+-----------+----------
events | 100000 | 3125
(1 row)但实际表里有 100 万行——reltuples 严重滞后,说明是统计过期问题。
-- 步骤 3:手工分析
ANALYZE events;
SELECT relname, reltuples::bigint FROM pg_class WHERE relname = 'events'; relname | reltuples
---------+-----------
events | 1000000
(1 row)-- 步骤 4:复测估算
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events WHERE created_at > now() - interval '1 day'; Index Scan using events_created_at_idx on events
(cost=0.42..5240.30 rows=47500 width=64)
(actual time=0.031..98.442 rows=48211 loops=1)估算从 1000 修正到 47500(实际 48211),计划从 Seq Scan 换成 Index Scan,执行时间从 312ms 降到 98ms。
关键动作:先判断归因(成本常量 or 统计),不要一上来就调参数。
场景三:用 BUFFERS 验证 I/O 假设
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE status = 'paid'; Seq Scan on orders (cost=0.00..24410.00 rows=100000 width=120)
(actual time=0.016..620.114 rows=98500 loops=1)
Filter: (status = 'paid'::text)
Rows Removed by Filter: 11500
Buffers: shared hit=3125
Planning Time: 0.355 ms
Execution Time: 622.003 msBuffers: shared hit=3125 表示 3125 个页全部命中缓存、零次物理读。这告诉我:random_page_cost = 4.0 在这里被高估了——既然页都在内存里,随机读的惩罚远小于 4 倍。可以考虑适当降低 random_page_cost,但不能只凭一次观察就全局改。
场景四:用开关验证"索引是否真的更快"
-- 对比:默认计划(可能选 Seq Scan)
SET enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = 42;
RESET enable_seqscan;用开关的目的是验证假设,而不是"让它走索引就完事":
若禁用 Seq Scan 后实际耗时明显下降
→ 说明成本常量偏保守(random_page_cost 偏大 或 effective_cache_size 偏小)
→ 应调整成本常量,而不是长期开着 enable_seqscan = off
若禁用后更慢了
→ 说明原来的 Seq Scan 是对的,问题在别处注意事项
不要用
enable_seqscan = off作为长期方案:它只是给顺序扫描加极大惩罚。官方明确说这是临时手段,正确做法是调整成本常量或补充统计信息。sql-- 不推荐:全局关闭顺序扫描 -- ALTER SYSTEM SET enable_seqscan = off;不要只根据几次实验就改成本常量:官方警告「没有确定理想值的方法」,成本常量应视为整个查询负载的平均值,基于少数实验修改非常危险。
effective_cache_size不会分配任何内存:它只影响估算。把它设成机器内存是常见错误——应该考虑shared_buffers加上操作系统页缓存中实际用于 PostgreSQL 的部分。ini# 不推荐:直接把机器总内存填进去 effective_cache_size = 64GB估算与实际差 10 倍以内通常无需处理:规划器只需选出正确形状的计划,不需要估算精确。只有偏差大到改变计划选择时才值得动手。
EXPLAIN ANALYZE会真正执行查询:在INSERT/UPDATE/DELETE上运行会改动数据。要观察写语句的计划,务必放进事务里回滚。sqlBEGIN; EXPLAIN ANALYZE UPDATE orders SET status = 'x' WHERE id = 1; ROLLBACK; -- 必须回滚,否则数据被改loops很大时,actual rows要乘以loops:Nested Loop 内层节点的actual rows是单次执行的行数,总行数 =rows × loops。rows=0的估算会让规划器极度乐观:即使实际也接近 0,这个节点之后的成本估算都会失真。扩展统计的 MCV 能给出更合理的上界。enable_partitionwise_join等分区优化默认关闭:开启前评估分区数与内存,因为它们会让受work_mem限制的节点数线性增长。规划时间本身也可能成为瓶颈:连接表很多时规划耗时可能超过执行耗时。GEQO 是兜底机制,但如果单个查询规划就要几百毫秒,要反思查询写法(如减少连接数、用 CTE 分步)。
统计信息与计划缓存无关(但 prepared statement 有关):普通查询每次都会重新规划,因此
ANALYZE后新计划立即生效;但已PREPARE的语句在部分情形下会缓存通用计划,可能不立即反映新统计。SETTINGS选项能一眼看出参数改动:排查"是不是有人改了参数"时,比逐个SHOW更高效。不要为了让某个查询走索引而去改全局
random_page_cost:应该先确认真实存储特性(SSD / 网络存储 / 全内存),再据此设定,而不是按单个查询反推。
最佳实践
把「估算 vs 实际」作为慢查询排查的第一步:看到
EXPLAIN (ANALYZE)后先比rows=两个数字,而不是先看执行时间。sqlEXPLAIN (ANALYZE, BUFFERS) SELECT ...;按「统计滞后 → 统计目标 → 多列相关」顺序排查:三者的处理手段完全不同,先归因再动手。
sql-- 归因一:先手工分析,看是否恢复 ANALYZE orders;常规加
BUFFERS:判断 I/O 假设是否成立,成本远低于收益。sqlEXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT ...;SSD / 全内存场景下调低
random_page_cost:SSD 建议从 1.1 起试;数据库完全在内存时甚至可以与seq_page_cost相等。inirandom_page_cost = 1.1定期校准
effective_cache_size:按「shared_buffers+ 操作系统缓存中可用于数据文件的部分」设定,而不是机器总内存。inieffective_cache_size = 24GB会话级临时调参用于验证,不写进全局配置:验证完
RESET,确认结论后再评估是否全局生效。sqlSET random_page_cost = 1.1; EXPLAIN (ANALYZE, BUFFERS) SELECT ...; RESET random_page_cost;批量数据加载后立即手工
ANALYZE:不等 autovacuum,避免"导入完就变慢"。sqlCOPY orders FROM '/data/orders.csv' WITH (FORMAT csv); ANALYZE orders;把
ANALYZE纳入迁移脚本:建表、加索引、批量变更之后,紧跟一次分析。用
SETTINGS选项固化排查习惯:确认计划是在默认参数下产生的,排除"别人改过参数"这一可能。sqlEXPLAIN (ANALYZE, BUFFERS, SETTINGS) SELECT ...;规划器开关只用于验证,且只在会话内:结论是要么调成本常量、要么补统计,而不是长期禁用某类计划。
sql-- 正确用法:临时验证 SET enable_nestloop = off; EXPLAIN ANALYZE SELECT ...; RESET enable_nestloop;关注
Batches与内存使用:Hash 相关节点出现分批落盘说明行数估算过大或work_mem不足。对关键查询建立"估算偏差"监控:用
pg_stat_statements找出平均耗时高且调用频繁的语句,定期人工复核其计划。sqlSELECT queryid, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(total_exec_time::numeric, 1) AS total_ms FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;单个查询的"偏方"不要上升为全局配置:涉及成本常量的改动应覆盖整体负载特征,而非某一个查询。
下一节
继续学习:连接池配置(PgBouncer)