Skip to content

Fix ndistinct-by-segments for partitioned tables - #2027

Open
Alena0704 wants to merge 1 commit into
apache:REL_2_STABLEfrom
Alena0704:fix-ndvbs-partitioned-rel2
Open

Alena0704 wants to merge 1 commit into
apache:REL_2_STABLEfrom
Alena0704:fix-ndvbs-partitioned-rel2

Conversation

@Alena0704

@Alena0704 Alena0704 commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

ORCA does not use two-stage aggregation on partitioned tables

ANALYZE adds up the per-segment ndistinct of all partitions to get the value
for the parent table (STATISTIC_KIND_NDV_BY_SEGMENTS). When the same values
are in every partition, they are counted once per partition, so the value is
too big.

ORCA uses this value to guess how many rows are left after the partial
aggregate. With a value that is too big, ORCA thinks the partial aggregate
does not help and sends all rows through the motion. The more partitions and
grouping columns, the bigger the error.

A segment cannot have more distinct values than the whole table, so
cap the value at the table's ndistinct times the number of segments.

Reproduction (3 segments, ORCA):

CREATE TABLE ndvbs_bench (id bigint, pk int, a text, b text, c text, d text)
  DISTRIBUTED BY (id)
  PARTITION BY RANGE (pk) (START (1) END (14) EVERY (1));      -- 13 partitions
-- a, b, c, d: 5 values each, repeated in every partition; 625 groups in total
INSERT INTO ndvbs_bench
  SELECT g, (g % 13) + 1, md5((g % 5)::text), md5(((g / 5) % 5)::text),
         md5(((g / 25) % 5)::text), md5(((g / 125) % 5)::text)
  FROM generate_series(1, 20000000) g;
ANALYZE ndvbs_bench;

SET optimizer = on;
EXPLAIN (ANALYZE) SELECT a, b, c, d, count(*) FROM ndvbs_bench GROUP BY a, b, c, d;

Before (REL_2_STABLE): one-stage plan, all 7M rows go through the motion.

 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..9093.03 rows=47 width=140) (actual time=15231.369..15232.369 rows=625 loops=1)
   ->  HashAggregate  (cost=0.00..9093.00 rows=16 width=140) (actual time=15231.369..15231.369 rows=220 loops=1)
         Group Key: a, b, c, d
         ->  Redistribute Motion 3:3  (slice2; segments: 3)  (cost=0.00..5474.87 rows=6666667 width=132) (actual time=3.000..7932.192 rows=7040000 loops=1)
               Hash Key: a, b, c, d
               ->  Dynamic Seq Scan on ndvbs_bench  (cost=0.00..1083.67 rows=6666667 width=132) (actual time=0.000..3109.075 rows=6667848 loops=1)
                     Number of partitions to scan: 13 (out of 13)
 Planning Time: 15.531 ms
 Optimizer: GPORCA
 Execution Time: 15233.284 ms

After: two-stage plan, only 660 rows go through the motion.

 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..6340.17 rows=47 width=140) (actual time=5547.134..5548.134 rows=625 loops=1)
   ->  Finalize HashAggregate  (cost=0.00..6340.14 rows=16 width=140) (actual time=5526.134..5526.134 rows=220 loops=1)
         Group Key: a, b, c, d
         ->  Redistribute Motion 3:3  (slice2; segments: 3)  (cost=0.00..6339.45 rows=1268 width=140) (actual time=3703.090..5526.134 rows=660 loops=1)
               Hash Key: a, b, c, d
               ->  Streaming Partial HashAggregate  (cost=0.00..6338.90 rows=1268 width=140) (actual time=3703.090..3703.090 rows=625 loops=1)
                     Group Key: a, b, c, d
                     ->  Dynamic Seq Scan on ndvbs_bench  (cost=0.00..1083.67 rows=6666667 width=132) (actual time=0.000..1347.033 rows=6667848 loops=1)
                           Number of partitions to scan: 13 (out of 13)
 Planning Time: 33.956 ms
 Optimizer: GPORCA
 Execution Time: 5555.216 ms

Type of Change

  • Bug fix (non-breaking change)
  • New feature (non-breaking change)
  • Breaking change (fix or feature with breaking changes)
  • Documentation update

Breaking Changes

Test Plan

  • Unit tests added/updated
  • Integration tests added/updated
  • Passed make installcheck
  • Passed make -C src/test installcheck-cbdb-parallel

Impact

Performance:

User-facing changes:

Dependencies:

Checklist

Additional Context

CI Skip Instructions


