From c68fe7488be6d93f29a2f2bdef91da7c8fd357ef Mon Sep 17 00:00:00 2001 From: jcoleman Date: Fri, 10 Apr 2020 21:40:50 +0000 Subject: [PATCH v4] Hash lookup const arrays in OR'd ScalarArrayOps Currently all scalar array op expressions execute as a linear search through the array argument. However when OR semantics are desired it's possible to instead use a hash lookup. Here we apply that optimization to constant arrays (so we don't need to worry about teaching expression execution when params change) of at least length 9 (since very short arrays average to the same number of comparisons for linear searches and thus avoid the preprocessing necessary to build the hash). --- src/backend/executor/execExpr.c | 84 +++++++- src/backend/executor/execExprInterp.c | 234 ++++++++++++++++++++++ src/include/executor/execExpr.h | 41 ++++ src/test/regress/expected/expressions.out | 45 +++++ src/test/regress/sql/expressions.sql | 12 ++ 5 files changed, 406 insertions(+), 10 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 2e463f5499..4b9714f071 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -31,6 +31,7 @@ #include "postgres.h" #include "access/nbtree.h" +#include "access/hash.h" #include "catalog/objectaccess.h" #include "catalog/pg_type.h" #include "executor/execExpr.h" @@ -50,6 +51,7 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" +#define MIN_ARRAY_SIZE_FOR_SAOP_HASH 9 typedef struct LastAttnumInfo { @@ -944,11 +946,13 @@ ExecInitExprRec(Expr *node, ExprState *state, case T_ScalarArrayOpExpr: { ScalarArrayOpExpr *opexpr = (ScalarArrayOpExpr *) node; + Oid func; Expr *scalararg; Expr *arrayarg; FmgrInfo *finfo; FunctionCallInfo fcinfo; AclResult aclresult; + bool useHash = false; Assert(list_length(opexpr->args) == 2); scalararg = (Expr *) linitial(opexpr->args); @@ -961,12 +965,62 @@ ExecInitExprRec(Expr *node, ExprState *state, if (aclresult != ACLCHECK_OK) aclcheck_error(aclresult, OBJECT_FUNCTION, get_func_name(opexpr->opfuncid)); - InvokeFunctionExecuteHook(opexpr->opfuncid); /* Set up the primary fmgr lookup information */ finfo = palloc0(sizeof(FmgrInfo)); fcinfo = palloc0(SizeForFunctionCallInfo(2)); - fmgr_info(opexpr->opfuncid, finfo); + func = opexpr->opfuncid; + + /* + * If we have a constant array and want OR semantics, then we + * implement the op with a hash lookup instead of looping + * through the entire array for each execution. + */ + if (opexpr->useOr && arrayarg && IsA(arrayarg, Const) && + !((Const *) arrayarg)->constisnull) + { + Datum arrdatum = ((Const *) arrayarg)->constvalue; + ArrayType *arr = (ArrayType *) DatumGetPointer(arrdatum); + int nitems; + + /* + * Only do the optimization if we have a large enough + * array to make it worth it. + */ + nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); + if (nitems >= MIN_ARRAY_SIZE_FOR_SAOP_HASH) + { + Oid hash_func; + + /* + * Find the hash op that matches the originally planned + * equality op. If we don't have one, we'll just fall + * back to the default linear scan implementation. + */ + useHash = get_op_hash_functions(opexpr->opno, NULL, &hash_func); + + if (useHash) + { + FmgrInfo *hash_finfo; + FunctionCallInfo hash_fcinfo; + + hash_finfo = palloc0(sizeof(FmgrInfo)); + hash_fcinfo = palloc0(SizeForFunctionCallInfo(2)); + fmgr_info(hash_func, hash_finfo); + fmgr_info_set_expr((Node *) node, hash_finfo); + InitFunctionCallInfoData(*hash_fcinfo, hash_finfo, 2, + opexpr->inputcollid, NULL, NULL); + InvokeFunctionExecuteHook(hash_func); + + scratch.d.scalararrayhashedop.hash_finfo = hash_finfo; + scratch.d.scalararrayhashedop.hash_fcinfo_data = hash_fcinfo; + scratch.d.scalararrayhashedop.hash_fn_addr = hash_finfo->fn_addr; + } + } + } + + InvokeFunctionExecuteHook(func); + fmgr_info(func, finfo); fmgr_info_set_expr((Node *) node, finfo); InitFunctionCallInfoData(*fcinfo, finfo, 2, opexpr->inputcollid, NULL, NULL); @@ -978,18 +1032,28 @@ ExecInitExprRec(Expr *node, ExprState *state, /* * Evaluate array argument into our return value. There's no * danger in that, because the return value is guaranteed to - * be overwritten by EEOP_SCALARARRAYOP, and will not be - * passed to any other expression. + * be overwritten by EEOP_SCALARARRAYOP[_HASHED], and will + * not be passed to any other expression. */ ExecInitExprRec(arrayarg, state, resv, resnull); /* And perform the operation */ - scratch.opcode = EEOP_SCALARARRAYOP; - scratch.d.scalararrayop.element_type = InvalidOid; - scratch.d.scalararrayop.useOr = opexpr->useOr; - scratch.d.scalararrayop.finfo = finfo; - scratch.d.scalararrayop.fcinfo_data = fcinfo; - scratch.d.scalararrayop.fn_addr = finfo->fn_addr; + if (useHash) + { + scratch.opcode = EEOP_SCALARARRAYOP_HASHED; + scratch.d.scalararrayhashedop.finfo = finfo; + scratch.d.scalararrayhashedop.fcinfo_data = fcinfo; + scratch.d.scalararrayhashedop.fn_addr = finfo->fn_addr; + } + else + { + scratch.opcode = EEOP_SCALARARRAYOP; + scratch.d.scalararrayop.element_type = InvalidOid; + scratch.d.scalararrayop.useOr = opexpr->useOr; + scratch.d.scalararrayop.finfo = finfo; + scratch.d.scalararrayop.fcinfo_data = fcinfo; + scratch.d.scalararrayop.fn_addr = finfo->fn_addr; + } ExprEvalPushStep(state, &scratch); break; } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 6308286d8c..d7968b299d 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -76,6 +76,7 @@ #include "utils/timestamp.h" #include "utils/typcache.h" #include "utils/xml.h" +#include "lib/qunique.h" /* * Use computed-goto-based opcode dispatch when computed gotos are available. @@ -178,6 +179,27 @@ static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate ExprContext *aggcontext, int setno); +static bool +saop_hash_element_match(struct saophash_hash *tb, Datum key1, Datum key2); +static uint32 saop_element_hash(struct saophash_hash *tb, Datum key); + +/* + * Define parameters for ScalarArrayOpExpr hash table code generation. The interface is + * *also* declared in execnodes.h (to generate the types, which are externally + * visible). + */ +#define SH_PREFIX saophash +#define SH_ELEMENT_TYPE ScalarArrayOpExprHashEntryData +#define SH_KEY_TYPE Datum +#define SH_KEY key +#define SH_HASH_KEY(tb, key) saop_element_hash(tb, key) +#define SH_EQUAL(tb, a, b) saop_hash_element_match(tb, a, b) +#define SH_SCOPE extern +#define SH_STORE_HASH +#define SH_GET_HASH(tb, a) a->hash +#define SH_DEFINE +#include "lib/simplehash.h" + /* * Prepare ExprState for interpreted execution. */ @@ -426,6 +448,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_DOMAIN_CHECK, &&CASE_EEOP_CONVERT_ROWTYPE, &&CASE_EEOP_SCALARARRAYOP, + &&CASE_EEOP_SCALARARRAYOP_HASHED, &&CASE_EEOP_XMLEXPR, &&CASE_EEOP_AGGREF, &&CASE_EEOP_GROUPING_FUNC, @@ -1436,6 +1459,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_SCALARARRAYOP_HASHED) + { + /* too complex for an inline implementation */ + ExecEvalScalarArrayOpHashed(state, op, econtext); + + EEO_NEXT(); + } + EEO_CASE(EEOP_DOMAIN_NOTNULL) { /* too complex for an inline implementation */ @@ -3345,6 +3376,209 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) *op->resnull = resultnull; } +/* + * Hash function for scalar array hash op elements. + * + * We use the element type's default hash opclass, and the column collation + * if the type is collation-sensitive. + */ +static uint32 +saop_element_hash(struct saophash_hash *tb, Datum key) +{ + ScalarArrayOpExprHashTable elements_tab = (ScalarArrayOpExprHashTable) tb->private_data; + FunctionCallInfo fcinfo = elements_tab->op->d.scalararrayhashedop.hash_fcinfo_data; + Datum hash; + + fcinfo->args[0].value = key; + fcinfo->args[0].isnull = false; + + hash = elements_tab->op->d.scalararrayhashedop.hash_fn_addr(fcinfo); + + return DatumGetUInt32(hash); +} + +/* + * Matching function for scalar array hash op elements, to be used in hashtable + * lookups. + */ +static bool +saop_hash_element_match(struct saophash_hash *tb, Datum key1, Datum key2) +{ + Datum result; + + ScalarArrayOpExprHashTable elements_tab = (ScalarArrayOpExprHashTable) tb->private_data; + FunctionCallInfo fcinfo = elements_tab->op->d.scalararrayhashedop.fcinfo_data; + + fcinfo->args[0].value = key1; + fcinfo->args[0].isnull = false; + fcinfo->args[1].value = key2; + fcinfo->args[1].isnull = false; + + result = elements_tab->op->d.scalararrayhashedop.fn_addr(fcinfo); + + return DatumGetBool(result); +} + +/* + * Evaluate "scalar op ANY (const array)". + * + * This is an optimized version of ExecEvalScalarArrayOp that only supports + * ANY (i.e., OR semantics) because it performs a hashtable lookup to determine + * if the array contains the value. + * + * Source array is in our result area, scalar arg is already evaluated into + * fcinfo->args[0]. + * + * The operator always yields boolean. + */ +void +ExecEvalScalarArrayOpHashed(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + ScalarArrayOpExprHashTable elements_tab = op->d.scalararrayhashedop.elements_tab; + FunctionCallInfo fcinfo = op->d.scalararrayhashedop.fcinfo_data; + bool strictfunc = op->d.scalararrayhashedop.finfo->fn_strict; + ArrayType *arr; + Datum scalar = fcinfo->args[0].value; + bool scalar_isnull = fcinfo->args[0].isnull; + Datum result; + bool resultnull; + bool hashfound; + + /* We don't setup a hashed scalar array op if the array const is null. */ + Assert(!*op->resnull); + + /* + * If the scalar is NULL, and the function is strict, return NULL; no + * point in executing the search. + */ + if (fcinfo->args[0].isnull && strictfunc) + { + *op->resnull = true; + return; + } + + /* Preprocess the array the first time we execute the op. */ + if (elements_tab == NULL) + { + int16 typlen; + bool typbyval; + char typalign; + int nitems; + int num_nulls = 0; + char *s; + bits8 *bitmap; + int bitmask; + MemoryContext oldcontext; + + arr = DatumGetArrayTypeP(*op->resvalue); + nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)); + + get_typlenbyvalalign(ARR_ELEMTYPE(arr), + &typlen, + &typbyval, + &typalign); + + oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory); + + elements_tab = (ScalarArrayOpExprHashTable) palloc(sizeof(ScalarArrayOpExprHashTableData)); + op->d.scalararrayhashedop.elements_tab = elements_tab; + elements_tab->op = op; + elements_tab->hashtab = saophash_create(CurrentMemoryContext, nitems, elements_tab); + + MemoryContextSwitchTo(oldcontext); + + s = (char *) ARR_DATA_PTR(arr); + bitmap = ARR_NULLBITMAP(arr); + bitmask = 1; + for (int i = 0; i < nitems; i++) + { + Datum element; + + /* Get array element, checking for NULL. */ + if (bitmap && (*bitmap & bitmask) == 0) + { + num_nulls++; + } + else + { + element = fetch_att(s, typbyval, typlen); + s = att_addlength_pointer(s, typlen, s); + s = (char *) att_align_nominal(s, typalign); + + saophash_insert(elements_tab->hashtab, element, &hashfound); + } + + /* Advance bitmap pointer if any. */ + if (bitmap) + { + bitmask <<= 1; + if (bitmask == 0x100) + { + bitmap++; + bitmask = 1; + } + } + } + + /* + * Remember if we had any nulls so that we know if we need to execute + * non-strict functions with a null lhs value if no match is found. + */ + op->d.scalararrayhashedop.has_nulls = num_nulls > 0; + + /* + * We only setup a binary search op if we have > 8 elements, so we don't + * need to worry about adding an optimization for the empty array case. + */ + Assert(nitems > 0); + } + + /* Check the hash to see if we have a match. */ + hashfound = NULL != saophash_lookup(elements_tab->hashtab, scalar); + + result = BoolGetDatum(hashfound); + resultnull = false; + + /* + * If we didn't find a match in the array, we still might need to handle + * the possibility of null values (we've previously removed them from the + * array). + */ + if (!DatumGetBool(result) && op->d.scalararrayhashedop.has_nulls) + { + if (strictfunc) + { + /* + * Had nulls and is a strict function, so instead of executing the + * function onces with a null rhs, we can assume null. + */ + result = (Datum) 0; + resultnull = true; + } + else + { + /* TODO: No regression test for this branch. */ + /* + * Execute function will null rhs just once. + * + * The hash lookup path will have scribbled on the lhs argument so + * we need to set it up also (even though we entered this function + * with it already set). + */ + fcinfo->args[0].value = scalar; + fcinfo->args[0].isnull = scalar_isnull; + fcinfo->args[1].value = (Datum) 0; + fcinfo->args[1].isnull = true; + + result = op->d.scalararrayhashedop.fn_addr(fcinfo); + resultnull = fcinfo->isnull; + } + } + + *op->resvalue = result; + *op->resnull = resultnull; +} + /* * Evaluate a NOT NULL domain constraint. */ diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 1b7f9865b0..dc298e3597 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -21,6 +21,30 @@ struct ExprEvalStep; struct SubscriptingRefState; +typedef struct ScalarArrayOpExprHashEntryData *ScalarArrayOpExprHashEntry; +typedef struct ScalarArrayOpExprHashTableData *ScalarArrayOpExprHashTable; + +typedef struct ScalarArrayOpExprHashEntryData +{ + Datum key; + uint32 status; /* hash status */ + uint32 hash; /* hash value (cached) */ +} ScalarArrayOpExprHashEntryData; + +/* define parameters necessary to generate the tuple hash table interface */ +#define SH_PREFIX saophash +#define SH_ELEMENT_TYPE ScalarArrayOpExprHashEntryData +#define SH_KEY_TYPE Datum +#define SH_SCOPE extern +#define SH_DECLARE +#include "lib/simplehash.h" + +typedef struct ScalarArrayOpExprHashTableData +{ + saophash_hash *hashtab; /* underlying hash table */ + struct ExprEvalStep *op; +} ScalarArrayOpExprHashTableData; + /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ #define EEO_FLAG_INTERPRETER_INITIALIZED (1 << 1) @@ -218,6 +242,7 @@ typedef enum ExprEvalOp /* evaluate assorted special-purpose expression types */ EEOP_CONVERT_ROWTYPE, EEOP_SCALARARRAYOP, + EEOP_SCALARARRAYOP_HASHED, EEOP_XMLEXPR, EEOP_AGGREF, EEOP_GROUPING_FUNC, @@ -554,6 +579,21 @@ typedef struct ExprEvalStep PGFunction fn_addr; /* actual call address */ } scalararrayop; + /* for EEOP_SCALARARRAYOP_HASHED */ + struct + { + bool has_nulls; + ScalarArrayOpExprHashTable elements_tab; + FmgrInfo *finfo; /* function's lookup data */ + FunctionCallInfo fcinfo_data; /* arguments etc */ + /* faster to access without additional indirection: */ + PGFunction fn_addr; /* actual call address */ + FmgrInfo *hash_finfo; /* function's lookup data */ + FunctionCallInfo hash_fcinfo_data; /* arguments etc */ + /* faster to access without additional indirection: */ + PGFunction hash_fn_addr; /* actual call address */ + } scalararrayhashedop; + /* for EEOP_XMLEXPR */ struct { @@ -725,6 +765,7 @@ extern void ExecEvalFieldStoreForm(ExprState *state, ExprEvalStep *op, extern void ExecEvalConvertRowtype(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op); +extern void ExecEvalScalarArrayOpHashed(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalConstraintNotNull(ExprState *state, ExprEvalStep *op); extern void ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op); extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op); diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out index 05a6eb07b2..42e1c0d1f2 100644 --- a/src/test/regress/expected/expressions.out +++ b/src/test/regress/expected/expressions.out @@ -158,3 +158,48 @@ select count(*) from date_tbl 13 (1 row) +-- +-- Tests for ScalarArrayOpExpr hash optimization +-- +select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1); + ?column? +---------- + t +(1 row) + +select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, null); + ?column? +---------- + +(1 row) + +select 1 in (null, null, null, null, null, null, null, null, null, null, null); + ?column? +---------- + +(1 row) + +select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1, null); + ?column? +---------- + t +(1 row) + +select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1); + ?column? +---------- + +(1 row) + +select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, null); + ?column? +---------- + +(1 row) + +select 'a' in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'); + ?column? +---------- + t +(1 row) + diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql index 1ca8bb151c..ef2738067b 100644 --- a/src/test/regress/sql/expressions.sql +++ b/src/test/regress/sql/expressions.sql @@ -65,3 +65,15 @@ select count(*) from date_tbl where f1 not between symmetric '1997-01-01' and '1998-01-01'; select count(*) from date_tbl where f1 not between symmetric '1997-01-01' and '1998-01-01'; + +-- +-- Tests for ScalarArrayOpExpr hash optimization +-- + +select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1); +select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, null); +select 1 in (null, null, null, null, null, null, null, null, null, null, null); +select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1, null); +select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1); +select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, null); +select 'a' in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'); -- 2.20.1