PostgreSQL 18 从新手到大师:实战指南 - 5.2 查询优化器原理
一、查询处理概述
PostgreSQL查询处理是一个复杂的过程,涉及多个阶段的协同工作。从用户提交SQL语句到返回查询结果,PostgreSQL需要经过解析、分析、优化、执行等多个步骤。查询优化器(Query Optimizer)是其中最核心、最复杂的组件之一,它负责为SQL查询生成最优的执行计划。
1.1 查询处理流程
PostgreSQL查询处理流程主要包括以下几个阶段:
- 解析(Parsing):将SQL文本转换为解析树(Parse Tree)
- 分析(Analysis):将解析树转换为查询树(Query Tree)
- 重写(Rewrite):根据规则重写查询树
- 优化(Optimization):生成最优执行计划
- 执行(Execution):执行执行计划,返回查询结果
1.2 查询优化器的作用
查询优化器的主要作用是:
- 为给定的SQL查询生成多种可能的执行计划
- 评估每个执行计划的成本
- 选择成本最低的执行计划
- 生成可执行的执行计划
二、解析器(Parser)
解析器负责将SQL文本转换为结构化的解析树。这个过程分为词法分析和语法分析两个步骤。
2.1 词法分析
词法分析将SQL文本分解为一个个的词法单元(Token),如关键字、标识符、常量、运算符等。
主要文件:
src/backend/parser/scanner.l:词法分析器(使用flex生成)
词法分析示例:
SELECT * FROM users WHERE id = 1;
词法分析后生成的词法单元:
SELECT, *, FROM, users, WHERE, id, =, 1, ;
2.2 语法分析
语法分析根据SQL语法规则,将词法单元组合成解析树。
主要文件:
src/backend/parser/gram.y:语法分析器(使用bison生成)
语法分析示例:
将上述词法单元组合成解析树,树的根节点是SELECT语句,子节点包括FROM子句、WHERE子句等。
2.3 解析树结构
解析树是一个表示SQL语句结构的树形数据结构,每个节点代表一个语法结构,如SELECT、FROM、WHERE等。解析树只包含语法信息,不包含语义信息。
三、分析器(Analyzer)
分析器(也称为语义分析器)负责将解析树转换为查询树(Query Tree),并进行语义检查。
3.1 语义检查
语义检查主要包括:
- 检查表和列是否存在
- 检查用户是否有权限访问表和列
- 检查数据类型是否匹配
- 检查函数和操作符是否存在且可用
3.2 查询树生成
查询树是一个表示查询逻辑的树形数据结构,包含以下主要组件:
- Query:查询树的根节点
- RangeTblEntry:表示查询中引用的表或子查询
- TargetEntry:表示查询结果中的列
- JoinExpr:表示连接操作
- Expr:表示表达式
- SortClause:表示排序条件
- GroupClause:表示分组条件
- HavingClause:表示HAVING条件
3.3 主要文件
src/backend/parser/analyze.c:分析器主文件src/backend/parser/parse_relation.c:表和列的处理src/backend/parser/parse_expr.c:表达式处理src/backend/parser/parse_clause.c:子句处理
四、查询重写器(Query Rewriter)
查询重写器负责根据规则对查询树进行重写,生成新的查询树。
4.1 重写规则
PostgreSQL支持多种重写规则,包括:
- 视图重写:将对视图的查询重写为对基表的查询
- 规则系统:用户定义的重写规则
- 内联视图:将子查询转换为连接操作
- 常量折叠:将常量表达式提前计算
- 谓词下推:将过滤条件推到子查询或连接操作中
4.2 视图重写
当查询引用视图时,查询重写器会将视图定义替换到查询中,生成对基表的查询。
示例:
-- 创建视图
CREATE VIEW active_users AS
SELECT id, name, email
FROM users
WHERE active = true;
-- 查询视图
SELECT * FROM active_users WHERE id > 100;
重写后:
SELECT id, name, email
FROM users
WHERE active = true AND id > 100;
4.3 规则系统
PostgreSQL支持用户定义的重写规则,可以通过CREATE RULE语句创建。
示例:
-- 创建规则
CREATE RULE users_insert_rule AS
ON INSERT TO users
DO ALSO
INSERT INTO user_audit (action, user_id, timestamp)
VALUES ('insert', NEW.id, NOW());
当执行INSERT INTO users语句时,查询重写器会根据规则添加对user_audit表的插入操作。
4.4 主要文件
src/backend/rewrite/rewriteHandler.c:查询重写器主文件src/backend/rewrite/rewriteManip.c:查询树操作src/backend/rewrite/viewrule.c:视图重写
五、查询优化器核心:规划器(Planner)
规划器是查询优化器的核心组件,负责生成最优执行计划。规划器采用基于成本的优化(Cost-Based Optimization,CBO)方法,评估每个可能的执行计划的成本,并选择成本最低的执行计划。
5.1 执行计划的表示
执行计划是一个树形结构,每个节点代表一个执行操作,如扫描表、连接、排序、聚合等。
主要执行节点类型:
-
扫描节点:
- SeqScan:顺序扫描表
- IndexScan:索引扫描
- IndexOnlyScan:仅索引扫描
- BitmapHeapScan:位图堆扫描
- BitmapIndexScan:位图索引扫描
-
连接节点:
- NestLoop:嵌套循环连接
- HashJoin:哈希连接
- MergeJoin:合并连接
-
聚合节点:
- Agg:聚合操作
- Group:分组操作
-
排序节点:
- Sort:排序操作
-
物化节点:
- Materialize:物化结果
-
** limit/offset节点**:
- Limit:限制结果数量
5.2 成本模型
规划器使用成本模型来评估执行计划的成本。成本模型基于以下因素:
- I/O成本:读取数据块的成本
- CPU成本:处理数据行的成本
- 内存成本:使用内存的成本
- 网络成本:数据传输的成本
主要成本参数:
seq_page_cost:顺序读取一个数据块的成本(默认1.0)random_page_cost:随机读取一个数据块的成本(默认4.0)cpu_tuple_cost:处理一个数据行的CPU成本(默认0.01)cpu_index_tuple_cost:处理一个索引行的CPU成本(默认0.005)cpu_operator_cost:执行一个操作符的CPU成本(默认0.0025)
5.3 规划生成算法
规划器使用动态规划算法生成执行计划,主要包括以下步骤:
- 生成基本关系的访问路径:为每个表生成可能的访问路径,如顺序扫描、索引扫描等
- 生成连接路径:为表之间的连接生成可能的连接路径,包括连接顺序、连接方法等
- 生成聚合和排序路径:为聚合和排序操作生成可能的路径
- 选择最优路径:评估所有可能的路径,选择成本最低的路径
5.4 连接顺序优化
连接顺序是影响查询性能的重要因素。对于n个表的连接,可能的连接顺序数量是n!(阶乘),这是一个非常大的数字。规划器使用动态规划算法来高效地搜索最优连接顺序。
动态规划算法:
- 初始化:为每个表生成访问路径
- 逐步构建:从2个表的连接开始,逐步构建到n个表的连接
- 剪枝:对于每个可能的表组合,只保留成本最低的几个路径
- 最终选择:从n个表的所有可能路径中选择成本最低的路径
5.5 主要文件
src/backend/optimizer/plan/planner.c:规划器主文件src/backend/optimizer/path/:路径生成src/backend/optimizer/plan/:执行计划生成src/backend/optimizer/cost/:成本计算src/backend/optimizer/util/:工具函数
六、执行器(Executor)
执行器负责执行规划器生成的执行计划,并返回查询结果。
6.1 执行模型
PostgreSQL执行器采用火山模型(Volcano Model),也称为迭代模型。在这个模型中,每个执行节点都是一个迭代器,提供以下三个函数:
- ExecInitNode:初始化执行节点
- ExecProcNode:处理一行数据,返回下一行结果
- ExecEndNode:结束执行节点,释放资源
6.2 执行流程
执行器从执行计划的根节点开始,递归地调用每个节点的ExecProcNode函数,直到所有数据处理完成。
执行流程示例:
对于以下执行计划:
Limit
-> Sort
-> SeqScan on users
执行流程:
- 初始化所有节点
- 调用Limit节点的ExecProcNode
- Limit节点调用Sort节点的ExecProcNode
- Sort节点调用SeqScan节点的ExecProcNode
- SeqScan节点扫描表,返回一行数据
- Sort节点对数据进行排序
- Limit节点限制返回的结果数量
- 返回结果给客户端
6.3 主要文件
src/backend/executor/execMain.c:执行器主文件src/backend/executor/execProcnode.c:执行节点处理src/backend/executor/node*.c:各种执行节点的实现
七、查询优化器高级特性
7.1 统计信息
查询优化器依赖于准确的统计信息来评估执行计划的成本。PostgreSQL收集以下统计信息:
-
表统计信息:
- 表的行数
- 数据块数量
- 行平均大小
- 存活行数和死亡行数
-
列统计信息:
- 唯一值数量
- 空值数量
- 直方图
- 高频值
- 相关性(列值与物理存储顺序的相关性)
收集统计信息:
-- 手动收集统计信息
ANALYZE users;
-- 收集特定表和列的统计信息
ANALYZE users (id, name);
主要文件:
src/backend/commands/analyze.c:统计信息收集src/backend/utils/adt/selfuncs.c:选择性估计
7.2 动态规划优化
为了提高动态规划算法的效率,PostgreSQL采用了以下优化:
- 剪枝:对于每个表组合,只保留成本最低的几个路径
- 贪婪算法:对于大型连接(超过12个表),使用贪婪算法来近似最优解
- 遗传算法:对于超大型连接,使用遗传算法来搜索最优解
7.3 参数化查询计划
PostgreSQL支持参数化查询计划,允许相同的查询计划用于不同的参数值。
-- 准备参数化查询
PREPARE get_user(int) AS
SELECT * FROM users WHERE id = $1;
-- 执行参数化查询
EXECUTE get_user(1);
EXECUTE get_user(2);
参数化查询计划可以提高查询性能,减少规划时间。
7.4 并行查询优化
PostgreSQL支持并行查询,允许查询在多个CPU核心上并行执行。
主要并行操作:
- 并行顺序扫描
- 并行索引扫描
- 并行哈希连接
- 并行聚合
配置参数:
max_parallel_workers:最大并行工作进程数max_parallel_workers_per_gather:每个Gather节点的最大并行工作进程数force_parallel_mode:强制并行模式
八、查询优化器调试
8.1 使用EXPLAIN命令
EXPLAIN命令用于查看查询计划,是调试查询优化器的主要工具。
基本用法:
-- 查看查询计划
EXPLAIN SELECT * FROM users WHERE id = 1;
-- 查看查询计划和实际执行统计信息
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;
-- 查看查询计划的详细成本信息
EXPLAIN (ANALYZE, VERBOSE, COSTS, BUFFERS, TIMING) SELECT * FROM users WHERE id = 1;
8.2 启用优化器日志
可以通过修改postgresql.conf文件启用优化器日志:
# 记录查询计划
log_min_duration_statement = 0
# 记录优化器详细信息
log_planner_stats = on
8.3 使用pg_hint_plan扩展
pg_hint_plan是一个PostgreSQL扩展,允许用户通过注释提示优化器选择特定的执行计划。
示例:
-- 提示使用索引扫描
/*+ IndexScan(users) */
SELECT * FROM users WHERE id = 1;
-- 提示使用嵌套循环连接
/*+ NestLoop(users, orders) */
SELECT * FROM users JOIN orders ON users.id = orders.user_id;
九、查询优化器最佳实践
9.1 提供准确的统计信息
- 定期运行ANALYZE命令收集统计信息
- 对于频繁更新的表,增加ANALYZE的频率
- 对于列值分布不均匀的表,调整统计信息收集参数
9.2 合理设计索引
- 为经常用于查询条件的列创建索引
- 为经常用于连接的列创建索引
- 考虑使用复合索引
- 避免创建过多索引
9.3 优化查询语句
- 只选择需要的列,避免使用SELECT *
- 合理使用WHERE条件,减少返回的行数
- 避免在WHERE子句中使用函数或表达式
- 合理使用JOIN操作,避免笛卡尔积
- 考虑使用CTE或临时表来优化复杂查询
9.4 调整优化器参数
- 根据硬件配置调整成本参数(如random_page_cost)
- 根据工作负载调整并行查询参数
- 考虑使用参数化查询
9.5 监控和分析查询性能
- 使用pg_stat_statements扩展监控查询性能
- 分析慢查询,找出性能瓶颈
- 使用EXPLAIN ANALYZE分析查询计划
- 定期审查和优化高频查询
十、实战案例:查询优化分析
10.1 案例描述
假设有以下查询,执行时间较长:
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.name
ORDER BY order_count DESC
LIMIT 10;
10.2 优化分析
-
查看执行计划:
EXPLAIN ANALYZE SELECT u.name, COUNT(o.id) AS order_count FROM users u JOIN orders o ON u.id = o.user_id WHERE u.created_at > '2023-01-01' GROUP BY u.name ORDER BY order_count DESC LIMIT 10; -
分析执行计划:
假设执行计划显示:- users表使用了顺序扫描,因为created_at列没有索引
- orders表使用了顺序扫描,因为user_id列没有索引
- 使用了嵌套循环连接,效率较低
-
优化措施:
- 为users.created_at列创建索引
- 为orders.user_id列创建索引
- 考虑使用哈希连接或合并连接
-
优化后的查询:
-- 创建索引 CREATE INDEX idx_users_created_at ON users(created_at); CREATE INDEX idx_orders_user_id ON orders(user_id); -- 再次执行查询 EXPLAIN ANALYZE SELECT u.name, COUNT(o.id) AS order_count FROM users u JOIN orders o ON u.id = o.user_id WHERE u.created_at > '2023-01-01' GROUP BY u.name ORDER BY order_count DESC LIMIT 10; -
验证优化效果:
比较优化前后的执行时间和成本,确认优化效果。
十一、总结
PostgreSQL查询优化器是一个复杂而精妙的组件,采用基于成本的优化方法,为SQL查询生成最优的执行计划。查询优化器的性能直接影响到PostgreSQL数据库的整体性能。
通过深入理解查询优化器的工作原理,包括解析、分析、重写、优化和执行等阶段,我们可以更好地设计数据库 schema、编写高效的SQL查询、调整优化器参数,从而提高PostgreSQL数据库的性能。
查询优化是一个持续的过程,需要定期监控查询性能、分析慢查询、调整索引和优化查询语句。同时,随着PostgreSQL版本的更新,查询优化器也在不断改进,我们需要关注新版本的优化器特性,充分利用这些特性来提高查询性能。
通过本章节的学习,读者应该掌握PostgreSQL查询优化器的基本原理和工作流程,能够使用EXPLAIN命令分析查询计划,理解成本模型和统计信息的重要性,并能够应用查询优化的最佳实践来提高数据库性能。
更多推荐



所有评论(0)