This is an automated email from the ASF dual-hosted git repository.
my-ship-it pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry.git
The following commit(s) were added to refs/heads/main by this push:
new 6e3051b4393 Fix assertion failure in getNdvBySegHeapTuple for empty
partitions.
6e3051b4393 is described below
commit 6e3051b4393f3353808090af6f52c579472f0587
Author: zhangwenchao <[email protected]>
AuthorDate: Mon Mar 2 16:44:30 2026 +0800
Fix assertion failure in getNdvBySegHeapTuple for empty partitions.
When merging leaf partition statistics for a partitioned table,
getNdvBySegHeapTuple could hit Assert(valuetype == FLOAT8OID) if a
partition had relTuples == 0 and its pg_statistic entry lacked a valid
STATISTIC_KIND_NDV_BY_SEGMENTS slot (or the slot had an unexpected
element type).
The original guard condition only handled two cases:
1. Non-empty partition with NDV value == 0
2. Non-empty partition without NDV_BY_SEGMENTS
It missed the case where an empty partition (relTuples == 0) has a
pg_statistic entry but no valid FLOAT8OID NDV_BY_SEGMENTS slot,
causing the code to fall through to the assertion.
Fix by checking valuetype != FLOAT8OID upfront: skip empty partitions
gracefully, and mark non-empty partitions as invalid.
Authored-by: Zhang Wenchao <[email protected]>
---
src/backend/commands/analyzeutils.c | 35 ++++++++++++++++++++++++++++-------
src/test/regress/expected/analyze.out | 35 +++++++++++++++++++++++++++++++++++
src/test/regress/sql/analyze.sql | 31 +++++++++++++++++++++++++++++++
3 files changed, 94 insertions(+), 7 deletions(-)
diff --git a/src/backend/commands/analyzeutils.c
b/src/backend/commands/analyzeutils.c
index c4f9fa5dedb..a0f5f823103 100644
--- a/src/backend/commands/analyzeutils.c
+++ b/src/backend/commands/analyzeutils.c
@@ -759,17 +759,38 @@ getNdvBySegHeapTuple(AttStatsSlot * *ndvbsSlots,
HeapTuple *heaptupleStats, floa
(void) get_attstatsslot(ndvbsSlots[i], heaptupleStats[i],
STATISTIC_KIND_NDV_BY_SEGMENTS, InvalidOid,
ATTSTATSSLOT_VALUES);
- if ((InvalidOid != ndvbsSlots[i]->valuetype && // result is not
empty
- // not empty partition with invalid ndvbs
- (relTuples[i] > 0 &&
DatumGetFloat8(ndvbsSlots[i]->values[0]) == 0)) ||
- // not empty partition without ndvbs
- (InvalidOid == ndvbsSlots[i]->valuetype && relTuples[i]
> 0)) {
+ if (ndvbsSlots[i]->valuetype != FLOAT8OID)
+ {
+ /*
+ * NDV_BY_SEGMENTS slot not found or has unexpected
type.
+ * Non-empty partitions must have valid NDV_BY_SEGMENTS;
+ * empty partitions (relTuples == 0) can be skipped.
+ */
+ if (relTuples[i] > 0)
+ {
+ valid = false;
+ break;
+ }
+ free_attstatsslot(ndvbsSlots[i]);
+ pfree(ndvbsSlots[i]);
+ ndvbsSlots[i] = NULL;
+ continue;
+ }
+
+ Assert(ndvbsSlots[i]->valuetype == FLOAT8OID);
+
+ if (ndvbsSlots[i]->nvalues != 1)
+ {
valid = false;
break;
}
- Assert(ndvbsSlots[i]->valuetype == FLOAT8OID);
- Assert(ndvbsSlots[i]->nvalues == 1);
+ /* Non-empty partition with zero NDV is suspicious */
+ if (relTuples[i] > 0 &&
DatumGetFloat8(ndvbsSlots[i]->values[0]) == 0)
+ {
+ valid = false;
+ break;
+ }
}
return valid;
}
diff --git a/src/test/regress/expected/analyze.out
b/src/test/regress/expected/analyze.out
index 78f1bc05f7e..b9397348d46 100644
--- a/src/test/regress/expected/analyze.out
+++ b/src/test/regress/expected/analyze.out
@@ -1344,3 +1344,38 @@ WHERE s.starelid = 'analyze_col_ndv_drop'::regclass AND
a.attname = 'b';
(1 row)
DROP TABLE analyze_col_ndv_drop;
+--
+-- Test merging leaf stats when an emptied leaf partition carries a stale
+-- pg_statistic tuple that lacks the NDV_BY_SEGMENTS slot (here because
+-- column c is entirely NULL in that leaf, so no slot was ever written).
+-- aggregate_leaf_partition_ndvbs used to dereference the zeroed slot's
+-- NULL values array and crash the QD.
+--
+CREATE TABLE analyze_ndvbs_empty_part (a int, b int, c int)
+DISTRIBUTED BY (a)
+PARTITION BY RANGE (b) (START (1) END (3) EVERY (1));
+INSERT INTO analyze_ndvbs_empty_part SELECT i, 1, NULL FROM generate_series(1,
100) i;
+INSERT INTO analyze_ndvbs_empty_part SELECT i, 2, i FROM generate_series(1,
100) i;
+ANALYZE analyze_ndvbs_empty_part_1_prt_1;
+ANALYZE analyze_ndvbs_empty_part_1_prt_2;
+-- empty the first leaf; its stale stats tuple survives with reltuples = 0
+DELETE FROM analyze_ndvbs_empty_part_1_prt_1;
+VACUUM analyze_ndvbs_empty_part_1_prt_1;
+-- must not crash; the empty leaf contributes nothing to the aggregated NDV
+ANALYZE ROOTPARTITION analyze_ndvbs_empty_part;
+SELECT a.attname,
+ CASE WHEN s.stakind1 = 8 THEN array_to_string(s.stavalues1, ',')
+ WHEN s.stakind2 = 8 THEN array_to_string(s.stavalues2, ',')
+ WHEN s.stakind3 = 8 THEN array_to_string(s.stavalues3, ',')
+ WHEN s.stakind4 = 8 THEN array_to_string(s.stavalues4, ',')
+ WHEN s.stakind5 = 8 THEN array_to_string(s.stavalues5, ',')
+ END AS stadistinctbyseg
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'analyze_ndvbs_empty_part'::regclass AND a.attname = 'c';
+ attname | stadistinctbyseg
+---------+------------------
+ c | 100
+(1 row)
+
+DROP TABLE analyze_ndvbs_empty_part;
diff --git a/src/test/regress/sql/analyze.sql b/src/test/regress/sql/analyze.sql
index 6d1c7ddd159..97c0f0d6e38 100644
--- a/src/test/regress/sql/analyze.sql
+++ b/src/test/regress/sql/analyze.sql
@@ -700,3 +700,34 @@ FROM pg_statistic s
JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
WHERE s.starelid = 'analyze_col_ndv_drop'::regclass AND a.attname = 'b';
DROP TABLE analyze_col_ndv_drop;
+
+--
+-- Test merging leaf stats when an emptied leaf partition carries a stale
+-- pg_statistic tuple that lacks the NDV_BY_SEGMENTS slot (here because
+-- column c is entirely NULL in that leaf, so no slot was ever written).
+-- aggregate_leaf_partition_ndvbs used to dereference the zeroed slot's
+-- NULL values array and crash the QD.
+--
+CREATE TABLE analyze_ndvbs_empty_part (a int, b int, c int)
+DISTRIBUTED BY (a)
+PARTITION BY RANGE (b) (START (1) END (3) EVERY (1));
+INSERT INTO analyze_ndvbs_empty_part SELECT i, 1, NULL FROM generate_series(1,
100) i;
+INSERT INTO analyze_ndvbs_empty_part SELECT i, 2, i FROM generate_series(1,
100) i;
+ANALYZE analyze_ndvbs_empty_part_1_prt_1;
+ANALYZE analyze_ndvbs_empty_part_1_prt_2;
+-- empty the first leaf; its stale stats tuple survives with reltuples = 0
+DELETE FROM analyze_ndvbs_empty_part_1_prt_1;
+VACUUM analyze_ndvbs_empty_part_1_prt_1;
+-- must not crash; the empty leaf contributes nothing to the aggregated NDV
+ANALYZE ROOTPARTITION analyze_ndvbs_empty_part;
+SELECT a.attname,
+ CASE WHEN s.stakind1 = 8 THEN array_to_string(s.stavalues1, ',')
+ WHEN s.stakind2 = 8 THEN array_to_string(s.stavalues2, ',')
+ WHEN s.stakind3 = 8 THEN array_to_string(s.stavalues3, ',')
+ WHEN s.stakind4 = 8 THEN array_to_string(s.stavalues4, ',')
+ WHEN s.stakind5 = 8 THEN array_to_string(s.stavalues5, ',')
+ END AS stadistinctbyseg
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'analyze_ndvbs_empty_part'::regclass AND a.attname = 'c';
+DROP TABLE analyze_ndvbs_empty_part;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]