当前位置: 首页 > news >正文

数据库查询优化器<1>查询重写 / 逻辑优化

语法树

ASTAbstract Syntax Tree,中文通常叫抽象语法树

在数据库里,用户写的 SQL 文本会先经过词法分析和语法分析,被转换成一种树形结构,这棵树就是 AST。它描述的是 SQL 的语法结构,而不是最终怎么执行。

例如 SQL:

SELECT name, age FROM users WHERE age > 18;

可以抽象成一棵 AST:

SelectStatement ├── SelectList │ ├── Column: name │ └── Column: age ├── From │ └── Table: users └── Where └── GreaterThan ├── Column: age └── Literal: 18

它的意思是:

这是一个 SELECT 查询 查询的列是 name 和 age 查询的表是 users 过滤条件是 age > 18

AST 的作用主要有三个:

  1. 让数据库理解 SQL 的结构
    SQL 文本只是字符串,数据库不能直接优化字符串,必须先把它变成结构化形式。
  2. 为语义分析做准备
    例如检查users表是否存在、age列是否存在、类型是否匹配、函数是否合法等。
  3. 为逻辑计划生成做准备
    数据库会根据 AST 生成逻辑查询计划,例如选择、投影、连接等关系代数表达式。

可以把整个流程理解为:

SQL 文本 → 词法分析:拆成 token → 语法分析:生成 AST → 语义分析:检查表名、列名、类型 → 逻辑计划:关系代数表达式 → 逻辑优化:查询重写 → 物理计划:选择具体执行算法 → 执行

简单说:

AST 是数据库把 SQL 从“字符串”变成“结构化语法表示”的第一步。

它还不是执行计划,只是 SQL 的语法骨架。

逻辑查询计划

逻辑查询计划是数据库把 SQL 的含义转换成的一种关系代数形式的操作树。它描述“要做哪些逻辑操作”,但还不决定“具体用什么物理算法执行”。

可以理解为:

SQL 文本 → AST 抽象语法树 → 语义分析 → 逻辑查询计划 → 逻辑优化 → 物理执行计划 → 执行

1. 逻辑查询计划描述什么

逻辑查询计划主要描述这些逻辑算子:

SQL 部分逻辑算子
FROM表扫描
WHERE选择 / 过滤σ
SELECT投影π
JOIN连接
GROUP BY分组聚合γ
HAVING聚合后过滤
DISTINCT去重
ORDER BY排序
LIMIT限制输出行数

例如 SQL:

SELECT name FROM users WHERE age > 18;

可以转换成逻辑查询计划:

Projection[name] └── Selection[age > 18] └── TableScan[users]

用关系代数表示就是:

π_name(σ_age>18(users))

意思是:

  1. users表取数据;
  2. 过滤age > 18的行;
  3. 只输出name列。

2. 逻辑查询计划不是物理执行计划

逻辑查询计划只说明做什么,不说明怎么做

例如:

SELECT * FROM orders o JOIN customers c ON o.customer_id = c.customer_id;

逻辑查询计划可能是:

Join[o.customer_id = c.customer_id] ├── TableScan[orders] └── TableScan[customers]

它只说明要把orderscustomers按条件连接起来。

但它还没有决定:

用 Hash Join 还是 Nested Loop Join? 先扫描 orders 还是先扫描 customers? 用全表扫描还是索引扫描? 是否并行执行? 内存如何分配?

这些属于物理执行计划阶段。


3. 逻辑查询计划和 AST 的区别

AST 是 SQL 的语法结构,逻辑查询计划是 SQL 的关系操作语义

同一个 SQL:

SELECT name FROM users WHERE age > 18;

AST 更像:

SelectStatement ├── SelectList │ └── Column: name ├── From │ └── Table: users └── Where └── GreaterThan ├── Column: age └── Literal: 18

逻辑查询计划更像:

Projection[name] └── Selection[age > 18] └── TableScan[users]

区别是:

对比项AST逻辑查询计划
表示内容SQL 的语法结构查询的关系代数操作
更接近SQL 文本数据库内部执行语义
关注点SELECTFROMWHERE怎么写扫描、过滤、投影、连接怎么组合
是否可优化不方便直接优化适合做查询重写和逻辑优化

