Skip to content

PostgreSQL 中 SELECT 语句执行流程

SELECT 语句在 PostgreSQL 中执行时,会依次经历解析、重写、规划与执行四个核心阶段,最终将结果返回客户端。整个过程以 exec_simple_query 函数作为统一入口,通过 Portal(门户对象)封装上下文。


一、PostgreSQL 系统架构

在深入执行流程前,先了解 PostgreSQL 的进程架构至关重要:

  • Postmaster(主守护进程):监听端口,接收客户端连接请求,并为每个客户端连接 fork 一个独立的 Backend 进程。
  • Backend(后端进程):处理具体的 SQL 语句,执行四大核心阶段,最终将结果返回客户端[reference:0]。

这种每个连接对应独立进程的“多进程”架构,与 MySQL 的多线程模型形成鲜明对比,在稳定性与扩展性上有其独特优势。


二、四大处理阶段概览

PostgreSQL 处理一条 SQL 命令遵循以下四个步骤,每个步骤都有明确的责任划分[reference:1]:

  1. 解析 (Parse):将 SQL 字符串转换为内存中的解析树。
  2. 分析/重写 (Analyze & Rewrite):进行语义分析并应用规则系统。
  3. 规划 (Plan):生成最优的执行计划。
  4. 执行 (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)
  • 表达式预处理
  • 处理 WINDOWHAVINGGROUP BYORDER 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_classpg_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]:

c
CreatePortal()      // 创建门户对象

PortalDefineQuery() // 关联查询计划

PortalStart()       // 初始化执行状态

PortalRun()         // 实际执行查询

PortalDrop()        // 释放资源
  • Portal 的两种路径:对于 SELECTINSERTUPDATEDELETE 等需要执行计划的 DML 语句,Portal 走执行器路径;对于 CREATEVACUUM 等 DDL 或工具语句,Portal 则走 Process Utility 路径,直接调用相应的处理函数[reference:35][reference:36]。

2. 执行器的三层架构

PostgreSQL 执行器采用经典的火山模型(Volcano Model),这是一种自上而下、逐层拉取的迭代器模式[reference:37][reference:38]。

执行器包含三个核心步骤:

  • ExecutorStart:初始化执行状态,构建状态树。
  • ExecutorRun:反复获取元组,直到条件满足。
  • ExecutorEnd:清理执行状态,释放资源[reference:39]。

3. 计划树与状态树

规划器生成的计划树(Plan Tree)是只读的,包含 SeqScanIndexScanNested LoopHashJoinAggregateSort 等计划节点[reference:40]。执行器初始化时,会构建一个与之对应的状态树(State Tree)。状态树中的每个节点包含指向计划树对应节点的指针,以及执行所需的运行时数据(如扫描位置、哈希表、排序缓存等)。这种设计使得计划树可以被多个执行过程安全复用,而执行期的所有状态修改都只发生在状态树中[reference:41]。

表达式树也会被编译为 ExprState 节点的线性形式,以加速评估过程[reference:42]。

4. 火山模型的执行过程

SELECT * FROM users WHERE age > 18 为例:

  1. 顶层:最外层的计划节点(如 Result 或直接 SeqScan)被调用,返回符合条件的元组。
  2. 拉取驱动:每个非叶子节点在执行时,通过调用子节点的处理函数来“拉取”所需的输入元组[reference:43]。
  3. 流向客户端:对于 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 = ondebug_pretty_print = on 可打印计划树;在 planner 函数设置断点可跟踪优化决策。
  • 执行器跟踪:在 ExecProcNode 设置断点,观察火山模型的逐层调用过程。
  • GUC 参数调优enable_seqscanenable_indexscanenable_hashjoin 等参数可强制禁用特定路径,帮助理解优化器的路径选择逻辑。

总结

PostgreSQL 的 SELECT 语句执行流程是一次从字符串到数据的精密转换:解析器将 SQL 文本转化为结构化的解析树,重写器应用规则进行语义增强,优化器通过代价模型在众多路径中选择最优计划,执行器以火山模型高效拉取和处理数据,最终通过 Portal 协调完成整个查询生命周期。每个阶段既相互独立又紧密协作,共同构成了 PostgreSQL 高效、可靠的查询执行引擎。