From 92f2ef66d35d11df28c9ab291ed711c206726a3e Mon Sep 17 00:00:00 2001 From: Linden Lance Date: Fri, 9 Oct 2026 01:34:45 +1300 Subject: [PATCH v4 2/2] Hash a ScalarArrayOpExpr whose array is fixed for one execution Only a Const array was hashed. So "x = ANY ($1)" or "x IN ($1, ...)" in a generic plan searched the whole array for every row, and a prepared statement could get much slower once it switched to a generic plan. Now an array built only from Consts, external Params and non-volatile expressions is hashed too. It is evaluated once, on the first row. Still not hashed: an array that can change from row to row, and a non-Const array in a standalone expression (such as a PL/pgSQL simple expression) or in an expression with soft errors (as in JSON ON ERROR). Discussion: https://postgr.es/m/TY6PR01MB17327691C30B403369B16B02AF4B02@TY6PR01MB17327.jpnprd01.prod.outlook.com --- src/backend/executor/execExpr.c | 42 +++- src/backend/executor/execExprInterp.c | 142 ++++++++++--- src/backend/optimizer/plan/planner.c | 2 +- src/backend/optimizer/util/clauses.c | 170 +++++++++++---- src/include/executor/execExpr.h | 3 + src/include/nodes/primnodes.h | 4 +- src/test/regress/expected/expressions.out | 241 ++++++++++++++++++++++ src/test/regress/sql/expressions.sql | 132 ++++++++++++ 8 files changed, 655 insertions(+), 81 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 408a657d6b8..a9a9ce8afef 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1272,14 +1272,32 @@ ExecInitExprRec(Expr *node, ExprState *state, FunctionCallInfo fcinfo; AclResult aclresult; Oid cmpfuncid; + bool use_hashed_step; + + Assert(list_length(opexpr->args) == 2); + scalararg = (Expr *) linitial(opexpr->args); + arrayarg = (Expr *) lsecond(opexpr->args); + + /* + * The hashed step reads a non-Const array only once. That is + * wrong where the expression is reused with new parameter + * values (no parent, as in PL/pgSQL) or must catch errors + * (escontext, as in JSON ON ERROR). + */ + use_hashed_step = OidIsValid(opexpr->hashfuncid) && + (IsA(arrayarg, Const) || + (state->parent != NULL && state->escontext == NULL)); /* * Select the correct comparison function. When we do hashed * NOT IN clauses, the opfuncid will be the inequality * comparison function and negfuncid will be set to equality. * We need to use the equality function for hash probes. + * + * Only the hashed step may use negfuncid; the plain step must + * use the operator itself ("<>"). */ - if (OidIsValid(opexpr->negfuncid)) + if (use_hashed_step && OidIsValid(opexpr->negfuncid)) { Assert(OidIsValid(opexpr->hashfuncid)); cmpfuncid = opexpr->negfuncid; @@ -1287,10 +1305,6 @@ ExecInitExprRec(Expr *node, ExprState *state, else cmpfuncid = opexpr->opfuncid; - Assert(list_length(opexpr->args) == 2); - scalararg = (Expr *) linitial(opexpr->args); - arrayarg = (Expr *) lsecond(opexpr->args); - /* Check permission to call function */ aclresult = object_aclcheck(ProcedureRelationId, cmpfuncid, GetUserId(), @@ -1300,7 +1314,7 @@ ExecInitExprRec(Expr *node, ExprState *state, get_func_name(cmpfuncid)); InvokeFunctionExecuteHook(cmpfuncid); - if (OidIsValid(opexpr->hashfuncid)) + if (use_hashed_step) { aclresult = object_aclcheck(ProcedureRelationId, opexpr->hashfuncid, GetUserId(), @@ -1320,13 +1334,13 @@ ExecInitExprRec(Expr *node, ExprState *state, opexpr->inputcollid, NULL, NULL); /* - * If hashfuncid is set, we create a EEOP_HASHED_SCALARARRAYOP + * If use_hashed_step, we create a EEOP_HASHED_SCALARARRAYOP * step instead of a EEOP_SCALARARRAYOP. This provides much * faster lookup performance than the normal linear search * when the number of items in the array is anything but very * small. */ - if (OidIsValid(opexpr->hashfuncid)) + if (use_hashed_step) { /* Evaluate scalar directly into left function argument */ ExecInitExprRec(scalararg, state, @@ -1339,7 +1353,17 @@ ExecInitExprRec(Expr *node, ExprState *state, * EEOP_HASHED_SCALARARRAYOP, and will not be passed to * any other expression. */ - ExecInitExprRec(arrayarg, state, resv, resnull); + if (IsA(arrayarg, Const)) + { + ExecInitExprRec(arrayarg, state, resv, resnull); + scratch.d.hashedscalararrayop.array_expr = NULL; + } + else + { + /* evaluated once, by EEOP_HASHED_SCALARARRAYOP */ + scratch.d.hashedscalararrayop.array_expr = + ExecInitExpr(arrayarg, state->parent); + } /* And perform the operation */ scratch.opcode = EEOP_HASHED_SCALARARRAYOP; diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 8b1b0b79137..ab8bb450339 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -226,8 +226,15 @@ static uint32 saop_element_hash(struct saophash_hash *tb, Datum key); */ typedef struct ScalarArrayOpExprHashTable { - saophash_hash *hashtab; /* underlying hash table */ + saophash_hash *hashtab; /* underlying hash table, or NULL if not + * hashed */ struct ExprEvalStep *op; + Datum cached_array; /* the array */ + bool array_isnull; + int nitems; /* number of array elements */ + int16 typlen; /* element type, to walk the array */ + bool typbyval; + char typalign; FmgrInfo hash_finfo; /* function's lookup data */ FunctionCallInfoBaseData hash_fcinfo_data; /* arguments etc */ } ScalarArrayOpExprHashTable; @@ -4241,8 +4248,32 @@ saop_hash_element_match(struct saophash_hash *tb, Datum key1, Datum key2) } /* - * Set up and return the run-time state on the step's first evaluation: build - * the hash table. + * Return the array. A Const array is already in op->resvalue; any other + * array is computed here. + * + * Const arrays do not take the non-Const route, because that route copies + * the array, and a Const array needs no copy: it is part of the plan. + */ +static void +saop_hash_eval_array(ExprEvalStep *op, ExprContext *econtext, + Datum *arr_value, bool *arr_isnull) +{ + ExprState *array_expr = op->d.hashedscalararrayop.array_expr; + + if (array_expr == NULL) + { + *arr_value = *op->resvalue; + *arr_isnull = *op->resnull; + return; + } + + /* Open-coded rather than ExecEvalExpr() to avoid including executor.h. */ + *arr_value = array_expr->evalfunc(array_expr, econtext, arr_isnull); +} + +/* + * Set up and return the run-time state on the step's first evaluation: keep + * the array, and build the hash table if the array is long enough. */ static ScalarArrayOpExprHashTable * saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) @@ -4254,11 +4285,9 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) bool strictfunc = fcinfo->flinfo->fn_strict; MemoryContext cxt = econtext->ecxt_per_query_memory; MemoryContext oldcontext; - ArrayType *arr; - int nitems; - int16 typlen; - bool typbyval; - char typalign; + Datum arr_value; + bool arr_isnull; + ArrayType *arr = NULL; uint8 typalignby; bool has_nulls = false; char *s; @@ -4268,14 +4297,7 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) Datum result; bool resultnull; - arr = DatumGetArrayTypeP(*op->resvalue); - nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); - - get_typlenbyvalalign(ARR_ELEMTYPE(arr), - &typlen, - &typbyval, - &typalign); - typalignby = typalign_to_alignby(typalign); + saop_hash_eval_array(op, econtext, &arr_value, &arr_isnull); oldcontext = MemoryContextSwitchTo(cxt); @@ -4284,9 +4306,32 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) SizeForFunctionCallInfo(1)); op->d.hashedscalararrayop.elements_tab = elements_tab; elements_tab->op = op; + elements_tab->array_isnull = arr_isnull; + + if (!arr_isnull) + { + /* copy a computed array, which may be short-lived or toasted */ + if (op->d.hashedscalararrayop.array_expr != NULL) + arr = DatumGetArrayTypePCopy(arr_value); + else + arr = DatumGetArrayTypeP(arr_value); + elements_tab->cached_array = PointerGetDatum(arr); + elements_tab->nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); + + get_typlenbyvalalign(ARR_ELEMTYPE(arr), + &elements_tab->typlen, + &elements_tab->typbyval, + &elements_tab->typalign); + } MemoryContextSwitchTo(oldcontext); + /* not worth hashing (also for a NULL or empty array) */ + if (elements_tab->nitems < MIN_ARRAY_SIZE_FOR_HASHED_SAOP) + return elements_tab; + + typalignby = typalign_to_alignby(elements_tab->typalign); + fmgr_info_cxt(saop->hashfuncid, &elements_tab->hash_finfo, cxt); fmgr_info_set_expr((Node *) saop, &elements_tab->hash_finfo); @@ -4303,12 +4348,13 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) * array happens to contain many duplicate values then it'll just mean * that we sized the table a bit on the large side. */ - elements_tab->hashtab = saophash_create(cxt, nitems, elements_tab); + elements_tab->hashtab = saophash_create(cxt, elements_tab->nitems, + elements_tab); s = (char *) ARR_DATA_PTR(arr); bitmap = ARR_NULLBITMAP(arr); bitmask = 1; - for (int i = 0; i < nitems; i++) + for (int i = 0; i < elements_tab->nitems; i++) { /* Get array element, checking for NULL. */ if (bitmap && (*bitmap & bitmask) == 0) @@ -4319,8 +4365,9 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) { Datum element; - element = fetch_att(s, typbyval, typlen); - s = att_addlength_pointer(s, typlen, s); + element = fetch_att(s, elements_tab->typbyval, + elements_tab->typlen); + s = att_addlength_pointer(s, elements_tab->typlen, s); s = (char *) att_nominal_alignby(s, typalignby); saophash_insert(elements_tab->hashtab, element, &hashfound); @@ -4358,8 +4405,9 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) fcinfo->args[0].value = (Datum) 0; fcinfo->args[0].isnull = true; - ExecEvalArrayCompareInternal(fcinfo, arr, typlen, typbyval, - typalign, true, &result, + ExecEvalArrayCompareInternal(fcinfo, arr, elements_tab->typlen, + elements_tab->typbyval, + elements_tab->typalign, true, &result, &resultnull); null_lhs_result = DatumGetBool(result); @@ -4376,15 +4424,15 @@ saop_hash_setup(ExprEvalStep *op, ExprContext *econtext) } /* - * Evaluate "scalar op ANY (const array)". + * Evaluate "scalar op ANY (fixed array)". * * Similar to ExecEvalScalarArrayOp, but optimized for faster repeat lookups * by building a hashtable on the first lookup. This hashtable will be reused * by subsequent lookups. Unlike ExecEvalScalarArrayOp, this version only * supports OR semantics. * - * Source array is in our result area, scalar arg is already evaluated into - * fcinfo->args[0]. + * A Const source array is in our result area (see saop_hash_setup() for + * others), scalar arg is already evaluated into fcinfo->args[0]. * * The operator always yields boolean. */ @@ -4402,7 +4450,28 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco bool hashfound; /* We don't setup a hashed scalar array op if the array const is null. */ - Assert(!*op->resnull); + Assert(op->d.hashedscalararrayop.array_expr != NULL || !*op->resnull); + + /* Set up on first evaluation */ + if (elements_tab == NULL) + elements_tab = saop_hash_setup(op, econtext); + + /* + * Unlike a Const array, a non-Const array can be NULL, empty or short. + * Handle these cases as ExecEvalScalarArrayOp() does, in the same order, + * so that NULL = ANY ('{}') gives false. + */ + if (elements_tab->array_isnull) + { + *op->resnull = true; + return; + } + if (elements_tab->nitems == 0) + { + *op->resvalue = BoolGetDatum(!inclause); + *op->resnull = false; + return; + } /* * If the scalar is NULL, and the function is strict, return NULL; no @@ -4414,9 +4483,24 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco return; } - /* Set up on first evaluation */ - if (elements_tab == NULL) - elements_tab = saop_hash_setup(op, econtext); + /* short array: search linearly, as ExecEvalScalarArrayOp() does */ + if (elements_tab->hashtab == NULL) + { + ExecEvalArrayCompareInternal(fcinfo, + DatumGetArrayTypeP(elements_tab->cached_array), + elements_tab->typlen, + elements_tab->typbyval, + elements_tab->typalign, + true, &result, &resultnull); + + /* NOT IN uses "=", so invert non-NULL results */ + if (!resultnull && !inclause) + result = BoolGetDatum(!DatumGetBool(result)); + + *op->resvalue = result; + *op->resnull = resultnull; + return; + } /* * When looking up an SQL NULL value with non-strict functions, we defer diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index edaa42d713a..e6acd561d0c 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -1460,7 +1460,7 @@ preprocess_expression(PlannerInfo *root, Node *expr, int kind) } /* - * Check for ANY ScalarArrayOpExpr with Const arrays and set the + * Check for ANY ScalarArrayOpExpr with fixed arrays and set the * hashfuncid of any that might execute more quickly by using hash lookups * instead of a linear search. */ diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6a54c47bef7..848e57e1364 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -132,6 +132,8 @@ static Relids find_nonnullable_rels_walker(Node *node, bool top_level); static List *find_nonnullable_vars_walker(Node *node, bool top_level); static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK); static bool convert_saop_to_hashed_saop_walker(Node *node, void *context); +static bool saop_array_may_vary(Node *node); +static bool saop_array_may_vary_walker(Node *node, void *context); static bool grouping_conflict_walker(Node *node, grouping_walker_ctx *ctx); static bool grouping_check_operands(Oid opno, Oid inputcollid, List *args, grouping_walker_ctx *ctx); @@ -2658,13 +2660,15 @@ eval_const_expressions(PlannerInfo *root, Node *node) * evaluate using a hash table rather than a linear search. * * We'll use a hash table if all of the following conditions are met: - * 1. The 2nd argument of the array contain only Consts. + * 1. The 2nd argument (the array) is a non-null Const, or is fixed for one + * execution (see saop_array_may_vary()). * 2. useOr is true or there is a valid negator operator for the * ScalarArrayOpExpr's opno. * 3. There's valid hash function for both left and righthand operands and * these hash functions are the same. * 4. If the array contains enough elements for us to consider it to be - * worthwhile using a hash table rather than a linear search. + * worthwhile using a hash table rather than a linear search. If the + * count is only known at run time, the executor checks it. */ void convert_saop_to_hashed_saop(Node *node) @@ -2685,9 +2689,37 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) Node *arrayarg = (Node *) lsecond(saop->args); Oid lefthashfunc; Oid righthashfunc; + bool try_hashing = false; - if (arrayarg && IsA(arrayarg, Const) && - !((Const *) arrayarg)->constisnull) + if (arrayarg && IsA(arrayarg, Const)) + { + Const *arrconst = castNode(Const, arrayarg); + + if (!arrconst->constisnull) + { + ArrayType *arr = (ArrayType *) DatumGetPointer(arrconst->constvalue); + + try_hashing = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)) >= + MIN_ARRAY_SIZE_FOR_HASHED_SAOP; + } + } + else if (arrayarg && + !saop_array_may_vary(arrayarg) && + !contain_volatile_functions(arrayarg)) + { + if (IsA(arrayarg, ArrayExpr) && + !castNode(ArrayExpr, arrayarg)->multidims) + try_hashing = + list_length(castNode(ArrayExpr, arrayarg)->elements) >= + MIN_ARRAY_SIZE_FOR_HASHED_SAOP; + else + { + /* length unknown until run time; the executor checks it */ + try_hashing = true; + } + } + + if (try_hashing) { if (saop->useOr) { @@ -2695,23 +2727,8 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) &lefthashfunc, &righthashfunc) && lefthashfunc == righthashfunc) { - Datum arrdatum = ((Const *) arrayarg)->constvalue; - ArrayType *arr = (ArrayType *) DatumGetPointer(arrdatum); - int nitems; - - /* - * Only fill in the hash functions if the array looks - * large enough for it to be worth hashing instead of - * doing a linear search. - */ - nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); - - if (nitems >= MIN_ARRAY_SIZE_FOR_HASHED_SAOP) - { - /* Looks good. Fill in the hash functions */ - saop->hashfuncid = lefthashfunc; - } - return false; + /* Looks good. Fill in the hash functions */ + saop->hashfuncid = lefthashfunc; } } else /* !saop->useOr */ @@ -2728,29 +2745,14 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) &lefthashfunc, &righthashfunc) && lefthashfunc == righthashfunc) { - Datum arrdatum = ((Const *) arrayarg)->constvalue; - ArrayType *arr = (ArrayType *) DatumGetPointer(arrdatum); - int nitems; + /* Looks good. Fill in the hash functions */ + saop->hashfuncid = lefthashfunc; /* - * Only fill in the hash functions if the array looks - * large enough for it to be worth hashing instead of - * doing a linear search. + * Also set the negfuncid. The executor will need that to + * perform hashtable lookups. */ - nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); - - if (nitems >= MIN_ARRAY_SIZE_FOR_HASHED_SAOP) - { - /* Looks good. Fill in the hash functions */ - saop->hashfuncid = lefthashfunc; - - /* - * Also set the negfuncid. The executor will need - * that to perform hashtable lookups. - */ - saop->negfuncid = get_opcode(negator); - } - return false; + saop->negfuncid = get_opcode(negator); } } } @@ -2759,6 +2761,92 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) return expression_tree_walker(node, convert_saop_to_hashed_saop_walker, NULL); } +/* + * saop_array_may_vary + * True unless 'node', a ScalarArrayOpExpr's array, is certain to be the + * same for every row of one execution. + * + * Only Consts, external Params and the node types listed in + * saop_array_may_vary_walker() are accepted; anything else may vary, e.g. + * merge_action(). Vars of any level are rejected: this runs before + * SS_replace_correlation_vars(), so an outer reference is still a Var. The + * caller rejects volatile functions. + */ +static bool +saop_array_may_vary(Node *node) +{ + return saop_array_may_vary_walker(node, NULL); +} + +static bool +saop_array_may_vary_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + switch (nodeTag(node)) + { + case T_Const: + return false; + + case T_Param: + return castNode(Param, node)->paramkind != PARAM_EXTERN; + + case T_FuncExpr: + if (castNode(FuncExpr, node)->funcretset) + return true; + break; + + case T_OpExpr: + if (castNode(OpExpr, node)->opretset) + return true; + break; + + case T_DistinctExpr: + if (castNode(DistinctExpr, node)->opretset) + return true; + break; + + case T_NullIfExpr: + if (castNode(NullIfExpr, node)->opretset) + return true; + break; + + case T_ArrayCoerceExpr: + + /* elemexpr reads elements via a CaseTestExpr: check only arg */ + return saop_array_may_vary((Node *) castNode(ArrayCoerceExpr, node)->arg); + + case T_List: + case T_ScalarArrayOpExpr: + case T_BoolExpr: + case T_RelabelType: + case T_CoerceViaIO: + case T_ConvertRowtypeExpr: + case T_CoerceToDomain: + case T_ArrayExpr: + case T_RowExpr: + case T_RowCompareExpr: + case T_CoalesceExpr: + case T_MinMaxExpr: + case T_NullTest: + case T_BooleanTest: + case T_FieldSelect: + case T_SubscriptingRef: + case T_CaseExpr: + case T_SQLValueFunction: + + /* fixed if their inputs are */ + break; + + default: + + /* anything else, including future node types, may vary */ + return true; + } + + return expression_tree_walker(node, saop_array_may_vary_walker, context); +} /*-------------------- * estimate_expression_value diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index fd1454d685b..3f026505e56 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -646,6 +646,9 @@ typedef struct ExprEvalStep * returns. */ bool null_lhs_isnull; struct ScalarArrayOpExprHashTable *elements_tab; + + /* non-Const array, evaluated once; NULL for a Const */ + struct ExprState *array_expr; FunctionCallInfo fcinfo_data; /* arguments etc */ ScalarArrayOpExpr *saop; } hashedscalararrayop; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7439c6732a7..0229d502263 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -891,11 +891,13 @@ typedef OpExpr NullIfExpr; * the result type (or the collation) because it must be boolean. * * A ScalarArrayOpExpr with a valid hashfuncid is evaluated during execution - * by building a hash table containing the Const values from the RHS arg. + * by building a hash table containing the values from the RHS arg, which is + * a Const or is fixed for one execution (see convert_saop_to_hashed_saop()). * This table is probed during expression evaluation. The planner will set * hashfuncid to the hash function which must be used to build and probe the * hash table. The executor determines if it should use hash-based checks or * the more traditional means based on if the hashfuncid is set or not. + * Exceptions: see ExecInitExprRec() and ExecEvalHashedScalarArrayOp(). * * When performing hashed NOT IN, the negfuncid will also be set to the * equality function which the hash table must use to build and probe the hash diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out index 730f7bc7eba..d0c5b2a4ed5 100644 --- a/src/test/regress/expected/expressions.out +++ b/src/test/regress/expected/expressions.out @@ -327,6 +327,247 @@ select return_text_input('a') not in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i f (1 row) +rollback; +-- +-- Hashed ScalarArrayOpExpr with a non-Const array fixed for one execution +-- +begin; +create table saop_stab (i int); +insert into saop_stab select g from generate_series(1, 20) g; +-- a stable plpgsql function is never inlined, so the array stays non-Const +create function saop_intarr(int[]) returns int[] as + $$ begin return $1; end $$ language plpgsql stable; +-- 8 and 9 elements: below and at the threshold +select array_agg(i order by i) from saop_stab where i = any (saop_intarr('{1,2,3,4,5,6,7,8}')); + array_agg +------------------- + {1,2,3,4,5,6,7,8} +(1 row) + +select array_agg(i order by i) from saop_stab where i = any (saop_intarr('{1,2,3,4,5,6,7,8,9}')); + array_agg +--------------------- + {1,2,3,4,5,6,7,8,9} +(1 row) + +-- an operator inside the array +select array_agg(i order by i) from saop_stab where i = any (saop_intarr('{1,2,3,4}') || saop_intarr('{5,6,7,8,9}')); + array_agg +--------------------- + {1,2,3,4,5,6,7,8,9} +(1 row) + +-- empty array and NULL scalar: false for IN, true for NOT IN +select x, x = any (saop_intarr('{}')) as "in", + x <> all (saop_intarr('{}')) as not_in +from (values (1), (null)) v(x); + x | in | not_in +---+----+-------- + 1 | f | t + | f | t +(2 rows) + +-- NOT IN, with a NULL element, and on a short array (searched linearly) +select count(*) from saop_stab where i <> all (saop_intarr('{1,2,3,4,5,6,7,8,9,null}')); + count +------- + 0 +(1 row) + +select count(*) from saop_stab where i <> all (saop_intarr('{1,2,3}')); + count +------- + 17 +(1 row) + +-- generic plan: $1 stays a Param; long, short, NULL and empty arrays +set plan_cache_mode = force_generic_plan; +prepare saop_p(int[]) as + select array_agg(i order by i) from saop_stab where i = any ($1); +execute saop_p('{1,2,3,4,5,6,7,8,9,10}'); + array_agg +------------------------ + {1,2,3,4,5,6,7,8,9,10} +(1 row) + +execute saop_p('{11,12,13}'); + array_agg +------------ + {11,12,13} +(1 row) + +execute saop_p(null); + array_agg +----------- + +(1 row) + +execute saop_p('{}'); + array_agg +----------- + +(1 row) + +deallocate saop_p; +-- IN ($1, ..., $N) is an ArrayExpr of Params +prepare saop_in(int,int,int,int,int,int,int,int,int,int) as + select array_agg(i order by i) from saop_stab + where i in ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10); +execute saop_in(1,2,3,4,5,6,7,8,9,10); + array_agg +------------------------ + {1,2,3,4,5,6,7,8,9,10} +(1 row) + +deallocate saop_in; +-- $1::int[] cast +prepare saop_cast(text) as + select array_agg(i order by i) from saop_stab where i = any ($1::int[]); +execute saop_cast('{2,4,6,8,10,12,14,16,18,20}'); + array_agg +----------------------------- + {2,4,6,8,10,12,14,16,18,20} +(1 row) + +deallocate saop_cast; +-- soft error reporting (JSON DEFAULT ... ON ERROR): not hashed +prepare saop_json(text) as + select json_value(jsonb '"x"', '$' returning int + default textcat((5 = any ($1::int[]))::text, '1')::int on error); +savepoint saop_json_error; +execute saop_json('not an array'); +ERROR: could not coerce ON ERROR expression (DEFAULT) to the RETURNING type +DETAIL: malformed array literal: "not an array" +rollback to savepoint saop_json_error; +deallocate saop_json; +-- rescans of the inner side reuse the hash table +set plan_cache_mode = force_generic_plan; +prepare saop_rs(int[]) as + select d.x, s.c + from (values (1),(2),(3)) d(x) + cross join lateral + (select count(*) as c from saop_stab where i = any ($1) and i > d.x) s + order by d.x; +explain (costs off) execute saop_rs('{1,2,3,4,5,6,7,8,9,10}'); + QUERY PLAN +--------------------------------------------------------------------------- + Sort + Sort Key: "*VALUES*".column1 + -> Nested Loop + -> Values Scan on "*VALUES*" + -> Aggregate + -> Seq Scan on saop_stab + Filter: ((i > "*VALUES*".column1) AND (i = ANY ($1))) +(7 rows) + +execute saop_rs('{1,2,3,4,5,6,7,8,9,10}'); + x | c +---+--- + 1 | 9 + 2 | 8 + 3 | 7 +(3 rows) + +deallocate saop_rs; +reset plan_cache_mode; +-- two hashable ScalarArrayOpExprs in one qual +prepare saop_two(int[], int[]) as + select array_agg(i order by i) from saop_stab where i = any ($1) or i = any ($2); +execute saop_two('{1,2,3,4,5,6,7,8,9,10}', '{15,16,17,18,19,20,1,2,3,4}'); + array_agg +------------------------------------------ + {1,2,3,4,5,6,7,8,9,10,15,16,17,18,19,20} +(1 row) + +deallocate saop_two; +-- not hashed: an array that varies per row (a Var) +select count(*) from saop_stab +where i = any (array[i, -1, -2, -3, -4, -5, -6, -7, -8]); + count +------- + 20 +(1 row) + +-- ... nor per group (an aggregate in HAVING) +select i % 3 as g, count(*) from saop_stab +group by i % 3 +having min(i) = any (array_agg(i)) +order by g; + g | count +---+------- + 0 | 6 + 1 | 7 + 2 | 7 +(3 rows) + +-- ... nor per rescan (an outer reference in a sub-select); compare with the +-- 8-element array, which is never hashed +select d.k, + (select count(*) from saop_stab + where i = any (array[d.k,d.k+1,d.k+2,d.k+3,d.k+4,d.k+5,d.k+6,d.k+7,d.k+8])) as ge9, + (select count(*) from saop_stab + where i = any (array[d.k,d.k+1,d.k+2,d.k+3,d.k+4,d.k+5,d.k+6,d.k+7])) as lt9 +from (values (1),(8),(15)) d(k) +order by d.k; + k | ge9 | lt9 +----+-----+----- + 1 | 9 | 8 + 8 | 9 | 8 + 15 | 6 | 6 +(3 rows) + +-- PL/pgSQL simple expressions are not hashed; NOT IN must still use "<>" +create function saop_ne_all(x int, a int[]) returns bool language plpgsql + as $$ begin return x <> all (a); end $$; +select saop_ne_all(11, '{1,2,3,4,5,6,7,8,9,10}'), + saop_ne_all(5, '{1,2,3,4,5,6,7,8,9,10}'), + saop_ne_all(5, '{11,12,13,14,15,16,17,18,19,20}'); + saop_ne_all | saop_ne_all | saop_ne_all +-------------+-------------+------------- + t | f | t +(1 row) + +-- ... nor an array with merge_action() +create table saop_merge (id int); +insert into saop_merge values (1); +with m as ( + merge into saop_merge t using (values (1), (2)) s(id) on t.id = s.id + when matched then update set id = s.id + when not matched then insert values (s.id) + returning merge_action() as action, + 'INSERT' = any (array[merge_action(), 'a', 'b', 'c', 'd', 'e', + 'f', 'g', 'h', 'i']) as is_insert) +select action, is_insert from m order by action; + action | is_insert +--------+----------- + INSERT | t + UPDATE | f +(2 rows) + +-- ... nor one with a set-returning function +select 'b' = any (regexp_matches('abcdefghijklmnopqrstuvwxyz', + '(.)(.)(.)(.)(.)(.)(.)(.)(.)', 'g')) as has_b; + has_b +------- + t + f +(2 rows) + +-- ... nor one with OLD/NEW of a view's expression column +create view saop_merge_v as + select id, saop_intarr('{1,2,3,4,5,6,7,8,9,10}') as arr from saop_merge; +with m as ( + merge into saop_merge_v t using (values (1), (3)) s(id) on t.id = s.id + when matched then update set id = s.id + when not matched then insert values (s.id) + returning merge_action() as action, 5 = any (old.arr) as old_has_5) +select action, old_has_5 from m order by action; + action | old_has_5 +--------+----------- + INSERT | + UPDATE | t +(2 rows) + rollback; -- Test with non-strict equality function. -- We need to create our own type for this. diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql index 3b3048f9731..aa81744863f 100644 --- a/src/test/regress/sql/expressions.sql +++ b/src/test/regress/sql/expressions.sql @@ -134,6 +134,138 @@ select return_text_input('a') not in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i rollback; +-- +-- Hashed ScalarArrayOpExpr with a non-Const array fixed for one execution +-- +begin; + +create table saop_stab (i int); +insert into saop_stab select g from generate_series(1, 20) g; + +-- a stable plpgsql function is never inlined, so the array stays non-Const +create function saop_intarr(int[]) returns int[] as + $$ begin return $1; end $$ language plpgsql stable; + +-- 8 and 9 elements: below and at the threshold +select array_agg(i order by i) from saop_stab where i = any (saop_intarr('{1,2,3,4,5,6,7,8}')); +select array_agg(i order by i) from saop_stab where i = any (saop_intarr('{1,2,3,4,5,6,7,8,9}')); +-- an operator inside the array +select array_agg(i order by i) from saop_stab where i = any (saop_intarr('{1,2,3,4}') || saop_intarr('{5,6,7,8,9}')); + +-- empty array and NULL scalar: false for IN, true for NOT IN +select x, x = any (saop_intarr('{}')) as "in", + x <> all (saop_intarr('{}')) as not_in +from (values (1), (null)) v(x); + +-- NOT IN, with a NULL element, and on a short array (searched linearly) +select count(*) from saop_stab where i <> all (saop_intarr('{1,2,3,4,5,6,7,8,9,null}')); +select count(*) from saop_stab where i <> all (saop_intarr('{1,2,3}')); + +-- generic plan: $1 stays a Param; long, short, NULL and empty arrays +set plan_cache_mode = force_generic_plan; +prepare saop_p(int[]) as + select array_agg(i order by i) from saop_stab where i = any ($1); +execute saop_p('{1,2,3,4,5,6,7,8,9,10}'); +execute saop_p('{11,12,13}'); +execute saop_p(null); +execute saop_p('{}'); +deallocate saop_p; + +-- IN ($1, ..., $N) is an ArrayExpr of Params +prepare saop_in(int,int,int,int,int,int,int,int,int,int) as + select array_agg(i order by i) from saop_stab + where i in ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10); +execute saop_in(1,2,3,4,5,6,7,8,9,10); +deallocate saop_in; + +-- $1::int[] cast +prepare saop_cast(text) as + select array_agg(i order by i) from saop_stab where i = any ($1::int[]); +execute saop_cast('{2,4,6,8,10,12,14,16,18,20}'); +deallocate saop_cast; +-- soft error reporting (JSON DEFAULT ... ON ERROR): not hashed +prepare saop_json(text) as + select json_value(jsonb '"x"', '$' returning int + default textcat((5 = any ($1::int[]))::text, '1')::int on error); +savepoint saop_json_error; +execute saop_json('not an array'); +rollback to savepoint saop_json_error; +deallocate saop_json; + +-- rescans of the inner side reuse the hash table +set plan_cache_mode = force_generic_plan; +prepare saop_rs(int[]) as + select d.x, s.c + from (values (1),(2),(3)) d(x) + cross join lateral + (select count(*) as c from saop_stab where i = any ($1) and i > d.x) s + order by d.x; +explain (costs off) execute saop_rs('{1,2,3,4,5,6,7,8,9,10}'); +execute saop_rs('{1,2,3,4,5,6,7,8,9,10}'); +deallocate saop_rs; +reset plan_cache_mode; + +-- two hashable ScalarArrayOpExprs in one qual +prepare saop_two(int[], int[]) as + select array_agg(i order by i) from saop_stab where i = any ($1) or i = any ($2); +execute saop_two('{1,2,3,4,5,6,7,8,9,10}', '{15,16,17,18,19,20,1,2,3,4}'); +deallocate saop_two; + +-- not hashed: an array that varies per row (a Var) +select count(*) from saop_stab +where i = any (array[i, -1, -2, -3, -4, -5, -6, -7, -8]); + +-- ... nor per group (an aggregate in HAVING) +select i % 3 as g, count(*) from saop_stab +group by i % 3 +having min(i) = any (array_agg(i)) +order by g; + +-- ... nor per rescan (an outer reference in a sub-select); compare with the +-- 8-element array, which is never hashed +select d.k, + (select count(*) from saop_stab + where i = any (array[d.k,d.k+1,d.k+2,d.k+3,d.k+4,d.k+5,d.k+6,d.k+7,d.k+8])) as ge9, + (select count(*) from saop_stab + where i = any (array[d.k,d.k+1,d.k+2,d.k+3,d.k+4,d.k+5,d.k+6,d.k+7])) as lt9 +from (values (1),(8),(15)) d(k) +order by d.k; + +-- PL/pgSQL simple expressions are not hashed; NOT IN must still use "<>" +create function saop_ne_all(x int, a int[]) returns bool language plpgsql + as $$ begin return x <> all (a); end $$; +select saop_ne_all(11, '{1,2,3,4,5,6,7,8,9,10}'), + saop_ne_all(5, '{1,2,3,4,5,6,7,8,9,10}'), + saop_ne_all(5, '{11,12,13,14,15,16,17,18,19,20}'); + +-- ... nor an array with merge_action() +create table saop_merge (id int); +insert into saop_merge values (1); +with m as ( + merge into saop_merge t using (values (1), (2)) s(id) on t.id = s.id + when matched then update set id = s.id + when not matched then insert values (s.id) + returning merge_action() as action, + 'INSERT' = any (array[merge_action(), 'a', 'b', 'c', 'd', 'e', + 'f', 'g', 'h', 'i']) as is_insert) +select action, is_insert from m order by action; + +-- ... nor one with a set-returning function +select 'b' = any (regexp_matches('abcdefghijklmnopqrstuvwxyz', + '(.)(.)(.)(.)(.)(.)(.)(.)(.)', 'g')) as has_b; + +-- ... nor one with OLD/NEW of a view's expression column +create view saop_merge_v as + select id, saop_intarr('{1,2,3,4,5,6,7,8,9,10}') as arr from saop_merge; +with m as ( + merge into saop_merge_v t using (values (1), (3)) s(id) on t.id = s.id + when matched then update set id = s.id + when not matched then insert values (s.id) + returning merge_action() as action, 5 = any (old.arr) as old_has_5) +select action, old_has_5 from m order by action; + +rollback; + -- Test with non-strict equality function. -- We need to create our own type for this. -- 2.53.0