【PostgreSQL内核学习 —— 外部排序生成与归并】

外部排序生成与归并

声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。

本文主要参考了 postgresql-15.0 的开源代码和《PostgresSQL数据库内核分析》一书

概述

  在 Sort 算子整体流程tuplesort_performsort 状态机,前文梳理了 PG Sort 算子执行框架排序状态机流转 ,其中 TSS_BUILDRUNS 分支对应大数据外部排序。

  本文聚焦该分支下 dumptuplesmergeruns 源码:数据超 work_mem 时,dumptuples 将内存元组排序后落地磁盘,生成多个有序 Run;全部数据分片完成后,mergeruns 采用平衡 K 路归并 ,多轮轮转合并磁盘有序段,最终得到全局有序数据。二者构成 PG 外部排序 "生成有序段 + 多路归并" 完整实现。

源码解读

Tuplesortstate 结构体

  TuplesortstatePostgreSQL 外部排序(Tuplesort 的核心私有状态结构体。

  TuplesortPostgreSQL 中处理大规模排序的核心引擎,用于以下场景:

  • ORDER BY + 大量数据(无法全放内存时)
  • CREATE INDEX(尤其是 B-tree 索引)
  • DISTINCTGROUP BY(哈希聚合放不下时)
  • WINDOW 函数、UNION 等需要排序的操作

  当内存不足以容纳所有元组时,Tuplesort 会采用外排序(External Merge Sort 算法:

  1. 第一阶段(BUILDRUNS :边读边排序,内存满后生成一个已排序的"归并段(Run)"写入磁盘磁带。
  2. 第二阶段(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 CURSORmark/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)时的最终转储。

主要执行流程

  1. 提前返回检查

    如果内存还够用且不是最终调用,则跳过转储,继续在内存中积累元组(提高效率)。

  2. run 保护

    避免生成空的归并段(除非是并行 worker)。

  3. 选择输出磁带

    通过 selectnewtape() 决定把当前 run 写到哪一个逻辑磁带上(实现轮转或多路分布)。

  4. 快速排序

    调用 tuplesort_sort_memtuples() 对内存中的元组进行原地快速排序,使其变为有序。

  5. 写入磁带

    将排序后的所有元组依次通过 WRITETUP 写入当前目标磁带(destTape)。

  6. 内存清理

    • 清空 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 函数

函数作用概述

  mergerunsPostgreSQL 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_mem
  • maxTapes 默认通常是 8~32,这里我们假设 maxTapes = 6(更真实)

第一阶段:dumptuples ------ 生成初始有序 Runs

  dumptuples 的职责是:当内存快满时,把当前内存中的无序元组排序后,完整写入一个磁盘 Run(归并段)。

  1. 开始读取数据Tuplesort 处于 TSS_BUILDRUNS 状态)
  2. 第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 结束
  1. 重复 15
    • 共产生 15 个初始 RunRun 1 ~ Run 15
    • 每个 Run10 万条,已在磁盘上有序

最终磁盘状态(第一阶段结束)

复制代码
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

  • 输入:15Run
  • 每次 mergeonerun6 路归并(使用最小堆)
  • 共需要执行 3mergeonerun
    • 合并 Run1~6 → 输出 Run A60万条)
    • 合并 Run7~12 → 输出 Run B60万条)
    • 合并 Run13~15 → 输出 Run C30万条)

此时磁盘状态

复制代码
Run A (60万,有序)
Run B (60万,有序)
Run C (30万,有序)

2轮归并(Pass 2

  • 输入:3Run(A、B、C)
  • 由于只有 3< maxTapes=6,直接做 3 路归并
  • mergeonerun 一次合并完 → 输出 Final Run150万条,全局有序)

归并结束

  • state->result_tape = outputTapes[0]
  • LogicalTapeFreeze(result_tape)
  • status = TSS_SORTEDONTAPE

最终总结对比表

阶段 函数 做了什么 对应代码关键动作 本例结果
第一阶段 dumptuples 内存满 → 排序 → 写磁盘生成 Run quicksort + WRITETUP + markrunend 15 个小有序 Run
第二阶段 mergeruns 多趟 k 路归并,直到只剩 1Run pass 循环 + mergeonerun + 磁带切换 最终 1 个全局有序 Run
最终返回 tuplesort_gettuple result_tape 逐条返回给用户 LogicalTapeRead 有序结果输出

相关推荐
阿演1 小时前
我把这个桌面数据库工具又升级了一轮:现在支持 ClickHouse,还能可视化建表和改表了
数据库·clickhouse·ai编程·数据库连接工具
hunterkkk(c++)1 小时前
学习dijkstra算法(c++)
c++·学习·算法
AI浩1 小时前
学习率调度分层式精讲:Warmup、Cosine、Linear Decay 与大模型训练节奏(分层式精讲)
学习
我命由我123451 小时前
Excel - Excel 覆盖模式与编辑模式
运维·学习·职场和发展·excel·求职招聘·职场发展·运维开发
SAP庖丁解码1 小时前
SAP 物料凭证表详解
数据库
H_老邪1 小时前
Docker 学习之路-Linux安装指定版本docker
学习·docker·容器
「維他檸檬茶」1 小时前
大模型算法学习6.3
学习
Jul1en_1 小时前
【Redis】一文讲透缓存更新策略与缓存预热、穿透、雪崩、击穿
数据库·redis·缓存
数智工坊1 小时前
周志华《Machine Learning》学习笔记--第五章--神经网络
人工智能·笔记·神经网络·学习·机器学习