Teach expr_is_nonnullable() to handle more expression types

Currently, the function expr_is_nonnullable() checks only Const and
Var expressions to determine if an expression is non-nullable.  This
patch extends the detection logic to handle more expression types.

This can enable several downstream optimizations, such as reducing
NullTest quals to constant truth values (e.g., "COALESCE(var, 1) IS
NULL" becomes FALSE) and converting "COUNT(expr)" to the more
efficient "COUNT(*)" when the expression is proven non-nullable.

This breaks a test case in test_predtest.sql, since we now simplify
"ARRAY[] IS NULL" to constant FALSE, preventing it from weakly
refuting a strict ScalarArrayOpExpr ("x = any(ARRAY[])").  To ensure
the refutation logic is still exercised as intended, wrap the array
argument in opaque_array().

Author: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: Tender Wang <tndrwang@gmail.com>
Reviewed-by: Dagfinn Ilmari Mannsåker <ilmari@ilmari.org>
Reviewed-by: David Rowley <dgrowleyml@gmail.com>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Discussion: https://postgr.es/m/CAMbWs49UhPBjm+NRpxerjaeuFKyUZJ_AjM3NBcSYK2JgZ6VTEQ@mail.gmail.com
This commit is contained in:
Richard Guo 2025-12-24 18:00:44 +09:00
parent cb7b7ec7a1
commit c8d2f68cc8
5 changed files with 284 additions and 10 deletions

View File

@ -4341,16 +4341,127 @@ var_is_nonnullable(PlannerInfo *root, Var *var, bool use_rel_info)
* nullability information before RelOptInfos are generated. These should
* pass 'use_rel_info' as false.
*
* For now, we only support Var and Const. Support for other node types may
* be possible.
* For now, we support only a limited set of expression types. Support for
* additional node types can be added in the future.
*/
bool
expr_is_nonnullable(PlannerInfo *root, Expr *expr, bool use_rel_info)
{
if (IsA(expr, Var) && root)
return var_is_nonnullable(root, (Var *) expr, use_rel_info);
if (IsA(expr, Const))
return !castNode(Const, expr)->constisnull;
/* since this function recurses, it could be driven to stack overflow */
check_stack_depth();
switch (nodeTag(expr))
{
case T_Var:
{
if (root)
return var_is_nonnullable(root, (Var *) expr, use_rel_info);
}
break;
case T_Const:
return !((Const *) expr)->constisnull;
case T_CoalesceExpr:
{
/*
* A CoalesceExpr returns NULL if and only if all its
* arguments are NULL. Therefore, we can determine that a
* CoalesceExpr cannot be NULL if at least one of its
* arguments can be proven non-nullable.
*/
CoalesceExpr *coalesceexpr = (CoalesceExpr *) expr;
foreach_ptr(Expr, arg, coalesceexpr->args)
{
if (expr_is_nonnullable(root, arg, use_rel_info))
return true;
}
}
break;
case T_MinMaxExpr:
{
/*
* Like CoalesceExpr, a MinMaxExpr returns NULL only if all
* its arguments evaluate to NULL.
*/
MinMaxExpr *minmaxexpr = (MinMaxExpr *) expr;
foreach_ptr(Expr, arg, minmaxexpr->args)
{
if (expr_is_nonnullable(root, arg, use_rel_info))
return true;
}
}
break;
case T_CaseExpr:
{
/*
* A CASE expression is non-nullable if all branch results are
* non-nullable. We must also verify that the default result
* (ELSE) exists and is non-nullable.
*/
CaseExpr *caseexpr = (CaseExpr *) expr;
/* The default result must be present and non-nullable */
if (caseexpr->defresult == NULL ||
!expr_is_nonnullable(root, caseexpr->defresult, use_rel_info))
return false;
/* All branch results must be non-nullable */
foreach_ptr(CaseWhen, casewhen, caseexpr->args)
{
if (!expr_is_nonnullable(root, casewhen->result, use_rel_info))
return false;
}
return true;
}
break;
case T_ArrayExpr:
{
/*
* An ARRAY[] expression always returns a valid Array object,
* even if it is empty (ARRAY[]) or contains NULLs
* (ARRAY[NULL]). It never evaluates to a SQL NULL.
*/
return true;
}
case T_NullTest:
{
/*
* An IS NULL / IS NOT NULL expression always returns a
* boolean value. It never returns SQL NULL.
*/
return true;
}
case T_BooleanTest:
{
/*
* A BooleanTest expression always evaluates to a boolean
* value. It never returns SQL NULL.
*/
return true;
}
case T_DistinctExpr:
{
/*
* IS DISTINCT FROM never returns NULL, effectively acting as
* though NULL were a normal data value.
*/
return true;
}
case T_RelabelType:
{
/*
* RelabelType does not change the nullability of the data.
* The result is non-nullable if and only if the argument is
* non-nullable.
*/
return expr_is_nonnullable(root, ((RelabelType *) expr)->arg,
use_rel_info);
}
default:
break;
}
return false;
}

View File

@ -1066,7 +1066,7 @@ w_r_holds | t
-- as does nullness of the array
select * from test_predtest($$
select x = any(opaque_array(array[y])), array[y] is null
select x = any(opaque_array(array[y])), opaque_array(array[y]) is null
from integers
$$);
-[ RECORD 1 ]-----+--

View File

@ -431,7 +431,7 @@ $$);
-- as does nullness of the array
select * from test_predtest($$
select x = any(opaque_array(array[y])), array[y] is null
select x = any(opaque_array(array[y])), opaque_array(array[y]) is null
from integers
$$);

