执行计划

1. explain和explain anayze

EXPLAINEXPLAIN ANALYZE生成的执行计划通常是一致的,但并不能保证完全一致

1.1. 核心功能对比

对比维度 EXPLAIN EXPLAIN ANALYZE
执行方式 仅生成执行计划,不执行查询 实际执行查询并生成计划
执行速度 毫秒级,非常快 取决于查询复杂度,可能很慢
副作用 无副作用,完全安全 DML语句会实际修改数据
资源消耗 极低,仅消耗规划资源 消耗实际执行所需的所有资源
-- 对于修改数据的语句,可以在事务中测试后回滚
BEGIN;
EXPLAIN ANALYZE UPDATE table_name SET column = value;
ROLLBACK;

1.2. 输出信息对比

信息类型 EXPLAIN EXPLAIN ANALYZE
执行计划 ✅ 显示(基于估算) ✅ 显示(基于实际执行)
成本估算 ✅ cost=start..total ✅ cost=start..total
实际执行时间 ❌ 无 ✅ actual time=start..end
预估行数 ✅ rows=估算值 ✅ rows=估算值 + actual rows=实际值
数据宽度 ✅ width=字节数 ✅ width=字节数
循环次数 ❌ 无 ✅ loops=N
过滤统计 ❌ 无 ✅ Rows Removed by Filter
规划时间 ❌ 无 ✅ Planning Time
总执行时间 ❌ 无 ✅ Execution Time

1.3. I/O和缓冲区信息

I/O 指标 EXPLAIN EXPLAIN ANALYZE EXPLAIN (ANALYZE, BUFFERS)
缓冲区命中 ❌ 无信息 ❌ 无信息 ✅ shared hit=N
磁盘读取 ❌ 无信息 ❌ 无信息 ✅ shared read=N
缓冲区脏页 ❌ 无信息 ❌ 无信息 ✅ dirtied=N
磁盘写入 ❌ 无信息 ❌ 无信息 ✅ written=N
临时文件I/O ❌ 无信息 ❌ 无信息 ✅ temp read/written=N
本地缓冲区 ❌ 无信息 ❌ 无信息 ✅ local hit/read=N

1.4. 准确性对比

准确性方面 EXPLAIN EXPLAIN ANALYZE
行数估算 基于统计信息,可能不准确 显示实际行数,100%准确
执行时间 无法提供 精确到毫秒
资源使用 无法评估 真实反映内存、I/O使用
并发影响 无法体现 反映真实并发环境影响
缓存状态 无法体现 反映当前缓存命中情况

1.5. 使用场景对比

使用场景 推荐选择 原因
日常开发调试 EXPLAIN 快速、安全,无副作用
性能调优 EXPLAIN ANALYZE 需要真实性能数据
生产环境诊断 EXPLAIN(谨慎使用ANALYZE) 避免对生产数据的影响
I/O瓶颈分析 EXPLAIN (ANALYZE, BUFFERS) 必须有实际I/O统计
索引效果验证 EXPLAIN ANALYZE 需要真实的性能提升数据
大批量操作评估 EXPLAIN → EXPLAIN ANALYZE 先快速评估,再精确测试
DML语句优化 事务中的EXPLAIN ANALYZE 可回滚,获得真实数据

1.6. 高级选项对比

选项 EXPLAIN支持 EXPLAIN ANALYZE支持 说明
VERBOSE 显示详细信息
COSTS 显示/隐藏成本信息
BUFFERS 显示缓冲区使用情况
TIMING 控制时间测量(默认开启)
SUMMARY 显示摘要信息
FORMAT 输出格式(TEXT/JSON/XML/YAML)

1.7. 性能影响对比

影响类型 EXPLAIN EXPLAIN ANALYZE
CPU使用 极低(仅规划) 查询实际CPU消耗
内存使用 极低 查询实际内存需求
磁盘I/O 查询实际I/O操作
网络传输 实际数据传输(如果有)
锁等待 可能遇到真实锁竞争
触发器执行 不执行 会执行所有触发器

1.8. 输出示例对比

1.8.1. EXPLAIN 输出

Nested Loop  (cost=0.29..16.34 rows=1 width=68)
  ->  Index Scan using pk_orders on orders  (cost=0.29..8.30 rows=1 width=36)
        Index Cond: (id = 123)
  ->  Index Scan using pk_customers on customers  (cost=0.29..8.04 rows=1 width=32)
        Index Cond: (id = orders.customer_id)

1.8.2. EXPLAIN ANALYZE 输出

