diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml index 027361f20bb..b1b3255d616 100644 --- a/doc/src/sgml/btree.sgml +++ b/doc/src/sgml/btree.sgml @@ -464,9 +464,12 @@ returns bool equalimage (equality implies image equality) support functions, registered under support function number 4. These functions allow the core code to - determine when it is safe to apply the btree deduplication - optimization. Currently, equalimage - functions are only called when building or rebuilding an index. + determine when two values that compare equal may be freely + substituted for one another. They are called when building or + rebuilding an index, to decide whether the btree deduplication + optimization is safe, and during query planning, to decide whether + an optimization that merges equal values may discard the + distinction between them. An equalimage function must have the diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c index 014faa1622f..e525bdf1847 100644 --- a/src/backend/access/nbtree/nbtutils.c +++ b/src/backend/access/nbtree/nbtutils.c @@ -1183,21 +1183,12 @@ _bt_allequalimage(Relation rel, bool debugmessage) for (int i = 0; i < IndexRelationGetNumberOfKeyAttributes(rel); i++) { - Oid opfamily = rel->rd_opfamily[i]; - Oid opcintype = rel->rd_opcintype[i]; - Oid collation = rel->rd_indcollation[i]; - Oid equalimageproc; - - equalimageproc = get_opfamily_proc(opfamily, opcintype, opcintype, - BTEQUALIMAGE_PROC); - /* - * If there is no BTEQUALIMAGE_PROC then deduplication is assumed to - * be unsafe. Otherwise, actually call proc and see what it says. + * An opclass that lacks a BTEQUALIMAGE_PROC, or whose procedure + * returns false, makes deduplication unsafe for the whole index. */ - if (!OidIsValid(equalimageproc) || - !DatumGetBool(OidFunctionCall1Coll(equalimageproc, collation, - ObjectIdGetDatum(opcintype)))) + if (!opfamily_is_equalimage(rel->rd_opfamily[i], rel->rd_opcintype[i], + rel->rd_indcollation[i])) { allequalimage = false; break; diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index fb6f81453ea..deaee399042 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -14,7 +14,6 @@ */ #include "postgres.h" -#include "access/nbtree.h" #include "access/sysattr.h" #include "catalog/pg_constraint.h" #include "catalog/pg_type.h" @@ -885,7 +884,6 @@ create_grouping_expr_infos(PlannerInfo *root) SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); TargetEntry *tle = get_sortgroupclause_tle(sgc, root->processed_tlist); TypeCacheEntry *tce; - Oid equalimageproc; Assert(tle->ressortgroupref > 0); @@ -911,22 +909,13 @@ create_grouping_expr_infos(PlannerInfo *root) !OidIsValid(tce->btree_opintype)) return; - equalimageproc = get_opfamily_proc(tce->btree_opf, - tce->btree_opintype, - tce->btree_opintype, - BTEQUALIMAGE_PROC); - /* - * If there is no BTEQUALIMAGE_PROC, eager aggregation is assumed to - * be unsafe. Otherwise, we call the procedure to check. We must be - * careful to pass the expression's actual collation, rather than the - * data type's default collation, to ensure that non-deterministic - * collations are correctly handled. + * We must be careful to pass the expression's actual collation, rather + * than the data type's default collation, to ensure that + * non-deterministic collations are correctly handled. */ - if (!OidIsValid(equalimageproc) || - !DatumGetBool(OidFunctionCall1Coll(equalimageproc, - exprCollation((Node *) tle->expr), - ObjectIdGetDatum(tce->btree_opintype)))) + if (!opfamily_is_equalimage(tce->btree_opf, tce->btree_opintype, + exprCollation((Node *) tle->expr))) return; exprs = lappend(exprs, tle->expr); diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 55cebe4a74b..07f25501da9 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -6414,18 +6414,22 @@ pull_paramids_walker(Node *node, Bitmapset **context) * semantics compatible with the grouping eqop, or, for a nondeterministic * collation, when the comparison applies a collation other than the column's. * - * For a nondeterministic collation, every other reference is rejected: a - * comparison under a different collation, and any function or operator over - * the column, because we cannot tell whether the function yields the same - * result for values the grouping treats as equal, and many do not. A column - * with a deterministic collation is not restricted this way. + * Every other reference -- a comparison under a different collation, or any + * function or operator over the column -- is opaque to us, so we accept it + * only when the grouping's equality is image equality. Then the values the + * grouping merges are interchangeable without loss of semantic information, + * and whatever wraps the column is bound to return the same answer for all of + * them. When it is not image equality, as for numeric (1 and 1.0), jsonb, + * float8 (0 and -0), record, or text under a nondeterministic collation, no + * such reasoning is available, and many wrappers do in fact distinguish the + * values: 1.0::text is not 1::text. * - * This leaves one case uncaught: with a deterministic collation, a function - * over the column can still feed a finer comparison than the direct-operand - * check sees, for example record_image_ops over a rebuilt record, or scale() - * over numeric where two equal values differ in scale. Catching it would - * require knowing that a type's equality is bitwise, which we do not test - * here. + * Image equality is not quite bitwise equality for varlena types, because + * TOAST compression is not applied consistently on input. Expressions that + * expose physical representation rather than value, pg_column_size() for one, + * can therefore still tell apart values that the grouping merges. Those are + * outside the semantic contract that an equalimage procedure describes, and we + * make no attempt to detect them. * * Returns true if any such conflict exists. */ @@ -6477,18 +6481,23 @@ grouping_conflict_walker(Node *node, grouping_walker_ctx *ctx) if (IsA(node, Var)) { Var *var = (Var *) node; + Oid grouping_eqop = ctx->get_eqop(var, ctx->cb_context); /* * A grouping column reaches here when it was not handled as a direct - * operand by a comparison node above (see the function header). That - * is safe for a deterministic collation, but not for a - * nondeterministic one, where the reference may distinguish values - * the grouping considers equal. A bare boolean qual is safe too: - * boolean is not collatable, so it takes the deterministic path here. + * operand by a comparison node above (see the function header), so we + * know nothing about the expression it is embedded in. Accept it only + * if the grouping's equality is image equality, which makes any two + * values the grouping merges interchangeable for every expression. + * + * This subsumes the nondeterministic-collation case: the equalimage + * procedure of a collatable type is handed the column's collation and + * answers false for a nondeterministic one. A bare boolean qual takes + * this path too, and stays safe, boolean equality being image + * equality. */ - if (OidIsValid(ctx->get_eqop(var, ctx->cb_context)) && - OidIsValid(var->varcollid) && - !get_collation_isdeterministic(var->varcollid)) + if (OidIsValid(grouping_eqop) && + !equality_op_is_equalimage(grouping_eqop, var->varcollid)) return true; return false; } diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index ee69f81945f..264a5575a04 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -16,7 +16,6 @@ #include -#include "access/nbtree.h" #include "catalog/pg_constraint.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -3038,7 +3037,6 @@ init_grouping_targets(PlannerInfo *root, RelOptInfo *rel, */ SortGroupClause *sgc; TypeCacheEntry *tce; - Oid equalimageproc; /* * But first, check if equality implies image equality for this @@ -3051,22 +3049,13 @@ init_grouping_targets(PlannerInfo *root, RelOptInfo *rel, !OidIsValid(tce->btree_opintype)) return false; - equalimageproc = get_opfamily_proc(tce->btree_opf, - tce->btree_opintype, - tce->btree_opintype, - BTEQUALIMAGE_PROC); - /* - * If there is no BTEQUALIMAGE_PROC, eager aggregation is assumed - * to be unsafe. Otherwise, we call the procedure to check. We - * must be careful to pass the expression's actual collation, + * We must be careful to pass the expression's actual collation, * rather than the data type's default collation, to ensure that * non-deterministic collations are correctly handled. */ - if (!OidIsValid(equalimageproc) || - !DatumGetBool(OidFunctionCall1Coll(equalimageproc, - exprCollation((Node *) expr), - ObjectIdGetDatum(tce->btree_opintype)))) + if (!opfamily_is_equalimage(tce->btree_opf, tce->btree_opintype, + exprCollation((Node *) expr))) return false; /* Create the SortGroupClause. */ diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 9ef3922d17c..360ba470ef1 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -17,6 +17,7 @@ #include "access/hash.h" #include "access/htup_details.h" +#include "access/nbtree.h" #include "bootstrap/bootstrap.h" #include "catalog/namespace.h" #include "catalog/pg_am.h" @@ -1038,6 +1039,109 @@ get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 procnum) return result; } +/* + * opfamily_is_equalimage + * + * Does opfamily promise "equality implies image equality" for the given + * input type and collation? + * + * A true result means that whenever the opfamily's ordering function reports + * two values equal, those values are interchangeable without any loss of + * semantic information; that is, no expression can tell them apart. Callers + * rely on this when they want to substitute one member of an equivalence + * class for another, as B-tree deduplication does. + * + * An opfamily that registers no BTEQUALIMAGE_PROC makes no such promise, so + * we must assume the property does not hold. Note that callers must pass the + * collation actually in use rather than the type's default collation: for a + * collatable type the answer depends on it, since a nondeterministic + * collation is not image equality. + */ +bool +opfamily_is_equalimage(Oid opfamily, Oid opcintype, Oid collation) +{ + Oid equalimageproc; + + equalimageproc = get_opfamily_proc(opfamily, opcintype, opcintype, + BTEQUALIMAGE_PROC); + + if (!OidIsValid(equalimageproc)) + return false; + + return DatumGetBool(OidFunctionCall1Coll(equalimageproc, collation, + ObjectIdGetDatum(opcintype))); +} + +/* + * equality_op_is_equalimage + * + * Does eqop define an equivalence under which equal values are + * interchangeable without any loss of semantic information? + * + * This is the operator-level counterpart of opfamily_is_equalimage(), for + * callers that know only the equality operator some mechanism uses to decide + * which values to merge -- a SortGroupClause's eqop, typically -- and not the + * opfamily it came from. + * + * Not knowing the opfamily is why we must demand a promise from every family + * in which eqop is the equality member, rather than accepting the first "yes" + * we find. Those families need not agree: texteq is the equality member of + * both text_ops and text_pattern_ops, and under a nondeterministic collation + * they describe different equivalences, the former case-folding where the + * latter is bytewise. text_pattern_ops registers btequalimage, which answers + * true whatever collation it is handed, so trusting it alone would let a + * caller substitute values that a case-insensitive grouping merged. + * + * A false result means "not proven", not "proven false", and callers must + * treat it as "not image equality". A type with no ordering opclass at all, + * such as xid, always lands there. + * + * 'collation' must be the collation actually applied to the values, not the + * type's default; see opfamily_is_equalimage(). + */ +bool +equality_op_is_equalimage(Oid eqop, Oid collation) +{ + Oid lefttype; + Oid righttype; + List *opfamilies; + bool result; + ListCell *lc; + + op_input_types(eqop, &lefttype, &righttype); + + /* + * An equalimage procedure describes a single type, so a cross-type + * operator gives us nothing to ask about. Grouping equality operators are + * never cross-type, so this costs no optimization in practice. + */ + if (lefttype != righttype) + return false; + + /* + * Collect the opfamilies before calling any of their procedures: the + * procedure is user-supplied code that can throw, and we would rather not + * be holding a syscache list reference when it does. + */ + opfamilies = get_mergejoin_opfamilies(eqop); + + /* No ordering opfamily at all means nothing promised anything. */ + result = (opfamilies != NIL); + + foreach(lc, opfamilies) + { + if (!opfamily_is_equalimage(lfirst_oid(lc), lefttype, collation)) + { + result = false; + break; + } + } + + list_free(opfamilies); + + return result; +} + /* ---------- ATTRIBUTE CACHES ---------- */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 865980cb0f1..09887ddf093 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -95,6 +95,8 @@ extern bool collations_agree_on_equality(Oid coll1, Oid coll2); extern bool op_is_safe_index_member(Oid opno); extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 procnum); +extern bool opfamily_is_equalimage(Oid opfamily, Oid opcintype, Oid collation); +extern bool equality_op_is_equalimage(Oid eqop, Oid collation); extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok); extern AttrNumber get_attnum(Oid relid, const char *attname); extern char get_attgenerated(Oid relid, AttrNumber attnum); diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index 7d07619956f..bae145a3b88 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -1809,6 +1809,130 @@ select a, count(*) from t_having group by a having a = row(1.0)::avg_rec; drop table t_having; drop type avg_rec; +-- A HAVING clause that reaches the grouping column through a wrapper, rather +-- than as a direct operand of a comparison, must NOT be pushed down to WHERE +-- unless the grouping's equality is image equality: the wrapper can tell apart +-- values that GROUP BY merged into one group. +create temp table t_eqimg (n numeric, f float8, j jsonb, i int); +insert into t_eqimg values (1, '0', '1', 1), (1.0, '-0', '1.0', 1); +-- baselines: each of these is a single group of two rows +select n, count(*) from t_eqimg group by n; + n | count +---+------- + 1 | 2 +(1 row) + +select f, count(*) from t_eqimg group by f; + f | count +---+------- + 0 | 2 +(1 row) + +select j, count(*) from t_eqimg group by j; + j | count +---+------- + 1 | 2 +(1 row) + +-- numeric equality ignores scale, so the clause must stay in HAVING +explain (costs off) +select n, count(*) from t_eqimg group by n having n::text = '1'; + QUERY PLAN +----------------------------------- + HashAggregate + Group Key: n + Filter: ((n)::text = '1'::text) + -> Seq Scan on t_eqimg +(4 rows) + +select n, count(*) from t_eqimg group by n having n::text = '1'; + n | count +---+------- + 1 | 2 +(1 row) + +-- float8 equality merges 0 and -0 +explain (costs off) +select f, count(*) from t_eqimg group by f having f::text = '0'; + QUERY PLAN +----------------------------------- + HashAggregate + Group Key: f + Filter: ((f)::text = '0'::text) + -> Seq Scan on t_eqimg +(4 rows) + +select f, count(*) from t_eqimg group by f having f::text = '0'; + f | count +---+------- + 0 | 2 +(1 row) + +-- jsonb numbers compare as numeric but print their trailing zeroes +explain (costs off) +select j, count(*) from t_eqimg group by j having j::text = '1'; + QUERY PLAN +----------------------------------- + HashAggregate + Group Key: j + Filter: ((j)::text = '1'::text) + -> Seq Scan on t_eqimg +(4 rows) + +select j, count(*) from t_eqimg group by j having j::text = '1'; + j | count +---+------- + 1 | 2 +(1 row) + +-- The same conflict reached through an outer WHERE over a GROUP BY subquery, +-- which subquery_push_qual turns into a HAVING clause before we get to it. +-- A WHERE clause may only select the subquery's output rows, never alter +-- them, so both the count and the group key must be unaffected here. +explain (costs off) +select n, c from (select n, count(*) c from t_eqimg group by n) s +where n::text = '1'; + QUERY PLAN +------------------------------------------- + HashAggregate + Group Key: t_eqimg.n + Filter: ((t_eqimg.n)::text = '1'::text) + -> Seq Scan on t_eqimg +(4 rows) + +select n, c from (select n, count(*) c from t_eqimg group by n) s +where n::text = '1'; + n | c +---+--- + 1 | 2 +(1 row) + +select n, c from (select n, count(*) c from t_eqimg group by n) s +where n::text = '1.0'; + n | c +---+--- +(0 rows) + +-- int equality is image equality, so a wrapped reference is still pushable +explain (costs off) +select i, count(*) from t_eqimg group by i having i + 1 = 2; + QUERY PLAN +------------------------------------- + GroupAggregate + Group Key: i + -> Sort + Sort Key: i + -> Seq Scan on t_eqimg + Filter: ((i + 1) = 2) +(6 rows) + +select i, count(*) from t_eqimg group by i having i + 1 = 2; + i | count +---+------- + 1 | 2 +(1 row) + +drop table t_eqimg; -- -- Test GROUP BY matching of join columns that are type-coerced due to USING -- diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index cf295d56507..1dad491f525 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -2270,6 +2270,81 @@ WHERE a *= ROW(1.0)::t_rec; (1.0) (2 rows) +ROLLBACK; +-- +-- A qual that reaches a grouping column of the subquery through a wrapper, +-- rather than as a direct operand of a comparison, is only pushable when the +-- grouping's equality is image equality. numeric equality is not: 1 and 1.0 +-- are equal but do not print alike, so a pushed-down qual could both drop a +-- row the grouping would have kept and change which row represents the group. +-- +BEGIN; +CREATE TEMP TABLE eqimg_num (n numeric); +INSERT INTO eqimg_num VALUES (1), (1.0); +-- the subquery emits a single row, so the outer WHERE can only keep or drop it +SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s; + n +--- + 1 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0'; + QUERY PLAN +--------------------------------------- + Subquery Scan on s + Filter: ((s.n)::text = '1.0'::text) + -> HashAggregate + Group Key: eqimg_num.n + -> Seq Scan on eqimg_num +(5 rows) + +SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0'; + n +--- +(0 rows) + +-- UNION groups by the same equality +EXPLAIN (COSTS OFF) +SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s +WHERE n::text = '1.0'; + QUERY PLAN +----------------------------------------------------- + Subquery Scan on s + Filter: ((s.n)::text = '1.0'::text) + -> HashAggregate + Group Key: eqimg_num.n + -> Append + -> Seq Scan on eqimg_num + -> Seq Scan on eqimg_num eqimg_num_1 +(7 rows) + +SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s +WHERE n::text = '1.0'; + n +--- +(0 rows) + +-- int equality is image equality, so the same shape of qual is pushable +CREATE TEMP TABLE eqimg_int (i int); +INSERT INTO eqimg_int VALUES (1), (1); +EXPLAIN (COSTS OFF) +SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2; + QUERY PLAN +------------------------------------- + Unique + -> Sort + Sort Key: eqimg_int.i + -> Seq Scan on eqimg_int + Filter: ((i + 1) = 2) +(5 rows) + +SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2; + i +--- + 1 +(1 row) + ROLLBACK; -- -- Test that LIMIT can be pushed to SORT through a subquery that just projects diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 91f8342166f..c9893ef17e6 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -652,6 +652,52 @@ select a, count(*) from t_having group by a having a = row(1.0)::avg_rec; drop table t_having; drop type avg_rec; +-- A HAVING clause that reaches the grouping column through a wrapper, rather +-- than as a direct operand of a comparison, must NOT be pushed down to WHERE +-- unless the grouping's equality is image equality: the wrapper can tell apart +-- values that GROUP BY merged into one group. +create temp table t_eqimg (n numeric, f float8, j jsonb, i int); +insert into t_eqimg values (1, '0', '1', 1), (1.0, '-0', '1.0', 1); + +-- baselines: each of these is a single group of two rows +select n, count(*) from t_eqimg group by n; +select f, count(*) from t_eqimg group by f; +select j, count(*) from t_eqimg group by j; + +-- numeric equality ignores scale, so the clause must stay in HAVING +explain (costs off) +select n, count(*) from t_eqimg group by n having n::text = '1'; +select n, count(*) from t_eqimg group by n having n::text = '1'; + +-- float8 equality merges 0 and -0 +explain (costs off) +select f, count(*) from t_eqimg group by f having f::text = '0'; +select f, count(*) from t_eqimg group by f having f::text = '0'; + +-- jsonb numbers compare as numeric but print their trailing zeroes +explain (costs off) +select j, count(*) from t_eqimg group by j having j::text = '1'; +select j, count(*) from t_eqimg group by j having j::text = '1'; + +-- The same conflict reached through an outer WHERE over a GROUP BY subquery, +-- which subquery_push_qual turns into a HAVING clause before we get to it. +-- A WHERE clause may only select the subquery's output rows, never alter +-- them, so both the count and the group key must be unaffected here. +explain (costs off) +select n, c from (select n, count(*) c from t_eqimg group by n) s +where n::text = '1'; +select n, c from (select n, count(*) c from t_eqimg group by n) s +where n::text = '1'; +select n, c from (select n, count(*) c from t_eqimg group by n) s +where n::text = '1.0'; + +-- int equality is image equality, so a wrapped reference is still pushable +explain (costs off) +select i, count(*) from t_eqimg group by i having i + 1 = 2; +select i, count(*) from t_eqimg group by i having i + 1 = 2; + +drop table t_eqimg; + -- -- Test GROUP BY matching of join columns that are type-coerced due to USING -- diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index 07438694f6e..d6ef3badaa2 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -1110,6 +1110,45 @@ WHERE a *= ROW(1.0)::t_rec; ROLLBACK; +-- +-- A qual that reaches a grouping column of the subquery through a wrapper, +-- rather than as a direct operand of a comparison, is only pushable when the +-- grouping's equality is image equality. numeric equality is not: 1 and 1.0 +-- are equal but do not print alike, so a pushed-down qual could both drop a +-- row the grouping would have kept and change which row represents the group. +-- +BEGIN; + +CREATE TEMP TABLE eqimg_num (n numeric); +INSERT INTO eqimg_num VALUES (1), (1.0); + +-- the subquery emits a single row, so the outer WHERE can only keep or drop it +SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s; + +EXPLAIN (COSTS OFF) +SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0'; + +SELECT n FROM (SELECT DISTINCT n FROM eqimg_num) s WHERE n::text = '1.0'; + +-- UNION groups by the same equality +EXPLAIN (COSTS OFF) +SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s +WHERE n::text = '1.0'; + +SELECT n FROM (SELECT n FROM eqimg_num UNION SELECT n FROM eqimg_num) s +WHERE n::text = '1.0'; + +-- int equality is image equality, so the same shape of qual is pushable +CREATE TEMP TABLE eqimg_int (i int); +INSERT INTO eqimg_int VALUES (1), (1); + +EXPLAIN (COSTS OFF) +SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2; + +SELECT i FROM (SELECT DISTINCT i FROM eqimg_int) s WHERE i + 1 = 2; + +ROLLBACK; + -- -- Test that LIMIT can be pushed to SORT through a subquery that just projects -- columns. We check for that having happened by looking to see if EXPLAIN