skip to content
End of Dev

静态分析器设计

/ 32 min read

This article is also available in English

Table of Contents

什么是静态分析器

给定一条输入 SQL 语句,以及 Catalog 元数据(和方言/函数注册表上下文),静态分析器会在不连接线上数据库、也不执行查询的前提下,推导出查询结果元数据。

对于每一条可分析的 SQL 语句,输出模型包含:

  • 输出列(名称)
  • 输出数据类型
  • 输出可空性
  • 结果基数
  • 结构语义错误与类型相关错误的诊断信息

分析结果能带来什么

在应用代码中,查询结果通常会映射到手写的结构体或 map。SQL 一旦变化、表结构演进,或者对结果形态的假设有误,这些映射就很容易漂移。

静态分析器通过产出可直接被代码生成器消费的元数据,消除这种漂移:

  • 列名 + 数据类型 定义生成行对象的字段与字段类型(类型安全)。
  • 可空性 定义生成字段是否可选/可空(空值安全)。
  • 基数 定义生成返回形态(单值、可选单值、集合,或保证为空)。

推荐的基数到返回形态映射:

基数区间 生成的 API 形态
────────────────────────────────────────────────────────────────────
ExactlyZero 无值 / 类 unit 返回(查询保证为空)
ExactlyOne 非空单行对象
AtMostOne 可选单行对象
OneOrMore 保证非空的集合(或集合 + 契约)
ZeroOrMore 集合

这种映射把常见运行时失败(接收类型不匹配、可空假设错误、字段映射过期)前移为编译期诊断与生成类型约束。

理论基础

为什么是关系代数?

SQL 是一种声明式语言,其理论基础是关系代数(E.F. Codd 于 1970 年提出的数学框架)。无论 SQL 查询多复杂,都可以分解为一组有限关系算子的组合:选择(σ)、投影(π)、连接(⋈)、聚合(γ)、集合运算(∪、∩、−)等。

每个关系算子对于其输入关系如何变换,都有明确定义的语义规则

  • Schema:输出会出现哪些列
  • 数据类型:每个输出列的类型
  • 可空性:每个输出列是否可能为 NULL
  • 基数:输出可能包含多少行

通过把 SQL 转换为关系代表达式树,再递归应用每个算子的规则,我们可以在不执行查询、也不连接数据库的情况下,推导出完整结果元数据。

这种方法优于临时拼凑的 SQL-AST 定向分析,因为:

  1. 完整性:算子集合是有限且封闭的;覆盖所有算子就等于覆盖所有 SQL
  2. 可组合性:算子天然可组合;嵌套查询、CTE、复杂连接都只是更深的树
  3. 正确性:每个算子的规则来自关系代数理论,而不是从 SQL 语法反推
  4. 可扩展性:新增 SQL 特性,只需映射到已有算子,或新增一个带自身规则的算子

三阶段流水线

SQL 文本 ──► Parse ──► Algebraize ──► Infer ──► ResultSet
│ │ │
sqlparser AST+Catalog RelationalExpr
→ RelationalExpr → Metadata

阶段 1:Parse

SQL 文本被解析为抽象语法树(AST)。该阶段使用外部 SQL 解析器,并支持方言感知。AST 保留 SQL 语句的语法结构。

可用信息:SQL 文本、方言

职责

  • 将 SQL 文本分词并解析为结构化 AST
  • 报告语法错误(非法 SQL)

不负责

  • 语义校验(未知表、类型不匹配等)
  • 访问 Catalog 或函数元数据
  • 语法解析之外的任何转换

阶段 2:Algebraize

把 AST 转换为 RelationalExpr 树,也就是关系代数表示。

可用信息:AST、Catalog(数据库 schema)、函数注册表(函数签名与分类)

职责

  • 基于 Catalog 解析表引用(校验表是否存在)
  • 基于输入关系解析列引用(校验列是否存在、消歧)
  • 基于函数注册表做函数分类(标量 / 聚合 / 窗口)
  • 校验函数元数(参数数量)
  • 校验 SQL 语义:GROUP BY 规则、聚合上下文限制(WHERE 中禁止聚合)、窗口函数上下文限制
  • 推导输出列名(别名、表达式命名)
  • 把投影通配符(*t.*)展开成显式列引用,便于结构规划
  • 按 SQL 逻辑执行顺序自底向上构建 RelationalExpr 树