@Alena0704
Alena0704 marked this pull request as draft September 17, 2026 14:14
@Alena0704
Alena0704 marked this pull request as ready for review September 17, 2026 20:00
@Alena0704 Alena0704 added type: Bug Something isn't working type: Performance cloudberry runs slow on some particular query type: Orca only orca has the issue labels Sep 18, 2026
@yjhjstz
yjhjstz requested a review from jiaqizho September 22, 2026 21:52

if (root_ndistinct > 0)
{
GpPolicy *policy = GpPolicyFetch(stats->attr->attrelid);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need pfree(policy);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the pfree(policy), thanks.

The cap is only an upper bound. When a value sits on a single segment it is still too high by the number of segments, and ORCA multiplies the per-column values, so with k grouping columns the error is numsegments^k and the multi-stage plan is still lost. Instead of capping, the merged value is now the root's ndistinct times the average number of segments a value sits on, taken from the partitions themselves. The sum of the partitions is still kept as an upper bound, and the average is at most the number of segments, so the value never exceeds the root's ndistinct times the number of segments either.

Reproduction (3 segments, ORCA). k has 13 values and is independent of the partitioning column, so every partition holds all of them, and a to f are bijections of k, so each of their values sits on one segment:

CREATE TABLE ndvbs_dk (id bigint, pk int, k int,
                       a text, b text, c text, d text, e text, f text)
  DISTRIBUTED BY (k)
  PARTITION BY RANGE (pk) (START (1) END (14) EVERY (1));
INSERT INTO ndvbs_dk
  SELECT g, (g % 13) + 1, (g / 13) % 13,
         md5((((g / 13) % 13)      )::text), md5((((g / 13) % 13) + 100)::text),
         md5((((g / 13) % 13) + 200)::text), md5((((g / 13) % 13) + 300)::text),
         md5((((g / 13) % 13) + 400)::text), md5((((g / 13) % 13) + 500)::text)
  FROM generate_series(1, 6000000) g;
ANALYZE ndvbs_dk;

SET optimizer = on;
EXPLAIN (ANALYZE) SELECT a, b, c, d, e, f, count(*)
  FROM ndvbs_dk GROUP BY a, b, c, d, e, f;

Before: a partition has 13, the root gets 39, and all 2.3M rows per segment go through the motion to produce 13 groups.

 Gather Motion 3:1  (cost=0.00..4334.14 rows=15307 width=200) (actual time=11650.023..11650.023 rows=13 loops=1)
   ->  HashAggregate  (cost=0.00..4322.73 rows=5103 width=200) (actual time=11650.023..11650.023 rows=5 loops=1)
         ->  Redistribute Motion 3:3  (cost=0.00..2613.36 rows=2000000 width=192) (actual time=2.000..8058.016 rows=2307694 loops=1)
               ->  Dynamic Seq Scan on ndvbs_dk  (cost=0.00..697.20 rows=2000000 width=192) (actual rows=2307695 loops=1)
 Execution Time: 11651.606 ms

After: the root gets 13, and 5 rows go through the motion.

 Gather Motion 3:1  (cost=0.00..3141.49 rows=15307 width=200) (actual time=3744.007..3744.007 rows=13 loops=1)
   ->  Finalize HashAggregate  (cost=0.00..3130.08 rows=5103 width=200) (actual time=3743.007..3743.007 rows=5 loops=1)
         ->  Redistribute Motion 3:3  (cost=0.00..3125.15 rows=5103 width=200) (actual time=2461.005..3743.007 rows=5 loops=1)
               ->  Streaming Partial HashAggregate  (cost=0.00..3121.96 rows=5103 width=200) (actual time=3742.007..3742.007 rows=5 loops=1)
                     ->  Dynamic Seq Scan on ndvbs_dk  (cost=0.00..697.20 rows=2000000 width=192) (actual rows=2307695 loops=1)
 Execution Time: 3744.836 ms

@jiaqizho jiaqizho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

When ANALYZE builds statistics for a partitioned table, it adds up the
per-segment ndistinct of all partitions.  If the same values appear in
every partition, they are counted once per partition, so the result is
too big: 8 partitions give 8 times the real value.

ORCA uses this number to estimate how many rows a partial aggregate
returns.  With the inflated number it thinks the partial aggregate
removes almost no rows, and picks a one-stage aggregate that sends all
rows through the motion.

A segment cannot have more distinct values than the whole table, so
cap the value at the table's ndistinct times the number of segments.
@Alena0704
Alena0704 force-pushed the fix-ndvbs-partitioned-rel2 branch from 204d280 to 45bb7bd Compare September 23, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: Bug Something isn't working type: Orca only orca has the issue type: Performance cloudberry runs slow on some particular query

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants