DuckDB/ORDER BY · 本地排序块

按字节排序 ORDER BY 的定长键

DuckDB 将排序键组织为可按字节比较的表示,定长短键用 LSD 基数排序,每轮通过计数和累计偏移稳定分配行。

v1.1.3检索与计算核验 2026-10-12
相关概念

工程问题#

数据库 ORDER BY 需要反复比较多个列并同步重排行。对于定长键,若每次比较都解读数据类型、空值和排序方向,会在大量行排序中重复执行这些工作。

实际实现#

PhysicalOrder 收集排序键与 payload,LocalSortState 生成本地有序块。RadixSort 根据输入选择实现;不含字符串、记录数超过短数组阈值且键宽不超过 LSD 阈值时,RadixSortLSD 从最低有效字节到最高字节逐轮统计计数、计算累计位置,再从后向前稳定散布记录。这里搬运的排序记录包含序列化键和行索引,完整 payload 在之后按排序结果重排。

处理流程#

  1. ORDER BY 计算键表达式并把键与 payload 送入本地排序状态
  2. 按键类型、记录数和键宽选择 LSD 路径
  3. 统计当前字节的 256 种取值并累计各桶结束位置
  4. 从后向前按桶位置写入目标缓冲区,保持相同字节的原相对次序
  5. 交换读写缓冲区,继续上一字节;排序块随后参与合并

源码片段#

固定版本 · 原文摘录
src/execution/operator/order/physical_order.cppv1.1.3 · L86–L103
查看原文件
86	// Obtain sorting columns87	auto &keys = lstate.keys;88	keys.Reset();89	lstate.key_executor.Execute(chunk, keys);9091	auto &payload = lstate.payload;92	payload.ReferenceColumns(chunk, projections);9394	// Sink the data into the local sort state95	keys.Verify();96	chunk.Verify();97	local_sort_state.SinkChunk(keys, payload);9899	// When sorting data reaches a certain size, we sort it100	if (local_sort_state.SizeInBytes() >= gstate.memory_per_thread) {101		local_sort_state.Sort(global_sort_state, true);102	}103	return SinkResultType::NEED_MORE_INPUT;
PhysicalOrder::Sink

ORDER BY 求出键、收集 payload,并在本地数据达到线程内存额度时调用排序。

