Skip to content

统计信息对执行计划的影响 ​

前面几节讲了统计信息「是什么」与「怎么收集」。本节回答最后一个问题:统计信息到底是怎么变成执行计划的。

PostgreSQL 的规划过程可以概括成一条链路:统计信息 → 选择性估算 → 行数(基数)估算 → 成本估算 → 计划选择。这条链路上任何一环失真,最终都可能选出一个「估算很便宜、实际很昂贵」的计划。理解这条链路,是把「调统计」和「调计划」两件事接起来的关键。

本节讲清估算与成本的换算关系、估算偏差如何具体地扭曲计划选择,以及在统计信息之外还有哪些调节手段(成本常量、规划器开关)以及它们的正确用法。

前置知识

阅读本节前,建议先了解:扩展统计信息(CREATE STATISTICS)

基础概念 ​

规划的五步链路 ​

text
① 统计信息         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 < 常量 / BETWEENhistogram_bounds 分桶分布不规则、统计目标太低
col IS NULLnull_frac基本不失真
a = 1 AND b = 2各条件选择性相乘列相关时严重低估
a = 1 OR b = 2容斥公式列相关时容易高估
无统计的表达式固定默认值(等值 0.5%)缺失表达式统计时
IN (...)逐值累加列表很长时可近似

从选择性到行数 ​

text
rows = 表的总行数 × 选择性

表的总行数取自 pg_class.reltuples(可能略有滞后),
规划器会按当前物理表大小做缩放修正。

EXPLAIN 输出里 rows= 后面那个数字,就是这一步算出来的估算基数。

从行数到成本 ​

成本由两大类构成:

text
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

一个重要性质:这些常量只有相对值有意义。把全部常量同比例放大或缩小,规划器的选择完全不变。

为什么「低估行数」特别危险 ​

text
低估行数 → 规划器以为结果集很小
        → 认为「用索引逐条取」很划算
        → 选择 Index Scan / Nested Loop
        → 实际行数很大,随机 I/O 爆炸,慢几个数量级

高估行数 → 规划器以为结果集很大
        → 选择 Seq Scan / Hash Join,或预先分配大内存
        → 若实际很小,浪费一次全表扫描
        → 更糟:Hash Join 的内存估算过大,造成 work_mem 溢出落盘

方向上,低估比高估危险得多:高估最多是"多干活",低估会让规划器选一个本质上不适合当前数据量的算法。

成本常量与统计信息的职责分工 ​

text
统计信息          → 决定 rows(估算的行数)
成本常量          → 决定「每行、每页值多少成本」
两者相乘          → 总成本

调错了对象,问题不会解决:
  行数估错       → 调成本常量治标不治本,必须修统计
  硬件特性变了   → 调统计没用,必须调成本常量

比如数据库基本全在内存里,随机读与顺序读几乎无差别,这种情况该调 random_page_cost;而如果只是列相关性导致行数估错,调 random_page_cost 最多是"碰巧对了这次",换个查询又会错。

术语表 ​

术语含义
选择性(selectivity)条件命中的行数比例
基数(cardinality)结果集的行数估算值
成本(cost)规划器用任意单位计算的抽象代价
代价常量(cost constant)把页数、行数换算成成本的系数
规划器开关(planner method config)enable_* 系列,禁用某类计划
误估(misestimate)估算基数与实际行数偏离

语法与用法 ​

观察估算与实际 ​

sql
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 100;
text
 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 倍以上就值得查统计。

只看计划的形状,不跑查询 ​

sql
EXPLAIN SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id;

不跑 EXPLAIN ANALYZE 时只做规划、不执行,适合观察估算(因为若用 ANALYZE,估算可能被平均掉或被实际值掩盖)。

加上成本与内存细节 ​

