外部排序生成与归并
- 概述
- 源码解读
-
- [Tuplesortstate 结构体](#Tuplesortstate 结构体)
- [tuplesort_performsort 函数](#tuplesort_performsort 函数)
- [dumptuples 函数](#dumptuples 函数)
- [mergeruns 函数](#mergeruns 函数)
- 案例分析
声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-15.0 的开源代码和《PostgresSQL数据库内核分析》一书
概述
在 Sort 算子整体流程、tuplesort_performsort 状态机,前文梳理了 PG Sort 算子执行框架 与排序状态机流转 ,其中 TSS_BUILDRUNS 分支对应大数据外部排序。
本文聚焦该分支下 dumptuples 与 mergeruns 源码:数据超 work_mem 时,dumptuples 将内存元组排序后落地磁盘,生成多个有序 Run;全部数据分片完成后,mergeruns 采用平衡 K 路归并 ,多轮轮转合并磁盘有序段,最终得到全局有序数据。二者构成 PG 外部排序 "生成有序段 + 多路归并" 完整实现。
源码解读
Tuplesortstate 结构体
Tuplesortstate 是 PostgreSQL 外部排序(Tuplesort) 的核心私有状态结构体。
Tuplesort 是 PostgreSQL 中处理大规模排序的核心引擎,用于以下场景:
ORDER BY+ 大量数据(无法全放内存时)CREATE INDEX(尤其是B-tree索引)DISTINCT、GROUP BY(哈希聚合放不下时)WINDOW函数、UNION等需要排序的操作
当内存不足以容纳所有元组时,Tuplesort 会采用外排序(External Merge Sort) 算法:
- 第一阶段(
BUILDRUNS) :边读边排序,内存满后生成一个已排序的"归并段(Run)"写入磁盘磁带。 - 第二阶段(
FINALMERGE) :多路归并(polyphase merge),将多个Run合并成最终有序结果。
该结构体就是支撑整个算法全流程的状态机,涵盖了内存管理、磁带调度、阶段控制、并行协调、优化机制等所有关键信息。
c
/*
* Tuplesort 操作的私有状态结构体
*
* 该结构体保存了外部排序(external sort)过程中所需的所有内部状态,
* 包括内存管理、磁带(tape)管理、堆排序状态、并行协调等。
*/
struct Tuplesortstate
{
TuplesortPublic base; /* 公共部分,对外部可见的接口状态 */
TupSortStatus status; /* 当前排序阶段的枚举状态(如 INITIAL、BUILDRUNS、FINALMERGE 等) */
bool bounded; /* 调用者是否指定了返回元组的最大数量(LIMIT 优化) */
bool boundUsed; /* 是否实际使用了有界堆(bounded heap)优化 */
int bound; /* 有界模式下允许返回的最大元组数量 */
int64 tupleMem; /* 当前所有元组占用的内存总量(单独统计,用于 dump 到磁带时快速扣减) */
int64 availMem; /* 当前剩余可用内存(字节) */
int64 allowedMem; /* 允许使用的总内存上限(字节) */
int maxTapes; /* 每次合并阶段允许使用的最大输入磁带数量 */
int64 maxSpace; /* 排序过程中占用的最大空间(内存或磁盘),用于监控 */
bool isMaxSpaceDisk; /* maxSpace 当前记录的是磁盘空间还是内存空间 */
TupSortStatus maxSpaceStatus; /* 达到 maxSpace 时所处的排序阶段 */
LogicalTapeSet *tapeset; /* 逻辑磁带集合对象(logtape.c),管理所有临时文件上的磁带 */
/*
* 内存中的元组数组
* - INITIAL 阶段:无序
* - SORTEDINMEM 阶段:已完成最终排序
* - BUILDRUNS / FINALMERGE 阶段:按堆序(heap order)组织
* - SORTEDONTAPE 阶段:该数组不再使用
*/
SortTuple *memtuples; /* SortTuple 结构体数组,存放当前在内存中的元组 */
int memtupcount; /* 当前内存中实际存放的元组数量 */
int memtupsize; /* memtuples 数组当前分配的长度 */
bool growmemtuples; /* memtuples 数组是否仍在动态增长中 */
/*
* Slab 分配器(固定大小槽位复用机制)
* 在进入合并阶段后启用,用于避免频繁 palloc/pfree 开销。
* 每个槽位大小固定(SLAB_SLOT_SIZE),主要用于保存合并堆中的少量元组。
*/
bool slabAllocatorUsed; /* 是否启用了 slab 分配器 */
char *slabMemoryBegin; /* slab 内存区域起始地址 */
char *slabMemoryEnd; /* slab 内存区域结束地址 */
SlabSlot *slabFreeHead; /* 空闲槽位链表头 */
size_t tape_buffer_mem; /* 为输入/输出磁带缓冲区分配的内存大小 */
/*
* 当从磁带返回元组给调用者时(TSS_SORTEDONTAPE 或 TSS_FINALMERGE 状态),
* 会暂存最后一个返回的元组,以便下次 gettuple 时复用其内存。
*/
void *lastReturnedTuple; /* 上一次从磁带返回的元组指针(用于内存回收) */
int currentRun; /* 构建初始归并段(run)时为当前 run 编号;完成后为初始 run 的总数 */
/*
* 逻辑磁带管理(多路归并核心)
* 初始 runs 写到 outputTapes,合并时 inputTapes 和 outputTapes 角色互换。
*/
LogicalTape **inputTapes; /* 当前合并阶段的输入磁带数组 */
int nInputTapes; /* 当前输入磁带数量 */
int nInputRuns; /* 当前需要合并的初始 run 数量 */
LogicalTape **outputTapes; /* 当前合并阶段的输出磁带数组 */
int nOutputTapes; /* 当前输出磁带数量 */
int nOutputRuns; /* 当前已产生的输出 run 数量 */
LogicalTape *destTape; /* 当前正在写入的输出磁带 */
/*
* 排序完成后,用于迭代返回结果的状态
*/
LogicalTape *result_tape; /* 最终结果所在的磁带(SORTEDONTAPE 模式) */
int current; /* 当前返回位置(仅在 SORTEDINMEM 模式下使用数组下标) */
bool eof_reached; /* 是否已到达结果末尾(支持游标) */
/* mark/restore 支持(用于游标回滚) */
int64 markpos_block; /* 标记位置的磁带块号 */
int markpos_offset; /* 标记位置的偏移或数组索引 */
bool markpos_eof; /* 标记位置是否已到 EOF */
/*
* 并行排序相关状态
*/
int worker; /* 当前 worker ID:-1 表示 leader 或串行,>=0 表示 worker */
Sharedsort *shared; /* 共享内存状态,用于 leader 与 worker 之间协调 */
int nParticipants; /* leader 已知的实际参与排序的 worker 数量 */
/*
* 缩写键(abbreviated key)优化相关
* 用于加速比较(如 text 类型的前缀比较),定期检查优化效果。
*/
int64 abbrevNext; /* 下一次检查 abbreviated key 优化有效性的元组序号 */
/*
* 性能统计
*/
PGRUsage ru_start; /* 排序开始时的资源使用快照(CPU、内存等) */
};
功能分类表格
| 功能类别 | 结构体成员 | 详细说明 |
|---|---|---|
| 基础状态 | base, status |
公共接口 + 当前排序阶段 |
有界排序(LIMIT) |
bounded, boundUsed, bound |
支持 LIMIT 优化,只保留 Top-N |
| 内存管理 | tupleMem, availMem, allowedMem, growmemtuples |
内存使用跟踪与动态扩展 |
Slab 分配器 |
slabAllocatorUsed, slabMemoryBegin, slabMemoryEnd, slabFreeHead |
合并阶段高效固定槽位复用 |
| 磁带管理 | tapeset, inputTapes, nInputTapes, outputTapes, nOutputTapes, destTape |
逻辑磁带读写调度 |
归并段(Run)管理 |
currentRun, nInputRuns, nOutputRuns |
初始 Run 数量与合并进度 |
| 结果迭代 | result_tape, current, eof_reached, lastReturnedTuple |
排序完成后逐 tuple 返回 |
| 游标支持 | markpos_block, markpos_offset, markpos_eof |
支持 FETCH ... FROM CURSOR 的 mark/restore |
| 并行排序 | worker, shared, nParticipants |
工作进程协调与共享状态 |
| 性能优化 | abbrevNext |
缩写键(abbreviated key)优化 |
| 资源监控 | maxSpace, isMaxSpaceDisk, maxSpaceStatus, ru_start, tape_buffer_mem |
空间峰值统计与性能分析 |
这个结构体设计非常精巧,完整支撑了 PostgreSQL 在海量数据下的高性能排序能力,是数据库内核中非常经典的外部排序实现。
tuplesort_performsort 函数
有关 tuplesort_performsort 函数的详细内容可以参考【PostgreSQL内核学习:深入理解 PostgreSQL 中的 tuplesort_performsort 函数】这篇文章。
dumptuples 函数
调用作用概述
dumptuples函数的 核心作用 :将内存中积累的元组转储到磁盘磁带,形成一个有序的初始归并段(Initial Run) 。这是 PostgreSQL Tuplesort 外部排序算法第一阶段(BUILDRUNS) 中最关键的函数之一。
调用时机
- 内存使用量达到上限(
LACKMEM(state)为真)时; - 或者输入数据已全部读取完毕(
alltuples = true)时的最终转储。
主要执行流程
-
提前返回检查
如果内存还够用且不是最终调用,则跳过转储,继续在内存中积累元组(提高效率)。
-
空
run保护避免生成空的归并段(除非是并行
worker)。 -
选择输出磁带
通过
selectnewtape()决定把当前run写到哪一个逻辑磁带上(实现轮转或多路分布)。 -
快速排序
调用
tuplesort_sort_memtuples()对内存中的元组进行原地快速排序,使其变为有序。 -
写入磁带
将排序后的所有元组依次通过
WRITETUP写入当前目标磁带(destTape)。 -
内存清理
- 清空
memtupcount; - 重置
tuplecontext内存上下文(防止碎片); - 更新内存统计(
FREEMEM); - 在磁带上标记本
run结束(markrunend)。
- 清空
在整体排序算法中的意义
- 第一阶段(
Run Generation) :不断重复"边读边排序 → 内存满则dumptuples→ 继续读"的过程,生成多个已排序的归并段(Runs)。 - 这些
run后续会进入第二阶段(FINALMERGE) 进行多路归并,最终得到全局有序的结果。 - 该函数是内存与磁盘之间的桥梁 ,直接决定了外部排序的
I/O效率和整体性能。
关键设计亮点
- 支持有界排序(
LIMIT) 优化; - 考虑内存碎片问题(
MemoryContextReset); - 支持并行排序 (
worker标识); - 详细的
trace日志,便于调试性能; - 避免产生空
run,减少不必要的I/O。
中文注释后的完整源码
c
/*
* dumptuples - 将内存中的元组转储到磁带,形成一个初始归并段(initial run)
*
* 当 alltuples = true 时,表示这是输入数据结束时的最终转储操作,
* 将当前内存中所有剩余元组全部写入磁带。
*
* 该函数是外部排序(External Merge Sort)第一阶段(BUILDRUNS)的核心操作:
* 当内存即将满时,把当前已排序的元组写到磁盘,形成一个有序的"run"。
*/
static void
dumptuples(Tuplesortstate *state, bool alltuples)
{
int memtupwrite;
int i;
/*
* 优化:如果内存还有足够空间,并且数组槽位也够用,且不是最终转储调用,
* 则不需要立即转储,继续积累元组。
*/
if (state->memtupcount < state->memtupsize && !LACKMEM(state) &&
!alltuples)
return;
/*
* 特殊情况处理:
* 如果内存中已经没有元组,且之前已经生成过至少一个 run,则直接返回。
* 目的是避免产生空的归并段(empty run)。
* 但在并行 worker 中,即使为空也必须生成至少一个 tape。
*/
if (state->memtupcount == 0 && state->currentRun > 0)
return;
Assert(state->status == TSS_BUILDRUNS);
/*
* 防止 run 数量溢出(理论上极少发生,但需安全检查)
*/
if (state->currentRun == INT_MAX)
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("cannot have more than %d runs for an external sort",
INT_MAX)));
/*
* 为新的 run 选择一个输出磁带(如果不是第一个 run)
*/
if (state->currentRun > 0)
selectnewtape(state);
state->currentRun++; /* 当前 run 编号递增 */
if (trace_sort)
elog(LOG, "worker %d starting quicksort of run %d: %s",
state->worker, state->currentRun,
pg_rusage_show(&state->ru_start));
/*
* 对当前内存中的所有元组进行快速排序(quicksort)
*/
tuplesort_sort_memtuples(state);
if (trace_sort)
elog(LOG, "worker %d finished quicksort of run %d: %s",
state->worker, state->currentRun,
pg_rusage_show(&state->ru_start));
/*
* 将排序后的所有元组顺序写入当前目标磁带
*/
memtupwrite = state->memtupcount;
for (i = 0; i < memtupwrite; i++)
{
SortTuple *stup = &state->memtuples[i];
WRITETUP(state, state->destTape, stup);
}
/* 清空内存中的元组计数 */
state->memtupcount = 0;
/*
* 重置 tuple 专用的内存上下文
* 这是为了避免长时间运行中出现内存碎片,尤其在元组大小差异很大时非常重要。
* 在有界排序(bounded sort)中,这一操作特别关键。
*/
MemoryContextReset(state->base.tuplecontext);
/*
* 更新内存使用统计:扣除已写入磁盘的元组占用的内存
*/
FREEMEM(state, state->tupleMem);
state->tupleMem = 0;
/* 在磁带上标记当前 run 的结束 */
markrunend(state->destTape);
if (trace_sort)
elog(LOG, "worker %d finished writing run %d to tape %d: %s",
state->worker, state->currentRun,
(state->currentRun - 1) % state->nOutputTapes + 1,
pg_rusage_show(&state->ru_start));
}
函数流程图
#mermaid-svg-ZLekQaOpuUw6KOYx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZLekQaOpuUw6KOYx .error-icon{fill:#552222;}#mermaid-svg-ZLekQaOpuUw6KOYx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZLekQaOpuUw6KOYx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZLekQaOpuUw6KOYx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZLekQaOpuUw6KOYx .marker.cross{stroke:#333333;}#mermaid-svg-ZLekQaOpuUw6KOYx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZLekQaOpuUw6KOYx p{margin:0;}#mermaid-svg-ZLekQaOpuUw6KOYx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx .cluster-label text{fill:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx .cluster-label span{color:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx .cluster-label span p{background-color:transparent;}#mermaid-svg-ZLekQaOpuUw6KOYx .label text,#mermaid-svg-ZLekQaOpuUw6KOYx span{fill:#333;color:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx .node rect,#mermaid-svg-ZLekQaOpuUw6KOYx .node circle,#mermaid-svg-ZLekQaOpuUw6KOYx .node ellipse,#mermaid-svg-ZLekQaOpuUw6KOYx .node polygon,#mermaid-svg-ZLekQaOpuUw6KOYx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZLekQaOpuUw6KOYx .rough-node .label text,#mermaid-svg-ZLekQaOpuUw6KOYx .node .label text,#mermaid-svg-ZLekQaOpuUw6KOYx .image-shape .label,#mermaid-svg-ZLekQaOpuUw6KOYx .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZLekQaOpuUw6KOYx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZLekQaOpuUw6KOYx .rough-node .label,#mermaid-svg-ZLekQaOpuUw6KOYx .node .label,#mermaid-svg-ZLekQaOpuUw6KOYx .image-shape .label,#mermaid-svg-ZLekQaOpuUw6KOYx .icon-shape .label{text-align:center;}#mermaid-svg-ZLekQaOpuUw6KOYx .node.clickable{cursor:pointer;}#mermaid-svg-ZLekQaOpuUw6KOYx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZLekQaOpuUw6KOYx .arrowheadPath{fill:#333333;}#mermaid-svg-ZLekQaOpuUw6KOYx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZLekQaOpuUw6KOYx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZLekQaOpuUw6KOYx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZLekQaOpuUw6KOYx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZLekQaOpuUw6KOYx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZLekQaOpuUw6KOYx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZLekQaOpuUw6KOYx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZLekQaOpuUw6KOYx .cluster text{fill:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx .cluster span{color:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZLekQaOpuUw6KOYx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZLekQaOpuUw6KOYx rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZLekQaOpuUw6KOYx .icon-shape,#mermaid-svg-ZLekQaOpuUw6KOYx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZLekQaOpuUw6KOYx .icon-shape p,#mermaid-svg-ZLekQaOpuUw6KOYx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZLekQaOpuUw6KOYx .icon-shape .label rect,#mermaid-svg-ZLekQaOpuUw6KOYx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZLekQaOpuUw6KOYx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZLekQaOpuUw6KOYx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZLekQaOpuUw6KOYx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
是
否
开始 dumptuples
内存未满?
且未触发内存不足?
且不是强制写出?
直接 return
无需写磁盘
内存元组为空?
且已生成过归并段?
直接 return
避免生成空 Run
断言检查:当前状态 = TSS_BUILDRUNS
归并段数量是否达到上限?
抛出错误,终止排序
currentRun > 0?
selectnewtape()
选择新磁带
currentRun++
归并段编号+1
对内存元组执行快速排序
循环将所有有序元组写入磁带
清空内存元组计数 memtupcount = 0
MemoryContextReset
重置元组内存,避免碎片
FREEMEM 归还元组内存
tupleMem = 0
markrunend
标记当前归并段结束
函数结束
已完成:mergeruns 函数完整中文注释与详细解读
mergeruns 函数
函数作用概述
mergeruns 是 PostgreSQL Tuplesort 外部排序的第二阶段核心函数 ,负责将第一阶段(dumptuples)生成的所有初始有序归并段(Initial Runs)通过多路平衡归并(Balanced k-Way Merge 算法合并成最终的有序结果。
它实现了经典的外部归并排序 的后半部分:多次合并 passes,直到只剩一个最终有序的 run 为止。该函数在内存不足以容纳全部数据时,是实现高效外部排序的关键。
详细逻辑解释
mergeruns 函数的主要工作是完成多趟(multi-pass)归并过程:
-
准备阶段:
- 禁用
abbreviated key优化(因为从磁盘读回的tuple已无abbreviated keys)。 - 重置内存上下文,切换到
slab分配器(合并阶段只需少量固定元组)。 - 释放大的
memtuples数组,重新分配较小的数组用于归并堆(每个输入磁带一个tuple)。 - 分配磁带缓冲区内存。
- 禁用
-
多趟归并循环:
- 当输入
run用完时,启动新一轮合并pass:把上一轮的输出磁带转为输入磁带。 - 为每个输入磁带分配合适的读缓冲区。
- 如果满足条件(无需随机访问 + 只剩最后一次合并),可直接进入
TSS_FINALMERGE状态并提前返回,实现on-the-fly归并。 - 否则,选择输出磁带,调用
mergeonerun合并一组run(从每个输入磁带取一个run进行k路归并)。 - 重复上述过程,直到所有
run合并为一个最终run。
- 当输入
-
结束阶段:
- 将最终结果磁带设置为
result_tape。 - 冻结磁带(
Freeze),标记为只读。 - 关闭所有输入磁带,释放资源。
- 将状态置为
TSS_SORTEDONTAPE。
- 将最终结果磁带设置为
该函数是外部排序从"多 run"到"单有序结果"的关键桥梁,充分复用磁带资源,支持多趟合并以适应有限的 maxTapes 限制。
中文注释后的完整源码
c
/*
* mergeruns -- 合并所有已完成的初始归并段(initial runs)
*
* 该函数实现了经典的 Balanced k-Way Merge(平衡 k 路归并)算法。
* 在调用此函数之前,所有输入数据已通过 dumptuples() 写成多个有序的初始 run,
* 存放在不同的逻辑磁带上。
*/
static void
mergeruns(Tuplesortstate *state)
{
int tapenum;
Assert(state->status == TSS_BUILDRUNS);
Assert(state->memtupcount == 0);
/*
* 如果启用了 abbreviated key 优化,在进入合并阶段时需要关闭它。
* 因为从磁盘读回的元组不会携带 abbreviated keys,且我们也不再需要重新生成。
*/
if (state->base.sortKeys != NULL && state->base.sortKeys->abbrev_converter != NULL)
{
state->base.sortKeys->abbrev_converter = NULL;
state->base.sortKeys->comparator = state->base.sortKeys->abbrev_full_comparator;
/* 清理不再使用的函数指针 */
state->base.sortKeys->abbrev_abort = NULL;
state->base.sortKeys->abbrev_full_comparator = NULL;
}
/*
* 重置 tuple 内存上下文
* 此时之前分配的元组都已释放,后续将切换使用 slab 分配器
*/
MemoryContextResetOnly(state->base.tuplecontext);
/*
* 释放之前用于初始 run 构建的大 memtuples 数组
* 合并阶段只需要较小的堆数组
*/
FREEMEM(state, GetMemoryChunkSpace(state->memtuples));
pfree(state->memtuples);
state->memtuples = NULL;
/*
* 初始化 slab 分配器
* 需要为每个输入磁带分配一个槽位(用于归并堆),再加一个槽位保存最后返回的元组
*/
if (state->base.tuples)
init_slab_allocator(state, state->nOutputTapes + 1);
else
init_slab_allocator(state, 0);
/*
* 为归并堆重新分配 memtuples 数组
* 该数组大小等于输入磁带数量,每个元素保存来自一个输入磁带的当前最小元组
*/
state->memtupsize = state->nOutputTapes;
state->memtuples = (SortTuple *) MemoryContextAlloc(state->base.maincontext,
state->nOutputTapes * sizeof(SortTuple));
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
/*
* 将剩余可用内存全部用于磁带缓冲区
* 在每一轮合并开始时,会在输入和输出磁带之间动态划分这部分内存
*/
state->tape_buffer_mem = state->availMem;
USEMEM(state, state->tape_buffer_mem);
if (trace_sort)
elog(LOG, "worker %d using %zu KB of memory for tape buffers",
state->worker, state->tape_buffer_mem / 1024);
/* ====================== 多趟归并主循环 ====================== */
for (;;)
{
/*
* 当输入 run 耗尽时(包括第一次进入),开始新一轮合并 pass
* 将上一轮的输出磁带转为本轮的输入磁带
*/
if (state->nInputRuns == 0)
{
int64 input_buffer_size;
/* 关闭上一轮已读空的输入磁带 */
if (state->nInputTapes > 0)
{
for (tapenum = 0; tapenum < state->nInputTapes; tapenum++)
LogicalTapeClose(state->inputTapes[tapenum]);
pfree(state->inputTapes);
}
/* 角色转换:上一轮输出 → 本轮输入 */
state->inputTapes = state->outputTapes;
state->nInputTapes = state->nOutputTapes;
state->nInputRuns = state->nOutputRuns;
/* 重置输出磁带信息,准备接收新数据 */
state->outputTapes = palloc0(state->nInputTapes * sizeof(LogicalTape *));
state->nOutputTapes = 0;
state->nOutputRuns = 0;
/*
* 计算本轮每个输入磁带的读缓冲区大小
*/
input_buffer_size = merge_read_buffer_size(state->tape_buffer_mem,
state->nInputTapes,
state->nInputRuns,
state->maxTapes);
if (trace_sort)
elog(LOG, "starting merge pass of %d input runs on %d tapes, "
INT64_FORMAT " KB of memory for each input tape: %s",
state->nInputRuns, state->nInputTapes,
input_buffer_size / 1024,
pg_rusage_show(&state->ru_start));
/* 倒带所有输入磁带,为读取做好准备 */
for (tapenum = 0; tapenum < state->nInputTapes; tapenum++)
LogicalTapeRewindForRead(state->inputTapes[tapenum], input_buffer_size);
/*
* 优化:如果只剩最后一轮合并,且不需要支持随机访问(非游标场景),
* 则可直接进入最终归并阶段(on-the-fly),无需再写中间磁带
*/
if ((state->base.sortopt & TUPLESORT_RANDOMACCESS) == 0
&& state->nInputRuns <= state->nInputTapes
&& !WORKER(state))
{
LogicalTapeSetForgetFreeSpace(state->tapeset);
beginmerge(state);
state->status = TSS_FINALMERGE;
return;
}
}
/* 为本轮合并选择一个新的输出磁带 */
selectnewtape(state);
/* 执行一轮 k 路归并:从每个输入磁带取一个 run 进行合并 */
mergeonerun(state);
/*
* 如果输入 run 已全部处理完,且本轮只产生了一个输出 run,
* 则整个归并过程结束
*/
if (state->nInputRuns == 0 && state->nOutputRuns <= 1)
break;
}
/*
* 归并完成,最终结果只剩一个 run 在一个磁带上
*/
state->result_tape = state->outputTapes[0];
if (!WORKER(state))
LogicalTapeFreeze(state->result_tape, NULL); /* 冻结磁带,标记为只读 */
else
worker_freeze_result_tape(state);
state->status = TSS_SORTEDONTAPE;
/* 关闭所有输入磁带,释放读缓冲区 */
for (tapenum = 0; tapenum < state->nInputTapes; tapenum++)
LogicalTapeClose(state->inputTapes[tapenum]);
}
函数流程图
#mermaid-svg-s1PRKkjxvn64ln22{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-s1PRKkjxvn64ln22 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-s1PRKkjxvn64ln22 .error-icon{fill:#552222;}#mermaid-svg-s1PRKkjxvn64ln22 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-s1PRKkjxvn64ln22 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-s1PRKkjxvn64ln22 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-s1PRKkjxvn64ln22 .marker.cross{stroke:#333333;}#mermaid-svg-s1PRKkjxvn64ln22 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-s1PRKkjxvn64ln22 p{margin:0;}#mermaid-svg-s1PRKkjxvn64ln22 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-s1PRKkjxvn64ln22 .cluster-label text{fill:#333;}#mermaid-svg-s1PRKkjxvn64ln22 .cluster-label span{color:#333;}#mermaid-svg-s1PRKkjxvn64ln22 .cluster-label span p{background-color:transparent;}#mermaid-svg-s1PRKkjxvn64ln22 .label text,#mermaid-svg-s1PRKkjxvn64ln22 span{fill:#333;color:#333;}#mermaid-svg-s1PRKkjxvn64ln22 .node rect,#mermaid-svg-s1PRKkjxvn64ln22 .node circle,#mermaid-svg-s1PRKkjxvn64ln22 .node ellipse,#mermaid-svg-s1PRKkjxvn64ln22 .node polygon,#mermaid-svg-s1PRKkjxvn64ln22 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-s1PRKkjxvn64ln22 .rough-node .label text,#mermaid-svg-s1PRKkjxvn64ln22 .node .label text,#mermaid-svg-s1PRKkjxvn64ln22 .image-shape .label,#mermaid-svg-s1PRKkjxvn64ln22 .icon-shape .label{text-anchor:middle;}#mermaid-svg-s1PRKkjxvn64ln22 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-s1PRKkjxvn64ln22 .rough-node .label,#mermaid-svg-s1PRKkjxvn64ln22 .node .label,#mermaid-svg-s1PRKkjxvn64ln22 .image-shape .label,#mermaid-svg-s1PRKkjxvn64ln22 .icon-shape .label{text-align:center;}#mermaid-svg-s1PRKkjxvn64ln22 .node.clickable{cursor:pointer;}#mermaid-svg-s1PRKkjxvn64ln22 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-s1PRKkjxvn64ln22 .arrowheadPath{fill:#333333;}#mermaid-svg-s1PRKkjxvn64ln22 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-s1PRKkjxvn64ln22 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-s1PRKkjxvn64ln22 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s1PRKkjxvn64ln22 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-s1PRKkjxvn64ln22 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s1PRKkjxvn64ln22 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-s1PRKkjxvn64ln22 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-s1PRKkjxvn64ln22 .cluster text{fill:#333;}#mermaid-svg-s1PRKkjxvn64ln22 .cluster span{color:#333;}#mermaid-svg-s1PRKkjxvn64ln22 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-s1PRKkjxvn64ln22 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-s1PRKkjxvn64ln22 rect.text{fill:none;stroke-width:0;}#mermaid-svg-s1PRKkjxvn64ln22 .icon-shape,#mermaid-svg-s1PRKkjxvn64ln22 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s1PRKkjxvn64ln22 .icon-shape p,#mermaid-svg-s1PRKkjxvn64ln22 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-s1PRKkjxvn64ln22 .icon-shape .label rect,#mermaid-svg-s1PRKkjxvn64ln22 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s1PRKkjxvn64ln22 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-s1PRKkjxvn64ln22 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-s1PRKkjxvn64ln22 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
是
否
否
否
是
开始 mergeruns()
断言校验: 状态=BUILDRUNS 且 内存元组为空
关闭缩写键优化(abbreviated key)
重置元组内存上下文
释放旧的大内存元组数组
初始化 Slab 内存分配器
重新分配小内存数组(用于归并堆)
分配磁带缓冲区内存
进入无限循环: 多轮归并主循环
nInputRuns == 0?
(新一轮归并开始)
关闭旧输入磁带, 释放资源
角色反转: 上轮输出磁带 = 本轮输入磁带
重置输出磁带结构
计算归并读缓冲区大小
倒回所有输入磁带, 准备读取
是否满足最终归并优化?
(无需随机访问+单轮归并)
进入 TSS_FINALMERGE 状态, 函数返回
继续执行归并
选择新的输出磁带
执行 mergeonerun: 合并一个有序段
输入段已空 且 输出段<=1?
跳出归并循环
设置结果磁带, 冻结结果
设置状态为 TSS_SORTEDONTAPE
关闭所有输入磁带, 释放资源
函数结束
案例分析
场景设置
sql
-- 用户表,150万条数据
CREATE TABLE users (
id INT,
name VARCHAR(50)
);
-- 插入 150 万条近似随机数据
INSERT INTO users
SELECT (random()*1000000)::int, 'user_' || i
FROM generate_series(1, 1500000) i;
-- 执行大排序(触发外部排序)
SELECT * FROM users ORDER BY id;
关键参数假设(真实常见配置):
work_mem = 4MB- 每条
SortTuple大约占 40 字节 (含overhead) - 内存大约能放下 约 10 万条 元组(含
memtuples数组等开销) maintenance_work_mem较大(索引场景),但这里是普通查询,走work_memmaxTapes默认通常是8~32,这里我们假设maxTapes = 6(更真实)
第一阶段:dumptuples ------ 生成初始有序 Runs
dumptuples 的职责是:当内存快满时,把当前内存中的无序元组排序后,完整写入一个磁盘 Run(归并段)。
- 开始读取数据 (
Tuplesort处于TSS_BUILDRUNS状态) - 第1轮 :
- 不断调用
puttuple把数据放入memtuples数组 - 当
LACKMEM(state)为真(内存不足 )时 → 调用dumptuples(state, false)
- 不断调用
dumptuples 内部执行(关键代码对应):
c
if (state->memtupcount < state->memtupsize && !LACKMEM(state) && !alltuples)
return; -- 内存还够,继续读
tuplesort_sort_memtuples(state); -- 快速排序(quicksort)
for (i = 0; i < memtupcount; i++)
WRITETUP(state, state->destTape, &memtuples[i]); -- 写入当前 destTape
state->memtupcount = 0;
MemoryContextReset(state->base.tuplecontext); -- 防碎片
FREEMEM(state, state->tupleMem);
markrunend(state->destTape); -- 标记 Run 结束
- 重复
15次 :- 共产生
15个初始Run(Run 1 ~ Run 15) - 每个
Run约10万条,已在磁盘上有序
- 共产生
最终磁盘状态(第一阶段结束):
Tape 0: Run 1 (10万,有序)
Tape 1: Run 2 (10万,有序)
...
Tape 5: Run 6 (10万,有序)
... 继续轮转写入
总计 15 个有序初始 Run
currentRun = 15
第二阶段:mergeruns ------ 多路平衡归并
mergeruns 使用平衡 k 路归并 算法,把多个初始 Run 通过多次合并 pass ,最终合并成一个全局有序的 result_tape 。
进入 mergeruns(state) 后:
c
/* 准备工作 */
禁用 abbreviated key 优化;
切换到 slab 分配器;
重新分配小的 memtuples 数组(用于归并堆);
tape_buffer_mem = 剩余内存;
然后进入多趟归并主循环:
c
for (;;) {
if (state->nInputRuns == 0) { -- 需要启动新的一轮 pass
// 把上一轮的 outputTapes 变成 inputTapes
state->inputTapes = state->outputTapes;
...
// 计算每条输入磁带的读缓冲区大小
input_buffer_size = merge_read_buffer_size(...);
for each input tape: LogicalTapeRewindForRead();
// 最后一轮优化判断
if (只需最后一轮 && !需要随机访问) {
state->status = TSS_FINALMERGE;
return; -- 直接进入最终 on-the-fly 归并
}
}
selectnewtape(state); -- 选一个输出磁带
mergeonerun(state); -- 执行一次 k 路归并
}
本例真实归并过程(maxTapes=6)
第1轮归并(
Pass 1):
- 输入:
15个Run- 每次
mergeonerun做6路归并(使用最小堆)- 共需要执行
3次mergeonerun:
- 合并
Run1~6→ 输出Run A(60万条)- 合并
Run7~12→ 输出Run B(60万条)- 合并
Run13~15→ 输出Run C(30万条)
此时磁盘状态:
Run A (60万,有序)
Run B (60万,有序)
Run C (30万,有序)
第2轮归并(Pass 2):
- 输入:
3个Run(A、B、C) - 由于只有
3个< maxTapes=6,直接做 3 路归并 mergeonerun一次合并完 → 输出Final Run(150万条,全局有序)
归并结束:
state->result_tape = outputTapes[0]LogicalTapeFreeze(result_tape)status = TSS_SORTEDONTAPE
最终总结对比表
| 阶段 | 函数 | 做了什么 | 对应代码关键动作 | 本例结果 |
|---|---|---|---|---|
| 第一阶段 | dumptuples |
内存满 → 排序 → 写磁盘生成 Run |
quicksort + WRITETUP + markrunend |
15 个小有序 Run |
| 第二阶段 | mergeruns |
多趟 k 路归并,直到只剩 1 个 Run |
多 pass 循环 + mergeonerun + 磁带切换 |
最终 1 个全局有序 Run |
| 最终返回 | tuplesort_gettuple |
从 result_tape 逐条返回给用户 |
LogicalTapeRead |
有序结果输出 |