From 76969e77d613b48ef935a1451e050a2d276f63d1 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Thu, 9 Jul 2026 17:51:10 +0900 Subject: [PATCH v2] Introduce UniqueKeys to track the distinctness of relations Add a UniqueKeys facility that records, for each relation the planner builds, the sets of expressions over which its output is known to be distinct. A RelOptInfo's uniquekeys list holds UniqueKey nodes, each a set of EquivalenceClasses that no two output rows agree on; an empty set means the relation emits at most one row. Expressing keys as EquivalenceClasses, as pathkeys do, lets a key stand for any member of the class and keeps the representation compact. Each UniqueKey also carries a "null_aware" flag. A NULL-aware key holds even treating NULLs as equal, which is what justifies removing a DISTINCT or GROUP BY. A key that is not NULL-aware guarantees distinctness only among rows whose key expressions are all non-NULL, which still suffices to prove a join inner-unique or to match a strict join clause. Keys are always kept over the base (un-nulled) EquivalenceClasses; the flag records the effect of an outer join null-extending a column. UniqueKeys are deduced bottom-up. A base relation's keys come from its unique, immediately enforced, non-partial indexes. A subquery inherits the keys already deduced for its own final relation, translated into the outer query, plus a whole-row key for a non-ALL set operation. A join relation's keys are derived from its inputs by preserving one side's key when the other side is unique for the join clauses, and by combining a key from each side; an inner join's strict clauses can restore NULL-awareness to a key that lost it. Finally the grouping and DISTINCT steps are distinct over their clause columns, and steps that neither add nor duplicate rows carry their input's keys through unchanged. To bound the bookkeeping, keys are tracked only over ECs that some consumer could use. Those consumers are: removing a redundant DISTINCT or GROUP BY step when the input is already distinct over the relevant columns; proving a join inner-unique in innerrel_is_unique(), now including multi-relation inner sides; and skipping the unique-ification of a semijoin's RHS when it is already distinct. See src/backend/optimizer/README for a fuller description. --- .../expected/pg_stash_advice.out | 42 +- .../postgres_fdw/expected/postgres_fdw.out | 28 +- src/backend/optimizer/README | 97 ++ src/backend/optimizer/path/Makefile | 3 +- src/backend/optimizer/path/allpaths.c | 39 + src/backend/optimizer/path/equivclass.c | 8 +- src/backend/optimizer/path/meson.build | 1 + src/backend/optimizer/path/pathkeys.c | 3 +- src/backend/optimizer/path/uniquekeys.c | 1330 +++++++++++++++++ src/backend/optimizer/plan/analyzejoins.c | 28 +- src/backend/optimizer/plan/planner.c | 152 +- src/backend/optimizer/util/relnode.c | 6 + src/include/nodes/pathnodes.h | 42 + src/include/optimizer/paths.h | 38 +- src/test/regress/expected/aggregates.out | 49 +- src/test/regress/expected/join.out | 39 +- src/test/regress/expected/uniquekeys.out | 710 +++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/aggregates.sql | 24 +- src/test/regress/sql/uniquekeys.sql | 231 +++ src/tools/pgindent/typedefs.list | 1 + 21 files changed, 2746 insertions(+), 127 deletions(-) create mode 100644 src/backend/optimizer/path/uniquekeys.c create mode 100644 src/test/regress/expected/uniquekeys.out create mode 100644 src/test/regress/sql/uniquekeys.sql diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice.out b/contrib/pg_stash_advice/expected/pg_stash_advice.out index 8c24a21295e..4e1b6d958a7 100644 --- a/contrib/pg_stash_advice/expected/pg_stash_advice.out +++ b/contrib/pg_stash_advice/expected/pg_stash_advice.out @@ -142,21 +142,20 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id WHERE val1 = 1 AND val2 = 1; - QUERY PLAN ---------------------------------------------------- - Nested Loop - -> Hash Join - Hash Cond: (f.dim2_id = d2.id) - -> Seq Scan on aa_fact f - -> Hash + QUERY PLAN +------------------------------------------------------------ + Hash Join + Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id)) + -> Seq Scan on aa_fact f + -> Hash + -> Nested Loop -> Seq Scan on aa_dim2 d2 Filter: (val2 = 1) - -> Index Scan using aa_dim1_pkey on aa_dim1 d1 - Index Cond: (id = f.dim1_id) - Filter: (val1 = 1) + -> Seq Scan on aa_dim1 d1 + Filter: (val1 = 1) Supplied Plan Advice: NESTED_LOOP_PLAIN(d1) /* matched */ -(12 rows) +(11 rows) -- Add a useless extra entry to our test stash. Shouldn't change the result -- from the previous test. @@ -172,21 +171,20 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id WHERE val1 = 1 AND val2 = 1; - QUERY PLAN ---------------------------------------------------- - Nested Loop - -> Hash Join - Hash Cond: (f.dim2_id = d2.id) - -> Seq Scan on aa_fact f - -> Hash + QUERY PLAN +------------------------------------------------------------ + Hash Join + Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id)) + -> Seq Scan on aa_fact f + -> Hash + -> Nested Loop -> Seq Scan on aa_dim2 d2 Filter: (val2 = 1) - -> Index Scan using aa_dim1_pkey on aa_dim1 d1 - Index Cond: (id = f.dim1_id) - Filter: (val1 = 1) + -> Seq Scan on aa_dim1 d1 + Filter: (val1 = 1) Supplied Plan Advice: NESTED_LOOP_PLAIN(d1) /* matched */ -(12 rows) +(11 rows) -- Try an empty stash to be sure it does nothing SELECT pg_create_advice_stash('regress_empty_stash'); diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 5a77671d35f..5fb42676159 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -3458,22 +3458,20 @@ select sum(c1%3), sum(distinct c1%3 order by c1%3) filter (where c1%3 < 2), c2 f -- Outer query is aggregation query explain (verbose, costs off) select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft1 t1 where t1.c1 = 6) from ft2 t2 where t2.c2 % 6 = 0 order by 1; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------- - Unique + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------ + Sort Output: ((SubPlan expr_1)) - -> Sort - Output: ((SubPlan expr_1)) - Sort Key: ((SubPlan expr_1)) - -> Foreign Scan - Output: (SubPlan expr_1) - Relations: Aggregate on (public.ft2 t2) - Remote SQL: SELECT count(*) FILTER (WHERE ((c2 = 6) AND ("C 1" < 10))) FROM "S 1"."T 1" WHERE (((c2 % 6) = 0)) - SubPlan expr_1 - -> Foreign Scan on public.ft1 t1 - Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10)))) - Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 6)) -(13 rows) + Sort Key: ((SubPlan expr_1)) + -> Foreign Scan + Output: (SubPlan expr_1) + Relations: Aggregate on (public.ft2 t2) + Remote SQL: SELECT count(*) FILTER (WHERE ((c2 = 6) AND ("C 1" < 10))) FROM "S 1"."T 1" WHERE (((c2 % 6) = 0)) + SubPlan expr_1 + -> Foreign Scan on public.ft1 t1 + Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10)))) + Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 6)) +(11 rows) select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft1 t1 where t1.c1 = 6) from ft2 t2 where t2.c2 % 6 = 0 order by 1; count diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README index 78a307cc523..44c853a6be6 100644 --- a/src/backend/optimizer/README +++ b/src/backend/optimizer/README @@ -1056,6 +1056,103 @@ Currently this happens only for queries involving multiple window functions with different orderings, for which extra sorts are needed anyway. +UniqueKeys +---------- + +The UniqueKeys data structure represents what is known about the distinctness +of the tuples generated by a relation. A RelOptInfo's uniquekeys field is a +list of UniqueKey nodes, each asserting that the relation cannot output two +rows that are equal on a particular set of expressions. As with PathKeys, +those expressions are represented by EquivalenceClasses, so a UniqueKey is +essentially a set of ECs that no two output rows agree on. A UniqueKey with +an empty set means the relation emits at most one row. + +Each UniqueKey also carries a "null_aware" flag. When it is true the +guarantee holds even treating NULLs as equal: no two rows are equal on the +key expressions under that reading either, which is what justifies removing a +redundant DISTINCT or GROUP BY step. When it is false, distinctness is +guaranteed only among rows whose key expressions are all non-NULL; that is +weaker, but still enough to prove a join inner-unique or to match a strict +join clause, where rows with NULL key values cannot match anything anyway. + +Uniquekeys reference whole EquivalenceClasses for the same reason pathkeys +do: whenever two of an EC's members both appear in a relation's output, a +restriction or join clause derived from the EC has been applied, so the two +are equal in every emitted row and naming the class rather than a particular +member loses nothing. Keys are always expressed over the base (un-nulled) +ECs. A Var emitted from the nullable side of an outer join carries +varnullingrel bits and so belongs to a different EC than the base Var (just +as for pathkeys); rather than switch a key onto that EC as it crosses the +join, we leave the key on the base EC and let the "null_aware" flag record +the outer join's effect. A consumer looking up a column's EC strips the +varnullingrel bits first, so a reference to the null-extended column still +matches the base-EC key. + +UniqueKeys are derived bottom-up, as each RelOptInfo is built. + +A base relation's keys come from its own structure. A plain table is +distinct over the columns of any unique, immediately enforced, non-partial +index; a key column equated to a constant may be dropped, since the remaining +columns still determine the row. Such a key is NULL-aware only if every +remaining column is known non-NULL, from a NOT NULL constraint or a strict +restriction clause, or the index is declared NULLS NOT DISTINCT. A subquery +in FROM inherits the distinctness already deduced for its own final relation: +whatever uniquekeys that relation carries are translated, with their +NULL-awareness, into the outer query's EquivalenceClasses. A set operation is +the one deduplicating step that leaves no key on the subquery's final +relation, so a non-ALL set operation's whole-output-row key is derived +directly. + +A join relation's keys are derived from those of its two inputs, say A and B. +Each output row of the join pairs one row of A with one row of B, except that +an outer join may emit a row of one side paired with a null-extension of the +other. Two rules generate the join's keys: + + Preservation: a key of one side alone remains a key of the join if the + join does not duplicate that side's rows, that is, each contributes at + most one output row. This holds when the other side is unique for the + join clauses (each row matches at most one row of it), and + unconditionally for the retained side of a semijoin or antijoin (whose + output is a subset of it). + + Combination: the union of a key of A and a key of B is a key of the + join. Two output rows equal on the union agree on A's key, hence come + from the same A row, and on B's key, hence the same B row; a given pair + of source rows is emitted at most once. + +Both rules inherit the input keys' NULL-awareness, and a Preservation key +additionally loses it if the join null-extends its side. In that case the +empty key does not survive at all: it has no columns for the flag to exclude, +yet the join emits a fresh null-extended row for every unmatched row of the +other side, so "at most one row" no longer holds. + +Conversely, a key that is not NULL-aware can be strengthened back at an inner +join whose own clauses force its columns non-NULL. An inner join applies +every clause to all output rows, so any expression a strict clause requires +non-NULL is non-NULL throughout the output; if every EC of such a key holds +such an expression, no output row has a NULL key column and the key is in +fact NULL-aware. It does not apply at outer joins, whose clauses do not +filter their null-extended rows. + +A side with lateral references into the other is re-executed once per row of +that other side, so its rows are effectively multiplied and it keeps its keys +only through Combination, not Preservation. + +Finally, the query's upper relations get keys too. The grouping step is +NULL-aware distinct over the grouping columns, and emits a single row (the +empty key) for a plain aggregation or grouping only by constants; the DISTINCT +step is NULL-aware distinct over the DISTINCT columns. A step that cannot +introduce duplicate rows, such as window-function evaluation, sorting, or the +final LockRows/Limit/ModifyTable, carries its input's keys through unchanged. + +To bound the bookkeeping, keys are built only over EquivalenceClasses that +some consumer could actually use: those of the DISTINCT clause, of the GROUP +BY clause when the grouping step could be a no-op, and those that can form +join clauses (including the single-member ECs of an outer join's clause +sides, and the un-nulled counterparts of any of these). A key column outside +this set is not tracked, so not every unique index gives rise to a UniqueKey. + + Parameterized Paths ------------------- diff --git a/src/backend/optimizer/path/Makefile b/src/backend/optimizer/path/Makefile index 1e199ff66f7..7b9820c25f3 100644 --- a/src/backend/optimizer/path/Makefile +++ b/src/backend/optimizer/path/Makefile @@ -21,6 +21,7 @@ OBJS = \ joinpath.o \ joinrels.o \ pathkeys.o \ - tidpath.o + tidpath.o \ + uniquekeys.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 9c040ecefc8..4b3fda0fc46 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -95,6 +95,7 @@ join_search_hook_type join_search_hook = NULL; static void set_base_rel_consider_startup(PlannerInfo *root); static void set_base_rel_sizes(PlannerInfo *root); static void setup_simple_grouped_rels(PlannerInfo *root); +static void set_base_rel_uniquekeys(PlannerInfo *root); static void set_base_rel_pathlists(PlannerInfo *root); static void set_rel_size(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); @@ -200,6 +201,11 @@ make_one_rel(PlannerInfo *root, List *joinlist) */ setup_simple_grouped_rels(root); + /* + * Deduce the base rels' unique keys before the join search uses them. + */ + set_base_rel_uniquekeys(root); + /* * We should now have size estimates for every actual table involved in * the query, and we also know which if any have been deleted from the @@ -374,6 +380,39 @@ setup_simple_grouped_rels(PlannerInfo *root) } } +/* + * set_base_rel_uniquekeys + * Deduce the unique keys of each base relation's output. + * + * This needs to be done after EquivalenceClass merging and the EC-mutating + * steps (such as join removal) are complete, and after subqueries in FROM + * have been planned, since a subquery RTE's keys are translated from its + * final relation's uniquekeys. It must run before the join search starts, + * which derives join relations' keys from the base rels'. + */ +static void +set_base_rel_uniquekeys(PlannerInfo *root) +{ + Index rti; + + for (rti = 1; rti < root->simple_rel_array_size; rti++) + { + RelOptInfo *rel = root->simple_rel_array[rti]; + + /* there may be empty slots corresponding to non-baserel RTEs */ + if (rel == NULL) + continue; + + Assert(rel->relid == rti); /* sanity check on array */ + + /* ignore RTEs that are "other rels" */ + if (rel->reloptkind != RELOPT_BASEREL) + continue; + + populate_baserel_uniquekeys(root, rel); + } +} + /* * set_base_rel_pathlists * Finds all paths available for scanning each base-relation entry. diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index e3697df51a2..cf808a14910 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -495,6 +495,7 @@ process_equivalence(PlannerInfo *root, ec->ec_min_security = restrictinfo->security_level; ec->ec_max_security = restrictinfo->security_level; ec->ec_merged = NULL; + /* ec_index is set later */ em1 = add_eq_member(ec, item1, item1_relids, jdomain, item1_type); em2 = add_eq_member(ec, item2, item2_relids, @@ -863,6 +864,7 @@ get_eclass_for_sort_expr(PlannerInfo *root, } } + newec->ec_index = list_length(root->eq_classes); root->eq_classes = lappend(root->eq_classes, newec); /* @@ -871,7 +873,6 @@ get_eclass_for_sort_expr(PlannerInfo *root, */ if (root->ec_merging_done) { - int ec_index = list_length(root->eq_classes) - 1; int i = -1; while ((i = bms_next_member(newec->ec_relids, i)) > 0) @@ -891,7 +892,7 @@ get_eclass_for_sort_expr(PlannerInfo *root, Assert(rel->reloptkind == RELOPT_BASEREL); rel->eclass_indexes = bms_add_member(rel->eclass_indexes, - ec_index); + newec->ec_index); } } @@ -1208,6 +1209,9 @@ generate_base_implied_equalities(PlannerInfo *root) Assert(ec->ec_merged == NULL); /* else shouldn't be in list */ Assert(!ec->ec_broken); /* not yet anyway... */ + /* The EC's position is now final, since no more merging can happen */ + ec->ec_index = ec_index; + /* * Generate implied equalities that are restriction clauses. * Single-member ECs won't generate any deductions, either here or at diff --git a/src/backend/optimizer/path/meson.build b/src/backend/optimizer/path/meson.build index 98f3ebd5192..7d480656ce6 100644 --- a/src/backend/optimizer/path/meson.build +++ b/src/backend/optimizer/path/meson.build @@ -10,4 +10,5 @@ backend_sources += files( 'joinrels.c', 'pathkeys.c', 'tidpath.c', + 'uniquekeys.c', ) diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 5eb71635d15..eac8d029ddf 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -35,7 +35,6 @@ static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys); static bool matches_boolean_partition_clause(RestrictInfo *rinfo, RelOptInfo *partrel, int partkeycol); -static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle); static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey); @@ -1248,7 +1247,7 @@ convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel, * We need this to ensure that we don't return pathkeys describing values * that are unavailable above the level of the subquery scan. */ -static Var * +Var * find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle) { ListCell *lc; diff --git a/src/backend/optimizer/path/uniquekeys.c b/src/backend/optimizer/path/uniquekeys.c new file mode 100644 index 00000000000..f2b6c66bb14 --- /dev/null +++ b/src/backend/optimizer/path/uniquekeys.c @@ -0,0 +1,1330 @@ +/*------------------------------------------------------------------------- + * + * uniquekeys.c + * Routines to deduce what expression sets a relation is distinct over + * + * A relation's UniqueKeys record the expression sets over which its output + * is distinct. They let us elide a redundant DISTINCT or GROUP BY step and + * detect unique joins (inner_unique). + * + * This file populates the keys of base relations, join relations, and upper + * relations, and provides the lookups by which the various consumers answer + * specific distinctness questions. + * + * See src/backend/optimizer/README for a great deal of information about + * the nature and use of unique keys. + * + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/optimizer/path/uniquekeys.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/sysattr.h" +#include "nodes/makefuncs.h" +#include "nodes/multibitmapset.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/clauses.h" +#include "optimizer/optimizer.h" +#include "optimizer/pathnode.h" +#include "optimizer/paths.h" +#include "optimizer/planmain.h" +#include "parser/parse_agg.h" +#include "parser/parsetree.h" +#include "rewrite/rewriteManip.h" +#include "utils/lsyscache.h" + +/* Arbitrary cap on the number of UniqueKeys tracked per relation */ +#define MAX_UNIQUEKEYS 8 + +static void populate_plain_rel_uniquekeys(PlannerInfo *root, + RelOptInfo *rel); +static void populate_subquery_rel_uniquekeys(PlannerInfo *root, + RelOptInfo *rel); +static void translate_subquery_uniquekeys(PlannerInfo *root, RelOptInfo *rel); +static bool add_subquery_key_col(PlannerInfo *root, RelOptInfo *rel, + SortGroupClause *sgc, TargetEntry *tle, + Bitmapset **key_ecs); +static void strengthen_uniquekeys_for_inner_join(PlannerInfo *root, + RelOptInfo *joinrel, + List *restrictlist); +static bool uniquekey_forced_nonnullable(PlannerInfo *root, UniqueKey *ukey, + List *nonnullable_vars); +static bool var_in_nonnullable_vars(Var *var, List *nonnullable_vars); +static Bitmapset *get_interesting_unique_ecs(PlannerInfo *root); +static Bitmapset *add_ojclause_ecs(PlannerInfo *root, Bitmapset *result, + List *ojclauses); +static Bitmapset *add_base_ec_positions(PlannerInfo *root, Bitmapset *result, + EquivalenceClass *ec); +static bool collect_clause_ecs(PlannerInfo *root, List *clause, + Bitmapset **ecs_p); +static void add_uniquekey(RelOptInfo *rel, Bitmapset *eclass_indexes, + bool null_aware); +static bool rel_has_uniquekey_within(RelOptInfo *rel, Bitmapset *ecs, + bool need_null_aware); +static bool side_is_unique(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *side, RelOptInfo *otherside, + JoinType jointype, List *restrictlist); +static int find_ec_position(PlannerInfo *root, Expr *expr, + List *opfamilies, Oid collation, + Bitmapset *candidates); +static int base_ec_position(PlannerInfo *root, EquivalenceClass *ec); + + +/* + * populate_baserel_uniquekeys + * Deduce the unique keys of a base relation's output. + * + * This is called once per base rel, after its restriction clauses and (for + * plain relations) index information have been set up. + */ +void +populate_baserel_uniquekeys(PlannerInfo *root, RelOptInfo *rel) +{ + Assert(rel->reloptkind == RELOPT_BASEREL); + + /* + * Compute this now even if we have no rules for this rtekind: it is + * cached on PlannerInfo and can create ECs, so it must not first run + * inside GEQO's short-lived context. + */ + (void) get_interesting_unique_ecs(root); + + if (rel->rtekind == RTE_RELATION) + populate_plain_rel_uniquekeys(root, rel); + else if (rel->rtekind == RTE_SUBQUERY) + populate_subquery_rel_uniquekeys(root, rel); + /* We have no deduction rules for other rtekinds */ +} + +/* + * populate_plain_rel_uniquekeys + * Deduce unique keys for a plain relation from its unique indexes. + */ +static void +populate_plain_rel_uniquekeys(PlannerInfo *root, RelOptInfo *rel) +{ + Bitmapset *interesting = get_interesting_unique_ecs(root); + List *nonnullable_vars = NIL; + bool nonnullable_valid = false; + + if (bms_is_empty(interesting)) + return; + + foreach_node(IndexOptInfo, ind, rel->indexlist) + { + Bitmapset *key_ecs = NULL; + bool null_aware = true; + bool useless = false; + int c; + + /* + * If the index is not unique, or not immediately enforced, or if it's + * a partial index, it's useless here. + */ + if (!ind->unique || !ind->immediate || ind->indpred != NIL) + continue; + + for (c = 0; c < ind->nkeycolumns; c++) + { + Expr *colexpr; + EquivalenceClass *ec; + Oid equality_op; + int pos; + + colexpr = list_nth_node(TargetEntry, ind->indextlist, c)->expr; + + /* + * An EC's opfamily list is the full family membership of an + * equality operator, which may be more than one family, so we + * can't just use the index's opfamily here. + */ + equality_op = get_opfamily_member_for_cmptype(ind->opfamily[c], + ind->opcintype[c], + ind->opcintype[c], + COMPARE_EQ); + if (!OidIsValid(equality_op)) /* shouldn't happen */ + elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", + COMPARE_EQ, ind->opcintype[c], ind->opcintype[c], + ind->opfamily[c]); + + /* + * Find the EC mentioning this column. A column no suitable EC + * mentions cannot be referenced by any consumer, so such an index + * gives no usable key. + */ + pos = find_ec_position(root, colexpr, + get_mergejoin_opfamilies(equality_op), + ind->indexcollations[c], + rel->eclass_indexes); + if (pos < 0) + { + useless = true; + break; + } + ec = (EquivalenceClass *) list_nth(root->eq_classes, pos); + + /* + * A column equated to a constant can be left out of the key; + * uniqueness is then guaranteed by the remaining columns. + */ + if (ec->ec_has_const) + continue; + + if (!bms_is_member(pos, interesting)) + { + useless = true; + break; + } + + key_ecs = bms_add_member(key_ecs, pos); + + /* + * The column might be NULL, unless a NOT NULL constraint or some + * strict restriction clause says otherwise; a NULLS NOT DISTINCT + * index enforces uniqueness even among NULLs, though. + */ + if (null_aware && !ind->nullsnotdistinct) + { + if (!nonnullable_valid) + { + foreach_node(RestrictInfo, rinfo, rel->baserestrictinfo) + { + nonnullable_vars = + mbms_add_members(nonnullable_vars, + find_nonnullable_vars((Node *) rinfo->clause)); + } + nonnullable_valid = true; + } + + if (!IsA(colexpr, Var) || + (!var_is_nonnullable(root, (Var *) colexpr, NOTNULL_SOURCE_RELOPT) && + !var_in_nonnullable_vars((Var *) colexpr, nonnullable_vars))) + null_aware = false; + } + } + + if (!useless) + add_uniquekey(rel, key_ecs, null_aware); + } +} + +/* + * populate_subquery_rel_uniquekeys + * Deduce unique keys for a subquery RTE relation, in the same spirit as + * query_is_distinct_for(). + */ +static void +populate_subquery_rel_uniquekeys(PlannerInfo *root, RelOptInfo *rel) +{ + Query *subquery = planner_rt_fetch(rel->relid, root)->subquery; + Bitmapset *interesting = get_interesting_unique_ecs(root); + Bitmapset *key_ecs = NULL; + + if (bms_is_empty(interesting)) + return; + + /* + * Most of a subquery's distinctness is recorded in the UniqueKeys of its + * final relation, so we translate those into outer representation. + */ + translate_subquery_uniquekeys(root, rel); + + /* + * A tlist SRF is expanded above the set operation below and can duplicate + * its rows, which would void the whole-row key we deduce there, so skip + * when the subquery has one. + */ + if (subquery->hasTargetSRFs) + return; + + /* + * A set operation is the one remaining case with no UniqueKey built for + * the subquery's final relation, so record that here. + */ + if (subquery->setOperations) + { + /* + * UNION, INTERSECT, EXCEPT guarantee uniqueness of the whole output + * row, except with ALL. + */ + SetOperationStmt *topop = castNode(SetOperationStmt, + subquery->setOperations); + ListCell *lg; + + if (topop->all) + return; + + lg = list_head(topop->groupClauses); + foreach_node(TargetEntry, tle, subquery->targetList) + { + SortGroupClause *sgc; + + if (tle->resjunk) + continue; + + Assert(lg != NULL); + sgc = (SortGroupClause *) lfirst(lg); + lg = lnext(topop->groupClauses, lg); + + if (!add_subquery_key_col(root, rel, sgc, tle, &key_ecs)) + return; + } + + /* A set operation deduplicates NULLs too */ + add_uniquekey(rel, key_ecs, true); + } +} + +/* + * add_subquery_key_col + * Add the EC of one deduplicated subquery output column to the key being + * built; returns false if the column makes a key impossible. + */ +static bool +add_subquery_key_col(PlannerInfo *root, RelOptInfo *rel, + SortGroupClause *sgc, TargetEntry *tle, + Bitmapset **key_ecs) +{ + List *opfamilies = get_mergejoin_opfamilies(sgc->eqop); + Oid collation = exprCollation((Node *) tle->expr); + Var *colvar; + EquivalenceClass *ec; + int pos; + + /* a resjunk column is not visible to the outer query */ + if (tle->resjunk) + return false; + + /* if the equality operator isn't btree equality, no EC can match */ + if (opfamilies == NIL) + return false; + + /* Find the EC mentioning the corresponding output Var */ + colvar = makeVar(rel->relid, tle->resno, + exprType((Node *) tle->expr), + exprTypmod((Node *) tle->expr), + collation, 0); + pos = find_ec_position(root, (Expr *) colvar, opfamilies, collation, + rel->eclass_indexes); + if (pos < 0) + return false; + ec = (EquivalenceClass *) list_nth(root->eq_classes, pos); + + if (ec->ec_has_const) + return true; + + if (!bms_is_member(pos, get_interesting_unique_ecs(root))) + return false; + + *key_ecs = bms_add_member(*key_ecs, pos); + return true; +} + +/* + * translate_subquery_uniquekeys + * Build an uniquekeys list that describes the distinctness of a + * subquery's result, in the terms of the outer query. This is + * essentially a task of conversion. + * + * The subquery's final relation carries UniqueKeys over the subquery's own + * ECs. For each such key we map every EC to the parent EC of the + * corresponding output column and, if all of them map into the parent's + * interesting set, record the translated key on "rel". + */ +static void +translate_subquery_uniquekeys(PlannerInfo *root, RelOptInfo *rel) +{ + PlannerInfo *subroot = rel->subroot; + RelOptInfo *subfinal; + List *subquery_tlist; + Bitmapset *interesting; + + /* There is no subroot if set_subquery_pathlist() never ran */ + if (subroot == NULL) + return; + + subfinal = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL); + if (subfinal->uniquekeys == NIL) + return; + + subquery_tlist = subroot->processed_tlist; + interesting = get_interesting_unique_ecs(root); + + foreach_node(UniqueKey, subkey, subfinal->uniquekeys) + { + Bitmapset *parent_ecs = NULL; + bool ok = true; + int i = -1; + + while ((i = bms_next_member(subkey->eclass_indexes, i)) >= 0) + { + EquivalenceClass *sub_ec = list_nth_node(EquivalenceClass, + subroot->eq_classes, i); + EquivalenceClass *outer_ec = NULL; + + /* + * Find the parent EC that represents this subquery EC, reached + * through one of the subquery's output columns. This is the + * non-volatile arm of convert_subquery_pathkeys(): any matching + * outer EC will do, since all of an EC's members are equal, so + * uniqueness over one is uniqueness over any. A volatile EC + * can't be a key and won't match, so we don't bother searching + * for it. + */ + if (!sub_ec->ec_has_volatile) + { + foreach_node(EquivalenceMember, sub_member, sub_ec->ec_members) + { + Expr *sub_expr = sub_member->em_expr; + Oid sub_expr_type = sub_member->em_datatype; + Oid sub_expr_coll = sub_ec->ec_collation; + + /* Child members should not exist in ec_members */ + Assert(!sub_member->em_is_child); + + foreach_node(TargetEntry, tle, subquery_tlist) + { + Var *outer_var; + Expr *tle_expr; + + /* Is the TLE actually available to the outer query? */ + outer_var = find_var_for_subquery_tle(rel, tle); + if (!outer_var) + continue; + + /* + * The targetlist entry is considered to match if it + * matches after sort-key canonicalization. That is + * needed since the sub_expr has been through the same + * process. + */ + tle_expr = canonicalize_ec_expression(tle->expr, + sub_expr_type, + sub_expr_coll); + if (!equal(tle_expr, sub_expr)) + continue; + + /* See if we have a matching EC for the TLE */ + outer_ec = get_eclass_for_sort_expr(root, + (Expr *) outer_var, + sub_ec->ec_opfamilies, + sub_expr_type, + sub_expr_coll, + 0, + rel->relids, + false); + + if (outer_ec) + break; + } + + if (outer_ec) + break; + } + } + + /* It must map to an EC the parent tracks keys over */ + if (outer_ec == NULL || + !bms_is_member(outer_ec->ec_index, interesting)) + { + ok = false; + break; + } + + parent_ecs = bms_add_member(parent_ecs, outer_ec->ec_index); + } + + /* + * An empty subkey (the subquery emits at most one row) translates to + * an empty parent key, which the loop above yields with ok == true. + */ + if (ok) + add_uniquekey(rel, parent_ecs, subkey->null_aware); + } +} + +/* + * populate_joinrel_uniquekeys + * Deduce the unique keys of a join relation's output from those of its + * input relations. + * + * This is called just once per joinrel, with whichever pair of input relations + * the joinrel happens to be built from first. Uniqueness is a property of the + * joinrel's output, so it is independent of the pair used to prove it; other + * pairs might prove additional keys, but chasing those isn't worth the cycles. + */ +void +populate_joinrel_uniquekeys(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + SpecialJoinInfo *sjinfo, + List *restrictlist) +{ + JoinType jointype = sjinfo->jointype; + RelOptInfo *lhs = outerrel; + RelOptInfo *rhs = innerrel; + + /* A full join null-extends both sides; nothing survives. */ + if (jointype == JOIN_FULL) + return; + + /* Nothing to derive if neither input has keys */ + if (outerrel->uniquekeys == NIL && innerrel->uniquekeys == NIL) + return; + + /* Identify the special join's LHS */ + if (jointype != JOIN_INNER) + { + if (bms_is_subset(sjinfo->min_lefthand, outerrel->relids) && + bms_is_subset(sjinfo->min_righthand, innerrel->relids)) + { + lhs = outerrel; + rhs = innerrel; + } + else if (bms_is_subset(sjinfo->min_lefthand, innerrel->relids) && + bms_is_subset(sjinfo->min_righthand, outerrel->relids)) + { + lhs = innerrel; + rhs = outerrel; + } + else + { + /* + * The special join is not applied at this join: a semijoin RHS + * unique-ified and joined to only part of its LHS (see + * join_is_legal). We could derive keys from the unique-ified + * rel's distinctness, but don't bother. + */ + Assert(jointype == JOIN_SEMI); + return; + } + } + + /* + * Preservation of the LHS keys: unconditional for a semi/antijoin, else + * the RHS must be unique for the clauses so each LHS row yields at most + * one join row. A lateral reference into the RHS re-executes the LHS per + * RHS row, voiding this. + */ + if (lhs->uniquekeys != NIL && + !bms_overlap(lhs->lateral_relids, rhs->relids) && + ((jointype == JOIN_SEMI || jointype == JOIN_ANTI) || + side_is_unique(root, joinrel, rhs, lhs, jointype, restrictlist))) + { + foreach_node(UniqueKey, ukey, lhs->uniquekeys) + { + add_uniquekey(joinrel, ukey->eclass_indexes, ukey->null_aware); + } + } + + /* The RHS keys are not usable above a semi/anti join */ + if (jointype == JOIN_SEMI || jointype == JOIN_ANTI) + return; + + /* + * Preservation of the RHS keys, if the LHS is unique for the clauses. + * Across a left join they lose NULL-awareness, and an empty key does not + * survive null-extension (one null-extended row per unmatched LHS row). + */ + if (rhs->uniquekeys != NIL && + !bms_overlap(rhs->lateral_relids, lhs->relids) && + side_is_unique(root, joinrel, lhs, rhs, jointype, restrictlist)) + { + foreach_node(UniqueKey, ukey, rhs->uniquekeys) + { + if (jointype == JOIN_INNER) + add_uniquekey(joinrel, ukey->eclass_indexes, ukey->null_aware); + else if (!bms_is_empty(ukey->eclass_indexes)) + { + Assert(jointype == JOIN_LEFT); + add_uniquekey(joinrel, ukey->eclass_indexes, false); + } + } + } + + /* Combination: the union of one key from each side is a key */ + foreach_node(UniqueKey, lkey, lhs->uniquekeys) + { + foreach_node(UniqueKey, rkey, rhs->uniquekeys) + { + add_uniquekey(joinrel, + bms_union(lkey->eclass_indexes, + rkey->eclass_indexes), + lkey->null_aware && rkey->null_aware); + } + } + + /* Strengthen keys whose columns the inner join's clauses force non-NULL */ + if (jointype == JOIN_INNER) + strengthen_uniquekeys_for_inner_join(root, joinrel, restrictlist); +} + +/* + * strengthen_uniquekeys_for_inner_join + * Mark a joinrel's non-NULL-aware keys NULL-aware where the inner join's + * own clauses force their columns non-NULL. + * + * An inner join applies each clause to every output row, so any Var a strict + * clause requires non-NULL is non-NULL throughout the output. If every EC of + * such a key holds such a Var, no output row has a NULL key column, so the key + * is really NULL-aware. Outer-join clauses are not among these, so a key that + * lost NULL-awareness to null-extension is left alone. + */ +static void +strengthen_uniquekeys_for_inner_join(PlannerInfo *root, RelOptInfo *joinrel, + List *restrictlist) +{ + List *nonnullable_vars = NIL; + + /* Which Vars do this inner join's clauses force non-NULL? */ + foreach_node(RestrictInfo, rinfo, restrictlist) + { + nonnullable_vars = + mbms_add_members(nonnullable_vars, + find_nonnullable_vars((Node *) rinfo->clause)); + } + + if (nonnullable_vars == NIL) + return; + + foreach_node(UniqueKey, ukey, joinrel->uniquekeys) + { + /* Only a non-NULL-aware key that has columns can be strengthened */ + if (ukey->null_aware || bms_is_empty(ukey->eclass_indexes)) + continue; + + if (uniquekey_forced_nonnullable(root, ukey, nonnullable_vars)) + ukey->null_aware = true; + } +} + +/* + * uniquekey_forced_nonnullable + * Does every EquivalenceClass of ukey hold a Var that nonnullable_vars + * forces non-NULL? + */ +static bool +uniquekey_forced_nonnullable(PlannerInfo *root, UniqueKey *ukey, + List *nonnullable_vars) +{ + int i = -1; + + while ((i = bms_next_member(ukey->eclass_indexes, i)) >= 0) + { + EquivalenceClass *ec = list_nth_node(EquivalenceClass, + root->eq_classes, i); + ListCell *lc; + + /* The EC must hold a Var some clause forces non-NULL */ + foreach(lc, ec->ec_members) + { + EquivalenceMember *em = (EquivalenceMember *) lfirst(lc); + + if (IsA(em->em_expr, Var) && + var_in_nonnullable_vars((Var *) em->em_expr, nonnullable_vars)) + break; + } + if (lc == NULL) + return false; + } + + return true; +} + +/* + * var_in_nonnullable_vars + * Is "var" listed in the "nonnullable_vars" multibitmapset? + */ +static bool +var_in_nonnullable_vars(Var *var, List *nonnullable_vars) +{ + /* skip upper-level Vars */ + if (var->varlevelsup != 0) + return false; + + return mbms_is_member(var->varno, + var->varattno - FirstLowInvalidHeapAttributeNumber, + nonnullable_vars); +} + +/* + * populate_grouped_rel_uniquekeys + * Deduce the unique keys of the output of the query's grouping step: it + * is NULL-aware distinct over the grouping columns. With no + * non-redundant grouping columns, at most one row is emitted. + * + * When the grouping step is a no-op, the output is just the input, and every + * key of the input survives. + */ +void +populate_grouped_rel_uniquekeys(PlannerInfo *root, RelOptInfo *grouped_rel, + RelOptInfo *input_rel, bool is_noop) +{ + Query *parse = root->parse; + Bitmapset *key_ecs; + + /* Set-returning functions are expanded above the grouping step */ + if (parse->hasTargetSRFs) + return; + + if (parse->groupingSets) + { + /* + * Grouping sets emit one row per set, so the output is distinct only + * when a single (necessarily empty) set remains, or GROUP BY DISTINCT + * collapses them to one; grouping sets over expressions (groupClause + * set) are not distinct. Either way the result is one row: an empty + * key. + */ + if (parse->groupClause || + (!parse->groupDistinct && + list_length(parse->groupingSets) != 1)) + return; + + add_uniquekey(grouped_rel, NULL, true); + return; + } + + /* Carry over the input's keys if grouping does no work */ + if (is_noop) + { + foreach_node(UniqueKey, ukey, input_rel->uniquekeys) + { + add_uniquekey(grouped_rel, ukey->eclass_indexes, ukey->null_aware); + } + } + + /* Collect the ECs of the query's grouping columns */ + if (!collect_clause_ecs(root, root->processed_groupClause, &key_ecs)) + return; + + if (!bms_is_subset(key_ecs, get_interesting_unique_ecs(root))) + return; + + add_uniquekey(grouped_rel, key_ecs, true); +} + +/* + * populate_distinct_rel_uniquekeys + * Deduce the unique keys of the output of the query's DISTINCT step: it + * is NULL-aware distinct over the DISTINCT (or DISTINCT ON) columns. + * + * Like grouping, when the DISTINCT step is a no-op, the output is just the + * input, and every key of the input survives. + * + * Unlike grouping, DISTINCT is applied after target-list SRF expansion, so its + * output is distinct over the clause columns even when the tlist has SRFs. + */ +void +populate_distinct_rel_uniquekeys(PlannerInfo *root, RelOptInfo *distinct_rel, + RelOptInfo *input_rel, bool is_noop) +{ + Bitmapset *key_ecs; + + /* + * Nothing within a query level consumes distinctness after the DISTINCT + * step, so these keys are useful only to a parent query. Don't bother + * unless we are a subquery. + */ + if (root->query_level <= 1) + return; + + /* Carry over the input's keys if DISTINCT does no work */ + if (is_noop) + { + foreach_node(UniqueKey, ukey, input_rel->uniquekeys) + { + add_uniquekey(distinct_rel, ukey->eclass_indexes, ukey->null_aware); + } + } + + /* Collect the ECs of the query's DISTINCT columns */ + if (!collect_clause_ecs(root, root->processed_distinctClause, &key_ecs)) + return; + + if (!bms_is_subset(key_ecs, get_interesting_unique_ecs(root))) + return; + + add_uniquekey(distinct_rel, key_ecs, true); +} + +/* + * populate_unique_rel_uniquekeys + * Deduce the unique keys established by unique-ifying a semijoin's RHS. + * + * Like grouping, when the unique-ification step is a no-op, the output is just + * the input, and every key of the input survives. + */ +void +populate_unique_rel_uniquekeys(PlannerInfo *root, RelOptInfo *unique_rel, + RelOptInfo *input_rel, SpecialJoinInfo *sjinfo, + bool is_noop) +{ + Bitmapset *key_ecs = NULL; + ListCell *lc1; + ListCell *lc2; + + Assert(sjinfo->jointype == JOIN_SEMI); + + /* Carry over the input's keys if DISTINCT does no work */ + if (is_noop) + { + foreach_node(UniqueKey, ukey, input_rel->uniquekeys) + { + add_uniquekey(unique_rel, ukey->eclass_indexes, ukey->null_aware); + } + } + + /* + * Unique-ification deduplicates the RHS over the semijoin's right-hand + * expressions, keeping one row per group (treating NULLs as equal, like + * GROUP BY), so the result is NULL-aware distinct over those expressions. + */ + forboth(lc1, sjinfo->semi_rhs_exprs, lc2, sjinfo->semi_operators) + { + Expr *expr = (Expr *) lfirst(lc1); + List *opfamilies = get_mergejoin_opfamilies(lfirst_oid(lc2)); + Oid collation = exprCollation((Node *) expr); + EquivalenceClass *ec; + int pos; + + /* The key needs all these expressions, so we can't skip one */ + if (opfamilies == NIL) + return; + + /* Match in base-EC space: strip any outer-join nulling first */ + expr = (Expr *) remove_nulling_relids((Node *) expr, + root->outer_join_rels, NULL); + pos = find_ec_position(root, expr, opfamilies, collation, NULL); + if (pos < 0) + return; + + ec = list_nth_node(EquivalenceClass, root->eq_classes, pos); + + /* A column equated to a constant is not needed in the key */ + if (ec->ec_has_const) + continue; + + key_ecs = bms_add_member(key_ecs, pos); + } + + add_uniquekey(unique_rel, key_ecs, true); +} + +/* + * uniquekeys_distinct_is_noop + * Is the input relation of the query's DISTINCT step already provably + * distinct over the DISTINCT columns, making the step a no-op? + */ +bool +uniquekeys_distinct_is_noop(PlannerInfo *root, RelOptInfo *input_rel) +{ + Query *parse = root->parse; + Bitmapset *covered = NULL; + + if (input_rel->uniquekeys == NIL) + return false; + + if (parse->hasDistinctOn) + return false; + + if (parse->hasTargetSRFs) + return false; + + /* Collect the DISTINCT columns' ECs */ + foreach_node(PathKey, pathkey, root->distinct_pathkeys) + { + int pos = base_ec_position(root, pathkey->pk_eclass); + + if (pos >= 0) + covered = bms_add_member(covered, pos); + } + + /* DISTINCT considers NULLs equal, so only NULL-aware keys qualify */ + return rel_has_uniquekey_within(input_rel, covered, true); +} + +/* + * uniquekeys_grouping_is_noop + * Is the input relation of the query's grouping step already provably + * distinct over the grouping columns, i.e. does every group consist of + * exactly one row? + */ +bool +uniquekeys_grouping_is_noop(PlannerInfo *root, RelOptInfo *input_rel) +{ + Query *parse = root->parse; + Bitmapset *covered = NULL; + ListCell *lc; + + if (input_rel->uniquekeys == NIL) + return false; + + if (parse->groupClause == NIL || + parse->groupingSets != NIL || + parse->hasAggs || + parse->havingQual != NULL) + return false; + + /* Collect the grouping columns' ECs from the leading pathkeys */ + foreach(lc, root->group_pathkeys) + { + int pos; + + /* Only the leading num_groupby_pathkeys are grouping columns */ + if (foreach_current_index(lc) >= root->num_groupby_pathkeys) + break; + + pos = base_ec_position(root, ((PathKey *) lfirst(lc))->pk_eclass); + if (pos >= 0) + covered = bms_add_member(covered, pos); + } + + /* like DISTINCT, grouping considers NULLs equal */ + return rel_has_uniquekey_within(input_rel, covered, true); +} + +/* + * uniquekeys_match_join_clauses + * Do the given join clauses prove the relation distinct, i.e. do their + * rel-side expressions cover some unique key of it? + * + * The caller must have already determined that each clause is a mergejoinable + * equality with an expression of this relation on one side, with the transient + * outer_is_left flags set accordingly; this matches the clause lists built for + * rel_is_distinct_for(). + */ +bool +uniquekeys_match_join_clauses(PlannerInfo *root, RelOptInfo *rel, + List *clause_list) +{ + Bitmapset *covered = NULL; + + if (rel->uniquekeys == NIL) + return false; + + foreach_node(RestrictInfo, rinfo, clause_list) + { + EquivalenceClass *ec; + int pos; + + /* caller identified the inner side for us */ + ec = rinfo->outer_is_left ? rinfo->right_ec : rinfo->left_ec; + if (ec && (pos = base_ec_position(root, ec)) >= 0) + covered = bms_add_member(covered, pos); + } + + /* + * Nullable keys are fine here: rows with NULL key values cannot match any + * outer row through these strict clauses. + */ + return rel_has_uniquekey_within(rel, covered, false); +} + +/* + * uniquekeys_uniquification_is_noop + * Would unique-ifying the given semijoin RHS relation be a no-op, because + * its output is already provably distinct over the unique-ification + * columns? + * + * Nullable keys are acceptable: the semijoin operators are strict, so rows + * with NULL unique-ification columns match nothing, and not collapsing their + * duplicates cannot change the result. + */ +bool +uniquekeys_uniquification_is_noop(PlannerInfo *root, RelOptInfo *rel, + SpecialJoinInfo *sjinfo) +{ + Bitmapset *covered = NULL; + ListCell *lc1; + ListCell *lc2; + + Assert(sjinfo->jointype == JOIN_SEMI); + + if (rel->uniquekeys == NIL) + return false; + + /* + * Collect the unique-ification columns' ECs. Columns with no matching EC + * are simply left out: unique-ification groups by all the columns and + * keeps one row per group, and a rel distinct over a subset of them is + * necessarily distinct over the full set. So a key covering any subset + * of the columns already proves the step a no-op. + */ + forboth(lc1, sjinfo->semi_rhs_exprs, lc2, sjinfo->semi_operators) + { + Expr *expr = (Expr *) lfirst(lc1); + List *opfamilies = get_mergejoin_opfamilies(lfirst_oid(lc2)); + Oid collation = exprCollation((Node *) expr); + int pos; + + if (opfamilies == NIL) + continue; + /* Match in base-EC space: strip any outer-join nulling first */ + expr = (Expr *) remove_nulling_relids((Node *) expr, + root->outer_join_rels, NULL); + pos = find_ec_position(root, expr, opfamilies, collation, NULL); + if (pos >= 0) + covered = bms_add_member(covered, pos); + } + + return rel_has_uniquekey_within(rel, covered, false); +} + +/* + * get_interesting_unique_ecs + * Determine which ECs are worth tracking in UniqueKeys: those of the + * DISTINCT clause, of the GROUP BY clause when its step might be a no-op, + * and those that can generate join clauses. The last group is the + * multi-relation ECs, plus the single-member ECs on each side of a + * mergejoinable outer-join clause; the latter never merge into a join EC + * of their own, but inner_unique proofs look them up, so we track them + * too. + * + * Each contributing EC is recorded as its base (un-nulled) position via + * add_base_ec_positions, which may materialize a base EC on demand; the + * resulting set therefore contains only base-EC positions. + * + * Computed once and cached. ECs created later (e.g. for sort pathkeys) are + * never interesting, which is fine: keys come from clauses and pathkeys that + * predate the join search. + */ +static Bitmapset * +get_interesting_unique_ecs(PlannerInfo *root) +{ + Query *parse = root->parse; + Bitmapset *result = NULL; + int necs; + int i; + + if (root->interesting_unique_ecs_valid) + return root->interesting_unique_ecs; + + /* The multi-relation ECs, i.e. those that can generate join clauses */ + necs = list_length(root->eq_classes); + for (i = 0; i < necs; i++) + { + EquivalenceClass *ec = list_nth_node(EquivalenceClass, + root->eq_classes, i); + Relids base_relids; + + if (ec->ec_has_volatile) + continue; + + base_relids = bms_difference(ec->ec_relids, root->outer_join_rels); + + if (bms_membership(base_relids) == BMS_MULTIPLE) + result = add_base_ec_positions(root, result, ec); + + bms_free(base_relids); + } + + /* The DISTINCT columns */ + foreach_node(PathKey, pathkey, root->distinct_pathkeys) + { + result = add_base_ec_positions(root, result, pathkey->pk_eclass); + } + + /* The GROUP BY columns, when the grouping step might be a no-op */ + if (parse->groupClause != NIL && + parse->groupingSets == NIL && + !parse->hasAggs && + parse->havingQual == NULL) + { + ListCell *lc; + + foreach(lc, root->group_pathkeys) + { + /* Only the leading num_groupby_pathkeys are grouping columns */ + if (foreach_current_index(lc) >= root->num_groupby_pathkeys) + break; + + result = add_base_ec_positions(root, result, + ((PathKey *) lfirst(lc))->pk_eclass); + } + } + + /* Both sides of each mergejoinable outer-join clause */ + result = add_ojclause_ecs(root, result, root->left_join_clauses); + result = add_ojclause_ecs(root, result, root->right_join_clauses); + result = add_ojclause_ecs(root, result, root->full_join_clauses); + + root->interesting_unique_ecs = result; + root->interesting_unique_ecs_valid = true; + + return result; +} + +/* + * add_ojclause_ecs + * Subroutine for get_interesting_unique_ecs: add the ECs of both sides of + * the given mergejoinable outer-join clauses. + */ +static Bitmapset * +add_ojclause_ecs(PlannerInfo *root, Bitmapset *result, List *ojclauses) +{ + foreach_node(OuterJoinClauseInfo, ojcinfo, ojclauses) + { + RestrictInfo *rinfo = ojcinfo->rinfo; + + if (rinfo->left_ec) + result = add_base_ec_positions(root, result, rinfo->left_ec); + if (rinfo->right_ec) + result = add_base_ec_positions(root, result, rinfo->right_ec); + } + return result; +} + +/* + * add_base_ec_positions + * Subroutine for get_interesting_unique_ecs: add the position(s) of the + * base (un-nulled) EC(s) corresponding to "ec" to "result". + * + * Keys live in base-EC space, so the interesting set holds base positions. If + * "ec" is already a base EC, we add its own position. Otherwise we strip the + * nullingrels and use get_eclass_for_sort_expr to bring the base EC into + * existence if it doesn't already exist, so the base key can later be built + * and matched. + */ +static Bitmapset * +add_base_ec_positions(PlannerInfo *root, Bitmapset *result, + EquivalenceClass *ec) +{ + /* The passed ec might be non-canonical, so chase up to the top */ + while (ec->ec_merged) + ec = ec->ec_merged; + + /* A const-only EC has no base position */ + if (bms_is_empty(ec->ec_relids)) + return result; + + /* If ec is already a base EC, we add its own position */ + if (!bms_overlap(ec->ec_relids, root->outer_join_rels)) + return bms_add_member(result, ec->ec_index); + + /* Otherwise materialize the base EC of each nulled member */ + foreach_node(EquivalenceMember, em, ec->ec_members) + { + Node *base_expr; + EquivalenceClass *base_ec; + + if (em->em_is_const || + !bms_overlap(em->em_relids, root->outer_join_rels)) + continue; + + base_expr = remove_nulling_relids((Node *) em->em_expr, + root->outer_join_rels, NULL); + base_ec = get_eclass_for_sort_expr(root, (Expr *) base_expr, + ec->ec_opfamilies, + em->em_datatype, + ec->ec_collation, + 0, NULL, true); + result = bms_add_member(result, base_ec->ec_index); + } + + return result; +} + +/* + * collect_clause_ecs + * Find the ECs of the columns of a grouping or DISTINCT clause, omitting + * columns that are equivalent to constants. + * + * If any column has no matching EC, return false: the caller is building a + * rel's key, which must account for all columns. + */ +static bool +collect_clause_ecs(PlannerInfo *root, List *clause, Bitmapset **ecs_p) +{ + Bitmapset *ecs = NULL; + + *ecs_p = NULL; + + foreach_node(SortGroupClause, sgc, clause) + { + TargetEntry *tle = get_sortgroupclause_tle(sgc, + root->parse->targetList); + List *opfamilies = get_mergejoin_opfamilies(sgc->eqop); + Oid collation = exprCollation((Node *) tle->expr); + EquivalenceClass *ec; + int pos = -1; + + if (opfamilies != NIL) + { + /* Match in base-EC space: strip any outer-join nulling first */ + Expr *expr; + + expr = (Expr *) remove_nulling_relids((Node *) tle->expr, + root->outer_join_rels, NULL); + pos = find_ec_position(root, expr, opfamilies, collation, NULL); + } + if (pos < 0) + return false; + ec = (EquivalenceClass *) list_nth(root->eq_classes, pos); + + if (!ec->ec_has_const) + ecs = bms_add_member(ecs, pos); + } + + *ecs_p = ecs; + return true; +} + +/* + * add_uniquekey + * Add a UniqueKey to a relation's uniquekeys list, maintaining + * minimality: a key dominated by another one, whose EC set is a + * subset and whose NULL-awareness is at least as strong, is never + * kept. + */ +static void +add_uniquekey(RelOptInfo *rel, Bitmapset *eclass_indexes, bool null_aware) +{ + UniqueKey *ukey; + ListCell *lc; + + foreach(lc, rel->uniquekeys) + { + UniqueKey *other = lfirst_node(UniqueKey, lc); + + /* Reject the new key if some existing key dominates it */ + if ((other->null_aware || !null_aware) && + bms_is_subset(other->eclass_indexes, eclass_indexes)) + return; + /* ... and remove existing keys the new one dominates */ + if ((null_aware || !other->null_aware) && + bms_is_subset(eclass_indexes, other->eclass_indexes)) + rel->uniquekeys = foreach_delete_current(rel->uniquekeys, lc); + } + + if (list_length(rel->uniquekeys) >= MAX_UNIQUEKEYS) + return; + + ukey = makeNode(UniqueKey); + ukey->eclass_indexes = eclass_indexes; + ukey->null_aware = null_aware; + rel->uniquekeys = lappend(rel->uniquekeys, ukey); +} + +/* + * rel_has_uniquekey_within + * Does the rel have a key whose ECs all appear in "ecs"? If + * need_null_aware, only NULL-aware keys qualify. + */ +static bool +rel_has_uniquekey_within(RelOptInfo *rel, Bitmapset *ecs, bool need_null_aware) +{ + ListCell *lc; + + foreach(lc, rel->uniquekeys) + { + UniqueKey *ukey = lfirst_node(UniqueKey, lc); + + if ((!need_null_aware || ukey->null_aware) && + bms_is_subset(ukey->eclass_indexes, ecs)) + return true; + } + return false; +} + +/* + * side_is_unique + * Check whether "side" provably contains at most one row matching any + * given row of "otherside", using the join's restrictlist. + */ +static bool +side_is_unique(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *side, RelOptInfo *otherside, + JoinType jointype, List *restrictlist) +{ + /* A one-row rel is trivially unique, even with no join clauses */ + if (rel_has_uniquekey_within(side, NULL, false)) + return true; + + return innerrel_is_unique(root, joinrel->relids, otherside->relids, + side, jointype, restrictlist, false); +} + +/* + * find_ec_position + * Find the position in root->eq_classes of an EC containing an expression + * matching "expr". If "candidates" is non-NULL, consider only the ECs at + * those positions. Returns -1 if there is none. + * + * Keys live in base-EC space, so a caller matching a reference that an outer + * join may have null-extended must first strip its varnullingrels. + */ +static int +find_ec_position(PlannerInfo *root, Expr *expr, + List *opfamilies, Oid collation, Bitmapset *candidates) +{ + foreach_node(EquivalenceClass, ec, root->eq_classes) + { + int pos = ec->ec_index; + + /* ec_index is final once EC merging is done */ + Assert(list_nth(root->eq_classes, pos) == ec); + + if (candidates && !bms_is_member(pos, candidates)) + continue; + if (ec->ec_has_volatile || + ec->ec_collation != collation || + !equal(ec->ec_opfamilies, opfamilies)) + continue; + if (find_ec_member_matching_expr(ec, expr, NULL)) + return pos; + } + return -1; +} + +/* + * base_ec_position + * Find the position of the base (un-nulled) EC corresponding to "ec". + * + * A consumer's column reference may belong to a nulled EC, while keys are kept + * on the base ECs. Take a usable member of "ec", strip it to base form, and + * look that up. If "ec" is already a base EC, this returns its own position. + */ +static int +base_ec_position(PlannerInfo *root, EquivalenceClass *ec) +{ + /* The passed ec might be non-canonical, so chase up to the top */ + while (ec->ec_merged) + ec = ec->ec_merged; + + /* ec_index is final once EC merging is done */ + Assert(list_nth(root->eq_classes, ec->ec_index) == ec); + + /* A const-only EC has no base position */ + if (bms_is_empty(ec->ec_relids)) + return -1; + + /* If ec is already a base EC, return its own position */ + if (!bms_overlap(ec->ec_relids, root->outer_join_rels)) + return ec->ec_index; + + foreach_node(EquivalenceMember, em, ec->ec_members) + { + Node *expr; + int pos; + + if (em->em_is_const || bms_is_empty(em->em_relids)) + continue; + + expr = (Node *) em->em_expr; + if (bms_overlap(em->em_relids, root->outer_join_rels)) + expr = remove_nulling_relids(expr, root->outer_join_rels, NULL); + + pos = find_ec_position(root, (Expr *) expr, ec->ec_opfamilies, + ec->ec_collation, NULL); + if (pos >= 0) + return pos; + } + + return -1; +} diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 881950e5264..4186afb7664 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -34,6 +34,7 @@ #include "parser/parse_agg.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" /* * Utility structure. A sorting procedure is needed to simplify the search @@ -1117,7 +1118,13 @@ reduce_unique_semijoins(PlannerInfo *root) static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel) { - /* We only know about baserels ... */ + /* + * If UniqueKeys have already been deduced for the rel, distinctness may + * be provable from them. + */ + if (rel->uniquekeys != NIL) + return true; + /* Otherwise we only know about baserels ... */ if (rel->reloptkind != RELOPT_BASEREL) return false; if (rel->rtekind == RTE_RELATION) @@ -1179,6 +1186,16 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list, List **extra_clauses) { /* + * UniqueKey fast path: if some key's expressions are all covered by the + * clauses, the rel is distinct for them. + */ + if (extra_clauses == NULL && + uniquekeys_match_join_clauses(root, rel, clause_list)) + return true; + + /* + * The fallback proofs below handle base relations only. + * * We could skip a couple of tests here if we assume all callers checked * rel_supports_distinctness first, but it doesn't seem worth taking any * risk for. @@ -1613,8 +1630,9 @@ innerrel_is_unique_ext(PlannerInfo *root, self_join ? &outer_exprs : NULL)) { /* - * Cache the positive result for future probes, being sure to keep it - * in the planner_cxt even if we are working in GEQO. + * Cache the positive result for future probes. Allocate it in the + * innerrel's own context, so a baserel's entry survives a GEQO cycle + * while a joinrel's dies with the rel. * * Note: one might consider trying to isolate the minimal subset of * the outerrels that proved the innerrel unique. But it's not worth @@ -1622,7 +1640,7 @@ innerrel_is_unique_ext(PlannerInfo *root, * and so we'll see the minimally sufficient outerrels before any * supersets of them anyway. */ - old_context = MemoryContextSwitchTo(root->planner_cxt); + old_context = MemoryContextSwitchTo(GetMemoryChunkContext(innerrel)); uniqueRelInfo = makeNode(UniqueRelInfo); uniqueRelInfo->outerrelids = bms_copy(outerrelids); uniqueRelInfo->self_join = self_join; @@ -1653,7 +1671,7 @@ innerrel_is_unique_ext(PlannerInfo *root, */ if (force_cache || root->assumeReplanning) { - old_context = MemoryContextSwitchTo(root->planner_cxt); + old_context = MemoryContextSwitchTo(GetMemoryChunkContext(innerrel)); innerrel->non_unique_for_rels = lappend(innerrel->non_unique_for_rels, bms_copy(outerrelids)); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index a0ff9159ae0..7ebff684072 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -2159,6 +2159,13 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, final_rel->useridiscurrent = current_rel->useridiscurrent; final_rel->fdwroutine = current_rel->fdwroutine; + /* + * LockRows/Limit/ModifyTable never duplicate rows, so the final rel is + * distinct over whatever the current rel was. Carrying the keys here + * lets a parent query reuse them. + */ + final_rel->uniquekeys = current_rel->uniquekeys; + /* * Generate paths for the final_rel. Insert all surviving paths, with * LockRows, Limit, and/or ModifyTable steps added if needed. @@ -4059,6 +4066,7 @@ create_grouping_paths(PlannerInfo *root, RelOptInfo *grouped_rel; RelOptInfo *partially_grouped_rel; AggClauseCosts agg_costs; + bool grouping_is_noop; MemSet(&agg_costs, 0, sizeof(AggClauseCosts)); get_agg_clause_costs(root, AGGSPLIT_SIMPLE, &agg_costs); @@ -4070,14 +4078,38 @@ create_grouping_paths(PlannerInfo *root, grouped_rel = make_grouping_rel(root, input_rel, target, target_parallel_safe, parse->havingQual); - /* - * Create either paths for a degenerate grouping or paths for ordinary - * grouping, as appropriate. - */ - if (is_degenerate_grouping(root)) + grouping_is_noop = uniquekeys_grouping_is_noop(root, input_rel); + + if (grouping_is_noop) + { + /* + * The input relation is provably distinct over the grouping columns + * and no aggregation work is needed, each input row forms a group all + * by itself, so grouping is a no-op: just use the input paths as they + * are, projecting the desired target. + */ + foreach_ptr(Path, path, input_rel->pathlist) + { + /* + * The input paths may already be ProjectionPaths, and we mustn't + * stack another rel's ProjectionPath atop one. Just project + * directly from the input's subpath. + */ + if (IsA(path, ProjectionPath)) + path = ((ProjectionPath *) path)->subpath; + + add_path(grouped_rel, (Path *) + create_projection_path(root, grouped_rel, path, target)); + } + } + else if (is_degenerate_grouping(root)) + { + /* Create paths for a degenerate grouping */ create_degenerate_grouping_paths(root, input_rel, grouped_rel); + } else { + /* Create paths for ordinary grouping */ int flags = 0; GroupPathExtraData extra; @@ -4150,6 +4182,11 @@ create_grouping_paths(PlannerInfo *root, } set_cheapest(grouped_rel); + + /* Deduce the grouped rel's unique keys */ + populate_grouped_rel_uniquekeys(root, grouped_rel, input_rel, + grouping_is_noop); + return grouped_rel; } @@ -4826,6 +4863,12 @@ create_window_paths(PlannerInfo *root, window_rel->useridiscurrent = input_rel->useridiscurrent; window_rel->fdwroutine = input_rel->fdwroutine; + /* + * Window functions don't change the set of rows, so uniqueness carries + * over. + */ + window_rel->uniquekeys = input_rel->uniquekeys; + /* * Consider computing window functions starting from the existing * cheapest-total path (which will likely require a sort) as well as any @@ -5056,6 +5099,7 @@ create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, PathTarget *target) { RelOptInfo *distinct_rel; + bool distinct_is_noop; /* For now, do all work in the (DISTINCT, NULL) upperrel */ distinct_rel = fetch_upper_rel(root, UPPERREL_DISTINCT, NULL); @@ -5077,11 +5121,37 @@ create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, distinct_rel->useridiscurrent = input_rel->useridiscurrent; distinct_rel->fdwroutine = input_rel->fdwroutine; - /* build distinct paths based on input_rel's pathlist */ - create_final_distinct_paths(root, input_rel, distinct_rel); + distinct_is_noop = uniquekeys_distinct_is_noop(root, input_rel); - /* now build distinct paths based on input_rel's partial_pathlist */ - create_partial_distinct_paths(root, input_rel, distinct_rel, target); + if (distinct_is_noop) + { + /* + * The input relation is provably distinct over the DISTINCT columns + * already, so no de-duplication step is required: just use the input + * paths as they are, projecting the desired target. + */ + foreach_ptr(Path, path, input_rel->pathlist) + { + /* + * The input paths may already be ProjectionPaths, and we mustn't + * stack another rel's ProjectionPath atop one. Just project + * directly from the input's subpath. + */ + if (IsA(path, ProjectionPath)) + path = ((ProjectionPath *) path)->subpath; + + add_path(distinct_rel, (Path *) + create_projection_path(root, distinct_rel, path, target)); + } + } + else + { + /* build distinct paths based on input_rel's pathlist */ + create_final_distinct_paths(root, input_rel, distinct_rel); + + /* now build distinct paths based on input_rel's partial_pathlist */ + create_partial_distinct_paths(root, input_rel, distinct_rel, target); + } /* Give a helpful error if we failed to create any paths */ if (distinct_rel->pathlist == NIL) @@ -5110,6 +5180,10 @@ create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, /* Now choose the best path(s) */ set_cheapest(distinct_rel); + /* Deduce the distinct rel's unique keys */ + populate_distinct_rel_uniquekeys(root, distinct_rel, input_rel, + distinct_is_noop); + return distinct_rel; } @@ -5602,6 +5676,11 @@ create_ordered_paths(PlannerInfo *root, ordered_rel->useridiscurrent = input_rel->useridiscurrent; ordered_rel->fdwroutine = input_rel->fdwroutine; + /* + * Sorting doesn't change the set of rows, so uniqueness carries over. + */ + ordered_rel->uniquekeys = input_rel->uniquekeys; + foreach(lc, input_rel->pathlist) { Path *input_path = (Path *) lfirst(lc); @@ -6905,6 +6984,9 @@ adjust_paths_for_srfs(PlannerInfo *root, RelOptInfo *rel, if (list_length(targets) == 1) return; + /* The ProjectSet steps added below duplicate rows, voiding any keys */ + rel->uniquekeys = NIL; + /* * Stack SRF-evaluation nodes atop each path for the rel. * @@ -8668,6 +8750,7 @@ create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo) List *sortPathkeys = NIL; List *groupClause = NIL; MemoryContext oldcontext; + bool unique_is_noop; /* Caller made a mistake if SpecialJoinInfo is the wrong one */ Assert(sjinfo->jointype == JOIN_SEMI); @@ -8715,6 +8798,11 @@ create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo) unique_rel->cheapest_total_path = NULL; unique_rel->cheapest_parameterized_paths = NIL; + /* + * clear unique keys + */ + unique_rel->uniquekeys = NIL; + /* * Build the target list for the unique rel. We also build the pathkeys * that represent the ordering requirements for the sort-based @@ -8899,17 +8987,51 @@ create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo) unique_rel->reltarget = create_pathtarget(root, newtlist); } - /* build unique paths based on input rel's pathlist */ - create_final_unique_paths(root, rel, sortPathkeys, groupClause, - sjinfo, unique_rel); + unique_is_noop = uniquekeys_uniquification_is_noop(root, rel, sjinfo); + + if (unique_is_noop) + { + /* + * The input relation is already provably distinct over the columns to + * be unique-ified, so no de-duplication step is required: just use + * the input paths as they are, projecting the new target. + */ + Path *cheapest_input_path = rel->cheapest_total_path; + + unique_rel->rows = rel->rows; - /* build unique paths based on input rel's partial_pathlist */ - create_partial_unique_paths(root, rel, sortPathkeys, groupClause, - sjinfo, unique_rel); + foreach_ptr(Path, input_path, rel->pathlist) + { + /* + * Ignore parameterized paths that are not the cheapest-total + * path, as in create_final_unique_paths. + */ + if (input_path->param_info && + input_path != cheapest_input_path) + continue; + + add_path(unique_rel, (Path *) + create_projection_path(root, unique_rel, input_path, + unique_rel->reltarget)); + } + } + else + { + /* build unique paths based on input rel's pathlist */ + create_final_unique_paths(root, rel, sortPathkeys, groupClause, + sjinfo, unique_rel); + + /* build unique paths based on input rel's partial_pathlist */ + create_partial_unique_paths(root, rel, sortPathkeys, groupClause, + sjinfo, unique_rel); + } /* Now choose the best path(s) */ set_cheapest(unique_rel); + populate_unique_rel_uniquekeys(root, unique_rel, rel, sjinfo, + unique_is_noop); + /* * There shouldn't be any partial paths for the unique relation; * otherwise, we won't be able to properly guarantee uniqueness. diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 687e923c46c..8342c17a1a0 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -289,6 +289,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) rel->fdw_private = NULL; rel->unique_for_rels = NIL; rel->non_unique_for_rels = NIL; + rel->uniquekeys = NIL; rel->unique_rel = NULL; rel->unique_pathkeys = NIL; rel->unique_groupclause = NIL; @@ -878,6 +879,7 @@ build_join_rel(PlannerInfo *root, joinrel->fdw_private = NULL; joinrel->unique_for_rels = NIL; joinrel->non_unique_for_rels = NIL; + joinrel->uniquekeys = NIL; joinrel->unique_rel = NULL; joinrel->unique_pathkeys = NIL; joinrel->unique_groupclause = NIL; @@ -988,6 +990,9 @@ build_join_rel(PlannerInfo *root, build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo, restrictlist); + populate_joinrel_uniquekeys(root, joinrel, outer_rel, inner_rel, + sjinfo, restrictlist); + /* Add the joinrel to the PlannerInfo. */ add_join_rel(root, joinrel); @@ -1077,6 +1082,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, joinrel->useridiscurrent = false; joinrel->fdwroutine = NULL; joinrel->fdw_private = NULL; + joinrel->uniquekeys = NIL; joinrel->unique_rel = NULL; joinrel->unique_pathkeys = NIL; joinrel->unique_groupclause = NIL; diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index d9650315016..2d211702414 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -534,6 +534,14 @@ struct PlannerInfo /* set operator pathkeys, if any */ List *setop_pathkeys; + /* + * Indexes in PlannerInfo's eq_classes list of ECs that are worth tracking + * in UniqueKeys. + */ + Bitmapset *interesting_unique_ecs; + /* true once interesting_unique_ecs has been computed */ + bool interesting_unique_ecs_valid; + /* Canonicalised partition schemes used in the query. */ List *part_schemes pg_node_attr(read_write_ignore); @@ -875,6 +883,10 @@ typedef struct PartitionSchemeData *PartitionScheme; * other rels for which we have tried and failed to prove * this one unique * + * In addition, uniquekeys is a list of UniqueKey nodes describing expression + * sets over which this relation is known to be distinct. It is populated + * bottom-up, for base rels and join rels alike; see uniquekeys.c. + * * Three fields are used to cache information about unique-ification of this * relation. This is used to support semijoins where the relation appears on * the RHS: the relation is first unique-ified, and then a regular join is @@ -1125,6 +1137,9 @@ typedef struct RelOptInfo /* known not unique for these set(s) */ List *non_unique_for_rels; + /* list of UniqueKey: expression sets this rel is distinct over */ + List *uniquekeys; + /* * information about unique-ification of this relation */ @@ -1674,6 +1689,7 @@ typedef struct EquivalenceClass Index ec_sortref; /* originating sortclause label, or 0 */ Index ec_min_security; /* minimum security_level in ec_sources */ Index ec_max_security; /* maximum security_level in ec_sources */ + int ec_index; /* position of this EC in root->eq_classes */ struct EquivalenceClass *ec_merged; /* set if merged into another EC */ } EquivalenceClass; @@ -1816,6 +1832,32 @@ typedef struct PathKey bool pk_nulls_first; /* do NULLs come before normal values? */ } PathKey; +/* + * UniqueKeys + * + * Asserts that a relation cannot emit two rows that are equal on the + * values of one expression from each of the EquivalenceClasses identified + * by eclass_indexes (indexes into root->eq_classes). An empty set means + * the relation emits at most one row. + * + * If "null_aware" is true, the proof holds even treating NULLs as equal, + * so the key can justify removing a DISTINCT or GROUP BY step. If it is + * false, uniqueness is only guaranteed among rows whose key expressions + * are all non-NULL, which still suffices for proofs based on strict join + * clauses, such as inner_unique detection. + */ +typedef struct UniqueKey +{ + pg_node_attr(no_copy_equal, no_read, no_query_jumble) + + NodeTag type; + + /* indexes into root->eq_classes of the key's expressions */ + Bitmapset *eclass_indexes; + /* does uniqueness hold even treating NULLs as equal? */ + bool null_aware; +} UniqueKey; + /* * Contains an order of group-by clauses and the corresponding list of * pathkeys. diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 17f2099ec3b..11a577772cc 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -74,6 +74,8 @@ extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel, Path *bitmapqual); extern void generate_partitionwise_join_paths(PlannerInfo *root, RelOptInfo *rel); +extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, + List *live_childrels); /* * indxpath.c @@ -245,6 +247,7 @@ extern List *build_expression_pathkey(PlannerInfo *root, Expr *expr, extern List *convert_subquery_pathkeys(PlannerInfo *root, RelOptInfo *rel, List *subquery_pathkeys, List *subquery_tlist); +extern Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle); extern List *build_join_pathkeys(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype, @@ -283,7 +286,38 @@ extern List *append_pathkeys(List *target, List *source); extern PathKey *make_canonical_pathkey(PlannerInfo *root, EquivalenceClass *eclass, Oid opfamily, CompareType cmptype, bool nulls_first); -extern void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, - List *live_childrels); + +/* + * uniquekeys.c + * routines to deduce what expression sets a relation is distinct over + */ +extern void populate_baserel_uniquekeys(PlannerInfo *root, RelOptInfo *rel); +extern void populate_joinrel_uniquekeys(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + SpecialJoinInfo *sjinfo, + List *restrictlist); +extern void populate_grouped_rel_uniquekeys(PlannerInfo *root, + RelOptInfo *grouped_rel, + RelOptInfo *input_rel, + bool is_noop); +extern void populate_distinct_rel_uniquekeys(PlannerInfo *root, + RelOptInfo *distinct_rel, + RelOptInfo *input_rel, + bool is_noop); +extern void populate_unique_rel_uniquekeys(PlannerInfo *root, + RelOptInfo *unique_rel, + RelOptInfo *input_rel, + SpecialJoinInfo *sjinfo, + bool is_noop); +extern bool uniquekeys_distinct_is_noop(PlannerInfo *root, + RelOptInfo *input_rel); +extern bool uniquekeys_grouping_is_noop(PlannerInfo *root, + RelOptInfo *input_rel); +extern bool uniquekeys_match_join_clauses(PlannerInfo *root, RelOptInfo *rel, + List *clause_list); +extern bool uniquekeys_uniquification_is_noop(PlannerInfo *root, + RelOptInfo *rel, + SpecialJoinInfo *sjinfo); #endif /* PATHS_H */ diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index 7d07619956f..0743f9dc160 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -1262,15 +1262,13 @@ explain (costs off) select distinct max(unique2) from tenk1; QUERY PLAN --------------------------------------------------------------------- - HashAggregate - Group Key: (InitPlan minmax_1).col1 + Result + Replaces: MinMaxAggregate InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique2 on tenk1 Index Cond: (unique2 IS NOT NULL) - -> Result - Replaces: MinMaxAggregate -(8 rows) +(6 rows) select distinct max(unique2) from tenk1; max @@ -1453,7 +1451,8 @@ explain (costs off) select distinct min(f1), max(f1) from minmaxtest; QUERY PLAN --------------------------------------------------------------------------------------------- - Unique + Result + Replaces: MinMaxAggregate InitPlan minmax_1 -> Limit -> Merge Append @@ -1476,11 +1475,7 @@ explain (costs off) -> Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8 Index Cond: (f1 IS NOT NULL) -> Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9 - -> Sort - Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_2).col1) - -> Result - Replaces: MinMaxAggregate -(27 rows) +(24 rows) select distinct min(f1), max(f1) from minmaxtest; min | max @@ -1503,15 +1498,13 @@ explain (costs off) --------------------------------------------------------------------- Seq Scan on int4_tbl t0 SubPlan expr_1 - -> HashAggregate - Group Key: (InitPlan minmax_1).col1 + -> Result + Replaces: MinMaxAggregate InitPlan minmax_1 -> Limit -> Seq Scan on int4_tbl t1 Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1)) - -> Result - Replaces: MinMaxAggregate -(10 rows) +(8 rows) select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1) from int4_tbl t0; @@ -1547,7 +1540,7 @@ create temp table t1 (a int, b int, c int, d int, primary key (a, b)); create temp table t2 (x int, y int, z int, primary key (x, y)); create temp table t3 (a int, b int, c int, primary key(a, b) deferrable); -- Non-primary-key columns can be removed from GROUP BY -explain (costs off) select * from t1 group by a,b,c,d; +explain (costs off) select count(*) from t1 group by a,b,c,d; QUERY PLAN ---------------------- HashAggregate @@ -1565,7 +1558,7 @@ explain (costs off) select a,c from t1 group by a,c,d; (3 rows) -- Test removal across multiple relations -explain (costs off) select * +explain (costs off) select count(*) from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z; QUERY PLAN @@ -1580,7 +1573,7 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z; (7 rows) -- Test case where t1 can be optimized but not t2 -explain (costs off) select t1.*,t2.x,t2.z +explain (costs off) select t1.*,t2.x,t2.z,count(*) from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z; QUERY PLAN @@ -1605,7 +1598,7 @@ explain (costs off) select * from t3 group by a,b,c; create temp table t1c () inherits (t1); -- Ensure we don't remove any columns when t1 has a child table -explain (costs off) select * from t1 group by a,b,c,d; +explain (costs off) select count(*) from t1 group by a,b,c,d; QUERY PLAN ------------------------------------- HashAggregate @@ -1616,7 +1609,7 @@ explain (costs off) select * from t1 group by a,b,c,d; (5 rows) -- Okay to remove columns if we're only querying the parent. -explain (costs off) select * from only t1 group by a,b,c,d; +explain (costs off) select count(*) from only t1 group by a,b,c,d; QUERY PLAN ---------------------- HashAggregate @@ -1634,7 +1627,7 @@ create temp table p_t1 ( create temp table p_t1_1 partition of p_t1 for values in(1); create temp table p_t1_2 partition of p_t1 for values in(2); -- Ensure we can remove non-PK columns for partitioned tables. -explain (costs off) select * from p_t1 group by a,b,c,d; +explain (costs off) select count(*) from p_t1 group by a,b,c,d; QUERY PLAN -------------------------------- HashAggregate @@ -1647,7 +1640,7 @@ explain (costs off) select * from p_t1 group by a,b,c,d; create unique index t2_z_uidx on t2(z); -- Ensure we don't remove any columns from the GROUP BY for a unique -- index on a NULLable column. -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; QUERY PLAN ---------------------- HashAggregate @@ -1657,7 +1650,7 @@ explain (costs off) select y,z from t2 group by y,z; -- Make the column NOT NULL and ensure we remove the redundant column alter table t2 alter column z set not null; -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; QUERY PLAN ---------------------- HashAggregate @@ -1668,7 +1661,7 @@ explain (costs off) select y,z from t2 group by y,z; -- When there are multiple supporting unique indexes and the GROUP BY contains -- columns to cover all of those, ensure we pick the index with the least -- number of columns so that we can remove more columns from the GROUP BY. -explain (costs off) select x,y,z from t2 group by x,y,z; +explain (costs off) select x,y,z,count(*) from t2 group by x,y,z; QUERY PLAN ---------------------- HashAggregate @@ -1678,7 +1671,7 @@ explain (costs off) select x,y,z from t2 group by x,y,z; -- As above but try ordering the columns differently to ensure we get the -- same result. -explain (costs off) select x,y,z from t2 group by z,x,y; +explain (costs off) select x,y,z,count(*) from t2 group by z,x,y; QUERY PLAN ---------------------- HashAggregate @@ -1689,7 +1682,7 @@ explain (costs off) select x,y,z from t2 group by z,x,y; -- Ensure we don't use a partial index as proof of functional dependency drop index t2_z_uidx; create index t2_z_uidx on t2 (z) where z > 0; -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; QUERY PLAN ---------------------- HashAggregate @@ -1703,7 +1696,7 @@ explain (costs off) select y,z from t2 group by y,z; drop index t2_z_uidx; alter table t2 alter column z drop not null; create unique index t2_z_uidx on t2(z) nulls not distinct; -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; QUERY PLAN ---------------------- HashAggregate diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 05f359d3aa7..f576b97ff41 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6419,17 +6419,15 @@ select d.* from d left join (select distinct * from b) s explain (costs off) select d.* from d left join (select * from b group by b.id, b.c_id) s on d.a = s.id; - QUERY PLAN ------------------------------------------- - Merge Right Join - Merge Cond: (b.id = d.a) - -> Group - Group Key: b.id - -> Index Scan using b_pkey on b - -> Sort - Sort Key: d.a - -> Seq Scan on d -(8 rows) + QUERY PLAN +--------------------------------- + Hash Left Join + Hash Cond: (d.a = s.id) + -> Seq Scan on d + -> Hash + -> Subquery Scan on s + -> Seq Scan on b +(6 rows) -- join removal is not possible when the GROUP BY contains non-empty grouping -- sets or multiple empty grouping sets @@ -6484,18 +6482,15 @@ select d.* from d left join (select 1 as x from b group by grouping sets((), gro explain (costs off) select d.* from d left join (select distinct * from b) s on d.a = s.id; - QUERY PLAN --------------------------------------- - Merge Right Join - Merge Cond: (b.id = d.a) - -> Unique - -> Sort - Sort Key: b.id, b.c_id + QUERY PLAN +--------------------------------- + Hash Left Join + Hash Cond: (d.a = s.id) + -> Seq Scan on d + -> Hash + -> Subquery Scan on s -> Seq Scan on b - -> Sort - Sort Key: d.a - -> Seq Scan on d -(9 rows) +(6 rows) -- join removal is not possible here explain (costs off) diff --git a/src/test/regress/expected/uniquekeys.out b/src/test/regress/expected/uniquekeys.out new file mode 100644 index 00000000000..fc6eb24fa88 --- /dev/null +++ b/src/test/regress/expected/uniquekeys.out @@ -0,0 +1,710 @@ +-- +-- UNIQUEKEYS +-- +-- Tests for the planner's inference of the expression sets a relation is +-- distinct over. UniqueKeys are not printed anywhere, so each test observes +-- them through one of the consumers that acts on them: +-- +-- * a redundant DISTINCT step is dropped +-- * a redundant GROUP BY step is dropped +-- * a join is marked "Inner Unique: true" +-- * a semijoin's RHS is not unique-ified +-- +-- one-column and two-column primary keys +create table uk_p (id int primary key, v int); +create table uk_q (id int primary key, v int); +create table uk_r (id int primary key, v int); +create table uk_pk2 (a int, b int, c int, primary key (a, b)); +-- a unique column that admits NULLs, and one that does not +create table uk_nul (u int unique, w int); +create table uk_nnd (u int, w int); +create unique index uk_nnd_u on uk_nnd (u) nulls not distinct; +-- text, whose '=' belongs to both text_ops and text_pattern_ops +create table uk_txt (t text primary key, v int); +-- indexes that cannot prove anything +create table uk_defer (id int, primary key (id) deferrable); +create table uk_part (a int, b int); +create unique index uk_part_a on uk_part (a) where b > 0; +-- +-- Base relation keys +-- +-- the whole primary key +explain (costs off) select distinct a, b from uk_pk2; + QUERY PLAN +-------------------- + Seq Scan on uk_pk2 +(1 row) + +-- a superset of it +explain (costs off) select distinct a, b, c from uk_pk2; + QUERY PLAN +-------------------- + Seq Scan on uk_pk2 +(1 row) + +-- a subset is not enough +explain (costs off) select distinct a from uk_pk2; + QUERY PLAN +-------------------------- + HashAggregate + Group Key: a + -> Seq Scan on uk_pk2 +(3 rows) + +-- a key column equated to a constant drops out of the key +explain (costs off) select distinct b from uk_pk2 where a = 5; + QUERY PLAN +---------------------------------------- + Bitmap Heap Scan on uk_pk2 + Recheck Cond: (a = 5) + -> Bitmap Index Scan on uk_pk2_pkey + Index Cond: (a = 5) +(4 rows) + +-- the key of a text column, whose '=' belongs to more than one opfamily +explain (costs off) select distinct t from uk_txt; + QUERY PLAN +-------------------- + Seq Scan on uk_txt +(1 row) + +-- a deferrable constraint is not enforced row by row +explain (costs off) select distinct id from uk_defer; + QUERY PLAN +---------------------------- + HashAggregate + Group Key: id + -> Seq Scan on uk_defer +(3 rows) + +-- a partial index does not cover the whole relation +explain (costs off) select distinct a from uk_part where b > 0; + QUERY PLAN +-------------------------------------------- + HashAggregate + Group Key: a + -> Bitmap Heap Scan on uk_part + Recheck Cond: (b > 0) + -> Bitmap Index Scan on uk_part_a +(5 rows) + +-- +-- NULL-awareness of base relation keys +-- +-- two NULLs are distinct to a unique index but equal to DISTINCT, so a +-- nullable unique column is not a NULL-aware key on its own +explain (costs off) select distinct u from uk_nul; + QUERY PLAN +-------------------------- + HashAggregate + Group Key: u + -> Seq Scan on uk_nul +(3 rows) + +-- a strict restriction clause rules the NULLs out +explain (costs off) select distinct u from uk_nul where u > 0; + QUERY PLAN +----------------------------------------- + Bitmap Heap Scan on uk_nul + Recheck Cond: (u > 0) + -> Bitmap Index Scan on uk_nul_u_key + Index Cond: (u > 0) +(4 rows) + +-- so does declaring the index NULLS NOT DISTINCT +explain (costs off) select distinct u from uk_nnd; + QUERY PLAN +-------------------- + Seq Scan on uk_nnd +(1 row) + +-- +-- Subquery-in-FROM keys +-- +-- a DISTINCT inside is translated into the outer query +explain (costs off) select distinct a, b from (select distinct a, b from uk_pk2) s; + QUERY PLAN +-------------------- + Seq Scan on uk_pk2 +(1 row) + +-- likewise a GROUP BY +explain (costs off) select distinct a, b from (select a, b from uk_pk2 group by a, b) s; + QUERY PLAN +-------------------- + Seq Scan on uk_pk2 +(1 row) + +-- a plain aggregate emits a single row +explain (costs off) select distinct m from (select max(a) as m from uk_pk2) s; + QUERY PLAN +-------------------------------------------------------------------- + Result + Replaces: MinMaxAggregate + InitPlan minmax_1 + -> Limit + -> Index Only Scan Backward using uk_pk2_pkey on uk_pk2 +(5 rows) + +-- a key deduced inside the subquery's own join search +explain (costs off) +select distinct x from + (select uk_p.id as x from uk_p join uk_q on uk_p.id = uk_q.id offset 0) s; + QUERY PLAN +---------------------------------- + Hash Join + Hash Cond: (uk_p.id = uk_q.id) + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q +(5 rows) + +-- sorting and LIMIT do not add rows, so the key survives them +explain (costs off) +select distinct x from (select distinct id as x from uk_p order by 1 limit 3) s; + QUERY PLAN +----------------------------------------------- + Limit + -> Index Only Scan using uk_p_pkey on uk_p +(2 rows) + +-- neither does window function evaluation +explain (costs off) +select distinct x from + (select distinct id as x, row_number() over (order by v) as rn from uk_p) s; + QUERY PLAN +------------------------------------------------------------------ + Subquery Scan on s + -> WindowAgg + Window: w1 AS (ORDER BY uk_p.v ROWS UNBOUNDED PRECEDING) + -> Sort + Sort Key: uk_p.v + -> Seq Scan on uk_p +(6 rows) + +-- but a target-list SRF multiplies rows, and voids the key +explain (costs off) +select distinct x from (select id as x, generate_series(1, 2) from uk_p) s; + QUERY PLAN +------------------------------ + HashAggregate + Group Key: uk_p.id + -> ProjectSet + -> Seq Scan on uk_p +(4 rows) + +-- +-- Set operation keys +-- +-- a non-ALL set operation is distinct over its whole output row +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 union select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; + QUERY PLAN +----------------------------------------------- + Hash Join + Hash Cond: (uk_pk2.a = uk_p.id) + -> HashAggregate + Group Key: uk_pk2.a, uk_pk2.b + -> Append + -> Seq Scan on uk_pk2 + -> Seq Scan on uk_pk2 uk_pk2_1 + -> Hash + -> Seq Scan on uk_p +(9 rows) + +-- which is not true of UNION ALL +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 union all select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; + QUERY PLAN +----------------------------------------------- + HashAggregate + Group Key: uk_pk2.a, uk_pk2.b + -> Hash Join + Hash Cond: (uk_pk2.a = uk_p.id) + -> Append + -> Seq Scan on uk_pk2 + -> Seq Scan on uk_pk2 uk_pk2_1 + -> Hash + -> Seq Scan on uk_p +(9 rows) + +-- INTERSECT and EXCEPT deduplicate too +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 intersect select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; + QUERY PLAN +----------------------------------------- + Hash Join + Hash Cond: (uk_pk2.a = uk_p.id) + -> HashSetOp Intersect + -> Seq Scan on uk_pk2 + -> Seq Scan on uk_pk2 uk_pk2_1 + -> Hash + -> Seq Scan on uk_p +(7 rows) + +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 except select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; + QUERY PLAN +----------------------------------------- + Hash Join + Hash Cond: (uk_pk2.a = uk_p.id) + -> HashSetOp Except + -> Seq Scan on uk_pk2 + -> Seq Scan on uk_pk2 uk_pk2_1 + -> Hash + -> Seq Scan on uk_p +(7 rows) + +-- +-- Join relation keys +-- +-- Preservation: uk_q is unique for the clause, so each uk_p row yields one row +explain (costs off) select distinct uk_p.id from uk_p join uk_q on uk_p.id = uk_q.id; + QUERY PLAN +---------------------------------- + Hash Join + Hash Cond: (uk_p.id = uk_q.id) + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q +(5 rows) + +-- Combination: the union of a key from each side +explain (costs off) select distinct uk_p.id, uk_q.id from uk_p cross join uk_q; + QUERY PLAN +------------------------------ + Nested Loop + -> Seq Scan on uk_p + -> Materialize + -> Seq Scan on uk_q +(4 rows) + +-- one side's key alone is not a key of a cross join +explain (costs off) select distinct uk_p.id from uk_p cross join uk_q; + QUERY PLAN +----------------------------------------------------- + Unique + -> Nested Loop + -> Index Only Scan using uk_p_pkey on uk_p + -> Materialize + -> Seq Scan on uk_q +(5 rows) + +-- a left join preserves the LHS key +explain (costs off) +select distinct uk_p.id, uk_q.v from uk_p left join uk_q on uk_p.id = uk_q.id; + QUERY PLAN +---------------------------------- + Hash Left Join + Hash Cond: (uk_p.id = uk_q.id) + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q +(5 rows) + +-- but the RHS key loses its NULL-awareness across one +explain (costs off) +select distinct uk_q.id from uk_p left join uk_q on uk_p.id = uk_q.id; + QUERY PLAN +---------------------------------------- + HashAggregate + Group Key: uk_q.id + -> Hash Left Join + Hash Cond: (uk_p.id = uk_q.id) + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q +(7 rows) + +-- a semijoin's output is a subset of its LHS +explain (costs off) +select distinct a, b from uk_pk2 where exists (select 1 from uk_p where uk_p.id = uk_pk2.c); + QUERY PLAN +----------------------------------- + Hash Join + Hash Cond: (uk_pk2.c = uk_p.id) + -> Seq Scan on uk_pk2 + -> Hash + -> Seq Scan on uk_p +(5 rows) + +-- and so is an antijoin's +explain (costs off) +select distinct a, b from uk_pk2 where not exists (select 1 from uk_p where uk_p.id = uk_pk2.c); + QUERY PLAN +----------------------------------- + Hash Anti Join + Hash Cond: (uk_pk2.c = uk_p.id) + -> Seq Scan on uk_pk2 + -> Hash + -> Seq Scan on uk_p +(5 rows) + +-- a full join null-extends both sides, so nothing survives +explain (costs off) +select distinct uk_p.id from uk_p full join uk_q on uk_p.id = uk_q.id; + QUERY PLAN +---------------------------------------- + HashAggregate + Group Key: uk_p.id + -> Hash Full Join + Hash Cond: (uk_p.id = uk_q.id) + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q +(7 rows) + +-- an inner join's strict clause restores NULL-awareness to a key +explain (costs off) select distinct u from uk_nul join uk_p on uk_nul.u = uk_p.id; + QUERY PLAN +----------------------------------- + Hash Join + Hash Cond: (uk_nul.u = uk_p.id) + -> Seq Scan on uk_nul + -> Hash + -> Seq Scan on uk_p +(5 rows) + +-- but only when the clause covers the key column +explain (costs off) select distinct u from uk_nul join uk_p on uk_nul.w = uk_p.id; + QUERY PLAN +----------------------------------------- + HashAggregate + Group Key: uk_nul.u + -> Hash Join + Hash Cond: (uk_nul.w = uk_p.id) + -> Seq Scan on uk_nul + -> Hash + -> Seq Scan on uk_p +(7 rows) + +-- a key survives stacked outer joins +explain (costs off) +select distinct uk_p.id, y.v from uk_p + left join uk_q x on uk_p.id = x.id left join uk_r y on x.id = y.id; + QUERY PLAN +-------------------------------------- + Hash Left Join + Hash Cond: (x.id = y.id) + -> Hash Left Join + Hash Cond: (uk_p.id = x.id) + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q x + -> Hash + -> Seq Scan on uk_r y +(9 rows) + +-- +-- Upper relation keys +-- +-- grouping by a superset of a key, with no aggregates, is a no-op +explain (costs off) select a, b from uk_pk2 group by a, b, c; + QUERY PLAN +-------------------- + Seq Scan on uk_pk2 +(1 row) + +-- grouping by a non-key set is not +explain (costs off) select a, c from uk_pk2 group by a, c; + QUERY PLAN +-------------------------- + HashAggregate + Group Key: a, c + -> Seq Scan on uk_pk2 +(3 rows) + +-- nor is it when there is aggregation to do +explain (costs off) select a, b, count(*) from uk_pk2 group by a, b; + QUERY PLAN +-------------------------- + HashAggregate + Group Key: a, b + -> Seq Scan on uk_pk2 +(3 rows) + +-- grouping sets emit one row per set +explain (costs off) select a, b from uk_pk2 group by grouping sets ((a, b), (a)); + QUERY PLAN +-------------------------- + HashAggregate + Hash Key: a, b + Hash Key: a + -> Seq Scan on uk_pk2 +(4 rows) + +-- +-- Consumers other than DISTINCT +-- +-- inner_unique proven from a joinrel's key rather than a base relation's +explain (verbose, costs off) +select uk_p.v from uk_p left join (uk_q join uk_r on uk_q.id = uk_r.id) on uk_p.v = uk_q.id; + QUERY PLAN +------------------------------------------------- + Hash Left Join + Output: uk_p.v + Inner Unique: true + Hash Cond: (uk_p.v = uk_q.id) + -> Seq Scan on public.uk_p + Output: uk_p.id, uk_p.v + -> Hash + Output: uk_q.id + -> Hash Join + Output: uk_q.id + Inner Unique: true + Hash Cond: (uk_q.id = uk_r.id) + -> Seq Scan on public.uk_q + Output: uk_q.id, uk_q.v + -> Hash + Output: uk_r.id + -> Seq Scan on public.uk_r + Output: uk_r.id +(18 rows) + +-- the RHS of this semijoin is already distinct, so it is not unique-ified +explain (costs off) +select * from uk_p, uk_q where (uk_p.id, uk_q.id) in (select a, b from uk_pk2); + QUERY PLAN +----------------------------------------- + Hash Join + Hash Cond: (uk_pk2.b = uk_q.id) + -> Hash Join + Hash Cond: (uk_pk2.a = uk_p.id) + -> Seq Scan on uk_pk2 + -> Hash + -> Seq Scan on uk_p + -> Hash + -> Seq Scan on uk_q +(9 rows) + +-- whereas this one is not, so it is +explain (costs off) +select * from uk_p, uk_q where (uk_p.id, uk_q.id) in (select c, c from uk_pk2); + QUERY PLAN +-------------------------------------------- + Hash Join + Hash Cond: (uk_p.id = uk_q.id) + -> Hash Join + Hash Cond: (uk_p.id = uk_pk2.c) + -> Seq Scan on uk_p + -> Hash + -> HashAggregate + Group Key: uk_pk2.c + -> Seq Scan on uk_pk2 + -> Hash + -> Seq Scan on uk_q +(11 rows) + +-- +-- Result correctness: steps that must not be removed +-- +-- Cases where an over-strong key would drop a step that is really needed, and +-- so return duplicate rows. +-- +create table uk_d1 (u int unique, w int); +create table uk_d2 (id int primary key, v int); +insert into uk_d1 values (null, 1), (null, 1), (1, 2); +insert into uk_d2 values (1, 10), (2, 20); +-- two NULL rows are distinct to the unique index but equal to DISTINCT +explain (costs off) select distinct u, w from uk_d1; + QUERY PLAN +------------------------- + HashAggregate + Group Key: u, w + -> Seq Scan on uk_d1 +(3 rows) + +select distinct u, w from uk_d1 order by u, w; + u | w +---+--- + 1 | 2 + | 1 +(2 rows) + +-- across a left join the RHS key covers only the matched rows +explain (costs off) select distinct uk_d1.u from uk_d2 left join uk_d1 on uk_d2.id = uk_d1.u; + QUERY PLAN +----------------------------------------- + HashAggregate + Group Key: uk_d1.u + -> Hash Left Join + Hash Cond: (uk_d2.id = uk_d1.u) + -> Seq Scan on uk_d2 + -> Hash + -> Seq Scan on uk_d1 +(7 rows) + +select distinct uk_d1.u from uk_d2 left join uk_d1 on uk_d2.id = uk_d1.u order by 1; + u +--- + 1 + +(2 rows) + +-- a target-list SRF duplicates the subquery's rows +explain (costs off) +select distinct x from (select id as x, generate_series(1, 2) from uk_d2) s; + QUERY PLAN +------------------------------- + HashAggregate + Group Key: uk_d2.id + -> ProjectSet + -> Seq Scan on uk_d2 +(4 rows) + +select distinct x from (select id as x, generate_series(1, 2) from uk_d2) s order by 1; + x +--- + 1 + 2 +(2 rows) + +-- UNION ALL does not deduplicate +explain (costs off) +select distinct s.id from (select id from uk_d2 union all select id from uk_d2) s; + QUERY PLAN +--------------------------------------- + HashAggregate + Group Key: uk_d2.id + -> Append + -> Seq Scan on uk_d2 + -> Seq Scan on uk_d2 uk_d2_1 +(5 rows) + +select distinct s.id from (select id from uk_d2 union all select id from uk_d2) s order by 1; + id +---- + 1 + 2 +(2 rows) + +-- +-- Result correctness: steps that are removed +-- +-- Removing a step replaces it with a bare projection, so these check that the +-- rewritten paths still emit the right rows and compute the right values. +-- +create table uk_e1 (a int, b int, c int, primary key (a, b)); +create table uk_e2 (id int primary key, v int); +create table uk_e3 (x int, y int); +insert into uk_e1 values (1, 1, 10), (1, 2, 20), (2, 1, 30); +insert into uk_e2 values (1, 1), (2, 1), (3, 300); +insert into uk_e3 values (1, 1), (1, 1), (2, 1), (9, 9); +-- DISTINCT over a superset of the key, projecting a computed column +explain (costs off) select distinct a, b, c * 10 from uk_e1; + QUERY PLAN +------------------- + Seq Scan on uk_e1 +(1 row) + +select distinct a, b, c * 10 from uk_e1 order by a, b; + a | b | ?column? +---+---+---------- + 1 | 1 | 100 + 1 | 2 | 200 + 2 | 1 | 300 +(3 rows) + +-- the remaining key columns still determine the row +explain (costs off) select distinct b, c from uk_e1 where a = 1; + QUERY PLAN +--------------------------------------- + Bitmap Heap Scan on uk_e1 + Recheck Cond: (a = 1) + -> Bitmap Index Scan on uk_e1_pkey + Index Cond: (a = 1) +(4 rows) + +select distinct b, c from uk_e1 where a = 1 order by b; + b | c +---+---- + 1 | 10 + 2 | 20 +(2 rows) + +-- the same, for a grouping step that groups nothing +explain (costs off) select a, b, c * 10 from uk_e1 group by a, b, c; + QUERY PLAN +------------------- + Seq Scan on uk_e1 +(1 row) + +select a, b, c * 10 from uk_e1 group by a, b, c order by a, b; + a | b | ?column? +---+---+---------- + 1 | 1 | 100 + 1 | 2 | 200 + 2 | 1 | 300 +(3 rows) + +-- DISTINCT removed above a join, which must still not collapse uk_e2.v +explain (costs off) +select distinct uk_e1.a, uk_e1.b, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a; + QUERY PLAN +----------------------------------- + Hash Join + Hash Cond: (uk_e1.a = uk_e2.id) + -> Seq Scan on uk_e1 + -> Hash + -> Seq Scan on uk_e2 +(5 rows) + +select distinct uk_e1.a, uk_e1.b, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a + order by 1, 2; + a | b | v +---+---+--- + 1 | 1 | 1 + 1 | 2 | 1 + 2 | 1 | 1 +(3 rows) + +-- unique-ification skipped, but the semijoin must still keep both (1,1) rows +explain (costs off) select * from uk_e3 where (x, y) in (select a, b from uk_e1); + QUERY PLAN +------------------------------------------------------------ + Hash Join + Hash Cond: ((uk_e3.x = uk_e1.a) AND (uk_e3.y = uk_e1.b)) + -> Seq Scan on uk_e3 + -> Hash + -> Seq Scan on uk_e1 +(5 rows) + +select * from uk_e3 where (x, y) in (select a, b from uk_e1) order by 1, 2; + x | y +---+--- + 1 | 1 + 1 | 1 + 2 | 1 +(3 rows) + +-- a join marked inner-unique must not stop short of any matching row +explain (verbose, costs off) +select uk_e2.id, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a and uk_e2.v = uk_e1.b; + QUERY PLAN +------------------------------------------------------------- + Hash Join + Output: uk_e2.id, uk_e2.v + Inner Unique: true + Hash Cond: ((uk_e2.id = uk_e1.a) AND (uk_e2.v = uk_e1.b)) + -> Seq Scan on public.uk_e2 + Output: uk_e2.id, uk_e2.v + -> Hash + Output: uk_e1.a, uk_e1.b + -> Seq Scan on public.uk_e1 + Output: uk_e1.a, uk_e1.b +(10 rows) + +select uk_e2.id, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a and uk_e2.v = uk_e1.b + order by 1; + id | v +----+--- + 1 | 1 + 2 | 1 +(2 rows) + +drop table uk_p, uk_q, uk_r, uk_pk2, uk_nul, uk_nnd, uk_txt; +drop table uk_defer, uk_part, uk_d1, uk_d2, uk_e1, uk_e2, uk_e3; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..09c56d65fc4 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other # psql depends on create_am # amutils depends on geometry, create_index_spgist, hash_index, brin -test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 +test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 uniquekeys # ---------- # Run these alone so they don't run out of parallel workers diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 91f8342166f..733eaba78af 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -529,18 +529,18 @@ create temp table t2 (x int, y int, z int, primary key (x, y)); create temp table t3 (a int, b int, c int, primary key(a, b) deferrable); -- Non-primary-key columns can be removed from GROUP BY -explain (costs off) select * from t1 group by a,b,c,d; +explain (costs off) select count(*) from t1 group by a,b,c,d; -- No removal can happen if the complete PK is not present in GROUP BY explain (costs off) select a,c from t1 group by a,c,d; -- Test removal across multiple relations -explain (costs off) select * +explain (costs off) select count(*) from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z; -- Test case where t1 can be optimized but not t2 -explain (costs off) select t1.*,t2.x,t2.z +explain (costs off) select t1.*,t2.x,t2.z,count(*) from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z; @@ -550,10 +550,10 @@ explain (costs off) select * from t3 group by a,b,c; create temp table t1c () inherits (t1); -- Ensure we don't remove any columns when t1 has a child table -explain (costs off) select * from t1 group by a,b,c,d; +explain (costs off) select count(*) from t1 group by a,b,c,d; -- Okay to remove columns if we're only querying the parent. -explain (costs off) select * from only t1 group by a,b,c,d; +explain (costs off) select count(*) from only t1 group by a,b,c,d; create temp table p_t1 ( a int, @@ -566,31 +566,31 @@ create temp table p_t1_1 partition of p_t1 for values in(1); create temp table p_t1_2 partition of p_t1 for values in(2); -- Ensure we can remove non-PK columns for partitioned tables. -explain (costs off) select * from p_t1 group by a,b,c,d; +explain (costs off) select count(*) from p_t1 group by a,b,c,d; create unique index t2_z_uidx on t2(z); -- Ensure we don't remove any columns from the GROUP BY for a unique -- index on a NULLable column. -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; -- Make the column NOT NULL and ensure we remove the redundant column alter table t2 alter column z set not null; -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; -- When there are multiple supporting unique indexes and the GROUP BY contains -- columns to cover all of those, ensure we pick the index with the least -- number of columns so that we can remove more columns from the GROUP BY. -explain (costs off) select x,y,z from t2 group by x,y,z; +explain (costs off) select x,y,z,count(*) from t2 group by x,y,z; -- As above but try ordering the columns differently to ensure we get the -- same result. -explain (costs off) select x,y,z from t2 group by z,x,y; +explain (costs off) select x,y,z,count(*) from t2 group by z,x,y; -- Ensure we don't use a partial index as proof of functional dependency drop index t2_z_uidx; create index t2_z_uidx on t2 (z) where z > 0; -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; -- A unique index defined as NULLS NOT DISTINCT does not need a supporting NOT -- NULL constraint on the indexed columns. Ensure the redundant columns are @@ -598,7 +598,7 @@ explain (costs off) select y,z from t2 group by y,z; drop index t2_z_uidx; alter table t2 alter column z drop not null; create unique index t2_z_uidx on t2(z) nulls not distinct; -explain (costs off) select y,z from t2 group by y,z; +explain (costs off) select y,z,count(*) from t2 group by y,z; drop table t1 cascade; drop table t2; diff --git a/src/test/regress/sql/uniquekeys.sql b/src/test/regress/sql/uniquekeys.sql new file mode 100644 index 00000000000..1143af35655 --- /dev/null +++ b/src/test/regress/sql/uniquekeys.sql @@ -0,0 +1,231 @@ +-- +-- UNIQUEKEYS +-- +-- Tests for the planner's inference of the expression sets a relation is +-- distinct over. UniqueKeys are not printed anywhere, so each test observes +-- them through one of the consumers that acts on them: +-- +-- * a redundant DISTINCT step is dropped +-- * a redundant GROUP BY step is dropped +-- * a join is marked "Inner Unique: true" +-- * a semijoin's RHS is not unique-ified +-- + +-- one-column and two-column primary keys +create table uk_p (id int primary key, v int); +create table uk_q (id int primary key, v int); +create table uk_r (id int primary key, v int); +create table uk_pk2 (a int, b int, c int, primary key (a, b)); +-- a unique column that admits NULLs, and one that does not +create table uk_nul (u int unique, w int); +create table uk_nnd (u int, w int); +create unique index uk_nnd_u on uk_nnd (u) nulls not distinct; +-- text, whose '=' belongs to both text_ops and text_pattern_ops +create table uk_txt (t text primary key, v int); +-- indexes that cannot prove anything +create table uk_defer (id int, primary key (id) deferrable); +create table uk_part (a int, b int); +create unique index uk_part_a on uk_part (a) where b > 0; + +-- +-- Base relation keys +-- +-- the whole primary key +explain (costs off) select distinct a, b from uk_pk2; +-- a superset of it +explain (costs off) select distinct a, b, c from uk_pk2; +-- a subset is not enough +explain (costs off) select distinct a from uk_pk2; +-- a key column equated to a constant drops out of the key +explain (costs off) select distinct b from uk_pk2 where a = 5; +-- the key of a text column, whose '=' belongs to more than one opfamily +explain (costs off) select distinct t from uk_txt; +-- a deferrable constraint is not enforced row by row +explain (costs off) select distinct id from uk_defer; +-- a partial index does not cover the whole relation +explain (costs off) select distinct a from uk_part where b > 0; + +-- +-- NULL-awareness of base relation keys +-- +-- two NULLs are distinct to a unique index but equal to DISTINCT, so a +-- nullable unique column is not a NULL-aware key on its own +explain (costs off) select distinct u from uk_nul; +-- a strict restriction clause rules the NULLs out +explain (costs off) select distinct u from uk_nul where u > 0; +-- so does declaring the index NULLS NOT DISTINCT +explain (costs off) select distinct u from uk_nnd; + +-- +-- Subquery-in-FROM keys +-- +-- a DISTINCT inside is translated into the outer query +explain (costs off) select distinct a, b from (select distinct a, b from uk_pk2) s; +-- likewise a GROUP BY +explain (costs off) select distinct a, b from (select a, b from uk_pk2 group by a, b) s; +-- a plain aggregate emits a single row +explain (costs off) select distinct m from (select max(a) as m from uk_pk2) s; +-- a key deduced inside the subquery's own join search +explain (costs off) +select distinct x from + (select uk_p.id as x from uk_p join uk_q on uk_p.id = uk_q.id offset 0) s; +-- sorting and LIMIT do not add rows, so the key survives them +explain (costs off) +select distinct x from (select distinct id as x from uk_p order by 1 limit 3) s; +-- neither does window function evaluation +explain (costs off) +select distinct x from + (select distinct id as x, row_number() over (order by v) as rn from uk_p) s; +-- but a target-list SRF multiplies rows, and voids the key +explain (costs off) +select distinct x from (select id as x, generate_series(1, 2) from uk_p) s; + +-- +-- Set operation keys +-- +-- a non-ALL set operation is distinct over its whole output row +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 union select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; +-- which is not true of UNION ALL +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 union all select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; +-- INTERSECT and EXCEPT deduplicate too +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 intersect select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; +explain (costs off) +select distinct s.a, s.b from + (select a, b from uk_pk2 except select a, b from uk_pk2) s join uk_p on s.a = uk_p.id; + +-- +-- Join relation keys +-- +-- Preservation: uk_q is unique for the clause, so each uk_p row yields one row +explain (costs off) select distinct uk_p.id from uk_p join uk_q on uk_p.id = uk_q.id; +-- Combination: the union of a key from each side +explain (costs off) select distinct uk_p.id, uk_q.id from uk_p cross join uk_q; +-- one side's key alone is not a key of a cross join +explain (costs off) select distinct uk_p.id from uk_p cross join uk_q; +-- a left join preserves the LHS key +explain (costs off) +select distinct uk_p.id, uk_q.v from uk_p left join uk_q on uk_p.id = uk_q.id; +-- but the RHS key loses its NULL-awareness across one +explain (costs off) +select distinct uk_q.id from uk_p left join uk_q on uk_p.id = uk_q.id; +-- a semijoin's output is a subset of its LHS +explain (costs off) +select distinct a, b from uk_pk2 where exists (select 1 from uk_p where uk_p.id = uk_pk2.c); +-- and so is an antijoin's +explain (costs off) +select distinct a, b from uk_pk2 where not exists (select 1 from uk_p where uk_p.id = uk_pk2.c); +-- a full join null-extends both sides, so nothing survives +explain (costs off) +select distinct uk_p.id from uk_p full join uk_q on uk_p.id = uk_q.id; +-- an inner join's strict clause restores NULL-awareness to a key +explain (costs off) select distinct u from uk_nul join uk_p on uk_nul.u = uk_p.id; +-- but only when the clause covers the key column +explain (costs off) select distinct u from uk_nul join uk_p on uk_nul.w = uk_p.id; +-- a key survives stacked outer joins +explain (costs off) +select distinct uk_p.id, y.v from uk_p + left join uk_q x on uk_p.id = x.id left join uk_r y on x.id = y.id; + +-- +-- Upper relation keys +-- +-- grouping by a superset of a key, with no aggregates, is a no-op +explain (costs off) select a, b from uk_pk2 group by a, b, c; +-- grouping by a non-key set is not +explain (costs off) select a, c from uk_pk2 group by a, c; +-- nor is it when there is aggregation to do +explain (costs off) select a, b, count(*) from uk_pk2 group by a, b; +-- grouping sets emit one row per set +explain (costs off) select a, b from uk_pk2 group by grouping sets ((a, b), (a)); + +-- +-- Consumers other than DISTINCT +-- +-- inner_unique proven from a joinrel's key rather than a base relation's +explain (verbose, costs off) +select uk_p.v from uk_p left join (uk_q join uk_r on uk_q.id = uk_r.id) on uk_p.v = uk_q.id; +-- the RHS of this semijoin is already distinct, so it is not unique-ified +explain (costs off) +select * from uk_p, uk_q where (uk_p.id, uk_q.id) in (select a, b from uk_pk2); +-- whereas this one is not, so it is +explain (costs off) +select * from uk_p, uk_q where (uk_p.id, uk_q.id) in (select c, c from uk_pk2); + +-- +-- Result correctness: steps that must not be removed +-- +-- Cases where an over-strong key would drop a step that is really needed, and +-- so return duplicate rows. +-- +create table uk_d1 (u int unique, w int); +create table uk_d2 (id int primary key, v int); +insert into uk_d1 values (null, 1), (null, 1), (1, 2); +insert into uk_d2 values (1, 10), (2, 20); + +-- two NULL rows are distinct to the unique index but equal to DISTINCT +explain (costs off) select distinct u, w from uk_d1; +select distinct u, w from uk_d1 order by u, w; + +-- across a left join the RHS key covers only the matched rows +explain (costs off) select distinct uk_d1.u from uk_d2 left join uk_d1 on uk_d2.id = uk_d1.u; +select distinct uk_d1.u from uk_d2 left join uk_d1 on uk_d2.id = uk_d1.u order by 1; + +-- a target-list SRF duplicates the subquery's rows +explain (costs off) +select distinct x from (select id as x, generate_series(1, 2) from uk_d2) s; +select distinct x from (select id as x, generate_series(1, 2) from uk_d2) s order by 1; + +-- UNION ALL does not deduplicate +explain (costs off) +select distinct s.id from (select id from uk_d2 union all select id from uk_d2) s; +select distinct s.id from (select id from uk_d2 union all select id from uk_d2) s order by 1; + +-- +-- Result correctness: steps that are removed +-- +-- Removing a step replaces it with a bare projection, so these check that the +-- rewritten paths still emit the right rows and compute the right values. +-- +create table uk_e1 (a int, b int, c int, primary key (a, b)); +create table uk_e2 (id int primary key, v int); +create table uk_e3 (x int, y int); +insert into uk_e1 values (1, 1, 10), (1, 2, 20), (2, 1, 30); +insert into uk_e2 values (1, 1), (2, 1), (3, 300); +insert into uk_e3 values (1, 1), (1, 1), (2, 1), (9, 9); + +-- DISTINCT over a superset of the key, projecting a computed column +explain (costs off) select distinct a, b, c * 10 from uk_e1; +select distinct a, b, c * 10 from uk_e1 order by a, b; + +-- the remaining key columns still determine the row +explain (costs off) select distinct b, c from uk_e1 where a = 1; +select distinct b, c from uk_e1 where a = 1 order by b; + +-- the same, for a grouping step that groups nothing +explain (costs off) select a, b, c * 10 from uk_e1 group by a, b, c; +select a, b, c * 10 from uk_e1 group by a, b, c order by a, b; + +-- DISTINCT removed above a join, which must still not collapse uk_e2.v +explain (costs off) +select distinct uk_e1.a, uk_e1.b, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a; +select distinct uk_e1.a, uk_e1.b, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a + order by 1, 2; + +-- unique-ification skipped, but the semijoin must still keep both (1,1) rows +explain (costs off) select * from uk_e3 where (x, y) in (select a, b from uk_e1); +select * from uk_e3 where (x, y) in (select a, b from uk_e1) order by 1, 2; + +-- a join marked inner-unique must not stop short of any matching row +explain (verbose, costs off) +select uk_e2.id, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a and uk_e2.v = uk_e1.b; +select uk_e2.id, uk_e2.v from uk_e2 join uk_e1 on uk_e2.id = uk_e1.a and uk_e2.v = uk_e1.b + order by 1; + +drop table uk_p, uk_q, uk_r, uk_pk2, uk_nul, uk_nnd, uk_txt; +drop table uk_defer, uk_part, uk_d1, uk_d2, uk_e1, uk_e2, uk_e3; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ff059c2a31e..f35c5c147bc 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3336,6 +3336,7 @@ UVersionInfo UnicodeNormalizationForm UnicodeNormalizationQC Unique +UniqueKey UniquePath UniqueRelInfo UniqueState -- 2.37.1 (Apple Git-137.1)