bigywater.tech
PostgreSQL · 9

PostgreSQL EXPLAIN ANALYZE 估计行数和实际行数差很多怎么办:过期统计信息、列相关、数据倾斜和 pg_stat_statements

“PostgreSQL 慢查询怎么看 EXPLAIN、估计行数和实际行数差很多怎么办” 是排查慢 SQL 的核心。这篇在本机自己起一个 PostgreSQL 16.15,关掉 autovacuum 让统计信息只在手动 ANALYZE 时更新,逐个复现过期统计、列相关性、数据倾斜三种估计错误,并用 pg_stat_statements 找最耗时的 SQL。

1 行 对 900000 行
统计信息过期时 Nested Loop 的估计行数和实际行数(767.68 ms),ANALYZE 之后 109.37 ms
1030 → 10200
两列相关时 where a = 55 and b = 5 的估计行数,CREATE STATISTICS 前后(实际 10000)
42.230 ms → 0.003 ms
pg_stat_statements 里排第一的无索引按 id 查询,建索引前后的平均耗时

读计划

实测 · 本机执行输出
    HashAggregate  (cost=19224.70..19224.74 rows=4 width=21) (actual time=78.070..78.072 rows=4 loops=1)
      Group Key: c.region
      Batches: 1  Memory Usage: 24kB
      Buffers: shared hit=5504
      ->  Hash Join  (cost=289.00..18487.23 rows=98330 width=9) (actual time=1.242..67.849 rows=99360 loops=1)
            Hash Cond: (o.customer_id = c.id)
            Buffers: shared hit=5504
            ->  Seq Scan on orders o  (cost=0.00..17940.00 rows=98330 width=8) (actual time=0.008..53.678 rows=99360 loops=1)
                  Filter: (amount > 900)
                  Rows Removed by Filter: 900640
                  Buffers: shared hit=5440
            ->  Hash  (cost=164.00..164.00 rows=10000 width=9) (actual time=1.228..1.228 rows=10000 loops=1)
                  Buckets: 16384  Batches: 1  Memory Usage: 558kB
                  Buffers: shared hit=64
                  ->  Seq Scan on customers c  (cost=0.00..164.00 rows=10000 width=9) (actual time=0.004..0.582 rows=10000 loops=1)
                        Buffers: shared hit=64
    Planning:
      Buffers: shared hit=85 read=15
    Planning Time: 0.324 ms
    Execution Time: 78.118 ms

每个节点同时有估计行数和实际行数、Buffers;先对比两者是否在一个量级。

统计过期

实测 · 本机执行输出
  没有 ANALYZE:e 的扫描节点估计 1 行、实际 900000 行;总耗时 767.68 ms
      Aggregate  (rows=1 width=8) (actual time=767.663..767.664 rows=1 loops=1)
        ->  Nested Loop  (rows=1 width=0) (actual time=0.016..742.781 rows=900000 loops=1)
              ->  Index Only Scan using e_kind_idx on e  (rows=1 width=2) (actual time=0.011..99.054 rows=900000 loops=1)
                    Index Cond: (kind = 'b'::text)
                    Heap Fetches: 900000
              ->  Seq Scan on small s  (rows=1 width=2) (actual time=0.000..0.000 rows=1 loops=900000)
  ANALYZE 之后:e 的扫描节点估计 898700 行、实际 900000 行;总耗时 109.37 ms
      Aggregate  (rows=1 width=8) (actual time=109.359..109.359 rows=1 loops=1)
        ->  Nested Loop  (rows=898700 width=0) (actual time=4.754..89.119 rows=900000 loops=1)
              ->  Seq Scan on small s  (rows=1 width=2) (actual time=0.005..0.005 rows=1 loops=1)
                    Filter: (kind = 'b'::text)
                    Rows Removed by Filter: 1
              ->  Seq Scan on e  (rows=898700 width=2) (actual time=4.749..54.231 rows=900000 loops=1)

大批量写入后没 ANALYZE,估计 1 行、实际几十万行,计划选错;ANALYZE 之后修正。

列相关

实测 · 本机执行输出
  没有扩展统计:估计 1030 行、实际 10000 行
  CREATE STATISTICS … (dependencies) 并 ANALYZE 之后:估计 10200 行、实际 10000 行

默认假设各列独立,相关列会被低估;CREATE STATISTICS (dependencies) 修正。

数据倾斜

实测 · 本机执行输出
  实际行数:v37 有 3885 行,v300 有 464 行
  statistics target = 10   v37:估计 668 行(实际 3885);v300:估计 668 行(实际 464)
  statistics target = 100  v37:估计 4633 行(实际 3885);v300:估计 368 行(实际 464)
  statistics target = 1000 v37:估计 3997 行(实际 3885);v300:估计 443 行(实际 464)

statistics target 太小时 MCV 列表装不下,不同的值得到同一个估计。

pg_stat_statements

实测 · 本机执行输出
  调用  200 次,总耗时   8445.9 ms,平均   42.230 ms,共返回   200 行  select * from orders where id = $1
  调用   20 次,总耗时    759.9 ms,平均   37.996 ms,共返回    20 行  select count(*), sum(amount) from orders where amount > $1
  调用    3 次,总耗时    437.0 ms,平均  145.666 ms,共返回    12 行  select c.region, count(*) from orders o join customers c on c.id = o.customer_id
  排在最前面的那条按 id 查 orders,orders.id 上没有索引,每次都是全表扫描。建索引后重跑同样的 200 次:
  调用  200 次,总耗时      0.6 ms,平均    0.003 ms

按总耗时找出最该优化的 SQL,再看它的计划。

分步演示

EXPLAIN 与统计信息
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / PostgreSQL 16.15(Homebrew);每个实验自己 initdb 一个数据目录,端口 9261 起,只监听 127.0.0.1,不碰本机已有的 5432 上的实例;命令行工具统一用 LC_ALL=C 取英文原文,路径换成 ;计时用 time.perf_counter 或 EXPLAIN ANALYZE 的 Execution Time,数字每次运行会有小幅抖动;实验结束后整个数据目录删除。autovacuum 关掉,统计信息只在手动 ANALYZE 时更新;关掉并行查询;加载 pg_stat_statements;各表数据由 setseed 之后的 random() 生成,耗时每次运行会不同
    • 没有验证:真实业务下 autovacuum 的自动 ANALYZE 能否及时跟上批量写入;CREATE STATISTICS 的 ndistinct、mcv 类型;auto_explain 记录慢查询计划;prepared statement 的 generic plan 与 custom plan;pg_stat_statements 在高并发下的开销;PostgreSQL 其他大版本
    京ICP备2026067502号-1京公网安备11010502063487号