Nested Loop  (cost=0.29..16.34 rows=1 width=68) (actual time=0.234..0.456 rows=1 loops=1)
  ->  Index Scan using pk_orders on orders  (cost=0.29..8.30 rows=1 width=36) (actual time=0.123..0.234 rows=1 loops=1)
        Index Cond: (id = 123)
        Buffers: shared hit=3
  ->  Index Scan using pk_customers on customers  (cost=0.29..8.04 rows=1 width=32) (actual time=0.089..0.145 rows=1 loops=1)
        Index Cond: (id = orders.customer_id)
        Buffers: shared hit=3
Planning Time: 0.567 ms
Execution Time: 0.678 ms

2. cost和actual time解读

(预估成本与预估数据量) (实际执行时间与实际数据量):(cost=0.29..8.30 rows=1 width=36) (actual time=0.123..0.234 rows=1 loops=1)

2.1. cost=0.29..8.30 rows=1 width=36

  • 0.29 = 启动成本 (Startup Cost)
    • 开始返回第一行数据前需要的成本
    • 包括:索引查找定位、初始化操作等
    • 对于索引扫描,通常是定位到第一个匹配记录的成本
  • 8.30 = 总成本 (Total Cost)
    • 返回所有结果行的总成本
    • 包括:启动成本 + 处理所有数据的成本
    • 这是优化器用来比较不同执行计划的主要指标
  • rows=1 = 预估行数
    • 优化器基于统计信息估算会返回1行
    • 基于表统计信息、WHERE条件选择性等计算
  • width=36 = 平均行宽度
    • 每行数据的平均字节数为 36 字节
    • 用于内存使用和I/O成本估算

2.2. actual time=0.123..0.234 rows=1 loops=1

这部分是实际执行时记录的真实数据(仅 EXPLAIN ANALYZE 或 EXPLAIN (ANALYZE, ...) 会输出):

  • 0.123 ms = 首行返回时间
    • 实际开始返回第一行数据的时间
    • 对应启动成本的实际耗时
  • 0.234 ms = 所有行返回完成时间
    • 返回所有结果行的总时间
    • 对应总成本的实际耗时
  • actual rows=1 = 实际返回行数
    • 真实返回了 1 行数据
    • 与预估的 rows=1 完全一致(估算准确)
  • loops=1 = 执行循环次数
    • 这个节点被执行了1次
    • 在嵌套循环中,内层节点的 loops 可能 > 1

3. Index Scan、BitMap scan

在数据库执行计划中,Index Scan(索引扫描)和 Bitmap Index Scan(位图索引扫描)是两种不同的索引使用方式,核心区别在于数据访问方式适用场景

3.1. 核心原理

3.1.1. Index Scan(索引扫描)

  • 方式:直接按照索引的物理结构(如B树的顺序)逐条扫描索引条目,直接定位并访问符合条件的表数据行
  • 过程
    1. 从索引中找到满足条件的条目(包含索引键值和对应表行的物理地址);
    2. 按照索引顺序,通过物理地址直接访问表中的数据行(可能随机访问磁盘);
    3. 若有过滤条件(如WHERE子句中的其他条件),在内存中进一步过滤数据。

3.1.2. Bitmap Index Scan(位图索引扫描)

第一阶段Bitmap Index Scan(位图索引扫描)先通过索引生成一个位图(Bitmap),记录特定数据块是否包含符合条件的行,每个位代表一个数据块(通常8KB)。

数据块位图示例:
Block 1: [1] - 包含符合条件的行
Block 2: [0] - 不包含符合条件的行  
Block 3: [1] - 包含符合条件的行
Block 4: [0] - 不包含符合条件的行
Block 5: [1] - 包含符合条件的行

Bitmap只告诉我们哪些数据块可能有符合条件的行,但数据块内部可能包含不符合条件的行,因此必须逐行再次检查所有过滤条件。

  • 过程
    1. 从索引中找到满足条件的数据块,用位图标记这些块的位置
    2. 若有多个条件(如 WHERE A=? AND B=?),可通过BitmapAnd/BitmapOr组合多个位图(快速计算交集 / 并集),得到最终的 “符合所有条件的数据块” 标记。
    3. 最后根据位图一次性批量访问表中所有符合条件的数据块(顺序访问磁盘,减少 IO 开销)。
      第二阶段Bitmap Heap Scan(位图堆扫描)通常与 Bitmap Index Scan 配合使用
      拿到标记了目标数据块的位图后,Bitmap Heap Scan 会按位图指示,批量访问这些数据块,并从块中提取出真正符合条件的行,此时需要二次过滤,因为一个块中可能只有部分行符合条件。此时会访问标记的数据块,逐行检查,对每一行执行Recheck Cond。

3.2. 关键区别

维度 Index Scan Bitmap Index Scan
数据访问方式 逐条访问索引条目,直接定位表行(可能随机 IO) 先生成位图,再批量访问表行(顺序IO为主)
多条件处理 需多次扫描索引或在内存中过滤,效率较低 通过BitmapAnd/BitmapOr高效组合多个条件
适用数据量 适合返回少量行(如结果集占表的 10% 以下) 适合返回中量行(如结果集占表的 10%-30%)
索引类型依赖 主要用于 B 树索引(最常见索引类型) 可用于B树索引,也常用于专门的位图索引(如 PostgreSQL 的特殊优化)
磁盘 IO 特点 随机IO较多(因按索引顺序访问,表行物理位置可能分散) 顺序IO为主(按位图批量访问,表行物理位置集中)
执行效率 少量数据时更快(无位图生成开销) 中量数据时更快(批量访问减少 IO 开销)

3.3. 适用场景

3.3.1. Index Scan更优的场景

  • 结果集很小(如通过唯一索引查询单行数据或limit查询少量数据);
  • 索引顺序与查询需要的排序顺序一致(可避免额外排序);
  • 仅需访问索引包含的字段(即 “索引覆盖查询”,无需回表)。

3.3.2. Bitmap Index Scan更优的场景

  • 查询包含多个条件(如WHERE A=? AND B=?),且每个条件都有对应的索引;
  • 结果集中等大小(既不适合全表扫描,也不适合逐条索引扫描);
  • 表数据在磁盘上存储较集中(如按顺序插入的表),批量访问效率高。

3.3.3. 举例说明

  • 若查询 WHERE id = 100id是主键),数据库会选择 Index Scan:直接通过主键索引定位到单行,效率最高。
  • 若查询 WHERE status = 'active' AND create_time > '2023-01-01',且statuscreate_time都有索引,数据库可能选择 Bitmap Index Scan
    1. 分别通过两个索引生成 “状态为active的行” 和 “创建时间符合条件的行” 的位图;
    2. BitmapAnd计算两个位图的交集(同时满足两个条件的行);
    3. 按位图批量读取这些行,比两次Index Scan更高效。

3.3.4. 总结

  • Index Scan 是 “点对点” 的精准访问,适合少量数据;
  • Bitmap Index Scan 是 “批量” 的范围访问,适合多条件、中量数据。

Bitmap Index Scan 的设计初衷是通过批量访问表数据减少磁盘 IO 开销:

  • 当结果集较大时,位图可以将分散的行位置 “聚合”,转化为连续的磁盘块访问(顺序 IO),比 Index Scan 的随机 IO 更高效。
  • 但当结果集很小时(如仅匹配 10 行),即使生成位图,也难以形成连续的磁盘块访问,批量访问的优势完全无法体现。此时,Index Scan 直接通过索引定位单行的随机 IO 开销,反而比 “生成位图 + 批量访问” 的总开销更小。

3.3.5. 实际案例

plan A

SELECT COUNT(1) 
FROM RESOURCE_TRACES rt1
WHERE rt1.RESOURCE_ID = ANY (
    SELECT rt2.RESOURCE_ID 
    FROM RESOURCE_TRACES rt2
    WHERE rt2.RESOURCE_TYPE IN ('nat_gateway', 'public-nat-snat-rules', 'public-nat-dnat-rules')
      AND rt2.TIMESTAMP < (NOW() - 90)
      AND rt2.ACTION = 'delete'
    LIMIT 2000
);