不负责

  • 数据类型推断(不决定列或表达式是什么类型)
  • 可空性推断(不决定列是否可为 NULL)
  • 基数推断(不决定查询会返回多少行)
  • 函数返回类型推断(不决定函数返回什么类型)
  • 函数参数类型校验(需要类型信息,此时尚不可用)

校验是构建过程内联执行,而不是独立 pass。若构树过程中发现语义错误(例如 WHERE 中使用聚合、未知表),会产出诊断并在可能情况下继续分析。

核心原则:Algebraize 阶段只关注结构正确性:这条 SQL 在结构上是否有效?所有引用能否被解析?该阶段构建树,但不会在树上标注类型与可空性信息。

阶段 3:Infer

对 RelationalExpr 树执行自底向上的递归遍历,在每个节点计算元数据。

可用信息:RelationalExpr 树、Catalog(用于 Scan 节点列类型/可空性、主键、唯一约束、外键)、函数注册表(用于返回类型与可空性推断)

职责

  • 推断全部输出列数据类型(来自 catalog 查询、字面量类型、算子规则、函数返回类型)
  • 推断全部输出列可空性(来自 catalog 约束、算子语义、函数可空规则)
  • 推断基数(来自算子规则、主键/唯一约束分析、limit 分析)
  • 校验函数参数类型(需要 Infer 阶段计算出的类型信息)

不负责

  • 名称解析(所有引用已在 RelationalExpr 树中解析完成)
  • 结构语义校验(默认树结构与名称解析已合法)
  • 修改 RelationalExpr 树(Infer 是只读的)

核心原则:Infer 阶段对 RelationalExpr只读且元数据推导是确定性的。它可以产出类型相关诊断(例如函数参数类型不匹配),但不会做结构校验或名称解析。

infer(node):
for each child of node:
child_metadata = infer(child)
return apply_operator_rules(node, child_metadata...)

关系代表达式树

核心中间表示是关系算子树。每个 SQL 查询都表示为这些算子的组合。

算子目录

类别 算子 记号 说明
──────────────────────────────────────────────────────────────────────
叶子节点 Scan R 从基础表读取全部行
Values {r1,r2,..} 字面量行集(例如 SELECT 1, VALUES ...)
一元算子 Selection σ(R) 按谓词过滤行
Projection π(R) 选择/计算输出列
Aggregation γ(R) 分组并应用聚合函数
Window ω(R) 在分区上计算窗口函数
Distinct δ(R) 去重
Sort τ(R) 按排序键排序
Limit λ(R) 限制行数(LIMIT / OFFSET)
别名 Alias ρ(R) 重命名关系(派生表、CTE)
二元算子 Join R ⋈ S 通过条件组合两侧关系
SetOperation R ∪ S 按集合论组合两侧关系

树结构(伪代码)

RelationalExpr =
// 叶子节点
| Scan(table)
| Values(rows: [[ScalarExpr]])
// 一元算子
| Selection(input: RelationalExpr, condition: ScalarExpr)
| Projection(input: RelationalExpr, columns: [ProjectionColumn])
| Aggregation(input: RelationalExpr, group_by: [ScalarExpr], aggregates: [ProjectionColumn])
| Window(input: RelationalExpr, window_exprs: [ProjectionColumn])
| Distinct(input: RelationalExpr)
| Sort(input: RelationalExpr, keys: [SortKey])
| Limit(input: RelationalExpr, count?: ScalarExpr, offset?: ScalarExpr)
// 别名
| Alias(input: RelationalExpr, name: String)
// 二元算子
| Join(left: RelationalExpr, right: RelationalExpr, kind: JoinKind, condition?: JoinCondition)
| SetOperation(left: RelationalExpr, right: RelationalExpr, op: SetOp, all: bool)

投影列携带可见性:

ProjectionColumn = {
expr: ScalarExpr,
alias?: String,
visibility: Visible | Hidden
}

Hidden 投影列是仅用于规划的内部列(例如按最终输出中不存在的表达式排序)。最终 Projection 会在生成查询 ResultSet 前丢弃隐藏列。

标量表达式树

关系算子处理的是关系(行集合)。而算子内部的表达式(谓词、计算列、排序键)是标量表达式,它们对每一行计算一个值。