CPPMIT
src/common/sort/sort_state.cppv1.1.3 · L237–L258
查看原文件
237void LocalSortState::Sort(GlobalSortState &global_sort_state, bool reorder_heap) {238	D_ASSERT(radix_sorting_data->count == payload_data->count);239	if (radix_sorting_data->count == 0) {240		return;241	}242	// Move all data to a single SortedBlock243	sorted_blocks.emplace_back(make_uniq<SortedBlock>(*buffer_manager, global_sort_state));244	auto &sb = *sorted_blocks.back();245	// Fixed-size sorting data246	auto sorting_block = ConcatenateBlocks(*radix_sorting_data);247	sb.radix_sorting_data.push_back(std::move(sorting_block));248	// Variable-size sorting data249	if (!sort_layout->all_constant) {250		auto &blob_data = *blob_sorting_data;251		auto new_block = ConcatenateBlocks(blob_data);252		sb.blob_sorting_data->data_blocks.push_back(std::move(new_block));253	}254	// Payload data255	auto payload_block = ConcatenateBlocks(*payload_data);256	sb.payload_data->data_blocks.push_back(std::move(payload_block));257	// Now perform the actual sort258	SortInMemory();
LocalSortState::Sort → SortInMemory

把已有行数据整理为本地排序块,随后调用 SortInMemory。

CPPMIT
src/common/sort/radix_sort.cppv1.1.3 · L305–L324
查看原文件
305	for (idx_t i = 0; i < sort_layout->column_count; i++) {306		sorting_size += sort_layout->column_sizes[i];307		contains_string = contains_string || sort_layout->logical_types[i].InternalType() == PhysicalType::VARCHAR;308		if (sort_layout->constant_size[i] && i < sort_layout->column_count - 1) {309			// Add columns to the sorting size until we reach a variable size column, or the last column310			continue;311		}312313		if (!ties) {314			// This is the first sort315			RadixSort(*buffer_manager, dataptr, count, col_offset, sorting_size, *sort_layout, contains_string);316			ties_ptr = make_unsafe_uniq_array_uninitialized<bool>(count);317			ties = ties_ptr.get();318			std::fill_n(ties, count - 1, true);319			ties[count - 1] = false;320		} else {321			// For subsequent sorts, we only have to subsort the tied tuples322			SubSortTiedTuples(*buffer_manager, dataptr, count, col_offset, sorting_size, ties, *sort_layout,323			                  contains_string);324		}
LocalSortState::SortInMemory → RadixSort

累积排序列的键宽并记录是否包含字符串;首次排序调用 RadixSort,后续仅处理仍相等的记录。

CPPMIT
src/common/sort/radix_sort.cppv1.1.3 · L238–L255
查看原文件
238//! Calls different sort functions, depending on the count and sorting sizes239void RadixSort(BufferManager &buffer_manager, const data_ptr_t &dataptr, const idx_t &count, const idx_t &col_offset,240               const idx_t &sorting_size, const SortLayout &sort_layout, bool contains_string) {241242	if (contains_string) {243		auto begin = duckdb_pdqsort::PDQIterator(dataptr, sort_layout.entry_size);244		auto end = begin + count;245		duckdb_pdqsort::PDQConstants constants(sort_layout.entry_size, col_offset, sorting_size, *end);246		return duckdb_pdqsort::pdqsort_branchless(begin, begin + count, constants);247	}248249	if (count <= SortConstants::INSERTION_SORT_THRESHOLD) {250		return InsertionSort(dataptr, nullptr, count, col_offset, sort_layout.entry_size, sorting_size, 0, false);251	}252253	if (sorting_size <= SortConstants::MSD_RADIX_SORT_SIZE_THRESHOLD) {254		return RadixSortLSD(buffer_manager, dataptr, count, col_offset, sort_layout.entry_size, sorting_size);255	}
RadixSort:算法选择

实际选择条件是字符串、行数和键宽;最后展示 LSD 分支,较宽键的 MSD 分支紧接其后。

CPPMIT
src/common/sort/radix_sort.cppv1.1.3 · L115–L134
查看原文件
115	idx_t counts[SortConstants::VALUES_PER_RADIX];116	for (idx_t r = 1; r <= sorting_size; r++) {117		// Init counts to 0118		memset(counts, 0, sizeof(counts));119		// Const some values for convenience120		const data_ptr_t source_ptr = swap ? temp_block.get() : dataptr;121		const data_ptr_t target_ptr = swap ? dataptr : temp_block.get();122		const idx_t offset = col_offset + sorting_size - r;123		// Collect counts124		data_ptr_t offset_ptr = source_ptr + offset;125		for (idx_t i = 0; i < count; i++) {126			counts[*offset_ptr]++;127			offset_ptr += row_width;128		}129		// Compute offsets from counts130		idx_t max_count = counts[0];131		for (idx_t val = 1; val < SortConstants::VALUES_PER_RADIX; val++) {132			max_count = MaxValue<idx_t>(max_count, counts[val]);133			counts[val] = counts[val] + counts[val - 1];134		}
RadixSortLSD:逐字节计数

由低位字节向前推进,统计当前字节的频率,再把计数转换为累计结束位置。

CPPMIT
src/common/sort/radix_sort.cppv1.1.3 · L135–L151
查看原文件
135		if (max_count == count) {136			continue;137		}138		// Re-order the data in temporary array139		data_ptr_t row_ptr = source_ptr + (count - 1) * row_width;140		for (idx_t i = 0; i < count; i++) {141			idx_t &radix_offset = --counts[*(row_ptr + offset)];142			FastMemcpy(target_ptr + radix_offset * row_width, row_ptr, row_width);143			row_ptr -= row_width;144		}145		swap = !swap;146	}147	// Move data back to original buffer (if it was swapped)148	if (swap) {149		memcpy(dataptr, temp_block.get(), count * row_width);150	}151}
RadixSortLSD:稳定散布

从后向前读取输入,每次先减少累计位置再放置记录,因此同值记录保持原顺序;全相等字节可跳过。

CPPMIT

调用链覆盖 ORDER BY 收集、LocalSortState、算法分派及 LSD 两个核心步骤。2021 年官方排序文章用于解释设计背景,具体分派条件以 v1.1.3 源码为准。

收益与取舍#

工程分析

带来的收益

  • 每轮计数分配避免逐对比较完整的多列键
  • 稳定的单字节排序使已处理低位的次序得以保留
  • 同一字节全部相等时可跳过该轮数据搬运

付出的代价

  • LSD 路径需要 O(n × 排序记录宽度) 临时缓冲,且每轮都可能搬运记录
  • 计数扫描为 O(d(n+256));考虑拷贝时还需乘入记录宽度,不能忽略宽排序记录的搬运成本
  • 字符串、小输入和较宽键可以走其他排序分支,不能将全部 ORDER BY 都描述为 LSD 基数排序

可比较的选择

逐记录比较排序无需将键按位拆解,适用于更一般的比较器;多列类型判断与字符串比较可能重复发生。

MSD 基数排序从高位开始细分桶,适合更宽的键;需要递归桶管理和短桶回退,源码中保留此分支。

版本与适用范围#

固定于 DuckDB v1.1.3 的本地排序块,重点是 RadixSortLSD 及其每轮计数排序。字符串走 PDQsort,短数组走插入排序,较宽定长键走 MSD;完整 ORDER BY 还包括键编码、块合并和可能的外部存储。

参考来源#

源码、官方文档与原始论文
01
PhysicalOrder::Sink

src/execution/operator/order/physical_order.cpp · PhysicalOrder::Sink

源码v1.1.3
02
LocalSortState::Sort → SortInMemory

src/common/sort/sort_state.cpp · LocalSortState::Sort → SortInMemory

源码v1.1.3
03
LocalSortState::SortInMemory → RadixSort

src/common/sort/radix_sort.cpp · LocalSortState::SortInMemory → RadixSort

源码v1.1.3
04
RadixSort:算法选择

src/common/sort/radix_sort.cpp · RadixSort:算法选择

源码v1.1.3
05
RadixSortLSD:逐字节计数

src/common/sort/radix_sort.cpp · RadixSortLSD:逐字节计数

源码v1.1.3
06
RadixSortLSD:稳定散布

src/common/sort/radix_sort.cpp · RadixSortLSD:稳定散布

源码v1.1.3
07官方文档2021-08-27(设计背景;具体实现以 v1.1.3 为准)
19 个匹配案例