Aggregate  (cost=7080.06..7080.07 rows=1 width=0) (actual time=563.674..563.674 rows=1 loops=1)
  Output: COUNT(1)
  Buffers: shared hit=150383 read=13
  ->  Nested Loop  (cost=4049.00..5918.89 rows=464467 width=0) (actual time=563.671..563.671 rows=0 loops=1)
        Buffers: shared hit=150383 read=13
        ->  

  (cost=4049.00..4053.15 rows=415 width=37) (actual time=563.670..563.670 rows=0 loops=1)
              Output: "ANY_subquery".RESOURCE_ID
              Buffers: shared hit=150383 read=13
              ->  Subquery Scan on "ANY_subquery"  (cost=0.01..4044.00 rows=2000 width=37) (actual time=563.666..563.666 rows=0 loops=1)
                    Output: "ANY_subquery".RESOURCE_ID, "ANY_subquery".RESOURCE_ID
                    Buffers: shared hit=150383 read=13
                    ->  Limit  (cost=0.01..4024.00 rows=2000 width=37) (actual time=563.664..563.664 rows=0 loops=1)
                          Output: PUBLIC.RESOURCE_TRACES.RESOURCE_ID
                          Buffers: shared hit=150383 read=13
                          ->  Index Scan using IX_RESOURCE_TRACES_RESOURCE_TYPE_TIMESTAMP on PUBLIC.RESOURCE_TRACES  (cost=0.01..143256.13 rows=71201 width=37) (actual time=563.663..563.663 rows=0 loops=1)
                                Output: PUBLIC.RESOURCE_TRACES.RESOURCE_ID
                                Index Cond: (((PUBLIC.RESOURCE_TRACES.RESOURCE_TYPE)::TEXT = ANY ('{nat_gateway,public-nat-snat-rules,public-nat-dnat-rules}'::TEXT[])) AND ((NOW() - 90::DOUBLE PRECISION) > PUBLIC.RESOURCE_TRACES."TIMESTAMP"))
                                Filter: ((PUBLIC.RESOURCE_TRACES.ACTION)::TEXT = 'delete'::TEXT)
                                Rows Removed by Filter: 124536
                                Buffers: shared hit=150383 read=13
        ->  Index Only Scan using IX_RESOURCE_TRACES_RESOURCE_ID on PUBLIC.RESOURCE_TRACES  (cost=0.00..4.45 rows=5 width=37) (never executed)
              Output: PUBLIC.RESOURCE_TRACES.RESOURCE_ID
              Index Cond: (PUBLIC.RESOURCE_TRACES.RESOURCE_ID = ("ANY_subquery".RESOURCE_ID)::TEXT)
              Heap Fetches: 0
Total runtime: 563.881 ms

plan B

SELECT COUNT(1) 
FROM RESOURCE_TRACES rt1
WHERE rt1.RESOURCE_ID = ANY (
    SELECT rt2.RESOURCE_ID 
    FROM RESOURCE_TRACES rt2
    WHERE rt2.RESOURCE_TYPE IN ('nat_gateway', 'public-nat-snat-rules', 'public-nat-dnat-rules')
      AND rt2.TIMESTAMP < (NOW() - 90)
      AND rt2.ACTION = 'delete'
);

Aggregate  (cost=191818.22..191818.23 rows=1 width=0) (actual time=1469.954..1469.954 rows=1 loops=1)
  Output: COUNT(1)
  Buffers: shared hit=5980 read=19165
  ->  Nested Loop  (cost=130613.08..189749.08 rows=827658 width=0) (actual time=1469.950..1469.950 rows=0 loops=1)
        Buffers: shared hit=5980 read=19165
        ->  HashAggregate  (cost=130613.08..130760.68 rows=14760 width=37) (actual time=1469.950..1469.950 rows=0 loops=1)
              Output: PUBLIC.RESOURCE_TRACES.RESOURCE_ID
              Buffers: shared hit=5980 read=19165
              ->  Bitmap Heap Scan on PUBLIC.RESOURCE_TRACES  (cost=59991.05..130435.08 rows=71201 width=37) (actual time=1469.919..1469.919 rows=0 loops=1)
                    Output: PUBLIC.RESOURCE_TRACES.RESOURCE_ID, PUBLIC.RESOURCE_TRACES.RESOURCE_ID
                    Recheck Cond: (((PUBLIC.RESOURCE_TRACES.RESOURCE_TYPE)::TEXT = ANY ('{nat_gateway,public-nat-snat-rules,public-nat-dnat-rules}'::TEXT[])) AND ((NOW() - 90::DOUBLE PRECISION) > PUBLIC.RESOURCE_TRACES."TIMESTAMP") AND ((PUBLIC.RESOURCE_TRACES.ACTION)::TEXT = 'delete'::TEXT))
                    Buffers: shared hit=5980 read=19165
                    ->  BitmapAnd  (cost=59991.05..59991.05 rows=71201 width=0) (actual time=1454.039..1454.039 rows=0 loops=1)
                          Buffers: shared hit=4139 read=18722
                          ->  Bitmap Index Scan on IX_RESOURCE_TRACES_RESOURCE_TYPE_TIMESTAMP  (cost=0.00..7105.65 rows=157120 width=0) (actual time=113.143..113.143 rows=149721 loops=1)
                                Index Cond: (((PUBLIC.RESOURCE_TRACES.RESOURCE_TYPE)::TEXT = ANY ('{nat_gateway,public-nat-snat-rules,public-nat-dnat-rules}'::TEXT[])) AND ((NOW() - 90::DOUBLE PRECISION) > PUBLIC.RESOURCE_TRACES."TIMESTAMP"))
                                Buffers: shared hit=4138 read=857
                          ->  Bitmap Index Scan on IX_RESOURCE_TRACES_ACTION  (cost=0.00..52849.55 rows=4735589 width=0) (actual time=1253.611..1253.611 rows=4897280 loops=1)
                                Index Cond: ((PUBLIC.RESOURCE_TRACES.ACTION)::TEXT = 'delete'::TEXT)
                                Buffers: shared hit=1 read=17865
        ->  Index Only Scan using IX_RESOURCE_TRACES_RESOURCE_ID on PUBLIC.RESOURCE_TRACES  (cost=0.00..3.95 rows=5 width=37) (never executed)
              Output: PUBLIC.RESOURCE_TRACES.RESOURCE_ID
              Index Cond: (PUBLIC.RESOURCE_TRACES.RESOURCE_ID = (PUBLIC.RESOURCE_TRACES.RESOURCE_ID)::TEXT)
              Heap Fetches: 0