ScalarExpr =
// 引用与字面量
| ColumnRef(table?, column) -- 限定名 (t.col) 或非限定名 (col)
| Literal(NULL | Bool | Int | Float | String)
// 运算符
| BinaryOp(left: ScalarExpr, op, right: ScalarExpr) -- +, -, =, <, AND, OR, LIKE, ...
| UnaryOp(op, expr: ScalarExpr) -- NOT, -, +
// 函数调用
| Function(name, args: [ScalarExpr]) -- 标量函数:UPPER, COALESCE, ...
| AggregateCall(name, args: [ScalarExpr], distinct?) -- COUNT, SUM, AVG, ...
| WindowCall(name, args: [ScalarExpr], partition_by, order_by) -- ROW_NUMBER, RANK, ...
// 类型转换
| Cast(expr: ScalarExpr, target_type)
// 谓词
| IsNull(expr: ScalarExpr, negated?)
| InList(expr: ScalarExpr, list: [ScalarExpr], negated?)
| Between(expr: ScalarExpr, low: ScalarExpr, high: ScalarExpr, negated?)
// 条件表达式
| Case(operand?: ScalarExpr, when_clauses: [(condition, result)], else?: ScalarExpr)
// 子查询(在标量上下文中嵌入关系表达式)
| ScalarSubquery(RelationalExpr) -- (SELECT max(x) FROM ...)
| InSubquery(expr: ScalarExpr, RelationalExpr, negated?)
| Exists(RelationalExpr, negated?)
// 通配符(用于 SELECT *)
| Wildcard
| QualifiedWildcard(table)

投影中的 WildcardQualifiedWildcardSELECT *SELECT t.*)会在 Algebraize 的投影构建阶段标准化为显式投影列。Infer 仅处理展开后的表示。

COUNT(*) 作为聚合函数的星号参数单独处理,不会展开为投影列。

关键设计决策:在 ScalarExpr 中表示聚合

聚合函数(COUNT、SUM 等)在 ScalarExpr 中表现为 AggregateCall 节点,因为它们在 SQL 语法上本就嵌入在表达式里(例如 SELECT price * COUNT(*))。但在 Algebraize 阶段,一旦检测到聚合调用,就会在关系树中创建 Aggregation 算子。Aggregation 算子是关系层的分组表示;ScalarExpr 中的 AggregateCall标量层对聚合计算结果的引用。

Algebraize:SQL 到关系代数转换

SQL 的逻辑执行顺序

SQL 有一套明确的逻辑执行顺序,与其书写顺序不同。Algebraize 阶段会按这个逻辑顺序自底向上构建 RelationalExpr 树:

步骤 SQL 子句 关系算子 作用
────────────────────────────────────────────────────────────────────────────────
1st FROM / JOIN Scan, Join, Alias 建立输入关系
2nd WHERE Selection 在分组前过滤行
3rd GROUP BY + aggregates* Aggregation 分组并聚合(*跨 SELECT/HAVING/ORDER BY 检测聚合)
4th HAVING Selection 过滤分组结果
5th Window functions* Window 计算窗口表达式(*跨 SELECT/ORDER BY 检测)
6th SELECT (+ hidden sort) Projection 构建可见输出列及可选隐藏排序列
7th DISTINCT Distinct 去重
8th ORDER BY Sort 对结果排序
9th LIMIT / OFFSET Limit 限制输出行数
10th FINAL OUTPUT Projection 丢弃隐藏列

转换伪代码

