当输入 SELECT * FROM users WHERE id = 42;并执行时,这条看似简单的 SQL 语句,实际上会在 PostgreSQL 内部触发一段复杂而精密的处理流程。该过程涉及多个后台进程、精细的内存管理机制,以及数十年数据库优化研究的成果。
查询执行的五个阶段
无论查询复杂与否,在 PostgreSQL 中都会经历同一条基本路径:
解析(Parsing) → 分析(Analysis) → 重写(Rewriting) → 规划(Planning) → 执行(Execution)
SQL 文本从一端进入,查询结果从另一端返回。每一个阶段内部都包含大量细致而关键的处理逻辑。
查询的起点:SQL 发送过程
以示例查询语句为例,从提交时刻开始追踪整个执行过程。应用程序首先与 PostgreSQL 服务器建立连接,随后通过 PostgreSQL 通信协议发送查询请求。
需要重点关注的是:当发送语句 SELECT * FROM users WHERE id = 42;时,PostgreSQL 会原封不动地接收该纯文本格式的语句。无论通过 psql 终端输入、应用程序调用,还是借助 ORM 框架执行,SQL 语句最终都会以文本字符串的形式传递至服务器。
服务器在接收文本后,会进行基础校验,例如字符编码是否合法、语句格式是否完整。随后,正式进入查询处理流程。
第一阶段:解析 ------ 从文本到结构
解析器是查询处理的首个环节,核心任务是将 SQL 文本转换为结构化的解析树(Parse Tree)。
在该阶段,解析器会逐字符读取 SQL 语句,并依据 PostgreSQL 定义的 SQL 语法规则进行匹配和拆解,识别其中的关键字(如 SELECT、FROM、WHERE)、表名、列名、运算符等语法要素。
这一过程类似于语言学中的句法分析,只关注语法结构本身,而不涉及语义含义。
例如,SELECT name FROM users WHERE id = 42;解析完成后,系统可以明确:
- 存在一个 SELECT 子句,包含列引用
name。 - 存在一个 FROM 子句,引用表
users。 - 存在一个 WHERE 子句,包含条件表达式
id = 42。
但此时解析器并不知道 users 表是否真实存在、name 是否为有效列名,也不了解涉及字段的数据类型。这些语义层面的验证工作,将由下一阶段完成。
第二阶段:分析 ------ 语义校验与绑定
分析器在解析树的基础上,构建语义有效的查询树(Query Tree),这是从"语法正确"迈向"语义正确"的关键阶段。
该阶段主要完成以下工作:
- 对象解析 :在系统目录中查找
users表,校验其是否存在;确认name、id是否为合法列。 - 类型检查 :校验
id = 42是否成立,例如id的数据类型是否支持与整数进行比较,对应的比较运算符是否存在。 - 权限校验 :确认当前会话是否具备访问
users表及相关列的 SELECT 权限。 - 语义信息补充:为查询树补充对象标识信息,如表和列对应的 OID、字段类型等。
若任一环节失败(表不存在、字段拼写错误、权限不足等),查询将在此阶段终止并返回错误。
完成该阶段后,查询的含义已被 PostgreSQL 完整理解,接下来进入自动转换处理。
第三阶段:重写 ------ 自动规则转换
重写器在语义有效的查询树基础上,应用一系列自动化转换规则,生成最终待执行的查询结构。常见的转换包括:
- 视图展开 :当查询对象为视图时,重写器会将视图查询转换为对底层基表的查询。例如,若视图
active_users的定义为SELECT * FROM users WHERE status = 'active',则查询SELECT * FROM active_users会被重写为直接查询users表并附加过滤条件status = 'active'。 - 行级安全策略(RLS) :若表定义了安全策略,重写器会自动注入额外的 WHERE 条件以实现访问控制。例如,存在按租户隔离数据的策略时,原查询
SELECT * FROM users WHERE id = 42可能被重写为SELECT * FROM users WHERE id = 42 AND tenant_id = 123。 - 用户自定义规则:通过规则系统定义的查询重写逻辑(在现代应用中相对较少使用)。
对于简单查询,该阶段可能不会发生明显变化;但在包含视图、安全策略的复杂系统中,重写可能对查询结构产生显著影响。
第四阶段:规划 ------ 寻找最优执行路径
规划器负责解决一个核心问题:如何以最低成本执行该查询。
这一阶段是 PostgreSQL 中最复杂、最具智能化特征的部分,涉及多维度决策。
访问路径选择
对于查询中涉及的每张表,规划器需要决定数据的读取方式。例如,是对 users 表执行全表顺序扫描,还是利用 id 字段上的索引直接定位目标数据行。规划器需要决定采用何种方式访问数据:
- 顺序扫描(Sequential Scan)
- 索引扫描(Index Scan / Bitmap Scan)
规划器会综合评估表规模、索引可用性及选择性。有时,即便存在索引,顺序扫描也可能更高效。
连接策略选择
在多表查询中,规划器需同时确定:
- 表的连接顺序
- 每一步连接所采用的算法
连接顺序的影响至关重要。例如,先连接表 A 和表 B,再与表 C 连接,和先连接表 B 和表 C,再与表 A 连接,两种方式的执行效率可能存在巨大差异。规划器会评估多种连接顺序,筛选出最优方案。
针对每个连接操作,PostgreSQL 支持的主要连接算法包括:
- 嵌套循环连接(Nested Loop):适用于小数据集,或当连接操作的其中一方数据量极少的场景。
- 哈希连接(Hash Join):在内存充足的情况下,对中大型数据集的连接操作具有较高效率。
- 归并连接(Merge Join):适用于两个输入数据集均已排序的场景。
不同连接顺序和算法组合,对整体性能影响巨大,规划器会评估多种可能性。
统计信息的作用
所有规划决策高度依赖统计信息。PostgreSQL 通过 ANALYZE 收集并维护表统计数据,包括:
- 表的总行数
- 各字段的不同值数量
- 数据分布情况
这些信息用于估算过滤条件和连接操作的结果规模,是成本评估的基础。统计信息不准确将直接导致规划决策偏差。
成本估算与最终计划
规划器会对多种候选执行计划进行成本估算,综合考虑:
- 磁盘 I/O 成本(从磁盘或缓存中读取数据页)
- CPU 计算成本(数据行处理、条件表达式计算)
- 内存消耗(排序、哈希操作)
最终选择成本最低的方案作为执行计划。当查询涉及大量表连接时,PostgreSQL 会启用遗传查询优化器(Genetic Query Optimizer)以避免组合爆炸。
规划结果可通过 EXPLAIN 查看,例如:
EXPLAIN SELECT name FROM users WHERE id = 42;
第五阶段:执行 ------ 生成结果
执行器依据执行计划逐步获取数据,并向客户端返回结果。PostgreSQL 采用拉取式(Pull-based)执行模型。
在该模型中,上层节点按需向下层节点请求数据,而非由下层主动推送。这种机制具备良好的内存效率,并天然支持 LIMIT 等提前终止操作。
仍以查询 SELECT name FROM users WHERE id = 42 为例,其执行计划的输出结果可能如下:
QUERY PLAN
-----------------------------------------------------------
Index Scan using users_id_idx on users (cost=0.00..8.27 rows=1 width=64)
Filter: (id = 42)
(2 rows)
以示例查询为例,执行流程为:
- 执行计划的顶层节点(负责向客户端返回结果)发起数据行请求。
- 该请求触发索引扫描节点,向其下层节点发起数据请求。
- 索引扫描节点利用 users_id_idx 索引定位满足条件 id = 42 的数据行。
- 从磁盘或缓存中读取目标数据行,并应用过滤条件进行校验。
- 数据结果沿执行计划逆向传递:索引扫描节点 → 客户端。
拉取式模型具有显著的内存效率优势,因为 PostgreSQL 仅在下游节点需要数据时才执行处理操作。同时,该模型也简化了结果集限制与提前终止等功能的实现 ------ 只需停止向上游节点请求数据即可。
执行器逐行处理数据,按照通信协议的要求格式化结果,然后通过网络连接将数据发送至客户端。
所有结果发送完成后,PostgreSQL 会自动执行清理操作:
- 销毁临时内存上下文
- 释放执行过程中获取的各类锁资源
- 删除执行期间生成的临时文件
至此,当前后端处理进程恢复空闲状态,准备接收下一个查询请求。
全流程回顾
以 SELECT name FROM users WHERE id = 42 为例,完整流程如下:
- 查询发送:应用程序建立连接,以纯文本形式发送查询语句。
- 解析:将文本转换为解析树,完成语法结构校验。
- 分析:验证语义合法性,补充元数据信息,生成查询树。
- 重写:执行自动转换操作,如视图展开、安全策略应用。
- 规划:评估访问路径、连接策略,结合统计信息生成最优执行计划。
- 执行:基于拉取式模型执行计划,生成并返回查询结果。
- 清理:释放资源,恢复进程空闲状态。
所有查询均遵循该路径,区别仅在于各阶段的复杂程度。
核心价值
理解 PostgreSQL 查询执行流程,能够带来以下核心价值:
- 编写更高效的查询语句:掌握规划器的工作原理,可针对性地优化查询结构,例如理解部分查询无法使用索引的原因、连接顺序对性能的影响,以及公用表表达式(CTE)与子查询的适用场景差异。
- 精准诊断性能问题:当查询执行缓慢时,可通过 EXPLAIN 命令定位问题环节,判断是规划器选择了低效执行路径、统计信息过期,还是缺少必要的索引。
- 设计更合理的数据库架构:基于查询执行机制的理解,能够优化索引设计、表分区方案以及视图的使用方式。
- 认知数据库底层复杂度:一条简单的 SELECT 语句背后,隐藏着连接管理、内存分配、语法校验、语义分析、规则应用、成本优化以及拉取式执行等一系列复杂机制,而这一切都在后台透明运行。
原文链接:
https://internals-for-interns.com/posts/sql-query-roadtrip-in-postgres/
作者:Jesús Espino