土法炼钢兴趣小组的算法知识备份

【PG 内核】查询解析与重写:从 SQL 字符串到 Query Tree

文章导航

分类入口
databasekernel
标签入口
#postgresql#pg-kernel#parser#analyzer#rewriter#gram-y#query-tree#parse-analyze#pg-rewrite#view-expansion#rtable#jointree

目录

查询解析与重写:从 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_parsedebug_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 → sortClause

3.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 节点,区分 colnametablename.colnameschemaname.tablename.colname 三种形式,最终调用 colNameToVar() 将列名映射到具体的 Var 节点(包含 varnovarattnovartype 等信息)。

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_classrelacl 列(存储 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=trueev_action 编码了 SELECT name, salary FROM employees WHERE salary > 100000 的 Query Tree。

当你查询 SELECT * FROM high_salary WHERE name LIKE 'A%' 时,Rewriter 执行的步骤是:

  1. Analyzer 已经把 high_salary 解析为一个 RangeTblEntryrelkind='v')。
  2. Rewriter 在 pg_rewrite 中找到 high_salary_RETURN 规则。
  3. 将规则动作(SELECT name, salary FROM employees WHERE salary > 100000)替换原查询中对 high_salary 的引用。
  4. 原查询的 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;

这个结构体看似庞大,但其中三个字段构成了查询的核心骨架:rtablejointreetargetList

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:目标列表

targetListList *,每个元素是 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 中会显示 commandTypeCMD_INSERT 保持不变,但目标表变成了 employees,所有字段映射通过 NEW 伪关系完成。

6.5 用 nodeToString 在代码中观察

如果你在阅读源码或使用 gdb,可以在关键时刻调用 elog(NOTICE, "%s", nodeToString(node)) 来输出当前树的结构——这正是 debug_print_parse 等参数内部使用的机制。PG 的 outfuncs.c 为每种节点类型实现了序列化函数,输出格式是可读的 Lisp 风格树。


七、关键要点

  1. Parser 只做语法,不做语义gram.y 将 SQL 字符串生成为 RawStmt 语法树,节点里只有字符串名称和字面量值,没有 OID 和类型信息。PG 通过 %nonassoc IDENT 规则允许关键字作为标识符使用,这是 SELECT select FROM select 合法化的原因。

  2. Analyzer 做名称解析、类型推导和权限检查parse_analyze()RawStmt 转换为 Query,核心工作包括:RangeVarGetRelid() 将表名字符串查为 relation OID,colNameToVar() 将列名映射到 Var 节点,exprType() 自底向上推导表达式类型,pg_class_aclmask() 在规划执行前就检查权限。

  3. Rewriter 负责规则触发和视图展开。视图的实质是 _RETURN 规则——CREATE VIEWpg_rewrite 中插入一条 is_instead=true 的规则。ApplyRetrieveRule() 将规则动作(视图定义查询)展开为 RTE_SUBQUERY 类型的 RTE,并将原查询的 WHERE 条件合入展开后的查询树。

  4. Query 有三个核心骨架字段rtable(Range Table,查询涉及的所有关系列表,每个关系用 RangeTblEntry 描述)、jointree(Join 树,FromExpr.fromlist 描述 FROM 子句中的关系和 JOIN 结构,FromExpr.quals 描述 WHERE 条件)、targetList(目标列表,每个 TargetEntry 描述输出列表达式和别名)。

  5. debug_print_parsedebug_print_rewritten 是观察查询编译过程的直接工具,不需要重新编译 PG。它们调用 nodeToString() 将内存中的节点树序列化为文本输出到 server log。这是理解 Planner 行为之前的第一手材料。

下一章:查询规划器——统计信息与代价模型,进入 pg_statistic 内部,理解 ANALYZE 如何收集统计信息,以及代价常量 random_page_costseq_page_cost 如何影响 Planner 的索引扫描 vs 顺序扫描决策。

上一章:VACUUM 与 Freezing


参考资料

源码(PG 17)

官方文档

论文

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-16 · database / kernel

【PG 内核】进程模型与共享内存:Postmaster 如何管理 100 个 Backend

拆解 PostgreSQL 多进程架构的核心:Postmaster 的启动与信号处理、Backend 进程的 fork()→InitPostgres→主循环生命周期、CreateSharedMemoryAndSemaphores() 的共享内存初始化流程、PGPROC/ProcArray/PGXACT 等关键共享内存结构的内存布局,以及 Background Worker 的注册与调度。理解了这个地基,才能理解 PG 为什么用进程而不是线程,以及 max_connections 为什么不能随便调大。

2026-06-16 · database / kernel

【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 可见性判断的共同前提。

2026-06-16 · database / kernel

【PG 内核】MVCC 实现:CLOG、hint bit 与快照可扩展性

在已有 MVCC 文章基础上深入 PG 并发控制的三个基础设施:CLOG 的 SLRU 结构(事务状态位、页面格式、SLRU 淘汰)、hint bit 的写入时机和竞争问题(何时写、谁写、写坏了怎么办)、PG 14 snapshot scalability 优化的具体机制(ProcArrayLock 为什么是瓶颈、xid/xmin 的原子更新如何减少持锁路径),以及事务 ID 回卷(wraparound)的威胁模型。最后与 InnoDB undo log 方案做系统性对比。

2026-06-16 · database / kernel

【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 的实操方法。


By .