一、查询计划概述

查询计划是PostgreSQL查询优化器为执行SQL查询生成的执行方案。它描述了PostgreSQL将如何执行查询,包括使用哪些索引、如何连接表、如何排序数据等。

1.1 查询计划的重要性

查询计划对查询性能有着至关重要的影响:

  1. 决定查询速度:好的查询计划可以让查询在毫秒级完成,而差的查询计划可能需要几分钟甚至几小时
  2. 影响系统资源使用:好的查询计划可以减少CPU、内存和I/O的使用
  3. 帮助识别性能瓶颈:通过分析查询计划,可以识别查询中的性能瓶颈
  4. 指导查询优化:查询计划可以指导开发人员优化查询语句和表设计

1.2 查询优化器的作用

查询优化器(Query Optimizer)是PostgreSQL的核心组件之一,负责生成最优的查询计划。它的主要职责包括:

  1. 解析查询:将SQL查询解析为语法树
  2. 语义分析:检查查询的语义正确性
  3. 生成候选计划:生成多个可能的查询计划
  4. 评估计划成本:使用统计信息评估每个计划的成本
  5. 选择最优计划:选择成本最低的查询计划

1.3 查询计划的成本模型

PostgreSQL使用基于成本的优化方法,成本模型考虑以下因素:

  1. I/O成本:读取数据页的成本
  2. CPU成本:处理数据的成本
  3. 内存使用:查询所需的内存量
  4. 网络成本:数据传输的成本

成本模型使用相对成本单位,默认情况下:

  • 顺序读取一个数据页的成本为1.0
  • 随机读取一个数据页的成本为4.0
  • 处理一个数据行的成本为0.01

二、查看查询计划

PostgreSQL提供了多种方式查看查询计划,最常用的是EXPLAIN命令。

2.1 EXPLAIN命令的基本用法

-- 基本EXPLAIN命令,只显示计划和估计成本
EXPLAIN SELECT * FROM employees WHERE department = 'IT';

-- EXPLAIN ANALYZE,显示实际执行时间和行数
EXPLAIN ANALYZE SELECT * FROM employees WHERE department = 'IT';

-- EXPLAIN (ANALYZE, BUFFERS),显示缓冲区使用情况
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM employees WHERE department = 'IT';

-- EXPLAIN (ANALYZE, VERBOSE),显示详细的计划信息
EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM employees WHERE department = 'IT';

-- EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON),以JSON格式输出
EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) SELECT * FROM employees WHERE department = 'IT';

2.2 EXPLAIN命令的输出格式

EXPLAIN命令支持以下输出格式:

  1. 文本格式:默认格式,适合直接查看
  2. JSON格式:适合程序处理
  3. XML格式:适合程序处理
  4. YAML格式:适合程序处理

2.3 常用的EXPLAIN选项

选项 描述
ANALYZE 执行查询并显示实际执行时间和行数
BUFFERS 显示缓冲区使用情况
COSTS 显示估计成本
VERBOSE 显示详细信息,包括列名和表别名
FORMAT 指定输出格式(TEXT、JSON、XML、YAML)
SETTINGS 显示查询使用的配置参数
SUMMARY 显示查询的摘要信息,包括总执行时间

三、查询计划的组成部分

3.1 查询计划的基本结构

查询计划是一个树形结构,每个节点代表一个操作。最底层的节点是扫描操作,上层节点是连接、聚合、排序等操作。

Gather  (cost=1000.00..1120.00 rows=1000 width=100)
  Workers Planned: 2
  ->  Parallel Seq Scan on employees  (cost=0.00..120.00 rows=500 width=100)
        Filter: (department = 'IT'::text)

