Skip to content

Commit 8fc409a

Browse files
authored
fix(query): avoid spill at Top-N limit boundary (#20186)
1 parent 144d982 commit 8fc409a

3 files changed

Lines changed: 116 additions & 2 deletions

File tree

src/query/pipeline/transforms/src/processors/transforms/sorts/sort_merge_limit.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ impl<R: Rows> MergeSort<R> for TransformSortMergeLimit<R> {
5757
self.next_index += 1;
5858

5959
let mut cursor = Cursor::new(input_index, init_rows);
60-
self.num_bytes += block.memory_size() as u64;
60+
let block_num_bytes = block.memory_size() as u64;
61+
self.num_bytes += block_num_bytes;
6162
self.num_rows += block.num_rows();
6263
let cur_index = input_index;
6364
self.buffer.insert(cur_index, block);
@@ -81,6 +82,15 @@ impl<R: Rows> MergeSort<R> for TransformSortMergeLimit<R> {
8182
cursor.advance();
8283
}
8384

85+
// String views may keep source buffers alive after filtering or slicing. Compact only
86+
// blocks that remain in the Top-N candidate set so discarded blocks avoid the copy.
87+
if let Some(block) = self.buffer.remove(&cur_index) {
88+
self.num_bytes -= block_num_bytes;
89+
let block = block.maybe_gc();
90+
self.num_bytes += block.memory_size() as u64;
91+
self.buffer.insert(cur_index, block);
92+
}
93+
8494
Ok(())
8595
}
8696

@@ -116,6 +126,57 @@ impl<R: Rows> MergeSort<R> for TransformSortMergeLimit<R> {
116126
}
117127
}
118128

129+
#[cfg(test)]
130+
mod tests {
131+
use databend_common_exception::Result;
132+
use databend_common_expression::Column;
133+
use databend_common_expression::DataBlock;
134+
use databend_common_expression::FromData;
135+
use databend_common_expression::types::Int32Type;
136+
use databend_common_expression::types::StringType;
137+
138+
use super::MergeSort;
139+
use super::TransformSortMergeLimit;
140+
use crate::sorts::core::Rows;
141+
use crate::sorts::core::SimpleRowsAsc;
142+
143+
#[test]
144+
fn test_top_n_compacts_retained_string_views() -> Result<()> {
145+
const SOURCE_ROWS: i32 = 2_000;
146+
const LIMIT: usize = 10;
147+
148+
let payload_suffix = "x".repeat(256);
149+
let keys = (0..SOURCE_ROWS).collect::<Vec<_>>();
150+
let payloads = keys
151+
.iter()
152+
.map(|key| format!("{key:08}-{payload_suffix}"))
153+
.collect::<Vec<_>>();
154+
let block = DataBlock::new_from_columns(vec![
155+
Int32Type::from_data(keys),
156+
StringType::from_data(payloads),
157+
])
158+
.slice(0..LIMIT);
159+
let rows = SimpleRowsAsc::<Int32Type>::from_column(&block.get_by_offset(0).to_column())?;
160+
161+
let mut sort = TransformSortMergeLimit::create(4_096, LIMIT);
162+
sort.add_block(block, rows)?;
163+
164+
let retained = sort.buffer.values().next().unwrap();
165+
let Column::String(payloads) = retained.get_by_offset(1).to_column() else {
166+
unreachable!("expected string payload column")
167+
};
168+
assert_eq!(payloads.total_bytes_len(), LIMIT * (8 + 1 + 256));
169+
assert!(
170+
payloads.total_buffer_len() < 16 * 1024,
171+
"Top-N retained {} bytes of source string buffers",
172+
payloads.total_buffer_len(),
173+
);
174+
assert_eq!(sort.num_bytes().0, retained.memory_size() as u64);
175+
176+
Ok(())
177+
}
178+
}
179+
119180
#[derive(Clone, Copy)]
120181
struct LocalCursorOrder;
121182

src/query/service/src/pipelines/processors/transforms/sort/sort_builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ impl<S: SortSpiller> TransformSortBuilder<S> {
212212
}
213213

214214
fn should_use_sort_limit(&self) -> bool {
215-
self.limit.map(|limit| limit < 10000).unwrap_or_default()
215+
self.limit.map(|limit| limit <= 10000).unwrap_or_default()
216216
}
217217

218218
fn new_base(&self) -> Base<S> {

tests/sqllogictests/suites/mode/standalone/explain/sort.test

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,59 @@ digraph {
138138
17 -> 18 [ label = "" ]
139139
}
140140

141+
# LIMIT 10000 is the inclusive boundary for the Top-N merge path.
142+
# Disable lazy read so this only verifies sort implementation selection.
143+
query T
144+
settings(lazy_read_threshold = 0) explain pipeline select a, b from t1 order by a limit 10000;
145+
----
146+
digraph {
147+
0 [ label = "PartitionStreamSource" ]
148+
1 [ label = "AsyncReadDataTransform" ]
149+
2 [ label = "DeserializeDataTransform" ]
150+
3 [ label = "Resize" ]
151+
4 [ label = "SortPartialTransform" ]
152+
5 [ label = "SortPartialTransform" ]
153+
6 [ label = "SortPartialTransform" ]
154+
7 [ label = "SortPartialTransform" ]
155+
8 [ label = "TransformSortMergeLimit" ]
156+
9 [ label = "TransformSortMergeLimit" ]
157+
10 [ label = "TransformSortMergeLimit" ]
158+
11 [ label = "TransformSortMergeLimit" ]
159+
12 [ label = "KWayMergePartitioner" ]
160+
13 [ label = "KWayMergeWorker" ]
161+
14 [ label = "KWayMergeWorker" ]
162+
15 [ label = "KWayMergeWorker" ]
163+
16 [ label = "KWayMergeWorker" ]
164+
17 [ label = "KWayMergeCombiner" ]
165+
18 [ label = "LimitTransform" ]
166+
19 [ label = "CompoundBlockOperator(Project)" ]
167+
0 -> 1 [ label = "" ]
168+
1 -> 2 [ label = "" ]
169+
2 -> 3 [ label = "" ]
170+
3 -> 4 [ label = "from: 0, to: 0" ]
171+
3 -> 5 [ label = "from: 1, to: 0" ]
172+
3 -> 6 [ label = "from: 2, to: 0" ]
173+
3 -> 7 [ label = "from: 3, to: 0" ]
174+
4 -> 8 [ label = "" ]
175+
5 -> 9 [ label = "" ]
176+
6 -> 10 [ label = "" ]
177+
7 -> 11 [ label = "" ]
178+
8 -> 12 [ label = "from: 0, to: 0" ]
179+
9 -> 12 [ label = "from: 0, to: 1" ]
180+
10 -> 12 [ label = "from: 0, to: 2" ]
181+
11 -> 12 [ label = "from: 0, to: 3" ]
182+
12 -> 13 [ label = "from: 0, to: 0" ]
183+
12 -> 14 [ label = "from: 1, to: 0" ]
184+
12 -> 15 [ label = "from: 2, to: 0" ]
185+
12 -> 16 [ label = "from: 3, to: 0" ]
186+
13 -> 17 [ label = "from: 0, to: 0" ]
187+
14 -> 17 [ label = "from: 0, to: 1" ]
188+
15 -> 17 [ label = "from: 0, to: 2" ]
189+
16 -> 17 [ label = "from: 0, to: 3" ]
190+
17 -> 18 [ label = "" ]
191+
18 -> 19 [ label = "" ]
192+
}
193+
141194

142195
# Sort spilling
143196
statement ok

0 commit comments

Comments
 (0)