4. 一个 JOIN 查询的例子

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;

初始逻辑查询计划可能是:

Projection[c.name, o.amount] └── Selection[c.region = 'Asia' AND o.amount > 100] └── Join[c.customer_id = o.customer_id] ├── TableScan[customers] └── TableScan[orders]

关系代数表示:

π_c.name,o.amount( σ_c.region='Asia' AND o.amount>100( customers ⋈_c.customer_id=o.customer_id orders ) )

逻辑优化后,可以变成:

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]

也就是:

π_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)) )

这里做了两个重要优化:

选择下推:先过滤行 投影下推:先减少列

5. 总结

逻辑查询计划就是数据库把 SQL 转换成的“关系代数操作树”。

它回答的是:

这个查询在逻辑上需要扫描哪些表、过滤哪些行、保留哪些列、执行哪些连接、做哪些聚合。

但它还不回答:

具体用哪个索引、哪个连接算法、哪个扫描方式、多少并行度。

这些要到物理执行计划阶段才决定。

关系代数基础

下面用一个大表总结关系代数基础符号

符号名称作用输入输出SQL 对应示例说明
RST关系表示一张表或中间结果一个关系表名 / 子查询Student关系可以理解为数据库中的表
元组Tuple表中的一行一行数据一行记录(1, 'Alice', 20)关系由多个元组组成
属性Attribute表中的一列一列字段列名nameage属性组成关系的 schema
σ_p(R)选择按条件筛选行一个关系R满足条件的行WHEREσ_age>18(Student)横向筛选;保留满足谓词p的元组
π_A(R)投影选择需要的列一个关系R指定列组成的新关系SELECT 列π_name,age(Student)纵向裁剪;经典关系代数中投影会去重
R × S笛卡尔积两个关系的所有行两两组合两个关系组合后的关系FROM R, S/CROSS JOINStudent × DeptR有 m 行、S有 n 行,结果有m × n
R ⋈_p S条件连接 / θ 连接按条件连接两个关系两个关系满足连接条件的组合行JOIN ... ONStudent ⋈_Student.dept_id=Dept.dept_id Dept可理解为σ_p(R × S)
R ⋈ S自然连接自动按同名属性连接两个关系按同名列匹配后的关系NATURAL JOINStudent ⋈ Dept会合并同名属性;工程中要谨慎使用
R ⟕_p S左外连接保留左表所有行,右表无匹配则补 NULL两个关系左表完整保留的连接结果LEFT JOIN ... ONStudent ⟕_Student.dept_id=Dept.dept_id Dept不满足普通内连接的交换律
R ⟖_p S右外连接保留右表所有行,左表无匹配则补 NULL两个关系右表完整保留的连接结果RIGHT JOIN ... ONStudent ⟖_Student.dept_id=Dept.dept_id Dept可视为左右表交换后的左外连接
R ⟗_p S全外连接保留左右两边所有行,无匹配处补 NULL两个关系双方都完整保留的连接结果FULL OUTER JOINStudent ⟗_Student.dept_id=Dept.dept_id DeptSQL 支持情况因数据库而异
R ⋉_p S半连接返回R中能在S找到匹配的行两个关系只包含左表属性的关系EXISTS/INStudent ⋉_Student.dept_id=Dept.dept_id Dept不输出右表列;常用于子查询优化
R ▷_p S反连接返回R中不能在S找到匹配的行两个关系只包含左表属性的关系NOT EXISTS/NOT INStudent ▷_Student.dept_id=Dept.dept_id DeptNOT IN受 NULL 影响,实际优化要谨慎
R ∪ S合并两个关系的元组两个兼容关系出现在RS中的元组UNIONπ_name(Student) ∪ π_name(Teacher)要求两个关系属性个数和类型兼容
R ∩ S取两个关系共同的元组两个兼容关系同时出现在RS中的元组INTERSECTπ_name(Student) ∩ π_name(Teacher)不是所有数据库都直接支持INTERSECT
R − SR中去掉也在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 FIRSTLIMIT_10(Student)通常需要和排序一起使用才有确定意义
attrs(R)属性集合表示关系R的所有列一个关系属性集合schemaattrs(Student)常用于描述规则前提
attrs(p)谓词属性集合表示条件p用到的列一个谓词属性集合条件涉及列attrs(age > 18) = {age}常用于判断谓词能否下推
pq谓词过滤或连接条件表达式TRUE / FALSE / UNKNOWNWHERE/ONage > 18SQL 中还要考虑 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 会先被解析成一种内部表示,例如:

