From 72bd38cc99a15da6f97373fae98027c908c398ea Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Mon, 8 Apr 2024 01:27:28 +0300 Subject: [PATCH] Transform OR clauses to ANY expression Replace (expr op C1) OR (expr op C2) ... with expr op ANY(ARRAY[C1, C2, ...]) on the preliminary stage of optimization when we are still working with the expression tree. Here Cn is a n-th constant expression, 'expr' is non-constant expression, 'op' is an operator which returns boolean result and has a commuter (for the case of reverse order of constant and non-constant parts of the expression, like 'Cn op expr'). Sometimes it can lead to not optimal plan. This is why there is a or_to_any_transform_limit GUC. It specifies a threshold value of length of arguments in an OR expression that triggers the OR-to-ANY transformation. Generally, more groupable OR arguments mean that transformation will be more likely to win than to lose. Discussion: https://postgr.es/m/567ED6CA.2040504%40sigaev.ru Author: Alena Rybakina Author: Andrey Lepikhov Reviewed-by: Peter Geoghegan Reviewed-by: Ranier Vilela Reviewed-by: Alexander Korotkov Reviewed-by: Robert Haas Reviewed-by: Jian He --- doc/src/sgml/config.sgml | 57 +++ src/backend/nodes/queryjumblefuncs.c | 27 ++ src/backend/optimizer/prep/prepqual.c | 399 +++++++++++++++++- src/backend/utils/misc/guc_tables.c | 12 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/nodes/queryjumble.h | 1 + src/include/optimizer/optimizer.h | 2 + src/test/regress/expected/create_index.out | 159 +++++++ src/test/regress/expected/join.out | 50 +++ src/test/regress/expected/partition_prune.out | 37 +- src/test/regress/sql/create_index.sql | 45 ++ src/test/regress/sql/join.sql | 11 + src/test/regress/sql/partition_prune.sql | 2 + src/tools/pgindent/typedefs.list | 2 + 14 files changed, 785 insertions(+), 20 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index d8e1282e12..ac945ca4d1 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -6304,6 +6304,63 @@ SELECT * FROM parent WHERE key = 2400; + + or_to_any_transform_limit (boolean) + + or_to_any_transform_limit configuration parameter + + + + + Sets the minimum length of arguments in an OR + expression exceeding which planner will try to lookup and group + multiple similar OR expressions to + ANY () + expressions. The grouping technique of this transformation is based + on the equivalence of variable sides. One side of such an expression + must be a constant clause, and the other must contain a variable + clause. The default value is 5. The value of + -1 completely disables the transformation. + + + The advantage of this OR-to-ANY transformation is + faster query planning and execution. In certain cases, this + transformation also leads to more effective plans containing + a single index scan instead of multiple bitmap scans. However, it + may also cause a planning regression when distinct + OR arguments are better to match to distinct indexes. + This may happen when they have different matching partial indexes or + have different distributions of other columns used in the query. + Generally, more groupable OR arguments mean that + transformation will be more likely to win than to lose. + + + For example, this query has its set of five OR + expressions transformed to ANY with the default + value of or_to_any_transform_limit. But not with + the increased value. + +# EXPLAIN SELECT * FROM tbl WHERE key = 1 OR key = 2 OR key = 3 OR key = 4 OR key = 5; + QUERY PLAN +----------------------------------------------------- + Seq Scan on tbl (cost=0.00..51.44 rows=64 width=4) + Filter: (key = ANY ('{1,2,3,4,5}'::integer[])) +(2 rows) + +# SET or_to_any_transform_limit = 6; +SET + +# EXPLAIN SELECT * FROM tbl WHERE key = 1 OR key = 2 OR key = 3 OR key = 4 OR key = 5; + QUERY PLAN +--------------------------------------------------------------------------- + Seq Scan on tbl (cost=0.00..67.38 rows=63 width=4) + Filter: ((key = 1) OR (key = 2) OR (key = 3) OR (key = 4) OR (key = 5)) +(2 rows) + + + + + plan_cache_mode (enum) diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index be823a7f8f..de29d51c63 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -141,6 +141,33 @@ JumbleQuery(Query *query) return jstate; } +JumbleState * +JumbleExpr(Expr *expr, uint64 *exprId) +{ + JumbleState *jstate = NULL; + + Assert(exprId != NULL); + + jstate = (JumbleState *) palloc(sizeof(JumbleState)); + + /* Set up workspace for query jumbling */ + jstate->jumble = (unsigned char *) palloc(JUMBLE_SIZE); + jstate->jumble_len = 0; + jstate->clocations_buf_size = 32; + jstate->clocations = (LocationLen *) + palloc(jstate->clocations_buf_size * sizeof(LocationLen)); + jstate->clocations_count = 0; + jstate->highest_extern_param_id = 0; + + /* Compute query ID */ + _jumbleNode(jstate, (Node *) expr); + *exprId = DatumGetUInt64(hash_any_extended(jstate->jumble, + jstate->jumble_len, + 0)); + + return jstate; +} + /* * Enables query identifier computation. * diff --git a/src/backend/optimizer/prep/prepqual.c b/src/backend/optimizer/prep/prepqual.c index cbcf83f847..1514dea8e9 100644 --- a/src/backend/optimizer/prep/prepqual.c +++ b/src/backend/optimizer/prep/prepqual.c @@ -31,16 +31,25 @@ #include "postgres.h" +#include "catalog/namespace.h" +#include "catalog/pg_operator.h" +#include "common/hashfn.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "nodes/queryjumble.h" #include "optimizer/optimizer.h" +#include "parser/parse_coerce.h" +#include "parser/parse_oper.h" #include "utils/lsyscache.h" +#include "utils/syscache.h" +int or_to_any_transform_limit = 5; static List *pull_ands(List *andlist); static List *pull_ors(List *orlist); static Expr *find_duplicate_ors(Expr *qual, bool is_check); static Expr *process_duplicate_ors(List *orlist); +static List *transform_or_to_any(List *orlist); /* @@ -266,6 +275,375 @@ negate_clause(Node *node) return (Node *) make_notclause((Expr *) node); } +/* + * The key for grouping similar operator expressions in transform_or_to_any(). + */ +typedef struct OrClauseGroupKey +{ + /* We need this to put this structure into list together with other nodes */ + NodeTag type; + + /* The expression of the variable side of operator */ + Expr *expr; + /* The operator of the operator expression */ + Oid opno; + /* The collation of the operator expression */ + Oid inputcollid; + /* The type of constant side of operator */ + Oid consttype; +} OrClauseGroupKey; + +/* + * The group of similar operator expressions in transform_or_to_any(). + */ +typedef struct OrClauseGroupEntry +{ + OrClauseGroupKey key; + + /* The list of constant sides of operators */ + List *consts; + + /* + * List of source expressions. We need this for convenience in case we + * will give up on transformation. + */ + List *exprs; +} OrClauseGroupEntry; + +/* + * The hash function for OrClauseGroupKey. + */ +static uint32 +orclause_hash(const void *data, Size keysize) +{ + OrClauseGroupKey *key = (OrClauseGroupKey *) data; + uint64 exprHash; + + Assert(keysize == sizeof(OrClauseGroupKey)); + Assert(IsA(data, Invalid)); + + (void) JumbleExpr(key->expr, &exprHash); + + return hash_combine((uint32) exprHash, + hash_combine((uint32) key->opno, + hash_combine((uint32) key->consttype, + (uint32) key->inputcollid))); +} + +/* + * The copy function for OrClauseGroupKey. + */ +static void * +orclause_keycopy(void *dest, const void *src, Size keysize) +{ + OrClauseGroupKey *src_key = (OrClauseGroupKey *) src; + OrClauseGroupKey *dst_key = (OrClauseGroupKey *) dest; + + Assert(sizeof(OrClauseGroupKey) == keysize); + Assert(IsA(src, Invalid)); + + dst_key->type = T_Invalid; + dst_key->expr = src_key->expr; + dst_key->opno = src_key->opno; + dst_key->consttype = src_key->consttype; + dst_key->inputcollid = src_key->inputcollid; + + return dst_key; +} + +/* + * The equality function for OrClauseGroupKey. + */ +static int +orclause_match(const void *data1, const void *data2, Size keysize) +{ + OrClauseGroupKey *key1 = (OrClauseGroupKey *) data1; + OrClauseGroupKey *key2 = (OrClauseGroupKey *) data2; + + Assert(sizeof(OrClauseGroupKey) == keysize); + Assert(IsA(key1, Invalid)); + Assert(IsA(key2, Invalid)); + + if (key1->opno == key2->opno && + key1->consttype == key2->consttype && + key1->inputcollid == key2->inputcollid && + equal(key1->expr, key2->expr)) + return 0; + + return 1; +} + +/* + * transform_or_to_any - + * Discover the args of an OR expression and try to group similar OR + * expressions to SAOP expressions. + * + * This transformation groups two-sided equality expression. One side of + * such an expression must be a plain constant or constant expression. The + * other side must be a variable expression without volatile functions. + * To group quals, opno, inputcollid of variable expression, and type of + * constant expression must be equal too. + * + * The grouping technique is based on the equivalence of variable sides of + * the expression: using exprId and equal() routine, it groups constant sides + * of similar clauses into an array. After the grouping procedure, each + * couple ('variable expression' and 'constant array') forms a new SAOP + * operation, which is added to the args list of the returning expression. + */ +static List * +transform_or_to_any(List *orlist) +{ + List *neworlist = NIL; + List *entries = NIL; + ListCell *lc; + HASHCTL info; + HTAB *or_group_htab = NULL; + int len_ors = list_length(orlist); + OrClauseGroupEntry *entry = NULL; + + Assert(or_to_any_transform_limit >= 0 && + len_ors >= or_to_any_transform_limit); + + MemSet(&info, 0, sizeof(info)); + info.keysize = sizeof(OrClauseGroupKey); + info.entrysize = sizeof(OrClauseGroupEntry); + info.hash = orclause_hash; + info.keycopy = orclause_keycopy; + info.match = orclause_match; + or_group_htab = hash_create("OR Groups", + len_ors, + &info, + HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_KEYCOPY); + + foreach(lc, orlist) + { + Node *orqual = lfirst(lc); + Node *const_expr; + Node *nconst_expr; + OrClauseGroupKey hashkey; + bool found; + Oid opno; + Oid consttype; + Node *leftop, + *rightop; + + if (!IsA(orqual, OpExpr)) + { + entries = lappend(entries, orqual); + continue; + } + + opno = ((OpExpr *) orqual)->opno; + if (get_op_rettype(opno) != BOOLOID) + { + /* Only operator returning boolean suits OR -> ANY transformation */ + entries = lappend(entries, orqual); + continue; + } + + /* + * Detect the constant side of the clause. Recall non-constant + * expression can be made not only with Vars, but also with Params, + * which is not bonded with any relation. Thus, we detect the const + * side - if another side is constant too, the orqual couldn't be an + * OpExpr. Get pointers to constant and expression sides of the qual. + */ + leftop = get_leftop(orqual); + if (IsA(leftop, RelabelType)) + leftop = (Node *) ((RelabelType *) leftop)->arg; + rightop = get_rightop(orqual); + if (IsA(rightop, RelabelType)) + rightop = (Node *) ((RelabelType *) rightop)->arg; + + if (IsA(leftop, Const)) + { + opno = get_commutator(opno); + + if (!OidIsValid(opno)) + { + /* commutator doesn't exist, we can't reverse the order */ + entries = lappend(entries, orqual); + continue; + } + + nconst_expr = get_rightop(orqual); + const_expr = get_leftop(orqual); + } + else if (IsA(rightop, Const)) + { + const_expr = get_rightop(orqual); + nconst_expr = get_leftop(orqual); + } + else + { + entries = lappend(entries, orqual); + continue; + } + + /* + * Forbid transformation for composite types, records, and volatile + * expressions. + */ + consttype = exprType(const_expr); + if (type_is_rowtype(exprType(const_expr)) || + type_is_rowtype(consttype) || + contain_volatile_functions((Node *) nconst_expr)) + { + entries = lappend(entries, orqual); + continue; + } + + /* + * At this point we definitely have a transformable clause. Classify + * it and add into specific group of clauses, or create new group. + */ + hashkey.type = T_Invalid; + hashkey.expr = (Expr *) nconst_expr; + hashkey.opno = opno; + hashkey.consttype = consttype; + hashkey.inputcollid = exprCollation(const_expr); + entry = hash_search(or_group_htab, &hashkey, HASH_ENTER, &found); + + if (unlikely(found)) + { + entry->consts = lappend(entry->consts, const_expr); + entry->exprs = lappend(entry->exprs, orqual); + } + else + { + entry->consts = list_make1(const_expr); + entry->exprs = list_make1(orqual); + + /* + * Add the entry to the list. It is needed exclusively to manage + * the problem with the order of transformed clauses in explain. + * Hash value can depend on the platform and version. Hence, + * sequental scan of the hash table would prone to change the + * order of clauses in lists and, as a result, break regression + * tests accidentially. + */ + entries = lappend(entries, entry); + } + } + + /* Let's convert each group of clauses to an ANY expression. */ + + /* + * Go through the list of groups and convert each, where number of consts + * more than 1. trivial groups move to OR-list again + */ + foreach(lc, entries) + { + Oid scalar_type; + Oid array_type; + + if (!IsA(lfirst(lc), Invalid)) + { + neworlist = lappend(neworlist, lfirst(lc)); + continue; + } + + entry = (OrClauseGroupEntry *) lfirst(lc); + + Assert(list_length(entry->consts) > 0); + Assert(list_length(entry->exprs) == list_length(entry->consts)); + + if (list_length(entry->consts) == 1) + { + /* + * Only one element returns origin expression into the BoolExpr + * args list unchanged. + */ + list_free(entry->consts); + neworlist = list_concat(neworlist, entry->exprs); + continue; + } + + /* + * Do the transformation. + */ + scalar_type = entry->key.consttype; + array_type = OidIsValid(scalar_type) ? get_array_type(scalar_type) : + InvalidOid; + + if (OidIsValid(array_type)) + { + /* + * OK: coerce all the right-hand non-Var inputs to the common type + * and build an ArrayExpr for them. + */ + List *aexprs = NIL; + ArrayExpr *newa = NULL; + ScalarArrayOpExpr *saopexpr = NULL; + HeapTuple opertup; + Form_pg_operator operform; + List *namelist = NIL; + + foreach(lc, entry->consts) + { + Node *node = (Node *) lfirst(lc); + + node = coerce_to_common_type(NULL, node, scalar_type, + "OR ANY Transformation"); + aexprs = lappend(aexprs, node); + } + + newa = makeNode(ArrayExpr); + /* array_collid will be set by parse_collate.c */ + newa->element_typeid = scalar_type; + newa->array_typeid = array_type; + newa->multidims = false; + newa->elements = aexprs; + newa->location = -1; + + /* + * Try to cast this expression to Const. Due to current strict + * transformation rules it should be done [almost] every time. + */ + newa = (ArrayExpr *) eval_const_expressions(NULL, (Node *) newa); + + opertup = SearchSysCache1(OPEROID, + ObjectIdGetDatum(entry->key.opno)); + if (!HeapTupleIsValid(opertup)) + elog(ERROR, "cache lookup failed for operator %u", + entry->key.opno); + + operform = (Form_pg_operator) GETSTRUCT(opertup); + if (!OperatorIsVisible(entry->key.opno)) + namelist = lappend(namelist, makeString(get_namespace_name(operform->oprnamespace))); + + namelist = lappend(namelist, makeString(pstrdup(NameStr(operform->oprname)))); + ReleaseSysCache(opertup); + + saopexpr = + (ScalarArrayOpExpr *) + make_scalar_array_op(NULL, + namelist, + true, + (Node *) entry->key.expr, + (Node *) newa, + -1); + saopexpr->inputcollid = entry->key.inputcollid; + + neworlist = lappend(neworlist, (void *) saopexpr); + } + else + { + /* + * If the const node's (right side of operator expression) type + * don't have “true” array type, then we cannnot do the + * transformation. We simply concatenate the expression node. + */ + list_free(entry->consts); + neworlist = list_concat(neworlist, entry->exprs); + } + } + hash_destroy(or_group_htab); + list_free(entries); + + /* One more trick: assemble correct clause */ + return neworlist; +} /* * canonicalize_qual @@ -601,10 +979,22 @@ process_duplicate_ors(List *orlist) } /* - * If no winners, we can't transform the OR + * If no winners, we can't do OR-to-ANY transformation. */ if (winners == NIL) - return make_orclause(orlist); + { + /* + * Make an attempt to group similar OR clauses into SAOP if the list + * is lengthy enough. + */ + if (or_to_any_transform_limit >= 0 && + list_length(orlist) >= or_to_any_transform_limit) + orlist = transform_or_to_any(orlist); + + /* Transformation could group all OR clauses to a single SAOP */ + return (list_length(orlist) == 1) ? + (Expr *) linitial(orlist) : make_orclause(orlist); + } /* * Generate new OR list consisting of the remaining sub-clauses. @@ -651,6 +1041,11 @@ process_duplicate_ors(List *orlist) } } + /* Make an attempt to group similar OR clauses into ANY operation */ + if (or_to_any_transform_limit >= 0 && + list_length(neworlist) >= or_to_any_transform_limit) + neworlist = transform_or_to_any(neworlist); + /* * Append reduced OR to the winners list, if it's not degenerate, handling * the special case of one element correctly (can that really happen?). diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 7d4e4387cf..f9bb2b0f9e 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -3657,6 +3657,18 @@ struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"or_to_any_transform_limit", PGC_USERSET, QUERY_TUNING_OTHER, + gettext_noop("Set the minimum length of the list of OR clauses to attempt the OR-to-ANY transformation."), + gettext_noop("Once the limit is reached, the planner will try to replace expression like " + "'x=c1 OR x=c2 ..' to the expression 'x = ANY(ARRAY[c1,c2,..])'"), + GUC_EXPLAIN + }, + &or_to_any_transform_limit, + 5, -1, INT_MAX, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 2166ea4a87..15b14ba850 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -392,6 +392,7 @@ # - Planner Method Configuration - #enable_async_append = on +#or_to_any_transform_limit = 0 #enable_bitmapscan = on #enable_gathermerge = on #enable_hashagg = on diff --git a/src/include/nodes/queryjumble.h b/src/include/nodes/queryjumble.h index f1c55c8067..5643ee8f65 100644 --- a/src/include/nodes/queryjumble.h +++ b/src/include/nodes/queryjumble.h @@ -65,6 +65,7 @@ extern PGDLLIMPORT int compute_query_id; extern const char *CleanQuerytext(const char *query, int *location, int *len); extern JumbleState *JumbleQuery(Query *query); +extern JumbleState *JumbleExpr(Expr *expr, uint64 *exprId); extern void EnableQueryId(void); extern PGDLLIMPORT bool query_id_enabled; diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h index 7b63c5cf71..960dfb97a3 100644 --- a/src/include/optimizer/optimizer.h +++ b/src/include/optimizer/optimizer.h @@ -133,6 +133,8 @@ extern void extract_query_dependencies(Node *query, /* in prep/prepqual.c: */ +extern PGDLLIMPORT int or_to_any_transform_limit; + extern Node *negate_clause(Node *node); extern Expr *canonicalize_qual(Expr *qual, bool is_check); diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index cf6eac5734..b4b42173e5 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -1889,6 +1889,165 @@ SELECT count(*) FROM tenk1 10 (1 row) +SET or_to_any_transform_limit = 0; +EXPLAIN (COSTS OFF) +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); + QUERY PLAN +------------------------------------------------------------------------------ + Index Scan using tenk1_thous_tenthous on tenk1 + Index Cond: ((thousand = 42) AND (tenthous = ANY ('{1,3,42}'::integer[]))) +(2 rows) + +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); + unique1 | unique2 | two | four | ten | twenty | hundred | thousand | twothousand | fivethous | tenthous | odd | even | stringu1 | stringu2 | string4 +---------+---------+-----+------+-----+--------+---------+----------+-------------+-----------+----------+-----+------+----------+----------+--------- + 42 | 5530 | 0 | 2 | 2 | 2 | 42 | 42 | 42 | 42 | 42 | 84 | 85 | QBAAAA | SEIAAA | OOOOxx +(1 row) + +SET or_to_any_transform_limit = 3; +EXPLAIN (COSTS OFF) -- or_transformation still works +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); + QUERY PLAN +------------------------------------------------------------------------------ + Index Scan using tenk1_thous_tenthous on tenk1 + Index Cond: ((thousand = 42) AND (tenthous = ANY ('{1,3,42}'::integer[]))) +(2 rows) + +SET or_to_any_transform_limit = 4; +EXPLAIN (COSTS OFF) -- or_transformation must be disabled +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------- + Bitmap Heap Scan on tenk1 + Recheck Cond: (((thousand = 42) AND (tenthous = 1)) OR ((thousand = 42) AND (tenthous = 3)) OR ((thousand = 42) AND (tenthous = 42))) + -> BitmapOr + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: ((thousand = 42) AND (tenthous = 1)) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: ((thousand = 42) AND (tenthous = 3)) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: ((thousand = 42) AND (tenthous = 42)) +(9 rows) + +RESET or_to_any_transform_limit; +SET or_to_any_transform_limit = 0; +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99); + QUERY PLAN +------------------------------------------------------------------------------------ + Aggregate + -> Bitmap Heap Scan on tenk1 + Recheck Cond: ((hundred = 42) AND (thousand = ANY ('{42,99}'::integer[]))) + -> BitmapAnd + -> Bitmap Index Scan on tenk1_hundred + Index Cond: (hundred = 42) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (thousand = ANY ('{42,99}'::integer[])) +(8 rows) + +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99); + count +------- + 10 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand < 42 OR thousand < 99 OR 43 > thousand OR 42 > thousand); + QUERY PLAN +------------------------------------------------------------------------------------------ + Aggregate + -> Bitmap Heap Scan on tenk1 + Recheck Cond: ((hundred = 42) AND (thousand < ANY ('{42,99,43,42}'::integer[]))) + -> BitmapAnd + -> Bitmap Index Scan on tenk1_hundred + Index Cond: (hundred = 42) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (thousand < ANY ('{42,99,43,42}'::integer[])) +(8 rows) + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3) OR thousand = 41; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on tenk1 + Recheck Cond: (((thousand = 42) AND (tenthous = ANY ('{1,3}'::integer[]))) OR (thousand = 41)) + -> BitmapOr + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: ((thousand = 42) AND (tenthous = ANY ('{1,3}'::integer[]))) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (thousand = 41) +(8 rows) + +SELECT count(*) FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3) OR thousand = 41; + count +------- + 10 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99 OR tenthous < 2) OR thousand = 41; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on tenk1 + Recheck Cond: (((hundred = 42) AND ((thousand = ANY ('{42,99}'::integer[])) OR (tenthous < 2))) OR (thousand = 41)) + -> BitmapOr + -> BitmapAnd + -> Bitmap Index Scan on tenk1_hundred + Index Cond: (hundred = 42) + -> BitmapOr + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (thousand = ANY ('{42,99}'::integer[])) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (tenthous < 2) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (thousand = 41) +(14 rows) + +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99 OR tenthous < 2) OR thousand = 41; + count +------- + 20 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 41 OR thousand = 99 AND tenthous = 2); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------ + Aggregate + -> Bitmap Heap Scan on tenk1 + Recheck Cond: ((hundred = 42) AND ((thousand = ANY ('{42,41}'::integer[])) OR ((thousand = 99) AND (tenthous = 2)))) + -> BitmapAnd + -> Bitmap Index Scan on tenk1_hundred + Index Cond: (hundred = 42) + -> BitmapOr + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: (thousand = ANY ('{42,41}'::integer[])) + -> Bitmap Index Scan on tenk1_thous_tenthous + Index Cond: ((thousand = 99) AND (tenthous = 2)) +(11 rows) + +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 41 OR thousand = 99 AND tenthous = 2); + count +------- + 10 +(1 row) + +RESET or_to_any_transform_limit; -- -- Check behavior with duplicate index column contents -- diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 8b640c2fc2..407d251a50 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -4233,6 +4233,56 @@ select * from tenk1 a join tenk1 b on Index Cond: (unique2 = 7) (19 rows) +SET or_to_any_transform_limit = 0; +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 = 1 and b.unique1 = 2) or + ((a.unique2 = 3 or a.unique2 = 7) and b.hundred = 4); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------ + Nested Loop + Join Filter: (((a.unique1 = 1) AND (b.unique1 = 2)) OR ((a.unique2 = ANY ('{3,7}'::integer[])) AND (b.hundred = 4))) + -> Bitmap Heap Scan on tenk1 b + Recheck Cond: ((unique1 = 2) OR (hundred = 4)) + -> BitmapOr + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 = 2) + -> Bitmap Index Scan on tenk1_hundred + Index Cond: (hundred = 4) + -> Materialize + -> Bitmap Heap Scan on tenk1 a + Recheck Cond: ((unique1 = 1) OR (unique2 = ANY ('{3,7}'::integer[]))) + -> BitmapOr + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 = 1) + -> Bitmap Index Scan on tenk1_unique2 + Index Cond: (unique2 = ANY ('{3,7}'::integer[])) +(17 rows) + +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 < 20 or a.unique1 = 3 or a.unique1 = 1 and b.unique1 = 2) or + ((a.unique2 = 3 or a.unique2 = 7) and b.hundred = 4); + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Join Filter: ((a.unique1 < 20) OR (a.unique1 = 3) OR ((a.unique1 = 1) AND (b.unique1 = 2)) OR ((a.unique2 = ANY ('{3,7}'::integer[])) AND (b.hundred = 4))) + -> Seq Scan on tenk1 b + -> Materialize + -> Bitmap Heap Scan on tenk1 a + Recheck Cond: ((unique1 < 20) OR (unique1 = 3) OR (unique1 = 1) OR (unique2 = ANY ('{3,7}'::integer[]))) + -> BitmapOr + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 < 20) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 = 3) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 = 1) + -> Bitmap Index Scan on tenk1_unique2 + Index Cond: (unique2 = ANY ('{3,7}'::integer[])) +(15 rows) + +RESET or_to_any_transform_limit; -- -- test placement of movable quals in a parameterized join tree -- diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index 46b78ba3c4..db507eff44 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -3,6 +3,7 @@ -- -- Force generic plans to be used for all prepared statements in this file. set plan_cache_mode = force_generic_plan; +set or_to_any_transform_limit = 0; create table lp (a char) partition by list (a); create table lp_default partition of lp default; create table lp_ef partition of lp for values in ('e', 'f'); @@ -82,23 +83,23 @@ explain (costs off) select * from lp where a is null; (2 rows) explain (costs off) select * from lp where a = 'a' or a = 'c'; - QUERY PLAN ----------------------------------------------------------- + QUERY PLAN +----------------------------------------------- Append -> Seq Scan on lp_ad lp_1 - Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) + Filter: (a = ANY ('{a,c}'::bpchar[])) -> Seq Scan on lp_bc lp_2 - Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar)) + Filter: (a = ANY ('{a,c}'::bpchar[])) (5 rows) explain (costs off) select * from lp where a is not null and (a = 'a' or a = 'c'); - QUERY PLAN --------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------- Append -> Seq Scan on lp_ad lp_1 - Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar))) + Filter: ((a IS NOT NULL) AND (a = ANY ('{a,c}'::bpchar[]))) -> Seq Scan on lp_bc lp_2 - Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar))) + Filter: ((a IS NOT NULL) AND (a = ANY ('{a,c}'::bpchar[]))) (5 rows) explain (costs off) select * from lp where a <> 'g'; @@ -515,10 +516,10 @@ explain (costs off) select * from rlp where a <= 31; (27 rows) explain (costs off) select * from rlp where a = 1 or a = 7; - QUERY PLAN --------------------------------- + QUERY PLAN +------------------------------------------ Seq Scan on rlp2 rlp - Filter: ((a = 1) OR (a = 7)) + Filter: (a = ANY ('{1,7}'::integer[])) (2 rows) explain (costs off) select * from rlp where a = 1 or b = 'ab'; @@ -596,13 +597,13 @@ explain (costs off) select * from rlp where a < 1 or (a > 20 and a < 25); -- where clause contradicts sub-partition's constraint explain (costs off) select * from rlp where a = 20 or a = 40; - QUERY PLAN ----------------------------------------- + QUERY PLAN +-------------------------------------------------- Append -> Seq Scan on rlp4_1 rlp_1 - Filter: ((a = 20) OR (a = 40)) + Filter: (a = ANY ('{20,40}'::integer[])) -> Seq Scan on rlp5_default rlp_2 - Filter: ((a = 20) OR (a = 40)) + Filter: (a = ANY ('{20,40}'::integer[])) (5 rows) explain (costs off) select * from rlp3 where a = 20; /* empty */ @@ -2072,10 +2073,10 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde'; explain (costs off) select * from hp where a = 1 and b = 'abcde' and (c = 2 or c = 3); - QUERY PLAN ----------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------- Seq Scan on hp2 hp - Filter: ((a = 1) AND (b = 'abcde'::text) AND ((c = 2) OR (c = 3))) + Filter: ((c = ANY ('{2,3}'::integer[])) AND (a = 1) AND (b = 'abcde'::text)) (2 rows) drop table hp2; diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index e296891cab..7059b4ea86 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -738,6 +738,51 @@ SELECT count(*) FROM tenk1 SELECT count(*) FROM tenk1 WHERE hundred = 42 AND (thousand = 42 OR thousand = 99); +SET or_to_any_transform_limit = 0; +EXPLAIN (COSTS OFF) +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); +SET or_to_any_transform_limit = 3; +EXPLAIN (COSTS OFF) -- or_transformation still works +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); +SET or_to_any_transform_limit = 4; +EXPLAIN (COSTS OFF) -- or_transformation must be disabled +SELECT * FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3 OR tenthous = 42); +RESET or_to_any_transform_limit; + +SET or_to_any_transform_limit = 0; +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99); +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99); +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand < 42 OR thousand < 99 OR 43 > thousand OR 42 > thousand); + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3) OR thousand = 41; +SELECT count(*) FROM tenk1 + WHERE thousand = 42 AND (tenthous = 1 OR tenthous = 3) OR thousand = 41; + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99 OR tenthous < 2) OR thousand = 41; +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 99 OR tenthous < 2) OR thousand = 41; + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 41 OR thousand = 99 AND tenthous = 2); +SELECT count(*) FROM tenk1 + WHERE hundred = 42 AND (thousand = 42 OR thousand = 41 OR thousand = 99 AND tenthous = 2); +RESET or_to_any_transform_limit; + -- -- Check behavior with duplicate index column contents -- diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index c4c6c7b8ba..266461fb5b 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -1409,6 +1409,17 @@ select * from tenk1 a join tenk1 b on (a.unique1 = 1 and b.unique1 = 2) or ((a.unique2 = 3 or a.unique2 = 7) and b.hundred = 4); +SET or_to_any_transform_limit = 0; +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 = 1 and b.unique1 = 2) or + ((a.unique2 = 3 or a.unique2 = 7) and b.hundred = 4); +explain (costs off) +select * from tenk1 a join tenk1 b on + (a.unique1 < 20 or a.unique1 = 3 or a.unique1 = 1 and b.unique1 = 2) or + ((a.unique2 = 3 or a.unique2 = 7) and b.hundred = 4); +RESET or_to_any_transform_limit; + -- -- test placement of movable quals in a parameterized join tree -- diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index dc71693861..c49153f38a 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -4,6 +4,7 @@ -- Force generic plans to be used for all prepared statements in this file. set plan_cache_mode = force_generic_plan; +set or_to_any_transform_limit = 0; create table lp (a char) partition by list (a); create table lp_default partition of lp default; @@ -21,6 +22,7 @@ explain (costs off) select * from lp where a is not null; explain (costs off) select * from lp where a is null; explain (costs off) select * from lp where a = 'a' or a = 'c'; explain (costs off) select * from lp where a is not null and (a = 'a' or a = 'c'); + explain (costs off) select * from lp where a <> 'g'; explain (costs off) select * from lp where a <> 'a' and a <> 'd'; explain (costs off) select * from lp where a not in ('a', 'd'); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6e0717c8c4..cb48e2f95a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1698,6 +1698,8 @@ NumericVar OM_uint32 OP OSAPerGroupState +OrClauseGroupEntry +OrClauseGroupKey OSAPerQueryState OSInfo OSSLCipher