3.2 查询计划中的关键信息

  1. 节点类型:操作的类型,如Seq Scan、Index Scan、Hash Join等
  2. 成本信息
    • cost=startup_cost..total_cost:估计的启动成本和总成本
    • rows=estimated_rows:估计返回的行数
    • width=estimated_width:估计每行的宽度(字节)
  3. 实际执行信息(使用ANALYZE时):
    • actual time=startup_time..total_time:实际启动时间和总执行时间
    • rows=actual_rows:实际返回的行数
    • loops=number_of_loops:操作执行的次数
  4. 过滤条件Filter: (condition),应用于扫描的过滤条件
  5. 索引信息:使用的索引名称和索引条件
  6. 连接信息:连接类型、连接条件和连接顺序

3.3 常见的查询计划节点

3.3.1 扫描节点
  • Seq Scan:顺序扫描整个表
  • Index Scan:索引扫描,先扫描索引,再回表获取数据
  • Index Only Scan:仅索引扫描,不需要回表获取数据
  • Bitmap Heap Scan:位图堆扫描,结合位图索引使用
  • Bitmap Index Scan:位图索引扫描,用于生成位图
  • Tid Scan:TID扫描,根据行标识符直接访问行
3.3.2 连接节点
  • Nested Loop Join:嵌套循环连接
  • Hash Join:哈希连接
  • Merge Join:合并连接
3.3.3 聚合和排序节点
  • GroupAggregate:分组聚合
  • HashAggregate:哈希聚合
  • Sort:排序操作
  • Unique:去重操作
3.3.4 其他节点
  • Limit:限制返回行数
  • Offset:跳过指定行数
  • Subquery Scan:子查询扫描
  • CTE Scan:公共表表达式扫描
  • Materialize:物化操作,将结果存储在内存中

四、解读查询计划

4.1 示例查询计划分析

EXPLAIN ANALYZE 
SELECT e.name, d.department_name, COUNT(*) as num_employees
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.salary > 5000
GROUP BY e.name, d.department_name
ORDER BY num_employees DESC
LIMIT 10;

查询计划