sql
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT * FROM orders WHERE status = 'paid';
选项作用
BUFFERS显示共享缓冲命中/读取次数,判断 I/O 假设是否符合实际
VERBOSE显示更多节点细节(输出列、schema 限定名)
SETTINGS显示影响本计划选择的非默认参数,排查成本常量改动很关键
WAL显示该语句产生的 WAL 记录数
TIMING OFF关闭逐节点计时,降低计时开销对结果的影响

调整成本常量 ​

sql
-- 会话级临时调整(推荐调试用)
SET random_page_cost = 1.1;

-- 恢复默认
RESET random_page_cost;

-- 查看当前值
SHOW random_page_cost;
ini
# 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

临时禁用某类计划 ​

sql
-- 会话级:暂时禁用顺序扫描,验证"走索引到底会不会更快"
SET enable_seqscan = off;
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'paid';
RESET enable_seqscan;

注意:off 不是"禁止",而是"给它加一个极大的惩罚成本"。如果确实没有别的路径可用,规划器仍会选它。

相关运行参数 ​

参数默认值作用
seq_page_cost1.0顺序读一页的成本
random_page_cost4.0随机读一页的成本
cpu_tuple_cost0.01处理一行的成本
cpu_index_tuple_cost0.005处理一个索引项的成本
cpu_operator_cost0.0025执行一次操作/函数的成本
parallel_setup_cost1000启动并行 worker 的成本
parallel_tuple_cost0.1跨进程传递一行的成本
min_parallel_table_scan_size8MB触发并行表扫描的最小数据量
min_parallel_index_scan_size512kB触发并行索引扫描的最小数据量
effective_cache_size4GB规划器假设的可用缓存总量(仅用于估算)
default_statistics_target100默认统计目标(范围 1~10000)

规划器开关一览 ​

开关默认禁用的计划类型
enable_seqscanon顺序扫描
enable_indexscanon索引扫描与仅索引扫描
enable_indexonlyscanon仅索引扫描(需 indexscan 也开着)
enable_bitmapscanon位图扫描
enable_tidscanonTID 扫描
enable_sorton显式排序
enable_incremental_sorton增量排序
enable_hashaggon哈希聚合
enable_hashjoinon哈希连接
enable_mergejoinon归并连接
enable_nestloopon嵌套循环连接
enable_materialon物化节点
enable_memoizeon嵌套循环内层的缓存
enable_gathermergeonGather Merge
enable_parallel_appendon并行 Append
enable_parallel_hashon并行哈希连接
enable_async_appendon异步感知的 Append
enable_partition_pruningon分区裁剪
enable_partitionwise_joinoff分区对分区连接
enable_partitionwise_aggregateoff分区内聚合
enable_presorted_aggregateon预排序聚合输入
enable_group_by_reorderingon按子节点顺序输出分组键
enable_distinct_reorderingon重排 DISTINCT 键
enable_self_join_eliminationon自连接消除

注意 enable_partitionwise_join 与 enable_partitionwise_aggregate 默认是 off——因为开启后规划耗时与内存占用会随分区数线性增长。

详细说明 ​

选择性估算的具体算法 ​

规划器对三类条件走不同的估算路径:

text
范围条件(<, >, BETWEEN)
  → 用 histogram_bounds 定位值落在第几个桶
  → 在桶内按「均匀分布」线性插值
  → 选择性 = (桶序号 + 桶内相对位置) / 桶数

等值条件(=)
  → 优先查 MCV 列表,命中则直接用该值的频率
  → 未命中则摊平: (1 - sum(mcv_freqs)) / (n_distinct - num_mcv)
     再扣掉 null_frac

IS NULL
  → 直接用 null_frac

直方图插值的具体过程 ​

官方 row-estimation-examples.html 给出的完整推导(针对 unique1 < 1000 的关系):

text
表 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' 类例子):

text
selectivity = 0.01833333 + 0.298387 × 0.96966667 = 0.307669
rows        = 10000 × 0.307669 = 3077

含义:MCV 部分贡献固定频率 0.01833333,剩余 0.96966667 的部分按直方图摊平,再被该条件的摊平比例 0.298387 缩放。