Total runtime: 1471.042 ms

plan a中添加了limit,数据库引擎分析认为最终查询结果数量较少,选了直接使用index scan的方式。

plan b中Bitmap Index Scan 的核心步骤是先创建“resource_type和timestamp满足条件的行”以及“action满足条件的行”的位图(Bitmap),这个过程本身存在固定成本。action匹配了接近500w行,resource_type和timestamp匹配接近16万行,而最终BitmapAnd后的结果是零,这个生成位图的时间完全是浪费的。

  • 即使最终只匹配少量行,也需要扫描索引中符合条件的条目,并为这些行在内存中标记对应的位图位置(例如,用二进制位表示 “第 N 行是否符合条件”)。
  • 对位图的初始化、位运算(如单个条件的位图生成)需要消耗 CPU 和内存资源,而这些开销对于 “仅返回几行数据” 的场景来说,占比会非常高。

相比之下,Index Scan(索引扫描)可以直接定位到少量符合条件的行,无需生成位图,省去了这部分固定成本。Plan A中通过timestamp和resource_type所有过滤后只剩16w行数据,再实际回表过滤action(这个过程过滤掉了全部的数据)数据。可以考虑进行索引优化

-- 当前索引
IX_RESOURCE_TRACES_RESOURCE_TYPE_TIMESTAMP (RESOURCE_TYPE, TIMESTAMP)

-- 创建包含ACTION的复合索引
CREATE INDEX IX_RESOURCE_TRACES_OPTIMIZED 
ON RESOURCE_TRACES (RESOURCE_TYPE, ACTION, TIMESTAMP);

3.3.5.1. ANY_subquery

  • "ANY_subquery":PostgreSQL 内部给子查询分配的别名
  • 当使用 = ANY (SELECT ...)IN (SELECT ...) 时,优化器会创建这个临时的子查询扫描节点,这不是用户定义的别名,而是系统自动生成的
  • plan a中的HashAggregate 的作用是:
    1. 对子查询结果去重:确保每个 RESOURCE_ID 只出现一次
    2. 优化连接操作:避免在 Nested Loop 中重复处理相同的值

4. HashAggregate

HashAggregate是 PostgreSQL 中用于执行聚合操作的一种算法,它使用哈希表来分组和计算聚合函数或实现去重目的等。

4.1. 基本工作原理

4.1.1. 哈希表结构

-- 假设查询
SELECT department, COUNT(*), AVG(salary) 
FROM employees 
GROUP BY department;

HashAggregate 会创建哈希表:

Hash Table:
Key (department) → Aggregated Values
"IT"            → {count: 15, sum: 750000}
"Sales"         → {count: 20, sum: 1200000}  
"HR"            → {count: 8,  sum: 320000}

4.1.2. 执行步骤

1. 扫描输入数据的每一行
2. 计算 GROUP BY 列的哈希值
3. 在哈希表中查找对应的组
4. 更新该组的聚合值
5. 最后输出所有组的结果

4.2. HashAggregate vs GroupAggregate

方面 HashAggregate GroupAggregate
算法 基于哈希表 基于预排序数据
输入要求 无需排序 需要按GROUP BY列排序
内存使用 需要足够内存存储哈希表 内存需求较小
适用场景 小到中等数据集,随机顺序 大数据集,已排序数据
性能特点 O(n) 时间复杂度 O(nlogn) 如果需要额外排序