View File

@ -488,7 +488,7 @@ DROP TABLE pred_tab;
-- Test that COALESCE expressions in predicates are simplified using
-- non-nullable arguments.
--
CREATE TABLE pred_tab (a int NOT NULL, b int);
CREATE TABLE pred_tab (a int NOT NULL, b int, c int);
-- Ensure that constant NULL arguments are dropped
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1;
@ -516,4 +516,119 @@ SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
Filter: (a > 1)
(2 rows)
--
-- Test detection of non-nullable expressions in predicates
--
-- CoalesceExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL;
QUERY PLAN
------------------------------------
Seq Scan on pred_tab
Filter: (COALESCE(b, c) IS NULL)
(2 rows)
-- MinMaxExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL;
QUERY PLAN
------------------------------------
Seq Scan on pred_tab
Filter: (GREATEST(b, c) IS NULL)
(2 rows)
-- CaseExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL;
QUERY PLAN
---------------------------------------------------------
Seq Scan on pred_tab
Filter: (CASE WHEN (c > 0) THEN b ELSE a END IS NULL)
(2 rows)
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL;
QUERY PLAN
---------------------------------------------------------------------
Seq Scan on pred_tab
Filter: (CASE WHEN (c > 0) THEN a ELSE NULL::integer END IS NULL)
(2 rows)
-- ArrayExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
-- NullTest
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
-- BooleanTest
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
-- DistinctExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
-- RelabelType
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
QUERY PLAN
------------------------------
Result
Replaces: Scan on pred_tab
One-Time Filter: false
(3 rows)
DROP TABLE pred_tab;

View File

@ -245,7 +245,7 @@ DROP TABLE pred_tab;
-- Test that COALESCE expressions in predicates are simplified using
-- non-nullable arguments.
--
CREATE TABLE pred_tab (a int NOT NULL, b int);
CREATE TABLE pred_tab (a int NOT NULL, b int, c int);
-- Ensure that constant NULL arguments are dropped
EXPLAIN (COSTS OFF)
@ -259,4 +259,52 @@ SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1;
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
--
-- Test detection of non-nullable expressions in predicates
--
-- CoalesceExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL;
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL;
-- MinMaxExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL;
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL;
-- CaseExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL;
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL;
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL;
-- ArrayExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL;
-- NullTest
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL;
-- BooleanTest
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL;
-- DistinctExpr
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL;
-- RelabelType
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
DROP TABLE pred_tab;