低估如何导致 Nested Loop ​

这是最经典的一种计划扭曲:

text
真实情况: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

高估如何导致内存问题 ​

text
Hash Join 的内存需求取决于输入行数估算:
  work_mem 不足时 → 分批落盘(Batches > 1)
  → 额外的临时文件 I/O

若估算行数虚高:
  规划器可能认为需要给哈希表配很大的内存
  但 work_mem 是硬上限,超了就落盘
  或者规划器干脆选择了看似"更省内存"的归并连接

EXPLAIN (ANALYZE) 里若看到 Batches: 8 或 Memory Usage 很大,就要检查行数估算。

统计信息滞后导致的"突然变慢" ​

text
场景:批量导入 2000 万行后,立刻执行查询
      此时 autovacuum 还没跑,reltuples 与列统计都还是旧值
      → 规划器以为表只有 10 万行
      → 选出的计划在大表上完全失效

表现为「同一查询几分钟前还很快,现在突然慢」。解法是加载数据后立即手工 ANALYZE,不等 autovacuum。

成本常量的调整逻辑 ​

text
random_page_cost 相对 seq_page_cost 决定「索引扫描 vs 顺序扫描」的倾向:

  调低 random_page_cost → 更倾向索引扫描
  调高                  → 更倾向顺序扫描

effective_cache_size → 更高则更倾向用索引(假设缓存命中率高)
                      → 只影响估算,不分配任何内存

官方提示:把它设得比 seq_page_cost 还小在物理上不合理,但如果数据库几乎全在内存里,让两者相等是合理的。SSD 上通常建议把 random_page_cost 降到 1.1 左右。

并行计划的成本门槛 ​

text
并行不是"免费的":
  启动并行 worker        → parallel_setup_cost(默认 1000)
  跨进程传每一行          → parallel_tuple_cost(默认 0.1)

因此小结果集上并行一定亏——
  只有当「串行成本 × 加速比」的收益超过上面两项固定开销时,
  规划器才考虑并行。

min_parallel_table_scan_size(8MB)
min_parallel_index_scan_size(512kB)
  → 表/索引太小连并行都不考虑

分区表的两个默认关闭项 ​

text
enable_partitionwise_join      默认 off
enable_partitionwise_aggregate 默认 off

原因(官方明确说明):
  开启后,受 work_mem 限制的节点数量会随分区数线性增长
  → 查询执行期间总内存占用可能大幅上升
  → 规划本身的内存与 CPU 开销也显著上升

分区数很多时开启要谨慎评估内存。

GEQO:连接数过多时的兜底 ​

当查询连接了大量表,穷举搜索空间爆炸,规划器改用遗传算法(GEQO)做启发式搜索,牺牲部分计划质量换取可接受的规划时间。这是规划时间本身成为瓶颈时的自动兜底机制,通常不需要手工干预。

估算偏差的三种归因 ​

遇到估算不准时,按这个顺序排查:

text
① 统计信息过期?
   → 手工 ANALYZE 后复测,若恢复说明是滞后问题
      (批量导入、大批量更新后最常见)

② 统计目标太低?
   → 提高该列统计目标(SET STATISTICS)后复测
      (数据分布不规则、MCV 之外还有大量中间值)

③ 多列相关?
   → 同一张表多个条件的估算严重偏离
      → 建扩展统计(CREATE STATISTICS)

这三种归因对应三套不同的处理手段,先判断再动手。

实战示例 ​

场景一:定位一次"低估 1500 倍"导致的错误计划 ​

sql
-- 步骤 1:看估算与实际
EXPLAIN (ANALYZE, BUFFERS) SELECT *
FROM orders
WHERE customer_id = 42 AND status = 'paid';
text
 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 秒。

sql
-- 步骤 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 订单),那就是典型的独立性假设失真。

sql
-- 步骤 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';
text
 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。

场景二:区分"统计滞后"与"成本常量不当" ​

