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]

Reply via email to