SQL 语句 → 语法树 AST → 逻辑查询计划 → 关系代数表达式 → 物理执行计划

查询重写 / 逻辑优化发生在生成物理执行计划之前。它的核心目标是:

在不改变查询语义的前提下,将原始查询转换为更容易执行、代价更低的等价逻辑查询计划。

也就是说,逻辑优化阶段并不直接决定“用哈希连接还是嵌套循环连接”,那属于物理优化阶段。逻辑优化主要处理的是:

能不能少读一些行? 能不能少保留一些列? 能不能把过滤条件提前? 能不能把子查询改成连接? 能不能改变连接顺序? 能不能消除无用的 JOIN、DISTINCT、GROUP BY?

例如,SQL:

SELECT o.order_id, c.name FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE c.region = 'Asia';

可以先抽象为关系代数表达式:

π_order_id, name ( σ_region='Asia' ( orders ⋈ orders.customer_id = customers.customer_id customers ) )

逻辑优化器可能将选择条件下推:

π_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. 选择级联规则

如果一个关系上连续做多次选择:

σ_p(σ_q(R)) ≡ σ_p AND q(R)

也就是说:

先过滤 q,再过滤 p

等价于:

一次性过滤 p AND q

例如 SQL:

SELECT * FROM users WHERE age > 18 AND city = 'Tokyo';

可以看作:

σ_age>18 AND city='Tokyo'(users)

也可以看作:

σ_age>18(σ_city='Tokyo'(users))

两者等价。


2. 选择交换规则

连续选择的顺序可以交换:

σ_p(σ_q(R)) ≡ σ_q(σ_p(R))

例如:

σ_age>18(σ_city='Tokyo'(users))

等价于:

σ_city='Tokyo'(σ_age>18(users))

这个规则说明,多个过滤条件之间的执行顺序可以重新排列。优化器通常会优先执行选择率更高的条件,也就是更能减少数据量的条件。


3. 选择条件分解规则

复合条件可以拆成多个简单条件:

σ_p AND q(R) ≡ σ_p(σ_q(R))

例如:

σ_age>18 AND city='Tokyo'(users)

可以分解为:

σ_age>18(σ_city='Tokyo'(users))

这条规则是谓词下推的基础。优化器可以把不同的过滤条件推到不同的数据源上。


回到顶部

四、投影运算的等价规则

投影运算π对应 SQL 中的SELECT列表。它的作用是保留需要的列,丢弃不需要的列。

1. 投影级联规则

如果连续做多次投影,只需要保留最外层需要的属性:

π_A(π_B(R)) ≡ π_A(R)

前提是:

A ⊆ B

例如:

π_name(π_id,name,age(users))

等价于:

π_name(users)

这说明中间多余的投影可以被消除。


2. 投影下推规则

对于连接查询:

π_A(R ⋈_p S)

可以重写为:

π_A( π_B(R) ⋈_p π_C(S) )

其中:

B = A 中属于 R 的属性 ∪ 连接条件 p 中属于 R 的属性 C = A 中属于 S 的属性 ∪ 连接条件 p 中属于 S 的属性

例如 SQL:

SELECT o.order_id, c.name FROM orders o JOIN customers c ON o.customer_id = c.customer_id;

最终只需要:

orders.order_id customers.name

但是连接条件还需要:

orders.customer_id customers.customer_id

所以可以在连接前裁剪列:

π_order_id, name ( π_order_id, customer_id(orders) ⋈ orders.customer_id = customers.customer_id π_customer_id, name(customers) )