sql
-- 步骤 1:用 SETTINGS 看是否有非默认参数在起作用
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT * FROM events WHERE created_at > now() - interval '1 day';
text
 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 ms

Settings 显示成本常量全是默认值,说明不是常量调过的问题。

sql
-- 步骤 2:既然是默认少量,看 reltuples 是否滞后
SELECT relname, reltuples::bigint, relpages
FROM pg_class WHERE relname = 'events';
text
 relname | reltuples | relpages
---------+-----------+----------
 events  |    100000 |     3125
(1 row)

但实际表里有 100 万行——reltuples 严重滞后,说明是统计过期问题。

sql
-- 步骤 3:手工分析
ANALYZE events;

SELECT relname, reltuples::bigint FROM pg_class WHERE relname = 'events';
text
 relname | reltuples
---------+-----------
 events  |   1000000
(1 row)
sql
-- 步骤 4:复测估算
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events WHERE created_at > now() - interval '1 day';
text
 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 假设 ​

sql
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE status = 'paid';
text
 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 ms

Buffers: shared hit=3125 表示 3125 个页全部命中缓存、零次物理读。这告诉我:random_page_cost = 4.0 在这里被高估了——既然页都在内存里,随机读的惩罚远小于 4 倍。可以考虑适当降低 random_page_cost,但不能只凭一次观察就全局改。

场景四:用开关验证"索引是否真的更快" ​

sql
-- 对比:默认计划(可能选 Seq Scan)
SET enable_seqscan = off;

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = 42;

RESET enable_seqscan;

用开关的目的是验证假设,而不是"让它走索引就完事":

text
若禁用 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 上运行会改动数据。要观察写语句的计划,务必放进事务里回滚。

    sql
    BEGIN;
    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= 两个数字,而不是先看执行时间。

    sql
    EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
  • 按「统计滞后 → 统计目标 → 多列相关」顺序排查:三者的处理手段完全不同,先归因再动手。

    sql
    -- 归因一:先手工分析,看是否恢复
    ANALYZE orders;
  • 常规加 BUFFERS:判断 I/O 假设是否成立,成本远低于收益。

    sql
    EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT ...;
  • SSD / 全内存场景下调低 random_page_cost:SSD 建议从 1.1 起试;数据库完全在内存时甚至可以与 seq_page_cost 相等。

    ini
    random_page_cost = 1.1
  • 定期校准 effective_cache_size:按「shared_buffers + 操作系统缓存中可用于数据文件的部分」设定,而不是机器总内存。

    ini
    effective_cache_size = 24GB
  • 会话级临时调参用于验证,不写进全局配置:验证完 RESET,确认结论后再评估是否全局生效。

    sql
    SET random_page_cost = 1.1;
    EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
    RESET random_page_cost;
  • 批量数据加载后立即手工 ANALYZE:不等 autovacuum,避免"导入完就变慢"。

    sql
    COPY orders FROM '/data/orders.csv' WITH (FORMAT csv);
    ANALYZE orders;
  • 把 ANALYZE 纳入迁移脚本:建表、加索引、批量变更之后,紧跟一次分析。

  • 用 SETTINGS 选项固化排查习惯:确认计划是在默认参数下产生的,排除"别人改过参数"这一可能。

    sql
    EXPLAIN (ANALYZE, BUFFERS, SETTINGS) SELECT ...;
  • 规划器开关只用于验证,且只在会话内:结论是要么调成本常量、要么补统计,而不是长期禁用某类计划。

    sql
    -- 正确用法:临时验证
    SET enable_nestloop = off;
    EXPLAIN ANALYZE SELECT ...;
    RESET enable_nestloop;
  • 关注 Batches 与内存使用:Hash 相关节点出现分批落盘说明行数估算过大或 work_mem 不足。

  • 对关键查询建立"估算偏差"监控:用 pg_stat_statements 找出平均耗时高且调用频繁的语句,定期人工复核其计划。

    sql
    SELECT 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)

参考链接 ​