查询解析与重写:从 SQL 字符串到 Query Tree
你用 psql 敲下
SELECT name, salary FROM employees WHERE dept_id = 3 ORDER BY salary DESC LIMIT 5;
并按下回车。在这条 SQL 被执行器拿到手里之前,它要经过
Parser、Analyzer、Rewriter
三个阶段的变形——从一段文本变成一棵语义完整的 Query
Tree。
这三个阶段在 PG
源码中分属三个目录:src/backend/parser/(词法和语法分析)、src/backend/parser/(语义分析,同目录不同文件)、src/backend/rewrite/(规则系统和视图展开)。每层有独立的职责边界:Parser
只保证语法正确,Analyzer
负责”这个名字指的是哪个表/列/函数”,Rewriter
负责”视图其实是一条子查询,把它展开”。
本文从源码路径拆解这三个阶段的内部流程,重点解释
Query 结构体在堆满了
rtable、jointree、targetList 之后到底长什么样——以及你能用
debug_print_parse 和
debug_print_rewritten
自己观察每一步的输出。
一、查询编译流水线:三步到位
一条 SQL 从文本到可执行的 Query Tree 的完整路径如下:
flowchart LR
SQL["SQL 字符串"] --> P["Parser<br/>(gram.y + scan.l)"]
P --> RT["RawStmt<br/>(语法树)"]
RT --> A["Analyzer<br/>(parse_analyze)"]
A --> QT["Query<br/>(语义树)"]
QT --> RW["Rewriter<br/>(rewriteQuery)"]
RW --> QT2["Query Tree<br/>(展开后)"]
QT2 --> PL["Planner<br/>(第 10-11 章)"]
```text
三个阶段的职责边界:
| 阶段 | 输入 | 输出 | 做什么 | 源码入口 |
|------|------|------|--------|---------|
| Parser | SQL 字符串 | `List *raw_parsetree_list`(`RawStmt` 链表) | 词法分析 (scan.l) + 语法分析 (gram.y),构建语法树 | `raw_parser()` |
| Analyzer | `RawStmt` 语法树 | `Query *` | 语义分析:表名/列名解析为 OID,类型推导,权限检查 | `parse_analyze()` |
| Rewriter | `Query *` | `List *querytree_list`(`Query` 链表) | 规则系统:视图展开,行级安全策略注入 | `QueryRewrite()` |
关键事实:**Parser 和 Analyzer 的职责绝不重叠**。Parser 不认识 `employees` 是表还是视图——它只知道那是个 identifier,生成一个 `RangeVar` 节点,把名字存进去。Analyzer 才去查 `pg_class`,把 `employees` 解析成 OID 12345。
---
## 二、Parser:从字符串到语法树
### 2.1 词法分析:scan.l
PG 的词法分析器由 flex 从 `src/backend/parser/scan.l` 生成。它把 SQL 字符串切成 token 流——identifier、keyword、数字字面量、字符串字面量、运算符符号等。
一个关键细节:**PG 的关键字不是固定的 token 集合**。`scan.l` 把所有看起来像 identifier 的东西先输出为 `IDENT` token,然后由语法分析器根据上下文判断它是关键字还是普通标识符。这意味着 `SELECT select FROM select;` 在 Parser 看来语法是合法的——`gram.y` 中大部分关键字都用 `%nonassoc IDENT` 规则处理,允许关键字出现在列名和表名的位置。
```c
// src/backend/parser/scan.l(简化逻辑)
// 核心规则:字母开头的 token → IDENT,数字 → ICONST/FCONST,字符串 → SCONST
{identifier} {
// 查 ScanKeywordLookup() 判断是否是 keyword
// 是 keyword → 返回对应的 keyword token
// 不是 keyword → 返回 IDENT
yylval.str = pstrdup(yytext);
return keyword->value;
}
2.2 语法分析:gram.y 的关键规则
src/backend/parser/gram.y 是 PG
源码中最大的单个文件之一(约 17000 行),由 Bison
从语法规则定义生成。它的顶层入口规则是
stmt,分发到各种 SQL 语句类型:
// src/backend/parser/gram.y(简化)
stmt:
AlterStmt | AnalyzeStmt | CreateStmt | DeleteStmt | DropStmt
| InsertStmt | SelectStmt | UpdateStmt | ViewStmt
| ...
;
SelectStmt:
select_no_parens %prec UMINUS
| select_with_parens %prec UMINUS
;
select_no_parens:
simple_select
| select_clause sort_clause
| select_clause sort_clause limit_clause
| ...
;
simple_select:
SELECT opt_all_clause opt_target_list
into_clause from_clause where_clause
group_clause having_clause window_clause
{
SelectStmt *n = makeNode(SelectStmt);
n->targetList = $3;
n->fromClause = $5;
n->whereClause = $6;
n->groupClause = $7;
n->havingClause = $8;
n->windowClause = $9;
$$ = (Node *)n;
}
;
```text
对于一条 `SELECT name, salary FROM employees WHERE dept_id = 3 ORDER BY salary DESC`,语法规则归约后的节点树(简化)为:
```text
RawStmt
└── stmt: SelectStmt
├── targetList: [ResTarget(name="name"), ResTarget(name="salary")]
├── fromClause: [RangeVar(schemaname=NULL, relname="employees")]
├── whereClause: A_Expr
│ ├── name: "="
│ ├── lexpr: ColumnRef(fields=["dept_id"])
│ └── rexpr: A_Const(val=Integer(3))
├── sortClause: [SortBy(node=ColumnRef(name="salary"), sortby_dir=SORTBY_DESC)]
└── limitClause: ...注意 Parser 的输出节点里没有
OID,没有类型信息,没有权限检查——只有语法结构。RangeVar
存的是表名字符串,ColumnRef
存的是列名,A_Const 存的是字面量值。
2.3 raw_parser():解析入口
Parser 的总入口在 raw_parser():
// src/backend/parser/parser.c
List *raw_parser(const char *str)
{
core_yyscan_t yyscanner;
yy_extra_type yyextra;
int yyresult;
// 初始化 flex scanner
yyscanner = scanner_init(str, &yyextra, ...);
// 调用 Bison 生成的解析器
yyresult = base_yyparse(yyscanner);
// yyextra.parsetree 存的是 parsing 过程中积累的 RawStmt 链表
return yyextra.parsetree;
}
```text
`raw_parser()` 在 `exec_simple_query()` 中被调用。返回的 `List` 可能包含多条 `RawStmt`(如果输入是多语句字符串,如 `SELECT 1; SELECT 2;`)。
---
## 三、Analyzer:语义分析
### 3.1 parse_analyze():分发入口
`parse_analyze()` 是语义分析的入口:
```c
// src/backend/parser/analyze.c
Query *parse_analyze(RawStmt *parseTree, const char *sourceText,
Oid *paramTypes, int numParams,
QueryEnvironment *queryEnv)
{
ParseState *pstate = make_parsestate(NULL);
Query *query;
pstate->p_sourcetext = sourceText;
// 分发到对应的 transform 函数
query = transformTopLevelStmt(pstate, parseTree);
// 检查是否有未解决的标识符或类型错误
if (pstate->p_hasAggs || pstate->p_hasWindowFuncs)
parseCheckAggregates(pstate, query);
if (pstate->p_hasSubLinks)
parseCheckSubLinks(pstate, query);
free_parsestate(pstate);
return query;
}transformTopLevelStmt() 根据
RawStmt 的节点类型分发:
// src/backend/parser/analyze.c
Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree)
{
switch (nodeTag(parseTree->stmt))
{
case T_SelectStmt:
return transformSelectStmt(pstate, (SelectStmt *)parseTree->stmt);
case T_InsertStmt:
return transformInsertStmt(pstate, (InsertStmt *)parseTree->stmt);
case T_DeleteStmt:
return transformDeleteStmt(pstate, (DeleteStmt *)parseTree->stmt);
case T_UpdateStmt:
return transformUpdateStmt(pstate, (UpdateStmt *)parseTree->stmt);
// ... CREATE TABLE, ALTER TABLE 等走 transformCreateStmt 等
default:
// 实用命令(DDL, VACUUM 等)走特殊路径
return transformUtilityStmt(pstate, parseTree);
}
}
```text
对于 `SELECT`,核心调用链为:
```text
transformSelectStmt()
→ transformFromClause() // FROM 子句 → rtable
→ transformFromClauseItem()
→ addRangeTableEntry() // 为每个表创建 RangeTblEntry
→ transformWhereClause() // WHERE → qual 表达式
→ transformTargetList() // SELECT 列表 → targetList
→ transformSortClause() // ORDER BY → sortClause3.2 名称解析:表名到 OID
Analyzer 的核心任务之一是把 Parser 输出的字符串名字解析为数据库内部标识符。以表名解析为例,调用链:
transformFromClauseItem()
→ transformTableEntry()
→ addRangeTableEntry()
→ RangeVarGetRelid() // 查 pg_class,字符串 → OID
→ heap_open(relid, AccessShareLock) // 打开 relation
→ get_rel_name(relid) // 确认名字
```text
`RangeVarGetRelid()` 是名称解析的关键函数。它接受 `RangeVar *relation`(存 `schemaname`、`relname`),走 search_path 查找对应的 relation OID,并在内部执行权限检查:
```c
// src/backend/catalog/namespace.c
Oid RangeVarGetRelid(const RangeVar *relation, LOCKMODE lockmode,
bool missing_ok, ...)
{
Oid relId;
if (relation->schemaname)
relId = GetSysCacheOid2(RELNAMENSP,
Anum_pg_class_oid,
CStringGetDatum(relation->relname),
ObjectIdGetDatum(namespaceId));
else
relId = RelnameGetRelid(relation->relname);
// ... 未找到时根据 missing_ok 抛错或返回 InvalidOid
}
对于列名,解析更加复杂。transformColumnRef()
处理 ColumnRef 节点,区分
colname、tablename.colname 和
schemaname.tablename.colname 三种形式,最终调用
colNameToVar() 将列名映射到具体的
Var 节点(包含
varno、varattno、vartype
等信息)。
3.3 类型推导
Analyzer
的另一项任务是给表达式树中的每个节点赋类型。这个过程由
transformExpr() 驱动,对每个表达式节点调用
exprType() 完成自底向上的类型推导:
// src/backend/parser/parse_expr.c
Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind)
{
switch (nodeTag(expr))
{
case T_ColumnRef:
return transformColumnRef(pstate, (ColumnRef *)expr);
case T_A_Const:
return transformAConst(pstate, (A_Const *)expr);
case T_A_Expr:
return transformAExprOp(pstate, (A_Expr *)expr); // 运算符类型推导
case T_FuncCall:
return transformFuncCall(pstate, (FuncCall *)expr); // 函数签名查找
// ...
}
}
```text
对于 `dept_id = 3` 这个表达式,Analyzer 需要:
1. 把 `ColumnRef("dept_id")` 解析为对应列的 `Var`(从该列的 `pg_attribute` 记录中拿到类型 OID 和 attnum)。
2. 把 `A_Const(Integer(3))` 从语法树的整数字面量转换为 `Const` 节点(标注类型为 `int4`)。
3. 查找 `=` 操作符的签名 `int4 = int4 → bool`(通过 `oper()` 函数查 `pg_operator` 系统表)。
4. 构造 `OpExpr` 节点,包含操作符 OID、两个参数的类型、结果类型 `bool`。
### 3.4 权限检查
PostgreSQL 在 Analyzer 阶段执行 DML 操作的权限检查,而不是在执行器阶段。这样做的优势是可以在执行开始前就发现权限不足的查询,避免执行到一半才报错。
权限检查通过 `ExecCheckRTPerms()` 执行,核心逻辑在 `ExecCheckRTEPerms()` 中:
```c
// src/backend/executor/execCheck.c(由 Analyzer 调用)
bool ExecCheckRTPerms(List *rangeTable, bool ereport_on_violation)
{
foreach(l, rangeTable)
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(l);
// 检查类型:SELECT / INSERT / UPDATE / DELETE
aclresult = pg_class_aclmask(rte->relid,
rte->checkAsUser,
requiredPerms, ACLMASK_ALL);
if (aclresult != requiredPerms)
// 报错:permission denied
}
}pg_class_aclmask() 查 pg_class
的 relacl 列(存储
ACL),结合调用者的角色成员关系,确定该角色对目标表是否有
SELECT / INSERT / UPDATE / DELETE 权限。
四、Rewriter:规则系统与视图展开
4.1 pg_rewrite 规则系统
PG 的重写规则存储在 pg_rewrite
系统表中。每当执行 CREATE RULE 时,PG
将规则的事件类型(SELECT/INSERT/UPDATE/DELETE)、条件、动作都编码为
pg_rewrite 中的一行。pg_rewrite
的关键列:
| 列名 | 含义 |
|---|---|
rulename |
规则名称 |
ev_class |
规则所属的表 OID |
ev_type |
触发类型:1=SELECT, 2=UPDATE,
3=INSERT, 4=DELETE |
ev_enabled |
规则是否启用 |
is_instead |
true 表示 INSTEAD OF
规则(拦截原操作) |
ev_qual |
规则条件(WHERE 子句的表达式树) |
ev_action |
规则动作(替换执行的查询树) |
Rewriter 的入口在 QueryRewrite():
// src/backend/rewrite/rewriteHandler.c
List *QueryRewrite(Query *parsetree)
{
// 1. 应用 CMD 规则:查找所有匹配 pg_rewrite 规则的 DML 语句
if (parsetree->commandType != CMD_UTILITY)
results = QueryRewriteOne(parsetree);
// 2. 替换为规则指定的动作
// 对于 instead 规则,原查询被丢弃
// 对于非 instead 规则,原查询和规则动作都执行
// 3. 展开所有 RTE 中的子查询(视图、WITH CTE)
foreach(l, results)
{
query = (Query *) lfirst(l);
FireRIRrules(query, ...); // 递归展开 RangeTblEntry 的子查询
}
return results;
}
```bash
### 4.2 视图展开的实质
视图在 PG 中并非独立的存储对象。`CREATE VIEW foo AS SELECT ...` 等价于创建了一条 `_RETURN` 规则。具体来说:
```sql
CREATE VIEW high_salary AS
SELECT name, salary FROM employees WHERE salary > 100000;这条语句在内部执行了两个操作: 1. 在
pg_class 中创建 high_salary 的
entry(relkind = 'v')。 2. 在
pg_rewrite
中插入一条规则:ev_type='1'(SELECT),is_instead=true,ev_action
编码了
SELECT name, salary FROM employees WHERE salary > 100000
的 Query Tree。
当你查询
SELECT * FROM high_salary WHERE name LIKE 'A%'
时,Rewriter 执行的步骤是:
- Analyzer 已经把
high_salary解析为一个RangeTblEntry(relkind='v')。 - Rewriter 在
pg_rewrite中找到high_salary的_RETURN规则。 - 将规则动作(
SELECT name, salary FROM employees WHERE salary > 100000)替换原查询中对high_salary的引用。 - 原查询的
WHERE name LIKE 'A%'被合入展开后的查询树,变成:SELECT name, salary FROM employees WHERE salary > 100000 AND name LIKE 'A%'。
这就是为什么视图只是”保存的查询”——它在每次引用时都被展开为子查询或直接合入外层查询树。
4.3 rewriteHandler.c 的核心函数
核心调用链:
// src/backend/rewrite/rewriteHandler.c
RewriteQuery()
→ QueryRewrite() // 入口
→ QueryRewriteOne() // 应用匹配的 pg_rewrite 规则
→ RewriteQuery() // 递归:规则动作本身可能触发更多规则
→ FireRIRrules() // 展开子查询(视图、CTE、子查询 RTE)
→ fireRIRrules()
→ ApplyRetrieveRule() // 应用 _RETURN 规则展开视图
ApplyRetrieveRule()
→ 从 pg_rewrite 提取 ev_action
→ 将 ev_action 的目标列表(targetList)和 WHERE 条件合入原查询
→ 将原查询的 WHERE 条件作为额外的 qual 添加到展开后的查询
→ 递归调用 FireRIRrules() 处理嵌套视图
```text
对于 `INSTEAD` 规则(`is_instead=true`),原查询被完全丢弃,只执行规则动作。对于非 `INSTEAD` 规则,规则动作在原查询之前或之后追加执行。
---
## 五、Query 结构体深度拆解
经过 Parser → Analyzer → Rewriter 三步后,一条 SQL 最终变成一个或多个 `Query` 结构体。这是查询编译流水线的核心输出,也是 Planner 的输入。
```c
// src/include/nodes/parsenodes.h
typedef struct Query
{
NodeTag type;
CmdType commandType; // CMD_SELECT / CMD_INSERT / CMD_UPDATE / CMD_DELETE / CMD_UTILITY
QuerySource querySource; // 来源:原始 SQL / 规则展开 / 其他
uint64 queryId; // query identifier(如 pg_stat_statements 中用于聚合)
bool canSetTag; // 这条 Query 是否是"主查询"(影响命令标签和行计数)
Node *utilityStmt; // 对于 CMD_UTILITY,指向 DDL 等实用命令节点
int resultRelation; // 对于 INSERT/UPDATE/DELETE,目标表在 rtable 中的索引
// 对于 SELECT ... FOR UPDATE,也需要锁定结果表
bool hasAggs; // 是否包含聚合函数(决定是否需要分组规划)
bool hasWindowFuncs; // 是否包含窗口函数
bool hasSubLinks; // 是否包含子查询(SubLink 节点)
bool hasDistinctOn; // 是否有 DISTINCT ON
bool hasRecursive; // 是否有递归 CTE
List *cteList; // WITH 子句定义的 CTE 列表
List *rtable; // 范围表(Range Table):查询涉及的所有关系
FromExpr *jointree; // Join 树:FROM 和 WHERE 的组合表示
List *targetList; // 目标列表:SELECT 的输出列
OnConflictExpr *onConflict; // INSERT ... ON CONFLICT 的处理规范
List *groupClause; // GROUP BY 子句
List *groupDistinct; // GROUP BY 是否带有 DISTINCT
List *groupingSets; // GROUPING SETS
Node *havingQual; // HAVING 子句
List *windowClause; // WINDOW 子句定义
List *distinctClause; // SELECT DISTINCT 的目标列表
List *sortClause; // ORDER BY 子句
Node *limitOffset; // LIMIT 偏移量
Node *limitCount; // LIMIT 行数
List *rowMarks; // 行级锁标记(FOR UPDATE / FOR SHARE / FOR KEY SHARE)
Node *setOperations; // UNION / INTERSECT / EXCEPT 的集合操作树
List *constraintDeps; // 约束依赖(PL/pgSQL 使用)
// PG 14+:合并聚合、窗口函数和子查询的标记
bool hasRowSecurity; // 是否启用了行级安全策略
bool hasForUpdate; // 是否有 FOR UPDATE 子句
} Query;这个结构体看似庞大,但其中三个字段构成了查询的核心骨架:rtable、jointree、targetList。
5.1 rtable:范围表
rtable 是一个
List *,每个元素是
RangeTblEntry(RTE)。RTE
描述查询涉及的一个”关系”,可以是普通表、子查询、视图、CTE、函数调用、JOIN
结果等:
// src/include/nodes/parsenodes.h
typedef struct RangeTblEntry
{
NodeTag type;
RTEKind rtekind; // RTE_RELATION / RTE_SUBQUERY / RTE_JOIN
// RTE_FUNCTION / RTE_VALUES / RTE_CTE / ...
Oid relid; // 关系 OID(RTE_RELATION 时有效)
char relkind; // 'r'=普通表, 'v'=视图, 'm'=物化视图, 'f'=外部表
int rellockmode; // 锁模式(AccessShareLock 等)
Query *subquery; // RTE_SUBQUERY 时,指向子查询的 Query 树
bool security_barrier; // 安全屏障视图标志
// JOIN RTE 相关
JoinType jointype;
int joinmergedcols;
List *joinaliasvars;
List *joinleftcols;
List *joinrightcols;
// 函数 RTE 相关
List *functions; // RangeTblFunction 链表
// 别名
Alias *alias; // 用户指定的表别名(如 FROM employees AS e)
Alias *eref; // 系统内部引用名
bool lateral; // 是否标记为 LATERAL
// 安全性
bool inh; // 是否包含继承子表
Oid userid; // 检查权限的用户 ID
int securityQuals; // 行级安全策略的 security_barrier 标记
} RangeTblEntry;
```text
对于查询 `SELECT e.name, d.dept_name FROM employees e JOIN departments d ON e.dept_id = d.id WHERE e.salary > 100000`,`rtable` 包含至少两个 `RangeTblEntry`:
- RTE[1]: `employees` (relid=employees表的OID, alias={aliasname="e"}, rtekind=RTE_RELATION)
- RTE[2]: `departments` (relid=departments表的OID, alias={aliasname="d"}, rtekind=RTE_RELATION)
如果在 Rewriter 阶段将视图展开,可能存在 RTE_SUBQUERY 类型的 RTE,指向展开后的子查询。
### 5.2 jointree:Join 树
`jointree` 是 `FromExpr *` 类型,它是 FROM 子句和 WHERE 子句的组合表示:
```c
// src/include/nodes/parsenodes.h
typedef struct FromExpr
{
NodeTag type;
List *fromlist; // FROM 子句中的关系列表(指向 rtable 的索引节点)
Node *quals; // WHERE 子句的表达式树
} FromExpr;fromlist 中的每个节点是一个
RangeTblRef(指向 rtable 中某个
RTE 的索引号)或 JoinExpr(显式 JOIN
的表示):
typedef struct JoinExpr
{
NodeTag type;
JoinType jointype; // JOIN_INNER / JOIN_LEFT / JOIN_RIGHT / JOIN_FULL
Node *larg; // 左子节点(RangeTblRef 或 JoinExpr)
Node *rarg; // 右子节点
List *usingClause; // USING(...) 子句
Node *quals; // ON 子句的表达式
Alias *alias; // JOIN 结果的别名
int rtindex; // 在 rtable 中的索引
} JoinExpr;
```text
对于上面的查询,`jointree` 的结构(简化)为:
```text
FromExpr
├── fromlist:
│ └── JoinExpr (INNER)
│ ├── larg: RangeTblRef(rtindex=1) // employees
│ ├── rarg: RangeTblRef(rtindex=2) // departments
│ └── quals: OpExpr("=", Var(1,dept_id), Var(2,id))
└── quals: OpExpr(">", Var(1,salary), Const(100000))值得注意:隐式
JOIN(FROM a, b WHERE a.x = b.y)和显式
JOIN(FROM a JOIN b ON a.x = b.y)在 Analyzer
处理之后,jointree 结构可能不同——隐式 JOIN 的条件会放在
FromExpr.quals 中,而显式 JOIN 的条件放在
JoinExpr.quals 中。这会影响 Planner 的 Join
顺序搜索行为。
5.3 targetList:目标列表
targetList 是
List *,每个元素是
TargetEntry,表示 SELECT 输出的每个列:
typedef struct TargetEntry
{
Expr xpr;
Expr *expr; // 该列的表达式(Var / OpExpr / FuncExpr / Const 等)
AttrNumber resno; // 结果列在输出中的序号(从 1 开始)
char *resname; // 列名(如果用户指定了别名则为别名)
Index ressortgroupref; // ORDER BY 和 GROUP BY 使用的引用编号
Oid resorigtbl; // 原始表 OID
AttrNumber resorigcol; // 原始列号
bool resjunk; // 是否为"垃圾"列(不输出给客户端,但用于内部计算)
} TargetEntry;
```text
对于 `SELECT e.name, e.salary * 1.1 AS adjusted FROM employees e`,`targetList` 包含两个 `TargetEntry`:
- TE(resno=1, resname="name"): expr = `Var(varno=1, varattno=name列的attnum, vartype=name列的类型)`, resjunk=false
- TE(resno=2, resname="adjusted"): expr = `OpExpr("*", Var(1,salary), Const(1.1))`, resjunk=false
Resjunk 列(`resjunk=true`)是优化器在执行阶段需要的中间计算列,但不返回给客户端。例如 `ORDER BY salary` 如果 `salary` 不在 SELECT 列表中,Optimizer 可能会创建一个 junk TargetEntry 来计算 `salary`,供排序使用。
---
## 六、实验:观察每一步的输出
### 6.1 启用 debug 输出
PG 提供了几个 developer-level 的 GUC 参数,让你可以在不重新编译的情况下观察 parse tree 和 rewritten tree:
```sql
-- 输出 parse tree(Analyzer 处理之后的 Query Tree)
SET debug_print_parse = on;
-- 输出 rewritten tree(Rewriter 处理之后的 Query Tree)
SET debug_print_rewritten = on;
-- 输出 planner 选择的 plan tree
SET debug_print_plan = on;
-- 单次执行时开启(避免全局日志污染)
-- 配合 client_min_messages 使用
SET client_min_messages = NOTICE;这些参数需要以超级用户身份设置,输出会发送到 server log。
6.2 观察 parse tree
创建实验表和视图:
CREATE TABLE employees (
id int PRIMARY KEY,
name text,
salary numeric,
dept_id int
);
INSERT INTO employees VALUES
(1, 'Alice', 120000, 1),
(2, 'Bob', 85000, 2),
(3, 'Charlie', 95000, 1);
CREATE VIEW high_salary AS
SELECT name, salary FROM employees WHERE salary > 90000;
```text
在 psql 中执行(需要超级用户):
```sql
SET debug_print_parse = on;
SELECT * FROM high_salary WHERE name LIKE 'A%';server log 中输出的 parse tree(经删减,关键字段)大致为:
QUERY:
commandType: CMD_SELECT
querySource: QSRC_ORIGINAL
rtable:
RTE[1]: rtekind=RTE_RELATION, relid=high_salary视图的OID, alias=high_salary
jointree:
FromExpr:
fromlist: [RangeTblRef(rtindex=1)]
quals: OpExpr("~~", Var(name列), Const('A%')) -- LIKE 'A%'
targetList:
TargetEntry(resno=1, resname="name", expr=Var(name列), resjunk=false)
TargetEntry(resno=2, resname="salary", expr=Var(salary列), resjunk=false)
```text
此时视图还没有被展开——`RTE[1]` 的 `rtekind` 仍然是 `RTE_RELATION`,指向 `high_salary` 这个 `relkind='v'` 的关系。
### 6.3 观察 rewritten tree
```sql
-- 同时开启 parse 和 rewritten 输出
SET debug_print_parse = on;
SET debug_print_rewritten = on;
SELECT * FROM high_salary WHERE name LIKE 'A%';
server log 中在 parse tree 之后,会输出 rewritten tree(经删减):
REWRITTEN QUERY:
commandType: CMD_SELECT
rtable:
RTE[1]: rtekind=RTE_SUBQUERY, alias=high_salary
subquery:
commandType: CMD_SELECT
rtable:
RTE[1]: rtekind=RTE_RELATION, relid=employees表OID
targetList:
TargetEntry(resjunk=false): Var(employees, name)
TargetEntry(resjunk=false): Var(employees, salary)
qual: OpExpr(">", Var(employees, salary), Const(90000))
jointree:
FromExpr:
fromlist: [RangeTblRef(rtindex=1)] -- 指向 RTE_SUBQUERY
quals: OpExpr("~~", Var(name列), Const('A%'))
targetList:
TargetEntry: Var(varno=1, name), Var(varno=1, salary)
```text
可见视图被展开为一个 `RTE_SUBQUERY` 类型的 RTE,子查询中包含了 `where salary > 90000` 的条件。外层查询的 `WHERE name LIKE 'A%'` 保留在 `jointree.quals` 中。
### 6.4 观察 INSERT 的规则触发
PG 允许为表创建 DO INSTEAD 规则来拦截 INSERT,这对实现可更新视图很常见:
```sql
CREATE RULE insert_high_salary AS
ON INSERT TO high_salary
DO INSTEAD
INSERT INTO employees (name, salary, dept_id)
VALUES (NEW.name, NEW.salary, 99);
SET debug_print_parse = on;
SET debug_print_rewritten = on;
INSERT INTO high_salary VALUES ('Dave', 150000);
rewritten tree 中会显示 commandType 从
CMD_INSERT 保持不变,但目标表变成了
employees,所有字段映射通过 NEW
伪关系完成。
6.5 用 nodeToString 在代码中观察
如果你在阅读源码或使用 gdb,可以在关键时刻调用
elog(NOTICE, "%s", nodeToString(node))
来输出当前树的结构——这正是 debug_print_parse
等参数内部使用的机制。PG 的 outfuncs.c
为每种节点类型实现了序列化函数,输出格式是可读的 Lisp
风格树。
七、关键要点
Parser 只做语法,不做语义。
gram.y将 SQL 字符串生成为RawStmt语法树,节点里只有字符串名称和字面量值,没有 OID 和类型信息。PG 通过%nonassoc IDENT规则允许关键字作为标识符使用,这是SELECT select FROM select合法化的原因。Analyzer 做名称解析、类型推导和权限检查。
parse_analyze()将RawStmt转换为Query,核心工作包括:RangeVarGetRelid()将表名字符串查为 relation OID,colNameToVar()将列名映射到Var节点,exprType()自底向上推导表达式类型,pg_class_aclmask()在规划执行前就检查权限。Rewriter 负责规则触发和视图展开。视图的实质是
_RETURN规则——CREATE VIEW在pg_rewrite中插入一条is_instead=true的规则。ApplyRetrieveRule()将规则动作(视图定义查询)展开为RTE_SUBQUERY类型的 RTE,并将原查询的 WHERE 条件合入展开后的查询树。Query 有三个核心骨架字段:
rtable(Range Table,查询涉及的所有关系列表,每个关系用RangeTblEntry描述)、jointree(Join 树,FromExpr.fromlist描述 FROM 子句中的关系和 JOIN 结构,FromExpr.quals描述 WHERE 条件)、targetList(目标列表,每个TargetEntry描述输出列表达式和别名)。debug_print_parse和debug_print_rewritten是观察查询编译过程的直接工具,不需要重新编译 PG。它们调用nodeToString()将内存中的节点树序列化为文本输出到 server log。这是理解 Planner 行为之前的第一手材料。
下一章:查询规划器——统计信息与代价模型,进入
pg_statistic 内部,理解 ANALYZE
如何收集统计信息,以及代价常量 random_page_cost
和 seq_page_cost 如何影响 Planner 的索引扫描 vs
顺序扫描决策。
参考资料
源码(PG 17)
src/backend/parser/parser.c:raw_parser()— 词法和语法分析的入口src/backend/parser/gram.y:Bison 语法规则,约 17000 行,定义所有 SQL 语句的语法结构src/backend/parser/scan.l:flex 词法规则,将 SQL 字符串切分为 token 流src/backend/parser/analyze.c:parse_analyze()、transformTopLevelStmt()— 语义分析入口和分发src/backend/parser/parse_clause.c:transformFromClause()、transformWhereClause()— FROM/WHERE 等子句的语义分析src/backend/parser/parse_expr.c:transformExpr()— 表达式语义分析,类型推导src/backend/parser/parse_relation.c:名称解析,addRangeTableEntry()、colNameToVar()src/backend/parser/parse_target.c:transformTargetList()— SELECT 列表的语义分析src/backend/rewrite/rewriteHandler.c:QueryRewrite()、ApplyRetrieveRule()、FireRIRrules()— 规则系统和视图展开src/backend/rewrite/rewriteDefine.c:DefineRule()—CREATE RULE的实现src/backend/rewrite/rewriteManip.c:查询树操作工具函数(增加/替换/删除条件等)src/backend/catalog/namespace.c:RangeVarGetRelid()— 字符串表名到 OID 的查询src/backend/catalog/aclchk.c:pg_class_aclmask()— 权限检查src/backend/executor/execCheck.c:ExecCheckRTPerms()— RTE 权限检查src/backend/tcop/postgres.c:exec_simple_query()— 查询执行的顶层调用链(展示了 Parser → Analyzer → Rewriter → Planner → Executor 的完整调用顺序)src/include/nodes/parsenodes.h:Query、RangeTblEntry、FromExpr、JoinExpr、TargetEntry等核心结构体定义
官方文档
- PostgreSQL Documentation, Chapter 50: Overview of PostgreSQL Internals — 第 50.1 节:SQL 查询语句的处理路径概述
- PostgreSQL Documentation, Chapter 37: The Rule System —
规则系统的完整说明,包括
_RETURN规则与视图的关系 - PostgreSQL Documentation, Chapter 5.7: Privileges — 权限检查机制
- PostgreSQL Documentation, Chapter 41: PL/pgSQL —
内部查询编译机制(
SPI_prepare调用相同的 Parser-Analyzer-Rewriter 路径)
论文
- Chamberlin, D. D. et al. A History and Evaluation of System R. CACM, 1981. — 查询编译流水线(Parser → Semantic Analysis → Optimization)的学术起源
- Stonebraker, M. & Rowe, L. A. The Design of POSTGRES. SIGMOD 1986. — POSTGRES 初始设计中规则系统的用意
- Stonebraker, M. et al. The Implementation of POSTGRES. IEEE Trans. on Knowledge and Data Engineering, 1990. — 早期 POSTGRES 查询重写的实现方案
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【PG 内核】进程模型与共享内存:Postmaster 如何管理 100 个 Backend
拆解 PostgreSQL 多进程架构的核心:Postmaster 的启动与信号处理、Backend 进程的 fork()→InitPostgres→主循环生命周期、CreateSharedMemoryAndSemaphores() 的共享内存初始化流程、PGPROC/ProcArray/PGXACT 等关键共享内存结构的内存布局,以及 Background Worker 的注册与调度。理解了这个地基,才能理解 PG 为什么用进程而不是线程,以及 max_connections 为什么不能随便调大。
【PG 内核】页面布局与元组格式:PG 如何把一行数据塞进 8KB
拆解 PostgreSQL 的物理存储层:Page 的 8KB 布局(PageHeaderData、ItemId 数组、special space)、HeapTupleHeaderData 的字段语义(xmin/xmax/ctid/t_infomask/t_infomask2)、TOAST 外存机制的压缩阈值与四种策略(PLAIN/EXTENDED/EXTERNAL/MAIN),以及用 pageinspect 扩展直接观察页面字节。理解页面格式是理解 VACUUM、Index Scan、MVCC 可见性判断的共同前提。
【PG 内核】MVCC 实现:CLOG、hint bit 与快照可扩展性
在已有 MVCC 文章基础上深入 PG 并发控制的三个基础设施:CLOG 的 SLRU 结构(事务状态位、页面格式、SLRU 淘汰)、hint bit 的写入时机和竞争问题(何时写、谁写、写坏了怎么办)、PG 14 snapshot scalability 优化的具体机制(ProcArrayLock 为什么是瓶颈、xid/xmin 的原子更新如何减少持锁路径),以及事务 ID 回卷(wraparound)的威胁模型。最后与 InnoDB undo log 方案做系统性对比。
【PG 内核】WAL 内部机制:从事务提交到磁盘刷写
拆解 PostgreSQL WAL 的完整内部机制:XLogInsert() 从分段锁到 WAL Buffer 的插入路径、XLogRecord 的物理布局(Header + Block Headers + Data)、Checkpoint 的两阶段流程与 IO 摊平算法、REDO 恢复的 RMGR 分发、wal_level 三级差异的 WAL 记录对比。运维部分聚焦 checkpoint IO 风暴的根因与 checkpoint_completion_target 的调优陷阱、max_wal_size 设小导致 WAL 段疯狂切换的机制,以及用 pg_waldump 定位问题 WAL record 的实操方法。