投影下推的作用是减少中间结果的宽度,降低内存、网络传输和连接代价。


回到顶部

五、选择与投影的交换规则

选择和投影有时可以交换:

σ_p(π_A(R)) ≡ π_A(σ_p(R))

前提是条件p涉及的属性都包含在A中。

如果p中使用了某些最终不输出的列,则需要先临时保留这些列:

π_A(σ_p(R)) ≡ π_A(σ_p(π_{A ∪ attrs(p)}(R)))

其中attrs(p)表示谓词p中涉及的属性集合。

例如:

SELECT name FROM users WHERE age > 18;

虽然最终只输出name,但是过滤条件需要age,因此不能一开始只保留name,而应当先保留:

name, age

逻辑形式为:

π_name( σ_age>18( π_name, age(users) ) )

回到顶部

六、笛卡尔积与连接的转换规则

SQL 中旧式连接写法:

SELECT * FROM R, S WHERE R.a = S.b;

在关系代数中可以表示为:

σ_R.a=S.b(R × S)

它等价于条件连接:

R ⋈_R.a=S.b S

因此有规则:

σ_p(R × S) ≡ R ⋈_p S

这是非常重要的重写规则。因为真正执行笛卡尔积通常代价极高,而连接算子可以使用索引、哈希、排序等物理方法高效执行。


回到顶部

七、连接运算的等价规则

连接是查询优化中最核心的部分。多表查询的性能往往主要取决于连接顺序和连接输入规模。

1. 内连接交换律

对于内连接:

R ⋈_p S ≡ S ⋈_p R

例如:

orders JOIN customers

和:

customers JOIN orders

在逻辑上是等价的,只要连接条件正确调整。

这个规则允许优化器决定先访问哪张表。


2. 内连接结合律

对于内连接:

(R ⋈ S) ⋈ T ≡ R ⋈ (S ⋈ T)

例如三表连接:

(orders ⋈ customers) ⋈ regions

可以改写为:

orders ⋈ (customers ⋈ regions)

这使得优化器可以枚举不同的连接顺序。

例如,如果customers ⋈ regions的结果很小,那么先执行这个连接可能更优。


3. 选择下推到连接

如果选择条件只涉及关系R中的属性:

σ_p(R ⋈ S) ≡ σ_p(R) ⋈ S

前提:

attrs(p) ⊆ attrs(R)

类似地,如果条件只涉及S

σ_p(R ⋈ S) ≡ R ⋈ σ_p(S)

例如:

SELECT * FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE c.region = 'Asia';

关系代数原式:

σ_c.region='Asia'( orders ⋈ orders.customer_id = customers.customer_id customers )

可以重写为:

orders ⋈ orders.customer_id = customers.customer_id σ_region='Asia'(customers)

这就是典型的谓词下推


4. 连接条件分解

如果连接条件由多个条件组成:

R ⋈_{p AND q} S

可以写成:

σ_q(R ⋈_p S)

或者:

σ_p AND q(R × S)

优化器可以根据不同条件的性质决定如何处理。例如,等值条件可能适合哈希连接,范围条件可能适合索引范围扫描或嵌套循环连接。


5. 多表连接重排

利用交换律和结合律,多表连接可以重排:

(A ⋈ B) ⋈ C

可以变成:

(A ⋈ C) ⋈ B

也可以变成:

A ⋈ (B ⋈ C)

在数据库优化器中,连接重排是最重要的优化之一。对于n张表的连接,可能的连接顺序非常多,因此优化器通常需要结合代价估计来选择较优方案。


回到顶部

八、并、交、差的等价规则

集合运算在关系代数中也有一系列等价规则。

1. 并集交换律

R ∪ S ≡ S ∪ R

2. 并集结合律

(R ∪ S) ∪ T ≡ R ∪ (S ∪ T)

3. 交集交换律

R ∩ S ≡ S ∩ R

4. 交集结合律

(R ∩ S) ∩ T ≡ R ∩ (S ∩ T)

5. 选择对并集的分配

σ_p(R ∪ S) ≡ σ_p(R) ∪ σ_p(S)

6. 投影对并集的分配

π_A(R ∪ S) ≡ π_A(R) ∪ π_A(S)

7. 选择对交集的分配

σ_p(R ∩ S) ≡ σ_p(R) ∩ σ_p(S)

8. 差集的注意事项

差集通常不满足交换律:

R − S ≠ S − R

也不满足普通结合律:

(R − S) − T ≠ R − (S − T)

因此优化器对差集的重写必须更加谨慎。


回到顶部

九、重命名规则

重命名算子ρ用于处理属性名或关系名冲突,尤其在自连接中非常重要。

例如:

SELECT * FROM employee e1 JOIN employee e2 ON e1.manager_id = e2.employee_id;

同一张表employee出现两次,因此需要重命名:

ρ_e1(employee) ⋈ e1.manager_id = e2.employee_id ρ_e2(employee)

重命名可以与选择、投影交换,但必须同步修改属性名:

ρ(σ_p(R)) ≡ σ_ρ(p)(ρ(R)) ρ(π_A(R)) ≡ π_ρ(A)(ρ(R))

其中ρ(p)表示将谓词中的属性名按重命名规则替换。


回到顶部

十、半连接与反连接规则

很多 SQL 子查询可以转化为半连接或反连接。

1. 半连接

半连接记作:

R ⋉_p S

它表示:

返回R中能够在S中找到匹配的元组,但不输出S的列。

半连接可以用普通连接和投影表示:

R ⋉_p S ≡ π_attrs(R)(R ⋈_p S)

SQL 中的EXISTS和部分IN子查询常常可以改写为半连接。

例如:

SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id );

可以表示为:

customers ⋉ customers.customer_id = orders.customer_id orders

2. 反连接

反连接记作:

R ▷_p S

它表示:

返回R中无法在S中找到匹配的元组。

反连接可以用差集表示:

R ▷_p S ≡ R − π_attrs(R)(R ⋈_p S)

SQL 中的NOT EXISTS通常可以改写为反连接。

例如:

SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id );

可以表示为:

customers ▷ customers.customer_id = orders.customer_id orders

回到顶部

十一、子查询重写规则

SQL 中的子查询通常会被逻辑优化器改写成连接、半连接、反连接或聚合连接。

1.IN子查询改写为半连接

SQL:

SELECT * FROM orders WHERE customer_id IN ( SELECT customer_id FROM customers WHERE region = 'Asia' );

逻辑形式可以改写为:

orders ⋉ orders.customer_id = customers.customer_id σ_region='Asia'(customers)

这里半连接只判断orders.customer_id是否出现在右侧结果中,不需要输出customers的列。


2.EXISTS子查询改写为半连接

SQL:

SELECT * FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id );

关系代数形式:

customers ⋉ customers.customer_id = orders.customer_id orders

3.NOT EXISTS子查询改写为反连接

SQL:

SELECT * FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id );

关系代数形式:

customers ▷ customers.customer_id = orders.customer_id orders

4. 标量子查询改写

SQL:

SELECT * FROM orders WHERE amount > ( SELECT AVG(amount) FROM orders );

可以理解为先计算聚合结果:

γ_AVG(amount)(orders)

然后再与外层查询结合:

σ_orders.amount > avg_amount( orders × γ_AVG(amount)(orders) )

在实际优化中,数据库通常会将这个标量子查询单独计算一次,再作为常量或单行关系参与过滤。


回到顶部

十二、聚合与分组的等价规则

扩展关系代数中,分组聚合通常写作:

γ_G, F(R)

其中:

G 表示分组属性 F 表示聚合函数

例如:

SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id;

可以写成:

γ_customer_id, COUNT(*)(orders)

1. 分组键上的选择可以下推

如果选择条件只涉及分组键,则可以下推到聚合之前:

σ_p(γ_G, F(R)) ≡ γ_G, F(σ_p(R))

前提:

attrs(p) ⊆ G

例如:

SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id HAVING customer_id > 100;

可以改写为:

SELECT customer_id, COUNT(*) FROM orders WHERE customer_id > 100 GROUP BY customer_id;

关系代数表示为:

σ_customer_id>100( γ_customer_id, COUNT(*)(orders) )

改写为:

γ_customer_id, COUNT(*)( σ_customer_id>100(orders) )

2. 聚合结果上的选择不能下推

如果条件依赖聚合结果,则不能下推。

例如:

SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id HAVING COUNT(*) > 10;

关系代数形式:

σ_COUNT(*)>10( γ_customer_id, COUNT(*)(orders) )

这里COUNT(*)是聚合之后才产生的值,因此不能改写为:

γ_customer_id, COUNT(*)( σ_COUNT(*)>10(orders) )

因为原始orders表中并不存在COUNT(*)这个属性。


3. 聚合前投影下推

聚合前只需要保留分组列和聚合函数使用的列:

γ_G, F(A)(R) ≡ γ_G, F(A)(π_{G ∪ A}(R))

例如:

SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id;

只需要保留:

customer_id, amount

因此可写为:

γ_customer_id, SUM(amount)( π_customer_id, amount(orders) )

回到顶部

十三、外连接相关的逻辑重写

外连接比内连接复杂,因为外连接会产生 NULL 填充行。因此,内连接的交换律和结合律不能简单应用到外连接上。

1. 外连接不能随意交换

一般来说:

R ⟕ S ≠ S ⟕ R

其中表示左外连接。

例如:

A LEFT JOIN B ON A.id = B.a_id

表示保留A中所有行。

而:

B LEFT JOIN A ON A.id = B.a_id

表示保留B中所有行。

两者语义不同。


2. 外连接转内连接

在某些情况下,外连接可以转换为内连接。

例如:

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 扩展行不会保留下来。

所以它等价于:

SELECT * FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE c.region = 'Asia';

关系代数上可以理解为:

σ_c.region='Asia'(orders ⟕ customers) ≡ σ_c.region='Asia'(orders ⋈ customers)

前提是谓词c.region = 'Asia'对 NULL 不成立,也就是它会拒绝 NULL 扩展行。这类谓词称为null-rejecting predicate


回到顶部

十四、视图展开规则

如果查询引用了视图,优化器通常会先将视图展开成其定义。

例如:

CREATE VIEW asia_customers AS SELECT * FROM customers WHERE region = 'Asia';

查询:

SELECT name FROM asia_customers WHERE age > 30;

展开为:

SELECT name FROM customers WHERE region = 'Asia' AND age > 30;

关系代数表示:

π_name( σ_age>30( σ_region='Asia'(customers) ) )

根据选择级联规则,可进一步合并为:

π_name( σ_region='Asia' AND age>30(customers) )

回到顶部

十五、连接消除规则

如果一个连接不会影响查询结果,则可以被消除。

例如:

SELECT o.order_id, o.amount FROM orders o JOIN customers c ON o.customer_id = c.customer_id;

如果满足:

orders.customer_id 是外键 customers.customer_id 是主键或唯一键 查询结果不使用 customers 的任何列 连接不会过滤 orders 的行

那么该查询可以改写为:

SELECT o.order_id, o.amount FROM orders o;

关系代数上可以理解为:

π_order_id, amount( orders ⋈ orders.customer_id = customers.customer_id customers ) ≡ π_order_id, amount(orders)

但这条规则依赖完整性约束,例如主键、外键、唯一性和非空约束。没有这些约束时,不能随意消除连接。


回到顶部

十六、DISTINCT 与重复消除规则

在纯关系代数中,关系通常被看作集合,不包含重复元组。而 SQL 默认是多重集语义,即允许重复行。

因此,涉及DISTINCT的优化必须特别注意。

如果某列本身已经唯一:

SELECT DISTINCT customer_id FROM customers;

并且customer_id是主键,则:

SELECT DISTINCT customer_id FROM customers;

等价于:

SELECT customer_id FROM customers;

关系代数中可以理解为:如果customer_id是唯一属性,那么对它做重复消除没有意义。

类似地:

δ(π_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:

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. 初始关系代数表达式

π_c.name( σ_c.region='Asia' AND o.amount>100( customers ⋈ c.customer_id=o.customer_id orders ) )

2. 选择条件分解

π_c.name( σ_c.region='Asia'( σ_o.amount>100( customers ⋈ c.customer_id=o.customer_id orders ) ) )

3. 选择下推

π_c.name( σ_c.region='Asia'(customers) ⋈ c.customer_id=o.customer_id σ_o.amount>100(orders) )

4. 投影下推

最终只需要c.name,连接还需要两边的customer_id,所以:

π_c.name( π_customer_id, name( σ_region='Asia'(customers) ) ⋈ c.customer_id=o.customer_id π_customer_id( σ_amount>100(orders) ) )

这样得到的逻辑计划相比初始计划更优:

先过滤 customers.region = 'Asia' 先过滤 orders.amount > 100 只保留连接和输出需要的列 再进行 JOIN 最后输出 name

这正是逻辑优化的基本思想。


回到顶部

十九、关系代数等价规则在 SQL 中的限制

教科书中的关系代数通常基于集合语义,但 SQL 具有更多复杂特性。因此,实际数据库优化器在使用这些规则时必须考虑以下因素:

因素影响
重复行SQL 默认允许重复行,某些集合等价规则在 bag 语义下不成立
NULLSQL 使用三值逻辑:TRUE、FALSE、UNKNOWN
外连接不能简单使用内连接的交换律和结合律
聚合函数COUNT(*)COUNT(col)SUM(col)对 NULL 和重复值敏感
ORDER BY关系代数默认无序,但 SQL 查询可能要求有序结果
LIMIT改写可能改变前 N 条结果
非确定函数RAND()NOW()UUID()不能随意移动或重复计算
用户自定义函数可能有副作用或高昂代价
约束信息连接消除、DISTINCT 消除依赖主键、外键、唯一约束
http://www.gsyq.cn/news/1637477.html

相关文章:

  • Meta Assistant / 告别命令行,我为一堆 Python 脚本做了一个 Windows 任务栏的“家”
  • 结合Nginx工作流程理解Epoll机制和Reactor模型
  • 设置Shell脚本开机自启
  • Python特征工程实战:从数据清洗到模型提效的完整流程
  • 开源项目C++ Workflow学习
  • 2026年避坑攻略:如何挑选性价比高的外墙保温装饰一体板厂家
  • GPT充值以后怎么用才不浪费?开发者把 ChatGPT 用进接口文档、代码审查和回归测试的 4 个工作流
  • Agent 架构
  • 手把手教你用8款一键生成论文工具,极速搞定各类论文
  • NSK滚珠丝杠W3205SS技术解析
  • Vite 环境变量治理:别把构建时配置当运行时开关
  • Linux syslog日志权限出错
  • 什么叫Padding Oracle
  • Wishbone BFM 设计与实现:从手写总线到自动化自检
  • 无货源自动拍单发货软件靠谱吗?新手先看货源关联和规格匹配一件代发工具教程解析
  • 课堂教学PPT模板推荐哪家?这6个平台教师亲测可用
  • 五大神经网络核心原理与实战:从CNN到GAN的直观理解与代码实现
  • 从离线分析到实时对话:JoyAI-VL-Interaction如何重塑视频AI交互范式
  • 自动扩缩容:3 种策略的适用场景
  • 【Aspose-CAD for Java】DWG转PDF实战:精准控制布局与图层,告别空白与错位
  • REACTOS RtlGetVersion 函数实现分析
  • 终极指南:如何用AI让Monika与你自由对话 - MonikA.I模组完全教程
  • 解决Ant发送邮件显示HTML源码问题:MIME类型配置详解
  • 三菱FX3U PLC运动轴控制与伺服调试实战
  • 王千源惊喜亮相HYROX杭州站 不止是演员,更是运动“源”
  • AIGC 内容指纹:生成内容入库前先做可追踪设计
  • 太香了!这个 GitHub 开源项目,让安卓模拟器直接跑在浏览器里,搞 AI 的必看
  • 基于单片机人脸识别电子密码锁智能门禁指纹识别语音提醒防盗成品12(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • 【考研】2026/7/4
  • LB200倒置相差显微镜:类器官与器官芯片生命科学的前沿窗口