4.3. 实际执行计划示例

4.3.1. 示例1:基本 GROUP BY

EXPLAIN (ANALYZE, BUFFERS) 
SELECT department, COUNT(*), AVG(salary)
FROM employees 
GROUP BY department;
HashAggregate  (cost=180.00..185.00 rows=5 width=20) 
               (actual time=12.345..12.567 rows=5 loops=1)
  Output: department, count(*), avg(salary)
  Group Key: department
  Batches: 1  Memory Usage: 24kB
  Buffers: shared hit=100
  ->  Seq Scan on employees  (cost=0.00..150.00 rows=1000 width=20) 
                             (actual time=0.123..8.456 rows=1000 loops=1)
        Output: department, salary
        Buffers: shared hit=100

关键信息解读

  • Group Key: department - 分组的列
  • Batches: 1 - 所有数据在一批处理(内存足够)
  • Memory Usage: 24kB - 哈希表占用内存
  • rows=5 - 最终输出5个不同的部门

4.3.2. 示例2:去重操作(DISTINCT)

EXPLAIN ANALYZE 
SELECT DISTINCT resource_id FROM resource_traces;
HashAggregate  (cost=4049.00..4053.15 rows=415 width=37) 
               (actual time=563.670..563.670 rows=0 loops=1)
  Output: resource_id
  Group Key: resource_id
  Buffers: shared hit=150383 read=13

这里 HashAggregate 用于:

  • 去重操作DISTINCT 本质上是按所有选择列分组
  • Group Key: resource_id 用于去重的列
  • rows=0: 输入数据为空,所以输出也为空

4.3.3. 示例3:内存不足时的批处理

HashAggregate  (cost=180.00..185.00 rows=50000 width=20) 
               (actual time=234.567..245.789 rows=50000 loops=1)
  Output: customer_id, sum(amount)
  Group Key: customer_id  
  Batches: 3  Memory Usage: 4096kB  Disk Usage: 15432kB
  ->  Seq Scan on orders

Batches > 1 说明:

  • 内存不足以一次性处理所有分组
  • 数据被分成3批处理
  • 使用了 15MB 的临时磁盘空间

4.4. 不同聚合场景下的 HashAggregate

4.4.1. COUNT DISTINCT

SELECT COUNT(DISTINCT customer_id) FROM orders;
Aggregate  (actual time=45.123..45.123 rows=1 loops=1)
  Output: count(DISTINCT customer_id)
  ->  HashAggregate  (actual time=34.567..41.234 rows=5000 loops=1)
        Output: customer_id
        Group Key: customer_id  -- 先去重
        ->  Seq Scan on orders

4.4.2. 多列分组

SELECT region, department, COUNT(*) 
FROM employees 
GROUP BY region, department;
HashAggregate  (actual time=23.456..25.789 rows=15 loops=1)
  Output: region, department, count(*)
  Group Key: region, department  -- 复合分组键

4.4.3. HAVING 子句

SELECT department, COUNT(*) 
FROM employees 
GROUP BY department 
HAVING COUNT(*) > 10;
HashAggregate  (actual time=12.345..12.456 rows=3 loops=1)
  Output: department, count(*)
  Group Key: department
  Filter: (count(*) > 10)  -- HAVING 条件
  Rows Removed by Filter: 2  -- 2个组被HAVING过滤掉

4.5. 性能调优考虑

4.5.1. work_mem 设置

-- 检查当前设置
SHOW work_mem;  -- 默认通常是 4MB

-- 如果看到 Batches > 1,考虑增加 work_mem
SET work_mem = '64MB';

-- 或者针对特定会话
SET LOCAL work_mem = '128MB';

4.5.2. 何时选择 HashAggregate vs GroupAggregate

-- PostgreSQL 会基于以下因素选择:
-- 1. 数据量大小
-- 2. 是否已按 GROUP BY 列排序
-- 3. work_mem 设置
-- 4. 预估的分组数量

-- 强制使用 GroupAggregate(通过排序)
SELECT department, COUNT(*) 
FROM employees 
ORDER BY department  -- 强制排序,可能选择 GroupAggregate
GROUP BY department;

4.5.3. 监控指标

  • Batches: 应该尽量为 1
  • Memory Usage: 不应该超过 work_mem
  • Disk Usage: 应该避免出现
  • actual rows vs estimated rows: 估算准确性