Skip to content

Commit 24e9fb2

Browse files
committed
refactor(query): simplify append topn stats commit
1 parent 6064b76 commit 24e9fb2

4 files changed

Lines changed: 86 additions & 141 deletions

File tree

src/query/storages/fuse/src/operations/commit.rs

Lines changed: 54 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -522,21 +522,13 @@ impl FuseTable {
522522
insert_hll: &BlockHLL,
523523
insert_rows: u64,
524524
insert_top_n: &BlockTopN,
525-
reuse_previous_stats: bool,
526525
refresh_top_n: bool,
527526
) -> Result<TableStatsGenerator> {
528-
let empty_snapshot = None;
529-
let stats_snapshot = if reuse_previous_stats {
530-
snapshot
531-
} else {
532-
&empty_snapshot
533-
};
534-
// Extract reusable base stats meta (row_count / hll, etc.) from snapshot.
535-
// Overwrite commits build statistics from inserted rows only.
536-
let summary = stats_snapshot.summary();
527+
// Extract previous stats meta (row_count / hll, etc.) from snapshot.
528+
let summary = snapshot.summary();
537529
let prev_stats_meta = summary.additional_stats_meta.as_ref();
538-
// Reusable table statistics file location (if any).
539-
let mut prev_stats_location = stats_snapshot.table_statistics_location();
530+
// Previous statistics file location (if any).
531+
let mut prev_stats_location = snapshot.table_statistics_location();
540532
let top_n_column_ids = if refresh_top_n && insert_rows > 0 {
541533
self.append_top_n_columns(self.schema())?
542534
.map(|(columns, _)| columns.values().map(|field| field.column_id()).collect())
@@ -559,43 +551,23 @@ impl FuseTable {
559551
));
560552
}
561553

562-
if insert_hll.is_empty() {
563-
let empty_hll = HashMap::new();
564-
let table_statistics = self
565-
.build_append_top_n_statistics(
566-
stats_snapshot,
567-
top_n_column_ids,
568-
&empty_hll,
569-
insert_top_n,
570-
table_row_count,
571-
)
572-
.await?;
573-
if let Some(stats) = &table_statistics {
574-
prev_stats_location = Some(self.new_table_statistics_location(stats)?);
575-
}
576-
577-
return Ok(TableStatsGenerator::new(
578-
prev_stats_meta.cloned(),
579-
prev_stats_location,
580-
0,
581-
0,
582-
HashMap::new(),
583-
table_statistics,
584-
));
585-
}
586-
587-
// Initialize a new HLL with inserted rows
588554
let mut new_hll = insert_hll.clone();
555+
let next_stats_meta = if insert_hll.is_empty() {
556+
prev_stats_meta.cloned()
557+
} else {
558+
None
559+
};
589560
// Calculate updated row_count
590-
let (hll_row_count, unstats_rows) = match prev_stats_meta {
561+
let (hll_row_count, unstats_rows) = match (!insert_hll.is_empty(), prev_stats_meta) {
562+
(false, _) => (0, 0),
591563
// Case 1: Previous stats exist and already contain HLL → merge directly
592-
Some(v) if v.hll.is_some() => {
564+
(true, Some(v)) if v.hll.is_some() => {
593565
let prev_hll = decode_column_hll(v.hll.as_ref().unwrap())?.unwrap();
594566
merge_column_hll_mut(&mut new_hll, &prev_hll);
595567
(v.row_count + insert_rows, v.unstats_rows)
596568
}
597569
// Case 2: Previous meta has no HLL → need to load from stats file
598-
_ => {
570+
(true, _) => {
599571
if let Some(loc) = &prev_stats_location {
600572
let ver = TableMetaLocationGenerator::table_statistics_version(loc);
601573
let reader = MetaReaders::table_snapshot_statistics_reader(self.get_operator());
@@ -653,7 +625,7 @@ impl FuseTable {
653625

654626
let table_statistics = if has_top_n_update {
655627
self.build_append_top_n_statistics(
656-
stats_snapshot,
628+
snapshot,
657629
top_n_column_ids,
658630
&new_hll,
659631
insert_top_n,
@@ -668,7 +640,7 @@ impl FuseTable {
668640
}
669641

670642
Ok(TableStatsGenerator::new(
671-
None,
643+
next_stats_meta,
672644
prev_stats_location,
673645
hll_row_count,
674646
unstats_rows,
@@ -701,28 +673,18 @@ impl FuseTable {
701673
} else {
702674
None
703675
};
704-
let empty_top_n = HashMap::new();
705-
let (mut top_n, append_top_n) = if let Some((previous, prev_stats)) = fresh_prev_stats {
706-
let prev_top_n = (previous.summary.row_count != 0).then_some(PreviousTopN {
707-
top_n: &prev_stats.top_n,
708-
summary: &previous.summary,
709-
});
710-
build_append_top_n_merge_inputs(top_n_column_ids, prev_top_n, insert_top_n)
711-
} else if let Some(previous) = snapshot.as_ref()
712-
&& previous.summary.row_count != 0
713-
{
714-
build_append_top_n_merge_inputs(
715-
top_n_column_ids,
716-
Some(PreviousTopN {
717-
top_n: &empty_top_n,
718-
summary: &previous.summary,
719-
}),
720-
insert_top_n,
721-
)
722-
} else {
723-
build_append_top_n_merge_inputs(top_n_column_ids, None, insert_top_n)
724-
};
725676

677+
let mut top_n = fresh_prev_stats
678+
.map(|(_, stats)| stats.top_n.clone())
679+
.unwrap_or_default();
680+
top_n.retain(|column_id, _| top_n_column_ids.contains(column_id));
681+
682+
let append_top_n = append_top_n_merge_input(
683+
&top_n_column_ids,
684+
snapshot.as_ref().map(|snapshot| &snapshot.summary),
685+
&top_n,
686+
insert_top_n,
687+
);
726688
merge_column_top_n_mut(&mut top_n, append_top_n)?;
727689
if top_n.is_empty() {
728690
return Ok(None);
@@ -753,58 +715,40 @@ impl FuseTable {
753715
}
754716
}
755717

756-
#[derive(Clone, Copy)]
757-
struct PreviousTopN<'a> {
758-
top_n: &'a BlockTopN,
759-
summary: &'a Statistics,
760-
}
761-
762-
impl PreviousTopN<'_> {
763-
fn missing_error(&self, column_id: ColumnId) -> Option<u64> {
764-
self.summary
765-
.col_stats
766-
.get(&column_id)
767-
.map(|stats| self.summary.row_count.saturating_sub(stats.null_count))
768-
}
769-
}
770-
771-
fn build_append_top_n_merge_inputs(
772-
top_n_column_ids: Vec<ColumnId>,
773-
prev_top_n: Option<PreviousTopN<'_>>,
718+
fn append_top_n_merge_input(
719+
column_ids: &[ColumnId],
720+
previous_summary: Option<&Statistics>,
721+
previous_top_n: &BlockTopN,
774722
insert_top_n: &BlockTopN,
775-
) -> (BlockTopN, BlockTopN) {
776-
let mut top_n = HashMap::new();
723+
) -> BlockTopN {
724+
let previous_summary = previous_summary.filter(|summary| summary.row_count != 0);
777725
let mut append_top_n = HashMap::new();
778726

779-
for column_id in top_n_column_ids {
780-
match prev_top_n {
781-
Some(prev_top_n) => {
782-
let append_column_top_n = insert_top_n.get(&column_id).cloned();
783-
if let Some(column_top_n) = prev_top_n.top_n.get(&column_id).cloned() {
784-
top_n.insert(column_id, column_top_n);
785-
if let Some(column_top_n) = append_column_top_n {
786-
append_top_n.insert(column_id, column_top_n);
787-
}
788-
} else if let Some(mut column_top_n) = append_column_top_n {
789-
let Some(missing_error) = prev_top_n.missing_error(column_id) else {
790-
continue;
791-
};
792-
for entry in &mut column_top_n.values {
793-
entry.count = entry.count.saturating_add(missing_error);
794-
entry.error = entry.error.saturating_add(missing_error);
795-
}
796-
append_top_n.insert(column_id, column_top_n);
797-
}
798-
}
799-
None => {
800-
if let Some(column_top_n) = insert_top_n.get(&column_id).cloned() {
801-
append_top_n.insert(column_id, column_top_n);
802-
}
803-
}
727+
for &column_id in column_ids {
728+
let Some(column_top_n) = insert_top_n.get(&column_id).cloned() else {
729+
continue;
730+
};
731+
if previous_top_n.contains_key(&column_id)
732+
|| column_has_no_previous_values(previous_summary, column_id)
733+
{
734+
append_top_n.insert(column_id, column_top_n);
804735
}
805736
}
806737

807-
(top_n, append_top_n)
738+
append_top_n
739+
}
740+
741+
fn column_has_no_previous_values(
742+
previous_summary: Option<&Statistics>,
743+
column_id: ColumnId,
744+
) -> bool {
745+
let Some(summary) = previous_summary else {
746+
return true;
747+
};
748+
summary
749+
.col_stats
750+
.get(&column_id)
751+
.is_none_or(|stats| stats.null_count == summary.row_count)
808752
}
809753

810754
pub(crate) fn is_fresh_table_snapshot_top_n(

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,17 +411,15 @@ async fn write_new_snapshot_and_build_table_meta(
411411
) -> Result<(TableMeta, u64)> {
412412
let fuse_table = FuseTable::try_from_table(table)?;
413413
let previous = fuse_table.read_table_snapshot().await?;
414-
let reuse_previous_stats = !snapshot_generator.is_overwrite();
415414
// Match single-table commits: transaction commits may collapse intermediate snapshot
416415
// lineage, so skip append TopN refresh until transaction stats invalidation is defined.
417-
let refresh_top_n = !txn_mgr.lock().is_active();
416+
let refresh_top_n = !snapshot_generator.is_overwrite() && !txn_mgr.lock().is_active();
418417
let mut table_stats_gen = fuse_table
419418
.generate_table_stats(
420419
&previous,
421420
insert_hll,
422421
insert_rows,
423422
insert_top_n,
424-
reuse_previous_stats,
425423
refresh_top_n,
426424
)
427425
.await?;

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -416,15 +416,11 @@ where F: SnapshotGenerator + Send + Sync + 'static
416416
fn has_insert_top_n_input(&self) -> bool {
417417
// Transaction commits may collapse intermediate snapshot lineage, so keep append TopN
418418
// refresh to autocommit inserts until transaction stats invalidation is defined.
419-
self.is_append_only_txn() && !self.ctx.txn_mgr().lock().is_active()
420-
}
421-
422-
fn reuse_previous_table_stats(&self) -> bool {
423-
!self
424-
.snapshot_gen
419+
self.snapshot_gen
425420
.as_any()
426421
.downcast_ref::<AppendGenerator>()
427-
.is_some_and(|g| g.is_overwrite())
422+
.is_some_and(|g| !g.is_overwrite())
423+
&& !self.ctx.txn_mgr().lock().is_active()
428424
}
429425

430426
/// Append-only inserts (e.g. `INSERT INTO t SELECT ...`) may skip committing if nothing was
@@ -652,7 +648,6 @@ where F: SnapshotGenerator + Send + Sync + 'static
652648
&self.insert_hll,
653649
self.insert_rows,
654650
&self.insert_top_n,
655-
self.reuse_previous_table_stats(),
656651
self.has_insert_top_n_input(),
657652
)
658653
.await?;
@@ -844,7 +839,6 @@ where F: SnapshotGenerator + Send + Sync + 'static
844839
&self.insert_hll,
845840
self.insert_rows,
846841
&self.insert_top_n,
847-
self.reuse_previous_table_stats(),
848842
self.has_insert_top_n_input(),
849843
)
850844
.await?;

tests/sqllogictests/suites/base/09_fuse_engine/09_0053_analyze_top_n.test

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ EXPLAIN SELECT * FROM t_edge WHERE a = 1
131131
----
132132
table: default.db_09_0053.t_edge<slt:ignore>estimated rows: 90.00
133133

134-
# Previous rows with no TopN entries but valid column stats can still seed append TopN.
134+
# Previous rows with no TopN entries but only NULL values can still seed append TopN.
135135
statement ok
136136
TRUNCATE TABLE t_edge
137137

@@ -153,16 +153,13 @@ EXPLAIN SELECT * FROM t_edge WHERE a = 1
153153
----
154154
table: default.db_09_0053.t_edge<slt:ignore>estimated rows: 90.00
155155

156-
# If old rows lack column stats for a newly configured TopN column, fail closed.
156+
# If old non-NULL rows have no fresh TopN for a newly configured column, fail closed.
157157
statement ok
158-
CREATE TABLE t_edge_default(a int) approx_distinct_columns = ''
158+
CREATE TABLE t_edge_default(a int, b int) approx_distinct_columns = ''
159159

160160
statement ok
161161
INSERT INTO t_edge_default
162-
SELECT number::int FROM numbers(100)
163-
164-
statement ok
165-
ALTER TABLE t_edge_default ADD COLUMN b int DEFAULT 1
162+
SELECT number::int, 1::int FROM numbers(100)
166163

167164
statement ok
168165
ALTER TABLE t_edge_default SET OPTIONS(analyze_top_n_columns = 'b', analyze_top_n_size = 2)
@@ -176,6 +173,29 @@ SELECT snapshot:table_statistics_location::String IS NULL FROM fuse_dump_snapsho
176173
----
177174
1
178175

176+
# Newly added nullable columns have implicit NULL history and can seed append TopN.
177+
statement ok
178+
CREATE TABLE t_added_null(a int) approx_distinct_columns = ''
179+
180+
statement ok
181+
INSERT INTO t_added_null
182+
SELECT number::int FROM numbers(100)
183+
184+
statement ok
185+
ALTER TABLE t_added_null ADD COLUMN b int NULL
186+
187+
statement ok
188+
ALTER TABLE t_added_null SET OPTIONS(analyze_top_n_columns = 'b', analyze_top_n_size = 2)
189+
190+
statement ok
191+
INSERT INTO t_added_null(a, b)
192+
SELECT number::int, if(number < 90, 1, number - 88)::int FROM numbers(100)
193+
194+
query T
195+
EXPLAIN SELECT * FROM t_added_null WHERE b = 1
196+
----
197+
table: default.db_09_0053.t_added_null<slt:ignore>estimated rows: 90.00
198+
179199
# Active transactions skip append TopN refresh until transaction stats lifecycle is handled.
180200
statement ok
181201
CREATE TABLE t_txn(a int) approx_distinct_columns = '' analyze_top_n_columns = 'a' analyze_top_n_size = 2
@@ -285,7 +305,7 @@ table: default.db_09_0053.t_no_snapshot_stats<slt:ignore>estimated rows: 90.00
285305
statement ok
286306
set enable_table_snapshot_stats = 1
287307

288-
# Reuse one analyzed table for append-after-analyze, new hot values, overwrite, and mutation invalidation.
308+
# Reuse one analyzed table for append-after-analyze, new hot values, and mutation invalidation.
289309
statement ok
290310
CREATE TABLE t_analyzed(id int, a int) approx_distinct_columns = '' analyze_top_n_columns = 'a' analyze_top_n_size = 2
291311

@@ -342,17 +362,6 @@ FROM (SELECT snapshot FROM fuse_dump_snapshots('db_09_0053', 't_analyzed') LIMIT
342362
----
343363
1
344364

345-
# INSERT OVERWRITE must build stats from overwritten rows only.
346-
statement ok
347-
INSERT OVERWRITE t_analyzed
348-
SELECT number::int, 2::int FROM numbers(100)
349-
350-
query B
351-
SELECT count(DISTINCT snapshot:table_statistics_location::String) = 2
352-
FROM (SELECT snapshot FROM fuse_dump_snapshots('db_09_0053', 't_analyzed') LIMIT 2)
353-
----
354-
1
355-
356365
# Non-insert mutations should not keep old TopN fresh.
357366
statement ok
358367
TRUNCATE TABLE t_analyzed

0 commit comments

Comments
 (0)