Skip to content

Commit c6d2891

Browse files
authored
fix: preserve imperfect block statistics for replace and merge (#20187)
* fix: preserve imperfect block statistics for replace and merge * fix test
1 parent 8fc409a commit c6d2891

2 files changed

Lines changed: 109 additions & 8 deletions

File tree

src/query/storages/fuse/src/operations/common/processors/transform_mutation_aggregator.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use databend_common_base::runtime::execute_futures_in_parallel;
2222
use databend_common_catalog::plan::BlockMetaWithHLL;
2323
use databend_common_catalog::table::Table;
2424
use databend_common_catalog::table_context::TableContext;
25+
use databend_common_exception::ErrorCode;
2526
use databend_common_exception::Result;
2627
use databend_common_expression::BlockMetaInfoPtr;
2728
use databend_common_expression::BlockThresholds;
@@ -387,10 +388,19 @@ impl TableMutationAggregator {
387388
.collect::<Vec<_>>();
388389
Some(SegmentStatistics::new(hlls, Vec::new()).to_bytes()?)
389390
};
390-
let all_perfect = new_blocks.len() > 1;
391+
// Only compaction/reclustering output may be force-marked perfect to keep
392+
// those operations at a fixed point. REPLACE/MERGE append after-images
393+
// must retain the physical perfect-block count from reduce_block_metas.
394+
let force_all_blocks_perfect = matches!(
395+
self.write_segment_ctx.kind,
396+
MutationKind::Compact | MutationKind::Recluster
397+
) && new_blocks.len() > 1;
391398

392399
let ctx = self.write_segment_ctx.clone();
393-
tasks.push(async move { ctx.write_segment(new_blocks, new_hlls, all_perfect).await });
400+
tasks.push(async move {
401+
ctx.write_segment(new_blocks, new_hlls, force_all_blocks_perfect)
402+
.await
403+
});
394404
}
395405

396406
let threads_nums = self.ctx.get_settings().get_max_threads()? as usize;
@@ -507,7 +517,7 @@ impl TableMutationAggregator {
507517
let write_segment_ctx = self.write_segment_ctx.clone();
508518

509519
tasks.push(async move {
510-
let mut all_perfect = false;
520+
let mut force_all_blocks_perfect = false;
511521
let (new_blocks, new_hlls, origin_summary) = if let Some(loc) = location {
512522
// read the old segment
513523
let compact_segment_info = SegmentsIO::read_compact_segment(
@@ -559,11 +569,20 @@ impl TableMutationAggregator {
559569
let stats = generate_segment_stats(new_hlls)?;
560570
(new_blocks, stats, Some(segment_info.summary))
561571
} else {
562-
// use by compact.
572+
// Only compact builds replacement segments without corresponding
573+
// entries in base_segments. Treating a missing base segment from
574+
// any other mutation as compact output could silently corrupt its
575+
// segment statistics.
576+
if !matches!(write_segment_ctx.kind, MutationKind::Compact) {
577+
return Err(ErrorCode::Internal(format!(
578+
"{} mutation references missing base segment index {}",
579+
write_segment_ctx.kind, index
580+
)));
581+
}
563582
assert!(segment_mutation.deleted_blocks.is_empty());
564583
// There are more than 1 blocks, means that the blocks can no longer be compacted.
565584
// They can be marked as perfect blocks.
566-
all_perfect = segment_mutation.replaced_blocks.len() > 1;
585+
force_all_blocks_perfect = segment_mutation.replaced_blocks.len() > 1;
567586
let (new_blocks, new_hlls) = segment_mutation
568587
.replaced_blocks
569588
.into_iter()
@@ -575,7 +594,7 @@ impl TableMutationAggregator {
575594
};
576595

577596
let new_segment_info = write_segment_ctx
578-
.write_segment(new_blocks, new_hlls, all_perfect)
597+
.write_segment(new_blocks, new_hlls, force_all_blocks_perfect)
579598
.await?;
580599

581600
Ok(SegmentLite {
@@ -830,14 +849,14 @@ impl WriteSegmentCtx {
830849
&self,
831850
blocks: Vec<Arc<BlockMeta>>,
832851
stats: Option<Vec<u8>>,
833-
all_perfect: bool,
852+
force_all_blocks_perfect: bool,
834853
) -> Result<(String, Statistics)> {
835854
let location = self
836855
.location_gen
837856
.gen_segment_info_location(self.table_meta_timestamps, false);
838857
let mut new_summary =
839858
reduce_block_metas(&blocks, self.thresholds, self.default_cluster_key);
840-
if all_perfect {
859+
if force_all_blocks_perfect {
841860
// To fix issue #13217.
842861
if new_summary.block_count > new_summary.perfect_block_count {
843862
warn!(
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
statement ok
2+
DROP DATABASE IF EXISTS issue_20181
3+
4+
statement ok
5+
CREATE DATABASE issue_20181
6+
7+
statement ok
8+
USE issue_20181
9+
10+
statement ok
11+
SET max_block_size = 6
12+
13+
# Keep block generation deterministic: each statement writes one perfect block
14+
# and one imperfect tail block, so compaction is triggered by the second write.
15+
statement ok
16+
SET max_threads = 1
17+
18+
# REPLACE append after-images must retain their physical imperfect-block count so
19+
# the second statement reaches the table-level auto-compaction threshold.
20+
statement ok
21+
CREATE TABLE replace_target(id UInt64)
22+
ROW_PER_BLOCK = 10
23+
AUTO_COMPACTION_IMPERFECT_BLOCKS_THRESHOLD = 2
24+
25+
statement ok
26+
REPLACE INTO replace_target ON(id)
27+
SELECT number FROM numbers(10)
28+
UNION ALL
29+
SELECT number + 10 FROM numbers(2)
30+
31+
statement ok
32+
REPLACE INTO replace_target ON(id)
33+
SELECT number + 12 FROM numbers(10)
34+
UNION ALL
35+
SELECT number + 22 FROM numbers(2)
36+
37+
# Each REPLACE writes one perfect block and one imperfect tail block. The second
38+
# tail reaches the threshold, and auto compaction merges both input segments.
39+
query IIB
40+
SELECT count(), sum(row_count), sum(block_count) < 4
41+
FROM fuse_segment('issue_20181', 'replace_target')
42+
----
43+
1 24 1
44+
45+
# MERGE unmatched inserts use the same AppendBlock segment-generation path.
46+
statement ok
47+
CREATE TABLE merge_target(id UInt64)
48+
ROW_PER_BLOCK = 10
49+
AUTO_COMPACTION_IMPERFECT_BLOCKS_THRESHOLD = 2
50+
51+
query I
52+
MERGE INTO merge_target AS t
53+
USING (
54+
SELECT number AS id FROM numbers(10)
55+
UNION ALL
56+
SELECT number + 10 AS id FROM numbers(2)
57+
) AS s
58+
ON t.id = s.id
59+
WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)
60+
----
61+
12
62+
63+
query I
64+
MERGE INTO merge_target AS t
65+
USING (
66+
SELECT number + 12 AS id FROM numbers(10)
67+
UNION ALL
68+
SELECT number + 22 AS id FROM numbers(2)
69+
) AS s
70+
ON t.id = s.id
71+
WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)
72+
----
73+
12
74+
75+
query IIB
76+
SELECT count(), sum(row_count), sum(block_count) < 4
77+
FROM fuse_segment('issue_20181', 'merge_target')
78+
----
79+
1 24 1
80+
81+
statement ok
82+
DROP DATABASE issue_20181

0 commit comments

Comments
 (0)