Skip to content

Fix ORCA GbAgg drop for nullable UNIQUE keys - #1941

Open
roseduan wants to merge 2 commits into
apache:mainfrom
roseduan:fix_orca_nullable_unqiue_index_agg
Open

roseduan wants to merge 2 commits into
apache:mainfrom
roseduan:fix_orca_nullable_unqiue_index_agg

Conversation

@roseduan

Copy link
Copy Markdown
Contributor

CXformSimplifyGbAgg::FDropGbAgg() drops a GROUP BY/DISTINCT's aggregate node when the grouping columns cover a key derived from a UNIQUE or PRIMARY KEY constraint, assuming the key already guarantees one row per value.

That's wrong for a plain UNIQUE column: SQL allows multiple NULLs in it (NULL is never "equal" for uniqueness checks), but dedup must still collapse those NULLs into one row.

Before:
create table repro (c0 numeric unique);
insert into repro values (1), (2), (null), (null), (null);
explain (costs off) select distinct c0 from repro;
Gather Motion 3:1 (slice1; segments: 3)
-> Seq Scan on repro -- no aggregate, all 5 rows returned

After:
Gather Motion 3:1 (slice1; segments: 3)
-> GroupAggregate
Group Key: repro.c0
-> Seq Scan on repro -- correctly returns 1, 2, NULL

Fix by also requiring every key column to be provably NOT NULL (CExpression::DeriveNotNullColumns()) before dropping the aggregate. PRIMARY KEY columns are always NOT NULL, so that case is unaffected.

Fixes #ISSUE_Number

What does this PR do?

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


Comment thread src/test/regress/expected/bfv_aggregate.out
@Alena0704 Alena0704 added the type: Bug Something isn't working label Oct 4, 2026
@roseduan
roseduan force-pushed the fix_orca_nullable_unqiue_index_agg branch 2 times, most recently from aada5be to 6cf597e Compare October 19, 2026 03:46
@roseduan
roseduan requested a review from yjhjstz October 19, 2026 03:48
CXformSimplifyGbAgg::FDropGbAgg() drops a GROUP BY/DISTINCT's
aggregate node when the grouping columns cover a key derived from
a UNIQUE or PRIMARY KEY constraint, assuming the key already
guarantees one row per value.

That's wrong for a plain UNIQUE column: SQL allows multiple NULLs
in it (NULL is never "equal" for uniqueness checks), but dedup
must still collapse those NULLs into one row.

Before:
    create table repro (c0 numeric unique);
    insert into repro values (1), (2), (null), (null), (null);
    explain (costs off) select distinct c0 from repro;
      Gather Motion 3:1  (slice1; segments: 3)
        ->  Seq Scan on repro          -- no aggregate, all 5 rows returned

After:
      Gather Motion 3:1  (slice1; segments: 3)
        ->  GroupAggregate
              Group Key: repro.c0
              ->  Seq Scan on repro    -- correctly returns 1, 2, NULL

Fix by also requiring every key column to be provably NOT NULL
(CExpression::DeriveNotNullColumns()) before dropping the
aggregate. PRIMARY KEY columns are always NOT NULL, so that case
is unaffected.
@roseduan
roseduan force-pushed the fix_orca_nullable_unqiue_index_agg branch from eb521c3 to be750a1 Compare October 19, 2026 08:19
@roseduan
roseduan requested a balanced review from Copilot October 20, 2026 02:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

4 open issues
What changed in this PR

Fixes an optimizer correctness issue where ORCA could incorrectly drop a GROUP BY / DISTINCT aggregate when the grouping columns cover a UNIQUE-derived key that is nullable (allowing multiple NULLs), by tracking NULL-distinctness of keys and requiring NOT NULL proof before dropping the aggregate.

Changes:

  • Track whether derived keys allow multiple NULLs (NULLs are distinct) in CKeyCollection.
  • Update CXformSimplifyGbAgg::FDropGbAgg() to only drop GbAgg when the key is not NULL-distinct or all key columns are provably NOT NULL.
  • Add regression coverage for DISTINCT on UNIQUE nullable columns (including partitioned tables) and update expected outputs.
File Description
src/​test/​regress/​sql/​bfv_aggregate.sql Adds SQL regression cases for DISTINCT over nullable UNIQUE keys (incl. partitions).
src/​test/​regress/​expected/​bfv_aggregate_optimizer.out Updates ORCA plan expectations to reflect preserved aggregates when NULLs matter.
src/​test/​regress/​expected/​bfv_aggregate.out Updates planner (non-ORCA) expected output for new regression cases.
src/​backend/​gporca/​libgpopt/​src/​xforms/​CXformSimplifyGbAgg.cpp Requires NOT NULL proof (or non-NULL-distinct key) before dropping GbAgg.
src/​backend/​gporca/​libgpopt/​src/​operators/​CLogical.cpp Propagates NULL-distinctness when deriving/combining keys.
src/​backend/​gporca/​libgpopt/​src/​base/​CKeyCollection.cpp Stores NULL-distinctness bitset per key; extends Add() behavior.
src/​backend/​gporca/​libgpopt/​include/​gpopt/​base/​CKeyCollection.h Adds bitset state and FNullsDistinct() accessor to CKeyCollection.

Review effort: Lite

CLogical::PkcCombineKeys(CMemoryPool *mp, CExpressionHandle &exprhdl)
{
CColRefSet *pcrs = GPOS_NEW(mp) CColRefSet(mp);
BOOL fNullsDistinct = false;
Comment on lines 347 to +352
CColRefArray *colref_array = pkc->PdrgpcrKey(mp);
pcrs->Include(colref_array);
colref_array->Release();

// the combined key permits multiple NULLs if one of its parts does
fNullsDistinct = fNullsDistinct || pkc->FNullsDistinct(0);
Comment on lines +62 to +67
// check if the key at given position permits multiple NULLs
BOOL
FNullsDistinct(ULONG ul) const
{
return m_pbsNullsDistinct->Get(ul);
}
Comment on lines +392 to +397
CColRef *colref = (*pdrgpcrOutput)[bsiter.Bit()];
pcrs->Include(colref);

// a UNIQUE constraint permits multiple NULLs in nullable columns
fNullsDistinct = fNullsDistinct ||
CColRefTable::PcrConvert(colref)->IsNullable();
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants