前置知识
语法树
AST 是 Abstract Syntax Tree ,中文通常叫 抽象语法树。
在数据库里,用户写的 SQL 文本会先经过词法分析和语法分析,被转换成一种树形结构,这棵树就是 AST。它描述的是 SQL 的语法结构,而不是最终怎么执行。
例如 SQL:
sql
SELECT name, age
FROM users
WHERE age > 18;
可以抽象成一棵 AST:
text
SelectStatement
├── SelectList
│ ├── Column: name
│ └── Column: age
├── From
│ └── Table: users
└── Where
└── GreaterThan
├── Column: age
└── Literal: 18
它的意思是:
text
这是一个 SELECT 查询
查询的列是 name 和 age
查询的表是 users
过滤条件是 age > 18
AST 的作用主要有三个:
- 让数据库理解 SQL 的结构
SQL 文本只是字符串,数据库不能直接优化字符串,必须先把它变成结构化形式。 - 为语义分析做准备
例如检查users表是否存在、age列是否存在、类型是否匹配、函数是否合法等。 - 为逻辑计划生成做准备
数据库会根据 AST 生成逻辑查询计划,例如选择、投影、连接等关系代数表达式。
可以把整个流程理解为:
text
SQL 文本
→ 词法分析:拆成 token
→ 语法分析:生成 AST
→ 语义分析:检查表名、列名、类型
→ 逻辑计划:关系代数表达式
→ 逻辑优化:查询重写
→ 物理计划:选择具体执行算法
→ 执行
简单说:
AST 是数据库把 SQL 从"字符串"变成"结构化语法表示"的第一步。
它还不是执行计划,只是 SQL 的语法骨架。
逻辑查询计划
逻辑查询计划 是数据库把 SQL 的含义转换成的一种关系代数形式的操作树。它描述"要做哪些逻辑操作",但还不决定"具体用什么物理算法执行"。
可以理解为:
text
SQL 文本
→ AST 抽象语法树
→ 语义分析
→ 逻辑查询计划
→ 逻辑优化
→ 物理执行计划
→ 执行
1. 逻辑查询计划描述什么
逻辑查询计划主要描述这些逻辑算子:
| SQL 部分 | 逻辑算子 |
|---|---|
FROM |
表扫描 |
WHERE |
选择 / 过滤 σ |
SELECT |
投影 π |
JOIN |
连接 ⋈ |
GROUP BY |
分组聚合 γ |
HAVING |
聚合后过滤 |
DISTINCT |
去重 |
ORDER BY |
排序 |
LIMIT |
限制输出行数 |
例如 SQL:
sql
SELECT name
FROM users
WHERE age > 18;
可以转换成逻辑查询计划:
text
Projection[name]
└── Selection[age > 18]
└── TableScan[users]
用关系代数表示就是:
text
π_name(σ_age>18(users))
意思是:
- 从
users表取数据; - 过滤
age > 18的行; - 只输出
name列。
2. 逻辑查询计划不是物理执行计划
逻辑查询计划只说明做什么 ,不说明怎么做。
例如:
sql
SELECT *
FROM orders o
JOIN customers c
ON o.customer_id = c.customer_id;
逻辑查询计划可能是:
text
Join[o.customer_id = c.customer_id]
├── TableScan[orders]
└── TableScan[customers]
它只说明要把 orders 和 customers 按条件连接起来。
但它还没有决定:
text
用 Hash Join 还是 Nested Loop Join?
先扫描 orders 还是先扫描 customers?
用全表扫描还是索引扫描?
是否并行执行?
内存如何分配?
这些属于物理执行计划阶段。
3. 逻辑查询计划和 AST 的区别
AST 是 SQL 的语法结构 ,逻辑查询计划是 SQL 的关系操作语义。
同一个 SQL:
sql
SELECT name
FROM users
WHERE age > 18;
AST 更像:
text
SelectStatement
├── SelectList
│ └── Column: name
├── From
│ └── Table: users
└── Where
└── GreaterThan
├── Column: age
└── Literal: 18
逻辑查询计划更像:
text
Projection[name]
└── Selection[age > 18]
└── TableScan[users]
区别是:
| 对比项 | AST | 逻辑查询计划 |
|---|---|---|
| 表示内容 | SQL 的语法结构 | 查询的关系代数操作 |
| 更接近 | SQL 文本 | 数据库内部执行语义 |
| 关注点 | SELECT、FROM、WHERE 怎么写 |
扫描、过滤、投影、连接怎么组合 |
| 是否可优化 | 不方便直接优化 | 适合做查询重写和逻辑优化 |
4. 一个 JOIN 查询的例子
sql
SELECT c.name, o.amount
FROM customers c
JOIN orders o
ON c.customer_id = o.customer_id
WHERE c.region = 'Asia'
AND o.amount > 100;
初始逻辑查询计划可能是:
text
Projection[c.name, o.amount]
└── Selection[c.region = 'Asia' AND o.amount > 100]
└── Join[c.customer_id = o.customer_id]
├── TableScan[customers]
└── TableScan[orders]
关系代数表示:
text
π_c.name,o.amount(
σ_c.region='Asia' AND o.amount>100(
customers ⋈_c.customer_id=o.customer_id orders
)
)
逻辑优化后,可以变成:
text
Projection[c.name, o.amount]
└── Join[c.customer_id = o.customer_id]
├── Projection[c.customer_id, c.name]
│ └── Selection[c.region = 'Asia']
│ └── TableScan[customers]
└── Projection[o.customer_id, o.amount]
└── Selection[o.amount > 100]
└── TableScan[orders]
也就是:
text
π_c.name,o.amount(
π_c.customer_id,c.name(σ_c.region='Asia'(customers))
⋈_c.customer_id=o.customer_id
π_o.customer_id,o.amount(σ_o.amount>100(orders))
)
这里做了两个重要优化:
text
选择下推:先过滤行
投影下推:先减少列
5. 总结
逻辑查询计划就是数据库把 SQL 转换成的"关系代数操作树"。
它回答的是:
这个查询在逻辑上需要扫描哪些表、过滤哪些行、保留哪些列、执行哪些连接、做哪些聚合。
但它还不回答:
具体用哪个索引、哪个连接算法、哪个扫描方式、多少并行度。
这些要到物理执行计划阶段才决定。
关系代数基础
下面用一个大表总结关系代数基础符号:
| 符号 | 名称 | 作用 | 输入 | 输出 | SQL 对应 | 示例 | 说明 |
|---|---|---|---|---|---|---|---|
R、S、T |
关系 | 表示一张表或中间结果 | --- | 一个关系 | 表名 / 子查询 | Student |
关系可以理解为数据库中的表 |
| 元组 | Tuple | 表中的一行 | --- | 一行数据 | 一行记录 | (1, 'Alice', 20) |
关系由多个元组组成 |
| 属性 | Attribute | 表中的一列 | --- | 一列字段 | 列名 | name、age |
属性组成关系的 schema |
σ_p(R) |
选择 | 按条件筛选行 | 一个关系 R |
满足条件的行 | WHERE |
σ_age>18(Student) |
横向筛选;保留满足谓词 p 的元组 |
π_A(R) |
投影 | 选择需要的列 | 一个关系 R |
指定列组成的新关系 | SELECT 列 |
π_name,age(Student) |
纵向裁剪;经典关系代数中投影会去重 |
R × S |
笛卡尔积 | 两个关系的所有行两两组合 | 两个关系 | 组合后的关系 | FROM R, S / CROSS JOIN |
Student × Dept |
若 R 有 m 行、S 有 n 行,结果有 m × n 行 |
R ⋈_p S |
条件连接 / θ 连接 | 按条件连接两个关系 | 两个关系 | 满足连接条件的组合行 | JOIN ... ON |
Student ⋈_Student.dept_id=Dept.dept_id Dept |
可理解为 σ_p(R × S) |
R ⋈ S |
自然连接 | 自动按同名属性连接 | 两个关系 | 按同名列匹配后的关系 | NATURAL JOIN |
Student ⋈ Dept |
会合并同名属性;工程中要谨慎使用 |
R ⟕_p S |
左外连接 | 保留左表所有行,右表无匹配则补 NULL | 两个关系 | 左表完整保留的连接结果 | LEFT JOIN ... ON |
Student ⟕_Student.dept_id=Dept.dept_id Dept |
不满足普通内连接的交换律 |
R ⟖_p S |
右外连接 | 保留右表所有行,左表无匹配则补 NULL | 两个关系 | 右表完整保留的连接结果 | RIGHT JOIN ... ON |
Student ⟖_Student.dept_id=Dept.dept_id Dept |
可视为左右表交换后的左外连接 |
R ⟗_p S |
全外连接 | 保留左右两边所有行,无匹配处补 NULL | 两个关系 | 双方都完整保留的连接结果 | FULL OUTER JOIN |
Student ⟗_Student.dept_id=Dept.dept_id Dept |
SQL 支持情况因数据库而异 |
R ⋉_p S |
半连接 | 返回 R 中能在 S 找到匹配的行 |
两个关系 | 只包含左表属性的关系 | EXISTS / IN |
Student ⋉_Student.dept_id=Dept.dept_id Dept |
不输出右表列;常用于子查询优化 |
R ▷_p S |
反连接 | 返回 R 中不能在 S 找到匹配的行 |
两个关系 | 只包含左表属性的关系 | NOT EXISTS / NOT IN |
Student ▷_Student.dept_id=Dept.dept_id Dept |
NOT IN 受 NULL 影响,实际优化要谨慎 |
R ∪ S |
并 | 合并两个关系的元组 | 两个兼容关系 | 出现在 R 或 S 中的元组 |
UNION |
π_name(Student) ∪ π_name(Teacher) |
要求两个关系属性个数和类型兼容 |
R ∩ S |
交 | 取两个关系共同的元组 | 两个兼容关系 | 同时出现在 R 和 S 中的元组 |
INTERSECT |
π_name(Student) ∩ π_name(Teacher) |
不是所有数据库都直接支持 INTERSECT |
R − S |
差 | 从 R 中去掉也在 S 中的元组 |
两个兼容关系 | 属于 R 但不属于 S 的元组 |
EXCEPT / MINUS |
π_name(Student) − π_name(Teacher) |
不满足交换律,即 R − S ≠ S − R |
ρ_x(R) |
关系重命名 | 给关系改名 | 一个关系 | 改名后的关系 | 表别名 | ρ_S1(Student) |
常用于自连接 |
ρ_{a/b}(R) |
属性重命名 | 给属性改名 | 一个关系 | 改列名后的关系 | 列别名 | ρ_{student_name/name}(Student) |
防止列名冲突或统一 schema |
γ_G,F(R) |
分组聚合 | 按属性分组并计算聚合函数 | 一个关系 | 聚合后的关系 | GROUP BY |
γ_dept_id,COUNT(*)(Student) |
G 是分组列,F 是聚合函数 |
δ(R) |
去重 | 删除重复元组 | 一个关系 | 无重复元组的关系 | DISTINCT |
δ(Student) |
经典关系代数默认集合语义;SQL 默认多重集语义 |
τ_A(R) |
排序 | 按属性排序 | 一个关系 | 有序结果 | ORDER BY |
τ_age DESC(Student) |
经典关系代数通常无序,排序属于扩展算子 |
LIMIT_n(R) |
限制行数 | 只取前 n 行 | 一个关系 | 最多 n 行 | LIMIT / FETCH FIRST |
LIMIT_10(Student) |
通常需要和排序一起使用才有确定意义 |
attrs(R) |
属性集合 | 表示关系 R 的所有列 |
一个关系 | 属性集合 | schema | attrs(Student) |
常用于描述规则前提 |
attrs(p) |
谓词属性集合 | 表示条件 p 用到的列 |
一个谓词 | 属性集合 | 条件涉及列 | attrs(age > 18) = {age} |
常用于判断谓词能否下推 |
p、q |
谓词 | 过滤或连接条件 | 表达式 | TRUE / FALSE / UNKNOWN | WHERE / ON |
age > 18 |
SQL 中还要考虑 NULL 三值逻辑 |
A ⊆ B |
子集 | 表示属性集合包含关系 | 两个集合 | 布尔判断 | --- | {name} ⊆ {id,name,age} |
常用于投影规则前提 |
≡ |
等价 | 表示两个表达式结果相同 | 两个表达式 | 等价关系 | 查询重写 | σ_p(σ_q(R)) ≡ σ_{p AND q}(R) |
查询优化的理论基础 |
最核心的几个:
| 符号 | 口诀 | SQL 对应 |
|---|---|---|
σ |
选行 | WHERE |
π |
选列 | SELECT |
⋈ |
连表 | JOIN |
γ |
分组统计 | GROUP BY |
∪ / ∩ / − |
集合运算 | UNION / INTERSECT / EXCEPT |
ρ |
改名 | 别名 AS |
查询重写 / 逻辑优化
一、查询重写 / 逻辑优化阶段的作用
在数据库系统中,用户提交的是 SQL 查询,而数据库内部通常不会直接执行 SQL 文本。SQL 会先被解析成一种内部表示,例如:
text
SQL 语句
→ 语法树 AST
→ 逻辑查询计划
→ 关系代数表达式
→ 物理执行计划
查询重写 / 逻辑优化发生在生成物理执行计划之前。它的核心目标是:
在不改变查询语义的前提下,将原始查询转换为更容易执行、代价更低的等价逻辑查询计划。
也就是说,逻辑优化阶段并不直接决定"用哈希连接还是嵌套循环连接",那属于物理优化阶段。逻辑优化主要处理的是:
text
能不能少读一些行?
能不能少保留一些列?
能不能把过滤条件提前?
能不能把子查询改成连接?
能不能改变连接顺序?
能不能消除无用的 JOIN、DISTINCT、GROUP BY?
例如,SQL:
sql
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = 'Asia';
可以先抽象为关系代数表达式:
text
π_order_id, name (
σ_region='Asia' (
orders ⋈ orders.customer_id = customers.customer_id customers
)
)
逻辑优化器可能将选择条件下推:
text
π_order_id, name (
orders ⋈ orders.customer_id = customers.customer_id
σ_region='Asia'(customers)
)
这样可以先过滤 customers,再进行连接,从而减少连接输入规模。
二、关系代数符号约定
下面使用常见关系代数符号:
| 符号 | 含义 |
|---|---|
R, S, T |
关系,即表或中间结果 |
σ_p(R) |
选择,表示从 R 中筛选满足条件 p 的元组 |
π_A(R) |
投影,表示只保留属性集合 A |
R × S |
笛卡尔积 |
R ⋈_p S |
条件连接 |
R ⋈ S |
自然连接或省略条件的连接 |
R ∪ S |
并 |
R ∩ S |
交 |
R − S |
差 |
ρ(R) |
重命名 |
γ_G, F(R) |
分组聚合,按 G 分组,计算聚合函数 F |
其中,选择、投影、连接是逻辑优化中最重要的几个算子。
三、选择运算的等价规则
选择运算 σ 对应 SQL 中的 WHERE 或部分 HAVING 条件。
1. 选择级联规则
如果一个关系上连续做多次选择:
text
σ_p(σ_q(R)) ≡ σ_p AND q(R)
也就是说:
text
先过滤 q,再过滤 p
等价于:
text
一次性过滤 p AND q
例如 SQL:
sql
SELECT *
FROM users
WHERE age > 18 AND city = 'Tokyo';
可以看作:
text
σ_age>18 AND city='Tokyo'(users)
也可以看作:
text
σ_age>18(σ_city='Tokyo'(users))
两者等价。
2. 选择交换规则
连续选择的顺序可以交换:
text
σ_p(σ_q(R)) ≡ σ_q(σ_p(R))
例如:
text
σ_age>18(σ_city='Tokyo'(users))
等价于:
text
σ_city='Tokyo'(σ_age>18(users))
这个规则说明,多个过滤条件之间的执行顺序可以重新排列。优化器通常会优先执行选择率更高的条件,也就是更能减少数据量的条件。
3. 选择条件分解规则
复合条件可以拆成多个简单条件:
text
σ_p AND q(R) ≡ σ_p(σ_q(R))
例如:
text
σ_age>18 AND city='Tokyo'(users)
可以分解为:
text
σ_age>18(σ_city='Tokyo'(users))
这条规则是谓词下推的基础。优化器可以把不同的过滤条件推到不同的数据源上。
四、投影运算的等价规则
投影运算 π 对应 SQL 中的 SELECT 列表。它的作用是保留需要的列,丢弃不需要的列。
1. 投影级联规则
如果连续做多次投影,只需要保留最外层需要的属性:
text
π_A(π_B(R)) ≡ π_A(R)
前提是:
text
A ⊆ B
例如:
text
π_name(π_id,name,age(users))
等价于:
text
π_name(users)
这说明中间多余的投影可以被消除。
2. 投影下推规则
对于连接查询:
text
π_A(R ⋈_p S)
可以重写为:
text
π_A(
π_B(R) ⋈_p π_C(S)
)
其中:
text
B = A 中属于 R 的属性 ∪ 连接条件 p 中属于 R 的属性
C = A 中属于 S 的属性 ∪ 连接条件 p 中属于 S 的属性
例如 SQL:
sql
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
最终只需要:
text
orders.order_id
customers.name
但是连接条件还需要:
text
orders.customer_id
customers.customer_id
所以可以在连接前裁剪列:
text
π_order_id, name (
π_order_id, customer_id(orders)
⋈ orders.customer_id = customers.customer_id
π_customer_id, name(customers)
)
投影下推的作用是减少中间结果的宽度,降低内存、网络传输和连接代价。
五、选择与投影的交换规则
选择和投影有时可以交换:
text
σ_p(π_A(R)) ≡ π_A(σ_p(R))
前提是条件 p 涉及的属性都包含在 A 中。
如果 p 中使用了某些最终不输出的列,则需要先临时保留这些列:
text
π_A(σ_p(R))
≡
π_A(σ_p(π_{A ∪ attrs(p)}(R)))
其中 attrs(p) 表示谓词 p 中涉及的属性集合。
例如:
sql
SELECT name
FROM users
WHERE age > 18;
虽然最终只输出 name,但是过滤条件需要 age,因此不能一开始只保留 name,而应当先保留:
text
name, age
逻辑形式为:
text
π_name(
σ_age>18(
π_name, age(users)
)
)
六、笛卡尔积与连接的转换规则
SQL 中旧式连接写法:
sql
SELECT *
FROM R, S
WHERE R.a = S.b;
在关系代数中可以表示为:
text
σ_R.a=S.b(R × S)
它等价于条件连接:
text
R ⋈_R.a=S.b S
因此有规则:
text
σ_p(R × S) ≡ R ⋈_p S
这是非常重要的重写规则。因为真正执行笛卡尔积通常代价极高,而连接算子可以使用索引、哈希、排序等物理方法高效执行。
七、连接运算的等价规则
连接是查询优化中最核心的部分。多表查询的性能往往主要取决于连接顺序和连接输入规模。
1. 内连接交换律
对于内连接:
text
R ⋈_p S ≡ S ⋈_p R
例如:
sql
orders JOIN customers
和:
sql
customers JOIN orders
在逻辑上是等价的,只要连接条件正确调整。
这个规则允许优化器决定先访问哪张表。
2. 内连接结合律
对于内连接:
text
(R ⋈ S) ⋈ T ≡ R ⋈ (S ⋈ T)
例如三表连接:
text
(orders ⋈ customers) ⋈ regions
可以改写为:
text
orders ⋈ (customers ⋈ regions)
这使得优化器可以枚举不同的连接顺序。
例如,如果 customers ⋈ regions 的结果很小,那么先执行这个连接可能更优。
3. 选择下推到连接
如果选择条件只涉及关系 R 中的属性:
text
σ_p(R ⋈ S) ≡ σ_p(R) ⋈ S
前提:
text
attrs(p) ⊆ attrs(R)
类似地,如果条件只涉及 S:
text
σ_p(R ⋈ S) ≡ R ⋈ σ_p(S)
例如:
sql
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = 'Asia';
关系代数原式:
text
σ_c.region='Asia'(
orders ⋈ orders.customer_id = customers.customer_id customers
)
可以重写为:
text
orders ⋈ orders.customer_id = customers.customer_id
σ_region='Asia'(customers)
这就是典型的谓词下推。
4. 连接条件分解
如果连接条件由多个条件组成:
text
R ⋈_{p AND q} S
可以写成:
text
σ_q(R ⋈_p S)
或者:
text
σ_p AND q(R × S)
优化器可以根据不同条件的性质决定如何处理。例如,等值条件可能适合哈希连接,范围条件可能适合索引范围扫描或嵌套循环连接。
5. 多表连接重排
利用交换律和结合律,多表连接可以重排:
text
(A ⋈ B) ⋈ C
可以变成:
text
(A ⋈ C) ⋈ B
也可以变成:
text
A ⋈ (B ⋈ C)
在数据库优化器中,连接重排是最重要的优化之一。对于 n 张表的连接,可能的连接顺序非常多,因此优化器通常需要结合代价估计来选择较优方案。
八、并、交、差的等价规则
集合运算在关系代数中也有一系列等价规则。
1. 并集交换律
text
R ∪ S ≡ S ∪ R
2. 并集结合律
text
(R ∪ S) ∪ T ≡ R ∪ (S ∪ T)
3. 交集交换律
text
R ∩ S ≡ S ∩ R
4. 交集结合律
text
(R ∩ S) ∩ T ≡ R ∩ (S ∩ T)
5. 选择对并集的分配
text
σ_p(R ∪ S) ≡ σ_p(R) ∪ σ_p(S)
6. 投影对并集的分配
text
π_A(R ∪ S) ≡ π_A(R) ∪ π_A(S)
7. 选择对交集的分配
text
σ_p(R ∩ S) ≡ σ_p(R) ∩ σ_p(S)
8. 差集的注意事项
差集通常不满足交换律:
text
R − S ≠ S − R
也不满足普通结合律:
text
(R − S) − T ≠ R − (S − T)
因此优化器对差集的重写必须更加谨慎。
九、重命名规则
重命名算子 ρ 用于处理属性名或关系名冲突,尤其在自连接中非常重要。
例如:
sql
SELECT *
FROM employee e1
JOIN employee e2 ON e1.manager_id = e2.employee_id;
同一张表 employee 出现两次,因此需要重命名:
text
ρ_e1(employee) ⋈ e1.manager_id = e2.employee_id ρ_e2(employee)
重命名可以与选择、投影交换,但必须同步修改属性名:
text
ρ(σ_p(R)) ≡ σ_ρ(p)(ρ(R))
ρ(π_A(R)) ≡ π_ρ(A)(ρ(R))
其中 ρ(p) 表示将谓词中的属性名按重命名规则替换。
十、半连接与反连接规则
很多 SQL 子查询可以转化为半连接或反连接。
1. 半连接
半连接记作:
text
R ⋉_p S
它表示:
返回
R中能够在S中找到匹配的元组,但不输出S的列。
半连接可以用普通连接和投影表示:
text
R ⋉_p S ≡ π_attrs(R)(R ⋈_p S)
SQL 中的 EXISTS 和部分 IN 子查询常常可以改写为半连接。
例如:
sql
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
可以表示为:
text
customers ⋉ customers.customer_id = orders.customer_id orders
2. 反连接
反连接记作:
text
R ▷_p S
它表示:
返回
R中无法在S中找到匹配的元组。
反连接可以用差集表示:
text
R ▷_p S ≡ R − π_attrs(R)(R ⋈_p S)
SQL 中的 NOT EXISTS 通常可以改写为反连接。
例如:
sql
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
可以表示为:
text
customers ▷ customers.customer_id = orders.customer_id orders
十一、子查询重写规则
SQL 中的子查询通常会被逻辑优化器改写成连接、半连接、反连接或聚合连接。
1. IN 子查询改写为半连接
SQL:
sql
SELECT *
FROM orders
WHERE customer_id IN (
SELECT customer_id
FROM customers
WHERE region = 'Asia'
);
逻辑形式可以改写为:
text
orders ⋉ orders.customer_id = customers.customer_id
σ_region='Asia'(customers)
这里半连接只判断 orders.customer_id 是否出现在右侧结果中,不需要输出 customers 的列。
2. EXISTS 子查询改写为半连接
SQL:
sql
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
关系代数形式:
text
customers ⋉ customers.customer_id = orders.customer_id orders
3. NOT EXISTS 子查询改写为反连接
SQL:
sql
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
关系代数形式:
text
customers ▷ customers.customer_id = orders.customer_id orders
4. 标量子查询改写
SQL:
sql
SELECT *
FROM orders
WHERE amount > (
SELECT AVG(amount)
FROM orders
);
可以理解为先计算聚合结果:
text
γ_AVG(amount)(orders)
然后再与外层查询结合:
text
σ_orders.amount > avg_amount(
orders × γ_AVG(amount)(orders)
)
在实际优化中,数据库通常会将这个标量子查询单独计算一次,再作为常量或单行关系参与过滤。
十二、聚合与分组的等价规则
扩展关系代数中,分组聚合通常写作:
text
γ_G, F(R)
其中:
text
G 表示分组属性
F 表示聚合函数
例如:
sql
SELECT customer_id, COUNT(*)
FROM orders
GROUP BY customer_id;
可以写成:
text
γ_customer_id, COUNT(*)(orders)
1. 分组键上的选择可以下推
如果选择条件只涉及分组键,则可以下推到聚合之前:
text
σ_p(γ_G, F(R)) ≡ γ_G, F(σ_p(R))
前提:
text
attrs(p) ⊆ G
例如:
sql
SELECT customer_id, COUNT(*)
FROM orders
GROUP BY customer_id
HAVING customer_id > 100;
可以改写为:
sql
SELECT customer_id, COUNT(*)
FROM orders
WHERE customer_id > 100
GROUP BY customer_id;
关系代数表示为:
text
σ_customer_id>100(
γ_customer_id, COUNT(*)(orders)
)
改写为:
text
γ_customer_id, COUNT(*)(
σ_customer_id>100(orders)
)
2. 聚合结果上的选择不能下推
如果条件依赖聚合结果,则不能下推。
例如:
sql
SELECT customer_id, COUNT(*)
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 10;
关系代数形式:
text
σ_COUNT(*)>10(
γ_customer_id, COUNT(*)(orders)
)
这里 COUNT(*) 是聚合之后才产生的值,因此不能改写为:
text
γ_customer_id, COUNT(*)(
σ_COUNT(*)>10(orders)
)
因为原始 orders 表中并不存在 COUNT(*) 这个属性。
3. 聚合前投影下推
聚合前只需要保留分组列和聚合函数使用的列:
text
γ_G, F(A)(R)
≡
γ_G, F(A)(π_{G ∪ A}(R))
例如:
sql
SELECT customer_id, SUM(amount)
FROM orders
GROUP BY customer_id;
只需要保留:
text
customer_id, amount
因此可写为:
text
γ_customer_id, SUM(amount)(
π_customer_id, amount(orders)
)
十三、外连接相关的逻辑重写
外连接比内连接复杂,因为外连接会产生 NULL 填充行。因此,内连接的交换律和结合律不能简单应用到外连接上。
1. 外连接不能随意交换
一般来说:
text
R ⟕ S ≠ S ⟕ R
其中 ⟕ 表示左外连接。
例如:
sql
A LEFT JOIN B ON A.id = B.a_id
表示保留 A 中所有行。
而:
sql
B LEFT JOIN A ON A.id = B.a_id
表示保留 B 中所有行。
两者语义不同。
2. 外连接转内连接
在某些情况下,外连接可以转换为内连接。
例如:
sql
SELECT *
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = 'Asia';
这个查询中,WHERE c.region = 'Asia' 会过滤掉右表为 NULL 的行。因此,左外连接产生的 NULL 扩展行不会保留下来。
所以它等价于:
sql
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = 'Asia';
关系代数上可以理解为:
text
σ_c.region='Asia'(orders ⟕ customers)
≡
σ_c.region='Asia'(orders ⋈ customers)
前提是谓词 c.region = 'Asia' 对 NULL 不成立,也就是它会拒绝 NULL 扩展行。这类谓词称为 null-rejecting predicate。
十四、视图展开规则
如果查询引用了视图,优化器通常会先将视图展开成其定义。
例如:
sql
CREATE VIEW asia_customers AS
SELECT *
FROM customers
WHERE region = 'Asia';
查询:
sql
SELECT name
FROM asia_customers
WHERE age > 30;
展开为:
sql
SELECT name
FROM customers
WHERE region = 'Asia'
AND age > 30;
关系代数表示:
text
π_name(
σ_age>30(
σ_region='Asia'(customers)
)
)
根据选择级联规则,可进一步合并为:
text
π_name(
σ_region='Asia' AND age>30(customers)
)
十五、连接消除规则
如果一个连接不会影响查询结果,则可以被消除。
例如:
sql
SELECT o.order_id, o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
如果满足:
text
orders.customer_id 是外键
customers.customer_id 是主键或唯一键
查询结果不使用 customers 的任何列
连接不会过滤 orders 的行
那么该查询可以改写为:
sql
SELECT o.order_id, o.amount
FROM orders o;
关系代数上可以理解为:
text
π_order_id, amount(
orders ⋈ orders.customer_id = customers.customer_id customers
)
≡
π_order_id, amount(orders)
但这条规则依赖完整性约束,例如主键、外键、唯一性和非空约束。没有这些约束时,不能随意消除连接。
十六、DISTINCT 与重复消除规则
在纯关系代数中,关系通常被看作集合,不包含重复元组。而 SQL 默认是多重集语义,即允许重复行。
因此,涉及 DISTINCT 的优化必须特别注意。
如果某列本身已经唯一:
sql
SELECT DISTINCT customer_id
FROM customers;
并且 customer_id 是主键,则:
sql
SELECT DISTINCT customer_id
FROM customers;
等价于:
sql
SELECT customer_id
FROM customers;
关系代数中可以理解为:如果 customer_id 是唯一属性,那么对它做重复消除没有意义。
类似地:
text
δ(π_key(R)) ≡ π_key(R)
其中 δ 表示重复消除,前提是 key 是唯一键。
十七、常见逻辑优化规则总结表
| 优化类型 | 关系代数规则 | 作用 |
|---|---|---|
| 选择合并 | σ_p(σ_q(R)) ≡ σ_p AND q(R) |
合并多个过滤条件 |
| 选择交换 | σ_p(σ_q(R)) ≡ σ_q(σ_p(R)) |
调整过滤顺序 |
| 选择下推 | σ_p(R ⋈ S) ≡ σ_p(R) ⋈ S |
尽早减少行数 |
| 投影合并 | π_A(π_B(R)) ≡ π_A(R) |
消除多余列裁剪 |
| 投影下推 | π_A(R ⋈ S) ≡ π_A(π_B(R) ⋈ π_C(S)) |
尽早减少列数 |
| 积转连接 | σ_p(R × S) ≡ R ⋈_p S |
避免笛卡尔积 |
| 连接交换 | R ⋈ S ≡ S ⋈ R |
改变连接顺序 |
| 连接结合 | (R ⋈ S) ⋈ T ≡ R ⋈ (S ⋈ T) |
多表连接重排 |
| 半连接改写 | R ⋉_p S ≡ π_attrs(R)(R ⋈_p S) |
优化 EXISTS / IN |
| 反连接改写 | R ▷_p S ≡ R − π_attrs(R)(R ⋈_p S) |
优化 NOT EXISTS |
| 聚合选择下推 | σ_p(γ_G,F(R)) ≡ γ_G,F(σ_p(R)) |
优化 HAVING |
| 视图展开 | Query(View) → Query(View Definition) |
让视图参与整体优化 |
| 外连接简化 | σ_p(R ⟕ S) ≡ σ_p(R ⋈ S) |
在特定条件下转内连接 |
| 连接消除 | π_A(R ⋈ S) ≡ π_A(R) |
删除无用 JOIN |
十八、一个完整示例
考虑 SQL:
sql
SELECT c.name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.region = 'Asia'
AND o.amount > 100;
1. 初始关系代数表达式
text
π_c.name(
σ_c.region='Asia' AND o.amount>100(
customers ⋈ c.customer_id=o.customer_id orders
)
)
2. 选择条件分解
text
π_c.name(
σ_c.region='Asia'(
σ_o.amount>100(
customers ⋈ c.customer_id=o.customer_id orders
)
)
)
3. 选择下推
text
π_c.name(
σ_c.region='Asia'(customers)
⋈ c.customer_id=o.customer_id
σ_o.amount>100(orders)
)
4. 投影下推
最终只需要 c.name,连接还需要两边的 customer_id,所以:
text
π_c.name(
π_customer_id, name(
σ_region='Asia'(customers)
)
⋈ c.customer_id=o.customer_id
π_customer_id(
σ_amount>100(orders)
)
)
这样得到的逻辑计划相比初始计划更优:
text
先过滤 customers.region = 'Asia'
先过滤 orders.amount > 100
只保留连接和输出需要的列
再进行 JOIN
最后输出 name
这正是逻辑优化的基本思想。
十九、关系代数等价规则在 SQL 中的限制
教科书中的关系代数通常基于集合语义,但 SQL 具有更多复杂特性。因此,实际数据库优化器在使用这些规则时必须考虑以下因素:
| 因素 | 影响 |
|---|---|
| 重复行 | SQL 默认允许重复行,某些集合等价规则在 bag 语义下不成立 |
| NULL | SQL 使用三值逻辑:TRUE、FALSE、UNKNOWN |
| 外连接 | 不能简单使用内连接的交换律和结合律 |
| 聚合函数 | COUNT(*)、COUNT(col)、SUM(col) 对 NULL 和重复值敏感 |
ORDER BY |
关系代数默认无序,但 SQL 查询可能要求有序结果 |
LIMIT |
改写可能改变前 N 条结果 |
| 非确定函数 | RAND()、NOW()、UUID() 不能随意移动或重复计算 |
| 用户自定义函数 | 可能有副作用或高昂代价 |
| 约束信息 | 连接消除、DISTINCT 消除依赖主键、外键、唯一约束 |
例如:
sql
SELECT *
FROM A LEFT JOIN B ON A.id = B.a_id
WHERE B.x = 1;
和:
sql
SELECT *
FROM A LEFT JOIN B
ON A.id = B.a_id AND B.x = 1;
通常并不等价。
第一个查询会过滤掉 B 不匹配的行;第二个查询仍然保留 A 中的行,只是右表列可能为 NULL。
二十、总结
查询重写 / 逻辑优化阶段的本质是:
利用关系代数等价规则,将 SQL 对应的逻辑表达式改写成语义相同但执行代价更低的表达式。
它的核心思想可以概括为四句话:
text
1. 尽早过滤行:选择下推
2. 尽早减少列:投影下推
3. 避免笛卡尔积:积转连接
4. 优化连接顺序:连接交换律与结合律
在理论上,查询重写依赖关系代数的等价变换;在工程上,数据库优化器还必须结合 SQL 的 NULL、重复行、外连接、聚合、约束和函数语义,谨慎判断每条规则是否可以安全应用。