From 6d1a0a477d9ea978025e321833b87038153e5719 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Mon, 5 Oct 2026 11:08:32 -0400 Subject: [PATCH v2 2/2] Add row estimate tests for IS [NOT] DISTINCT FROM Check that join, semijoin and antijoin estimates count rows with NULL on both sides. Co-authored-by: Rahul Yadav Discussion: https://postgr.es/m/17545-a0ca4de888953169@postgresql.org --- src/test/regress/expected/planner_est.out | 55 +++++++++++++++++++++++ src/test/regress/sql/planner_est.sql | 36 +++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out index 236cb274a78..66ea7afd3fd 100644 --- a/src/test/regress/expected/planner_est.out +++ b/src/test/regress/expected/planner_est.out @@ -221,4 +221,59 @@ EXPLAIN (COSTS OFF) SELECT * FROM char_table_1 WHERE c < 'Q'; Filter: (c < 'Q'::"char") (2 rows) +-- +-- Test IS [NOT] DISTINCT FROM join estimates with NULLs on both sides. Only +-- the top plan line is shown, since the rest is not stable across platforms. +-- +CREATE TEMP TABLE distinct_t1 AS + SELECT CASE WHEN i > 30 THEN i END AS a FROM generate_series(1, 100) i; +CREATE TEMP TABLE distinct_t2 AS + SELECT CASE WHEN i > 100 THEN i - 100 END AS a FROM generate_series(1, 200) i; +ANALYZE distinct_t1, distinct_t2; +-- Ensure pairs of NULLs are counted +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t1 t1 JOIN distinct_t2 t2 ON t1.a IS NOT DISTINCT FROM t2.a;$$, +true, true, false, true) LIMIT 1; + explain_mask_costs +-------------------------------------------------------------------------- + Nested Loop (cost=N..N rows=3070 width=N) (actual rows=3070.00 loops=1) +(1 row) + +-- The equivalent OR clause should get about the same estimate +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t1 t1 JOIN distinct_t2 t2 ON t1.a = t2.a OR (t1.a IS NULL AND t2.a IS NULL);$$, +true, true, false, true) LIMIT 1; + explain_mask_costs +-------------------------------------------------------------------------- + Nested Loop (cost=N..N rows=3060 width=N) (actual rows=3070.00 loops=1) +(1 row) + +-- Ensure pairs of NULLs are not counted +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t1 t1 JOIN distinct_t2 t2 ON t1.a IS DISTINCT FROM t2.a;$$, +true, true, false, true) LIMIT 1; + explain_mask_costs +---------------------------------------------------------------------------- + Nested Loop (cost=N..N rows=16930 width=N) (actual rows=16930.00 loops=1) +(1 row) + +-- Ensure NULLs are matched in semijoins and antijoins +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t2 t2 WHERE EXISTS + (SELECT 1 FROM distinct_t1 t1 WHERE t1.a IS NOT DISTINCT FROM t2.a);$$, +true, true, false, true) LIMIT 1; + explain_mask_costs +---------------------------------------------------------------------------------- + Nested Loop Semi Join (cost=N..N rows=170 width=N) (actual rows=170.00 loops=1) +(1 row) + +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t2 t2 WHERE NOT EXISTS + (SELECT 1 FROM distinct_t1 t1 WHERE t1.a IS NOT DISTINCT FROM t2.a);$$, +true, true, false, true) LIMIT 1; + explain_mask_costs +-------------------------------------------------------------------------------- + Nested Loop Anti Join (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) +(1 row) + DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql index 2b696a4e4e5..58e812cd6ec 100644 --- a/src/test/regress/sql/planner_est.sql +++ b/src/test/regress/sql/planner_est.sql @@ -153,4 +153,40 @@ CREATE TEMP TABLE char_table_1 AS ANALYZE char_table_1; EXPLAIN (COSTS OFF) SELECT * FROM char_table_1 WHERE c < 'Q'; +-- +-- Test IS [NOT] DISTINCT FROM join estimates with NULLs on both sides. Only +-- the top plan line is shown, since the rest is not stable across platforms. +-- +CREATE TEMP TABLE distinct_t1 AS + SELECT CASE WHEN i > 30 THEN i END AS a FROM generate_series(1, 100) i; +CREATE TEMP TABLE distinct_t2 AS + SELECT CASE WHEN i > 100 THEN i - 100 END AS a FROM generate_series(1, 200) i; +ANALYZE distinct_t1, distinct_t2; + +-- Ensure pairs of NULLs are counted +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t1 t1 JOIN distinct_t2 t2 ON t1.a IS NOT DISTINCT FROM t2.a;$$, +true, true, false, true) LIMIT 1; + +-- The equivalent OR clause should get about the same estimate +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t1 t1 JOIN distinct_t2 t2 ON t1.a = t2.a OR (t1.a IS NULL AND t2.a IS NULL);$$, +true, true, false, true) LIMIT 1; + +-- Ensure pairs of NULLs are not counted +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t1 t1 JOIN distinct_t2 t2 ON t1.a IS DISTINCT FROM t2.a;$$, +true, true, false, true) LIMIT 1; + +-- Ensure NULLs are matched in semijoins and antijoins +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t2 t2 WHERE EXISTS + (SELECT 1 FROM distinct_t1 t1 WHERE t1.a IS NOT DISTINCT FROM t2.a);$$, +true, true, false, true) LIMIT 1; + +SELECT * FROM explain_mask_costs($$ +SELECT * FROM distinct_t2 t2 WHERE NOT EXISTS + (SELECT 1 FROM distinct_t1 t1 WHERE t1.a IS NOT DISTINCT FROM t2.a);$$, +true, true, false, true) LIMIT 1; + DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); -- 2.37.1 (Apple Git-137.1)