From 51bccfbda48e5198d7f0c00e64c28a936a4e2deb Mon Sep 17 00:00:00 2001 From: Linden Lance Date: Fri, 9 Oct 2026 01:34:45 +1300 Subject: [PATCH v3 2/2] Hash a ScalarArrayOpExpr whose array is fixed for one execution Since PG 14 (50e17ad281) a ScalarArrayOpExpr with "useOr" can be evaluated with a hash table instead of a linear scan of the array, but only when the array argument is a Const. In a generic plan, a parameterised list -- "col = ANY($1)", "col IN ($1, $2, ..., $N)", "col = ANY($1::int[])", "col = ANY(string_to_array($1, ','))" -- keeps a Param / ArrayExpr / FuncExpr in the plan and falls back to the O(rows * N) linear path; so does an array computed by a stable function, in any plan. That is the shape emitted by JDBC setArray, psycopg and asyncpg, and by any driver that expands an IN list into bind parameters, and a prepared statement switches to its generic plan after five executions when that plan looks cheaper. MySQL and MariaDB binary-search their sorted in_vector for the same query and stay sub-linear. Treat "is this array fixed for the execution?" as the property that matters, not the node type. * clauses.c: convert_saop_to_hashed_saop_walker() accepts, besides a non-null Const, any non-Const array argument that cannot vary from one row to the next (nor from group to group, nor from one window frame to the next). saop_array_may_vary_walker() decides that with an allowlist: it accepts only Consts, external Params, and node types that compute their result from their inputs alone -- function and operator calls that don't return a set, casts, array and row constructors, CASE, COALESCE and the like -- and treats everything else as varying: a Var or PlaceHolderVar of any level, an aggregate, grouping or window function, merge_action(), a ReturningExpr (OLD/NEW of a view column that is an expression, which is NULL when that row doesn't exist), a sub-select, a non-external Param, a CaseTestExpr or CoerceToDomainValue placeholder, and any node type added later. An ArrayCoerceExpr's per-element expression is not examined, as it reads each element through its own CaseTestExpr. Volatile functions are rejected too. A Var of any level must be rejected because this runs in preprocess_expression() before SS_replace_correlation_vars(), so an outer-query reference is still a Var, not a Param. The element count is known at plan time for a Const and for a one-dimensional ArrayExpr, so the MIN_ARRAY_SIZE_FOR_HASHED_SAOP cutoff is applied there; for anything else the executor applies it once the run-time array is known. The walker now also descends into the arguments of a ScalarArrayOpExpr it has just handled, as it already did for a non-Const array, so nested ones are considered too. * execExpr.c: for a non-Const stable array in a plan node the hashed step no longer emits the array sub-expression inline (which would rebuild it every row); it is compiled into an independent ExprState (array_expr) that the step owns and evaluates once. A standalone ExprState -- a PL/pgSQL "simple expression", whose compiled state is reused across calls with different parameter values -- gets a plain EEOP_SCALARARRAYOP instead, which must then use the operator's own function rather than the equality function a hashed NOT IN uses. So does an expression compiled to report errors softly (a JSON DEFAULT ... ON ERROR or ON EMPTY expression), since the separately compiled array would raise them. * execExprInterp.c: on the first evaluation, saop_hash_setup() evaluates array_expr, keeps a detoasted copy of the result in the run-time state, and builds the hash table from it. A run-time array shorter than the threshold gets no hash table and is searched linearly through ExecEvalArrayCompareInternal() instead, using element type metadata cached in the run-time state. As in ExecEvalScalarArrayOp(), a NULL array yields NULL and an empty one false (true for NOT IN) even for a NULL scalar, so the NULL-scalar shortcut for a strict function now comes after the array is known. array_expr takes the place of the finfo field removed by the previous patch, so ExprEvalStep does not grow. Regression tests are added to src/test/regress/sql/expressions.sql next to the existing hashed-SAOP tests: an external Param (long, short, NULL and empty arrays), IN ($1, ..., $N), $1::int[], a stable function, an operator inside the array, NOT IN, a NULL scalar with an empty array, rescans, a JSON DEFAULT ... ON ERROR expression, and arrays that must not be hashed: a Var, an aggregate in HAVING, a correlated sub-select, merge_action(), a set-returning function, OLD/NEW of a view column, and a PL/pgSQL simple expression. Not handled: a correlated array (a Var / PlaceHolderVar / PARAM_EXEC, or an ARRAY(SELECT ...) sub-select), which is deliberately excluded and stays on the linear path. Discussion: https://postgr.es/m/TY6PR01MB17327691C30B403369B16B02AF4B02@TY6PR01MB17327.jpnprd01.prod.outlook.com --- src/backend/executor/execExpr.c | 39 +++- src/backend/executor/execExprInterp.c | 138 ++++++++++--- src/backend/optimizer/plan/planner.c | 2 +- src/backend/optimizer/util/clauses.c | 153 ++++++++++---- 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, 631 insertions(+), 81 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 408a657d6b8..aebd03a3bba 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1272,6 +1272,21 @@ 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); + + /* + * A non-Const array is read only once by the hashed step. + * Don't hash it where that's unsafe: in an expression reused + * with new values (no parent plan, e.g. PL/pgSQL) or one that + * must catch errors (escontext set). + */ + use_hashed_step = OidIsValid(opexpr->hashfuncid) && + (IsA(arrayarg, Const) || + (state->parent != NULL && state->escontext == NULL)); /* * Select the correct comparison function. When we do hashed @@ -1279,7 +1294,7 @@ ExecInitExprRec(Expr *node, ExprState *state, * comparison function and negfuncid will be set to equality. * We need to use the equality function for hash probes. */ - if (OidIsValid(opexpr->negfuncid)) + if (use_hashed_step && OidIsValid(opexpr->negfuncid)) { Assert(OidIsValid(opexpr->hashfuncid)); cmpfuncid = opexpr->negfuncid; @@ -1287,10 +1302,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 +1311,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 +1331,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 +1350,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..a792384778a 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,29 @@ 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. + */ +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 +4282,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 +4294,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 +4303,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 +4345,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 +4362,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 +4402,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 +4421,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 +4447,27 @@ 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); + + /* + * As in ExecEvalScalarArrayOp(), a NULL array yields NULL and an empty + * one false (true for NOT IN), even for a NULL scalar. + */ + 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 +4479,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..ade60520c09 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -132,6 +132,7 @@ 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_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 +2659,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_walker()). * 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 +2688,36 @@ 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 = (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_walker(arrayarg, NULL) && + !contain_volatile_functions(arrayarg)) + { + if (IsA(arrayarg, ArrayExpr) && + !((ArrayExpr *) arrayarg)->multidims) + try_hashing = list_length(((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 +2725,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 +2743,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 +2759,77 @@ 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_walker + * True unless 'node', a ScalarArrayOpExpr's array, is certain to be the + * same for every row of one execution. + * + * Accepts only Consts, external Params and the node types listed below. Vars + * of every level are rejected, since this runs before + * SS_replace_correlation_vars(). The caller rejects volatile functions. + */ +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 ((Param *) node)->paramkind != PARAM_EXTERN; + + case T_FuncExpr: + if (((FuncExpr *) node)->funcretset) + return true; + break; + + case T_OpExpr: + case T_DistinctExpr: + case T_NullIfExpr: + if (((OpExpr *) node)->opretset) + return true; + break; + + case T_ArrayCoerceExpr: + + /* elemexpr reads elements via a CaseTestExpr: check only arg */ + return saop_array_may_vary_walker((Node *) ((ArrayCoerceExpr *) node)->arg, + context); + + 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