algebraize_select(query):
// Step 1: FROM → base relation
expr = build_from(query.from) // Scan, Join, Alias nodes
// 预扫描各子句中的表达式类别
agg_usage = collect_aggregate_usage(query.select, query.having, query.order_by)
win_usage = collect_window_usage(query.select, query.order_by)
// Step 2: WHERE → Selection
if query.where:
validate_no_aggregates(query.where)
validate_no_window_functions(query.where)
expr = Selection(expr, build_scalar(query.where))
// Step 3: GROUP BY + aggregates → Aggregation
if query.group_by or agg_usage.any:
validate_grouping_rules(query)
expr = Aggregation(expr, build_group_by(query), build_projections(query))
// Step 4: HAVING → Selection
if query.having:
expr = Selection(expr, build_scalar(query.having))
// Step 5: Window functions → Window
if win_usage.any:
expr = Window(expr, build_window_exprs(query))
// Step 6.1: 构建可见 SELECT 投影列
// (包括通配符展开:`*`、`t.*`)
visible_cols = build_visible_projection_columns(query.select)
// Step 6.2: ORDER BY 规划块
{
// 1) 解析 ORDER BY 引用(序号 / 别名 / 表达式等价)
order_refs = resolve_order_by_refs(query.order_by, visible_cols)
// 2) 校验 ORDER BY 约束(DISTINCT 限制、别名歧义等)
validate_order_by_constraints(order_refs, query.distinct)
// 3) 在允许时物化隐藏排序列,然后构建排序键
hidden_sort_cols, rewritten_order_refs = materialize_hidden_sort_columns(
order_refs,
query.distinct
)
sort_keys = build_sort_keys(rewritten_order_refs)
}
// Step 6.3: SELECT + 可选隐藏 ORDER BY 表达式 → Projection
expr = Projection(expr, visible_cols + hidden_sort_cols)
// Step 7: DISTINCT → Distinct
if query.distinct:
expr = Distinct(expr)
// Step 8: ORDER BY → Sort
if query.order_by:
expr = Sort(expr, sort_keys)
// Step 9: LIMIT / OFFSET → Limit
if query.limit or query.offset:
expr = Limit(expr, query.limit, query.offset)
// Step 10: 最终输出投影(丢弃隐藏列)
if hidden_sort_cols not empty:
expr = Projection(expr, visible_cols)
return expr

说明:

  • 隐藏排序列允许非 DISTINCT 查询按未输出的表达式排序。
  • 对于 DISTINCT,若 ORDER BY 引用不在可见输出中的表达式,会产出诊断(不允许隐藏排序列兜底)。

ORDER BY 绑定策略

ORDER BY 条目按以下优先级在 SELECT 后投影上解析:

  1. 序号引用ORDER BY 1ORDER BY 2 等):绑定到可见投影位置。
  2. 别名引用:绑定到唯一命名的可见投影别名。
  3. 表达式等价:如果标准化后的标量表达式等价,则绑定到可见投影表达式。
  4. 回退
  • 若不存在 DISTINCT:将该表达式物化为隐藏投影列并绑定。
  • 若存在 DISTINCT:产出诊断。

分布式 ORDER BY 规划流程:

  1. 对每个 ORDER BY 条目,按顺序尝试绑定:序号位置、唯一可见别名、标准化表达式等价。
  2. 绑定成功则生成指向对应可见列的排序键。
  3. 若绑定失败且查询不是 DISTINCT,则为该表达式物化隐藏投影列(按标准化表达式形态去重),并将排序键绑定到该隐藏列。
  4. 若绑定失败且查询是 DISTINCT,则产出诊断。
  5. 返回规划好的隐藏列和排序键。

实现说明:

  • 隐藏列按标准化标量表达式去重,而非按 SQL 文本去重。
  • 隐藏别名使用内部保留前缀(例如 __ord$),不会暴露到最终输出。
  • 若别名绑定存在歧义(可见别名重复),会产出诊断。

示例:SQL 到关系代数树

给定:

SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.name
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC
LIMIT 10

得到的树(自底向上阅读):

Limit(count=10)
└─ Sort(order_count DESC)
└─ Projection(u.name, COUNT(o.id) AS order_count)
└─ Selection(COUNT(o.id) > 5) -- HAVING
└─ Aggregation(group_by=[u.name], aggs=[COUNT(o.id)])
└─ Selection(u.active = true) -- WHERE
└─ Join(LEFT, ON u.id = o.user_id)
├─ Alias("u")
│ └─ Scan(users)
└─ Alias("o")
└─ Scan(orders)

在这个示例中,ORDER BY 使用的是可见别名(order_count),因此不需要隐藏排序列,最终输出投影步骤也可省略。

构建期间的校验

语义校验在构树过程中进行,而不是单独 pass:

  • 未知表/列:在对照 Catalog 解析引用时发现
  • WHERE 中出现聚合:在构建 WHERE 的 Selection 节点时发现
  • WHERE/HAVING 中出现窗口函数:在构建标量表达式时发现
  • 聚合/窗口检测范围:聚合使用跨 SELECTHAVINGORDER BY 收集;窗口使用跨 SELECTORDER BY 收集
  • GROUP BY 下 SELECT 出现未聚合列:在构建 Aggregation/Projection 时发现
  • ORDER BY / DISTINCT 限制:存在 DISTINCT 时,ORDER BY 必须引用可见输出表达式(或其序号/别名等价)
  • ORDER BY 隐藏列规划:不存在 DISTINCT 时,非输出 ORDER BY 表达式会被物化为隐藏投影列,并在最终投影中裁剪
  • ORDER BY 别名歧义:可见别名重复会导致基于别名的 ORDER BY 绑定无效
  • 列引用歧义:未限定列名在多输入关系中可匹配时发现

Infer:算子语义规则

每个关系算子都定义了如何把输入元数据变换为输出元数据的确定性规则。Infer 阶段本质上是一个自底向上的递归遍历,逐节点应用这些规则。

元数据模型

RelationalMetadata = {
columns: [ColumnMetadata], -- 输出 schema
cardinality: Cardinality -- 行数上下界
}
ColumnMetadata = {
table: String?, -- 源表名(用于限定引用)
name: String, -- 列名
data_type: DataType, -- 列的数据类型
nullable: bool -- 列是否可为 NULL
}
Cardinality = {
min: MinRows, -- 下界
max: MaxRows -- 上界
}
MinRows = Zero | One
MaxRows = Zero | One | Many
-- 不变式:min <= max
别名:
ExactlyZero = [Zero, Zero]
ExactlyOne = [One, One]
AtMostOne = [Zero, One]
OneOrMore = [One, Many]
ZeroOrMore = [Zero, Many]

推断伪代码

infer(expr):
match expr:
Scan(table) → infer_scan(table)
Values(rows) → infer_values(rows)
Selection(input, cond) → infer_selection(infer(input), cond)
Projection(input, cols) → infer_projection(infer(input), cols)
Aggregation(input, gb, aggs) → infer_aggregation(infer(input), gb, aggs)
Window(input, exprs) → infer_window(infer(input), exprs)
Distinct(input) → infer_distinct(infer(input))
Sort(input, keys) → infer_sort(infer(input))
Limit(input, count, offset) → infer_limit(infer(input), count, offset)
Alias(input, name) → infer_alias(infer(input), name)
Join(left, right, kind, cond)→ infer_join(infer(left), infer(right), kind, cond)
SetOperation(left, right, op, all)→ infer_set_op(infer(left), infer(right), op, all)

各算子规则

叶子算子

Scan(table)

  • Schema:来自 Catalog 中表定义的列
  • 可空性:来自 Catalog(列的 NOT NULL 约束)
  • 基数:ZeroOrMore(基础表可以有任意行)

Values(rows)

  • Schema:按列综合所有行推断(公共类型/类型提升)
  • 可空性:按列跨所有行推断(nullable = OR(row_i_col_nullable)
  • 基数:空集为 ExactlyZero,一行为 ExactlyOne,多行为 OneOrMore

一元算子

Selection(input, condition) — σ

  • Schema:= input(过滤不改变列)
  • 可空性:= input
  • 基数:取决于条件分析(见“基数分析”章节)

Projection(input, columns) — π

  • Schema:来自投影列表(每列名称和推断类型)
  • 可空性:按表达式逐列推断(见“标量表达式类型推断”)
  • 基数:= input(投影不改变行数)

Aggregation(input, group_by, aggregates) — γ

  • Schema:group-by 列(透传自输入)+ 聚合结果列
  • 可空性:group-by 列保留输入可空性;聚合结果遵循函数专属规则
  • 基数:若 group_by 为空则 ExactlyOne;否则 = input

Window(input, window_exprs) — ω

  • Schema:= input(窗口函数不删除或重排列;它增加计算列)
  • 可空性:透传列保持输入可空性;窗口函数结果遵循函数专属规则
  • 基数:= input(窗口函数不改变行数)

Distinct(input) — δ

  • Schema:= input
  • 可空性:= input
  • 基数:= input(保守处理;去重不会增加行数)

Sort(input, keys) — τ

  • Schema:= input
  • 可空性:= input
  • 基数:= input(排序不改变行数)

Limit(input, count, offset) — λ

  • Schema:= input
  • 可空性:= input
  • 基数:见“Limit 基数规则”章节

Alias(input, name) — ρ

  • Schema:= input,但所有列会被重标记为新表名
  • 可空性:= input
  • 基数:= input

二元算子

Join(left, right, kind, condition) — ⋈

Schema:

  • 所有连接类型都为 left ++ rightJOIN USING 例外,见下文专节)

基数(区间模型):

  • Inner
    • min = One 当且仅当(guaranteed_match_from_left && left.min == One)或(guaranteed_match_from_right && right.min == One);否则 Zero
    • max = upper_mul(left.max, right.max)
    • at_most_one_right_per_left,则 max = upper_min(max, left.max)
    • at_most_one_left_per_right,则 max = upper_min(max, right.max)
  • Left
    • min = left.min
    • 若(right.max == Zeroat_most_one_right_per_left)则 max = left.max,否则 upper_mul(left.max, right.max)
  • Right
    • min = right.min
    • 若(left.max == Zeroat_most_one_left_per_right)则 max = right.max,否则 upper_mul(left.max, right.max)
  • Full
    • min = lower_or(left.min, right.min)
    • left.max == Zero,则 max = right.max
    • right.max == Zero,则 max = left.max
    • 否则 max = Many
  • Cross
    • min = lower_and(left.min, right.min)
    • max = upper_mul(left.max, right.max)

可空性:

  • Inner/Cross:保持两侧原可空性。
  • Left/Right/Full:按外连接可空强制规则处理(见“连接可空性细化”)。

SetOperation(left, right, op, all) — ∪ ∩ −

Schema 与可空性:

  • schema 取自左侧(先完成兼容性检查)
  • Union All / Union(distinct):可空性合并(nullable = left.nullable OR right.nullable
  • Intersect All / Intersect(distinct):可空性合并(nullable = left.nullable OR right.nullable
  • Except All / Except(distinct):可空性继承左侧

基数:

  • Union Allmin = lower_or(left.min, right.min)max = upper_add(left.max, right.max)
  • Union(distinct):下界同上;保守上界 upper_add(left.max, right.max)
  • Intersect Allmin = Zeromax = upper_min(left.max, right.max)
  • Intersect(distinct):min = Zeromax = upper_min(left.max, right.max)
  • Except Allmax = left.max,当 right.max == Zeromin = left.min,否则 Zero
  • Except(distinct):max = left.max,当 right.max == Zeromin = left.min,否则 Zero

基数分析

基数表示查询结果行数的上下界。它对下游代码生成至关重要: 若查询保证最多返回一行,就可以映射成标量而非集合。

基数域

基数用区间表示:

[min, max]
min ∈ {Zero, One}
max ∈ {Zero, One, Many}
约束:min <= max

规范别名:

ExactlyZero = [Zero, Zero]
ExactlyOne = [One, One]
AtMostOne = [Zero, One]
OneOrMore = [One, Many]
ZeroOrMore = [Zero, Many]

基数组合器

drop_lower_bound([m, M]) = [Zero, M]
constrain_at_most_one([m, M]) = [m, min(M, One)]
upper_min(a, b): max 域最小值(Zero < One < Many)
upper_add(a, b): Zero+Zero=Zero, Zero+One=One, One+One=Many, Many+x=Many
upper_mul(a, b): Zero*x=Zero, One*One=One, 其他情况为 Many
lower_or(a, b): 任一侧为 One 则 One,否则 Zero
lower_and(a, b): 两侧都为 One 才是 One,否则 Zero

Selection 基数规则

Selection(WHERE)是基数分析中最关键的算子。通过分析谓词结构,我们可以判断过滤后是否保证最多一行:

条件模式 影响
──────────────────────────────────────────────────────────────────────
WHERE false / WHERE 1=0 → ExactlyZero
WHERE 输入主键全部列都被等值约束 → AtMostOne
WHERE 输入唯一键全部列都被等值约束 → AtMostOne
WHERE 完整输入键 IN (<single tuple>) → AtMostOne
WHERE 完整且已证明 NOT NULL 的输入键被约束为 NULL → ExactlyZero
WHERE key 列 IS NULL 且 key 可空性为 nullable/unknown → 不细化
WHERE c1 = ? AND c2 = ?(AND 覆盖完整输入 PK/UK) → AtMostOne
WHERE c = v AND c = w(v != w) → ExactlyZero
WHERE ... OR ... → 不细化(保守)
其他 → 不细化

分析过程:

  1. 从条件中提取等值约束(column = value)
  2. 提取完整键上的单元组 IN 约束
  3. 在 AND 合取中收集被约束列
  4. 识别矛盾模式(如 c = v AND c = wv != w)以及对已证明非空键的不可能空判断
  5. 检查被约束列是否覆盖当前输入关系中有效的完整主键/唯一约束
  6. 若覆盖,则结果最多一行

input_constraints 表示关系树当前位置下,对当前输入关系依然有效的主键/唯一约束(由 Catalog 约束和关系算子语义共同导出)。

analyze_selection_cardinality(input_cardinality, condition, input_constraints):
if is_always_false(condition):
return ExactlyZero
// Step 1: 矛盾和不可能谓词检查
if has_contradictory_equalities(condition):
return ExactlyZero
if has_full_key_is_null_on_proven_non_nullable_key(condition, input_constraints):
return ExactlyZero
// 保守规则:不跨 OR 析取做细化
if contains_or(condition):
return input_cardinality
// Step 2: 收集来自等值和 single-tuple IN 的键约束
eq_columns = extract_equality_columns(condition) // 被 "=" 约束的列
in_single_tuple_keys = extract_single_tuple_in_keys(condition) // 被 IN(single tuple) 约束的键列
constrained_columns = union(eq_columns, in_single_tuple_keys)
// Step 3: 覆盖完整主键 => 最多一行
if covers_any_primary_key(constrained_columns, input_constraints):
return constrain_at_most_one(input_cardinality)
// Step 4: 覆盖完整唯一键 => 最多一行
if covers_any_unique_key(constrained_columns, input_constraints):
return constrain_at_most_one(input_cardinality)
return input_cardinality // 无法继续细化

Limit 基数规则

LIMIT 0 → ExactlyZero(空结果)
LIMIT 1 → constrain_at_most_one(input)
LIMIT n (n > 1) → input(无有效细化)
OFFSET > 0 → 若 input.max <= One 则 ExactlyZero,否则 drop_lower_bound(input)
LIMIT 1 + OFFSET > 0 → 若 input.max <= One 则 ExactlyZero,否则 AtMostOne

连接可空性细化

基础规则

连接类型 可空性变换
──────────────────────────────────────────────────────────────
Inner 保持两侧原样
Cross 保持两侧原样
Left 保持左侧,右侧强制可空
Right 保持右侧,左侧强制可空
Full 两侧都强制可空

保证匹配优化(严格)

仅当全部条件成立时,才可以跳过外连接的可空强制:

  1. ON 是纯等值连接谓词合取(a.col = b.col)。
  2. 这些等值对覆盖了从保留侧到可空侧的完整外键映射。
  3. 保留侧上的所有外键列都为 NOT NULL
  4. 被引用侧中,这些列对覆盖了完整 PRIMARY KEYUNIQUE 键。
  5. 除了这些等值键谓词外,不存在额外的 ON 谓词。

全部满足时:

  • LEFT JOIN:右侧保持原可空性(不再强制可空)。
  • RIGHT JOIN:左侧对称处理。
  • FULL JOIN:不优化(仍强制两侧可空)。

伪代码:

guaranteed_match_from_left(on, left, right, catalog):
if !is_pure_equi_join_conjunction(on):
return false
// 拒绝除等值键对之外的任何 ON 谓词
if has_extra_on_predicates(on):
return false
pairs = extract_equijoin_pairs(on)
if !covers_full_fk_not_null(left -> right, pairs, catalog):
return false
if !covers_full_unique_or_pk(right, pairs, catalog):
return false
return true

连接基数的伴随谓词

at_most_one_right_per_left(on, left, right, catalog):
if !is_pure_equi_join_conjunction(on):
return false
pairs = extract_equijoin_pairs(on)
return covers_full_unique_or_pk_not_null(right, pairs, catalog)

guaranteed_match_from_rightat_most_one_left_per_right 通过交换 left/right 角色按对称方式定义。

JOIN USING 列合并

JOIN USING(col) 对每个 USING 列对只输出一份列。

合并后输出列的可空性:

  • INNER USING:保守基线 left.nullable OR right.nullable(可在后续等价/约束传播中收紧)
  • LEFT USINGleft.nullable
  • RIGHT USINGright.nullable
  • FULL USINGleft.nullable OR right.nullable

标量表达式类型推断

每个 ScalarExpr 节点都有两个被推断的属性:数据类型可空性。两者都通过表达式树自底向上计算。

按表达式种类的推断规则

表达式 数据类型 可空性
──────────────────────────────────────────────────────────────────────────
ColumnRef(t, c) 来自 catalog/输入 schema 来自 catalog/输入 schema
Literal(NULL) Unknown true
Literal(Bool/Int/...) 来自字面量类型 false
BinaryOp(l, op, r) 来自运算符规则 nullable(l) OR nullable(r)
UnaryOp(NOT, e) Bool nullable(e)
UnaryOp(-, e) type(e) nullable(e)
Function(name, args) 来自函数注册表 来自函数注册表
AggregateCall(name, ..) 来自函数注册表 来自函数注册表
WindowCall(name, ...) 来自函数注册表 函数特定并受 frame 影响
Cast(e, target) target nullable(e)
IsNull(e) Bool false(IS NULL 永不返回 NULL)
InList(e, list) Bool nullable(e) OR list 中任一项可空
InSubquery(e, q) Bool nullable(e) OR 子查询输出表达式可空
Between(e, lo, hi) Bool nullable(e) OR nullable(lo) OR nullable(hi)
Case(...) 见下文 见下文
ScalarSubquery(q) 来自子查询单列类型 true(子查询可能无行)
Exists(q) Bool false
Wildcard / QualifiedWildcard 在 Algebraize 投影构建期间展开

CASE 表达式规则

CASE WHEN c1 THEN r1 WHEN c2 THEN r2 ELSE e END
数据类型: common_type(r1, r2, e) -- 所有分支的类型提升
可空性: nullable(r1) OR nullable(r2) OR nullable(e)
OR(无 ELSE 子句 → 隐式 ELSE NULL → 必然可空)

函数系统

函数是 Algebraize 与 Infer 两个阶段的共享关注点。Algebraize 需要函数分类(标量/聚合/窗口)并做元数校验;Infer 需要确定返回类型与可空性。

函数类别

类别 示例 上下文
────────────────────────────────────────────────────────────────────
Scalar UPPER, LOWER, COALESCE, IF, 任意表达式位置
CONCAT, ABS, ROUND, CAST, ...
Aggregate COUNT, SUM, AVG, MIN, MAX, 仅可在 Aggregation 中,或
GROUP_CONCAT, ARRAY_AGG, ... 配合 OVER(转为窗口)
Window ROW_NUMBER, RANK, DENSE_RANK, 仅可与 OVER 子句一起使用
NTILE, LAG, LEAD, FIRST_VALUE, ...

解析优先级

解析函数名时,优先级如下:

  1. 窗口函数:函数带 OVER 且命中已知窗口函数
  2. 聚合作为窗口:函数带 OVER 且命中已知聚合函数,则分类为窗口
  3. 聚合函数:函数不带 OVER 且命中已知聚合函数,并位于聚合合法上下文(结合签名/上下文检查)
  4. 标量函数:命中已知标量函数
  5. 未知:未识别函数的回退分类

聚合作为窗口

当聚合函数(例如 SUM)与 OVER 子句一起使用时,它会被当作窗口表达式处理。其可空性并非统一总是可空,而是函数特定并受窗口 frame 影响。

安全基线规则:

  • 排名/分布窗口函数(例如 ROW_NUMBERRANKDENSE_RANKNTILE)对每个产出行都非空。
  • COUNT(...) OVER (...) 非空。
  • 其他窗口聚合(SUM/AVG/MIN/MAX/...)在空 frame 或函数语义可能产出 NULL 时可空。
  • 值窗口函数(LAG/LEAD/FIRST_VALUE/LAST_VALUE/NTH_VALUE)使用函数特定可空规则(参数、默认值、frame 行为)。

函数类型推断

每个函数定义:

function_def = {
name: String,
arity: Exact(n) | Range(min, max) | AtLeast(n) | Any,
return_type: Fixed(type) | SameAsArg(index) | NumericPromotion | Custom(fn),
nullability: AnyArgNullable | AlwaysNullable | NeverNullable | Custom(fn)
}

常见可空性模式:

模式 含义 示例
────────────────────────────────────────────────────────────────────
AnyArgNullable 任一参数可空则可空 UPPER(x), ABS(x), x + y
AlwaysNullable 与参数无关,总是可空 仅返回可空值的实现相关函数
NeverNullable 与参数无关,永不可空 COUNT, EXISTS
Custom 函数专属逻辑 IF, CASE, IFNULL, COALESCE