Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1kZmCo-0000lQ-2q for pgsql-hackers@arkaria.postgresql.org; Tue, 03 Nov 2020 02:37:54 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1kZmCm-0001PB-58 for pgsql-hackers@arkaria.postgresql.org; Tue, 03 Nov 2020 02:37:52 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1kZmCl-0001P2-N4 for pgsql-hackers@lists.postgresql.org; Tue, 03 Nov 2020 02:37:51 +0000 Received: from mail-ej1-x643.google.com ([2a00:1450:4864:20::643]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1kZmCi-0001gl-Bv for pgsql-hackers@postgresql.org; Tue, 03 Nov 2020 02:37:51 +0000 Received: by mail-ej1-x643.google.com with SMTP id za3so21997482ejb.5 for ; Mon, 02 Nov 2020 18:37:48 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=2ndquadrant-com.20150623.gappssmtp.com; s=20150623; h=date:from:to:cc:subject:message-id:references:mime-version :content-disposition:in-reply-to; bh=fskdzVuyRwJgedhcmfe/eULIddHyrnKwfQWj4nfenis=; b=bTHvArhtTEOxRRWM23Ap3Z5Cpq6QqdPjhah+6JQBZQdUPlxmj7W0Rz4QhchhyL7/K2 m1ce/CPxquop0lGeKuH3LbDCtLuxEjRbcdg3jlM5mhTm0n08oR6925DJ1EzJoadog63L hDmOa0SKNct7hiczLjz9aPnqBCxfU+i3LWv/F6BfNV96lYZCbPZoBLtHcxElXSi2p2Il 4kqn8SDPSCes4/Y7vUHqhJYoDMQjIgHLMGwPc3XEwQ00JykojoM6KOHuUMNQcEywcs/b 1PiwUS/GJC1/4kPM8PUTm96qlx3GXzVWcVv3kZm49go5llfAeMBiRZ9be+Cb0JqRwQR3 rjzA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:from:to:cc:subject:message-id:references :mime-version:content-disposition:in-reply-to; bh=fskdzVuyRwJgedhcmfe/eULIddHyrnKwfQWj4nfenis=; b=T9kiXU1EXYIDA6VLm7kd1VRkAUb7CbeiYIxrHXh2nYcm1t1IIFCg7gyk/EIe/ZKzwx zAY5FkIz5AULoRTIMXmeU3g658XHavd4nccxUFQPilm4hi0oY9LT3Vt0dzuwWhBo7Dkv 7n3GMEMfGCPwidNrk2OHsWx+RUuEm0Y5kF4NbL4F1/OFhe4jzsY6CdM81USY9hNrxnzs KxXiB2vf6KE5QntMV5KRoZgGlK3SqtCF0ZMu6OU7ld1psPib5s0SHN3Hb/TdMNkEiqLC vcDXEIvBWFRKUt18sHkWj802xGWa9Zi4h8g5rV7kL20q11gry8iOQ52vK/JYMqrwqYtU QMJw== X-Gm-Message-State: AOAM530vVeadDwTWtRm1CNgHEtjyl9OmUl28yJybqg4fazDFxyjoYlwe b0LKO3TOCR0iIWZlSCzF9byzVw== X-Google-Smtp-Source: ABdhPJy5pZugMwIfRqLQXT7Xz9eO1smwZZgvipN/SrgXd48qQ36RWei9YDFQy45sSapricTcNdECVQ== X-Received: by 2002:a17:906:519b:: with SMTP id y27mr17122521ejk.25.1604371066607; Mon, 02 Nov 2020 18:37:46 -0800 (PST) Received: from localhost (ip-86-49-245-76.net.upcbroadband.cz. [86.49.245.76]) by smtp.gmail.com with ESMTPSA id y3sm10317897ejk.92.2020.11.02.18.37.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 02 Nov 2020 18:37:45 -0800 (PST) Date: Tue, 3 Nov 2020 03:37:43 +0100 From: Tomas Vondra To: James Coleman Cc: Jaime Casanova , Pg Hackers Subject: Re: enable_incremental_sort changes query behavior Message-ID: <20201103023743.sswkl6h74dkbcfmp@development> References: <20201029220652.glf4xk2p7wkfisb6@development> <20201030210316.cbwv4arwp4lcfgx2@development> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="u2smeq7kv6ryw7fy" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --u2smeq7kv6ryw7fy Content-Type: text/plain; charset=us-ascii; format=flowed Content-Disposition: inline On Fri, Oct 30, 2020 at 07:37:33PM -0400, James Coleman wrote: > > ... > >I was looking at this some more, and I'm still convinced that this is >correct, but I don't think a comment about it being an optimization >(though I suspect that that its major benefit), since it came from, >and must parallel, find_ec_member_for_tle, and there is no such >explanation there. I don't want to backfill reasoning onto that >decision, but I do think it's important to parallel it since it's >ultimately what is going to cause errors if we're out of sync with it. > >As to why find_em_expr_for_rel is different, I think the answer is >that it's used by the FDW code, and it seems to me to make sense that >in that case we'd only send sort conditions to the foreign server if >the em actually contains rels. > >The new series attached includes the primary fix for this issue, the >additional comments that could either go in at the same time or not, >and my patch with semi-related questions (which isn't intended to be >committed, but to keep track of the questions). > Thanks. Attached are two patches that I plan to get committed 0001 is what you sent, with slightly reworded commit message. This is the actual fix. I'm still thinking about Robert's comment that perhaps we should be considering only expressions already in the relation's target, which would imply checking for volatile expressions is not enough. But I've been unable to convince myself it's correct or incorrect. In any case 0001 is a clear improvement - in the worst case we'll have to make it stricter in the future. 0002 partially reverts ba3e76cc571 by moving find_em_expr_for_rel back to postgres_fdw.c. We've moved it to equivclass.c so that it could be called from both the FDW and allpaths.c, but now that the functions diverged it's only called from the FDW again. So maybe we should move it back, but I guess that's not a good thing in a minor release. regards -- Tomas Vondra http://www.2ndQuadrant.com PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services --u2smeq7kv6ryw7fy Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename*0="v7-0001-Fix-get_useful_pathkeys_for_relation-for-volatile-ex."; filename*1=patch From b2c8085256aa6d6e34f6c9b8394738ee1bef3997 Mon Sep 17 00:00:00 2001 From: James Coleman Date: Sat, 3 Oct 2020 10:35:47 -0400 Subject: [PATCH 1/3] Fix get_useful_pathkeys_for_relation for volatile expressions When considering Incremental Sort below a Gather Merge, we need to be a bit more careful when matching pathkeys to EC members. It's not enough to find a member whose Vars are all in the current relation's target; volatile expressions in particular need to be contained in the target, otherwise it's too early to use the pathkey. Reported-by: Jaime Casanova Author: James Coleman Reviewed-by: Tomas Vondra Backpatch-through: 13 Discussion: https://postgr.es/m/CAJGNTeNaxpXgBVcRhJX%2B2vSbq%2BF2kJqGBcvompmpvXb7pq%2BoFA%40mail.gmail.com --- src/backend/optimizer/path/allpaths.c | 13 +-- src/backend/optimizer/path/equivclass.c | 70 +++++++++++++ src/include/optimizer/paths.h | 1 + .../regress/expected/incremental_sort.out | 98 +++++++++++++++++++ src/test/regress/sql/incremental_sort.sql | 31 ++++++ 5 files changed, 207 insertions(+), 6 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 8ad6384c6a..84a69b064a 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -2816,7 +2816,8 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel) /* * Considering query_pathkeys is always worth it, because it might allow * us to avoid a total sort when we have a partially presorted path - * available. + * available or to push the total sort into the parallel portion of the + * query. */ if (root->query_pathkeys) { @@ -2829,17 +2830,17 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel) EquivalenceClass *pathkey_ec = pathkey->pk_eclass; /* - * We can only build an Incremental Sort for pathkeys which - * contain an EC member in the current relation, so ignore any - * suffix of the list as soon as we find a pathkey without an EC - * member the relation. + * We can only build a sort for pathkeys which contain an EC + * member in the current relation's target, so ignore any suffix + * of the list as soon as we find a pathkey without an EC member + * in the relation. * * By still returning the prefix of the pathkeys list that does * meet criteria of EC membership in the current relation, we * enable not just an incremental sort on the entirety of * query_pathkeys but also incremental sort below a JOIN. */ - if (!find_em_expr_for_rel(pathkey_ec, rel)) + if (!find_em_expr_usable_for_sorting_rel(pathkey_ec, rel)) break; npathkeys++; diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index a21b3b4756..f98fd7b3eb 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -797,6 +797,76 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel) return NULL; } +/* + * Find an equivalence class member expression that can be safely used by a + * sort node on top of the provided relation. The rules here must match those + * applied in prepare_sort_from_pathkeys. + */ +Expr * +find_em_expr_usable_for_sorting_rel(EquivalenceClass *ec, RelOptInfo *rel) +{ + ListCell *lc_em; + + /* + * If there is more than one equivalence member matching these + * requirements we'll be content to choose any one of them. + */ + foreach(lc_em, ec->ec_members) + { + EquivalenceMember *em = lfirst(lc_em); + Expr *em_expr = em->em_expr; + PathTarget *target = rel->reltarget; + ListCell *lc_target_expr; + + /* + * We shouldn't be trying to sort by an equivalence class that + * contains a constant, so no need to consider such cases any further. + */ + if (em->em_is_const) + continue; + + /* + * Any Vars in the equivalence class member need to come from this + * relation. This is a superset of prepare_sort_from_pathkeys ignoring + * child members unless they belong to the rel being sorted. + */ + if (!bms_is_subset(em->em_relids, rel->relids)) + continue; + + /* + * As long as the expression isn't volatile then + * prepare_sort_from_pathkeys is able to generate a new target entry, + * so there's no need to verify that one already exists. + */ + if (!ec->ec_has_volatile) + return em->em_expr; + + /* + * If, however, it's volatile, we have to verify that the + * equivalence member's expr is already generated in the + * relation's target (we do strip relabels first from both + * expressions, which is cheap and might allow us to match + * more expressions). + */ + while (em_expr && IsA(em_expr, RelabelType)) + em_expr = ((RelabelType *) em_expr)->arg; + + foreach(lc_target_expr, target->exprs) + { + Expr *target_expr = lfirst(lc_target_expr); + + while (target_expr && IsA(target_expr, RelabelType)) + target_expr = ((RelabelType *) target_expr)->arg; + + if (equal(target_expr, em_expr)) + return em->em_expr; + } + } + + /* We didn't find any suitable equivalence class expression */ + return NULL; +} + /* * generate_base_implied_equalities * Generate any restriction clauses that we can deduce from equivalence diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 2134227ebc..8a4c6f8b59 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -135,6 +135,7 @@ extern EquivalenceClass *get_eclass_for_sort_expr(PlannerInfo *root, Relids rel, bool create_it); extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel); +extern Expr *find_em_expr_usable_for_sorting_rel(EquivalenceClass *ec, RelOptInfo *rel); extern void generate_base_implied_equalities(PlannerInfo *root); extern List *generate_join_implied_equalities(PlannerInfo *root, Relids join_relids, diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out index e376ea1276..7cf2eee7c1 100644 --- a/src/test/regress/expected/incremental_sort.out +++ b/src/test/regress/expected/incremental_sort.out @@ -1469,3 +1469,101 @@ explain (costs off) select * from t union select * from t order by 1,3; (13 rows) drop table t; +-- Sort pushdown can't go below where expressions are part of the rel target. +-- In particular this is interesting for volatile expressions which have to +-- go above joins since otherwise we'll incorrectly use expression evaluations +-- across multiple rows. +set enable_hashagg=off; +set enable_seqscan=off; +set enable_incremental_sort = off; +set parallel_tuple_cost=0; +set parallel_setup_cost=0; +set min_parallel_table_scan_size = 0; +set min_parallel_index_scan_size = 0; +-- Parallel sort below join. +explain (costs off) select distinct sub.unique1, stringu1 +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub; + QUERY PLAN +-------------------------------------------------------------------------- + Unique + -> Nested Loop + -> Gather Merge + Workers Planned: 2 + -> Sort + Sort Key: tenk1.unique1, tenk1.stringu1 + -> Parallel Index Scan using tenk1_unique1 on tenk1 + -> Function Scan on generate_series +(8 rows) + +explain (costs off) select sub.unique1, stringu1 +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub +order by 1, 2; + QUERY PLAN +-------------------------------------------------------------------- + Nested Loop + -> Gather Merge + Workers Planned: 2 + -> Sort + Sort Key: tenk1.unique1, tenk1.stringu1 + -> Parallel Index Scan using tenk1_unique1 on tenk1 + -> Function Scan on generate_series +(7 rows) + +-- Parallel sort but with expression that can be safely generated at the base rel. +explain (costs off) select distinct sub.unique1, md5(stringu1) +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub; + QUERY PLAN +---------------------------------------------------------------------------------------- + Unique + -> Nested Loop + -> Gather Merge + Workers Planned: 2 + -> Sort + Sort Key: tenk1.unique1, (md5((tenk1.stringu1)::text)) COLLATE "C" + -> Parallel Index Scan using tenk1_unique1 on tenk1 + -> Function Scan on generate_series +(8 rows) + +explain (costs off) select sub.unique1, md5(stringu1) +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub +order by 1, 2; + QUERY PLAN +---------------------------------------------------------------------------------- + Nested Loop + -> Gather Merge + Workers Planned: 2 + -> Sort + Sort Key: tenk1.unique1, (md5((tenk1.stringu1)::text)) COLLATE "C" + -> Parallel Index Scan using tenk1_unique1 on tenk1 + -> Function Scan on generate_series +(7 rows) + +-- Parallel sort but with expression not available until the upper rel. +explain (costs off) select distinct sub.unique1, stringu1 || random()::text +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub; + QUERY PLAN +--------------------------------------------------------------------------------------------- + Unique + -> Sort + Sort Key: tenk1.unique1, (((tenk1.stringu1)::text || (random())::text)) COLLATE "C" + -> Gather + Workers Planned: 2 + -> Nested Loop + -> Parallel Index Scan using tenk1_unique1 on tenk1 + -> Function Scan on generate_series +(8 rows) + +explain (costs off) select sub.unique1, stringu1 || random()::text +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub +order by 1, 2; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Sort Key: tenk1.unique1, (((tenk1.stringu1)::text || (random())::text)) COLLATE "C" + -> Gather + Workers Planned: 2 + -> Nested Loop + -> Parallel Index Scan using tenk1_unique1 on tenk1 + -> Function Scan on generate_series +(7 rows) + diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sql index 9c040c90e6..3ee11c394b 100644 --- a/src/test/regress/sql/incremental_sort.sql +++ b/src/test/regress/sql/incremental_sort.sql @@ -221,3 +221,34 @@ set enable_hashagg to off; explain (costs off) select * from t union select * from t order by 1,3; drop table t; + +-- Sort pushdown can't go below where expressions are part of the rel target. +-- In particular this is interesting for volatile expressions which have to +-- go above joins since otherwise we'll incorrectly use expression evaluations +-- across multiple rows. +set enable_hashagg=off; +set enable_seqscan=off; +set enable_incremental_sort = off; +set parallel_tuple_cost=0; +set parallel_setup_cost=0; +set min_parallel_table_scan_size = 0; +set min_parallel_index_scan_size = 0; + +-- Parallel sort below join. +explain (costs off) select distinct sub.unique1, stringu1 +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub; +explain (costs off) select sub.unique1, stringu1 +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub +order by 1, 2; +-- Parallel sort but with expression that can be safely generated at the base rel. +explain (costs off) select distinct sub.unique1, md5(stringu1) +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub; +explain (costs off) select sub.unique1, md5(stringu1) +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub +order by 1, 2; +-- Parallel sort but with expression not available until the upper rel. +explain (costs off) select distinct sub.unique1, stringu1 || random()::text +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub; +explain (costs off) select sub.unique1, stringu1 || random()::text +from tenk1, lateral (select tenk1.unique1 from generate_series(1, 1000)) as sub +order by 1, 2; -- 2.25.4 --u2smeq7kv6ryw7fy Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename*0="v7-0002-partially-revert-ba3e76cc571eba3dea19c9465ff15ac3ac1."; filename*1=patch From aefe619b1cecb4ca70f2035b2b62ff270a4e80e7 Mon Sep 17 00:00:00 2001 From: Tomas Vondra Date: Tue, 3 Nov 2020 03:22:50 +0100 Subject: [PATCH 2/3] partially revert ba3e76cc571eba3dea19c9465ff15ac3ac186576 --- contrib/postgres_fdw/postgres_fdw.c | 29 +++++++++++++++++++++++++ src/backend/optimizer/path/equivclass.c | 29 ------------------------- src/include/optimizer/paths.h | 1 - 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 9c5aaacc51..dcf533d700 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -6524,6 +6524,35 @@ conversion_error_callback(void *arg) } } +/* + * Find an equivalence class member expression, all of whose Vars, come from + * the indicated relation. + */ +Expr * +find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel) +{ + ListCell *lc_em; + + foreach(lc_em, ec->ec_members) + { + EquivalenceMember *em = lfirst(lc_em); + + if (bms_is_subset(em->em_relids, rel->relids) && + !bms_is_empty(em->em_relids)) + { + /* + * If there is more than one equivalence member whose Vars are + * taken entirely from this relation, we'll be content to choose + * any one of those. + */ + return em->em_expr; + } + } + + /* We didn't find any suitable equivalence class expression */ + return NULL; +} + /* * Find an equivalence class member expression to be computed as a sort column * in the given target. diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index f98fd7b3eb..a507a76bcf 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -768,35 +768,6 @@ get_eclass_for_sort_expr(PlannerInfo *root, return newec; } -/* - * Find an equivalence class member expression, all of whose Vars, come from - * the indicated relation. - */ -Expr * -find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel) -{ - ListCell *lc_em; - - foreach(lc_em, ec->ec_members) - { - EquivalenceMember *em = lfirst(lc_em); - - if (bms_is_subset(em->em_relids, rel->relids) && - !bms_is_empty(em->em_relids)) - { - /* - * If there is more than one equivalence member whose Vars are - * taken entirely from this relation, we'll be content to choose - * any one of those. - */ - return em->em_expr; - } - } - - /* We didn't find any suitable equivalence class expression */ - return NULL; -} - /* * Find an equivalence class member expression that can be safely used by a * sort node on top of the provided relation. The rules here must match those diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 8a4c6f8b59..5f6ffbd860 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -134,7 +134,6 @@ extern EquivalenceClass *get_eclass_for_sort_expr(PlannerInfo *root, Index sortref, Relids rel, bool create_it); -extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel); extern Expr *find_em_expr_usable_for_sorting_rel(EquivalenceClass *ec, RelOptInfo *rel); extern void generate_base_implied_equalities(PlannerInfo *root); extern List *generate_join_implied_equalities(PlannerInfo *root, -- 2.25.4 --u2smeq7kv6ryw7fy--