PostgreSQL 中 SELECT 语句执行流程
SELECT 语句在 PostgreSQL 中执行时,会依次经历解析、重写、规划与执行四个核心阶段,最终将结果返回客户端。整个过程以 exec_simple_query 函数作为统一入口,通过 Portal(门户对象)封装上下文。
一、PostgreSQL 系统架构
在深入执行流程前,先了解 PostgreSQL 的进程架构至关重要:
- Postmaster(主守护进程):监听端口,接收客户端连接请求,并为每个客户端连接 fork 一个独立的 Backend 进程。
- Backend(后端进程):处理具体的 SQL 语句,执行四大核心阶段,最终将结果返回客户端[reference:0]。
这种每个连接对应独立进程的“多进程”架构,与 MySQL 的多线程模型形成鲜明对比,在稳定性与扩展性上有其独特优势。
二、四大处理阶段概览
PostgreSQL 处理一条 SQL 命令遵循以下四个步骤,每个步骤都有明确的责任划分[reference:1]:
- 解析 (Parse):将 SQL 字符串转换为内存中的解析树。
- 分析/重写 (Analyze & Rewrite):进行语义分析并应用规则系统。
- 规划 (Plan):生成最优的执行计划。
- 执行 (Execute):运行计划并获取数据。
这四个阶段均由 exec_simple_query 函数(位于 src/backend/tcop/postgres.c)串起[reference:2]。
三、第一阶段:词法与语法分析(Parsing)
执行链路的第一步是解析。
调用入口
exec_simple_query 调用 pg_parse_query 进入解析过程,后者调用 raw_parser 函数来完成实际的词法和语法分析[reference:3][reference:4]。
词法分析(Lexical Analysis)
词法分析器定义在 src/backend/parser/scan.l 文件中,由 Flex 工具生成[reference:5][reference:6]。它的职责是将 SQL 字符串拆解成一个个记号(token),例如关键字、标识符、常量、操作符等。
语法分析(Syntax Analysis)
语法分析器定义在 src/backend/parser/gram.y 文件中,由 Bison 工具生成[reference:7][reference:8]。raw_parser 函数调用 base_yyparse 驱动语法分析,将 token 序列按照 SQL 语法规则组装成“原始解析树”(Raw Parse Tree)[reference:9][reference:10]。
关键源文件:
- scan.l (词法规则)
- gram.y (语法规则)
- kwlist.h (关键字列表)
- parser.c (raw_parser 入口)此时生成的解析树尚未进行语义校验,仅表示语法的结构。对于一个 SELECT 语句,其语法树节点类型通常为 T_SelectStmt[reference:11]。
四、第二阶段:语义分析与查询重写(Analyze & Rewrite)
完成解析后,原始的解析树会被送入分析/重写阶段,这一步的核心目标是生成“查询树”(Query Tree)。
语义分析
pg_analyze_and_rewrite 函数对解析树进行语义检查,将 raw parse tree 转换成 query tree[reference:12]。这个转换过程中,会访问数据库系统表来校验表名、列名是否存在,并对权限进行初步检查,因此需要持有相应锁[reference:13]。对于 SELECT 语句,这个过程会把一条语句拆分成多个逻辑部分,例如 FROM 子句、WHERE 条件、GROUP BY 子句、ORDER BY 子句以及 HAVING 子句等[reference:14]。
查询重写
查询重写模块(位于 src/backend/rewrite)会根据存储在系统表 pg_rewrite 中的规则对查询树进行改写[reference:15]。这也是 PostgreSQL 中**视图(View)**的实现基础——视图本质上就是一个存储了查询规则的“语法糖”,在处理时会被重写为其定义的查询[reference:16]。该模块的输出结果是零个或多个新的查询树[reference:17]。
关键源文件:
- analyze.c (语义分析)
- rewriteDefine.c / rewriteHandler.c (查询重写)五、第三阶段:查询规划与优化(Planning)
如果说前两个阶段重在“理解”,那么优化器阶段则负责“决策”。它的输入是一棵查询树,输出是最优的执行计划树。
1. 入口函数
pg_plan_queries 函数调用 planner 函数,后者是优化器的外部接口[reference:18][reference:19]。planner 函数位于 src/backend/optimizer/plan/planner.c 文件中[reference:20][reference:21]。PostgreSQL 也提供了 planner_hook 钩子,允许外部插件接管或扩展规划行为[reference:22]。
2. 逻辑优化
planner 内部调用 subquery_planner 完成具体的优化工作[reference:23]。subquery_planner 的名字有些歧义,实际上它不仅仅是处理子查询的优化,更是整个查询优化的核心入口[reference:24]。它主要负责:
- 提升子链接(SubLink)和子查询(SubQuery)
- 表达式预处理
- 处理
WINDOW、HAVING、GROUP BY、ORDER BY等子句[reference:25]
3. 物理优化(路径生成)
物理优化的核心在 query_planner 函数中,负责生成实际的数据访问路径[reference:26]。
① 扫描路径(Scan Path)
对于单个表,规划器会生成多种可能的扫描路径,并选择成本最低的[reference:27]:
- 顺序扫描(Sequential Scan):总是可行,直接读取整个表。
- 索引扫描(Index Scan):若查询条件与某个索引匹配,则生成该路径。
- 仅索引扫描(Index Only Scan):当索引本身已包含查询所需的所有列时,可避免回表,效率更高。
- 位图扫描(Bitmap Scan):多个索引条件下,通过位图合并定位行。
② 连接路径(Join Path)
当查询涉及多表连接时,规划器会为每个连接对生成三种基本的连接策略[reference:28]:
- 嵌套循环连接(Nested Loop Join):适用于小表驱动大表且有索引的场景。
- 归并连接(Merge Join):要求两表已按连接键排序,适合预排序数据。
- 哈希连接(Hash Join):先构建右表的哈希表,再扫描左表匹配,适合大数据量等值连接。
③ 动态规划与遗传算法
当连接的表数量超过 geqo_threshold(默认为 12)时,优化器会采用遗传查询优化器(GEQO),通过启发式搜索在有限时间内找到较优解,避免穷举搜索带来的指数级开销[reference:29]。
4. 代价估算
PostgreSQL 是一个基于代价的优化器(CBO)。它根据系统表 pg_class 和 pg_statistic 中的统计信息(如表的行数、不同值的个数、列的数据分布等),为每一种路径估算执行成本,最终选择总成本最低的路径[reference:30]。
5. 路径与计划
规划器的搜索过程使用**路径(Path)数据结构,它是计划的简化表示,仅包含规划决策所需的核心信息。当最低成本的路径确定后,会构建完整的计划树(Plan Tree)**并传递给执行器[reference:31]。
关键源文件:
- planner.c (优化器入口)
- subselect.c (子查询处理)
- joinpath.c (连接路径生成)
- costsize.c (代价估算)
- pathnode.c (路径管理)六、第四阶段:查询执行(Execution)
执行器负责执行规划器生成的计划树,并将结果返回给客户端。执行阶段通过 Portal(门户)对象来封装和管理。
1. Portal 生命周期
Portal 是查询执行的载体,代表一个可运行或正在运行的查询的执行状态[reference:32]。它的完整生命周期包括以下几个阶段[reference:33][reference:34]:
CreatePortal() // 创建门户对象
↓
PortalDefineQuery() // 关联查询计划
↓
PortalStart() // 初始化执行状态
↓
PortalRun() // 实际执行查询
↓
PortalDrop() // 释放资源- Portal 的两种路径:对于
SELECT、INSERT、UPDATE、DELETE等需要执行计划的 DML 语句,Portal 走执行器路径;对于CREATE、VACUUM等 DDL 或工具语句,Portal 则走 Process Utility 路径,直接调用相应的处理函数[reference:35][reference:36]。
2. 执行器的三层架构
PostgreSQL 执行器采用经典的火山模型(Volcano Model),这是一种自上而下、逐层拉取的迭代器模式[reference:37][reference:38]。
执行器包含三个核心步骤:
ExecutorStart:初始化执行状态,构建状态树。ExecutorRun:反复获取元组,直到条件满足。ExecutorEnd:清理执行状态,释放资源[reference:39]。
3. 计划树与状态树
规划器生成的计划树(Plan Tree)是只读的,包含 SeqScan、IndexScan、Nested Loop、HashJoin、Aggregate、Sort 等计划节点[reference:40]。执行器初始化时,会构建一个与之对应的状态树(State Tree)。状态树中的每个节点包含指向计划树对应节点的指针,以及执行所需的运行时数据(如扫描位置、哈希表、排序缓存等)。这种设计使得计划树可以被多个执行过程安全复用,而执行期的所有状态修改都只发生在状态树中[reference:41]。
表达式树也会被编译为 ExprState 节点的线性形式,以加速评估过程[reference:42]。
4. 火山模型的执行过程
以 SELECT * FROM users WHERE age > 18 为例:
- 顶层:最外层的计划节点(如
Result或直接SeqScan)被调用,返回符合条件的元组。 - 拉取驱动:每个非叶子节点在执行时,通过调用子节点的处理函数来“拉取”所需的输入元组[reference:43]。
- 流向客户端:对于
SELECT查询,执行器只需将顶层结果元组发送给客户端[reference:44]。
5. 关键执行器节点类型
| 节点类型 | 功能 | 源文件 |
|---|---|---|
| SeqScan | 顺序扫描表 | nodeSeqscan.c |
| IndexScan | 索引扫描 | nodeIndexscan.c |
| BitmapHeapScan | 位图堆扫描 | nodeBitmapHeapscan.c |
| NestLoop | 嵌套循环连接 | nodeNestloop.c |
| MergeJoin | 归并连接 | nodeMergejoin.c |
| HashJoin | 哈希连接 | nodeHashjoin.c |
| Sort | 显式排序 | nodeSort.c |
| Group | 分组操作 | nodeGroup.c |
| Agg | 聚合操作 | nodeAgg.c |
| Limit | 限制结果行数 | nodeLimit.c |
每种节点类型都实现了三个核心函数:ExecInit(初始化)、Exec/ExecProcNode(执行)、ExecEnd(清理)[reference:45]。
关键源文件:
- execMain.c (执行器主入口)
- execProcnode.c (节点分发表)
- pquery.c (Portal 管理)
- portalmem.c (Portal 内存管理)七、执行流程总览
将上述四个阶段串联起来,完整的执行流程如下:
用户发起 SQL
│
▼
┌─────────────────────────────────────────────────────────────┐
│ exec_simple_query (src/backend/tcop/postgres.c) │
│ │ │
│ ├─ start_xact_command // 开启事务 │
│ │ │
│ ├─ pg_parse_query // 词法/语法分析 │
│ │ └─ raw_parser // 生成原始解析树 │
│ │ │
│ ├─ pg_analyze_and_rewrite // 语义分析与重写 │
│ │ ├─ parse_analyze // 生成查询树 │
│ │ └─ QueryRewrite // 应用规则/视图 │
│ │ │
│ ├─ pg_plan_queries // 查询规划 │
│ │ └─ planner // 优化器入口 │
│ │ ├─ subquery_planner // 逻辑优化 │
│ │ └─ query_planner // 物理优化+代价估算 │
│ │ │
│ ├─ CreatePortal // 创建执行门户 │
│ ├─ PortalDefineQuery // 关联计划树 │
│ ├─ PortalStart // 初始化状态树 │
│ ├─ PortalRun // 执行查询 │
│ │ ├─ ExecutorStart // 启动执行器 │
│ │ ├─ ExecutorRun // 火山模型拉取元组 │
│ │ └─ ExecutorEnd // 清理 │
│ │ │
│ └─ finish_xact_command // 提交事务 │
└─────────────────────────────────────────────────────────────┘
│
▼
返回结果给客户端八、扩展阅读与调试建议
- 解析器调试:可在
raw_parser函数设置断点,观察解析树的生成。 - 优化器跟踪:设置
debug_print_plan = on和debug_pretty_print = on可打印计划树;在planner函数设置断点可跟踪优化决策。 - 执行器跟踪:在
ExecProcNode设置断点,观察火山模型的逐层调用过程。 - GUC 参数调优:
enable_seqscan、enable_indexscan、enable_hashjoin等参数可强制禁用特定路径,帮助理解优化器的路径选择逻辑。
总结
PostgreSQL 的 SELECT 语句执行流程是一次从字符串到数据的精密转换:解析器将 SQL 文本转化为结构化的解析树,重写器应用规则进行语义增强,优化器通过代价模型在众多路径中选择最优计划,执行器以火山模型高效拉取和处理数据,最终通过 Portal 协调完成整个查询生命周期。每个阶段既相互独立又紧密协作,共同构成了 PostgreSQL 高效、可靠的查询执行引擎。