Appearance
下面把 Join operation 按“为什么难、到底在比什么、每种算法什么时候用、题目怎么做”讲清楚。你先抓住一句话:
Join 的本质是:从两个关系中找出满足连接条件的 tuple pair。难点不是逻辑,而是怎样避免把两张表所有 tuple 两两比较。本章 PPT 中 Join 主要比较 Nested-loop、Block nested-loop、Indexed nested-loop、Merge join、Hash join 的代价和适用条件。
1. Join 到底在做什么?
假设有两个关系:
text
depositor(customer_name, account_number)
customer(customer_name, street, city)查询:
sql
select *
from depositor natural join customer;意思是:
找出
depositor.customer_name = customer.customer_name的 tuple pair,然后拼接输出。
逻辑上非常简单:
text
对 depositor 中每一行 d
找 customer 中 customer_name 相同的行 c
输出 d 和 c 的拼接问题是:如果两张表很大,不能真的粗暴两两比较。
2. Join 算法的核心分类
本章 Join 算法可以按“借助什么信息”分类:
| 算法 | 借助什么 | 适用连接条件 | 核心思想 |
|---|---|---|---|
| Nested-loop join | 什么都不借助 | 任意 join condition | 每条 tuple 两两比较 |
| Block nested-loop join | 利用 block 批量处理 | 任意 join condition | 每次拿一批外表 block,减少内表扫描次数 |
| Indexed nested-loop join | 利用 inner 表索引 | equi-join / natural join | 外表每条 tuple 去 inner 表索引查 |
| Merge join | 利用排序 | equi-join / natural join | 两边按 join key 排序后线性归并 |
| Hash join | 利用哈希分区 | equi-join / natural join | 相同 join key 必在同一分区,只在对应分区内连接 |
你可以先记住:
text
任意条件:Nested-loop / Block nested-loop
等值连接:Indexed nested-loop / Merge join / Hash join3. Join 题目中最重要的符号
考试题一般会给这些量:
| 符号 | 含义 |
|---|---|
r, s | 两个关系 |
nr | relation r 的 tuple 数 |
ns | relation s 的 tuple 数 |
br | relation r 的 block 数 |
bs | relation s 的 block 数 |
M | 可用内存 block 数 |
Hi | B+ tree index 高度 |
c | 对 inner relation 做一次索引查找的代价 |
最容易错的是:
n是 tuple 数,b是 block 数。
Nested-loop 用 tuple 数;Block nested-loop 用 block 数。
4. Nested-Loop Join:最笨但最通用
是什么
普通嵌套循环连接:
text
for each tuple tr in r:
for each tuple ts in s:
if tr and ts satisfy join condition:
output tr ⋈ ts为什么需要
它不要求索引、不要求排序、不要求等值连接。只要能判断 join condition,就能用。
直观理解
就是暴力双重循环。
如果 r 有 5000 条,s 有 10000 条,就要检查:
text
5000 * 10000 = 50,000,000对 tuple pair。
代价公式
设 r 是 outer relation,s 是 inner relation。
worst case:
text
block transfers = nr * bs + br
seeks = nr + br为什么是 nr * bs + br?
先读一遍 outer relation:
br对 outer 的每个 tuple,都扫描一遍 inner relation:
nr * bs
典型例子
PPT 中:
text
depositor: nr = 5000, br = 100
customer: ns = 10000, bs = 400若 depositor 做 outer:
text
BT = nr * bs + br
= 5000 * 400 + 100
= 2,000,100若 customer 做 outer:
text
BT = 10000 * 100 + 400
= 1,000,400所以普通 nested-loop 中,不一定让 block 少的表做 outer,而是要看:
text
outer tuple 数 * inner block 数 + outer block 数易错点
| 错误 | 正确 |
|---|---|
用 br * bs + br 算普通 nested-loop | 那是 block nested-loop |
| 以为普通 nested-loop 一定让小表 outer | 要比较 nr * bs + br |
| 忘记它支持任意 join condition | 它最通用,但通常最慢 |
5. Block Nested-Loop Join:把外层 tuple 换成 block
是什么
普通 nested-loop 是:
text
每次拿 outer 的一条 tupleBlock nested-loop 是:
text
每次拿 outer 的一个 block 或一批 blocks伪代码:
text
for each block Br in r:
for each block Bs in s:
compare tuples inside Br and Bs为什么更快
普通 nested-loop 中,outer 每条 tuple 都要扫描一遍 inner。
Block nested-loop 中,outer 每个 block 才扫描一遍 inner。
如果一个 block 中有很多 tuple,就能减少很多次 inner 扫描。
M = 3 时的代价
设 r 是 outer,s 是 inner:
text
block transfers = br * bs + br
seeks = 2 * br为什么?
读 outer:
br对 outer 的每个 block,扫描一次 inner:
br * bs
改进版本:M > 3
如果内存有 M 个 block:
M - 2个 block 放 outer chunk;1 个 block 读 inner;
1 个 block 放 output。
代价:
text
block transfers = ceil(br / (M - 2)) * bs + br
seeks = 2 * ceil(br / (M - 2))典型例子
仍然用:
text
depositor: br = 100
customer: bs = 400M = 3,depositor 做 outer:
text
BT = 100 * 400 + 100
= 40,100相比普通 nested-loop:
text
2,000,100已经小很多。
如果 M = 10,outer 仍是 customer,inner 是 depositor:
text
br = 400
bs = 100
M - 2 = 8text
BT = ceil(400 / 8) * 100 + 400
= 50 * 100 + 400
= 5,400内存越大,outer 被分成的 chunk 越少,inner 被重复扫描的次数越少。
什么时候选谁做 outer?
Block nested-loop 通常让 block 数较少的关系做 outer。
因为代价主要是:
text
ceil(br / (M - 2)) * bs + brouter 的 block 数 br 决定要扫描 inner 多少轮。
易错点
| 错误 | 正确 |
|---|---|
把 M - 2 写成 M - 1 | 要留 inner buffer 和 output buffer,所以是 M - 2 |
| 用 tuple 数计算 block nested-loop | Block nested-loop 用 block 数 |
| 一律让 tuple 少的表做 outer | 这里重点看 block 数 |
忘记加 + br | outer 本身也要读一遍 |
6. Indexed Nested-Loop Join:外表逐条查 inner 索引
是什么
如果 inner relation 的 join attribute 上有索引,就不用每次扫描 inner 全表。
做法是:
text
for each tuple tr in outer r:
use index on inner s to find matching tuples适用条件
必须满足:
join 是 equi-join 或 natural join;
inner relation 的 join attribute 上有 index。
例如:
sql
select *
from depositor d join customer c
on d.customer_name = c.customer_name;如果 customer.customer_name 上有索引,可以让:
text
outer = depositor
inner = customer然后对 depositor 的每条记录去 customer 索引查。
代价公式
text
Cost = nr * c + br其中:
| 符号 | 含义 |
|---|---|
nr | outer relation 的 tuple 数 |
br | outer relation 的 block 数 |
c | 对 inner 做一次 index lookup 并取出匹配记录的代价 |
如果 inner 上是 B+ tree key index,高度为 Hi:
text
c = Hi + 1所以:
text
Cost = nr * (Hi + 1) + brPPT 例子
text
depositor: n = 5000, b = 100
customer: n = 10000, b = 400customer 在 customer_name 上有 primary B+ tree index,高度为 3。
让 depositor 做 outer:
text
nr = 5000
br = 100
c = 3 + 1 = 4所以:
text
Cost = 5000 * 4 + 100
= 20,100这比 block nested-loop 的 40,100 还低。
如果两边都有索引,怎么选 outer?
通常选 tuple 数较少 的关系做 outer。
因为公式是:
text
nr * c + br核心受 nr 影响。
易错点
| 错误 | 正确 |
|---|---|
| 索引在 outer 上也能用 | Indexed nested-loop 要用 inner 上的索引 |
| 选 block 少的表做 outer | 这里通常看 tuple 数少的表 |
忘记扫描 outer 的 br | 公式要加 + br |
| 非等值连接也能用 | 通常只适合 equi-join / natural join |
7. Merge Join:两边排序后像拉链一样合并
是什么
也叫 sort-merge join。
先让两张表都按 join attribute 排序,然后从头到尾线性扫描。
例如两边按 customer_name 排序:
text
depositor: A, B, D, H
customer: A, A, C, D, H扫描过程类似合并两个有序数组。
适用条件
只适用于:
text
equi-join
natural join因为它依赖“相等的 join key 会排在一起”。
核心思想
排序后:
如果
r.key < s.key,移动 r;如果
r.key > s.key,移动 s;如果相等,输出匹配组合。
输入已经有序时的代价
text
BT = br + bs因为每个 relation 顺序读一遍。
如果输入没有排序,需要加 external sort-merge 的代价。
优点
输入已有序时非常快;
顺序 I/O;
可以顺便产生有序输出;
适合之后还有
order by或group by的情况。
缺点
只适合等值连接;
未排序时要先排序,排序成本可能很高;
如果某个 join key 有大量重复值,需要小心处理一组 tuple 的组合。
易错点
| 错误 | 正确 |
|---|---|
Merge join 可以处理 <, > join | 主要用于 equi/natural join |
只算 br + bs | 前提是两边已经有序 |
排序 fan-in 用 M | external sort-merge 用 M - 1 |
8. Hash Join:把可能匹配的 tuple 分到同一个桶
是什么
Hash join 是大表等值连接的常用算法。
它利用一个关键事实:
如果两个 tuple 的 join key 相等,那么对 join key 做相同 hash function 后,它们一定进入同一个 partition。
所以不需要全表互相比较,只需要对应分区之间连接。
适用条件
只适用于:
text
equi-join
natural join因为 hash 只能保证相等值进入同一桶,不能处理 <, >, != 这类条件。
两个阶段
阶段 1:Partition
对两个关系用同一个 hash function h 按 join attribute 分区。
text
r → Hr0, Hr1, ..., Hrn
s → Hs0, Hs1, ..., Hsn如果:
text
h(r.key) = i则 tuple 进入 Hri。
如果两个 tuple 的 key 相等,它们一定进入同编号分区:
text
Hri 与 Hsi所以只需要做:
text
Hr0 ⋈ Hs0
Hr1 ⋈ Hs1
...
Hrn ⋈ Hsn阶段 2:Build and Probe
通常选较小关系 s 做 build input。
对每个分区 i:
text
1. 把 Hsi 读入内存
2. 在 Hsi 上建内存 hash index
3. 扫描 Hri
4. 对每个 tuple 去内存 hash index 中查匹配为什么 build input 要选小表?
因为 build partition 要放入内存。
如果 build relation 太大,partition 放不下,就要 recursive partitioning,代价上升。
无递归分区时的代价
PPT 给出:
text
BT = 3(br + bs) + 4 * nh通常忽略 partially filled blocks 时:
text
BT ≈ 3(br + bs)为什么是 3 倍?
partition 阶段:
读 r、s 一遍:
br + bs写 r、s 分区:
br + bs合计
2(br + bs)
join 阶段:
- 再读各分区一遍:
br + bs
- 再读各分区一遍:
总计:
text
3(br + bs)如果 build input 整个能放入内存
那就不用 partition,直接:
text
1. 把 build relation 读入内存建 hash table
2. 扫描 probe relation代价:
text
BT = br + bs这是 hash join 最理想情况。
是否需要 recursive partitioning?
关键条件:
text
每个 build partition ≤ M - 2如果平均分区后:
text
bs / (M - 1) > M - 2就可能需要递归分区。
近似记忆:
text
bs > (M - 1)^2 时,可能需要递归分区PPT 例子
text
M = 20
depositor: b = 100
customer: b = 400选 depositor 做 build input:
text
bs = 100
M - 2 = 18要让每个 build partition 不超过 18 blocks:
text
ceil(100 / 18) = 6所以分成 6 个 partitions。
每个 depositor partition 大约:
text
100 / 6 ≈ 17能放进内存,不需要递归分区。
忽略 partially filled blocks:
text
BT = 3(100 + 400)
= 1500比 indexed nested-loop 的 20100 小很多。
易错点
| 错误 | 正确 |
|---|---|
| Hash join 可用于任意 join | 只适合 equi/natural join |
| build input 随便选 | 应选较小关系 |
| 只要总 build relation 大于 M 就不能用 | 关键是每个 partition 能否放入内存 |
| 分区 hash 和内存 hash index 是同一个 | 通常使用不同 hash function |
| hash join 一定最快 | skew、overflow、内存不足会让代价上升 |
9. 五种 Join 算法怎么选?
可以用这张决策表。
| 场景 | 优先考虑 |
|---|---|
任意 join condition,例如 <, >, != | Block nested-loop join |
| equi-join,inner join attribute 有索引,outer 较小 | Indexed nested-loop join |
| equi-join,两边已经按 join key 排序 | Merge join |
| equi-join,两边无序,大表连接 | Hash join |
| build relation 能全部进内存 | In-memory hash join,代价约 br + bs |
| 内存很小,无索引,无排序 | Block nested-loop join |
| 查询还需要有序输出 | Merge join 可能更合适 |
| secondary index 匹配很多记录 | 小心 indexed nested-loop,可能随机 I/O 很贵 |
10. Join 算法代价总表
| 算法 | 适用条件 | Block transfer cost |
|---|---|---|
| Nested-loop join | 任意 join | nr * bs + br |
| Block nested-loop join, M=3 | 任意 join | br * bs + br |
| Block nested-loop join, M>3 | 任意 join | ceil(br/(M-2))*bs + br |
| Indexed nested-loop join | equi/natural,inner 有索引 | nr * c + br |
| Indexed NLJ with B+ tree | inner B+ tree lookup | nr * (Hi+1) + br |
| Merge join, already sorted | equi/natural | br + bs |
| Merge join, unsorted | equi/natural | sort(r) + sort(s) + br + bs |
| Hash join, no recursion | equi/natural | 3(br + bs),简化版 |
| Hash join, build in memory | equi/natural | br + bs |
注意:
表中
r默认是 outer relation;s默认是 inner 或 build relation,具体看算法;不同算法中
r/s的角色含义不完全一样,做题时要先标清楚。
11. 做 Join 计算题的固定套路
遇到 Join 题,按这个流程做。
Step 1:写清楚统计量
例如:
text
R: nr = 10000, br = 400
S: ns = 5000, bs = 100
M = 10Step 2:判断 join 类型
text
是否 equi-join / natural join?如果不是等值连接:
text
不能用 indexed NLJ / merge join / hash join
通常考虑 nested-loop 或 block nested-loopStep 3:判断有什么物理条件
text
有没有索引?
索引在哪个 relation?
relation 是否已经排序?
内存 M 多大?
build partition 能否放进内存?Step 4:确定角色
不同算法角色不同:
| 算法 | 角色选择 |
|---|---|
| Nested-loop | 比较 nr*bs+br |
| Block nested-loop | 通常 block 少的做 outer |
| Indexed nested-loop | 有索引的 relation 做 inner |
| Hash join | 小 relation 做 build input |
| Merge join | 两边对称,关键看是否已排序 |
Step 5:代入公式
不要混淆 tuple 和 block。
12. 用同一组数据横向比较
假设:
text
depositor:
nr = 5000
br = 100
customer:
ns = 10000
bs = 40012.1 Nested-loop join
depositor 做 outer:
text
BT = 5000 * 400 + 100
= 2,000,100customer 做 outer:
text
BT = 10000 * 100 + 400
= 1,000,40012.2 Block nested-loop join
depositor 做 outer:
text
BT = 100 * 400 + 100
= 40,100customer 做 outer:
text
BT = 400 * 100 + 400
= 40,400所以 depositor 做 outer 稍好。
12.3 Indexed nested-loop join
假设 customer 在 join attribute 上有 B+ tree index,高度为 3。
depositor 做 outer,customer 做 inner:
text
BT = 5000 * (3 + 1) + 100
= 20,10012.4 Hash join
假设 depositor 做 build input,且不需要 recursive partitioning:
text
BT = 3(100 + 400)
= 150012.5 已排序 merge join
如果两边已经按 join key 排序:
text
BT = 100 + 400
= 500但如果没排序,要加排序成本。
13. 最重要的直觉排序
在理想条件下,代价大致可能是:
text
已排序 merge join / build in-memory hash join
< hash join
< indexed nested-loop join
< block nested-loop join
< tuple nested-loop join但这不是绝对排序。
反例:
如果两边没排序,merge join 要先排序,可能不划算;
如果 indexed nested-loop 的 outer 很大,且 inner 的 secondary index 导致随机 I/O,可能很慢;
如果 hash join 有严重 skew,可能 overflow;
如果一个表非常小,nested-loop 也可能足够好。
14. 最容易考的判断题
判断 1
“Hash join 可以用于任意 theta join。”
错误。
Hash join 主要用于 equi-join / natural join。
判断 2
“Block nested-loop join 一定比 indexed nested-loop join 慢。”
错误。
如果索引选择性差,或者 secondary index 导致大量随机 I/O,indexed nested-loop 也可能慢。
判断 3
“Merge join 的代价总是 br + bs。”
错误。
只有两边已经按 join attribute 排序时才是。否则要加排序成本。
判断 4
“Indexed nested-loop join 要求 outer relation 上有索引。”
错误。
要求 inner relation 的 join attribute 上有索引。
判断 5
“Hash join 中 build input 应该选较大的 relation。”
错误。
通常选较小 relation,因为 build partition 要放入内存。
15. 一句话总结每个 Join
text
Nested-loop:
最通用,最暴力,按 tuple 两两比较。
Block nested-loop:
用 block 批处理,减少扫描 inner 的次数。
Indexed nested-loop:
outer 每条 tuple 去 inner 的索引里查。
Merge join:
两边按 join key 排序,然后线性归并。
Hash join:
两边按 join key 哈希分区,只连接对应分区。真正做题时,先问四个问题:
text
1. 是不是 equi-join?
2. inner 上有没有索引?
3. 两边是否已经排序?
4. 内存 M 是否足够支持 hash join 或 block nested-loop 优化?这四个问题问完,Join operation 基本就能选算法、套公式、算代价。