Limit  (cost=200.00..250.00 rows=10 width=52) (actual time=10.00..15.00 rows=10 loops=1)
  ->  Sort  (cost=200.00..225.00 rows=1000 width=52) (actual time=10.00..12.00 rows=10 loops=1)
        Sort Key: (count(*)) DESC
        Sort Method: top-N heapsort  Memory: 25kB
        ->  HashAggregate  (cost=100.00..150.00 rows=1000 width=52) (actual time=5.00..8.00 rows=500 loops=1)
              Group Key: e.name, d.department_name
              Batches: 1  Memory Usage: 4096kB
              ->  Hash Join  (cost=50.00..80.00 rows=2000 width=36) (actual time=2.00..4.00 rows=2000 loops=1)
                    Hash Cond: (e.department_id = d.id)
                    ->  Seq Scan on employees e  (cost=0.00..30.00 rows=2000 width=28) (actual time=0.00..1.00 rows=2000 loops=1)
                          Filter: (salary > 5000)
                          Rows Removed by Filter: 3000
                    ->  Hash  (cost=30.00..30.00 rows=1000 width=16) (actual time=1.00..1.00 rows=1000 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 64kB
                          ->  Seq Scan on departments d  (cost=0.00..30.00 rows=1000 width=16) (actual time=0.00..0.50 rows=1000 loops=1)
Planning Time: 0.50 ms
Execution Time: 15.50 ms

4.2 解读步骤

  1. 从顶层到底层解读

    • 顶层是Limit节点,限制返回10行
    • 下一层是Sort节点,按num_employees降序排序
    • 再下一层是HashAggregate节点,进行分组聚合
    • 然后是Hash Join节点,连接employees和departments表
    • 最底层是两个Seq Scan节点,分别扫描employees和departments表
  2. 分析成本和实际执行时间

    • 估计总成本:250.00
    • 实际总执行时间:15.50 ms
    • 估计行数:1000,实际行数:10
  3. 分析扫描方式

    • 两个表都使用了顺序扫描(Seq Scan)
    • employees表扫描了5000行,过滤掉了3000行,实际返回2000行
  4. 分析连接方式

    • 使用了Hash Join,适合两个表都比较大的情况
    • 先扫描departments表并构建哈希表,然后扫描employees表并进行哈希连接
  5. 分析聚合方式

    • 使用了HashAggregate,适合分组聚合
    • 内存使用:4096kB
  6. 分析排序方式

    • 使用了top-N heapsort,适合只需要前N行的情况
    • 内存使用:25kB

4.3 识别性能瓶颈

从上面的查询计划中,可以识别出以下性能瓶颈:

  1. 顺序扫描:两个表都使用了顺序扫描,如果表很大,可能会很慢
  2. 过滤掉大量行:employees表过滤掉了3000行,只返回2000行
  3. 哈希连接的内存使用:虽然当前内存使用合理,但如果表很大,可能会溢出到磁盘

五、查询计划优化技巧

5.1 优化扫描方式

  1. 添加合适的索引

    -- 为department和salary列添加索引
    CREATE INDEX idx_employees_department_salary ON employees (department, salary);
    
  2. 使用覆盖索引

    -- 创建覆盖索引,包含查询所需的所有列
    CREATE INDEX idx_employees_department_salary_name ON employees (department, salary) INCLUDE (name);
    
  3. 调整表的存储参数

    -- 设置合适的填充因子
    ALTER TABLE employees SET (fillfactor = 80);
    

5.2 优化连接方式

  1. 选择合适的连接顺序

    • 小表驱动大表
    • 确保连接列上有索引
  2. 使用合适的连接类型

    • 对于小表,使用Nested Loop Join
    • 对于大表,使用Hash Join或Merge Join
  3. 调整连接相关参数

    -- 调整哈希连接的内存使用
    SET work_mem = '64MB';
    

5.3 优化聚合和排序

  1. 优化聚合查询

    • 在分组列上添加索引
    • 考虑使用部分聚合
  2. 优化排序操作

    • 在排序列上添加索引
    • 避免不必要的排序
    • 调整排序内存参数:
      SET work_mem = '64MB';
      
  3. 使用物化视图

    -- 创建物化视图
    CREATE MATERIALIZED VIEW department_employee_count AS
    SELECT department, COUNT(*) as num_employees
    FROM employees
    GROUP BY department;
    
    -- 查询物化视图
    SELECT * FROM department_employee_count;
    

5.4 优化子查询和CTE

  1. 避免 correlated subquery

    -- 不好的做法:correlated subquery
    SELECT * FROM employees e
    WHERE salary > (SELECT AVG(salary) FROM employees WHERE department = e.department);
    
    -- 好的做法:使用JOIN
    SELECT e.* FROM employees e
    JOIN (SELECT department, AVG(salary) as avg_salary FROM employees GROUP BY department) d
    ON e.department = d.department AND e.salary > d.avg_salary;
    
  2. 使用CTE替代子查询

    -- 使用CTE
    WITH dept_avg AS (
      SELECT department, AVG(salary) as avg_salary
      FROM employees
      GROUP BY department
    )
    SELECT e.* FROM employees e
    JOIN dept_avg d ON e.department = d.department AND e.salary > d.avg_salary;
    
  3. 考虑物化CTE

    -- 物化CTE
    WITH dept_avg AS MATERIALIZED (
      SELECT department, AVG(salary) as avg_salary
      FROM employees
      GROUP BY department
    )
    SELECT e.* FROM employees e
    JOIN dept_avg d ON e.department = d.department AND e.salary > d.avg_salary;
    

六、使用pg_stat_statements分析慢查询

pg_stat_statements是PostgreSQL的一个扩展,用于收集和统计SQL查询的执行信息。

6.1 安装和配置pg_stat_statements

-- 安装扩展
CREATE EXTENSION pg_stat_statements;

-- 配置参数
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';
ALTER SYSTEM SET pg_stat_statements.track = 'all';
ALTER SYSTEM SET pg_stat_statements.max = 10000;
ALTER SYSTEM SET pg_stat_statements.track_utility = on;

-- 重启PostgreSQL使配置生效
-- sudo systemctl restart postgresql-18

6.2 使用pg_stat_statements查询慢查询

-- 查看执行时间最长的10个查询
SELECT 
    queryid,
    query,
    calls,
    total_time,
    mean_time,
    rows,
    100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM 
    pg_stat_statements
ORDER BY 
    total_time DESC
LIMIT 10;

-- 查看调用次数最多的10个查询
SELECT 
    queryid,
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM 
    pg_stat_statements
ORDER BY 
    calls DESC
LIMIT 10;

-- 查看平均执行时间最长的10个查询
SELECT 
    queryid,
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM 
    pg_stat_statements
ORDER BY 
    mean_time DESC
LIMIT 10;

-- 重置统计信息
SELECT pg_stat_statements_reset();

6.3 分析pg_stat_statements结果

从pg_stat_statements的结果中,可以分析出以下信息:

  1. 执行时间最长的查询:需要重点优化
  2. 调用次数最多的查询:即使单次执行时间短,总执行时间可能很长
  3. 平均执行时间最长的查询:可能存在性能问题
  4. 命中率:shared_blks_hit / (shared_blks_hit + shared_blks_read),命中率低表示需要更多的I/O

七、查询计划优化实践

7.1 实践案例:优化慢查询

问题:以下查询执行缓慢:

SELECT 
    e.name, 
    d.department_name, 
    SUM(s.amount) as total_sales
FROM 
    employees e
JOIN 
    departments d ON e.department_id = d.id
JOIN 
    sales s ON e.id = s.employee_id
WHERE 
    s.sale_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY 
    e.name, d.department_name
ORDER BY 
    total_sales DESC;

分析步骤

  1. 查看查询计划

    EXPLAIN ANALYZE SELECT ...;
    
  2. 识别瓶颈

    • 发现sales表使用了顺序扫描
    • 连接操作消耗了大量时间
    • 排序操作消耗了大量内存
  3. 优化措施

    • 为sales表的employee_id和sale_date列添加索引
    • 为连接列添加索引
    • 调整work_mem参数
  4. 实施优化

    -- 为sales表添加索引
    CREATE INDEX idx_sales_employee_id_sale_date ON sales (employee_id, sale_date);
    
    -- 调整work_mem参数
    SET work_mem = '128MB';
    
  5. 验证优化效果

    • 再次执行EXPLAIN ANALYZE
    • 比较优化前后的执行时间和成本
    • 确认性能提升

7.2 常见查询优化模式

  1. 索引覆盖:使用覆盖索引避免回表
  2. 索引下推:将过滤条件下推到索引扫描阶段
  3. 哈希聚合:使用哈希聚合替代排序聚合
  4. 物化视图:预计算常用的聚合查询
  5. 分区表:将大表划分为小表,提高查询速度
  6. 并行查询:使用并行查询提高查询速度

八、查询计划的高级特性

8.1 并行查询计划

并行查询计划包含Gather或Gather Merge节点,表示使用了并行查询:

Gather  (cost=1000.00..1120.00 rows=1000 width=100)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Seq Scan on employees  (cost=0.00..120.00 rows=500 width=100)
        Filter: (department = 'IT'::text)

8.2 分区表查询计划

分区表查询计划包含Append节点,表示扫描了多个分区:

Append  (cost=0.00..200.00 rows=1000 width=100)
  ->  Seq Scan on sales_2023_01  (cost=0.00..50.00 rows=250 width=100)
        Filter: (sale_date BETWEEN '2023-01-01' AND '2023-12-31')
  ->  Seq Scan on sales_2023_02  (cost=0.00..50.00 rows=250 width=100)
        Filter: (sale_date BETWEEN '2023-01-01' AND '2023-12-31')
  ->  Seq Scan on sales_2023_03  (cost=0.00..50.00 rows=250 width=100)
        Filter: (sale_date BETWEEN '2023-01-01' AND '2023-12-31')
  ->  Seq Scan on sales_2023_04  (cost=0.00..50.00 rows=250 width=100)
        Filter: (sale_date BETWEEN '2023-01-01' AND '2023-12-31')

8.3 递归查询计划

递归查询计划包含Recursive Union节点,表示使用了递归查询:

Recursive Union  (cost=0.00..1000.00 rows=1000 width=100)
  ->  Seq Scan on employees  (cost=0.00..50.00 rows=10 width=100)
        Filter: (manager_id IS NULL)
  ->  Hash Join  (cost=0.55..90.00 rows=99 width=100)
        Hash Cond: (e.manager_id = recursive_cte.id)
        ->  Seq Scan on employees e  (cost=0.00..50.00 rows=1000 width=100)
        ->  Hash  (cost=0.22..0.22 rows=20 width=4)
              ->  WorkTable Scan on recursive_cte  (cost=0.00..0.22 rows=20 width=4)

九、PostgreSQL vs SQL Server 2019+ vs MySQL 8.0+:查询计划分析对比

在本节中,我们将PostgreSQL 18的查询计划分析与SQL Server 2019+和MySQL 8.0+进行对比,分析它们在查询优化器、查询计划生成和分析工具方面的差异。

9.1 查询优化器对比

特性 PostgreSQL 18 SQL Server 2019+ MySQL 8.0+
优化器类型 基于成本的优化器(CBO) 基于成本的优化器(CBO) 基于成本的优化器(CBO)
统计信息收集 自动收集,可手动触发 自动收集,可手动触发 自动收集,可手动触发
统计信息类型 直方图、高频值、相关性统计 直方图、采样统计、列统计 直方图、索引统计、表统计
并行查询支持 支持,动态并行度调整 支持,自适应并行度 有限支持,最高8个线程
查询计划缓存 支持,计划缓存机制 支持,执行计划缓存 支持,查询缓存(默认关闭)
优化器扩展性 支持扩展,可添加自定义优化规则 有限扩展能力 有限扩展能力

9.2 查询计划生成对比

特性 PostgreSQL 18 SQL Server 2019+ MySQL 8.0+
计划生成方法 动态规划,遗传算法 动态规划,启发式算法 基于成本的贪心算法
计划类型 串行计划、并行计划 串行计划、并行计划、批处理计划 串行计划、有限并行计划
连接算法 Nested Loop、Hash Join、Merge Join Nested Loop、Hash Join、Merge Join Nested Loop、Hash Join、Merge Join
扫描算法 Seq Scan、Index Scan、Index Only Scan、Bitmap Scan 多种扫描类型,包括聚集索引扫描、非聚集索引扫描等 Seq Scan、Index Scan、Index Only Scan
聚合算法 GroupAggregate、HashAggregate 多种聚合算法 Group By、Hash Aggregate
排序算法 快速排序、外部排序、top-N heapsort 多种排序算法,包括合并排序等 快速排序、外部排序

9.3 查询计划查看工具对比

特性 PostgreSQL 18 SQL Server 2019+ MySQL 8.0+
命令行工具 EXPLAIN、EXPLAIN ANALYZE SHOWPLAN、STATISTICS IO/TIME、Actual Execution Plan EXPLAIN、EXPLAIN ANALYZE
图形化工具 pgAdmin SQL Server Management Studio (SSMS) MySQL Workbench
在线工具 explain.depesz.com 无官方在线工具 无官方在线工具
计划格式 TEXT、JSON、XML、YAML 文本、XML、图形化 TEXT、JSON
详细程度 支持不同详细级别 支持不同详细级别 支持基本和详细计划
实际执行信息 支持ANALYZE选项 支持Actual Execution Plan 支持ANALYZE选项

9.4 查询计划分析对比

特性 PostgreSQL 18 SQL Server 2019+ MySQL 8.0+
计划可读性 良好,层次分明 优秀,图形化展示清晰 良好,易于理解
成本估算准确性 良好,基于统计信息 优秀,企业级优化 良好,适合中小规模数据
实际执行与估算对比 支持,ANALYZE选项 支持,Actual Execution Plan 支持,ANALYZE选项
性能瓶颈识别 支持,通过成本和时间分析 优秀,图形化高亮瓶颈 支持,通过成本和时间分析
优化建议 有限,需手动分析 优秀,内置优化建议 有限,需手动分析
大规模数据支持 优秀,适合TB级数据 优秀,适合PB级数据 良好,适合GB级数据

9.5 查询优化工具对比

特性 PostgreSQL 18 SQL Server 2019+ MySQL 8.0+
慢查询分析 pg_stat_statements Query Store、Extended Events Slow Query Log、Performance Schema
自动计划捕获 auto_explain Query Store Slow Query Log
计划比较 需手动比较 Query Store支持计划比较 需手动比较
索引建议 有限,需手动分析 优秀,Missing Index建议 有限,EXPLAIN输出提示
性能监控 pg_stat_activity、pg_stat_progress_* Dynamic Management Views (DMVs) Performance Schema、Sys Schema
可视化工具 pgAdmin、pgBadger SSMS、Azure Data Studio MySQL Workbench、Percona Monitoring and Management

十、查询计划分析工具

10.1 pgAdmin

pgAdmin提供了可视化的查询计划分析工具,可以:

  • 以图形方式显示查询计划
  • 显示每个节点的成本和执行时间
  • 提供优化建议

10.2 explain.depesz.com

explain.depesz.com是一个在线工具,可以:

  • 格式化查询计划
  • 高亮显示性能瓶颈
  • 提供优化建议
  • 比较不同查询计划

10.3 pgBadger

pgBadger是一个日志分析工具,可以:

  • 分析PostgreSQL日志
  • 生成查询性能报告
  • 识别慢查询
  • 提供优化建议

10.4 auto_explain

auto_explain是PostgreSQL的一个扩展,可以:

  • 自动记录慢查询的查询计划
  • 配置记录条件(执行时间、行数等)
  • 帮助识别生产环境中的慢查询
-- 安装扩展
CREATE EXTENSION auto_explain;

-- 配置auto_explain
ALTER SYSTEM SET auto_explain.log_min_duration = '100ms';
ALTER SYSTEM SET auto_explain.log_analyze = on;
ALTER SYSTEM SET auto_explain.log_buffers = on;
ALTER SYSTEM SET auto_explain.log_format = 'json';
ALTER SYSTEM SET auto_explain.log_nested_statements = on;
ALTER SYSTEM SET auto_explain.log_timing = on;
ALTER SYSTEM SET auto_explain.sample_rate = 1.0;

-- 重启PostgreSQL使配置生效
-- sudo systemctl restart postgresql-18

十一、总结

查询计划分析是PostgreSQL性能优化的重要组成部分。通过分析查询计划,可以识别性能瓶颈,采取相应的优化措施,提高查询性能。

通过本章的学习,你应该已经掌握了:

  1. 查询计划的基本概念和组成部分
  2. 如何使用EXPLAIN命令查看和分析查询计划
  3. 如何解读查询计划中的关键信息
  4. 如何识别查询计划中的性能瓶颈
  5. 查询计划优化的基本技巧
  6. 如何使用pg_stat_statements和其他工具分析慢查询
  7. 查询优化器的配置参数

在实际应用中,查询计划分析是一个迭代过程,需要不断尝试和调整,才能找到最优的查询计划。通过掌握查询计划分析的技巧,可以显著提高PostgreSQL数据库的查询性能。

十二、思考与练习

  1. 什么是查询计划?查询计划的重要性是什么?
  2. 如何查看查询计划?EXPLAIN命令的常用选项有哪些?
  3. 查询计划的基本结构是什么?包含哪些关键信息?
  4. 常见的查询计划节点有哪些?各适用于什么场景?
  5. 如何解读查询计划?如何识别性能瓶颈?
  6. 查询计划优化的基本技巧有哪些?
  7. 如何使用pg_stat_statements分析慢查询?
  8. 如何配置查询优化器的参数?
  9. 常见的查询优化模式有哪些?
  10. 如何使用查询计划分析工具?
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