From 6c287794c65276dbdce69771fde86e9b93267469 Mon Sep 17 00:00:00 2001 From: Evdokimov Ilia Date: Wed, 4 Mar 2026 00:35:56 +0300 Subject: [PATCH v6 2/2] Reduce planning time for large NOT IN lists containing NULL For x <> ALL (...), the presence of a NULL makes the selectivity 0.0. The planner currently still iterates over all elements and computes per-element selectivity, even though the final result is known. Add an early NULL check for constant arrays and immediately return 0.0 under ALL semantics. This reduces planning time for large <> ALL lists without changing semantics. --- src/backend/utils/adt/selfuncs.c | 17 +++++++++++ src/test/regress/expected/selectivity_est.out | 30 +++++++++++++++++++ src/test/regress/sql/selectivity_est.sql | 22 ++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index dd7e11c0ca5..d93ce6e6ea9 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -2018,6 +2018,15 @@ scalararraysel(PlannerInfo *root, if (arrayisnull) /* qual can't succeed if null array */ return (Selectivity) 0.0; arrayval = DatumGetArrayTypeP(arraydatum); + + /* + * For ALL semantics, if the array contains NULL, assume operator is + * strict. The ScalarArrayOpExpr cannot evaluate to TRUE, so return + * zero. + */ + if (!useOr && array_contains_nulls(arrayval)) + return (Selectivity) 0.0; + get_typlenbyvalalign(ARR_ELEMTYPE(arrayval), &elmlen, &elmbyval, &elmalign); deconstruct_array(arrayval, @@ -2115,6 +2124,14 @@ scalararraysel(PlannerInfo *root, List *args; Selectivity s2; + /* + * For ALL semantics, if the array contains NULL, assume operator + * is strict. The ScalarArrayOpExpr cannot evaluate to TRUE, so + * return zero. + */ + if (!useOr && IsA(elem, Const) && ((Const *) elem)->constisnull) + return (Selectivity) 0.0; + /* * Theoretically, if elem isn't of nominal_element_type we should * insert a RelabelType, but it seems unlikely that any operator diff --git a/src/test/regress/expected/selectivity_est.out b/src/test/regress/expected/selectivity_est.out index 8fc5c9c9e07..d482f2ae7a0 100644 --- a/src/test/regress/expected/selectivity_est.out +++ b/src/test/regress/expected/selectivity_est.out @@ -175,4 +175,34 @@ false, true, false, true); Function Scan on generate_series g (cost=N..N rows=1000 width=N) (1 row) +-- +-- Test <> ALL when array initially contained NULL but no longer does +-- +CREATE FUNCTION replace_elem(arr int[], idx int, val int) +RETURNS int[] AS $$ +BEGIN + arr[idx] := val; + RETURN arr; +end; +$$ language plpgsql IMMUTABLE; +SELECT explain_mask_costs( + 'SELECT * FROM tenk1 WHERE unique1 <> ALL(ARRAY[1,99,3])', +false, true, false, true ); + explain_mask_costs +---------------------------------------------------- + Seq Scan on tenk1 (cost=N..N rows=9997 width=N) + Filter: (unique1 <> ALL ('{1,99,3}'::integer[])) +(2 rows) + +-- same array, constructed from an array with a NULL +SELECT explain_mask_costs( + 'SELECT * FROM tenk1 WHERE unique1 <> ALL(replace_elem(ARRAY[1,NULL,3], 2, 99))', +false, true, false, true ); + explain_mask_costs +---------------------------------------------------- + Seq Scan on tenk1 (cost=N..N rows=9997 width=N) + Filter: (unique1 <> ALL ('{1,99,3}'::integer[])) +(2 rows) + +DROP FUNCTION replace_elem; DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); diff --git a/src/test/regress/sql/selectivity_est.sql b/src/test/regress/sql/selectivity_est.sql index 416d5ea1f75..3ffec43907f 100644 --- a/src/test/regress/sql/selectivity_est.sql +++ b/src/test/regress/sql/selectivity_est.sql @@ -122,5 +122,27 @@ SELECT explain_mask_costs($$ SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$, false, true, false, true); +-- +-- Test <> ALL when array initially contained NULL but no longer does +-- +CREATE FUNCTION replace_elem(arr int[], idx int, val int) +RETURNS int[] AS $$ +BEGIN + arr[idx] := val; + RETURN arr; +end; +$$ language plpgsql IMMUTABLE; + +SELECT explain_mask_costs( + 'SELECT * FROM tenk1 WHERE unique1 <> ALL(ARRAY[1,99,3])', +false, true, false, true ); + +-- same array, constructed from an array with a NULL +SELECT explain_mask_costs( + 'SELECT * FROM tenk1 WHERE unique1 <> ALL(replace_elem(ARRAY[1,NULL,3], 2, 99))', +false, true, false, true ); + +DROP FUNCTION replace_elem; + DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); -- 2.34.1