📘 教案 23:外部排序(External Sorting · 工程级)
一、问题模型(必须精确定义)
给定一个包含 (N) 条记录的数据集,单条记录大小为 ® 字节;主存(RAM)可用容量为 (M) 字节;磁盘以**块(block)**为单位读写,每块大小为 (B) 字节。
目标:对全部数据按某键排序。
约束:
N⋅R≫M\]\[ N \\cdot R \\gg M \]\[N⋅R≫M
即:数据无法一次性装入内存。
二、评价指标:I/O 复杂度
外部排序的瓶颈不是 CPU,而是磁盘 I/O 次数。标准度量为:
I/O cost=读块数+写块数\]\[ \\text{I/O cost} = \\text{读块数} + \\text{写块数} \]\[I/O cost=读块数+写块数
我们以"扫描全数据一次"的 I/O 代价为基准:
Θ!(NRB)\]\[ \\Theta!\\left(\\frac{N R}{B}\\right) \]\[Θ!(BNR)
三、两阶段多路归并(TPMMS)
外部排序的标准算法是 Two-Phase Multiway Merge Sort (TPMMS):
Phase 1:生成初始有序段(runs)
- 每次读入至多 (M) 字节(约 (M/R) 条记录)
- 在内存中排序(任意内部排序算法)
- 写回磁盘形成一个有序段(run)
得到有序段数量:
r≈⌈NRM⌉\]\[ r \\approx \\left\\lceil \\frac{N R}{M} \\right\\rceil \]\[r≈⌈MNR⌉
Phase 2:多路归并(k-way merge)
- 同时打开 (k) 个有序段
- 为每个段分配一个输入缓冲区,为输出分配一个输出缓冲区
- 使用最小堆(优先队列)维护当前各段的最小元素
- 反复输出最小元素,直到完成
四、关键参数:归并路数 (k)
设每个缓冲区大小为 (B),需要:
- (k) 个输入缓冲
- 1 个输出缓冲
内存约束:
(k+1)⋅B≤M⇒k≤⌊MB⌋−1\]\[ (k + 1) \\cdot B \\le M \\Rightarrow k \\le \\left\\lfloor \\frac{M}{B} \\right\\rfloor - 1 \]\[(k+1)⋅B≤M⇒k≤⌊BM⌋−1
五、归并轮数(passes)
若一次不能归并完(即 (r > k)),需多轮归并。
归并轮数约为:
⌈logkr⌉\]\[ \\left\\lceil \\log_k r \\right\\rceil \]\[⌈logkr⌉
六、总 I/O 复杂度
-
Phase 1:读一遍 + 写一遍
2⋅Θ!(NRB)\]\[ 2 \\cdot \\Theta!\\left(\\frac{N R}{B}\\right) \]\[2⋅Θ!(BNR)
-
Phase 2:每一轮归并读+写一遍
总计:
Θ!(NRB)⋅(1+⌈logkr⌉)⋅2\]\[ \\Theta!\\left(\\frac{N R}{B}\\right) \\cdot \\left(1 + \\left\\lceil \\log_k r \\right\\rceil \\right) \\cdot 2 \]\[Θ!(BNR)⋅(1+⌈logkr⌉)⋅2
简化表达(忽略常数):
O!(NBlogMBNM)\]\[ O!\\left(\\frac{N}{B} \\log_{\\frac{M}{B}} \\frac{N}{M}\\right) \]\[O!(BNlogBMMN)
这就是外部排序的经典 I/O 复杂度。
七、Phase 1 优化:替换选择(Replacement Selection)
目标:让初始有序段更长,减少 ®。
方法:
- 使用最小堆维护当前可输出元素
- 新读入元素若 (\ge) 上一次输出,则继续当前 run
- 否则标记为"下一 run"
结果:在随机输入下,平均 run 长度约为:
≈2M\]\[ \\approx 2M \]\[≈2M
于是:
r≈NR2M\]\[ r \\approx \\frac{N R}{2M} \]\[r≈2MNR
👉 直接减少一半的段数,从而减少归并轮数。
八、归并实现细节(可落地)
1. 缓冲区设计
- 每个输入 run 分配 1 个 block 缓冲
- 输出 1 个 block 缓冲
- I/O 采用顺序读写(避免随机 I/O)
2. 数据结构
- 最小堆大小为 (k)
- 每个堆元素记录:当前值 + 来源 run 的标识
3. 流程
-
各 run 读入首块到输入缓冲
-
将各缓冲的首元素入堆
-
反复:
- 弹出最小值写入输出缓冲
- 从对应 run 的缓冲取下一个元素入堆
- 若该缓冲耗尽,则读入该 run 的下一块
九、工程优化要点
1. 增大 (k)
k↑⇒归并轮数↓\]\[ k \\uparrow \\Rightarrow \\text{归并轮数} \\downarrow \]\[k↑⇒归并轮数↓
但受限于 (M/B)。
2. 顺序 I/O
- 避免随机读写(性能数量级差异)
- 文件按 run 连续存储
3. 压缩 / 列式存储(场景相关)
减少 ®,从而减少 I/O 量。
4. 并行化
-
多磁盘并行读写
-
MapReduce / Spark:
- Map:生成局部有序段
- Shuffle:按 key 分区
- Reduce:分区内归并
十、与内存排序的本质差异
| 维度 | 内存排序 | 外部排序 |
|---|---|---|
| 资源瓶颈 | CPU | I/O |
| 访问模式 | 随机访问 | 顺序访问优先 |
| 优化目标 | 比较次数 | I/O 次数 |
十一、适用场景
- TB / PB 级日志排序
- 数据仓库(ETL 排序阶段)
- 数据库 ORDER BY / GROUP BY
- 大规模去重(结合外部排序 + 归并去重)
十二、结论性表述
外部排序通过将数据划分为可内存处理的有序段,并以多路归并的方式逐步合并,在受限内存条件下以最小化 I/O 次数为目标完成全局排序,其性能由块大小、内存容量与归并路数共同决定。