From: Tomas Vondra <tomas.vondra@2ndquadrant.com>
To: James Coleman <jtc331@gmail.com>
Cc: Michael Paquier <michael@paquier.xyz>
Cc: Rafia Sabih <rafia.pghackers@gmail.com>
Cc: Peter Geoghegan <pg@bowt.ie>
Cc: Simon Riggs <simon@2ndquadrant.com>
Cc: Shaun Thomas <shaun.thomas@2ndquadrant.com>
Cc: Dmitry Dolgov <9erthalion6@gmail.com>
Cc: Alexander Korotkov <a.korotkov@postgrespro.ru>
Cc: Tom Lane <tgl@sss.pgh.pa.us>
Cc: PostgreSQL Developers <pgsql-hackers@lists.postgresql.org>
Cc: Alvaro Herrera <alvherre@2ndquadrant.com>
Subject: Re: [PATCH] Incremental sort (was: PoC: Partial sort)
Date: Sat, 21 Mar 2020 01:56:40 +0100
Message-ID: <20200321005640.vrwcpyis6ahmspwt@development> (raw)
In-Reply-To: <CAAaqYe-g9Gsc7_imKK7N871JOHQzZ=VGKd=nTdK-5PQ53YuWYg@mail.gmail.com>
References: <CAAaqYe8-TCKPskcMytCMX2aM8QnnrgcJP6=tSnJOuQ2CcuQJfg@mail.gmail.com>
<CAAaqYe-KCy3tW6j2VLi++C6Z+d1wgi81sOxiKo0+HWVR2noNLg@mail.gmail.com>
<20200314001254.gdusqeanl7wrxmr7@development>
<CAAaqYe_cVOS3akUic2PSaEW0noB=9bBxMkkDuj0HBZJqHHh5yQ@mail.gmail.com>
<CAAaqYe_ZpNFJXGTN1VFNqnOLxLfqib0OZ7L-UGP_MAdZi0WbiA@mail.gmail.com>
<CAAaqYe9Y6Hc35N2+eRMhPvc1YJ1LhdZjzT8k7J1KGcx_JC+_Kw@mail.gmail.com>
<CAAaqYe_9eQoZZ1rS8pXy+3AdkAHjejB0ANbh+-ipv04NcxxCYQ@mail.gmail.com>
<CAAaqYe_NjXv9TZ3rjO=hMzL3KvEj-9Ado3qUKRS3F0DCcS3R4g@mail.gmail.com>
<20200314195822.bc2mqyvolo2w5l2w@development>
<CAAaqYe-g9Gsc7_imKK7N871JOHQzZ=VGKd=nTdK-5PQ53YuWYg@mail.gmail.com>
Hi,
I've looked at v38 but it seems it's a bit broken by some recent explain
changes (mostly missing type in declarations). Attached is v39 fixing
those issues, and including a bunch of fixes based on a review - most of
the changes is in comments, so I've instead kept them in separate "fix"
patches after each part.
In general I'm mostly happy with the current shape of the patch, and
unless there are some objections I'd like to get some of it committed
sometime next week.
I've done a fair amount of testing with various queries, and the plan
changes seem pretty sensible. I'm still not entirely sure whether to be
a bit conservative and only tweak the first patch adding incremental
sort to extra places, or commit both.
The main thing I still have on my plate is assessment of how much more
expensive can the planning due to increased number of paths we
generate/keep (due to considering extra pathkeys). I haven't seen any
significant slowdowns, but I plan to look at some extreme cases (many
similar and applicable indexes etc.).
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
From 6fd38a5d79b88202dee963d52629527e37693be6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 28 Jul 2019 15:55:54 +0200
Subject: [PATCH 1/8] Consider low startup cost when adding partial path
45be99f8cd5d606086e0a458c9c72910ba8a613d added `add_partial_path` with the
comment:
> Neither do we need to consider startup costs:
> parallelism is only used for plans that will be run to completion.
> Therefore, this routine is much simpler than add_path: it needs to
> consider only pathkeys and total cost.
I'm not entirely sure if that is still true or not--I can't easily come
up with a scenario in which it's not, but I also can't come up with an
inherent reason why such a scenario cannot exist.
Regardless, the in-progress incremental sort patch uncovered a new case
where it definitely no longer holds, and, as a result a higher cost plan
ends up being chosen because a low startup cost partial path is ignored
in favor of a lower total cost partial path and a limit is a applied on
top of that which would normal favor the lower startup cost plan.
---
src/backend/optimizer/util/pathnode.c | 47 ++++++++++-----------------
1 file changed, 18 insertions(+), 29 deletions(-)
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.cindex 8ba8122ee2..702189fe17 100644--- a/src/backend/optimizer/util/pathnode.c+++ b/src/backend/optimizer/util/pathnode.c@@ -777,41 +777,30 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
/* Unless pathkeys are incompatible, keep just one of the two paths. */
if (keyscmp != PATHKEYS_DIFFERENT)
{
- if (new_path->total_cost > old_path->total_cost * STD_FUZZ_FACTOR)- {- /* New path costs more; keep it only if pathkeys are better. */- if (keyscmp != PATHKEYS_BETTER1)- accept_new = false;- }- else if (old_path->total_cost > new_path->total_cost- * STD_FUZZ_FACTOR)+ PathCostComparison costcmp;++ /*+ * Do a fuzzy cost comparison with standard fuzziness limit.+ */+ costcmp = compare_path_costs_fuzzily(new_path, old_path,+ STD_FUZZ_FACTOR);++ if (costcmp == COSTS_BETTER1)
{
- /* Old path costs more; keep it only if pathkeys are better. */- if (keyscmp != PATHKEYS_BETTER2)+ if (keyscmp == PATHKEYS_BETTER1)
remove_old = true;
}
- else if (keyscmp == PATHKEYS_BETTER1)- {- /* Costs are about the same, new path has better pathkeys. */- remove_old = true;- }- else if (keyscmp == PATHKEYS_BETTER2)+ else if (costcmp == COSTS_BETTER2)
{
- /* Costs are about the same, old path has better pathkeys. */- accept_new = false;- }- else if (old_path->total_cost > new_path->total_cost * 1.0000000001)- {- /* Pathkeys are the same, and the old path costs more. */- remove_old = true;+ if (keyscmp == PATHKEYS_BETTER2)+ accept_new = false;
}
- else+ else if (costcmp == COSTS_EQUAL)
{
- /*- * Pathkeys are the same, and new path isn't materially- * cheaper.- */- accept_new = false;+ if (keyscmp == PATHKEYS_BETTER1)+ remove_old = true;+ else if (keyscmp == PATHKEYS_BETTER2)+ accept_new = false;
}
}
--
2.21.1
From b7edae462892ebb9e87025b6801985d8e4b31824 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 16:26:37 +0100
Subject: [PATCH 2/8] fix comments
---
src/backend/optimizer/util/pathnode.c | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.cindex 702189fe17..9c8f3b1f0b 100644--- a/src/backend/optimizer/util/pathnode.c+++ b/src/backend/optimizer/util/pathnode.c@@ -733,10 +733,11 @@ add_path_precheck(RelOptInfo *parent_rel,
*
* Because we don't consider parameterized paths here, we also don't
* need to consider the row counts as a measure of quality: every path will
- * produce the same number of rows. Neither do we need to consider startup- * costs: parallelism is only used for plans that will be run to completion.- * Therefore, this routine is much simpler than add_path: it needs to- * consider only pathkeys and total cost.+ * produce the same number of rows. It may however matter how much the+ * path ordering matches the final ordering, needed by upper parts of the+ * plan, because that will affect how expensive the incremental sort is.+ * because of that we need to consider both the total and startup path,+ * in addition to pathkeys.
*
* As with add_path, we pfree paths that are found to be dominated by
* another partial path; this requires that there be no other references to
@@ -774,7 +775,14 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
/* Compare pathkeys. */
keyscmp = compare_pathkeys(new_path->pathkeys, old_path->pathkeys);
- /* Unless pathkeys are incompatible, keep just one of the two paths. */+ /*+ * Unless pathkeys are incompatible, see if one of the paths dominates+ * the other (both in startup and total cost). It may happen that one+ * path has lower startup cost, the other has lower total cost.+ *+ * XXX Perhaps we could do this only when incremental sort is enabled,+ * and use the simpler version (compring just total cost) otherwise?+ */
if (keyscmp != PATHKEYS_DIFFERENT)
{
PathCostComparison costcmp;
--
2.21.1
From b4c6c28fbf438a02d1d20f0d22b8df3bb51ce922 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 15:25:55 +0100
Subject: [PATCH 3/8] Implement incremental sort
Incremental sort is an optimized variant of multikey sort for cases
when the input is already sorted by a prefix of the sort keys. For
example when a sort by (key1, key2 ... keyN) is requested, and the
input is already sorted by (key1, key2 ... keyM), M < N, we can
divide the input into groups where keys (key1, ... keyM) are equal,
and only sort on the remaining columns.
The implemented algorithm operates in two different modes:
- Fetching a minimum number of tuples without checking prefix key
group membership and sorting on all columns when safe.
- Fetching all tuples for a single prefix key group and sorting on
solely the unsorted columns.
We always begin in the first mode, and employ a heuristic to switch
into the second mode if we believe it's beneficial.
Sorting incrementally can potentially use less memory (and possibly
avoid spilling to disk), avoid fetching and sorting all tuples in the
dataset (particularly useful when a LIMIT clause has been specified),
and begin returning tuples before the entire result set is available.
Small datasets which fit entirely in memory and must be fully realized
and sorted may be slightly slower, which we reflect in the costing
implementation.
The hybrid mode approach allows us to optimize for both very small
groups (where the overhead of a new tuplesort is high) and very large
groups (where we can lower cost by not having to sort on already sorted
columns), albeit at some extra cost while switching between modes.
Co-authored-by: Alexander Korotkov <a.korotkov@postgrespro.ru>
---
doc/src/sgml/config.sgml | 14 +
src/backend/commands/explain.c | 211 ++-
src/backend/executor/Makefile | 1 +
src/backend/executor/execAmi.c | 14 +
src/backend/executor/execParallel.c | 18 +
src/backend/executor/execProcnode.c | 33 +
src/backend/executor/nodeIncrementalSort.c | 1201 +++++++++++++++
src/backend/executor/nodeSort.c | 3 +-
src/backend/nodes/copyfuncs.c | 49 +-
src/backend/nodes/outfuncs.c | 25 +-
src/backend/nodes/readfuncs.c | 37 +-
src/backend/optimizer/path/allpaths.c | 4 +
src/backend/optimizer/path/costsize.c | 198 ++-
src/backend/optimizer/path/pathkeys.c | 61 +-
src/backend/optimizer/plan/createplan.c | 143 +-
src/backend/optimizer/plan/planner.c | 73 +-
src/backend/optimizer/plan/setrefs.c | 1 +
src/backend/optimizer/plan/subselect.c | 1 +
src/backend/optimizer/util/pathnode.c | 51 +
src/backend/utils/misc/guc.c | 9 +
src/backend/utils/sort/tuplesort.c | 296 +++-
src/include/executor/execdebug.h | 2 +
src/include/executor/nodeIncrementalSort.h | 28 +
src/include/nodes/execnodes.h | 77 +
src/include/nodes/nodes.h | 3 +
src/include/nodes/pathnodes.h | 9 +
src/include/nodes/plannodes.h | 11 +
src/include/optimizer/cost.h | 10 +
src/include/optimizer/pathnode.h | 6 +
src/include/optimizer/paths.h | 2 +
src/include/utils/tuplesort.h | 3 +
.../expected/drop-index-concurrently-1.out | 2 +-
.../regress/expected/incremental_sort.out | 1320 +++++++++++++++++
.../regress/expected/partition_aggregate.out | 2 +
src/test/regress/expected/sysviews.out | 3 +-
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/incremental_sort.sql | 88 ++
src/test/regress/sql/partition_aggregate.sql | 2 +
39 files changed, 3852 insertions(+), 162 deletions(-)
create mode 100644 src/backend/executor/nodeIncrementalSort.c
create mode 100644 src/include/executor/nodeIncrementalSort.h
create mode 100644 src/test/regress/expected/incremental_sort.out
create mode 100644 src/test/regress/sql/incremental_sort.sql
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgmlindex 70854ae298..fe77f8eb4c 100644--- a/doc/src/sgml/config.sgml+++ b/doc/src/sgml/config.sgml@@ -4542,6 +4542,20 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
</listitem>
</varlistentry>
+ <varlistentry id="guc-enable-incrementalsort" xreflabel="enable_incrementalsort">+ <term><varname>enable_incrementalsort</varname> (<type>boolean</type>)+ <indexterm>+ <primary><varname>enable_incrementalsort</varname> configuration parameter</primary>+ </indexterm>+ </term>+ <listitem>+ <para>+ Enables or disables the query planner's use of incremental sort+ steps. The default is <literal>on</literal>.+ </para>+ </listitem>+ </varlistentry>+
<varlistentry id="guc-enable-indexscan" xreflabel="enable_indexscan">
<term><varname>enable_indexscan</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.cindex 58141d8393..dd4600a214 100644--- a/src/backend/commands/explain.c+++ b/src/backend/commands/explain.c@@ -82,6 +82,8 @@ static void show_upper_qual(List *qual, const char *qlabel,
ExplainState *es);
static void show_sort_keys(SortState *sortstate, List *ancestors,
ExplainState *es);
+static void show_incremental_sort_keys(IncrementalSortState *incrsortstate,+ List *ancestors, ExplainState *es);
static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
ExplainState *es);
static void show_agg_keys(AggState *astate, List *ancestors,
@@ -95,7 +97,7 @@ static void show_grouping_set_keys(PlanState *planstate,
static void show_group_keys(GroupState *gstate, List *ancestors,
ExplainState *es);
static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
- int nkeys, AttrNumber *keycols,+ int nkeys, int nPresortedKeys, AttrNumber *keycols,
Oid *sortOperators, Oid *collations, bool *nullsFirst,
List *ancestors, ExplainState *es);
static void show_sortorder_options(StringInfo buf, Node *sortexpr,
@@ -103,6 +105,8 @@ static void show_sortorder_options(StringInfo buf, Node *sortexpr,
static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
List *ancestors, ExplainState *es);
static void show_sort_info(SortState *sortstate, ExplainState *es);
+static void show_incremental_sort_info(IncrementalSortState *incrsortstate,+ ExplainState *es);
static void show_hash_info(HashState *hashstate, ExplainState *es);
static void show_hashagg_info(AggState *hashstate, ExplainState *es);
static void show_tidbitmap_info(BitmapHeapScanState *planstate,
@@ -1240,6 +1244,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_Sort:
pname = sname = "Sort";
break;
+ case T_IncrementalSort:+ pname = sname = "Incremental Sort";+ break;
case T_Group:
pname = sname = "Group";
break;
@@ -1899,6 +1906,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
show_sort_keys(castNode(SortState, planstate), ancestors, es);
show_sort_info(castNode(SortState, planstate), es);
break;
+ case T_IncrementalSort:+ show_incremental_sort_keys(castNode(IncrementalSortState, planstate),+ ancestors, es);+ show_incremental_sort_info(castNode(IncrementalSortState, planstate),+ es);+ break;
case T_MergeAppend:
show_merge_append_keys(castNode(MergeAppendState, planstate),
ancestors, es);
@@ -2227,12 +2240,29 @@ show_sort_keys(SortState *sortstate, List *ancestors, ExplainState *es)
Sort *plan = (Sort *) sortstate->ss.ps.plan;
show_sort_group_keys((PlanState *) sortstate, "Sort Key",
- plan->numCols, plan->sortColIdx,+ plan->numCols, 0, plan->sortColIdx,
plan->sortOperators, plan->collations,
plan->nullsFirst,
ancestors, es);
}
+/*+ * Show the sort keys for a IncrementalSort node.+ */+static void+show_incremental_sort_keys(IncrementalSortState *incrsortstate,+ List *ancestors, ExplainState *es)+{+ IncrementalSort *plan = (IncrementalSort *) incrsortstate->ss.ps.plan;++ show_sort_group_keys((PlanState *) incrsortstate, "Sort Key",+ plan->sort.numCols, plan->presortedCols,+ plan->sort.sortColIdx,+ plan->sort.sortOperators, plan->sort.collations,+ plan->sort.nullsFirst,+ ancestors, es);+}+
/*
* Likewise, for a MergeAppend node.
*/
@@ -2243,7 +2273,7 @@ show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
MergeAppend *plan = (MergeAppend *) mstate->ps.plan;
show_sort_group_keys((PlanState *) mstate, "Sort Key",
- plan->numCols, plan->sortColIdx,+ plan->numCols, 0, plan->sortColIdx,
plan->sortOperators, plan->collations,
plan->nullsFirst,
ancestors, es);
@@ -2267,7 +2297,7 @@ show_agg_keys(AggState *astate, List *ancestors,
show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
else
show_sort_group_keys(outerPlanState(astate), "Group Key",
- plan->numCols, plan->grpColIdx,+ plan->numCols, 0, plan->grpColIdx,
NULL, NULL, NULL,
ancestors, es);
@@ -2336,7 +2366,7 @@ show_grouping_set_keys(PlanState *planstate,
if (sortnode)
{
show_sort_group_keys(planstate, "Sort Key",
- sortnode->numCols, sortnode->sortColIdx,+ sortnode->numCols, 0, sortnode->sortColIdx,
sortnode->sortOperators, sortnode->collations,
sortnode->nullsFirst,
ancestors, es);
@@ -2393,7 +2423,7 @@ show_group_keys(GroupState *gstate, List *ancestors,
/* The key columns refer to the tlist of the child plan */
ancestors = lcons(plan, ancestors);
show_sort_group_keys(outerPlanState(gstate), "Group Key",
- plan->numCols, plan->grpColIdx,+ plan->numCols, 0, plan->grpColIdx,
NULL, NULL, NULL,
ancestors, es);
ancestors = list_delete_first(ancestors);
@@ -2406,13 +2436,14 @@ show_group_keys(GroupState *gstate, List *ancestors,
*/
static void
show_sort_group_keys(PlanState *planstate, const char *qlabel,
- int nkeys, AttrNumber *keycols,+ int nkeys, int nPresortedKeys, AttrNumber *keycols,
Oid *sortOperators, Oid *collations, bool *nullsFirst,
List *ancestors, ExplainState *es)
{
Plan *plan = planstate->plan;
List *context;
List *result = NIL;
+ List *resultPresorted = NIL;
StringInfoData sortkeybuf;
bool useprefix;
int keyno;
@@ -2452,9 +2483,13 @@ show_sort_group_keys(PlanState *planstate, const char *qlabel,
nullsFirst[keyno]);
/* Emit one property-list item per sort key */
result = lappend(result, pstrdup(sortkeybuf.data));
+ if (keyno < nPresortedKeys)+ resultPresorted = lappend(resultPresorted, exprstr);
}
ExplainPropertyList(qlabel, result, es);
+ if (nPresortedKeys > 0)+ ExplainPropertyList("Presorted Key", resultPresorted, es);
}
/*
@@ -2668,6 +2703,168 @@ show_sort_info(SortState *sortstate, ExplainState *es)
}
}
++static void+show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,+ const char *groupLabel, ExplainState *es)+{+ ListCell *methodCell;+ int methodCount = list_length(groupInfo->sortMethods);++ if (es->format == EXPLAIN_FORMAT_TEXT)+ {+ appendStringInfoSpaces(es->str, es->indent * 2);+ appendStringInfo(es->str, "%s Groups: %ld (Methods: ", groupLabel,+ groupInfo->groupCount);+ foreach(methodCell, groupInfo->sortMethods)+ {+ const char *sortMethodName;++ sortMethodName = tuplesort_method_name(methodCell->int_value);+ appendStringInfo(es->str, "%s", sortMethodName);+ if (foreach_current_index(methodCell) < methodCount - 1)+ appendStringInfo(es->str, ", ");+ }+ appendStringInfo(es->str, ")");++ if (groupInfo->maxMemorySpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;+ const char *spaceTypeName;++ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);+ appendStringInfo(es->str, " %s: %ldkB (avg), %ldkB (max)",+ spaceTypeName, avgSpace,+ groupInfo->maxMemorySpaceUsed);+ }++ if (groupInfo->maxDiskSpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;++ const char *spaceTypeName;++ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_DISK);+ /* Add a semicolon separator only if memory stats were printed. */+ if (groupInfo->maxMemorySpaceUsed > 0)+ appendStringInfo(es->str, ";");+ appendStringInfo(es->str, " %s: %ldkB (avg), %ldkB (max)",+ spaceTypeName, avgSpace,+ groupInfo->maxDiskSpaceUsed);+ }++ appendStringInfo(es->str, "\n");+ }+ else+ {+ List *methodNames = NIL;+ StringInfoData groupName;++ initStringInfo(&groupName);+ appendStringInfo(&groupName, "%s Groups", groupLabel);+ ExplainOpenGroup("Incremental Sort Groups", groupName.data, true, es);+ ExplainPropertyInteger("Group Count", NULL, groupInfo->groupCount, es);++ foreach(methodCell, groupInfo->sortMethods)+ {+ const *sortMethodName = tuplesort_method_name(methodCell->int_value);++ methodNames = lappend(methodNames, sortMethodName);+ }+ ExplainPropertyList("Sort Methods Used", methodNames, es);++ if (groupInfo->maxMemorySpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;+ const *spaceTypeName;++ ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);+ ExplainPropertyInteger("Maximum Sort Space Used", "kB",+ groupInfo->maxMemorySpaceUsed, es);+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);+ ExplainPropertyText("Sort Space Type", spaceTypeName, es);+ }+ if (groupInfo->maxDiskSpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;+ const *spaceTypeName;++ ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);+ ExplainPropertyInteger("Maximum Sort Space Used", "kB",+ groupInfo->maxDiskSpaceUsed, es);+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);+ ExplainPropertyText("Sort Space Type", spaceTypeName, es);+ }++ ExplainCloseGroup("Incremental Sort Groups", "XXX Groups", true, es);+ }+}++/*+ * If it's EXPLAIN ANALYZE, show tuplesort stats for a incremental sort node+ */+static void+show_incremental_sort_info(IncrementalSortState *incrsortstate,+ ExplainState *es)+{+ IncrementalSortGroupInfo *fullsortGroupInfo;+ IncrementalSortGroupInfo *prefixsortGroupInfo;++ if (!(es->analyze && incrsortstate->sort_Done))+ return;++ fullsortGroupInfo = &incrsortstate->incsort_info.fullsortGroupInfo;+ if (fullsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(fullsortGroupInfo, "Full-sort", es);+ prefixsortGroupInfo = &incrsortstate->incsort_info.prefixsortGroupInfo;+ if (prefixsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(prefixsortGroupInfo, "Presorted", es);++ if (incrsortstate->shared_info != NULL)+ {+ int n;+ bool opened_group = false;++ for (n = 0; n < incrsortstate->shared_info->num_workers; n++)+ {+ IncrementalSortInfo *incsort_info =+ &incrsortstate->shared_info->sinfo[n];++ /*+ * XXX: The previous version of the patch chcked:+ * fullsort_instrument->sortMethod == SORT_TYPE_STILL_IN_PROGRESS+ * and continued if the condition was true (with the comment+ * "ignore any unfilled slots"). I'm not convinced that makes+ * sense since the same sort instrument can have been used+ * multiple times, so the last time it being used being still in+ * progress, doesn't seem to be relevant. Instead I'm now checking+ * to see if the group count for each group info is 0. If both are+ * 0, then we exclude the worker since it didn't contribute+ * anything meaningful.+ */+ fullsortGroupInfo = &incsort_info->fullsortGroupInfo;+ prefixsortGroupInfo = &incsort_info->prefixsortGroupInfo;+ if (fullsortGroupInfo->groupCount == 0 &&+ prefixsortGroupInfo->groupCount == 0)+ continue;++ if (!opened_group)+ {+ ExplainOpenGroup("Workers", "Workers", false, es);+ opened_group = true;+ }++ if (fullsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(fullsortGroupInfo, "Full-sort", es);+ if (prefixsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(prefixsortGroupInfo, "Presorted", es);+ }++ if (opened_group)+ ExplainCloseGroup("Workers", "Workers", false, es);+ }+}+
/*
* Show information on hash buckets/batches.
*/
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefileindex a983800e4b..f990c6473a 100644--- a/src/backend/executor/Makefile+++ b/src/backend/executor/Makefile@@ -46,6 +46,7 @@ OBJS = \
nodeGroup.o \
nodeHash.o \
nodeHashjoin.o \
+ nodeIncrementalSort.o \
nodeIndexonlyscan.o \
nodeIndexscan.o \
nodeLimit.o \
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.cindex b12aeb3334..e2154ba86a 100644--- a/src/backend/executor/execAmi.c+++ b/src/backend/executor/execAmi.c@@ -30,6 +30,7 @@
#include "executor/nodeGroup.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
+#include "executor/nodeIncrementalSort.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "executor/nodeLimit.h"
@@ -252,6 +253,10 @@ ExecReScan(PlanState *node)
ExecReScanSort((SortState *) node);
break;
+ case T_IncrementalSortState:+ ExecReScanIncrementalSort((IncrementalSortState *) node);+ break;+
case T_GroupState:
ExecReScanGroup((GroupState *) node);
break;
@@ -557,8 +562,17 @@ ExecSupportsBackwardScan(Plan *node)
case T_CteScan:
case T_Material:
case T_Sort:
+ /* these don't evaluate tlist */
return true;
+ case T_IncrementalSort:++ /*+ * Unlike full sort, incremental sort keeps only a single group of+ * tuples in memory, so it can't scan backwards.+ */+ return false;+
case T_LockRows:
case T_Limit:
return ExecSupportsBackwardScan(outerPlan(node));
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.cindex a753d6efa0..333d4ba1fb 100644--- a/src/backend/executor/execParallel.c+++ b/src/backend/executor/execParallel.c@@ -31,6 +31,7 @@
#include "executor/nodeForeignscan.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
+#include "executor/nodeIncrementalSort.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "executor/nodeSeqscan.h"
@@ -282,6 +283,10 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e)
/* even when not parallel-aware, for EXPLAIN ANALYZE */
ExecSortEstimate((SortState *) planstate, e->pcxt);
break;
+ case T_IncrementalSortState:+ /* even when not parallel-aware, for EXPLAIN ANALYZE */+ ExecIncrementalSortEstimate((IncrementalSortState *) planstate, e->pcxt);+ break;
default:
break;
@@ -495,6 +500,10 @@ ExecParallelInitializeDSM(PlanState *planstate,
/* even when not parallel-aware, for EXPLAIN ANALYZE */
ExecSortInitializeDSM((SortState *) planstate, d->pcxt);
break;
+ case T_IncrementalSortState:+ /* even when not parallel-aware, for EXPLAIN ANALYZE */+ ExecIncrementalSortInitializeDSM((IncrementalSortState *) planstate, d->pcxt);+ break;
default:
break;
@@ -957,6 +966,7 @@ ExecParallelReInitializeDSM(PlanState *planstate,
break;
case T_HashState:
case T_SortState:
+ case T_IncrementalSortState:
/* these nodes have DSM state, but no reinitialization is required */
break;
@@ -1017,6 +1027,9 @@ ExecParallelRetrieveInstrumentation(PlanState *planstate,
case T_SortState:
ExecSortRetrieveInstrumentation((SortState *) planstate);
break;
+ case T_IncrementalSortState:+ ExecIncrementalSortRetrieveInstrumentation((IncrementalSortState *) planstate);+ break;
case T_HashState:
ExecHashRetrieveInstrumentation((HashState *) planstate);
break;
@@ -1303,6 +1316,11 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt)
/* even when not parallel-aware, for EXPLAIN ANALYZE */
ExecSortInitializeWorker((SortState *) planstate, pwcxt);
break;
+ case T_IncrementalSortState:+ /* even when not parallel-aware, for EXPLAIN ANALYZE */+ ExecIncrementalSortInitializeWorker((IncrementalSortState *) planstate,+ pwcxt);+ break;
default:
break;
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.cindex 7b2e84f402..d15a86a706 100644--- a/src/backend/executor/execProcnode.c+++ b/src/backend/executor/execProcnode.c@@ -88,6 +88,7 @@
#include "executor/nodeGroup.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
+#include "executor/nodeIncrementalSort.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "executor/nodeLimit.h"
@@ -313,6 +314,11 @@ ExecInitNode(Plan *node, EState *estate, int eflags)
estate, eflags);
break;
+ case T_IncrementalSort:+ result = (PlanState *) ExecInitIncrementalSort((IncrementalSort *) node,+ estate, eflags);+ break;+
case T_Group:
result = (PlanState *) ExecInitGroup((Group *) node,
estate, eflags);
@@ -693,6 +699,10 @@ ExecEndNode(PlanState *node)
ExecEndSort((SortState *) node);
break;
+ case T_IncrementalSortState:+ ExecEndIncrementalSort((IncrementalSortState *) node);+ break;+
case T_GroupState:
ExecEndGroup((GroupState *) node);
break;
@@ -839,6 +849,29 @@ ExecSetTupleBound(int64 tuples_needed, PlanState *child_node)
sortState->bound = tuples_needed;
}
}
+ else if (IsA(child_node, IncrementalSortState))+ {+ /*+ * If it is a Sort node, notify it that it can use bounded sort.+ *+ * Note: it is the responsibility of nodeSort.c to react properly to+ * changes of these parameters. If we ever redesign this, it'd be a+ * good idea to integrate this signaling with the parameter-change+ * mechanism.+ */+ IncrementalSortState *sortState = (IncrementalSortState *) child_node;++ if (tuples_needed < 0)+ {+ /* make sure flag gets reset if needed upon rescan */+ sortState->bounded = false;+ }+ else+ {+ sortState->bounded = true;+ sortState->bound = tuples_needed;+ }+ }
else if (IsA(child_node, AppendState))
{
/*
diff --git a/src/backend/executor/nodeIncrementalSort.c b/src/backend/executor/nodeIncrementalSort.cnew file mode 100644index 0000000000..32ce05a63c--- /dev/null+++ b/src/backend/executor/nodeIncrementalSort.c@@ -0,0 +1,1201 @@+/*-------------------------------------------------------------------------+ *+ * nodeIncrementalSort.c+ * Routines to handle incremental sorting of relations.+ *+ * DESCRIPTION+ *+ * Incremental sort is an optimized variant of multikey sort for cases+ * when the input is already sorted by a prefix of the sort keys. For+ * example when a sort by (key1, key2 ... keyN) is requested, and the+ * input is already sorted by (key1, key2 ... keyM), M < N, we can+ * divide the input into groups where keys (key1, ... keyM) are equal,+ * and only sort on the remaining columns.+ *+ * Consider the following example. We have input tuples consisting of+ * two integers (X, Y) already presorted by X, while it's required to+ * sort them by both X and Y. Let input tuples be following.+ *+ * (1, 5)+ * (1, 2)+ * (2, 9)+ * (2, 1)+ * (2, 5)+ * (3, 3)+ * (3, 7)+ *+ * Incremental sort algorithm would split the input into the following+ * groups, which have equal X, and then sort them by Y individually:+ *+ * (1, 5) (1, 2)+ * (2, 9) (2, 1) (2, 5)+ * (3, 3) (3, 7)+ *+ * After sorting these groups and putting them altogether, we would get+ * the following result which is sorted by X and Y, as requested:+ *+ * (1, 2)+ * (1, 5)+ * (2, 1)+ * (2, 5)+ * (2, 9)+ * (3, 3)+ * (3, 7)+ *+ * Incremental sort may be more efficient than plain sort, particularly+ * on large datasets, as it reduces the amount of data to sort at once,+ * making it more likely it fits into work_mem (eliminating the need to+ * spill to disk). But the main advantage of incremental sort is that+ * it can start producing rows early, before sorting the whole dataset,+ * which is a significant benefit especially for queries with LIMIT.+ *+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ *+ * IDENTIFICATION+ * src/backend/executor/nodeIncrementalSort.c+ *+ *-------------------------------------------------------------------------+ */++#include "postgres.h"++#include "access/htup_details.h"+#include "executor/execdebug.h"+#include "executor/nodeIncrementalSort.h"+#include "miscadmin.h"+#include "utils/lsyscache.h"+#include "utils/tuplesort.h"++static void+instrumentSortedGroup(PlanState *pstate, IncrementalSortGroupInfo *groupInfo,+ Tuplesortstate *sortState)+{+ IncrementalSortState *node = castNode(IncrementalSortState, pstate);+ TuplesortInstrumentation sort_instr;++ groupInfo->groupCount++;++ tuplesort_get_stats(sortState, &sort_instr);+ switch (sort_instr.spaceType)+ {+ case SORT_SPACE_TYPE_DISK:+ groupInfo->totalDiskSpaceUsed += sort_instr.spaceUsed;+ if (sort_instr.spaceUsed > groupInfo->maxDiskSpaceUsed)+ groupInfo->maxDiskSpaceUsed = sort_instr.spaceUsed;++ break;+ case SORT_SPACE_TYPE_MEMORY:+ groupInfo->totalMemorySpaceUsed += sort_instr.spaceUsed;+ if (sort_instr.spaceUsed > groupInfo->maxMemorySpaceUsed)+ groupInfo->maxMemorySpaceUsed = sort_instr.spaceUsed;++ break;+ }++ if (!list_member_int(groupInfo->sortMethods, sort_instr.sortMethod))+ groupInfo->sortMethods = lappend_int(groupInfo->sortMethods,+ sort_instr.sortMethod);++ /* Record shared stats if we're a parallel worker. */+ if (node->shared_info && node->am_worker)+ {+ Assert(IsParallelWorker());+ Assert(ParallelWorkerNumber <= node->shared_info->num_workers);++ memcpy(&node->shared_info->sinfo[ParallelWorkerNumber],+ &node->incsort_info, sizeof(IncrementalSortInfo));+ }+}++/*+ * Prepare information for presorted_keys comparison.+ */+static void+preparePresortedCols(IncrementalSortState *node)+{+ IncrementalSort *plannode = castNode(IncrementalSort, node->ss.ps.plan);++ node->presorted_keys =+ (PresortedKeyData *) palloc(plannode->presortedCols *+ sizeof(PresortedKeyData));++ /* Pre-cache comparison functions for each pre-sorted key. */+ for (int i = 0; i < plannode->presortedCols; i++)+ {+ Oid equalityOp,+ equalityFunc;+ PresortedKeyData *key;++ key = &node->presorted_keys[i];+ key->attno = plannode->sort.sortColIdx[i];++ equalityOp = get_equality_op_for_ordering_op(plannode->sort.sortOperators[i],+ NULL);+ if (!OidIsValid(equalityOp))+ elog(ERROR, "missing equality operator for ordering operator %u",+ plannode->sort.sortOperators[i]);++ equalityFunc = get_opcode(equalityOp);+ if (!OidIsValid(equalityFunc))+ elog(ERROR, "missing function for operator %u", equalityOp);++ /* Lookup the comparison function */+ fmgr_info_cxt(equalityFunc, &key->flinfo, CurrentMemoryContext);++ /* We can initialize the callinfo just once and re-use it */+ key->fcinfo = palloc0(SizeForFunctionCallInfo(2));+ InitFunctionCallInfoData(*key->fcinfo, &key->flinfo, 2,+ plannode->sort.collations[i], NULL, NULL);+ key->fcinfo->args[0].isnull = false;+ key->fcinfo->args[1].isnull = false;+ }+}++/*+ * Check whether a given tuple belongs to the current sort group.+ *+ * We do this by comparing its first 'presortedCols' column values to+ * the pivot tuple of the current group.+ */+static bool+isCurrentGroup(IncrementalSortState *node, TupleTableSlot *pivot, TupleTableSlot *tuple)+{+ int presortedCols;++ presortedCols = castNode(IncrementalSort, node->ss.ps.plan)->presortedCols;++ /*+ * That the input is sorted by keys * (0, ... n) implies that the tail+ * keys are more likely to change. Therefore we do our comparison starting+ * from the last pre-sorted column to optimize for early detection of+ * inequality and minimizing the number of function calls..+ */+ for (int i = presortedCols - 1; i >= 0; i--)+ {+ Datum datumA,+ datumB,+ result;+ bool isnullA,+ isnullB;+ AttrNumber attno = node->presorted_keys[i].attno;+ PresortedKeyData *key;++ datumA = slot_getattr(pivot, attno, &isnullA);+ datumB = slot_getattr(tuple, attno, &isnullB);++ /* Special case for NULL-vs-NULL, else use standard comparison */+ if (isnullA || isnullB)+ {+ if (isnullA == isnullB)+ continue;+ else+ return false;+ }++ key = &node->presorted_keys[i];++ key->fcinfo->args[0].value = datumA;+ key->fcinfo->args[1].value = datumB;++ /* just for paranoia's sake, we reset isnull each time */+ key->fcinfo->isnull = false;++ result = FunctionCallInvoke(key->fcinfo);++ /* Check for null result, since caller is clearly not expecting one */+ if (key->fcinfo->isnull)+ elog(ERROR, "function %u returned NULL", key->flinfo.fn_oid);++ if (!DatumGetBool(result))+ return false;+ }+ return true;+}++/*+ * Switch to presorted prefix mode.+ *+ * When we determine that we've likely encountered a large batch of tuples all+ * having the same presorted prefix values, we want to optimize tuplesort by+ * only sorting on unsorted suffix keys.+ *+ * The problem is that we've already accumulated several tuples in another+ * tuplesort configured to sort by all columns (assuming that there may be+ * more than one prefix key group). So to switch to presorted prefix mode we+ * have to go back an look at all the tuples we've already accumulated and+ * verify they're all part of the same prefix key group before sorting them+ * solely by unsorted suffix keys.+ *+ * While it's likely that all already fetch tuples are all part of a single+ * prefix group, we also have to handle the possibility that there is at least+ * one different prefix key group before the large prefix key group.+ */+static void+switchToPresortedPrefixMode(PlanState *pstate)+{+ IncrementalSortState *node = castNode(IncrementalSortState, pstate);+ ScanDirection dir;+ int64 nTuples = 0;+ bool lastTuple = false;+ bool firstTuple = true;+ TupleDesc tupDesc;+ PlanState *outerNode;+ IncrementalSort *plannode = castNode(IncrementalSort, node->ss.ps.plan);++ dir = node->ss.ps.state->es_direction;+ outerNode = outerPlanState(node);+ tupDesc = ExecGetResultType(outerNode);++ if (node->prefixsort_state == NULL)+ {+ Tuplesortstate *prefixsort_state;+ int presortedCols = plannode->presortedCols;++ /*+ * Optimize the sort by assuming the prefix columns are all equal and+ * thus we only need to sort by any remaining columns.+ */+ prefixsort_state = tuplesort_begin_heap(tupDesc,+ plannode->sort.numCols - presortedCols,+ &(plannode->sort.sortColIdx[presortedCols]),+ &(plannode->sort.sortOperators[presortedCols]),+ &(plannode->sort.collations[presortedCols]),+ &(plannode->sort.nullsFirst[presortedCols]),+ work_mem,+ NULL,+ false);+ node->prefixsort_state = prefixsort_state;+ }+ else+ {+ /* Next group of presorted data */+ tuplesort_reset(node->prefixsort_state);+ }++ /*+ * If the current node has a bound, then it's reasonably likely that a+ * large prefix key group will benefit from bounded sort, so configure the+ * tuplesort to allow for that optimization.+ */+ if (node->bounded)+ {+ SO1_printf("Setting bound on presorted prefix tuplesort to: %ld\n",+ node->bound - node->bound_Done);+ tuplesort_set_bound(node->prefixsort_state,+ node->bound - node->bound_Done);+ }++ for (;;)+ {+ lastTuple = node->n_fullsort_remaining - nTuples == 1;++ /*+ * When we encounter multiple prefix key groups inside the full sort+ * tuplesort we have to carry over the last read tuple into the next+ * batch.+ */+ if (firstTuple && !TupIsNull(node->transfer_tuple))+ {+ tuplesort_puttupleslot(node->prefixsort_state, node->transfer_tuple);+ nTuples++;++ /* The carried over tuple is our new group pivot tuple. */+ ExecCopySlot(node->group_pivot, node->transfer_tuple);+ }+ else+ {+ tuplesort_gettupleslot(node->fullsort_state,+ ScanDirectionIsForward(dir),+ false, node->transfer_tuple, NULL);++ /*+ * If this is our first time through the loop, then we need to+ * save the first tuple we get as our new group pivot.+ */+ if (TupIsNull(node->group_pivot))+ ExecCopySlot(node->group_pivot, node->transfer_tuple);++ if (isCurrentGroup(node, node->group_pivot, node->transfer_tuple))+ {+ tuplesort_puttupleslot(node->prefixsort_state, node->transfer_tuple);+ nTuples++;+ }+ else+ {+ /*+ * The tuple isn't part of the current batch so we need to+ * carry it over into the next set up tuples we transfer out+ * of the full sort tuplesort into the presorted prefix+ * tuplesort. We don't actually have to do anything special to+ * save the tuple since we've already loaded it into the+ * node->transfer_tuple slot, and, even though that slot+ * points to memory inside the full sort tuplesort, we can't+ * reset that tuplesort anyway until we've fully transferred+ * out of its tuples, so this reference is safe. We do need to+ * reset the group pivot tuple though since we've finished the+ * current prefix key group.+ */+ ExecClearTuple(node->group_pivot);+ break;+ }+ }++ firstTuple = false;++ if (lastTuple)++ /*+ * We retain the current group pivot tuple since we haven't yet+ * found the end of the current prefix key group.+ */+ break;+ }++ /*+ * Track how many tuples remain in the full sort batch so that we know if+ * we need to sort multiple prefix key groups before processing tuples+ * remaining in the large single prefix key group we think we've+ * encountered.+ */+ SO1_printf("Moving %ld tuples to presorted prefix tuplesort\n", nTuples);+ node->n_fullsort_remaining -= nTuples;+ SO1_printf("Setting n_fullsort_remaining to %ld\n", node->n_fullsort_remaining);++ if (lastTuple)+ {+ /*+ * We've confirmed that all tuples remaining in the full sort batch is+ * in the same prefix key group and moved all of those tuples into the+ * presorted prefix tuplesort. Now we can save our pivot comparison+ * tuple and continue fetching tuples from the outer execution node to+ * load into the presorted prefix tuplesort.+ */+ ExecCopySlot(node->group_pivot, node->transfer_tuple);+ SO_printf("Setting execution_status to INCSORT_LOADPREFIXSORT (switchToPresortedPrefixMode)\n");+ node->execution_status = INCSORT_LOADPREFIXSORT;++ /*+ * Make sure we clear the transfer tuple slot so that next time we+ * encounter a large prefix key group we don't incorrectly assume we+ * have a tuple carried over from the previous group.+ */+ ExecClearTuple(node->transfer_tuple);+ }+ else+ {+ /*+ * We finished a group but didn't consume all of the tuples from the+ * full sort batch sorter, so we'll sort this batch, let the inner+ * node read out all of those tuples, and then come back around to+ * find another batch.+ */+ SO1_printf("Sorting presorted prefix tuplesort with %ld tuples\n", nTuples);+ tuplesort_performsort(node->prefixsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.prefixsortGroupInfo,+ node->prefixsort_state);++ if (node->bounded)+ {+ /*+ * If the current node has a bound, and we've already sorted n+ * tuples, then the functional bound remaining is (original bound+ * - n), so store the current number of processed tuples for use+ * in configuring sorting bound.+ */+ SO2_printf("Changing bound_Done from %ld to %ld\n",+ Min(node->bound, node->bound_Done + nTuples), node->bound_Done);+ node->bound_Done = Min(node->bound, node->bound_Done + nTuples);+ }++ SO_printf("Setting execution_status to INCSORT_READPREFIXSORT (switchToPresortedPrefixMode)\n");+ node->execution_status = INCSORT_READPREFIXSORT;+ }+}++/*+ * Sorting many small groups with tuplesort is inefficient. In order to+ * cope with this problem we don't start a new group until the current one+ * contains at least DEFAULT_MIN_GROUP_SIZE tuples (unfortunately this also+ * means we can't assume small groups of tuples all have the same prefix keys.)+ * When we have a bound that's less than DEFAULT_MIN_GROUP_SIZE we start looking+ * for the new group as soon as we've met our bound to avoid fetching more+ * tuples than we absolutely have to fetch.+ */+#define DEFAULT_MIN_GROUP_SIZE 32++/*+ * While we've optimized for small prefix key groups by not starting our prefix+ * key comparisons until we've reached a minimum number of tuples, we don't want+ * that optimization to cause us to lose out on the benefits of being able to+ * assume a large group of tuples is fully presorted by its prefix keys.+ * Therefore we use the DEFAULT_MAX_FULL_SORT_GROUP_SIZE cutoff as a heuristic+ * for determining when we believe we've encountered a large group, and, if we+ * get to that point without finding a new prefix key group we transition to+ * presorted prefix key mode.+ */+#define DEFAULT_MAX_FULL_SORT_GROUP_SIZE (2 * DEFAULT_MIN_GROUP_SIZE)++/* ----------------------------------------------------------------+ * ExecIncrementalSort+ *+ * Assuming that outer subtree returns tuple presorted by some prefix+ * of target sort columns, performs incremental sort. The implemented+ * algorithm operates in two different modes:+ * - Fetching a minimum number of tuples without checking prefix key+ * group membership and sorting on all columns when safe.+ * - Fetching all tuples for a single prefix key group and sorting on+ * solely the unsorted columns.+ * We always begin in the first mode, and employ a heuristic to switch+ * into the second mode if we believe it's beneficial.+ *+ * Sorting incrementally can potentially use less memory, avoid fetching+ * and sorting all tuples in the the dataset, and begin returning tuples+ * before the entire result set is available.+ *+ * The hybrid mode approach allows us to optimize for both very small+ * groups (where the overhead of a new tuplesort is high) and very large+ * groups (where we can lower cost by not having to sort on already sorted+ * columns), albeit at some extra cost while switching between modes.+ *+ * Conditions:+ * -- none.+ *+ * Initial States:+ * -- the outer child is prepared to return the first tuple.+ * ----------------------------------------------------------------+ */+static TupleTableSlot *+ExecIncrementalSort(PlanState *pstate)+{+ IncrementalSortState *node = castNode(IncrementalSortState, pstate);+ EState *estate;+ ScanDirection dir;+ Tuplesortstate *read_sortstate;+ Tuplesortstate *fullsort_state;+ TupleTableSlot *slot;+ IncrementalSort *plannode = (IncrementalSort *) node->ss.ps.plan;+ PlanState *outerNode;+ TupleDesc tupDesc;+ int64 nTuples = 0;+ int64 minGroupSize;++ CHECK_FOR_INTERRUPTS();++ estate = node->ss.ps.state;+ dir = estate->es_direction;+ fullsort_state = node->fullsort_state;++ if (node->execution_status == INCSORT_READFULLSORT+ || node->execution_status == INCSORT_READPREFIXSORT)+ {+ /*+ * Return next tuple from the current sorted group set if available.+ */+ read_sortstate = node->execution_status == INCSORT_READFULLSORT ?+ fullsort_state : node->prefixsort_state;+ slot = node->ss.ps.ps_ResultTupleSlot;++ /*+ * We have to populate the slot from the tuplesort before checking+ * outerNodeDone because it will NULL the slot if no more tuples+ * remain. If the tuplesort is empty, but we don't have any more+ * tuples avaialable for sort from the outer node, then outerNodeDone+ * will have been set so we'll return the empty slot to the caller.+ */+ if (tuplesort_gettupleslot(read_sortstate, ScanDirectionIsForward(dir),+ false, slot, NULL) || node->outerNodeDone)++ /*+ * TODO: there isn't a good test case for the node->outerNodeDone+ * case directly, but lots of other stuff fails if it's not there.+ * If the outer node will fail when trying to fetch too many+ * tuples, then things break if this check isn't here.+ */+ return slot;+ else if (node->n_fullsort_remaining > 0)+ {+ /*+ * When we transition to presorted prefix mode, we might have+ * accumulated at least one additional prefix key group in the+ * full sort tuplesort. The first call to+ * switchToPresortedPrefixMode() will have pulled the first one of+ * those groups out, and we've returned those tuples to the inner+ * node, but if we tuples remaining in that tuplesort (i.e.,+ * n_fullsort_remaining > 0) at this point we need to do that+ * again.+ */+ SO1_printf("Re-calling switchToPresortedPrefixMode() because n_fullsort_remaining is > 0 (%ld)\n",+ node->n_fullsort_remaining);+ switchToPresortedPrefixMode(pstate);+ }+ else+ {+ /*+ * If we don't have any already sorted tuples to read, and we're+ * not in the middle of transitioning into presorted prefix sort+ * mode, then it's time to start the process all over again by+ * building new full sort group.+ */+ SO_printf("Setting execution_status to INCSORT_LOADFULLSORT (n_fullsort_remaining > 0)\n");+ node->execution_status = INCSORT_LOADFULLSORT;+ }+ }++ /*+ * Want to scan subplan in the forward direction while creating the sorted+ * data.+ */+ estate->es_direction = ForwardScanDirection;++ outerNode = outerPlanState(node);+ tupDesc = ExecGetResultType(outerNode);++ if (node->execution_status == INCSORT_LOADFULLSORT)+ {+ /*+ * Initialize tuplesort module (only needed before the first group).+ */+ if (fullsort_state == NULL)+ {+ /*+ * Initialize presorted column support structures for+ * isCurrentGroup().+ */+ preparePresortedCols(node);++ /*+ * Since we optimize small prefix key groups by accumulating a+ * minimum number of tuples before sorting, we can't assume that a+ * group of tuples all have the same prefix key values. Hence we+ * setup the full sort tuplesort to sort by all requested sort+ * columns.+ */+ fullsort_state = tuplesort_begin_heap(tupDesc,+ plannode->sort.numCols,+ plannode->sort.sortColIdx,+ plannode->sort.sortOperators,+ plannode->sort.collations,+ plannode->sort.nullsFirst,+ work_mem,+ NULL,+ false);+ node->fullsort_state = fullsort_state;+ }+ else+ {+ /* Reset sort for a new prefix key group. */+ tuplesort_reset(fullsort_state);+ }++ /*+ * Calculate the remaining tuples left if the bounded and configure+ * both bounded sort and the minimum group size accordingly.+ */+ if (node->bounded)+ {+ int64 currentBound = node->bound - node->bound_Done;++ /*+ * Bounded sort isn't likely to be a useful optimization for full+ * sort mode since we limit full sort mode to a relatively small+ * number of tuples and tuplesort doesn't switch over to top-n+ * heap sort anyway unless it hits (2 * bound) tuples.+ */+ if (currentBound < DEFAULT_MIN_GROUP_SIZE)+ tuplesort_set_bound(fullsort_state, currentBound);++ minGroupSize = Min(DEFAULT_MIN_GROUP_SIZE, currentBound);+ }+ else+ minGroupSize = DEFAULT_MIN_GROUP_SIZE;++ /*+ * Because we have to read the next tuple to find out that we've+ * encountered a new prefix key group on subsequent groups we have to+ * carry over that extra tuple and add it to the new group's sort+ * here.+ */+ if (!TupIsNull(node->group_pivot))+ {+ tuplesort_puttupleslot(fullsort_state, node->group_pivot);+ nTuples++;++ /*+ * We're in full sort mode accumulating a minimum number of tuples+ * and not checking for prefix key equality yet, so we can't+ * assume the group pivot tuple will reamin the same -- unless+ * we're using a minimum group size of 1, in which case the pivot+ * is obviously still the pviot.+ */+ if (nTuples != minGroupSize)+ ExecClearTuple(node->group_pivot);+ }++ for (;;)+ {+ /*+ * TODO: do we need to check for interrupts inside these loops or+ * will the outer node handle that?+ */++ slot = ExecProcNode(outerNode);++ /*+ * When the outer node can't provide us any more tuples, then we+ * can sort the current group and return those tuples.+ */+ if (TupIsNull(slot))+ {+ node->outerNodeDone = true;++ SO1_printf("Sorting fullsort with %ld tuples\n", nTuples);+ tuplesort_performsort(fullsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.fullsortGroupInfo,+ fullsort_state);++ SO_printf("Setting execution_status to INCSORT_READFULLSORT (final tuple)\n");+ node->execution_status = INCSORT_READFULLSORT;+ break;+ }++ /* Accumulate the next group of presorted tuples. */+ if (nTuples < minGroupSize)+ {+ /*+ * If we have yet hit our target minimum group size, then+ * don't both with checking for inclusion in the current+ * prefix group since a large number of very tiny sorts is+ * inefficient.+ */+ tuplesort_puttupleslot(fullsort_state, slot);+ nTuples++;++ /* Keep the last tuple of our minimal group as a pivot. */+ if (nTuples == minGroupSize)+ ExecCopySlot(node->group_pivot, slot);+ }+ else+ {+ /*+ * Once we've accumulated a minimum number of tuples, we start+ * checking for a new prefix key group. Only after we find+ * changed prefix keys can we guarantee sort stability of the+ * tuples we've already accumulated.+ */+ if (isCurrentGroup(node, node->group_pivot, slot))+ {+ /*+ * As long as the prefix keys match the pivot tuple then+ * load the tuple into the tuplesort.+ */+ tuplesort_puttupleslot(fullsort_state, slot);+ nTuples++;+ }+ else+ {+ /*+ * Since the tuple we fetched isn't part of the current+ * prefix key group we can't sort it as part of this sort+ * group. Instead we need to carry it over to the next+ * group. We use the group_pivot slot as a temp container+ * for that purpose even though we won't actually treat it+ * as a group pivot.+ */+ ExecCopySlot(node->group_pivot, slot);++ if (node->bounded)+ {+ /*+ * If the current node has a bound, and we've already+ * sorted n tuples, then the functional bound+ * remaining is (original bound - n), so store the+ * current number of processed tuples for use in+ * configuring sorting bound.+ */+ SO2_printf("Changing bound_Done from %ld to %ld\n",+ node->bound_Done,+ Min(node->bound, node->bound_Done + nTuples));+ node->bound_Done = Min(node->bound, node->bound_Done + nTuples);+ }++ /*+ * Once we find changed prefix keys we can complete the+ * sort and begin reading out the sorted tuples.+ */+ SO1_printf("Sorting fullsort tuplesort with %ld tuples\n",+ nTuples);+ tuplesort_performsort(fullsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.fullsortGroupInfo,+ fullsort_state);++ SO_printf("Setting execution_status to INCSORT_READFULLSORT (found end of group)\n");+ node->execution_status = INCSORT_READFULLSORT;+ break;+ }+ }++ /*+ * Once we've processed DEFAULT_MAX_FULL_SORT_GROUP_SIZE tuples+ * then we make the assumption that it's likely that we've found a+ * large group of tuples having a single prefix key (as long as+ * the last tuple didn't shift us into reading from the full sort+ * mode tuplesort).+ */+ if (nTuples > DEFAULT_MAX_FULL_SORT_GROUP_SIZE &&+ node->execution_status != INCSORT_READFULLSORT)+ {+ /*+ * The group pivot we have stored has already been put into+ * the tuplesort; we don't want to carry it over.+ */+ ExecClearTuple(node->group_pivot);++ /*+ * Unfortunately the tuplesort API doesn't include a way to+ * retrieve tuples unless a sort has been performed, so we+ * perform the sort even though we could just as easily rely+ * on FIFO retrieval semantics when transferring them to the+ * presorted prefix tuplesort.+ */+ SO1_printf("Sorting fullsort tuplesort with %ld tuples\n", nTuples);+ tuplesort_performsort(fullsort_state);+ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.fullsortGroupInfo,+ fullsort_state);++ /*+ * If the full sort tuplesort happened to switch into top-n+ * heapsort mode then we will only be able to retrieve+ * currentBound tuples (since the tuplesort will have only+ * retained the top-n tuples). This is safe even though we+ * haven't yet completed fetching the current prefix key group+ * because the tuples we've "lost" already sorted "below" the+ * retained ones, and we're already contractually guaranteed+ * to not need any more than the currentBount tuples.+ */+ if (tuplesort_used_bound(node->fullsort_state))+ {+ int64 currentBound = node->bound - node->bound_Done;++ SO2_printf("Read %ld tuples, but setting to %ld because we used bounded sort\n",+ nTuples, Min(currentBound, nTuples));+ nTuples = Min(currentBound, nTuples);+ }++ SO1_printf("Setting n_fullsort_remaining to %ld and calling switchToPresortedPrefixMode()\n",+ nTuples);++ /*+ * Track the number of tuples we need to move from the+ * fullsort to presorted prefix sort (we might have multiple+ * prefix key groups, so we need a way to see if we've+ * actually finished).+ */+ node->n_fullsort_remaining = nTuples;++ /* Transition the tuples to the presorted prefix tuplesort. */+ switchToPresortedPrefixMode(pstate);++ /*+ * Since we know we had tuples to move to the presorted prefix+ * tuplesort, we know that unless that transition has verified+ * that all tuples belonged to the same prefix key group (in+ * which case we can go straight to continuing to load tuples+ * into that tuplesort), we should have a tuple to return+ * here.+ *+ * Either way, the appropriate execution status should have+ * been set by switchToPresortedPrefixMode(), so we can drop+ * out of the loop here and let the appropriate path kick in.+ */+ break;+ }+ }+ }++ if (node->execution_status == INCSORT_LOADPREFIXSORT)+ {+ /*+ * Since we only enter this state after determining that all remaining+ * tuples in the full sort tuplesort have the same prefix, we've+ * already established a current group pivot tuple (but wasn't carried+ * over; it's already been put into the prefix sort tuplesort).+ */+ Assert(!TupIsNull(node->group_pivot));++ for (;;)+ {+ slot = ExecProcNode(outerNode);++ /* Check to see if there are no more tuples to fetch. */+ if (TupIsNull(slot))+ {+ node->outerNodeDone = true;+ break;+ }++ if (isCurrentGroup(node, node->group_pivot, slot))+ {+ /*+ * Fetch tuples and put them into the presorted prefix+ * tuplesort until we find changed prefix keys. Only then can+ * we guarantee sort stability of the tuples we've already+ * accumulated.+ */+ tuplesort_puttupleslot(node->prefixsort_state, slot);+ nTuples++;+ }+ else+ {+ /*+ * Since the tuple we fetched isn't part of the current prefix+ * key group we can't sort it as part of this sort group.+ * Instead we need to carry it over to the next group. We use+ * the group_pivot slot as a temp container for that purpose+ * even though we won't actually treat it as a group pivot.+ */+ ExecCopySlot(node->group_pivot, slot);+ break;+ }+ }++ /* Perform the sort and return the tuples to the inner plan nodes. */+ SO1_printf("Sorting presorted prefix tuplesort with >= %ld tuples\n", nTuples);+ tuplesort_performsort(node->prefixsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.prefixsortGroupInfo,+ node->prefixsort_state);++ SO_printf("Setting execution_status to INCSORT_READPREFIXSORT (found end of group)\n");+ node->execution_status = INCSORT_READPREFIXSORT;++ if (node->bounded)+ {+ /*+ * If the current node has a bound, and we've already sorted n+ * tuples, then the functional bound remaining is (original bound+ * - n), so store the current number of processed tuples for use+ * in configuring sorting bound.+ */+ SO2_printf("Changing bound_Done from %ld to %ld\n",+ node->bound_Done,+ Min(node->bound, node->bound_Done + nTuples));+ node->bound_Done = Min(node->bound, node->bound_Done + nTuples);+ }+ }++ /* Restore to user specified direction. */+ estate->es_direction = dir;++ /*+ * Remember that we've begun our scan and sort so we know how to handle+ * rescan.+ */+ node->sort_Done = true;++ /*+ * Get the first or next tuple from tuplesort. Returns NULL if no more+ * tuples.+ */+ read_sortstate = node->execution_status == INCSORT_READFULLSORT ?+ fullsort_state : node->prefixsort_state;+ slot = node->ss.ps.ps_ResultTupleSlot;+ (void) tuplesort_gettupleslot(read_sortstate, ScanDirectionIsForward(dir),+ false, slot, NULL);+ return slot;+}++/* ----------------------------------------------------------------+ * ExecInitIncrementalSort+ *+ * Creates the run-time state information for the sort node+ * produced by the planner and initializes its outer subtree.+ * ----------------------------------------------------------------+ */+IncrementalSortState *+ExecInitIncrementalSort(IncrementalSort *node, EState *estate, int eflags)+{+ IncrementalSortState *incrsortstate;++ SO_printf("ExecInitIncrementalSort: initializing sort node\n");++ /*+ * Incremental sort can't be used with either EXEC_FLAG_REWIND,+ * EXEC_FLAG_BACKWARD or EXEC_FLAG_MARK, because we hold only current+ * bucket in tuplesortstate.+ */+ Assert((eflags & (EXEC_FLAG_REWIND |+ EXEC_FLAG_BACKWARD |+ EXEC_FLAG_MARK)) == 0);++ /*+ * create state structure+ */+ incrsortstate = makeNode(IncrementalSortState);+ incrsortstate->ss.ps.plan = (Plan *) node;+ incrsortstate->ss.ps.state = estate;+ incrsortstate->ss.ps.ExecProcNode = ExecIncrementalSort;++ incrsortstate->bounded = false;+ incrsortstate->sort_Done = false;+ incrsortstate->outerNodeDone = false;+ incrsortstate->fullsort_state = NULL;+ incrsortstate->prefixsort_state = NULL;+ incrsortstate->group_pivot = NULL;+ incrsortstate->transfer_tuple = NULL;+ incrsortstate->n_fullsort_remaining = 0;+ incrsortstate->bound_Done = 0;+ incrsortstate->presorted_keys = NULL;++ if (incrsortstate->ss.ps.instrument != NULL)+ {+ IncrementalSortGroupInfo *fullsortGroupInfo =+ &incrsortstate->incsort_info.fullsortGroupInfo;+ IncrementalSortGroupInfo *prefixsortGroupInfo =+ &incrsortstate->incsort_info.prefixsortGroupInfo;++ fullsortGroupInfo->groupCount = 0;+ fullsortGroupInfo->maxDiskSpaceUsed = 0;+ fullsortGroupInfo->totalDiskSpaceUsed = 0;+ fullsortGroupInfo->maxMemorySpaceUsed = 0;+ fullsortGroupInfo->totalMemorySpaceUsed = 0;+ fullsortGroupInfo->sortMethods = NIL;+ prefixsortGroupInfo->groupCount = 0;+ prefixsortGroupInfo->maxDiskSpaceUsed = 0;+ prefixsortGroupInfo->totalDiskSpaceUsed = 0;+ prefixsortGroupInfo->maxMemorySpaceUsed = 0;+ prefixsortGroupInfo->totalMemorySpaceUsed = 0;+ prefixsortGroupInfo->sortMethods = NIL;+ }++ /*+ * Miscellaneous initialization+ *+ * Sort nodes don't initialize their ExprContexts because they never call+ * ExecQual or ExecProject.+ */++ /*+ * initialize child nodes+ *+ * We shield the child node from the need to support REWIND, BACKWARD, or+ * MARK/RESTORE.+ */+ eflags &= ~(EXEC_FLAG_REWIND | EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK);++ outerPlanState(incrsortstate) = ExecInitNode(outerPlan(node), estate, eflags);++ /*+ * Initialize scan slot and type.+ */+ ExecCreateScanSlotFromOuterPlan(estate, &incrsortstate->ss, &TTSOpsMinimalTuple);++ /*+ * Initialize return slot and type. No need to initialize projection info+ * because this node doesn't do projections.+ */+ ExecInitResultTupleSlotTL(&incrsortstate->ss.ps, &TTSOpsMinimalTuple);+ incrsortstate->ss.ps.ps_ProjInfo = NULL;++ /* make standalone slot to store previous tuple from outer node */+ incrsortstate->group_pivot =+ MakeSingleTupleTableSlot(ExecGetResultType(outerPlanState(incrsortstate)),+ &TTSOpsMinimalTuple);+ incrsortstate->transfer_tuple =+ MakeSingleTupleTableSlot(ExecGetResultType(outerPlanState(incrsortstate)),+ &TTSOpsMinimalTuple);++ SO_printf("ExecInitIncrementalSort: sort node initialized\n");++ return incrsortstate;+}++/* ----------------------------------------------------------------+ * ExecEndIncrementalSort(node)+ * ----------------------------------------------------------------+ */+void+ExecEndIncrementalSort(IncrementalSortState *node)+{+ SO_printf("ExecEndIncrementalSort: shutting down sort node\n");++ /*+ * clean out the tuple table+ */+ ExecClearTuple(node->ss.ss_ScanTupleSlot);+ /* must drop pointer to sort result tuple */+ ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);+ /* must drop stanalone tuple slot from outer node */+ ExecDropSingleTupleTableSlot(node->group_pivot);+ ExecDropSingleTupleTableSlot(node->transfer_tuple);++ /*+ * Release tuplesort resources.+ */+ if (node->fullsort_state != NULL)+ {+ tuplesort_end(node->fullsort_state);+ node->fullsort_state = NULL;+ }+ if (node->prefixsort_state != NULL)+ {+ tuplesort_end(node->prefixsort_state);+ node->prefixsort_state = NULL;+ }++ /*+ * Shut down the subplan.+ */+ ExecEndNode(outerPlanState(node));++ SO_printf("ExecEndIncrementalSort: sort node shutdown\n");+}++void+ExecReScanIncrementalSort(IncrementalSortState *node)+{+ PlanState *outerPlan = outerPlanState(node);++ /*+ * XXX: This is suspect.+ *+ * If we haven't sorted yet, just return. If outerplan's chgParam is not+ * NULL then it will be re-scanned by ExecProcNode, else no reason to+ * re-scan it at all.+ */+ if (!node->sort_Done)+ return;++ /* must drop pointer to sort result tuple */+ ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);++ node->outerNodeDone = false;++ /*+ * If subnode is to be rescanned then we forget previous sort results; we+ * have to re-read the subplan and re-sort. Also must re-sort if the+ * bounded-sort parameters changed or we didn't select randomAccess.+ *+ * Otherwise we can just rewind and rescan the sorted output.+ */+ node->sort_Done = false;+ if (node->fullsort_state != NULL)+ {+ tuplesort_end(node->fullsort_state);+ node->fullsort_state = NULL;+ }+ if (node->prefixsort_state != NULL)+ {+ tuplesort_end(node->prefixsort_state);+ node->prefixsort_state = NULL;+ }+ node->bound_Done = 0;++ /*+ * if chgParam of subnode is not null then plan will be re-scanned by+ * first ExecProcNode.+ */+ if (outerPlan->chgParam == NULL)+ ExecReScan(outerPlan);+}++/* ----------------------------------------------------------------+ * Parallel Query Support+ * ----------------------------------------------------------------+ */++/* ----------------------------------------------------------------+ * ExecSortEstimate+ *+ * Estimate space required to propagate sort statistics.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortEstimate(IncrementalSortState *node, ParallelContext *pcxt)+{+ Size size;++ /* don't need this if not instrumenting or no workers */+ if (!node->ss.ps.instrument || pcxt->nworkers == 0)+ return;++ size = mul_size(pcxt->nworkers, sizeof(IncrementalSortInfo));+ size = add_size(size, offsetof(SharedIncrementalSortInfo, sinfo));+ shm_toc_estimate_chunk(&pcxt->estimator, size);+ shm_toc_estimate_keys(&pcxt->estimator, 1);+}++/* ----------------------------------------------------------------+ * ExecSortInitializeDSM+ *+ * Initialize DSM space for sort statistics.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortInitializeDSM(IncrementalSortState *node, ParallelContext *pcxt)+{+ Size size;++ /* don't need this if not instrumenting or no workers */+ if (!node->ss.ps.instrument || pcxt->nworkers == 0)+ return;++ size = offsetof(SharedIncrementalSortInfo, sinfo)+ + pcxt->nworkers * sizeof(IncrementalSortInfo);+ node->shared_info = shm_toc_allocate(pcxt->toc, size);+ /* ensure any unfilled slots will contain zeroes */+ memset(node->shared_info, 0, size);+ node->shared_info->num_workers = pcxt->nworkers;+ shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id,+ node->shared_info);+}++/* ----------------------------------------------------------------+ * ExecSortInitializeWorker+ *+ * Attach worker to DSM space for sort statistics.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortInitializeWorker(IncrementalSortState *node, ParallelWorkerContext *pwcxt)+{+ node->shared_info =+ shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, true);+ node->am_worker = true;+}++/* ----------------------------------------------------------------+ * ExecSortRetrieveInstrumentation+ *+ * Transfer sort statistics from DSM to private memory.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortRetrieveInstrumentation(IncrementalSortState *node)+{+ Size size;+ SharedIncrementalSortInfo *si;++ if (node->shared_info == NULL)+ return;++ size = offsetof(SharedIncrementalSortInfo, sinfo)+ + node->shared_info->num_workers * sizeof(IncrementalSortInfo);+ si = palloc(size);+ memcpy(si, node->shared_info, size);+ node->shared_info = si;+}diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.cindex 5d1debc196..9d2bfd7ed6 100644--- a/src/backend/executor/nodeSort.c+++ b/src/backend/executor/nodeSort.c@@ -93,7 +93,8 @@ ExecSort(PlanState *pstate)
plannode->collations,
plannode->nullsFirst,
work_mem,
- NULL, node->randomAccess);+ NULL,+ node->randomAccess);
if (node->bounded)
tuplesort_set_bound(tuplesortstate, node->bound);
node->tuplesortstate = (void *) tuplesortstate;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.cindex eaab97f753..e21f48327d 100644--- a/src/backend/nodes/copyfuncs.c+++ b/src/backend/nodes/copyfuncs.c@@ -927,6 +927,24 @@ _copyMaterial(const Material *from)
}
+/*+ * CopySortFields+ *+ * This function copies the fields of the Sort node. It is used by+ * all the copy functions for classes which inherit from Sort.+ */+static void+CopySortFields(const Sort *from, Sort *newnode)+{+ CopyPlanFields((const Plan *) from, (Plan *) newnode);++ COPY_SCALAR_FIELD(numCols);+ COPY_POINTER_FIELD(sortColIdx, from->numCols * sizeof(AttrNumber));+ COPY_POINTER_FIELD(sortOperators, from->numCols * sizeof(Oid));+ COPY_POINTER_FIELD(collations, from->numCols * sizeof(Oid));+ COPY_POINTER_FIELD(nullsFirst, from->numCols * sizeof(bool));+}+
/*
* _copySort
*/
@@ -938,13 +956,29 @@ _copySort(const Sort *from)
/*
* copy node superclass fields
*/
- CopyPlanFields((const Plan *) from, (Plan *) newnode);+ CopySortFields(from, newnode);- COPY_SCALAR_FIELD(numCols);- COPY_POINTER_FIELD(sortColIdx, from->numCols * sizeof(AttrNumber));- COPY_POINTER_FIELD(sortOperators, from->numCols * sizeof(Oid));- COPY_POINTER_FIELD(collations, from->numCols * sizeof(Oid));- COPY_POINTER_FIELD(nullsFirst, from->numCols * sizeof(bool));+ return newnode;+}+++/*+ * _copyIncrementalSort+ */+static IncrementalSort *+_copyIncrementalSort(const IncrementalSort *from)+{+ IncrementalSort *newnode = makeNode(IncrementalSort);++ /*+ * copy node superclass fields+ */+ CopySortFields((const Sort *) from, (Sort *) newnode);++ /*+ * copy remainder of node+ */+ COPY_SCALAR_FIELD(presortedCols);
return newnode;
}
@@ -4895,6 +4929,9 @@ copyObjectImpl(const void *from)
case T_Sort:
retval = _copySort(from);
break;
+ case T_IncrementalSort:+ retval = _copyIncrementalSort(from);+ break;
case T_Group:
retval = _copyGroup(from);
break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.cindex e084c3f069..6c83372c9f 100644--- a/src/backend/nodes/outfuncs.c+++ b/src/backend/nodes/outfuncs.c@@ -837,10 +837,8 @@ _outMaterial(StringInfo str, const Material *node)
}
static void
-_outSort(StringInfo str, const Sort *node)+_outSortInfo(StringInfo str, const Sort *node)
{
- WRITE_NODE_TYPE("SORT");-
_outPlanInfo(str, (const Plan *) node);
WRITE_INT_FIELD(numCols);
@@ -850,6 +848,24 @@ _outSort(StringInfo str, const Sort *node)
WRITE_BOOL_ARRAY(nullsFirst, node->numCols);
}
+static void+_outSort(StringInfo str, const Sort *node)+{+ WRITE_NODE_TYPE("SORT");++ _outSortInfo(str, node);+}++static void+_outIncrementalSort(StringInfo str, const IncrementalSort *node)+{+ WRITE_NODE_TYPE("INCREMENTALSORT");++ _outSortInfo(str, (const Sort *) node);++ WRITE_INT_FIELD(presortedCols);+}+
static void
_outUnique(StringInfo str, const Unique *node)
{
@@ -3783,6 +3799,9 @@ outNode(StringInfo str, const void *obj)
case T_Sort:
_outSort(str, obj);
break;
+ case T_IncrementalSort:+ _outIncrementalSort(str, obj);+ break;
case T_Unique:
_outUnique(str, obj);
break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.cindex d5b23a3479..c5bbbf459e 100644--- a/src/backend/nodes/readfuncs.c+++ b/src/backend/nodes/readfuncs.c@@ -2150,12 +2150,13 @@ _readMaterial(void)
}
/*
- * _readSort+ * ReadCommonSort+ * Assign the basic stuff of all nodes that inherit from Sort
*/
-static Sort *-_readSort(void)+static void+ReadCommonSort(Sort *local_node)
{
- READ_LOCALS(Sort);+ READ_TEMP_LOCALS();
ReadCommonPlan(&local_node->plan);
@@ -2164,6 +2165,32 @@ _readSort(void)
READ_OID_ARRAY(sortOperators, local_node->numCols);
READ_OID_ARRAY(collations, local_node->numCols);
READ_BOOL_ARRAY(nullsFirst, local_node->numCols);
+}++/*+ * _readSort+ */+static Sort *+_readSort(void)+{+ READ_LOCALS_NO_FIELDS(Sort);++ ReadCommonSort(local_node);++ READ_DONE();+}++/*+ * _readIncrementalSort+ */+static IncrementalSort *+_readIncrementalSort(void)+{+ READ_LOCALS(IncrementalSort);++ ReadCommonSort(&local_node->sort);++ READ_INT_FIELD(presortedCols);
READ_DONE();
}
@@ -2801,6 +2828,8 @@ parseNodeString(void)
return_value = _readMaterial();
else if (MATCH("SORT", 4))
return_value = _readSort();
+ else if (MATCH("INCREMENTALSORT", 15))+ return_value = _readIncrementalSort();
else if (MATCH("GROUP", 5))
return_value = _readGroup();
else if (MATCH("AGG", 3))
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.cindex 905bbe77d8..ccf46dd0aa 100644--- a/src/backend/optimizer/path/allpaths.c+++ b/src/backend/optimizer/path/allpaths.c@@ -3881,6 +3881,10 @@ print_path(PlannerInfo *root, Path *path, int indent)
ptype = "Sort";
subpath = ((SortPath *) path)->subpath;
break;
+ case T_IncrementalSortPath:+ ptype = "IncrementalSort";+ subpath = ((SortPath *) path)->subpath;+ break;
case T_GroupPath:
ptype = "Group";
subpath = ((GroupPath *) path)->subpath;
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.cindex 8cf694b61d..8efbb660b9 100644--- a/src/backend/optimizer/path/costsize.c+++ b/src/backend/optimizer/path/costsize.c@@ -128,6 +128,7 @@ bool enable_indexonlyscan = true;
bool enable_bitmapscan = true;
bool enable_tidscan = true;
bool enable_sort = true;
+bool enable_incrementalsort = true;
bool enable_hashagg = true;
bool enable_hashagg_disk = true;
bool enable_groupingsets_hash_disk = false;
@@ -1648,9 +1649,9 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
}
/*
- * cost_sort- * Determines and returns the cost of sorting a relation, including- * the cost of reading the input data.+ * cost_tuplesort+ * Determines and returns the cost of sorting a relation using tuplesort,+ * not including the cost of reading the input data.
*
* If the total volume of data to sort is less than sort_mem, we will do
* an in-memory sort, which requires no I/O and about t*log2(t) tuple
@@ -1677,39 +1678,23 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
* specifying nonzero comparison_cost; typically that's used for any extra
* work that has to be done to prepare the inputs to the comparison operators.
*
- * 'pathkeys' is a list of sort keys- * 'input_cost' is the total cost for reading the input data
* 'tuples' is the number of tuples in the relation
* 'width' is the average tuple width in bytes
* 'comparison_cost' is the extra cost per comparison, if any
* 'sort_mem' is the number of kilobytes of work memory allowed for the sort
* 'limit_tuples' is the bound on the number of output tuples; -1 if no bound
- *- * NOTE: some callers currently pass NIL for pathkeys because they- * can't conveniently supply the sort keys. Since this routine doesn't- * currently do anything with pathkeys anyway, that doesn't matter...- * but if it ever does, it should react gracefully to lack of key data.- * (Actually, the thing we'd most likely be interested in is just the number- * of sort keys, which all callers *could* supply.)
*/
-void-cost_sort(Path *path, PlannerInfo *root,- List *pathkeys, Cost input_cost, double tuples, int width,- Cost comparison_cost, int sort_mem,- double limit_tuples)+static void+cost_tuplesort(Cost *startup_cost, Cost *run_cost,+ double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples)
{
- Cost startup_cost = input_cost;- Cost run_cost = 0;
double input_bytes = relation_byte_size(tuples, width);
double output_bytes;
double output_tuples;
long sort_mem_bytes = sort_mem * 1024L;
- if (!enable_sort)- startup_cost += disable_cost;-- path->rows = tuples;-
/*
* We want to be sure the cost of a sort is never estimated as zero, even
* if passed-in tuple count is zero. Besides, mustn't do log(0)...
@@ -1748,7 +1733,7 @@ cost_sort(Path *path, PlannerInfo *root,
*
* Assume about N log2 N comparisons
*/
- startup_cost += comparison_cost * tuples * LOG2(tuples);+ *startup_cost = comparison_cost * tuples * LOG2(tuples);
/* Disk costs */
@@ -1759,7 +1744,7 @@ cost_sort(Path *path, PlannerInfo *root,
log_runs = 1.0;
npageaccesses = 2.0 * npages * log_runs;
/* Assume 3/4ths of accesses are sequential, 1/4th are not */
- startup_cost += npageaccesses *+ *startup_cost += npageaccesses *
(seq_page_cost * 0.75 + random_page_cost * 0.25);
}
else if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes)
@@ -1770,12 +1755,12 @@ cost_sort(Path *path, PlannerInfo *root,
* factor is a bit higher than for quicksort. Tweak it so that the
* cost curve is continuous at the crossover point.
*/
- startup_cost += comparison_cost * tuples * LOG2(2.0 * output_tuples);+ *startup_cost = comparison_cost * tuples * LOG2(2.0 * output_tuples);
}
else
{
/* We'll use plain quicksort on all the input tuples */
- startup_cost += comparison_cost * tuples * LOG2(tuples);+ *startup_cost = comparison_cost * tuples * LOG2(tuples);
}
/*
@@ -1786,8 +1771,163 @@ cost_sort(Path *path, PlannerInfo *root,
* here --- the upper LIMIT will pro-rate the run cost so we'd be double
* counting the LIMIT otherwise.
*/
- run_cost += cpu_operator_cost * tuples;+ *run_cost = cpu_operator_cost * tuples;+}++/*+ * cost_full_sort+ * Determines and returns the cost of sorting a relation, including the+ * cost of reading the input data.+ *+ * For the precise description of how the cost is calculated, see the comment+ * for cost_tuplesort().+ */+void+cost_full_sort(Cost *startup_cost, Cost *run_cost,+ Cost input_total_cost, double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples)+{+ cost_tuplesort(startup_cost, run_cost,+ tuples, width,+ comparison_cost, sort_mem,+ limit_tuples);++ if (!enable_sort)+ *startup_cost += disable_cost;++ *startup_cost += input_total_cost;+}++/*+ * cost_incremental_sort+ * Determines and returns the cost of sorting a relation incrementally, when+ * the input path is already sorted by some of the pathkeys.+ *+ * 'presorted_keys' is the number of leading pathkeys by which the input path+ * is sorted.+ *+ * We estimate the number of groups into which the relation is divided by the+ * leading pathkeys, and then calculate the cost of sorting a single group+ * with tuplesort using cost_tuplesort().+ */+void+cost_incremental_sort(Path *path,+ PlannerInfo *root, List *pathkeys, int presorted_keys,+ Cost input_startup_cost, Cost input_total_cost,+ double input_tuples, int width, Cost comparison_cost, int sort_mem,+ double limit_tuples)+{+ Cost startup_cost = 0,+ run_cost = 0,+ input_run_cost = input_total_cost - input_startup_cost;+ double group_tuples,+ input_groups;+ Cost group_startup_cost,+ group_run_cost,+ group_input_run_cost;+ List *presortedExprs = NIL;+ ListCell *l;+ int i = 0;++ Assert(presorted_keys != 0);++ /*+ * We want to be sure the cost of a sort is never estimated as zero, even+ * if passed-in tuple count is zero. Besides, mustn't do log(0)...+ */+ if (input_tuples < 2.0)+ input_tuples = 2.0;+ /* Extract presorted keys as list of expressions */+ foreach(l, pathkeys)+ {+ PathKey *key = (PathKey *) lfirst(l);+ EquivalenceMember *member = (EquivalenceMember *)+ linitial(key->pk_eclass->ec_members);++ presortedExprs = lappend(presortedExprs, member->em_expr);++ i++;+ if (i >= presorted_keys)+ break;+ }++ /* Estimate number of groups with equal presorted keys */+ input_groups = estimate_num_groups(root, presortedExprs, input_tuples, NULL);+ group_tuples = input_tuples / input_groups;+ group_input_run_cost = input_run_cost / input_groups;++ /*+ * Estimate average cost of sorting of one group where presorted keys are+ * equal. Incremental sort is sensitive to distribution of tuples to the+ * groups, where we're relying on quite rough assumptions. Thus, we're+ * pessimistic about incremental sort performance and increase its average+ * group size by half.+ */+ cost_tuplesort(&group_startup_cost, &group_run_cost,+ 1.5 * group_tuples, width, comparison_cost, sort_mem,+ limit_tuples);++ /*+ * Startup cost of incremental sort is the startup cost of its first group+ * plus the cost of its input.+ */+ startup_cost += group_startup_cost+ + input_startup_cost + group_input_run_cost;++ /*+ * After we started producing tuples from the first group, the cost of+ * producing all the tuples is given by the cost to finish processing this+ * group, plus the total cost to process the remaining groups, plus the+ * remaining cost of input.+ */+ run_cost += group_run_cost+ + (group_run_cost + group_startup_cost) * (input_groups - 1)+ + group_input_run_cost * (input_groups - 1);++ /*+ * Incremental sort adds some overhead by itself. Firstly, it has to+ * detect the sort groups. This is roughly equal to one extra copy and+ * comparison per tuple. Secondly, it has to reset the tuplesort context+ * for every group.+ */+ run_cost += (cpu_tuple_cost + comparison_cost) * input_tuples;+ run_cost += 2.0 * cpu_tuple_cost * input_groups;++ path->rows = input_tuples;+ path->startup_cost = startup_cost;+ path->total_cost = startup_cost + run_cost;+}++/*+ * cost_sort+ * Determines and returns the cost of sorting a relation, including+ * the cost of reading the input data.+ *+ * NOTE: some callers currently pass NIL for pathkeys because they+ * can't conveniently supply the sort keys. Since this routine doesn't+ * currently do anything with pathkeys anyway, that doesn't matter...+ * but if it ever does, it should react gracefully to lack of key data.+ * (Actually, the thing we'd most likely be interested in is just the number+ * of sort keys, which all callers *could* supply.)+ */+void+cost_sort(Path *path, PlannerInfo *root,+ List *pathkeys, Cost input_cost, double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples)++{+ Cost startup_cost;+ Cost run_cost;++ cost_full_sort(&startup_cost, &run_cost,+ input_cost,+ tuples, width, comparison_cost, sort_mem,+ limit_tuples);++ path->rows = tuples;
path->startup_cost = startup_cost;
path->total_cost = startup_cost + run_cost;
}
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.cindex 71b9d42c99..74799cd8fd 100644--- a/src/backend/optimizer/path/pathkeys.c+++ b/src/backend/optimizer/path/pathkeys.c@@ -334,6 +334,51 @@ pathkeys_contained_in(List *keys1, List *keys2)
return false;
}
++/*+ * pathkeys_common_contained_in+ * Same as pathkeys_contained_in, but also sets length of longest+ * common prefix of keys1 and keys2.+ */+bool+pathkeys_common_contained_in(List *keys1, List *keys2, int *n_common)+{+ int n = 0;+ ListCell *key1,+ *key2;++ forboth(key1, keys1, key2, keys2)+ {+ PathKey *pathkey1 = (PathKey *) lfirst(key1);+ PathKey *pathkey2 = (PathKey *) lfirst(key2);++ if (pathkey1 != pathkey2)+ {+ *n_common = n;+ return false;+ }+ n++;+ }++ *n_common = n;+ return (key1 == NULL);+}+++/*+ * pathkeys_common+ * Returns length of longest common prefix of keys1 and keys2.+ */+int+pathkeys_common(List *keys1, List *keys2)+{+ int n;++ (void) pathkeys_common_contained_in(keys1, keys2, &n);+ return n;+}++
/*
* get_cheapest_path_for_pathkeys
* Find the cheapest path (according to the specified criterion) that
@@ -1793,19 +1838,23 @@ right_merge_direction(PlannerInfo *root, PathKey *pathkey)
static int
pathkeys_useful_for_ordering(PlannerInfo *root, List *pathkeys)
{
+ int n_common_pathkeys;+
if (root->query_pathkeys == NIL)
return 0; /* no special ordering requested */
if (pathkeys == NIL)
return 0; /* unordered path */
- if (pathkeys_contained_in(root->query_pathkeys, pathkeys))- {- /* It's useful ... or at least the first N keys are */- return list_length(root->query_pathkeys);- }+ (void) pathkeys_common_contained_in(root->query_pathkeys, pathkeys,+ &n_common_pathkeys);- return 0; /* path ordering not useful */+ /*+ * Return the number of path keys in common, or 0 if there are none. Any+ * leading common pathkeys could be useful for ordering because we can use+ * the incremental sort.+ */+ return n_common_pathkeys;
}
/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.cindex fc25908dc6..1d7d4eb3e7 100644--- a/src/backend/optimizer/plan/createplan.c+++ b/src/backend/optimizer/plan/createplan.c@@ -98,6 +98,8 @@ static Plan *create_projection_plan(PlannerInfo *root,
int flags);
static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
+static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,+ IncrementalSortPath *best_path, int flags);
static Group *create_group_plan(PlannerInfo *root, GroupPath *best_path);
static Unique *create_upper_unique_plan(PlannerInfo *root, UpperUniquePath *best_path,
int flags);
@@ -244,6 +246,10 @@ static MergeJoin *make_mergejoin(List *tlist,
static Sort *make_sort(Plan *lefttree, int numCols,
AttrNumber *sortColIdx, Oid *sortOperators,
Oid *collations, bool *nullsFirst);
+static IncrementalSort *make_incrementalsort(Plan *lefttree,+ int numCols, int presortedCols,+ AttrNumber *sortColIdx, Oid *sortOperators,+ Oid *collations, bool *nullsFirst);
static Plan *prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
Relids relids,
const AttrNumber *reqColIdx,
@@ -258,6 +264,8 @@ static EquivalenceMember *find_ec_member_for_tle(EquivalenceClass *ec,
Relids relids);
static Sort *make_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
Relids relids);
+static IncrementalSort *make_incrementalsort_from_pathkeys(Plan *lefttree,+ List *pathkeys, Relids relids, int presortedCols);
static Sort *make_sort_from_groupcols(List *groupcls,
AttrNumber *grpColIdx,
Plan *lefttree);
@@ -460,6 +468,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
(SortPath *) best_path,
flags);
break;
+ case T_IncrementalSort:+ plan = (Plan *) create_incrementalsort_plan(root,+ (IncrementalSortPath *) best_path,+ flags);+ break;
case T_Group:
plan = (Plan *) create_group_plan(root,
(GroupPath *) best_path);
@@ -1994,6 +2007,32 @@ create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags)
return plan;
}
+/*+ * create_incrementalsort_plan+ *+ * Do the same as create_sort_plan, but create IncrementalSort plan.+ */+static IncrementalSort *+create_incrementalsort_plan(PlannerInfo *root, IncrementalSortPath *best_path,+ int flags)+{+ IncrementalSort *plan;+ Plan *subplan;++ /* See comments in create_sort_plan() above */+ subplan = create_plan_recurse(root, best_path->spath.subpath,+ flags | CP_SMALL_TLIST);+ plan = make_incrementalsort_from_pathkeys(subplan,+ best_path->spath.path.pathkeys,+ IS_OTHER_REL(best_path->spath.subpath->parent) ?+ best_path->spath.path.parent->relids : NULL,+ best_path->presortedCols);++ copy_generic_path_info(&plan->sort.plan, (Path *) best_path);++ return plan;+}+
/*
* create_group_plan
*
@@ -5088,17 +5127,24 @@ static void
label_sort_with_costsize(PlannerInfo *root, Sort *plan, double limit_tuples)
{
Plan *lefttree = plan->plan.lefttree;
- Path sort_path; /* dummy for result of cost_sort */-- cost_sort(&sort_path, root, NIL,- lefttree->total_cost,- lefttree->plan_rows,- lefttree->plan_width,- 0.0,- work_mem,- limit_tuples);- plan->plan.startup_cost = sort_path.startup_cost;- plan->plan.total_cost = sort_path.total_cost;+ Cost startup_cost,+ run_cost;++ /*+ * This function shouldn't have to deal with IncrementalSort plans because+ * they are only created from corresponding Path nodes.+ */+ Assert(IsA(plan, Sort));++ cost_full_sort(&startup_cost, &run_cost,+ lefttree->total_cost,+ lefttree->plan_rows,+ lefttree->plan_width,+ 0.0,+ work_mem,+ limit_tuples);+ plan->plan.startup_cost = startup_cost;+ plan->plan.total_cost = startup_cost + run_cost;
plan->plan.plan_rows = lefttree->plan_rows;
plan->plan.plan_width = lefttree->plan_width;
plan->plan.parallel_aware = false;
@@ -5677,9 +5723,12 @@ make_sort(Plan *lefttree, int numCols,
AttrNumber *sortColIdx, Oid *sortOperators,
Oid *collations, bool *nullsFirst)
{
- Sort *node = makeNode(Sort);- Plan *plan = &node->plan;+ Sort *node;+ Plan *plan;+ node = makeNode(Sort);++ plan = &node->plan;
plan->targetlist = lefttree->targetlist;
plan->qual = NIL;
plan->lefttree = lefttree;
@@ -5693,6 +5742,37 @@ make_sort(Plan *lefttree, int numCols,
return node;
}
+/*+ * make_incrementalsort --- basic routine to build an IncrementalSort plan node+ *+ * Caller must have built the sortColIdx, sortOperators, collations, and+ * nullsFirst arrays already.+ */+static IncrementalSort *+make_incrementalsort(Plan *lefttree, int numCols, int presortedCols,+ AttrNumber *sortColIdx, Oid *sortOperators,+ Oid *collations, bool *nullsFirst)+{+ IncrementalSort *node;+ Plan *plan;++ node = makeNode(IncrementalSort);++ plan = &node->sort.plan;+ plan->targetlist = lefttree->targetlist;+ plan->qual = NIL;+ plan->lefttree = lefttree;+ plan->righttree = NULL;+ node->presortedCols = presortedCols;+ node->sort.numCols = numCols;+ node->sort.sortColIdx = sortColIdx;+ node->sort.sortOperators = sortOperators;+ node->sort.collations = collations;+ node->sort.nullsFirst = nullsFirst;++ return node;+}+
/*
* prepare_sort_from_pathkeys
* Prepare to sort according to given pathkeys
@@ -6039,6 +6119,42 @@ make_sort_from_pathkeys(Plan *lefttree, List *pathkeys, Relids relids)
collations, nullsFirst);
}
+/*+ * make_incrementalsort_from_pathkeys+ * Create sort plan to sort according to given pathkeys+ *+ * 'lefttree' is the node which yields input tuples+ * 'pathkeys' is the list of pathkeys by which the result is to be sorted+ * 'relids' is the set of relations required by prepare_sort_from_pathkeys()+ * 'presortedCols' is the number of presorted columns in input tuples+ */+static IncrementalSort *+make_incrementalsort_from_pathkeys(Plan *lefttree, List *pathkeys,+ Relids relids, int presortedCols)+{+ int numsortkeys;+ AttrNumber *sortColIdx;+ Oid *sortOperators;+ Oid *collations;+ bool *nullsFirst;++ /* Compute sort column info, and adjust lefttree as needed */+ lefttree = prepare_sort_from_pathkeys(lefttree, pathkeys,+ relids,+ NULL,+ false,+ &numsortkeys,+ &sortColIdx,+ &sortOperators,+ &collations,+ &nullsFirst);++ /* Now build the Sort node */+ return make_incrementalsort(lefttree, numsortkeys, presortedCols,+ sortColIdx, sortOperators,+ collations, nullsFirst);+}+
/*
* make_sort_from_sortclauses
* Create sort plan to sort according to given sortclauses
@@ -6774,6 +6890,7 @@ is_projection_capable_path(Path *path)
case T_Hash:
case T_Material:
case T_Sort:
+ case T_IncrementalSort:
case T_Unique:
case T_SetOp:
case T_LockRows:
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex eb25c2f470..b2f4aaadb5 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -4869,7 +4869,7 @@ create_distinct_paths(PlannerInfo *root,
else
{
Size hashentrysize = hash_agg_entry_size(
- 0, cheapest_input_path->pathtarget->width, 0);+ 0, cheapest_input_path->pathtarget->width, 0);
/* Allow hashing only if hashtable is predicted to fit in work_mem */
allow_hash = (hashentrysize * numDistinctRows <= work_mem * 1024L);
@@ -4925,8 +4925,8 @@ create_distinct_paths(PlannerInfo *root,
* Build a new upperrel containing Paths for ORDER BY evaluation.
*
* All paths in the result must satisfy the ORDER BY ordering.
- * The only new path we need consider is an explicit sort on the- * cheapest-total existing path.+ * The only new paths we need consider is an explicit full or+ * incremental sort on the cheapest-total existing path.
*
* input_rel: contains the source-data Paths
* target: the output tlist the result Paths must emit
@@ -4965,29 +4965,60 @@ create_ordered_paths(PlannerInfo *root,
foreach(lc, input_rel->pathlist)
{
- Path *path = (Path *) lfirst(lc);+ Path *input_path = (Path *) lfirst(lc);+ Path *sorted_path = input_path;
bool is_sorted;
+ int presorted_keys;++ is_sorted = pathkeys_common_contained_in(root->sort_pathkeys,+ input_path->pathkeys, &presorted_keys);- is_sorted = pathkeys_contained_in(root->sort_pathkeys,- path->pathkeys);- if (path == cheapest_input_path || is_sorted)+ if (is_sorted)
{
- if (!is_sorted)+ /* Use the input path as is, but add a projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);+ }+ else+ {+ if (input_path == cheapest_input_path)
{
- /* An explicit sort here can take advantage of LIMIT */- path = (Path *) create_sort_path(root,- ordered_rel,- path,- root->sort_pathkeys,- limit_tuples);+ /*+ * Sort the cheapest input path. An explicit sort here can+ * take advantage of LIMIT.+ */+ sorted_path = (Path *) create_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ limit_tuples);+ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);+ }+ if (enable_incrementalsort && presorted_keys > 0)+ {+ /* Also consider incremental sort. */+ sorted_path = (Path *) create_incremental_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ presorted_keys,+ limit_tuples);++ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);
}
-- /* Add projection step if needed */- if (path->pathtarget != target)- path = apply_projection_to_path(root, ordered_rel,- path, target);-- add_path(ordered_rel, path);
}
}
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.cindex 3dcded506b..2b676bf406 100644--- a/src/backend/optimizer/plan/setrefs.c+++ b/src/backend/optimizer/plan/setrefs.c@@ -678,6 +678,7 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
case T_Material:
case T_Sort:
+ case T_IncrementalSort:
case T_Unique:
case T_SetOp:
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.cindex 3650e8329d..b02fcb9bfe 100644--- a/src/backend/optimizer/plan/subselect.c+++ b/src/backend/optimizer/plan/subselect.c@@ -2688,6 +2688,7 @@ finalize_plan(PlannerInfo *root, Plan *plan,
case T_Hash:
case T_Material:
case T_Sort:
+ case T_IncrementalSort:
case T_Unique:
case T_SetOp:
case T_Group:
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.cindex 9c8f3b1f0b..e15bc1baaa 100644--- a/src/backend/optimizer/util/pathnode.c+++ b/src/backend/optimizer/util/pathnode.c@@ -2750,6 +2750,57 @@ create_set_projection_path(PlannerInfo *root,
return pathnode;
}
+/*+ * create_incremental_sort_path+ * Creates a pathnode that represents performing an incremental sort.+ *+ * 'rel' is the parent relation associated with the result+ * 'subpath' is the path representing the source of data+ * 'pathkeys' represents the desired sort order+ * 'presorted_keys' is the number of keys by which the input path is+ * already sorted+ * 'limit_tuples' is the estimated bound on the number of output tuples,+ * or -1 if no LIMIT or couldn't estimate+ */+SortPath *+create_incremental_sort_path(PlannerInfo *root,+ RelOptInfo *rel,+ Path *subpath,+ List *pathkeys,+ int presorted_keys,+ double limit_tuples)+{+ IncrementalSortPath *sort = makeNode(IncrementalSortPath);+ SortPath *pathnode = &sort->spath;++ pathnode->path.pathtype = T_IncrementalSort;+ pathnode->path.parent = rel;+ /* Sort doesn't project, so use source path's pathtarget */+ pathnode->path.pathtarget = subpath->pathtarget;+ /* For now, assume we are above any joins, so no parameterization */+ pathnode->path.param_info = NULL;+ pathnode->path.parallel_aware = false;+ pathnode->path.parallel_safe = rel->consider_parallel &&+ subpath->parallel_safe;+ pathnode->path.parallel_workers = subpath->parallel_workers;+ pathnode->path.pathkeys = pathkeys;++ pathnode->subpath = subpath;++ cost_incremental_sort(&pathnode->path,+ root, pathkeys, presorted_keys,+ subpath->startup_cost,+ subpath->total_cost,+ subpath->rows,+ subpath->pathtarget->width,+ 0.0, /* XXX comparison_cost shouldn't be 0? */+ work_mem, limit_tuples);++ sort->presortedCols = presorted_keys;++ return pathnode;+}+
/*
* create_sort_path
* Creates a pathnode that represents performing an explicit sort.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.cindex af876d1f01..b6ce724557 100644--- a/src/backend/utils/misc/guc.c+++ b/src/backend/utils/misc/guc.c@@ -989,6 +989,15 @@ static struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {+ {"enable_incrementalsort", PGC_USERSET, QUERY_TUNING_METHOD,+ gettext_noop("Enables the planner's use of incremental sort steps."),+ NULL+ },+ &enable_incrementalsort,+ true,+ NULL, NULL, NULL+ },
{
{"enable_hashagg", PGC_USERSET, QUERY_TUNING_METHOD,
gettext_noop("Enables the planner's use of hashed aggregation plans."),
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.cindex d02e676aa3..583551d197 100644--- a/src/backend/utils/sort/tuplesort.c+++ b/src/backend/utils/sort/tuplesort.c@@ -125,6 +125,16 @@
#define PARALLEL_SORT(state) ((state)->shared == NULL ? 0 : \
(state)->worker >= 0 ? 1 : 2)
+/*+ * Initial size of memtuples array. We're trying to select this size so that+ * array doesn't exceed ALLOCSET_SEPARATE_THRESHOLD and so that the overhead of+ * allocation might possibly be lowered. However, we don't consider array sizes+ * less than 1024.+ *+ */+#define INITIAL_MEMTUPSIZE Max(1024, \+ ALLOCSET_SEPARATE_THRESHOLD / sizeof(SortTuple) + 1)+
/* GUC variables */
#ifdef TRACE_SORT
bool trace_sort = false;
@@ -241,6 +251,14 @@ struct Tuplesortstate
int64 allowedMem; /* total memory allowed, in bytes */
int maxTapes; /* number of tapes (Knuth's T) */
int tapeRange; /* maxTapes-1 (Knuth's P) */
+ int64 maxSpace; /* maximum amount of space occupied among sort+ * of groups, either in-memory or on-disk */+ bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk+ * space, false when it's value for in-memory+ * space */+ TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */+ MemoryContext maincontext; /* memory context for tuple sort metadata that+ * persists across multiple batches */
MemoryContext sortcontext; /* memory context holding most sort data */
MemoryContext tuplecontext; /* sub-context of sortcontext for tuple data */
LogicalTapeSet *tapeset; /* logtape.c object for tapes in a temp file */
@@ -591,6 +609,7 @@ struct Sharedsort
static Tuplesortstate *tuplesort_begin_common(int workMem,
SortCoordinate coordinate,
bool randomAccess);
+static void tuplesort_begin_batch(Tuplesortstate *state);
static void puttuple_common(Tuplesortstate *state, SortTuple *tuple);
static bool consider_abort_common(Tuplesortstate *state);
static void inittapes(Tuplesortstate *state, bool mergeruns);
@@ -647,6 +666,8 @@ static void worker_freeze_result_tape(Tuplesortstate *state);
static void worker_nomergeruns(Tuplesortstate *state);
static void leader_takeover_tapes(Tuplesortstate *state);
static void free_sort_tuple(Tuplesortstate *state, SortTuple *stup);
+static void tuplesort_free(Tuplesortstate *state);+static void tuplesort_updatemax(Tuplesortstate *state);
/*
* Special versions of qsort just for SortTuple objects. qsort_tuple() sorts
@@ -682,8 +703,8 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
bool randomAccess)
{
Tuplesortstate *state;
+ MemoryContext maincontext;
MemoryContext sortcontext;
- MemoryContext tuplecontext;
MemoryContext oldcontext;
/* See leader_takeover_tapes() remarks on randomAccess support */
@@ -691,31 +712,31 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
elog(ERROR, "random access disallowed under parallel sort");
/*
- * Create a working memory context for this sort operation. All data- * needed by the sort will live inside this context.+ * Memory context surviving tuplesort_reset. This memory context holds+ * data which is useful to keep while sorting multiple similar batches.
*/
- sortcontext = AllocSetContextCreate(CurrentMemoryContext,+ maincontext = AllocSetContextCreate(CurrentMemoryContext,
"TupleSort main",
ALLOCSET_DEFAULT_SIZES);
/*
- * Caller tuple (e.g. IndexTuple) memory context.- *- * A dedicated child context used exclusively for caller passed tuples- * eases memory management. Resetting at key points reduces- * fragmentation. Note that the memtuples array of SortTuples is allocated- * in the parent context, not this context, because there is no need to- * free memtuples early.+ * Create a working memory context for one sort operation. The content of+ * this context is deleted by tuplesort_reset.
*/
- tuplecontext = AllocSetContextCreate(sortcontext,- "Caller tuples",- ALLOCSET_DEFAULT_SIZES);+ sortcontext = AllocSetContextCreate(maincontext,+ "TupleSort sort",+ ALLOCSET_DEFAULT_SIZES);
/*
- * Make the Tuplesortstate within the per-sort context. This way, we+ * Additionally a working memory context for tuples is setup in+ * tuplesort_begin_batch.+ */++ /*+ * Make the Tuplesortstate within the per-sortstate context. This way, we
* don't need a separate pfree() operation for it at shutdown.
*/
- oldcontext = MemoryContextSwitchTo(sortcontext);+ oldcontext = MemoryContextSwitchTo(maincontext);
state = (Tuplesortstate *) palloc0(sizeof(Tuplesortstate));
@@ -724,11 +745,8 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
pg_rusage_init(&state->ru_start);
#endif
- state->status = TSS_INITIAL;
state->randomAccess = randomAccess;
- state->bounded = false;
state->tuples = true;
- state->boundUsed = false;
/*
* workMem is forced to be at least 64KB, the current minimum valid value
@@ -737,38 +755,21 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
* with very little memory.
*/
state->allowedMem = Max(workMem, 64) * (int64) 1024;
- state->availMem = state->allowedMem;
state->sortcontext = sortcontext;
- state->tuplecontext = tuplecontext;- state->tapeset = NULL;-- state->memtupcount = 0;+ state->maincontext = maincontext;
/*
* Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;
* see comments in grow_memtuples().
*/
- state->memtupsize = Max(1024,- ALLOCSET_SEPARATE_THRESHOLD / sizeof(SortTuple) + 1);-- state->growmemtuples = true;- state->slabAllocatorUsed = false;- state->memtuples = (SortTuple *) palloc(state->memtupsize * sizeof(SortTuple));-- USEMEM(state, GetMemoryChunkSpace(state->memtuples));-- /* workMem must be large enough for the minimal memtuples array */- if (LACKMEM(state))- elog(ERROR, "insufficient memory allowed for sort");-- state->currentRun = 0;+ state->memtupsize = INITIAL_MEMTUPSIZE;+ state->memtuples = NULL;
/*
- * maxTapes, tapeRange, and Algorithm D variables will be initialized by- * inittapes(), if needed+ * After all of the other non-parallel-related state, we setup all of the+ * state needed for each batch.
*/
-- state->result_tape = -1; /* flag that result tape has not been formed */+ tuplesort_begin_batch(state);
/*
* Initialize parallel-related state based on coordination information
@@ -802,6 +803,70 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
return state;
}
+static void+tuplesort_begin_batch(Tuplesortstate *state)+{+ MemoryContext oldcontext;++ oldcontext = MemoryContextSwitchTo(state->maincontext);++ /*+ * Caller tuple (e.g. IndexTuple) memory context.+ *+ * A dedicated child context used exclusively for caller passed tuples+ * eases memory management. Resetting at key points reduces+ * fragmentation. Note that the memtuples array of SortTuples is allocated+ * in the parent context, not this context, because there is no need to+ * free memtuples early.+ */+ state->tuplecontext = AllocSetContextCreate(state->sortcontext,+ "Caller tuples",+ ALLOCSET_DEFAULT_SIZES);++ state->status = TSS_INITIAL;+ state->bounded = false;+ state->boundUsed = false;++ state->availMem = state->allowedMem;++ state->tapeset = NULL;++ state->memtupcount = 0;++ /*+ * Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;+ * see comments in grow_memtuples().+ */+ state->growmemtuples = true;+ state->slabAllocatorUsed = false;+ if (state->memtuples != NULL && state->memtupsize != INITIAL_MEMTUPSIZE)+ {+ pfree(state->memtuples);+ state->memtuples = NULL;+ state->memtupsize = INITIAL_MEMTUPSIZE;+ }+ if (state->memtuples == NULL)+ {+ state->memtuples = (SortTuple *) palloc(state->memtupsize * sizeof(SortTuple));+ USEMEM(state, GetMemoryChunkSpace(state->memtuples));+ }++ /* workMem must be large enough for the minimal memtuples array */+ if (LACKMEM(state))+ elog(ERROR, "insufficient memory allowed for sort");++ state->currentRun = 0;++ /*+ * maxTapes, tapeRange, and Algorithm D variables will be initialized by+ * inittapes(), if needed+ */++ state->result_tape = -1; /* flag that result tape has not been formed */++ MemoryContextSwitchTo(oldcontext);+}+
Tuplesortstate *
tuplesort_begin_heap(TupleDesc tupDesc,
int nkeys, AttrNumber *attNums,
@@ -814,7 +879,7 @@ tuplesort_begin_heap(TupleDesc tupDesc,
MemoryContext oldcontext;
int i;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
AssertArg(nkeys > 0);
@@ -890,7 +955,7 @@ tuplesort_begin_cluster(TupleDesc tupDesc,
Assert(indexRel->rd_rel->relam == BTREE_AM_OID);
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -985,7 +1050,7 @@ tuplesort_begin_index_btree(Relation heapRel,
MemoryContext oldcontext;
int i;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -1063,7 +1128,7 @@ tuplesort_begin_index_hash(Relation heapRel,
randomAccess);
MemoryContext oldcontext;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -1106,7 +1171,7 @@ tuplesort_begin_datum(Oid datumType, Oid sortOperator, Oid sortCollation,
int16 typlen;
bool typbyval;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -1223,17 +1288,19 @@ tuplesort_set_bound(Tuplesortstate *state, int64 bound)
state->sortKeys->abbrev_full_comparator = NULL;
}
+bool+tuplesort_used_bound(Tuplesortstate *state)+{+ return state->boundUsed;+}+
/*
- * tuplesort_end- *- * Release resources and clean up.+ * tuplesort_free
*
- * NOTE: after calling this, any pointers returned by tuplesort_getXXX are- * pointing to garbage. Be careful not to attempt to use or free such- * pointers afterwards!+ * Internal routine for freeing resources of tuplesort.
*/
-void-tuplesort_end(Tuplesortstate *state)+static void+tuplesort_free(Tuplesortstate *state)
{
/* context swap probably not needed, but let's be safe */
MemoryContext oldcontext = MemoryContextSwitchTo(state->sortcontext);
@@ -1291,10 +1358,104 @@ tuplesort_end(Tuplesortstate *state)
MemoryContextSwitchTo(oldcontext);
/*
- * Free the per-sort memory context, thereby releasing all working memory,- * including the Tuplesortstate struct itself.+ * Free the per-sort memory context, thereby releasing all working memory.+ */+ MemoryContextReset(state->sortcontext);+}++/*+ * tuplesort_end+ *+ * Release resources and clean up.+ *+ * NOTE: after calling this, any pointers returned by tuplesort_getXXX are+ * pointing to garbage. Be careful not to attempt to use or free such+ * pointers afterwards!+ */+void+tuplesort_end(Tuplesortstate *state)+{+ tuplesort_free(state);++ /*+ * Free the main memory context, including the Tuplesortstate struct+ * itself.
*/
- MemoryContextDelete(state->sortcontext);+ MemoryContextDelete(state->maincontext);+}++/*+ * tuplesort_updatemax+ *+ * Update maximum resource usage statistics.+ */+static void+tuplesort_updatemax(Tuplesortstate *state)+{+ int64 spaceUsed;+ bool isSpaceDisk;++ /*+ * Note: it might seem we should provide both memory and disk usage for a+ * disk-based sort. However, the current code doesn't track memory space+ * accurately once we have begun to return tuples to the caller (since we+ * don't account for pfree's the caller is expected to do), so we cannot+ * rely on availMem in a disk sort. This does not seem worth the overhead+ * to fix. Is it worth creating an API for the memory context code to+ * tell us how much is actually used in sortcontext?+ */+ if (state->tapeset)+ {+ isSpaceDisk = true;+ spaceUsed = LogicalTapeSetBlocks(state->tapeset) * BLCKSZ;+ }+ else+ {+ isSpaceDisk = false;+ spaceUsed = state->allowedMem - state->availMem;+ }++ /*+ * Sort evicts data to the disk when it didn't manage to fit those data to+ * the main memory. This is why we assume space used on the disk to be+ * more important for tracking resource usage than space used in memory.+ * Note that amount of space occupied by some tuple set on the disk might+ * be less than amount of space occupied by the same tuple set in the+ * memory due to more compact representation.+ */+ if ((isSpaceDisk && !state->isMaxSpaceDisk) ||+ (isSpaceDisk == state->isMaxSpaceDisk && spaceUsed > state->maxSpace))+ {+ state->maxSpace = spaceUsed;+ state->isMaxSpaceDisk = isSpaceDisk;+ state->maxSpaceStatus = state->status;+ }+}++/*+ * tuplesort_reset+ *+ * Reset the tuplesort. Reset all the data in the tuplesort, but leave the+ * meta-information in. After tuplesort_reset, tuplesort is ready to start+ * a new sort. This allows avoiding recreation of tuple sort states (and+ * save resources) when sorting multiple small batches.+ */+void+tuplesort_reset(Tuplesortstate *state)+{+ tuplesort_updatemax(state);+ tuplesort_free(state);++ /*+ * After we've freed up per-batch memory, re-setup all of the state common+ * to both the first batch and any subsequent batch.+ */+ tuplesort_begin_batch(state);++ state->lastReturnedTuple = NULL;+ state->slabMemoryBegin = NULL;+ state->slabMemoryEnd = NULL;+ state->slabFreeHead = NULL;
}
/*
@@ -2591,8 +2752,7 @@ mergeruns(Tuplesortstate *state)
* Reset tuple memory. We've freed all the tuples that we previously
* allocated. We will use the slab allocator from now on.
*/
- MemoryContextDelete(state->tuplecontext);- state->tuplecontext = NULL;+ MemoryContextResetOnly(state->tuplecontext);
/*
* We no longer need a large memtuples array. (We will allocate a smaller
@@ -2642,7 +2802,8 @@ mergeruns(Tuplesortstate *state)
* from each input tape.
*/
state->memtupsize = numInputTapes;
- state->memtuples = (SortTuple *) palloc(numInputTapes * sizeof(SortTuple));+ state->memtuples = (SortTuple *) MemoryContextAlloc(state->maincontext,+ numInputTapes * sizeof(SortTuple));
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
/*
@@ -3138,18 +3299,15 @@ tuplesort_get_stats(Tuplesortstate *state,
* to fix. Is it worth creating an API for the memory context code to
* tell us how much is actually used in sortcontext?
*/
- if (state->tapeset)- {+ tuplesort_updatemax(state);++ if (state->isMaxSpaceDisk)
stats->spaceType = SORT_SPACE_TYPE_DISK;
- stats->spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024);- }
else
- {
stats->spaceType = SORT_SPACE_TYPE_MEMORY;
- stats->spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024;- }+ stats->spaceUsed = (state->maxSpace + 1023) / 1024;- switch (state->status)+ switch (state->maxSpaceStatus)
{
case TSS_SORTEDINMEM:
if (state->boundUsed)
diff --git a/src/include/executor/execdebug.h b/src/include/executor/execdebug.hindex 2e9920111f..4af6e0013d 100644--- a/src/include/executor/execdebug.h+++ b/src/include/executor/execdebug.h@@ -86,10 +86,12 @@
#define SO_nodeDisplay(l) nodeDisplay(l)
#define SO_printf(s) printf(s)
#define SO1_printf(s, p) printf(s, p)
+#define SO2_printf(s, p1, p2) printf(s, p1, p2)
#else
#define SO_nodeDisplay(l)
#define SO_printf(s)
#define SO1_printf(s, p)
+#define SO2_printf(s, p1, p2)
#endif /* EXEC_SORTDEBUG */
/* ----------------
diff --git a/src/include/executor/nodeIncrementalSort.h b/src/include/executor/nodeIncrementalSort.hnew file mode 100644index 0000000000..e62c02a4f3--- /dev/null+++ b/src/include/executor/nodeIncrementalSort.h@@ -0,0 +1,28 @@+/*-------------------------------------------------------------------------+ *+ * nodeIncrementalSort.h+ *+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ * src/include/executor/nodeIncrementalSort.h+ *+ *-------------------------------------------------------------------------+ */+#ifndef NODEINCREMENTALSORT_H+#define NODEINCREMENTALSORT_H++#include "access/parallel.h"+#include "nodes/execnodes.h"++extern IncrementalSortState *ExecInitIncrementalSort(IncrementalSort *node, EState *estate, int eflags);+extern void ExecEndIncrementalSort(IncrementalSortState *node);+extern void ExecReScanIncrementalSort(IncrementalSortState *node);++/* parallel instrumentation support */+extern void ExecIncrementalSortEstimate(IncrementalSortState *node, ParallelContext *pcxt);+extern void ExecIncrementalSortInitializeDSM(IncrementalSortState *node, ParallelContext *pcxt);+extern void ExecIncrementalSortInitializeWorker(IncrementalSortState *node, ParallelWorkerContext *pcxt);+extern void ExecIncrementalSortRetrieveInstrumentation(IncrementalSortState *node);++#endif /* NODEINCREMENTALSORT_H */diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.hindex 3d27d50f09..e6a9b67675 100644--- a/src/include/nodes/execnodes.h+++ b/src/include/nodes/execnodes.h@@ -1980,6 +1980,21 @@ typedef struct MaterialState
Tuplestorestate *tuplestorestate;
} MaterialState;
++/* ----------------+ * When performing sorting by multiple keys, it's possible that the input+ * dataset is already sorted on a prefix of those keys. We call these+ * "presorted keys".+ * PresortedKeyData represents information about one such key.+ * ----------------+ */+typedef struct PresortedKeyData+{+ FmgrInfo flinfo; /* comparison function info */+ FunctionCallInfo fcinfo; /* comparison function call info */+ OffsetNumber attno; /* attribute number in tuple */+} PresortedKeyData;+
/* ----------------
* Shared memory container for per-worker sort information
* ----------------
@@ -2008,6 +2023,68 @@ typedef struct SortState
SharedSortInfo *shared_info; /* one entry per worker */
} SortState;
+typedef struct IncrementalSortGroupInfo+{+ int64 groupCount;+ long maxDiskSpaceUsed;+ long totalDiskSpaceUsed;+ long maxMemorySpaceUsed;+ long totalMemorySpaceUsed;+ List *sortMethods;+} IncrementalSortGroupInfo;++typedef struct IncrementalSortInfo+{+ IncrementalSortGroupInfo fullsortGroupInfo;+ IncrementalSortGroupInfo prefixsortGroupInfo;+} IncrementalSortInfo;++/* ----------------+ * Shared memory container for per-worker incremental sort information+ * ----------------+ */+typedef struct SharedIncrementalSortInfo+{+ int num_workers;+ IncrementalSortInfo sinfo[FLEXIBLE_ARRAY_MEMBER];+} SharedIncrementalSortInfo;++/* ----------------+ * IncrementalSortState information+ * ----------------+ */+typedef enum+{+ INCSORT_LOADFULLSORT,+ INCSORT_LOADPREFIXSORT,+ INCSORT_READFULLSORT,+ INCSORT_READPREFIXSORT,+} IncrementalSortExecutionStatus;++typedef struct IncrementalSortState+{+ ScanState ss; /* its first field is NodeTag */+ bool bounded; /* is the result set bounded? */+ int64 bound; /* if bounded, how many tuples are needed */+ bool sort_Done; /* sort completed yet? */+ bool outerNodeDone; /* finished fetching tuples from outer node */+ int64 bound_Done; /* value of bound we did the sort with */+ IncrementalSortExecutionStatus execution_status;+ int64 n_fullsort_remaining;+ Tuplesortstate *fullsort_state; /* private state of tuplesort.c */+ Tuplesortstate *prefixsort_state; /* private state of tuplesort.c */+ /* the keys by which the input path is already sorted */+ PresortedKeyData *presorted_keys;++ IncrementalSortInfo incsort_info;++ /* slot for pivot tuple defining values of presorted keys within group */+ TupleTableSlot *group_pivot;+ TupleTableSlot *transfer_tuple;+ bool am_worker; /* are we a worker? */+ SharedIncrementalSortInfo *shared_info; /* one entry per worker */+} IncrementalSortState;+
/* ---------------------
* GroupState information
* ---------------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.hindex 8a76afe8cc..50b1ba5186 100644--- a/src/include/nodes/nodes.h+++ b/src/include/nodes/nodes.h@@ -74,6 +74,7 @@ typedef enum NodeTag
T_HashJoin,
T_Material,
T_Sort,
+ T_IncrementalSort,
T_Group,
T_Agg,
T_WindowAgg,
@@ -130,6 +131,7 @@ typedef enum NodeTag
T_HashJoinState,
T_MaterialState,
T_SortState,
+ T_IncrementalSortState,
T_GroupState,
T_AggState,
T_WindowAggState,
@@ -245,6 +247,7 @@ typedef enum NodeTag
T_ProjectionPath,
T_ProjectSetPath,
T_SortPath,
+ T_IncrementalSortPath,
T_GroupPath,
T_UpperUniquePath,
T_AggPath,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.hindex 0ceb809644..28d580dd3c 100644--- a/src/include/nodes/pathnodes.h+++ b/src/include/nodes/pathnodes.h@@ -1620,6 +1620,15 @@ typedef struct SortPath
Path *subpath; /* path representing input source */
} SortPath;
+/*+ * IncrementalSortPath+ */+typedef struct IncrementalSortPath+{+ SortPath spath;+ int presortedCols; /* number of presorted columns */+} IncrementalSortPath;+
/*
* GroupPath represents grouping (of presorted input)
*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.hindex 4869fe7b6d..fe4046b64b 100644--- a/src/include/nodes/plannodes.h+++ b/src/include/nodes/plannodes.h@@ -774,6 +774,17 @@ typedef struct Sort
bool *nullsFirst; /* NULLS FIRST/LAST directions */
} Sort;
++/* ----------------+ * incremental sort node+ * ----------------+ */+typedef struct IncrementalSort+{+ Sort sort;+ int presortedCols; /* number of presorted columns */+} IncrementalSort;+
/* ---------------
* group node -
* Used for queries with GROUP BY (but no aggregates) specified.
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.hindex 735ba09650..5725b4828e 100644--- a/src/include/optimizer/cost.h+++ b/src/include/optimizer/cost.h@@ -53,6 +53,7 @@ extern PGDLLIMPORT bool enable_indexonlyscan;
extern PGDLLIMPORT bool enable_bitmapscan;
extern PGDLLIMPORT bool enable_tidscan;
extern PGDLLIMPORT bool enable_sort;
+extern PGDLLIMPORT bool enable_incrementalsort;
extern PGDLLIMPORT bool enable_hashagg;
extern PGDLLIMPORT bool enable_hashagg_disk;
extern PGDLLIMPORT bool enable_groupingsets_hash_disk;
@@ -103,6 +104,15 @@ extern void cost_sort(Path *path, PlannerInfo *root,
List *pathkeys, Cost input_cost, double tuples, int width,
Cost comparison_cost, int sort_mem,
double limit_tuples);
+extern void cost_full_sort(Cost *startup_cost, Cost *run_cost,+ Cost input_total_cost, double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples);+extern void cost_incremental_sort(Path *path,+ PlannerInfo *root, List *pathkeys, int presorted_keys,+ Cost input_startup_cost, Cost input_total_cost,+ double input_tuples, int width, Cost comparison_cost, int sort_mem,+ double limit_tuples);
extern void cost_append(AppendPath *path);
extern void cost_merge_append(Path *path, PlannerInfo *root,
List *pathkeys, int n_streams,
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.hindex e450fe112a..bcd08af753 100644--- a/src/include/optimizer/pathnode.h+++ b/src/include/optimizer/pathnode.h@@ -184,6 +184,12 @@ extern ProjectSetPath *create_set_projection_path(PlannerInfo *root,
RelOptInfo *rel,
Path *subpath,
PathTarget *target);
+extern SortPath *create_incremental_sort_path(PlannerInfo *root,+ RelOptInfo *rel,+ Path *subpath,+ List *pathkeys,+ int presorted_keys,+ double limit_tuples);
extern SortPath *create_sort_path(PlannerInfo *root,
RelOptInfo *rel,
Path *subpath,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.hindex 9ab73bd20c..85f5fe37ea 100644--- a/src/include/optimizer/paths.h+++ b/src/include/optimizer/paths.h@@ -188,6 +188,8 @@ typedef enum
extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
extern bool pathkeys_contained_in(List *keys1, List *keys2);
+extern bool pathkeys_common_contained_in(List *keys1, List *keys2, int *n_common);+extern int pathkeys_common(List *keys1, List *keys2);
extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
Relids required_outer,
CostSelector cost_criterion,
diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.hindex a2fdd3fcd3..0e9ab4e586 100644--- a/src/include/utils/tuplesort.h+++ b/src/include/utils/tuplesort.h@@ -215,6 +215,7 @@ extern Tuplesortstate *tuplesort_begin_datum(Oid datumType,
bool randomAccess);
extern void tuplesort_set_bound(Tuplesortstate *state, int64 bound);
+extern bool tuplesort_used_bound(Tuplesortstate *state);
extern void tuplesort_puttupleslot(Tuplesortstate *state,
TupleTableSlot *slot);
@@ -239,6 +240,8 @@ extern bool tuplesort_skiptuples(Tuplesortstate *state, int64 ntuples,
extern void tuplesort_end(Tuplesortstate *state);
+extern void tuplesort_reset(Tuplesortstate *state);+
extern void tuplesort_get_stats(Tuplesortstate *state,
TuplesortInstrumentation *stats);
extern const char *tuplesort_method_name(TuplesortMethod m);
diff --git a/src/test/isolation/expected/drop-index-concurrently-1.out b/src/test/isolation/expected/drop-index-concurrently-1.outindex 75dff56bc4..8e6adb66bb 100644--- a/src/test/isolation/expected/drop-index-concurrently-1.out+++ b/src/test/isolation/expected/drop-index-concurrently-1.out@@ -21,7 +21,7 @@ QUERY PLAN
Sort
Sort Key: id, data
- -> Seq Scan on test_dc+ -> Index Scan using test_dc_pkey on test_dc
Filter: ((data)::text = '34'::text)
step select2: SELECT * FROM test_dc WHERE data=34 ORDER BY id,data;
id data
diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.outnew file mode 100644index 0000000000..4f6f2288a3--- /dev/null+++ b/src/test/regress/expected/incremental_sort.out@@ -0,0 +1,1320 @@+-- When we have to sort the entire table, incremental sort will+-- be slower than plain sort, so it should not be used.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;+ QUERY PLAN +-----------------------------------+ Sort+ Sort Key: tenk1.four, tenk1.ten+ -> Sort+ Sort Key: tenk1.four+ -> Seq Scan on tenk1+(5 rows)++-- When there is a LIMIT clause, incremental sort is beneficial because+-- it only has to sort some of the groups, and not the entire table.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten+limit 1;+ QUERY PLAN +-----------------------------------------+ Limit+ -> Incremental Sort+ Sort Key: tenk1.four, tenk1.ten+ Presorted Key: tenk1.four+ -> Sort+ Sort Key: tenk1.four+ -> Seq Scan on tenk1+(7 rows)++-- When work_mem is not enough to sort the entire table, incremental sort+-- may be faster if individual groups still fit into work_mem.+set work_mem to '2MB';+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;+ QUERY PLAN +-----------------------------------+ Incremental Sort+ Sort Key: tenk1.four, tenk1.ten+ Presorted Key: tenk1.four+ -> Sort+ Sort Key: tenk1.four+ -> Seq Scan on tenk1+(6 rows)++reset work_mem;+-- TODO if an analyze happens here the plans might change; should we+-- solve by inserting extra rows or by adding a GUC that would somehow+-- forcing the time of plan we expect.+create table t(a integer, b integer);+-- A single large group tested around each mode transition point.+insert into t(a, b) select 1, i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 31;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+(31 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 32;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+(32 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 33;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+(33 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 65;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+ 1 | 34+ 1 | 35+ 1 | 36+ 1 | 37+ 1 | 38+ 1 | 39+ 1 | 40+ 1 | 41+ 1 | 42+ 1 | 43+ 1 | 44+ 1 | 45+ 1 | 46+ 1 | 47+ 1 | 48+ 1 | 49+ 1 | 50+ 1 | 51+ 1 | 52+ 1 | 53+ 1 | 54+ 1 | 55+ 1 | 56+ 1 | 57+ 1 | 58+ 1 | 59+ 1 | 60+ 1 | 61+ 1 | 62+ 1 | 63+ 1 | 64+ 1 | 65+(65 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 66;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+ 1 | 34+ 1 | 35+ 1 | 36+ 1 | 37+ 1 | 38+ 1 | 39+ 1 | 40+ 1 | 41+ 1 | 42+ 1 | 43+ 1 | 44+ 1 | 45+ 1 | 46+ 1 | 47+ 1 | 48+ 1 | 49+ 1 | 50+ 1 | 51+ 1 | 52+ 1 | 53+ 1 | 54+ 1 | 55+ 1 | 56+ 1 | 57+ 1 | 58+ 1 | 59+ 1 | 60+ 1 | 61+ 1 | 62+ 1 | 63+ 1 | 64+ 1 | 65+ 1 | 66+(66 rows)++delete from t;+-- An initial large group followed by a small group.+insert into t(a, b) select (case when i < 50 then 1 else 2 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 55;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 55;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+ 1 | 34+ 1 | 35+ 1 | 36+ 1 | 37+ 1 | 38+ 1 | 39+ 1 | 40+ 1 | 41+ 1 | 42+ 1 | 43+ 1 | 44+ 1 | 45+ 1 | 46+ 1 | 47+ 1 | 48+ 1 | 49+ 2 | 50+ 2 | 51+ 2 | 52+ 2 | 53+ 2 | 54+ 2 | 55+(55 rows)++-- Test EXPLAIN ANALYZE (text output) with only a fullsort group.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 55;+ QUERY PLAN +-------------------------------------------------------------------------------------------------+ Limit (actual rows=55 loops=1)+ -> Incremental Sort (actual rows=55 loops=1)+ Sort Key: t.a, t.b+ Presorted Key: t.a+ Full-sort Groups: 2 (Methods: quicksort, top-N heapsort) Memory: 27kB (avg), 27kB (max)+ -> Sort (actual rows=100 loops=1)+ Sort Key: t.a+ Sort Method: quicksort Memory: 30kB+ -> Seq Scan on t (actual rows=100 loops=1)+(9 rows)++explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 55;+ QUERY PLAN +-------------------------------------------------------------------+ [ ++ { ++ "Plan": { ++ "Node Type": "Limit", ++ "Parallel Aware": false, ++ "Actual Rows": 55, ++ "Actual Loops": 1, ++ "Plans": [ ++ { ++ "Node Type": "Incremental Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 55, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a", "t.b"], ++ "Presorted Key": ["t.a"], ++ "Full-sort Groups": { ++ "Group Count": 2, ++ "Sort Methods Used": ["quicksort", "top-N heapsort"],++ "Average Sort Space Used": 27, ++ "Maximum Sort Space Used": 27, ++ "Sort Space Type": "Memory" ++ }, ++ "Plans": [ ++ { ++ "Node Type": "Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 100, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a"], ++ "Sort Method": "quicksort", ++ "Sort Space Used": 30, ++ "Sort Space Type": "Memory", ++ "Plans": [ ++ { ++ "Node Type": "Seq Scan", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Relation Name": "t", ++ "Alias": "t", ++ "Actual Rows": 100, ++ "Actual Loops": 1 ++ } ++ ] ++ } ++ ] ++ } ++ ] ++ }, ++ "Triggers": [ ++ ] ++ } ++ ]+(1 row)++delete from t;+-- An initial small group followed by a large group.+insert into t(a, b) select (case when i < 5 then i else 9 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 70;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 70;+ a | b +---+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 9 | 5+ 9 | 6+ 9 | 7+ 9 | 8+ 9 | 9+ 9 | 10+ 9 | 11+ 9 | 12+ 9 | 13+ 9 | 14+ 9 | 15+ 9 | 16+ 9 | 17+ 9 | 18+ 9 | 19+ 9 | 20+ 9 | 21+ 9 | 22+ 9 | 23+ 9 | 24+ 9 | 25+ 9 | 26+ 9 | 27+ 9 | 28+ 9 | 29+ 9 | 30+ 9 | 31+ 9 | 32+ 9 | 33+ 9 | 34+ 9 | 35+ 9 | 36+ 9 | 37+ 9 | 38+ 9 | 39+ 9 | 40+ 9 | 41+ 9 | 42+ 9 | 43+ 9 | 44+ 9 | 45+ 9 | 46+ 9 | 47+ 9 | 48+ 9 | 49+ 9 | 50+ 9 | 51+ 9 | 52+ 9 | 53+ 9 | 54+ 9 | 55+ 9 | 56+ 9 | 57+ 9 | 58+ 9 | 59+ 9 | 60+ 9 | 61+ 9 | 62+ 9 | 63+ 9 | 64+ 9 | 65+ 9 | 66+ 9 | 67+ 9 | 68+ 9 | 69+ 9 | 70+(70 rows)++-- Test EXPLAIN ANALYZE (text output) with both fullsort and presorted groups.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 70;+ QUERY PLAN +---------------------------------------------------------------------------------+ Limit (actual rows=70 loops=1)+ -> Incremental Sort (actual rows=70 loops=1)+ Sort Key: t.a, t.b+ Presorted Key: t.a+ Full-sort Groups: 1 (Methods: quicksort) Memory: 28kB (avg), 28kB (max)+ Presorted Groups: 5 (Methods: quicksort) Memory: 25kB (avg), 25kB (max)+ -> Sort (actual rows=100 loops=1)+ Sort Key: t.a+ Sort Method: quicksort Memory: 30kB+ -> Seq Scan on t (actual rows=100 loops=1)+(10 rows)++explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 70;+ QUERY PLAN +---------------------------------------------------+ [ ++ { ++ "Plan": { ++ "Node Type": "Limit", ++ "Parallel Aware": false, ++ "Actual Rows": 70, ++ "Actual Loops": 1, ++ "Plans": [ ++ { ++ "Node Type": "Incremental Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 70, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a", "t.b"], ++ "Presorted Key": ["t.a"], ++ "Full-sort Groups": { ++ "Group Count": 1, ++ "Sort Methods Used": ["quicksort"], ++ "Average Sort Space Used": 28, ++ "Maximum Sort Space Used": 28, ++ "Sort Space Type": "Memory" ++ }, ++ "Presorted Groups": { ++ "Group Count": 5, ++ "Sort Methods Used": ["quicksort"], ++ "Average Sort Space Used": 25, ++ "Maximum Sort Space Used": 25, ++ "Sort Space Type": "Memory" ++ }, ++ "Plans": [ ++ { ++ "Node Type": "Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 100, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a"], ++ "Sort Method": "quicksort", ++ "Sort Space Used": 30, ++ "Sort Space Type": "Memory", ++ "Plans": [ ++ { ++ "Node Type": "Seq Scan", ++ "Parent Relationship": "Outer",++ "Parallel Aware": false, ++ "Relation Name": "t", ++ "Alias": "t", ++ "Actual Rows": 100, ++ "Actual Loops": 1 ++ } ++ ] ++ } ++ ] ++ } ++ ] ++ }, ++ "Triggers": [ ++ ] ++ } ++ ]+(1 row)++delete from t;+-- Small groups of 10 tuples each tested around each mode transition point.+insert into t(a, b) select i / 10, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 31;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+(31 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 32;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+(32 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 33;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+ 3 | 33+(33 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 65;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+ 3 | 33+ 3 | 34+ 3 | 35+ 3 | 36+ 3 | 37+ 3 | 38+ 3 | 39+ 4 | 40+ 4 | 41+ 4 | 42+ 4 | 43+ 4 | 44+ 4 | 45+ 4 | 46+ 4 | 47+ 4 | 48+ 4 | 49+ 5 | 50+ 5 | 51+ 5 | 52+ 5 | 53+ 5 | 54+ 5 | 55+ 5 | 56+ 5 | 57+ 5 | 58+ 5 | 59+ 6 | 60+ 6 | 61+ 6 | 62+ 6 | 63+ 6 | 64+ 6 | 65+(65 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 66;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+ 3 | 33+ 3 | 34+ 3 | 35+ 3 | 36+ 3 | 37+ 3 | 38+ 3 | 39+ 4 | 40+ 4 | 41+ 4 | 42+ 4 | 43+ 4 | 44+ 4 | 45+ 4 | 46+ 4 | 47+ 4 | 48+ 4 | 49+ 5 | 50+ 5 | 51+ 5 | 52+ 5 | 53+ 5 | 54+ 5 | 55+ 5 | 56+ 5 | 57+ 5 | 58+ 5 | 59+ 6 | 60+ 6 | 61+ 6 | 62+ 6 | 63+ 6 | 64+ 6 | 65+ 6 | 66+(66 rows)++delete from t;+-- Small groups of only 1 tuple each tested around each mode transition point.+insert into t(a, b) select i, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 31;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+(31 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 32;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+(32 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 33;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+ 33 | 33+(33 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 65;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+ 33 | 33+ 34 | 34+ 35 | 35+ 36 | 36+ 37 | 37+ 38 | 38+ 39 | 39+ 40 | 40+ 41 | 41+ 42 | 42+ 43 | 43+ 44 | 44+ 45 | 45+ 46 | 46+ 47 | 47+ 48 | 48+ 49 | 49+ 50 | 50+ 51 | 51+ 52 | 52+ 53 | 53+ 54 | 54+ 55 | 55+ 56 | 56+ 57 | 57+ 58 | 58+ 59 | 59+ 60 | 60+ 61 | 61+ 62 | 62+ 63 | 63+ 64 | 64+ 65 | 65+(65 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 66;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+ 33 | 33+ 34 | 34+ 35 | 35+ 36 | 36+ 37 | 37+ 38 | 38+ 39 | 39+ 40 | 40+ 41 | 41+ 42 | 42+ 43 | 43+ 44 | 44+ 45 | 45+ 46 | 46+ 47 | 47+ 48 | 48+ 49 | 49+ 50 | 50+ 51 | 51+ 52 | 52+ 53 | 53+ 54 | 54+ 55 | 55+ 56 | 56+ 57 | 57+ 58 | 58+ 59 | 59+ 60 | 60+ 61 | 61+ 62 | 62+ 63 | 63+ 64 | 64+ 65 | 65+ 66 | 66+(66 rows)++delete from t;+drop table t;diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.outindex fbc8d3ac6c..4425853572 100644--- a/src/test/regress/expected/partition_aggregate.out+++ b/src/test/regress/expected/partition_aggregate.out@@ -8,6 +8,8 @@ SET enable_partitionwise_aggregate TO true;
SET enable_partitionwise_join TO true;
-- Disable parallel plans.
SET max_parallel_workers_per_gather TO 0;
+-- Disable incremental sort, which can influence selected plans due to fuzz factor.+SET enable_incrementalsort TO off;
--
-- Tests for list partitioned tables.
--
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.outindex 715842b87a..a126f0ad61 100644--- a/src/test/regress/expected/sysviews.out+++ b/src/test/regress/expected/sysviews.out@@ -78,6 +78,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_hashagg | on
enable_hashagg_disk | on
enable_hashjoin | on
+ enable_incrementalsort | on
enable_indexonlyscan | on
enable_indexscan | on
enable_material | on
@@ -91,7 +92,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_seqscan | on
enable_sort | on
enable_tidscan | on
-(19 rows)+(20 rows)
-- Test that the pg_timezone_names and pg_timezone_abbrevs views are
-- more-or-less working. We can't test their contents in any great detail
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_scheduleindex d2b17dd3ea..175c1d5a49 100644--- a/src/test/regress/parallel_schedule+++ b/src/test/regress/parallel_schedule@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tidscan collate.icu.utf8+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tidscan collate.icu.utf8 incremental_sort
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_scheduleindex acba391332..2bcd994361 100644--- a/src/test/regress/serial_schedule+++ b/src/test/regress/serial_schedule@@ -88,6 +88,7 @@ test: select_distinct_on
test: select_implicit
test: select_having
test: subselect
+test: incremental_sort
test: union
test: case
test: join
diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sqlnew file mode 100644index 0000000000..9320a10b91--- /dev/null+++ b/src/test/regress/sql/incremental_sort.sql@@ -0,0 +1,88 @@+-- When we have to sort the entire table, incremental sort will+-- be slower than plain sort, so it should not be used.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;++-- When there is a LIMIT clause, incremental sort is beneficial because+-- it only has to sort some of the groups, and not the entire table.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten+limit 1;++-- When work_mem is not enough to sort the entire table, incremental sort+-- may be faster if individual groups still fit into work_mem.+set work_mem to '2MB';+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;+reset work_mem;++-- TODO if an analyze happens here the plans might change; should we+-- solve by inserting extra rows or by adding a GUC that would somehow+-- forcing the time of plan we expect.+create table t(a integer, b integer);++-- A single large group tested around each mode transition point.+insert into t(a, b) select 1, i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+select * from (select * from t order by a) s order by a, b limit 31;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+select * from (select * from t order by a) s order by a, b limit 32;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+select * from (select * from t order by a) s order by a, b limit 33;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+select * from (select * from t order by a) s order by a, b limit 65;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+select * from (select * from t order by a) s order by a, b limit 66;+delete from t;++-- An initial large group followed by a small group.+insert into t(a, b) select (case when i < 50 then 1 else 2 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 55;+select * from (select * from t order by a) s order by a, b limit 55;+-- Test EXPLAIN ANALYZE (text output) with only a fullsort group.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 55;+explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 55;+delete from t;++-- An initial small group followed by a large group.+insert into t(a, b) select (case when i < 5 then i else 9 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 70;+select * from (select * from t order by a) s order by a, b limit 70;+-- Test EXPLAIN ANALYZE (text output) with both fullsort and presorted groups.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 70;+explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 70;+delete from t;++-- Small groups of 10 tuples each tested around each mode transition point.+insert into t(a, b) select i / 10, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+select * from (select * from t order by a) s order by a, b limit 31;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+select * from (select * from t order by a) s order by a, b limit 32;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+select * from (select * from t order by a) s order by a, b limit 33;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+select * from (select * from t order by a) s order by a, b limit 65;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+select * from (select * from t order by a) s order by a, b limit 66;+delete from t;++-- Small groups of only 1 tuple each tested around each mode transition point.+insert into t(a, b) select i, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+select * from (select * from t order by a) s order by a, b limit 31;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+select * from (select * from t order by a) s order by a, b limit 32;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+select * from (select * from t order by a) s order by a, b limit 33;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+select * from (select * from t order by a) s order by a, b limit 65;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+select * from (select * from t order by a) s order by a, b limit 66;+delete from t;++drop table t;diff --git a/src/test/regress/sql/partition_aggregate.sql b/src/test/regress/sql/partition_aggregate.sqlindex dcd6edbad2..6a8db29a07 100644--- a/src/test/regress/sql/partition_aggregate.sql+++ b/src/test/regress/sql/partition_aggregate.sql@@ -9,6 +9,8 @@ SET enable_partitionwise_aggregate TO true;
SET enable_partitionwise_join TO true;
-- Disable parallel plans.
SET max_parallel_workers_per_gather TO 0;
+-- Disable incremental sort, which can influence selected plans due to fuzz factor.+SET enable_incrementalsort TO off;
--
-- Tests for list partitioned tables.
--
2.21.1
From d0ee7837d2e8137390f6e64188ecbd4a1addb11d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 16:55:28 +0100
Subject: [PATCH 4/8] fix
---
src/backend/commands/explain.c | 6 ++--
src/backend/executor/execProcnode.c | 11 ++++----
src/backend/optimizer/plan/planner.c | 41 +++++++++++++++++-----------
src/backend/utils/sort/tuplesort.c | 10 +++++--
src/include/nodes/plannodes.h | 1 -
5 files changed, 42 insertions(+), 27 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.cindex dd4600a214..0256dd42f1 100644--- a/src/backend/commands/explain.c+++ b/src/backend/commands/explain.c@@ -2767,7 +2767,7 @@ show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
foreach(methodCell, groupInfo->sortMethods)
{
- const *sortMethodName = tuplesort_method_name(methodCell->int_value);+ const char *sortMethodName = tuplesort_method_name(methodCell->int_value);
methodNames = lappend(methodNames, sortMethodName);
}
@@ -2776,7 +2776,7 @@ show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
if (groupInfo->maxMemorySpaceUsed > 0)
{
long avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;
- const *spaceTypeName;+ const char *spaceTypeName;
ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);
ExplainPropertyInteger("Maximum Sort Space Used", "kB",
@@ -2787,7 +2787,7 @@ show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
if (groupInfo->maxDiskSpaceUsed > 0)
{
long avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;
- const *spaceTypeName;+ const char *spaceTypeName;
ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);
ExplainPropertyInteger("Maximum Sort Space Used", "kB",
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.cindex d15a86a706..5662e7d742 100644--- a/src/backend/executor/execProcnode.c+++ b/src/backend/executor/execProcnode.c@@ -852,12 +852,13 @@ ExecSetTupleBound(int64 tuples_needed, PlanState *child_node)
else if (IsA(child_node, IncrementalSortState))
{
/*
- * If it is a Sort node, notify it that it can use bounded sort.+ * If it is an IncrementalSort node, notify it that it can use bounded+ * sort.
*
- * Note: it is the responsibility of nodeSort.c to react properly to- * changes of these parameters. If we ever redesign this, it'd be a- * good idea to integrate this signaling with the parameter-change- * mechanism.+ * Note: it is the responsibility of nodeIncrementalSort.c to react+ * properly to changes of these parameters. If we ever redesign this,+ * it'd be a good idea to integrate this signaling with the+ * parameter-change mechanism.
*/
IncrementalSortState *sortState = (IncrementalSortState *) child_node;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex b2f4aaadb5..0e01cf8cb1 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -4869,7 +4869,7 @@ create_distinct_paths(PlannerInfo *root,
else
{
Size hashentrysize = hash_agg_entry_size(
- 0, cheapest_input_path->pathtarget->width, 0);+ 0, cheapest_input_path->pathtarget->width, 0);
/* Allow hashing only if hashtable is predicted to fit in work_mem */
allow_hash = (hashentrysize * numDistinctRows <= work_mem * 1024L);
@@ -4932,6 +4932,9 @@ create_distinct_paths(PlannerInfo *root,
* target: the output tlist the result Paths must emit
* limit_tuples: estimated bound on the number of output tuples,
* or -1 if no LIMIT or couldn't estimate
+ *+ * XXX This only looks at sort_pathkeys. I wonder if it needs to look at the+ * other pathkeys (grouping, ...) like generate_useful_gather_paths.
*/
static RelOptInfo *
create_ordered_paths(PlannerInfo *root,
@@ -5002,23 +5005,29 @@ create_ordered_paths(PlannerInfo *root,
add_path(ordered_rel, sorted_path);
}
- if (enable_incrementalsort && presorted_keys > 0)- {- /* Also consider incremental sort. */- sorted_path = (Path *) create_incremental_sort_path(root,- ordered_rel,- input_path,- root->sort_pathkeys,- presorted_keys,- limit_tuples);- /* Add projection step if needed */- if (sorted_path->pathtarget != target)- sorted_path = apply_projection_to_path(root, ordered_rel,- sorted_path, target);+ /* With incremental sort disabled, don't build those paths. */+ if (!enable_incrementalsort)+ continue;- add_path(ordered_rel, sorted_path);- }+ /* Likewise, if the path can't be used for incremental sort. */+ if (!presorted_keys)+ continue;++ /* Also consider incremental sort. */+ sorted_path = (Path *) create_incremental_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ presorted_keys,+ limit_tuples);++ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);
}
}
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.cindex 583551d197..77c15ebd78 100644--- a/src/backend/utils/sort/tuplesort.c+++ b/src/backend/utils/sort/tuplesort.c@@ -256,8 +256,8 @@ struct Tuplesortstate
bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk
* space, false when it's value for in-memory
* space */
- TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */- MemoryContext maincontext; /* memory context for tuple sort metadata that+ TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */+ MemoryContext maincontext; /* memory context for tuple sort metadata that
* persists across multiple batches */
MemoryContext sortcontext; /* memory context holding most sort data */
MemoryContext tuplecontext; /* sub-context of sortcontext for tuple data */
@@ -803,6 +803,9 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
return state;
}
+/*+ * XXX Missing comment.+ */
static void
tuplesort_begin_batch(Tuplesortstate *state)
{
@@ -1288,6 +1291,9 @@ tuplesort_set_bound(Tuplesortstate *state, int64 bound)
state->sortKeys->abbrev_full_comparator = NULL;
}
+/*+ * XXX Missing comment.+ */
bool
tuplesort_used_bound(Tuplesortstate *state)
{
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.hindex fe4046b64b..136d794219 100644--- a/src/include/nodes/plannodes.h+++ b/src/include/nodes/plannodes.h@@ -774,7 +774,6 @@ typedef struct Sort
bool *nullsFirst; /* NULLS FIRST/LAST directions */
} Sort;
-
/* ----------------
* incremental sort node
* ----------------
--
2.21.1
From 1baba95ea4d800de7d9ffcd24f04cec279cde518 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 28 Jul 2019 15:59:05 +0200
Subject: [PATCH 5/8] Consider incremental sort paths in additional places
---
src/backend/optimizer/path/allpaths.c | 221 +++++++++++++++++++++++++-
src/backend/optimizer/plan/planner.c | 130 ++++++++++++++-
src/include/optimizer/paths.h | 2 +
3 files changed, 350 insertions(+), 3 deletions(-)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.cindex ccf46dd0aa..9a92948fe3 100644--- a/src/backend/optimizer/path/allpaths.c+++ b/src/backend/optimizer/path/allpaths.c@@ -556,7 +556,7 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
*/
if (rel->reloptkind == RELOPT_BASEREL &&
bms_membership(root->all_baserels) != BMS_SINGLETON)
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/* Now find the cheapest of the paths for this rel */
set_cheapest(rel);
@@ -2727,6 +2727,223 @@ generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows)
}
}
+/*+ * Find an equivalence class member expression, all of whose Vars, come from+ * the indicated relation.+ */+static 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;+}++/*+ * get_useful_pathkeys_for_relation+ * Determine which orderings of a relation might be useful.+ *+ * Getting data in sorted order can be useful either because the requested+ * order matches the final output ordering for the overall query we're+ * planning, or because it enables an efficient merge join. Here, we try+ * to figure out which pathkeys to consider.+ */+static List *+get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)+{+ List *useful_pathkeys_list = NIL;+ ListCell *lc;++ /*+ * Pushing the query_pathkeys to the remote server is always worth+ * considering, because it might let us avoid a local sort.+ */+ if (root->query_pathkeys)+ {+ bool query_pathkeys_ok = true;++ foreach(lc, root->query_pathkeys)+ {+ PathKey *pathkey = (PathKey *) lfirst(lc);+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;+ Expr *em_expr;++ /*+ * The planner and executor don't have any clever strategy for+ * taking data sorted by a prefix of the query's pathkeys and+ * getting it to be sorted by all of those pathkeys. We'll just+ * end up re-sorting the entire data set. So, unless we can push+ * down all of the query pathkeys, forget it.+ *+ * is_foreign_expr would detect volatile expressions as well, but+ * checking ec_has_volatile here saves some cycles.+ */+ if (pathkey_ec->ec_has_volatile ||+ !(em_expr = find_em_expr_for_rel(pathkey_ec, rel)))+ {+ query_pathkeys_ok = false;+ break;+ }+ }++ /*+ * This ends up allowing us to do incremental sort on top of an index+ * scan all parallelized under a gather merge node.+ */+ if (query_pathkeys_ok)+ useful_pathkeys_list = list_make1(list_copy(root->query_pathkeys));+ }++ return useful_pathkeys_list;+}++/*+ * generate_useful_gather_paths+ * Generate parallel access paths for a relation by pushing a Gather or+ * Gather Merge on top of a partial path.+ *+ * Unlike generate_gather_paths, this does not look only at pathkeys of the+ * input paths (aiming to preserve the ordering). It also considers ordering+ * that might be useful by nodes above the gather merge node, and tries to+ * add a sort (regular or incremental) to provide that.+ */+void+generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows)+{+ ListCell *lc;+ double rows;+ double *rowsp = NULL;+ List *useful_pathkeys_list = NIL;+ Path *cheapest_partial_path = NULL;++ /* If there are no partial paths, there's nothing to do here. */+ if (rel->partial_pathlist == NIL)+ return;++ /* Should we override the rel's rowcount estimate? */+ if (override_rows)+ rowsp = &rows;++ /* generate the regular gather merge paths */+ generate_gather_paths(root, rel, override_rows);++ /* when incremental sort is disabled, we're done */+ if (!enable_incrementalsort)+ return;++ /* consider incremental sort for interesting orderings */+ useful_pathkeys_list = get_useful_pathkeys_for_relation(root, rel);++ /* used for explicit sort paths */+ cheapest_partial_path = linitial(rel->partial_pathlist);++ /*+ * Consider incremental sort paths for each interesting ordering.+ *+ * XXX I wonder if we need to consider adding a projection here, as+ * create_ordered_paths does.+ */+ foreach(lc, useful_pathkeys_list)+ {+ List *useful_pathkeys = lfirst(lc);+ ListCell *lc2;+ bool is_sorted;+ int presorted_keys;++ foreach(lc2, rel->partial_pathlist)+ {+ Path *subpath = (Path *) lfirst(lc2);+ GatherMergePath *path;++ /* path has no ordering at all, can't use incremental sort */+ if (subpath->pathkeys == NIL)+ continue;++ is_sorted = pathkeys_common_contained_in(useful_pathkeys,+ subpath->pathkeys,+ &presorted_keys);++ if (is_sorted)+ {+ path = create_gather_merge_path(root, rel, subpath, rel->reltarget,+ subpath->pathkeys, NULL, rowsp);++ add_path(rel, &path->path);+ continue;+ }++ Assert(!is_sorted);++ /*+ * consider regular sort for cheapest partial path (for each+ * useful pathkeys)+ */+ if (cheapest_partial_path == subpath)+ {+ Path *tmp;++ tmp = (Path *) create_sort_path(root,+ rel,+ subpath,+ useful_pathkeys,+ -1.0);++ rows = tmp->rows * tmp->parallel_workers;++ path = create_gather_merge_path(root, rel,+ tmp,+ rel->reltarget,+ tmp->pathkeys,+ NULL,+ rowsp);++ add_path(rel, &path->path);++ /* Fall through */+ }++ /* Also consider incremental sort */+ if (presorted_keys > 0)+ {+ Path *tmp;++ tmp = (Path *) create_incremental_sort_path(root,+ rel,+ subpath,+ useful_pathkeys,+ presorted_keys,+ -1);++ path = create_gather_merge_path(root, rel,+ tmp,+ rel->reltarget,+ tmp->pathkeys,+ NULL,+ rowsp);++ add_path(rel, &path->path);+ }+ }+ }+}+
/*
* make_rel_from_joinlist
* Build access paths using a "joinlist" to guide the join path search.
@@ -2899,7 +3116,7 @@ standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels)
* once we know the final targetlist (see grouping_planner).
*/
if (lev < levels_needed)
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/* Find and save the cheapest paths for this rel */
set_cheapest(rel);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex 0e01cf8cb1..46dc355af3 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -6434,7 +6434,9 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
foreach(lc, input_rel->pathlist)
{
Path *path = (Path *) lfirst(lc);
+ Path *path_original = path;
bool is_sorted;
+ int presorted_keys;
is_sorted = pathkeys_contained_in(root->group_pathkeys,
path->pathkeys);
@@ -6493,6 +6495,80 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
Assert(false);
}
}
++ /*+ * Now we may consider incremental sort on this path, but only+ * when the path is not already sorted and when incremental sort+ * is enabled.+ */+ if (is_sorted || !enable_incrementalsort)+ continue;++ /* Restore the input path (we might have added Sort on top). */+ path = path_original;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* We've already skipped fully sorted paths above. */+ Assert(!is_sorted);++ /* no shared prefix, not point in building incremental sort */+ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ /* Now decide what to stick atop it */+ if (parse->groupingSets)+ {+ consider_groupingsets_paths(root, grouped_rel,+ path, true, can_hash,+ gd, agg_costs, dNumGroups);+ }+ else if (parse->hasAggs)+ {+ /*+ * We have aggregation, possibly with plain GROUP BY. Make+ * an AggPath.+ */+ add_path(grouped_rel, (Path *)+ create_agg_path(root,+ grouped_rel,+ path,+ grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_SIMPLE,+ parse->groupClause,+ havingQual,+ agg_costs,+ dNumGroups));+ }+ else if (parse->groupClause)+ {+ /*+ * We have GROUP BY without aggregation or grouping sets.+ * Make a GroupPath.+ */+ add_path(grouped_rel, (Path *)+ create_group_path(root,+ grouped_rel,+ path,+ parse->groupClause,+ havingQual,+ dNumGroups));+ }+ else+ {+ /* Other cases should have been handled above */+ Assert(false);+ }
}
/*
@@ -6819,7 +6895,9 @@ create_partial_grouping_paths(PlannerInfo *root,
foreach(lc, input_rel->partial_pathlist)
{
Path *path = (Path *) lfirst(lc);
+ Path *path_original = path;
bool is_sorted;
+ int presorted_keys;
is_sorted = pathkeys_contained_in(root->group_pathkeys,
path->pathkeys);
@@ -6854,6 +6932,56 @@ create_partial_grouping_paths(PlannerInfo *root,
NIL,
dNumPartialPartialGroups));
}
++ /*+ * Now we may consider incremental sort on this path, but only+ * when the path is not already sorted and when incremental sort+ * is enabled.+ */+ if (is_sorted || !enable_incrementalsort)+ continue;++ /* Restore the input path (we might have added Sort on top). */+ path = path_original;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* We've already skipped fully sorted paths above. */+ Assert(!is_sorted);++ /* no shared prefix, not point in building incremental sort */+ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ partially_grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ if (parse->hasAggs)+ add_partial_path(partially_grouped_rel, (Path *)+ create_agg_path(root,+ partially_grouped_rel,+ path,+ partially_grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_INITIAL_SERIAL,+ parse->groupClause,+ NIL,+ agg_partial_costs,+ dNumPartialPartialGroups));+ else+ add_partial_path(partially_grouped_rel, (Path *)+ create_group_path(root,+ partially_grouped_rel,+ path,+ parse->groupClause,+ NIL,+ dNumPartialPartialGroups));
}
}
@@ -7235,7 +7363,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
* one of the generated paths may turn out to be the cheapest one.
*/
if (rel->consider_parallel && !IS_OTHER_REL(rel))
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/*
* Reassess which paths are the cheapest, now that we've potentially added
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.hindex 85f5fe37ea..f6994779de 100644--- a/src/include/optimizer/paths.h+++ b/src/include/optimizer/paths.h@@ -54,6 +54,8 @@ extern RelOptInfo *standard_join_search(PlannerInfo *root, int levels_needed,
extern void generate_gather_paths(PlannerInfo *root, RelOptInfo *rel,
bool override_rows);
+extern void generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel,+ bool override_rows);
extern int compute_parallel_worker(RelOptInfo *rel, double heap_pages,
double index_pages, int max_workers);
extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
--
2.21.1
From 5752bf071168a1dfd16fa3ae045182ccd3e5ee81 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 18:26:16 +0100
Subject: [PATCH 6/8] fix
---
src/backend/optimizer/path/allpaths.c | 62 +++++++++++++++++----------
1 file changed, 39 insertions(+), 23 deletions(-)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.cindex 9a92948fe3..6838a238cd 100644--- a/src/backend/optimizer/path/allpaths.c+++ b/src/backend/optimizer/path/allpaths.c@@ -2764,6 +2764,14 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
* order matches the final output ordering for the overall query we're
* planning, or because it enables an efficient merge join. Here, we try
* to figure out which pathkeys to consider.
+ *+ * This allows us to do incremental sort on top of an index scan under a gather+ * merge node, i.e. parallelized.+ *+ * XXX At the moment this can only ever return a list with a single element,+ * because it looks at query_pathkeys only. So we might return the pathkeys+ * directly, but it seems plausible we'll want to consider other orderings+ * in the future.
*/
static List *
get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
@@ -2772,8 +2780,8 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
ListCell *lc;
/*
- * Pushing the query_pathkeys to the remote server is always worth- * considering, because it might let us avoid a local sort.+ * Considering query_pathkeys is always worth it, because it might let us+ * avoid a local sort.
*/
if (root->query_pathkeys)
{
@@ -2786,14 +2794,9 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
Expr *em_expr;
/*
- * The planner and executor don't have any clever strategy for- * taking data sorted by a prefix of the query's pathkeys and- * getting it to be sorted by all of those pathkeys. We'll just- * end up re-sorting the entire data set. So, unless we can push- * down all of the query pathkeys, forget it.- *- * is_foreign_expr would detect volatile expressions as well, but- * checking ec_has_volatile here saves some cycles.+ * We can't use incremental sort for pathkeys containing volatile+ * expressions. We could walk the exppression itself, but checking+ * ec_has_volatile here saves some cycles.
*/
if (pathkey_ec->ec_has_volatile ||
!(em_expr = find_em_expr_for_rel(pathkey_ec, rel)))
@@ -2803,10 +2806,6 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
}
}
- /*- * This ends up allowing us to do incremental sort on top of an index- * scan all parallelized under a gather merge node.- */
if (query_pathkeys_ok)
useful_pathkeys_list = list_make1(list_copy(root->query_pathkeys));
}
@@ -2819,10 +2818,10 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
* Generate parallel access paths for a relation by pushing a Gather or
* Gather Merge on top of a partial path.
*
- * Unlike generate_gather_paths, this does not look only at pathkeys of the- * input paths (aiming to preserve the ordering). It also considers ordering- * that might be useful by nodes above the gather merge node, and tries to- * add a sort (regular or incremental) to provide that.+ * Unlike plain generate_gather_paths, this looks both at pathkeys of input+ * paths (aiming to preserve the ordering), but also considers ordering that+ * might be useful for nodes above the gather merge node, and tries to add+ * a sort (regular or incremental) to provide that.
*/
void
generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows)
@@ -2841,7 +2840,7 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
if (override_rows)
rowsp = &rows;
- /* generate the regular gather merge paths */+ /* generate the regular gather (merge) paths */
generate_gather_paths(root, rel, override_rows);
/* when incremental sort is disabled, we're done */
@@ -2851,7 +2850,7 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
/* consider incremental sort for interesting orderings */
useful_pathkeys_list = get_useful_pathkeys_for_relation(root, rel);
- /* used for explicit sort paths */+ /* used for explicit (full) sort paths */
cheapest_partial_path = linitial(rel->partial_pathlist);
/*
@@ -2880,6 +2879,14 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
subpath->pathkeys,
&presorted_keys);
+ /*+ * When the partial path is already sorted, we can just add a gather+ * merge on top, and we're done - no point in adding explicit sort.+ *+ * XXX Can't we skip this (maybe only for the cheapest partial path)+ * when the path is already sorted? Then it's likely duplicate with+ * the path created by generate_gather_paths.+ */
if (is_sorted)
{
path = create_gather_merge_path(root, rel, subpath, rel->reltarget,
@@ -2892,8 +2899,14 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
Assert(!is_sorted);
/*
- * consider regular sort for cheapest partial path (for each- * useful pathkeys)+ * Consider regular sort for the cheapest partial path (for each+ * useful pathkeys). We know the path is not sorted, because we'd+ * not get here otherwise.+ *+ * XXX This is not redundant with the gather merge path created in+ * generate_gather_paths, because that merely preserves ordering of+ * the cheapest partial path, while here we add an explicit sort to+ * get match the useful ordering.
*/
if (cheapest_partial_path == subpath)
{
@@ -2919,7 +2932,10 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
/* Fall through */
}
- /* Also consider incremental sort */+ /*+ * Consider incremental sort, but only when the subpath is already+ * partially sorted on a pathkey prefix.+ */
if (presorted_keys > 0)
{
Path *tmp;
--
2.21.1
From 2e6cbe5bc169ebe1e9971b5256003d02349ad1ce Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 28 Jul 2019 16:03:56 +0200
Subject: [PATCH 7/8] A couple more places for incremental sort
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/plan/planner.c | 220 ++++++++++++++++++++++++-
2 files changed, 217 insertions(+), 5 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.cindex 6d897936d7..ff33acc7b6 100644--- a/src/backend/optimizer/geqo/geqo_eval.c+++ b/src/backend/optimizer/geqo/geqo_eval.c@@ -274,7 +274,7 @@ merge_clump(PlannerInfo *root, List *clumps, Clump *new_clump, int num_gene,
* grouping_planner).
*/
if (old_clump->size + new_clump->size < num_gene)
- generate_gather_paths(root, joinrel, false);+ generate_useful_gather_paths(root, joinrel, false);
/* Find and save the cheapest paths for this joinrel */
set_cheapest(joinrel);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex 46dc355af3..2880fcabe8 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -5080,6 +5080,67 @@ create_ordered_paths(PlannerInfo *root,
add_path(ordered_rel, path);
}
++ /*+ * Consider incremental sort with a gather merge on partial paths.+ *+ * XXX This is probably duplicate with the paths we already generate+ * in generate_useful_gather_paths in apply_scanjoin_target_to_paths.+ */+ if (enable_incrementalsort)+ {+ ListCell *lc;++ foreach(lc, input_rel->partial_pathlist)+ {+ Path *input_path = (Path *) lfirst(lc);+ Path *sorted_path = input_path;+ bool is_sorted;+ int presorted_keys;+ double total_groups;++ /*+ * We don't care if this is the cheapest partial path - we+ * can't simply skip it, because it may be partially sorted in+ * which case we want to consider incremental sort on top of+ * it (instead of full sort, which is what happens above).+ */++ is_sorted = pathkeys_common_contained_in(root->sort_pathkeys,+ input_path->pathkeys,+ &presorted_keys);++ /* Ignore already sorted paths */+ if (is_sorted)+ continue;++ if (presorted_keys == 0)+ continue;++ /* Since we have presorted keys, consider incremental sort. */+ sorted_path = (Path *) create_incremental_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ presorted_keys,+ limit_tuples);+ total_groups = input_path->rows *+ input_path->parallel_workers;+ sorted_path = (Path *)+ create_gather_merge_path(root, ordered_rel,+ sorted_path,+ sorted_path->pathtarget,+ root->sort_pathkeys, NULL,+ &total_groups);++ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);+ }+ }
}
/*
@@ -6580,12 +6641,18 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
foreach(lc, partially_grouped_rel->pathlist)
{
Path *path = (Path *) lfirst(lc);
+ Path *path_original = path;+ bool is_sorted;+ int presorted_keys;++ is_sorted = pathkeys_contained_in(root->group_pathkeys,+ path->pathkeys);
/*
* Insert a Sort node, if required. But there's no point in
* sorting anything but the cheapest path.
*/
- if (!pathkeys_contained_in(root->group_pathkeys, path->pathkeys))+ if (!is_sorted)
{
if (path != partially_grouped_rel->cheapest_total_path)
continue;
@@ -6616,6 +6683,56 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
parse->groupClause,
havingQual,
dNumGroups));
++ /*+ * Now we may consider incremental sort on this path, but only+ * when the path is not already sorted and when incremental+ * sort is enabled.+ */+ if (is_sorted || !enable_incrementalsort)+ continue;++ /* Restore the input path (we might have added Sort on top). */+ path = path_original;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* We've already skipped fully sorted paths above. */+ Assert(!is_sorted);++ /* no shared prefix, not point in building incremental sort */+ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ if (parse->hasAggs)+ add_path(grouped_rel, (Path *)+ create_agg_path(root,+ grouped_rel,+ path,+ grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_FINAL_DESERIAL,+ parse->groupClause,+ havingQual,+ agg_final_costs,+ dNumGroups));+ else+ add_path(grouped_rel, (Path *)+ create_group_path(root,+ grouped_rel,+ path,+ parse->groupClause,+ havingQual,+ dNumGroups));
}
}
}
@@ -6887,6 +7004,60 @@ create_partial_grouping_paths(PlannerInfo *root,
dNumPartialGroups));
}
}
++ /*+ * Also consider incremental sort on all partially sorted paths.+ */+ if (enable_incrementalsort)+ {+ foreach(lc, input_rel->pathlist)+ {+ Path *path = (Path *) lfirst(lc);+ bool is_sorted;+ int presorted_keys;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* Ignore already sorted paths */+ if (is_sorted)+ continue;++ if (presorted_keys == 0)+ continue;++ /* Since we have presorted keys, consider incremental sort. */+ path = (Path *) create_incremental_sort_path(root,+ partially_grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ if (parse->hasAggs)+ add_path(partially_grouped_rel, (Path *)+ create_agg_path(root,+ partially_grouped_rel,+ path,+ partially_grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_INITIAL_SERIAL,+ parse->groupClause,+ NIL,+ agg_partial_costs,+ dNumPartialGroups));+ else+ add_path(partially_grouped_rel, (Path *)+ create_group_path(root,+ partially_grouped_rel,+ path,+ parse->groupClause,+ NIL,+ dNumPartialGroups));+ }+ }+
}
if (can_sort && cheapest_partial_path != NULL)
@@ -6951,10 +7122,10 @@ create_partial_grouping_paths(PlannerInfo *root,
/* We've already skipped fully sorted paths above. */
Assert(!is_sorted);
- /* no shared prefix, not point in building incremental sort */
if (presorted_keys == 0)
continue;
+ /* Since we have presorted keys, consider incremental sort. */
path = (Path *) create_incremental_sort_path(root,
partially_grouped_rel,
path,
@@ -7079,10 +7250,11 @@ create_partial_grouping_paths(PlannerInfo *root,
static void
gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel)
{
+ ListCell *lc;
Path *cheapest_partial_path;
/* Try Gather for unordered paths and Gather Merge for ordered ones. */
- generate_gather_paths(root, rel, true);+ generate_useful_gather_paths(root, rel, true);
/* Try cheapest partial path + explicit Sort + Gather Merge. */
cheapest_partial_path = linitial(rel->partial_pathlist);
@@ -7108,6 +7280,46 @@ gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel)
add_path(rel, path);
}
++ if (!enable_incrementalsort)+ return;++ /* also consider incremental sort on partial paths, if enabled */+ foreach(lc, rel->partial_pathlist)+ {+ Path *path = (Path *) lfirst(lc);+ bool is_sorted;+ int presorted_keys;+ double total_groups;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ if (is_sorted)+ continue;++ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ path = (Path *)+ create_gather_merge_path(root,+ rel,+ path,+ rel->reltarget,+ root->group_pathkeys,+ NULL,+ &total_groups);++ add_path(rel, path);+ }
}
/*
@@ -7209,7 +7421,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
* paths by doing it after the final scan/join target has been
* applied.
*/
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/* Can't use parallel query above this level. */
rel->partial_pathlist = NIL;
--
2.21.1
From 59e9a858763a274405b5015344526092c6e55af5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 18:48:19 +0100
Subject: [PATCH 8/8] fix
---
src/backend/optimizer/plan/planner.c | 16 +++++++---------
1 file changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex 2880fcabe8..b4763e79f8 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -5100,17 +5100,17 @@ create_ordered_paths(PlannerInfo *root,
double total_groups;
/*
- * We don't care if this is the cheapest partial path - we- * can't simply skip it, because it may be partially sorted in- * which case we want to consider incremental sort on top of- * it (instead of full sort, which is what happens above).+ * We don't care if this is the cheapest partial path - we can't+ * simply skip it, because it may be partially sorted in which+ * case we want to consider adding incremental sort (instead of+ * full sort, which is what happens above).
*/
is_sorted = pathkeys_common_contained_in(root->sort_pathkeys,
input_path->pathkeys,
&presorted_keys);
- /* Ignore already sorted paths */+ /* No point in adding incremental sort on fully sorted paths. */
if (is_sorted)
continue;
@@ -7005,9 +7005,7 @@ create_partial_grouping_paths(PlannerInfo *root,
}
}
- /*- * Also consider incremental sort on all partially sorted paths.- */+ /* Consider incremental sort on all partial paths, if enabled. */
if (enable_incrementalsort)
{
foreach(lc, input_rel->pathlist)
@@ -7122,10 +7120,10 @@ create_partial_grouping_paths(PlannerInfo *root,
/* We've already skipped fully sorted paths above. */
Assert(!is_sorted);
+ /* no shared prefix, not point in building incremental sort */
if (presorted_keys == 0)
continue;
- /* Since we have presorted keys, consider incremental sort. */
path = (Path *) create_incremental_sort_path(root,
partially_grouped_rel,
path,
--
2.21.1
Attachments:
[text/plain] v39-0001-Consider-low-startup-cost-when-adding-partial-path.patch (3.2K, ../20200321005640.vrwcpyis6ahmspwt@development/2-v39-0001-Consider-low-startup-cost-when-adding-partial-path.patch)
download | inline diff:
From 6fd38a5d79b88202dee963d52629527e37693be6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 28 Jul 2019 15:55:54 +0200
Subject: [PATCH 1/8] Consider low startup cost when adding partial path
45be99f8cd5d606086e0a458c9c72910ba8a613d added `add_partial_path` with the
comment:
> Neither do we need to consider startup costs:
> parallelism is only used for plans that will be run to completion.
> Therefore, this routine is much simpler than add_path: it needs to
> consider only pathkeys and total cost.
I'm not entirely sure if that is still true or not--I can't easily come
up with a scenario in which it's not, but I also can't come up with an
inherent reason why such a scenario cannot exist.
Regardless, the in-progress incremental sort patch uncovered a new case
where it definitely no longer holds, and, as a result a higher cost plan
ends up being chosen because a low startup cost partial path is ignored
in favor of a lower total cost partial path and a limit is a applied on
top of that which would normal favor the lower startup cost plan.
---
src/backend/optimizer/util/pathnode.c | 47 ++++++++++-----------------
1 file changed, 18 insertions(+), 29 deletions(-)
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.cindex 8ba8122ee2..702189fe17 100644--- a/src/backend/optimizer/util/pathnode.c+++ b/src/backend/optimizer/util/pathnode.c@@ -777,41 +777,30 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
/* Unless pathkeys are incompatible, keep just one of the two paths. */
if (keyscmp != PATHKEYS_DIFFERENT)
{
- if (new_path->total_cost > old_path->total_cost * STD_FUZZ_FACTOR)- {- /* New path costs more; keep it only if pathkeys are better. */- if (keyscmp != PATHKEYS_BETTER1)- accept_new = false;- }- else if (old_path->total_cost > new_path->total_cost- * STD_FUZZ_FACTOR)+ PathCostComparison costcmp;++ /*+ * Do a fuzzy cost comparison with standard fuzziness limit.+ */+ costcmp = compare_path_costs_fuzzily(new_path, old_path,+ STD_FUZZ_FACTOR);++ if (costcmp == COSTS_BETTER1)
{
- /* Old path costs more; keep it only if pathkeys are better. */- if (keyscmp != PATHKEYS_BETTER2)+ if (keyscmp == PATHKEYS_BETTER1)
remove_old = true;
}
- else if (keyscmp == PATHKEYS_BETTER1)- {- /* Costs are about the same, new path has better pathkeys. */- remove_old = true;- }- else if (keyscmp == PATHKEYS_BETTER2)+ else if (costcmp == COSTS_BETTER2)
{
- /* Costs are about the same, old path has better pathkeys. */- accept_new = false;- }- else if (old_path->total_cost > new_path->total_cost * 1.0000000001)- {- /* Pathkeys are the same, and the old path costs more. */- remove_old = true;+ if (keyscmp == PATHKEYS_BETTER2)+ accept_new = false;
}
- else+ else if (costcmp == COSTS_EQUAL)
{
- /*- * Pathkeys are the same, and new path isn't materially- * cheaper.- */- accept_new = false;+ if (keyscmp == PATHKEYS_BETTER1)+ remove_old = true;+ else if (keyscmp == PATHKEYS_BETTER2)+ accept_new = false;
}
}
--
2.21.1
[text/plain] v39-0002-fix-comments.patch (2.2K, ../20200321005640.vrwcpyis6ahmspwt@development/3-v39-0002-fix-comments.patch)
download | inline diff:
From b7edae462892ebb9e87025b6801985d8e4b31824 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 16:26:37 +0100
Subject: [PATCH 2/8] fix comments
---
src/backend/optimizer/util/pathnode.c | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.cindex 702189fe17..9c8f3b1f0b 100644--- a/src/backend/optimizer/util/pathnode.c+++ b/src/backend/optimizer/util/pathnode.c@@ -733,10 +733,11 @@ add_path_precheck(RelOptInfo *parent_rel,
*
* Because we don't consider parameterized paths here, we also don't
* need to consider the row counts as a measure of quality: every path will
- * produce the same number of rows. Neither do we need to consider startup- * costs: parallelism is only used for plans that will be run to completion.- * Therefore, this routine is much simpler than add_path: it needs to- * consider only pathkeys and total cost.+ * produce the same number of rows. It may however matter how much the+ * path ordering matches the final ordering, needed by upper parts of the+ * plan, because that will affect how expensive the incremental sort is.+ * because of that we need to consider both the total and startup path,+ * in addition to pathkeys.
*
* As with add_path, we pfree paths that are found to be dominated by
* another partial path; this requires that there be no other references to
@@ -774,7 +775,14 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
/* Compare pathkeys. */
keyscmp = compare_pathkeys(new_path->pathkeys, old_path->pathkeys);
- /* Unless pathkeys are incompatible, keep just one of the two paths. */+ /*+ * Unless pathkeys are incompatible, see if one of the paths dominates+ * the other (both in startup and total cost). It may happen that one+ * path has lower startup cost, the other has lower total cost.+ *+ * XXX Perhaps we could do this only when incremental sort is enabled,+ * and use the simpler version (compring just total cost) otherwise?+ */
if (keyscmp != PATHKEYS_DIFFERENT)
{
PathCostComparison costcmp;
--
2.21.1
[text/plain] v39-0003-Implement-incremental-sort.patch (154.1K, ../20200321005640.vrwcpyis6ahmspwt@development/4-v39-0003-Implement-incremental-sort.patch)
download | inline diff:
From b4c6c28fbf438a02d1d20f0d22b8df3bb51ce922 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 15:25:55 +0100
Subject: [PATCH 3/8] Implement incremental sort
Incremental sort is an optimized variant of multikey sort for cases
when the input is already sorted by a prefix of the sort keys. For
example when a sort by (key1, key2 ... keyN) is requested, and the
input is already sorted by (key1, key2 ... keyM), M < N, we can
divide the input into groups where keys (key1, ... keyM) are equal,
and only sort on the remaining columns.
The implemented algorithm operates in two different modes:
- Fetching a minimum number of tuples without checking prefix key
group membership and sorting on all columns when safe.
- Fetching all tuples for a single prefix key group and sorting on
solely the unsorted columns.
We always begin in the first mode, and employ a heuristic to switch
into the second mode if we believe it's beneficial.
Sorting incrementally can potentially use less memory (and possibly
avoid spilling to disk), avoid fetching and sorting all tuples in the
dataset (particularly useful when a LIMIT clause has been specified),
and begin returning tuples before the entire result set is available.
Small datasets which fit entirely in memory and must be fully realized
and sorted may be slightly slower, which we reflect in the costing
implementation.
The hybrid mode approach allows us to optimize for both very small
groups (where the overhead of a new tuplesort is high) and very large
groups (where we can lower cost by not having to sort on already sorted
columns), albeit at some extra cost while switching between modes.
Co-authored-by: Alexander Korotkov <a.korotkov@postgrespro.ru>
---
doc/src/sgml/config.sgml | 14 +
src/backend/commands/explain.c | 211 ++-
src/backend/executor/Makefile | 1 +
src/backend/executor/execAmi.c | 14 +
src/backend/executor/execParallel.c | 18 +
src/backend/executor/execProcnode.c | 33 +
src/backend/executor/nodeIncrementalSort.c | 1201 +++++++++++++++
src/backend/executor/nodeSort.c | 3 +-
src/backend/nodes/copyfuncs.c | 49 +-
src/backend/nodes/outfuncs.c | 25 +-
src/backend/nodes/readfuncs.c | 37 +-
src/backend/optimizer/path/allpaths.c | 4 +
src/backend/optimizer/path/costsize.c | 198 ++-
src/backend/optimizer/path/pathkeys.c | 61 +-
src/backend/optimizer/plan/createplan.c | 143 +-
src/backend/optimizer/plan/planner.c | 73 +-
src/backend/optimizer/plan/setrefs.c | 1 +
src/backend/optimizer/plan/subselect.c | 1 +
src/backend/optimizer/util/pathnode.c | 51 +
src/backend/utils/misc/guc.c | 9 +
src/backend/utils/sort/tuplesort.c | 296 +++-
src/include/executor/execdebug.h | 2 +
src/include/executor/nodeIncrementalSort.h | 28 +
src/include/nodes/execnodes.h | 77 +
src/include/nodes/nodes.h | 3 +
src/include/nodes/pathnodes.h | 9 +
src/include/nodes/plannodes.h | 11 +
src/include/optimizer/cost.h | 10 +
src/include/optimizer/pathnode.h | 6 +
src/include/optimizer/paths.h | 2 +
src/include/utils/tuplesort.h | 3 +
.../expected/drop-index-concurrently-1.out | 2 +-
.../regress/expected/incremental_sort.out | 1320 +++++++++++++++++
.../regress/expected/partition_aggregate.out | 2 +
src/test/regress/expected/sysviews.out | 3 +-
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/incremental_sort.sql | 88 ++
src/test/regress/sql/partition_aggregate.sql | 2 +
39 files changed, 3852 insertions(+), 162 deletions(-)
create mode 100644 src/backend/executor/nodeIncrementalSort.c
create mode 100644 src/include/executor/nodeIncrementalSort.h
create mode 100644 src/test/regress/expected/incremental_sort.out
create mode 100644 src/test/regress/sql/incremental_sort.sql
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgmlindex 70854ae298..fe77f8eb4c 100644--- a/doc/src/sgml/config.sgml+++ b/doc/src/sgml/config.sgml@@ -4542,6 +4542,20 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
</listitem>
</varlistentry>
+ <varlistentry id="guc-enable-incrementalsort" xreflabel="enable_incrementalsort">+ <term><varname>enable_incrementalsort</varname> (<type>boolean</type>)+ <indexterm>+ <primary><varname>enable_incrementalsort</varname> configuration parameter</primary>+ </indexterm>+ </term>+ <listitem>+ <para>+ Enables or disables the query planner's use of incremental sort+ steps. The default is <literal>on</literal>.+ </para>+ </listitem>+ </varlistentry>+
<varlistentry id="guc-enable-indexscan" xreflabel="enable_indexscan">
<term><varname>enable_indexscan</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.cindex 58141d8393..dd4600a214 100644--- a/src/backend/commands/explain.c+++ b/src/backend/commands/explain.c@@ -82,6 +82,8 @@ static void show_upper_qual(List *qual, const char *qlabel,
ExplainState *es);
static void show_sort_keys(SortState *sortstate, List *ancestors,
ExplainState *es);
+static void show_incremental_sort_keys(IncrementalSortState *incrsortstate,+ List *ancestors, ExplainState *es);
static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
ExplainState *es);
static void show_agg_keys(AggState *astate, List *ancestors,
@@ -95,7 +97,7 @@ static void show_grouping_set_keys(PlanState *planstate,
static void show_group_keys(GroupState *gstate, List *ancestors,
ExplainState *es);
static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
- int nkeys, AttrNumber *keycols,+ int nkeys, int nPresortedKeys, AttrNumber *keycols,
Oid *sortOperators, Oid *collations, bool *nullsFirst,
List *ancestors, ExplainState *es);
static void show_sortorder_options(StringInfo buf, Node *sortexpr,
@@ -103,6 +105,8 @@ static void show_sortorder_options(StringInfo buf, Node *sortexpr,
static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
List *ancestors, ExplainState *es);
static void show_sort_info(SortState *sortstate, ExplainState *es);
+static void show_incremental_sort_info(IncrementalSortState *incrsortstate,+ ExplainState *es);
static void show_hash_info(HashState *hashstate, ExplainState *es);
static void show_hashagg_info(AggState *hashstate, ExplainState *es);
static void show_tidbitmap_info(BitmapHeapScanState *planstate,
@@ -1240,6 +1244,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
case T_Sort:
pname = sname = "Sort";
break;
+ case T_IncrementalSort:+ pname = sname = "Incremental Sort";+ break;
case T_Group:
pname = sname = "Group";
break;
@@ -1899,6 +1906,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
show_sort_keys(castNode(SortState, planstate), ancestors, es);
show_sort_info(castNode(SortState, planstate), es);
break;
+ case T_IncrementalSort:+ show_incremental_sort_keys(castNode(IncrementalSortState, planstate),+ ancestors, es);+ show_incremental_sort_info(castNode(IncrementalSortState, planstate),+ es);+ break;
case T_MergeAppend:
show_merge_append_keys(castNode(MergeAppendState, planstate),
ancestors, es);
@@ -2227,12 +2240,29 @@ show_sort_keys(SortState *sortstate, List *ancestors, ExplainState *es)
Sort *plan = (Sort *) sortstate->ss.ps.plan;
show_sort_group_keys((PlanState *) sortstate, "Sort Key",
- plan->numCols, plan->sortColIdx,+ plan->numCols, 0, plan->sortColIdx,
plan->sortOperators, plan->collations,
plan->nullsFirst,
ancestors, es);
}
+/*+ * Show the sort keys for a IncrementalSort node.+ */+static void+show_incremental_sort_keys(IncrementalSortState *incrsortstate,+ List *ancestors, ExplainState *es)+{+ IncrementalSort *plan = (IncrementalSort *) incrsortstate->ss.ps.plan;++ show_sort_group_keys((PlanState *) incrsortstate, "Sort Key",+ plan->sort.numCols, plan->presortedCols,+ plan->sort.sortColIdx,+ plan->sort.sortOperators, plan->sort.collations,+ plan->sort.nullsFirst,+ ancestors, es);+}+
/*
* Likewise, for a MergeAppend node.
*/
@@ -2243,7 +2273,7 @@ show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
MergeAppend *plan = (MergeAppend *) mstate->ps.plan;
show_sort_group_keys((PlanState *) mstate, "Sort Key",
- plan->numCols, plan->sortColIdx,+ plan->numCols, 0, plan->sortColIdx,
plan->sortOperators, plan->collations,
plan->nullsFirst,
ancestors, es);
@@ -2267,7 +2297,7 @@ show_agg_keys(AggState *astate, List *ancestors,
show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
else
show_sort_group_keys(outerPlanState(astate), "Group Key",
- plan->numCols, plan->grpColIdx,+ plan->numCols, 0, plan->grpColIdx,
NULL, NULL, NULL,
ancestors, es);
@@ -2336,7 +2366,7 @@ show_grouping_set_keys(PlanState *planstate,
if (sortnode)
{
show_sort_group_keys(planstate, "Sort Key",
- sortnode->numCols, sortnode->sortColIdx,+ sortnode->numCols, 0, sortnode->sortColIdx,
sortnode->sortOperators, sortnode->collations,
sortnode->nullsFirst,
ancestors, es);
@@ -2393,7 +2423,7 @@ show_group_keys(GroupState *gstate, List *ancestors,
/* The key columns refer to the tlist of the child plan */
ancestors = lcons(plan, ancestors);
show_sort_group_keys(outerPlanState(gstate), "Group Key",
- plan->numCols, plan->grpColIdx,+ plan->numCols, 0, plan->grpColIdx,
NULL, NULL, NULL,
ancestors, es);
ancestors = list_delete_first(ancestors);
@@ -2406,13 +2436,14 @@ show_group_keys(GroupState *gstate, List *ancestors,
*/
static void
show_sort_group_keys(PlanState *planstate, const char *qlabel,
- int nkeys, AttrNumber *keycols,+ int nkeys, int nPresortedKeys, AttrNumber *keycols,
Oid *sortOperators, Oid *collations, bool *nullsFirst,
List *ancestors, ExplainState *es)
{
Plan *plan = planstate->plan;
List *context;
List *result = NIL;
+ List *resultPresorted = NIL;
StringInfoData sortkeybuf;
bool useprefix;
int keyno;
@@ -2452,9 +2483,13 @@ show_sort_group_keys(PlanState *planstate, const char *qlabel,
nullsFirst[keyno]);
/* Emit one property-list item per sort key */
result = lappend(result, pstrdup(sortkeybuf.data));
+ if (keyno < nPresortedKeys)+ resultPresorted = lappend(resultPresorted, exprstr);
}
ExplainPropertyList(qlabel, result, es);
+ if (nPresortedKeys > 0)+ ExplainPropertyList("Presorted Key", resultPresorted, es);
}
/*
@@ -2668,6 +2703,168 @@ show_sort_info(SortState *sortstate, ExplainState *es)
}
}
++static void+show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,+ const char *groupLabel, ExplainState *es)+{+ ListCell *methodCell;+ int methodCount = list_length(groupInfo->sortMethods);++ if (es->format == EXPLAIN_FORMAT_TEXT)+ {+ appendStringInfoSpaces(es->str, es->indent * 2);+ appendStringInfo(es->str, "%s Groups: %ld (Methods: ", groupLabel,+ groupInfo->groupCount);+ foreach(methodCell, groupInfo->sortMethods)+ {+ const char *sortMethodName;++ sortMethodName = tuplesort_method_name(methodCell->int_value);+ appendStringInfo(es->str, "%s", sortMethodName);+ if (foreach_current_index(methodCell) < methodCount - 1)+ appendStringInfo(es->str, ", ");+ }+ appendStringInfo(es->str, ")");++ if (groupInfo->maxMemorySpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;+ const char *spaceTypeName;++ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);+ appendStringInfo(es->str, " %s: %ldkB (avg), %ldkB (max)",+ spaceTypeName, avgSpace,+ groupInfo->maxMemorySpaceUsed);+ }++ if (groupInfo->maxDiskSpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;++ const char *spaceTypeName;++ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_DISK);+ /* Add a semicolon separator only if memory stats were printed. */+ if (groupInfo->maxMemorySpaceUsed > 0)+ appendStringInfo(es->str, ";");+ appendStringInfo(es->str, " %s: %ldkB (avg), %ldkB (max)",+ spaceTypeName, avgSpace,+ groupInfo->maxDiskSpaceUsed);+ }++ appendStringInfo(es->str, "\n");+ }+ else+ {+ List *methodNames = NIL;+ StringInfoData groupName;++ initStringInfo(&groupName);+ appendStringInfo(&groupName, "%s Groups", groupLabel);+ ExplainOpenGroup("Incremental Sort Groups", groupName.data, true, es);+ ExplainPropertyInteger("Group Count", NULL, groupInfo->groupCount, es);++ foreach(methodCell, groupInfo->sortMethods)+ {+ const *sortMethodName = tuplesort_method_name(methodCell->int_value);++ methodNames = lappend(methodNames, sortMethodName);+ }+ ExplainPropertyList("Sort Methods Used", methodNames, es);++ if (groupInfo->maxMemorySpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;+ const *spaceTypeName;++ ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);+ ExplainPropertyInteger("Maximum Sort Space Used", "kB",+ groupInfo->maxMemorySpaceUsed, es);+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);+ ExplainPropertyText("Sort Space Type", spaceTypeName, es);+ }+ if (groupInfo->maxDiskSpaceUsed > 0)+ {+ long avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;+ const *spaceTypeName;++ ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);+ ExplainPropertyInteger("Maximum Sort Space Used", "kB",+ groupInfo->maxDiskSpaceUsed, es);+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);+ ExplainPropertyText("Sort Space Type", spaceTypeName, es);+ }++ ExplainCloseGroup("Incremental Sort Groups", "XXX Groups", true, es);+ }+}++/*+ * If it's EXPLAIN ANALYZE, show tuplesort stats for a incremental sort node+ */+static void+show_incremental_sort_info(IncrementalSortState *incrsortstate,+ ExplainState *es)+{+ IncrementalSortGroupInfo *fullsortGroupInfo;+ IncrementalSortGroupInfo *prefixsortGroupInfo;++ if (!(es->analyze && incrsortstate->sort_Done))+ return;++ fullsortGroupInfo = &incrsortstate->incsort_info.fullsortGroupInfo;+ if (fullsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(fullsortGroupInfo, "Full-sort", es);+ prefixsortGroupInfo = &incrsortstate->incsort_info.prefixsortGroupInfo;+ if (prefixsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(prefixsortGroupInfo, "Presorted", es);++ if (incrsortstate->shared_info != NULL)+ {+ int n;+ bool opened_group = false;++ for (n = 0; n < incrsortstate->shared_info->num_workers; n++)+ {+ IncrementalSortInfo *incsort_info =+ &incrsortstate->shared_info->sinfo[n];++ /*+ * XXX: The previous version of the patch chcked:+ * fullsort_instrument->sortMethod == SORT_TYPE_STILL_IN_PROGRESS+ * and continued if the condition was true (with the comment+ * "ignore any unfilled slots"). I'm not convinced that makes+ * sense since the same sort instrument can have been used+ * multiple times, so the last time it being used being still in+ * progress, doesn't seem to be relevant. Instead I'm now checking+ * to see if the group count for each group info is 0. If both are+ * 0, then we exclude the worker since it didn't contribute+ * anything meaningful.+ */+ fullsortGroupInfo = &incsort_info->fullsortGroupInfo;+ prefixsortGroupInfo = &incsort_info->prefixsortGroupInfo;+ if (fullsortGroupInfo->groupCount == 0 &&+ prefixsortGroupInfo->groupCount == 0)+ continue;++ if (!opened_group)+ {+ ExplainOpenGroup("Workers", "Workers", false, es);+ opened_group = true;+ }++ if (fullsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(fullsortGroupInfo, "Full-sort", es);+ if (prefixsortGroupInfo->groupCount > 0)+ show_incremental_sort_group_info(prefixsortGroupInfo, "Presorted", es);+ }++ if (opened_group)+ ExplainCloseGroup("Workers", "Workers", false, es);+ }+}+
/*
* Show information on hash buckets/batches.
*/
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefileindex a983800e4b..f990c6473a 100644--- a/src/backend/executor/Makefile+++ b/src/backend/executor/Makefile@@ -46,6 +46,7 @@ OBJS = \
nodeGroup.o \
nodeHash.o \
nodeHashjoin.o \
+ nodeIncrementalSort.o \
nodeIndexonlyscan.o \
nodeIndexscan.o \
nodeLimit.o \
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.cindex b12aeb3334..e2154ba86a 100644--- a/src/backend/executor/execAmi.c+++ b/src/backend/executor/execAmi.c@@ -30,6 +30,7 @@
#include "executor/nodeGroup.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
+#include "executor/nodeIncrementalSort.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "executor/nodeLimit.h"
@@ -252,6 +253,10 @@ ExecReScan(PlanState *node)
ExecReScanSort((SortState *) node);
break;
+ case T_IncrementalSortState:+ ExecReScanIncrementalSort((IncrementalSortState *) node);+ break;+
case T_GroupState:
ExecReScanGroup((GroupState *) node);
break;
@@ -557,8 +562,17 @@ ExecSupportsBackwardScan(Plan *node)
case T_CteScan:
case T_Material:
case T_Sort:
+ /* these don't evaluate tlist */
return true;
+ case T_IncrementalSort:++ /*+ * Unlike full sort, incremental sort keeps only a single group of+ * tuples in memory, so it can't scan backwards.+ */+ return false;+
case T_LockRows:
case T_Limit:
return ExecSupportsBackwardScan(outerPlan(node));
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.cindex a753d6efa0..333d4ba1fb 100644--- a/src/backend/executor/execParallel.c+++ b/src/backend/executor/execParallel.c@@ -31,6 +31,7 @@
#include "executor/nodeForeignscan.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
+#include "executor/nodeIncrementalSort.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "executor/nodeSeqscan.h"
@@ -282,6 +283,10 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e)
/* even when not parallel-aware, for EXPLAIN ANALYZE */
ExecSortEstimate((SortState *) planstate, e->pcxt);
break;
+ case T_IncrementalSortState:+ /* even when not parallel-aware, for EXPLAIN ANALYZE */+ ExecIncrementalSortEstimate((IncrementalSortState *) planstate, e->pcxt);+ break;
default:
break;
@@ -495,6 +500,10 @@ ExecParallelInitializeDSM(PlanState *planstate,
/* even when not parallel-aware, for EXPLAIN ANALYZE */
ExecSortInitializeDSM((SortState *) planstate, d->pcxt);
break;
+ case T_IncrementalSortState:+ /* even when not parallel-aware, for EXPLAIN ANALYZE */+ ExecIncrementalSortInitializeDSM((IncrementalSortState *) planstate, d->pcxt);+ break;
default:
break;
@@ -957,6 +966,7 @@ ExecParallelReInitializeDSM(PlanState *planstate,
break;
case T_HashState:
case T_SortState:
+ case T_IncrementalSortState:
/* these nodes have DSM state, but no reinitialization is required */
break;
@@ -1017,6 +1027,9 @@ ExecParallelRetrieveInstrumentation(PlanState *planstate,
case T_SortState:
ExecSortRetrieveInstrumentation((SortState *) planstate);
break;
+ case T_IncrementalSortState:+ ExecIncrementalSortRetrieveInstrumentation((IncrementalSortState *) planstate);+ break;
case T_HashState:
ExecHashRetrieveInstrumentation((HashState *) planstate);
break;
@@ -1303,6 +1316,11 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt)
/* even when not parallel-aware, for EXPLAIN ANALYZE */
ExecSortInitializeWorker((SortState *) planstate, pwcxt);
break;
+ case T_IncrementalSortState:+ /* even when not parallel-aware, for EXPLAIN ANALYZE */+ ExecIncrementalSortInitializeWorker((IncrementalSortState *) planstate,+ pwcxt);+ break;
default:
break;
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.cindex 7b2e84f402..d15a86a706 100644--- a/src/backend/executor/execProcnode.c+++ b/src/backend/executor/execProcnode.c@@ -88,6 +88,7 @@
#include "executor/nodeGroup.h"
#include "executor/nodeHash.h"
#include "executor/nodeHashjoin.h"
+#include "executor/nodeIncrementalSort.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "executor/nodeLimit.h"
@@ -313,6 +314,11 @@ ExecInitNode(Plan *node, EState *estate, int eflags)
estate, eflags);
break;
+ case T_IncrementalSort:+ result = (PlanState *) ExecInitIncrementalSort((IncrementalSort *) node,+ estate, eflags);+ break;+
case T_Group:
result = (PlanState *) ExecInitGroup((Group *) node,
estate, eflags);
@@ -693,6 +699,10 @@ ExecEndNode(PlanState *node)
ExecEndSort((SortState *) node);
break;
+ case T_IncrementalSortState:+ ExecEndIncrementalSort((IncrementalSortState *) node);+ break;+
case T_GroupState:
ExecEndGroup((GroupState *) node);
break;
@@ -839,6 +849,29 @@ ExecSetTupleBound(int64 tuples_needed, PlanState *child_node)
sortState->bound = tuples_needed;
}
}
+ else if (IsA(child_node, IncrementalSortState))+ {+ /*+ * If it is a Sort node, notify it that it can use bounded sort.+ *+ * Note: it is the responsibility of nodeSort.c to react properly to+ * changes of these parameters. If we ever redesign this, it'd be a+ * good idea to integrate this signaling with the parameter-change+ * mechanism.+ */+ IncrementalSortState *sortState = (IncrementalSortState *) child_node;++ if (tuples_needed < 0)+ {+ /* make sure flag gets reset if needed upon rescan */+ sortState->bounded = false;+ }+ else+ {+ sortState->bounded = true;+ sortState->bound = tuples_needed;+ }+ }
else if (IsA(child_node, AppendState))
{
/*
diff --git a/src/backend/executor/nodeIncrementalSort.c b/src/backend/executor/nodeIncrementalSort.cnew file mode 100644index 0000000000..32ce05a63c--- /dev/null+++ b/src/backend/executor/nodeIncrementalSort.c@@ -0,0 +1,1201 @@+/*-------------------------------------------------------------------------+ *+ * nodeIncrementalSort.c+ * Routines to handle incremental sorting of relations.+ *+ * DESCRIPTION+ *+ * Incremental sort is an optimized variant of multikey sort for cases+ * when the input is already sorted by a prefix of the sort keys. For+ * example when a sort by (key1, key2 ... keyN) is requested, and the+ * input is already sorted by (key1, key2 ... keyM), M < N, we can+ * divide the input into groups where keys (key1, ... keyM) are equal,+ * and only sort on the remaining columns.+ *+ * Consider the following example. We have input tuples consisting of+ * two integers (X, Y) already presorted by X, while it's required to+ * sort them by both X and Y. Let input tuples be following.+ *+ * (1, 5)+ * (1, 2)+ * (2, 9)+ * (2, 1)+ * (2, 5)+ * (3, 3)+ * (3, 7)+ *+ * Incremental sort algorithm would split the input into the following+ * groups, which have equal X, and then sort them by Y individually:+ *+ * (1, 5) (1, 2)+ * (2, 9) (2, 1) (2, 5)+ * (3, 3) (3, 7)+ *+ * After sorting these groups and putting them altogether, we would get+ * the following result which is sorted by X and Y, as requested:+ *+ * (1, 2)+ * (1, 5)+ * (2, 1)+ * (2, 5)+ * (2, 9)+ * (3, 3)+ * (3, 7)+ *+ * Incremental sort may be more efficient than plain sort, particularly+ * on large datasets, as it reduces the amount of data to sort at once,+ * making it more likely it fits into work_mem (eliminating the need to+ * spill to disk). But the main advantage of incremental sort is that+ * it can start producing rows early, before sorting the whole dataset,+ * which is a significant benefit especially for queries with LIMIT.+ *+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ *+ * IDENTIFICATION+ * src/backend/executor/nodeIncrementalSort.c+ *+ *-------------------------------------------------------------------------+ */++#include "postgres.h"++#include "access/htup_details.h"+#include "executor/execdebug.h"+#include "executor/nodeIncrementalSort.h"+#include "miscadmin.h"+#include "utils/lsyscache.h"+#include "utils/tuplesort.h"++static void+instrumentSortedGroup(PlanState *pstate, IncrementalSortGroupInfo *groupInfo,+ Tuplesortstate *sortState)+{+ IncrementalSortState *node = castNode(IncrementalSortState, pstate);+ TuplesortInstrumentation sort_instr;++ groupInfo->groupCount++;++ tuplesort_get_stats(sortState, &sort_instr);+ switch (sort_instr.spaceType)+ {+ case SORT_SPACE_TYPE_DISK:+ groupInfo->totalDiskSpaceUsed += sort_instr.spaceUsed;+ if (sort_instr.spaceUsed > groupInfo->maxDiskSpaceUsed)+ groupInfo->maxDiskSpaceUsed = sort_instr.spaceUsed;++ break;+ case SORT_SPACE_TYPE_MEMORY:+ groupInfo->totalMemorySpaceUsed += sort_instr.spaceUsed;+ if (sort_instr.spaceUsed > groupInfo->maxMemorySpaceUsed)+ groupInfo->maxMemorySpaceUsed = sort_instr.spaceUsed;++ break;+ }++ if (!list_member_int(groupInfo->sortMethods, sort_instr.sortMethod))+ groupInfo->sortMethods = lappend_int(groupInfo->sortMethods,+ sort_instr.sortMethod);++ /* Record shared stats if we're a parallel worker. */+ if (node->shared_info && node->am_worker)+ {+ Assert(IsParallelWorker());+ Assert(ParallelWorkerNumber <= node->shared_info->num_workers);++ memcpy(&node->shared_info->sinfo[ParallelWorkerNumber],+ &node->incsort_info, sizeof(IncrementalSortInfo));+ }+}++/*+ * Prepare information for presorted_keys comparison.+ */+static void+preparePresortedCols(IncrementalSortState *node)+{+ IncrementalSort *plannode = castNode(IncrementalSort, node->ss.ps.plan);++ node->presorted_keys =+ (PresortedKeyData *) palloc(plannode->presortedCols *+ sizeof(PresortedKeyData));++ /* Pre-cache comparison functions for each pre-sorted key. */+ for (int i = 0; i < plannode->presortedCols; i++)+ {+ Oid equalityOp,+ equalityFunc;+ PresortedKeyData *key;++ key = &node->presorted_keys[i];+ key->attno = plannode->sort.sortColIdx[i];++ equalityOp = get_equality_op_for_ordering_op(plannode->sort.sortOperators[i],+ NULL);+ if (!OidIsValid(equalityOp))+ elog(ERROR, "missing equality operator for ordering operator %u",+ plannode->sort.sortOperators[i]);++ equalityFunc = get_opcode(equalityOp);+ if (!OidIsValid(equalityFunc))+ elog(ERROR, "missing function for operator %u", equalityOp);++ /* Lookup the comparison function */+ fmgr_info_cxt(equalityFunc, &key->flinfo, CurrentMemoryContext);++ /* We can initialize the callinfo just once and re-use it */+ key->fcinfo = palloc0(SizeForFunctionCallInfo(2));+ InitFunctionCallInfoData(*key->fcinfo, &key->flinfo, 2,+ plannode->sort.collations[i], NULL, NULL);+ key->fcinfo->args[0].isnull = false;+ key->fcinfo->args[1].isnull = false;+ }+}++/*+ * Check whether a given tuple belongs to the current sort group.+ *+ * We do this by comparing its first 'presortedCols' column values to+ * the pivot tuple of the current group.+ */+static bool+isCurrentGroup(IncrementalSortState *node, TupleTableSlot *pivot, TupleTableSlot *tuple)+{+ int presortedCols;++ presortedCols = castNode(IncrementalSort, node->ss.ps.plan)->presortedCols;++ /*+ * That the input is sorted by keys * (0, ... n) implies that the tail+ * keys are more likely to change. Therefore we do our comparison starting+ * from the last pre-sorted column to optimize for early detection of+ * inequality and minimizing the number of function calls..+ */+ for (int i = presortedCols - 1; i >= 0; i--)+ {+ Datum datumA,+ datumB,+ result;+ bool isnullA,+ isnullB;+ AttrNumber attno = node->presorted_keys[i].attno;+ PresortedKeyData *key;++ datumA = slot_getattr(pivot, attno, &isnullA);+ datumB = slot_getattr(tuple, attno, &isnullB);++ /* Special case for NULL-vs-NULL, else use standard comparison */+ if (isnullA || isnullB)+ {+ if (isnullA == isnullB)+ continue;+ else+ return false;+ }++ key = &node->presorted_keys[i];++ key->fcinfo->args[0].value = datumA;+ key->fcinfo->args[1].value = datumB;++ /* just for paranoia's sake, we reset isnull each time */+ key->fcinfo->isnull = false;++ result = FunctionCallInvoke(key->fcinfo);++ /* Check for null result, since caller is clearly not expecting one */+ if (key->fcinfo->isnull)+ elog(ERROR, "function %u returned NULL", key->flinfo.fn_oid);++ if (!DatumGetBool(result))+ return false;+ }+ return true;+}++/*+ * Switch to presorted prefix mode.+ *+ * When we determine that we've likely encountered a large batch of tuples all+ * having the same presorted prefix values, we want to optimize tuplesort by+ * only sorting on unsorted suffix keys.+ *+ * The problem is that we've already accumulated several tuples in another+ * tuplesort configured to sort by all columns (assuming that there may be+ * more than one prefix key group). So to switch to presorted prefix mode we+ * have to go back an look at all the tuples we've already accumulated and+ * verify they're all part of the same prefix key group before sorting them+ * solely by unsorted suffix keys.+ *+ * While it's likely that all already fetch tuples are all part of a single+ * prefix group, we also have to handle the possibility that there is at least+ * one different prefix key group before the large prefix key group.+ */+static void+switchToPresortedPrefixMode(PlanState *pstate)+{+ IncrementalSortState *node = castNode(IncrementalSortState, pstate);+ ScanDirection dir;+ int64 nTuples = 0;+ bool lastTuple = false;+ bool firstTuple = true;+ TupleDesc tupDesc;+ PlanState *outerNode;+ IncrementalSort *plannode = castNode(IncrementalSort, node->ss.ps.plan);++ dir = node->ss.ps.state->es_direction;+ outerNode = outerPlanState(node);+ tupDesc = ExecGetResultType(outerNode);++ if (node->prefixsort_state == NULL)+ {+ Tuplesortstate *prefixsort_state;+ int presortedCols = plannode->presortedCols;++ /*+ * Optimize the sort by assuming the prefix columns are all equal and+ * thus we only need to sort by any remaining columns.+ */+ prefixsort_state = tuplesort_begin_heap(tupDesc,+ plannode->sort.numCols - presortedCols,+ &(plannode->sort.sortColIdx[presortedCols]),+ &(plannode->sort.sortOperators[presortedCols]),+ &(plannode->sort.collations[presortedCols]),+ &(plannode->sort.nullsFirst[presortedCols]),+ work_mem,+ NULL,+ false);+ node->prefixsort_state = prefixsort_state;+ }+ else+ {+ /* Next group of presorted data */+ tuplesort_reset(node->prefixsort_state);+ }++ /*+ * If the current node has a bound, then it's reasonably likely that a+ * large prefix key group will benefit from bounded sort, so configure the+ * tuplesort to allow for that optimization.+ */+ if (node->bounded)+ {+ SO1_printf("Setting bound on presorted prefix tuplesort to: %ld\n",+ node->bound - node->bound_Done);+ tuplesort_set_bound(node->prefixsort_state,+ node->bound - node->bound_Done);+ }++ for (;;)+ {+ lastTuple = node->n_fullsort_remaining - nTuples == 1;++ /*+ * When we encounter multiple prefix key groups inside the full sort+ * tuplesort we have to carry over the last read tuple into the next+ * batch.+ */+ if (firstTuple && !TupIsNull(node->transfer_tuple))+ {+ tuplesort_puttupleslot(node->prefixsort_state, node->transfer_tuple);+ nTuples++;++ /* The carried over tuple is our new group pivot tuple. */+ ExecCopySlot(node->group_pivot, node->transfer_tuple);+ }+ else+ {+ tuplesort_gettupleslot(node->fullsort_state,+ ScanDirectionIsForward(dir),+ false, node->transfer_tuple, NULL);++ /*+ * If this is our first time through the loop, then we need to+ * save the first tuple we get as our new group pivot.+ */+ if (TupIsNull(node->group_pivot))+ ExecCopySlot(node->group_pivot, node->transfer_tuple);++ if (isCurrentGroup(node, node->group_pivot, node->transfer_tuple))+ {+ tuplesort_puttupleslot(node->prefixsort_state, node->transfer_tuple);+ nTuples++;+ }+ else+ {+ /*+ * The tuple isn't part of the current batch so we need to+ * carry it over into the next set up tuples we transfer out+ * of the full sort tuplesort into the presorted prefix+ * tuplesort. We don't actually have to do anything special to+ * save the tuple since we've already loaded it into the+ * node->transfer_tuple slot, and, even though that slot+ * points to memory inside the full sort tuplesort, we can't+ * reset that tuplesort anyway until we've fully transferred+ * out of its tuples, so this reference is safe. We do need to+ * reset the group pivot tuple though since we've finished the+ * current prefix key group.+ */+ ExecClearTuple(node->group_pivot);+ break;+ }+ }++ firstTuple = false;++ if (lastTuple)++ /*+ * We retain the current group pivot tuple since we haven't yet+ * found the end of the current prefix key group.+ */+ break;+ }++ /*+ * Track how many tuples remain in the full sort batch so that we know if+ * we need to sort multiple prefix key groups before processing tuples+ * remaining in the large single prefix key group we think we've+ * encountered.+ */+ SO1_printf("Moving %ld tuples to presorted prefix tuplesort\n", nTuples);+ node->n_fullsort_remaining -= nTuples;+ SO1_printf("Setting n_fullsort_remaining to %ld\n", node->n_fullsort_remaining);++ if (lastTuple)+ {+ /*+ * We've confirmed that all tuples remaining in the full sort batch is+ * in the same prefix key group and moved all of those tuples into the+ * presorted prefix tuplesort. Now we can save our pivot comparison+ * tuple and continue fetching tuples from the outer execution node to+ * load into the presorted prefix tuplesort.+ */+ ExecCopySlot(node->group_pivot, node->transfer_tuple);+ SO_printf("Setting execution_status to INCSORT_LOADPREFIXSORT (switchToPresortedPrefixMode)\n");+ node->execution_status = INCSORT_LOADPREFIXSORT;++ /*+ * Make sure we clear the transfer tuple slot so that next time we+ * encounter a large prefix key group we don't incorrectly assume we+ * have a tuple carried over from the previous group.+ */+ ExecClearTuple(node->transfer_tuple);+ }+ else+ {+ /*+ * We finished a group but didn't consume all of the tuples from the+ * full sort batch sorter, so we'll sort this batch, let the inner+ * node read out all of those tuples, and then come back around to+ * find another batch.+ */+ SO1_printf("Sorting presorted prefix tuplesort with %ld tuples\n", nTuples);+ tuplesort_performsort(node->prefixsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.prefixsortGroupInfo,+ node->prefixsort_state);++ if (node->bounded)+ {+ /*+ * If the current node has a bound, and we've already sorted n+ * tuples, then the functional bound remaining is (original bound+ * - n), so store the current number of processed tuples for use+ * in configuring sorting bound.+ */+ SO2_printf("Changing bound_Done from %ld to %ld\n",+ Min(node->bound, node->bound_Done + nTuples), node->bound_Done);+ node->bound_Done = Min(node->bound, node->bound_Done + nTuples);+ }++ SO_printf("Setting execution_status to INCSORT_READPREFIXSORT (switchToPresortedPrefixMode)\n");+ node->execution_status = INCSORT_READPREFIXSORT;+ }+}++/*+ * Sorting many small groups with tuplesort is inefficient. In order to+ * cope with this problem we don't start a new group until the current one+ * contains at least DEFAULT_MIN_GROUP_SIZE tuples (unfortunately this also+ * means we can't assume small groups of tuples all have the same prefix keys.)+ * When we have a bound that's less than DEFAULT_MIN_GROUP_SIZE we start looking+ * for the new group as soon as we've met our bound to avoid fetching more+ * tuples than we absolutely have to fetch.+ */+#define DEFAULT_MIN_GROUP_SIZE 32++/*+ * While we've optimized for small prefix key groups by not starting our prefix+ * key comparisons until we've reached a minimum number of tuples, we don't want+ * that optimization to cause us to lose out on the benefits of being able to+ * assume a large group of tuples is fully presorted by its prefix keys.+ * Therefore we use the DEFAULT_MAX_FULL_SORT_GROUP_SIZE cutoff as a heuristic+ * for determining when we believe we've encountered a large group, and, if we+ * get to that point without finding a new prefix key group we transition to+ * presorted prefix key mode.+ */+#define DEFAULT_MAX_FULL_SORT_GROUP_SIZE (2 * DEFAULT_MIN_GROUP_SIZE)++/* ----------------------------------------------------------------+ * ExecIncrementalSort+ *+ * Assuming that outer subtree returns tuple presorted by some prefix+ * of target sort columns, performs incremental sort. The implemented+ * algorithm operates in two different modes:+ * - Fetching a minimum number of tuples without checking prefix key+ * group membership and sorting on all columns when safe.+ * - Fetching all tuples for a single prefix key group and sorting on+ * solely the unsorted columns.+ * We always begin in the first mode, and employ a heuristic to switch+ * into the second mode if we believe it's beneficial.+ *+ * Sorting incrementally can potentially use less memory, avoid fetching+ * and sorting all tuples in the the dataset, and begin returning tuples+ * before the entire result set is available.+ *+ * The hybrid mode approach allows us to optimize for both very small+ * groups (where the overhead of a new tuplesort is high) and very large+ * groups (where we can lower cost by not having to sort on already sorted+ * columns), albeit at some extra cost while switching between modes.+ *+ * Conditions:+ * -- none.+ *+ * Initial States:+ * -- the outer child is prepared to return the first tuple.+ * ----------------------------------------------------------------+ */+static TupleTableSlot *+ExecIncrementalSort(PlanState *pstate)+{+ IncrementalSortState *node = castNode(IncrementalSortState, pstate);+ EState *estate;+ ScanDirection dir;+ Tuplesortstate *read_sortstate;+ Tuplesortstate *fullsort_state;+ TupleTableSlot *slot;+ IncrementalSort *plannode = (IncrementalSort *) node->ss.ps.plan;+ PlanState *outerNode;+ TupleDesc tupDesc;+ int64 nTuples = 0;+ int64 minGroupSize;++ CHECK_FOR_INTERRUPTS();++ estate = node->ss.ps.state;+ dir = estate->es_direction;+ fullsort_state = node->fullsort_state;++ if (node->execution_status == INCSORT_READFULLSORT+ || node->execution_status == INCSORT_READPREFIXSORT)+ {+ /*+ * Return next tuple from the current sorted group set if available.+ */+ read_sortstate = node->execution_status == INCSORT_READFULLSORT ?+ fullsort_state : node->prefixsort_state;+ slot = node->ss.ps.ps_ResultTupleSlot;++ /*+ * We have to populate the slot from the tuplesort before checking+ * outerNodeDone because it will NULL the slot if no more tuples+ * remain. If the tuplesort is empty, but we don't have any more+ * tuples avaialable for sort from the outer node, then outerNodeDone+ * will have been set so we'll return the empty slot to the caller.+ */+ if (tuplesort_gettupleslot(read_sortstate, ScanDirectionIsForward(dir),+ false, slot, NULL) || node->outerNodeDone)++ /*+ * TODO: there isn't a good test case for the node->outerNodeDone+ * case directly, but lots of other stuff fails if it's not there.+ * If the outer node will fail when trying to fetch too many+ * tuples, then things break if this check isn't here.+ */+ return slot;+ else if (node->n_fullsort_remaining > 0)+ {+ /*+ * When we transition to presorted prefix mode, we might have+ * accumulated at least one additional prefix key group in the+ * full sort tuplesort. The first call to+ * switchToPresortedPrefixMode() will have pulled the first one of+ * those groups out, and we've returned those tuples to the inner+ * node, but if we tuples remaining in that tuplesort (i.e.,+ * n_fullsort_remaining > 0) at this point we need to do that+ * again.+ */+ SO1_printf("Re-calling switchToPresortedPrefixMode() because n_fullsort_remaining is > 0 (%ld)\n",+ node->n_fullsort_remaining);+ switchToPresortedPrefixMode(pstate);+ }+ else+ {+ /*+ * If we don't have any already sorted tuples to read, and we're+ * not in the middle of transitioning into presorted prefix sort+ * mode, then it's time to start the process all over again by+ * building new full sort group.+ */+ SO_printf("Setting execution_status to INCSORT_LOADFULLSORT (n_fullsort_remaining > 0)\n");+ node->execution_status = INCSORT_LOADFULLSORT;+ }+ }++ /*+ * Want to scan subplan in the forward direction while creating the sorted+ * data.+ */+ estate->es_direction = ForwardScanDirection;++ outerNode = outerPlanState(node);+ tupDesc = ExecGetResultType(outerNode);++ if (node->execution_status == INCSORT_LOADFULLSORT)+ {+ /*+ * Initialize tuplesort module (only needed before the first group).+ */+ if (fullsort_state == NULL)+ {+ /*+ * Initialize presorted column support structures for+ * isCurrentGroup().+ */+ preparePresortedCols(node);++ /*+ * Since we optimize small prefix key groups by accumulating a+ * minimum number of tuples before sorting, we can't assume that a+ * group of tuples all have the same prefix key values. Hence we+ * setup the full sort tuplesort to sort by all requested sort+ * columns.+ */+ fullsort_state = tuplesort_begin_heap(tupDesc,+ plannode->sort.numCols,+ plannode->sort.sortColIdx,+ plannode->sort.sortOperators,+ plannode->sort.collations,+ plannode->sort.nullsFirst,+ work_mem,+ NULL,+ false);+ node->fullsort_state = fullsort_state;+ }+ else+ {+ /* Reset sort for a new prefix key group. */+ tuplesort_reset(fullsort_state);+ }++ /*+ * Calculate the remaining tuples left if the bounded and configure+ * both bounded sort and the minimum group size accordingly.+ */+ if (node->bounded)+ {+ int64 currentBound = node->bound - node->bound_Done;++ /*+ * Bounded sort isn't likely to be a useful optimization for full+ * sort mode since we limit full sort mode to a relatively small+ * number of tuples and tuplesort doesn't switch over to top-n+ * heap sort anyway unless it hits (2 * bound) tuples.+ */+ if (currentBound < DEFAULT_MIN_GROUP_SIZE)+ tuplesort_set_bound(fullsort_state, currentBound);++ minGroupSize = Min(DEFAULT_MIN_GROUP_SIZE, currentBound);+ }+ else+ minGroupSize = DEFAULT_MIN_GROUP_SIZE;++ /*+ * Because we have to read the next tuple to find out that we've+ * encountered a new prefix key group on subsequent groups we have to+ * carry over that extra tuple and add it to the new group's sort+ * here.+ */+ if (!TupIsNull(node->group_pivot))+ {+ tuplesort_puttupleslot(fullsort_state, node->group_pivot);+ nTuples++;++ /*+ * We're in full sort mode accumulating a minimum number of tuples+ * and not checking for prefix key equality yet, so we can't+ * assume the group pivot tuple will reamin the same -- unless+ * we're using a minimum group size of 1, in which case the pivot+ * is obviously still the pviot.+ */+ if (nTuples != minGroupSize)+ ExecClearTuple(node->group_pivot);+ }++ for (;;)+ {+ /*+ * TODO: do we need to check for interrupts inside these loops or+ * will the outer node handle that?+ */++ slot = ExecProcNode(outerNode);++ /*+ * When the outer node can't provide us any more tuples, then we+ * can sort the current group and return those tuples.+ */+ if (TupIsNull(slot))+ {+ node->outerNodeDone = true;++ SO1_printf("Sorting fullsort with %ld tuples\n", nTuples);+ tuplesort_performsort(fullsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.fullsortGroupInfo,+ fullsort_state);++ SO_printf("Setting execution_status to INCSORT_READFULLSORT (final tuple)\n");+ node->execution_status = INCSORT_READFULLSORT;+ break;+ }++ /* Accumulate the next group of presorted tuples. */+ if (nTuples < minGroupSize)+ {+ /*+ * If we have yet hit our target minimum group size, then+ * don't both with checking for inclusion in the current+ * prefix group since a large number of very tiny sorts is+ * inefficient.+ */+ tuplesort_puttupleslot(fullsort_state, slot);+ nTuples++;++ /* Keep the last tuple of our minimal group as a pivot. */+ if (nTuples == minGroupSize)+ ExecCopySlot(node->group_pivot, slot);+ }+ else+ {+ /*+ * Once we've accumulated a minimum number of tuples, we start+ * checking for a new prefix key group. Only after we find+ * changed prefix keys can we guarantee sort stability of the+ * tuples we've already accumulated.+ */+ if (isCurrentGroup(node, node->group_pivot, slot))+ {+ /*+ * As long as the prefix keys match the pivot tuple then+ * load the tuple into the tuplesort.+ */+ tuplesort_puttupleslot(fullsort_state, slot);+ nTuples++;+ }+ else+ {+ /*+ * Since the tuple we fetched isn't part of the current+ * prefix key group we can't sort it as part of this sort+ * group. Instead we need to carry it over to the next+ * group. We use the group_pivot slot as a temp container+ * for that purpose even though we won't actually treat it+ * as a group pivot.+ */+ ExecCopySlot(node->group_pivot, slot);++ if (node->bounded)+ {+ /*+ * If the current node has a bound, and we've already+ * sorted n tuples, then the functional bound+ * remaining is (original bound - n), so store the+ * current number of processed tuples for use in+ * configuring sorting bound.+ */+ SO2_printf("Changing bound_Done from %ld to %ld\n",+ node->bound_Done,+ Min(node->bound, node->bound_Done + nTuples));+ node->bound_Done = Min(node->bound, node->bound_Done + nTuples);+ }++ /*+ * Once we find changed prefix keys we can complete the+ * sort and begin reading out the sorted tuples.+ */+ SO1_printf("Sorting fullsort tuplesort with %ld tuples\n",+ nTuples);+ tuplesort_performsort(fullsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.fullsortGroupInfo,+ fullsort_state);++ SO_printf("Setting execution_status to INCSORT_READFULLSORT (found end of group)\n");+ node->execution_status = INCSORT_READFULLSORT;+ break;+ }+ }++ /*+ * Once we've processed DEFAULT_MAX_FULL_SORT_GROUP_SIZE tuples+ * then we make the assumption that it's likely that we've found a+ * large group of tuples having a single prefix key (as long as+ * the last tuple didn't shift us into reading from the full sort+ * mode tuplesort).+ */+ if (nTuples > DEFAULT_MAX_FULL_SORT_GROUP_SIZE &&+ node->execution_status != INCSORT_READFULLSORT)+ {+ /*+ * The group pivot we have stored has already been put into+ * the tuplesort; we don't want to carry it over.+ */+ ExecClearTuple(node->group_pivot);++ /*+ * Unfortunately the tuplesort API doesn't include a way to+ * retrieve tuples unless a sort has been performed, so we+ * perform the sort even though we could just as easily rely+ * on FIFO retrieval semantics when transferring them to the+ * presorted prefix tuplesort.+ */+ SO1_printf("Sorting fullsort tuplesort with %ld tuples\n", nTuples);+ tuplesort_performsort(fullsort_state);+ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.fullsortGroupInfo,+ fullsort_state);++ /*+ * If the full sort tuplesort happened to switch into top-n+ * heapsort mode then we will only be able to retrieve+ * currentBound tuples (since the tuplesort will have only+ * retained the top-n tuples). This is safe even though we+ * haven't yet completed fetching the current prefix key group+ * because the tuples we've "lost" already sorted "below" the+ * retained ones, and we're already contractually guaranteed+ * to not need any more than the currentBount tuples.+ */+ if (tuplesort_used_bound(node->fullsort_state))+ {+ int64 currentBound = node->bound - node->bound_Done;++ SO2_printf("Read %ld tuples, but setting to %ld because we used bounded sort\n",+ nTuples, Min(currentBound, nTuples));+ nTuples = Min(currentBound, nTuples);+ }++ SO1_printf("Setting n_fullsort_remaining to %ld and calling switchToPresortedPrefixMode()\n",+ nTuples);++ /*+ * Track the number of tuples we need to move from the+ * fullsort to presorted prefix sort (we might have multiple+ * prefix key groups, so we need a way to see if we've+ * actually finished).+ */+ node->n_fullsort_remaining = nTuples;++ /* Transition the tuples to the presorted prefix tuplesort. */+ switchToPresortedPrefixMode(pstate);++ /*+ * Since we know we had tuples to move to the presorted prefix+ * tuplesort, we know that unless that transition has verified+ * that all tuples belonged to the same prefix key group (in+ * which case we can go straight to continuing to load tuples+ * into that tuplesort), we should have a tuple to return+ * here.+ *+ * Either way, the appropriate execution status should have+ * been set by switchToPresortedPrefixMode(), so we can drop+ * out of the loop here and let the appropriate path kick in.+ */+ break;+ }+ }+ }++ if (node->execution_status == INCSORT_LOADPREFIXSORT)+ {+ /*+ * Since we only enter this state after determining that all remaining+ * tuples in the full sort tuplesort have the same prefix, we've+ * already established a current group pivot tuple (but wasn't carried+ * over; it's already been put into the prefix sort tuplesort).+ */+ Assert(!TupIsNull(node->group_pivot));++ for (;;)+ {+ slot = ExecProcNode(outerNode);++ /* Check to see if there are no more tuples to fetch. */+ if (TupIsNull(slot))+ {+ node->outerNodeDone = true;+ break;+ }++ if (isCurrentGroup(node, node->group_pivot, slot))+ {+ /*+ * Fetch tuples and put them into the presorted prefix+ * tuplesort until we find changed prefix keys. Only then can+ * we guarantee sort stability of the tuples we've already+ * accumulated.+ */+ tuplesort_puttupleslot(node->prefixsort_state, slot);+ nTuples++;+ }+ else+ {+ /*+ * Since the tuple we fetched isn't part of the current prefix+ * key group we can't sort it as part of this sort group.+ * Instead we need to carry it over to the next group. We use+ * the group_pivot slot as a temp container for that purpose+ * even though we won't actually treat it as a group pivot.+ */+ ExecCopySlot(node->group_pivot, slot);+ break;+ }+ }++ /* Perform the sort and return the tuples to the inner plan nodes. */+ SO1_printf("Sorting presorted prefix tuplesort with >= %ld tuples\n", nTuples);+ tuplesort_performsort(node->prefixsort_state);++ if (pstate->instrument != NULL)+ instrumentSortedGroup(pstate,+ &node->incsort_info.prefixsortGroupInfo,+ node->prefixsort_state);++ SO_printf("Setting execution_status to INCSORT_READPREFIXSORT (found end of group)\n");+ node->execution_status = INCSORT_READPREFIXSORT;++ if (node->bounded)+ {+ /*+ * If the current node has a bound, and we've already sorted n+ * tuples, then the functional bound remaining is (original bound+ * - n), so store the current number of processed tuples for use+ * in configuring sorting bound.+ */+ SO2_printf("Changing bound_Done from %ld to %ld\n",+ node->bound_Done,+ Min(node->bound, node->bound_Done + nTuples));+ node->bound_Done = Min(node->bound, node->bound_Done + nTuples);+ }+ }++ /* Restore to user specified direction. */+ estate->es_direction = dir;++ /*+ * Remember that we've begun our scan and sort so we know how to handle+ * rescan.+ */+ node->sort_Done = true;++ /*+ * Get the first or next tuple from tuplesort. Returns NULL if no more+ * tuples.+ */+ read_sortstate = node->execution_status == INCSORT_READFULLSORT ?+ fullsort_state : node->prefixsort_state;+ slot = node->ss.ps.ps_ResultTupleSlot;+ (void) tuplesort_gettupleslot(read_sortstate, ScanDirectionIsForward(dir),+ false, slot, NULL);+ return slot;+}++/* ----------------------------------------------------------------+ * ExecInitIncrementalSort+ *+ * Creates the run-time state information for the sort node+ * produced by the planner and initializes its outer subtree.+ * ----------------------------------------------------------------+ */+IncrementalSortState *+ExecInitIncrementalSort(IncrementalSort *node, EState *estate, int eflags)+{+ IncrementalSortState *incrsortstate;++ SO_printf("ExecInitIncrementalSort: initializing sort node\n");++ /*+ * Incremental sort can't be used with either EXEC_FLAG_REWIND,+ * EXEC_FLAG_BACKWARD or EXEC_FLAG_MARK, because we hold only current+ * bucket in tuplesortstate.+ */+ Assert((eflags & (EXEC_FLAG_REWIND |+ EXEC_FLAG_BACKWARD |+ EXEC_FLAG_MARK)) == 0);++ /*+ * create state structure+ */+ incrsortstate = makeNode(IncrementalSortState);+ incrsortstate->ss.ps.plan = (Plan *) node;+ incrsortstate->ss.ps.state = estate;+ incrsortstate->ss.ps.ExecProcNode = ExecIncrementalSort;++ incrsortstate->bounded = false;+ incrsortstate->sort_Done = false;+ incrsortstate->outerNodeDone = false;+ incrsortstate->fullsort_state = NULL;+ incrsortstate->prefixsort_state = NULL;+ incrsortstate->group_pivot = NULL;+ incrsortstate->transfer_tuple = NULL;+ incrsortstate->n_fullsort_remaining = 0;+ incrsortstate->bound_Done = 0;+ incrsortstate->presorted_keys = NULL;++ if (incrsortstate->ss.ps.instrument != NULL)+ {+ IncrementalSortGroupInfo *fullsortGroupInfo =+ &incrsortstate->incsort_info.fullsortGroupInfo;+ IncrementalSortGroupInfo *prefixsortGroupInfo =+ &incrsortstate->incsort_info.prefixsortGroupInfo;++ fullsortGroupInfo->groupCount = 0;+ fullsortGroupInfo->maxDiskSpaceUsed = 0;+ fullsortGroupInfo->totalDiskSpaceUsed = 0;+ fullsortGroupInfo->maxMemorySpaceUsed = 0;+ fullsortGroupInfo->totalMemorySpaceUsed = 0;+ fullsortGroupInfo->sortMethods = NIL;+ prefixsortGroupInfo->groupCount = 0;+ prefixsortGroupInfo->maxDiskSpaceUsed = 0;+ prefixsortGroupInfo->totalDiskSpaceUsed = 0;+ prefixsortGroupInfo->maxMemorySpaceUsed = 0;+ prefixsortGroupInfo->totalMemorySpaceUsed = 0;+ prefixsortGroupInfo->sortMethods = NIL;+ }++ /*+ * Miscellaneous initialization+ *+ * Sort nodes don't initialize their ExprContexts because they never call+ * ExecQual or ExecProject.+ */++ /*+ * initialize child nodes+ *+ * We shield the child node from the need to support REWIND, BACKWARD, or+ * MARK/RESTORE.+ */+ eflags &= ~(EXEC_FLAG_REWIND | EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK);++ outerPlanState(incrsortstate) = ExecInitNode(outerPlan(node), estate, eflags);++ /*+ * Initialize scan slot and type.+ */+ ExecCreateScanSlotFromOuterPlan(estate, &incrsortstate->ss, &TTSOpsMinimalTuple);++ /*+ * Initialize return slot and type. No need to initialize projection info+ * because this node doesn't do projections.+ */+ ExecInitResultTupleSlotTL(&incrsortstate->ss.ps, &TTSOpsMinimalTuple);+ incrsortstate->ss.ps.ps_ProjInfo = NULL;++ /* make standalone slot to store previous tuple from outer node */+ incrsortstate->group_pivot =+ MakeSingleTupleTableSlot(ExecGetResultType(outerPlanState(incrsortstate)),+ &TTSOpsMinimalTuple);+ incrsortstate->transfer_tuple =+ MakeSingleTupleTableSlot(ExecGetResultType(outerPlanState(incrsortstate)),+ &TTSOpsMinimalTuple);++ SO_printf("ExecInitIncrementalSort: sort node initialized\n");++ return incrsortstate;+}++/* ----------------------------------------------------------------+ * ExecEndIncrementalSort(node)+ * ----------------------------------------------------------------+ */+void+ExecEndIncrementalSort(IncrementalSortState *node)+{+ SO_printf("ExecEndIncrementalSort: shutting down sort node\n");++ /*+ * clean out the tuple table+ */+ ExecClearTuple(node->ss.ss_ScanTupleSlot);+ /* must drop pointer to sort result tuple */+ ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);+ /* must drop stanalone tuple slot from outer node */+ ExecDropSingleTupleTableSlot(node->group_pivot);+ ExecDropSingleTupleTableSlot(node->transfer_tuple);++ /*+ * Release tuplesort resources.+ */+ if (node->fullsort_state != NULL)+ {+ tuplesort_end(node->fullsort_state);+ node->fullsort_state = NULL;+ }+ if (node->prefixsort_state != NULL)+ {+ tuplesort_end(node->prefixsort_state);+ node->prefixsort_state = NULL;+ }++ /*+ * Shut down the subplan.+ */+ ExecEndNode(outerPlanState(node));++ SO_printf("ExecEndIncrementalSort: sort node shutdown\n");+}++void+ExecReScanIncrementalSort(IncrementalSortState *node)+{+ PlanState *outerPlan = outerPlanState(node);++ /*+ * XXX: This is suspect.+ *+ * If we haven't sorted yet, just return. If outerplan's chgParam is not+ * NULL then it will be re-scanned by ExecProcNode, else no reason to+ * re-scan it at all.+ */+ if (!node->sort_Done)+ return;++ /* must drop pointer to sort result tuple */+ ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);++ node->outerNodeDone = false;++ /*+ * If subnode is to be rescanned then we forget previous sort results; we+ * have to re-read the subplan and re-sort. Also must re-sort if the+ * bounded-sort parameters changed or we didn't select randomAccess.+ *+ * Otherwise we can just rewind and rescan the sorted output.+ */+ node->sort_Done = false;+ if (node->fullsort_state != NULL)+ {+ tuplesort_end(node->fullsort_state);+ node->fullsort_state = NULL;+ }+ if (node->prefixsort_state != NULL)+ {+ tuplesort_end(node->prefixsort_state);+ node->prefixsort_state = NULL;+ }+ node->bound_Done = 0;++ /*+ * if chgParam of subnode is not null then plan will be re-scanned by+ * first ExecProcNode.+ */+ if (outerPlan->chgParam == NULL)+ ExecReScan(outerPlan);+}++/* ----------------------------------------------------------------+ * Parallel Query Support+ * ----------------------------------------------------------------+ */++/* ----------------------------------------------------------------+ * ExecSortEstimate+ *+ * Estimate space required to propagate sort statistics.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortEstimate(IncrementalSortState *node, ParallelContext *pcxt)+{+ Size size;++ /* don't need this if not instrumenting or no workers */+ if (!node->ss.ps.instrument || pcxt->nworkers == 0)+ return;++ size = mul_size(pcxt->nworkers, sizeof(IncrementalSortInfo));+ size = add_size(size, offsetof(SharedIncrementalSortInfo, sinfo));+ shm_toc_estimate_chunk(&pcxt->estimator, size);+ shm_toc_estimate_keys(&pcxt->estimator, 1);+}++/* ----------------------------------------------------------------+ * ExecSortInitializeDSM+ *+ * Initialize DSM space for sort statistics.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortInitializeDSM(IncrementalSortState *node, ParallelContext *pcxt)+{+ Size size;++ /* don't need this if not instrumenting or no workers */+ if (!node->ss.ps.instrument || pcxt->nworkers == 0)+ return;++ size = offsetof(SharedIncrementalSortInfo, sinfo)+ + pcxt->nworkers * sizeof(IncrementalSortInfo);+ node->shared_info = shm_toc_allocate(pcxt->toc, size);+ /* ensure any unfilled slots will contain zeroes */+ memset(node->shared_info, 0, size);+ node->shared_info->num_workers = pcxt->nworkers;+ shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id,+ node->shared_info);+}++/* ----------------------------------------------------------------+ * ExecSortInitializeWorker+ *+ * Attach worker to DSM space for sort statistics.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortInitializeWorker(IncrementalSortState *node, ParallelWorkerContext *pwcxt)+{+ node->shared_info =+ shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, true);+ node->am_worker = true;+}++/* ----------------------------------------------------------------+ * ExecSortRetrieveInstrumentation+ *+ * Transfer sort statistics from DSM to private memory.+ * ----------------------------------------------------------------+ */+void+ExecIncrementalSortRetrieveInstrumentation(IncrementalSortState *node)+{+ Size size;+ SharedIncrementalSortInfo *si;++ if (node->shared_info == NULL)+ return;++ size = offsetof(SharedIncrementalSortInfo, sinfo)+ + node->shared_info->num_workers * sizeof(IncrementalSortInfo);+ si = palloc(size);+ memcpy(si, node->shared_info, size);+ node->shared_info = si;+}diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.cindex 5d1debc196..9d2bfd7ed6 100644--- a/src/backend/executor/nodeSort.c+++ b/src/backend/executor/nodeSort.c@@ -93,7 +93,8 @@ ExecSort(PlanState *pstate)
plannode->collations,
plannode->nullsFirst,
work_mem,
- NULL, node->randomAccess);+ NULL,+ node->randomAccess);
if (node->bounded)
tuplesort_set_bound(tuplesortstate, node->bound);
node->tuplesortstate = (void *) tuplesortstate;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.cindex eaab97f753..e21f48327d 100644--- a/src/backend/nodes/copyfuncs.c+++ b/src/backend/nodes/copyfuncs.c@@ -927,6 +927,24 @@ _copyMaterial(const Material *from)
}
+/*+ * CopySortFields+ *+ * This function copies the fields of the Sort node. It is used by+ * all the copy functions for classes which inherit from Sort.+ */+static void+CopySortFields(const Sort *from, Sort *newnode)+{+ CopyPlanFields((const Plan *) from, (Plan *) newnode);++ COPY_SCALAR_FIELD(numCols);+ COPY_POINTER_FIELD(sortColIdx, from->numCols * sizeof(AttrNumber));+ COPY_POINTER_FIELD(sortOperators, from->numCols * sizeof(Oid));+ COPY_POINTER_FIELD(collations, from->numCols * sizeof(Oid));+ COPY_POINTER_FIELD(nullsFirst, from->numCols * sizeof(bool));+}+
/*
* _copySort
*/
@@ -938,13 +956,29 @@ _copySort(const Sort *from)
/*
* copy node superclass fields
*/
- CopyPlanFields((const Plan *) from, (Plan *) newnode);+ CopySortFields(from, newnode);- COPY_SCALAR_FIELD(numCols);- COPY_POINTER_FIELD(sortColIdx, from->numCols * sizeof(AttrNumber));- COPY_POINTER_FIELD(sortOperators, from->numCols * sizeof(Oid));- COPY_POINTER_FIELD(collations, from->numCols * sizeof(Oid));- COPY_POINTER_FIELD(nullsFirst, from->numCols * sizeof(bool));+ return newnode;+}+++/*+ * _copyIncrementalSort+ */+static IncrementalSort *+_copyIncrementalSort(const IncrementalSort *from)+{+ IncrementalSort *newnode = makeNode(IncrementalSort);++ /*+ * copy node superclass fields+ */+ CopySortFields((const Sort *) from, (Sort *) newnode);++ /*+ * copy remainder of node+ */+ COPY_SCALAR_FIELD(presortedCols);
return newnode;
}
@@ -4895,6 +4929,9 @@ copyObjectImpl(const void *from)
case T_Sort:
retval = _copySort(from);
break;
+ case T_IncrementalSort:+ retval = _copyIncrementalSort(from);+ break;
case T_Group:
retval = _copyGroup(from);
break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.cindex e084c3f069..6c83372c9f 100644--- a/src/backend/nodes/outfuncs.c+++ b/src/backend/nodes/outfuncs.c@@ -837,10 +837,8 @@ _outMaterial(StringInfo str, const Material *node)
}
static void
-_outSort(StringInfo str, const Sort *node)+_outSortInfo(StringInfo str, const Sort *node)
{
- WRITE_NODE_TYPE("SORT");-
_outPlanInfo(str, (const Plan *) node);
WRITE_INT_FIELD(numCols);
@@ -850,6 +848,24 @@ _outSort(StringInfo str, const Sort *node)
WRITE_BOOL_ARRAY(nullsFirst, node->numCols);
}
+static void+_outSort(StringInfo str, const Sort *node)+{+ WRITE_NODE_TYPE("SORT");++ _outSortInfo(str, node);+}++static void+_outIncrementalSort(StringInfo str, const IncrementalSort *node)+{+ WRITE_NODE_TYPE("INCREMENTALSORT");++ _outSortInfo(str, (const Sort *) node);++ WRITE_INT_FIELD(presortedCols);+}+
static void
_outUnique(StringInfo str, const Unique *node)
{
@@ -3783,6 +3799,9 @@ outNode(StringInfo str, const void *obj)
case T_Sort:
_outSort(str, obj);
break;
+ case T_IncrementalSort:+ _outIncrementalSort(str, obj);+ break;
case T_Unique:
_outUnique(str, obj);
break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.cindex d5b23a3479..c5bbbf459e 100644--- a/src/backend/nodes/readfuncs.c+++ b/src/backend/nodes/readfuncs.c@@ -2150,12 +2150,13 @@ _readMaterial(void)
}
/*
- * _readSort+ * ReadCommonSort+ * Assign the basic stuff of all nodes that inherit from Sort
*/
-static Sort *-_readSort(void)+static void+ReadCommonSort(Sort *local_node)
{
- READ_LOCALS(Sort);+ READ_TEMP_LOCALS();
ReadCommonPlan(&local_node->plan);
@@ -2164,6 +2165,32 @@ _readSort(void)
READ_OID_ARRAY(sortOperators, local_node->numCols);
READ_OID_ARRAY(collations, local_node->numCols);
READ_BOOL_ARRAY(nullsFirst, local_node->numCols);
+}++/*+ * _readSort+ */+static Sort *+_readSort(void)+{+ READ_LOCALS_NO_FIELDS(Sort);++ ReadCommonSort(local_node);++ READ_DONE();+}++/*+ * _readIncrementalSort+ */+static IncrementalSort *+_readIncrementalSort(void)+{+ READ_LOCALS(IncrementalSort);++ ReadCommonSort(&local_node->sort);++ READ_INT_FIELD(presortedCols);
READ_DONE();
}
@@ -2801,6 +2828,8 @@ parseNodeString(void)
return_value = _readMaterial();
else if (MATCH("SORT", 4))
return_value = _readSort();
+ else if (MATCH("INCREMENTALSORT", 15))+ return_value = _readIncrementalSort();
else if (MATCH("GROUP", 5))
return_value = _readGroup();
else if (MATCH("AGG", 3))
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.cindex 905bbe77d8..ccf46dd0aa 100644--- a/src/backend/optimizer/path/allpaths.c+++ b/src/backend/optimizer/path/allpaths.c@@ -3881,6 +3881,10 @@ print_path(PlannerInfo *root, Path *path, int indent)
ptype = "Sort";
subpath = ((SortPath *) path)->subpath;
break;
+ case T_IncrementalSortPath:+ ptype = "IncrementalSort";+ subpath = ((SortPath *) path)->subpath;+ break;
case T_GroupPath:
ptype = "Group";
subpath = ((GroupPath *) path)->subpath;
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.cindex 8cf694b61d..8efbb660b9 100644--- a/src/backend/optimizer/path/costsize.c+++ b/src/backend/optimizer/path/costsize.c@@ -128,6 +128,7 @@ bool enable_indexonlyscan = true;
bool enable_bitmapscan = true;
bool enable_tidscan = true;
bool enable_sort = true;
+bool enable_incrementalsort = true;
bool enable_hashagg = true;
bool enable_hashagg_disk = true;
bool enable_groupingsets_hash_disk = false;
@@ -1648,9 +1649,9 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
}
/*
- * cost_sort- * Determines and returns the cost of sorting a relation, including- * the cost of reading the input data.+ * cost_tuplesort+ * Determines and returns the cost of sorting a relation using tuplesort,+ * not including the cost of reading the input data.
*
* If the total volume of data to sort is less than sort_mem, we will do
* an in-memory sort, which requires no I/O and about t*log2(t) tuple
@@ -1677,39 +1678,23 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
* specifying nonzero comparison_cost; typically that's used for any extra
* work that has to be done to prepare the inputs to the comparison operators.
*
- * 'pathkeys' is a list of sort keys- * 'input_cost' is the total cost for reading the input data
* 'tuples' is the number of tuples in the relation
* 'width' is the average tuple width in bytes
* 'comparison_cost' is the extra cost per comparison, if any
* 'sort_mem' is the number of kilobytes of work memory allowed for the sort
* 'limit_tuples' is the bound on the number of output tuples; -1 if no bound
- *- * NOTE: some callers currently pass NIL for pathkeys because they- * can't conveniently supply the sort keys. Since this routine doesn't- * currently do anything with pathkeys anyway, that doesn't matter...- * but if it ever does, it should react gracefully to lack of key data.- * (Actually, the thing we'd most likely be interested in is just the number- * of sort keys, which all callers *could* supply.)
*/
-void-cost_sort(Path *path, PlannerInfo *root,- List *pathkeys, Cost input_cost, double tuples, int width,- Cost comparison_cost, int sort_mem,- double limit_tuples)+static void+cost_tuplesort(Cost *startup_cost, Cost *run_cost,+ double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples)
{
- Cost startup_cost = input_cost;- Cost run_cost = 0;
double input_bytes = relation_byte_size(tuples, width);
double output_bytes;
double output_tuples;
long sort_mem_bytes = sort_mem * 1024L;
- if (!enable_sort)- startup_cost += disable_cost;-- path->rows = tuples;-
/*
* We want to be sure the cost of a sort is never estimated as zero, even
* if passed-in tuple count is zero. Besides, mustn't do log(0)...
@@ -1748,7 +1733,7 @@ cost_sort(Path *path, PlannerInfo *root,
*
* Assume about N log2 N comparisons
*/
- startup_cost += comparison_cost * tuples * LOG2(tuples);+ *startup_cost = comparison_cost * tuples * LOG2(tuples);
/* Disk costs */
@@ -1759,7 +1744,7 @@ cost_sort(Path *path, PlannerInfo *root,
log_runs = 1.0;
npageaccesses = 2.0 * npages * log_runs;
/* Assume 3/4ths of accesses are sequential, 1/4th are not */
- startup_cost += npageaccesses *+ *startup_cost += npageaccesses *
(seq_page_cost * 0.75 + random_page_cost * 0.25);
}
else if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes)
@@ -1770,12 +1755,12 @@ cost_sort(Path *path, PlannerInfo *root,
* factor is a bit higher than for quicksort. Tweak it so that the
* cost curve is continuous at the crossover point.
*/
- startup_cost += comparison_cost * tuples * LOG2(2.0 * output_tuples);+ *startup_cost = comparison_cost * tuples * LOG2(2.0 * output_tuples);
}
else
{
/* We'll use plain quicksort on all the input tuples */
- startup_cost += comparison_cost * tuples * LOG2(tuples);+ *startup_cost = comparison_cost * tuples * LOG2(tuples);
}
/*
@@ -1786,8 +1771,163 @@ cost_sort(Path *path, PlannerInfo *root,
* here --- the upper LIMIT will pro-rate the run cost so we'd be double
* counting the LIMIT otherwise.
*/
- run_cost += cpu_operator_cost * tuples;+ *run_cost = cpu_operator_cost * tuples;+}++/*+ * cost_full_sort+ * Determines and returns the cost of sorting a relation, including the+ * cost of reading the input data.+ *+ * For the precise description of how the cost is calculated, see the comment+ * for cost_tuplesort().+ */+void+cost_full_sort(Cost *startup_cost, Cost *run_cost,+ Cost input_total_cost, double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples)+{+ cost_tuplesort(startup_cost, run_cost,+ tuples, width,+ comparison_cost, sort_mem,+ limit_tuples);++ if (!enable_sort)+ *startup_cost += disable_cost;++ *startup_cost += input_total_cost;+}++/*+ * cost_incremental_sort+ * Determines and returns the cost of sorting a relation incrementally, when+ * the input path is already sorted by some of the pathkeys.+ *+ * 'presorted_keys' is the number of leading pathkeys by which the input path+ * is sorted.+ *+ * We estimate the number of groups into which the relation is divided by the+ * leading pathkeys, and then calculate the cost of sorting a single group+ * with tuplesort using cost_tuplesort().+ */+void+cost_incremental_sort(Path *path,+ PlannerInfo *root, List *pathkeys, int presorted_keys,+ Cost input_startup_cost, Cost input_total_cost,+ double input_tuples, int width, Cost comparison_cost, int sort_mem,+ double limit_tuples)+{+ Cost startup_cost = 0,+ run_cost = 0,+ input_run_cost = input_total_cost - input_startup_cost;+ double group_tuples,+ input_groups;+ Cost group_startup_cost,+ group_run_cost,+ group_input_run_cost;+ List *presortedExprs = NIL;+ ListCell *l;+ int i = 0;++ Assert(presorted_keys != 0);++ /*+ * We want to be sure the cost of a sort is never estimated as zero, even+ * if passed-in tuple count is zero. Besides, mustn't do log(0)...+ */+ if (input_tuples < 2.0)+ input_tuples = 2.0;+ /* Extract presorted keys as list of expressions */+ foreach(l, pathkeys)+ {+ PathKey *key = (PathKey *) lfirst(l);+ EquivalenceMember *member = (EquivalenceMember *)+ linitial(key->pk_eclass->ec_members);++ presortedExprs = lappend(presortedExprs, member->em_expr);++ i++;+ if (i >= presorted_keys)+ break;+ }++ /* Estimate number of groups with equal presorted keys */+ input_groups = estimate_num_groups(root, presortedExprs, input_tuples, NULL);+ group_tuples = input_tuples / input_groups;+ group_input_run_cost = input_run_cost / input_groups;++ /*+ * Estimate average cost of sorting of one group where presorted keys are+ * equal. Incremental sort is sensitive to distribution of tuples to the+ * groups, where we're relying on quite rough assumptions. Thus, we're+ * pessimistic about incremental sort performance and increase its average+ * group size by half.+ */+ cost_tuplesort(&group_startup_cost, &group_run_cost,+ 1.5 * group_tuples, width, comparison_cost, sort_mem,+ limit_tuples);++ /*+ * Startup cost of incremental sort is the startup cost of its first group+ * plus the cost of its input.+ */+ startup_cost += group_startup_cost+ + input_startup_cost + group_input_run_cost;++ /*+ * After we started producing tuples from the first group, the cost of+ * producing all the tuples is given by the cost to finish processing this+ * group, plus the total cost to process the remaining groups, plus the+ * remaining cost of input.+ */+ run_cost += group_run_cost+ + (group_run_cost + group_startup_cost) * (input_groups - 1)+ + group_input_run_cost * (input_groups - 1);++ /*+ * Incremental sort adds some overhead by itself. Firstly, it has to+ * detect the sort groups. This is roughly equal to one extra copy and+ * comparison per tuple. Secondly, it has to reset the tuplesort context+ * for every group.+ */+ run_cost += (cpu_tuple_cost + comparison_cost) * input_tuples;+ run_cost += 2.0 * cpu_tuple_cost * input_groups;++ path->rows = input_tuples;+ path->startup_cost = startup_cost;+ path->total_cost = startup_cost + run_cost;+}++/*+ * cost_sort+ * Determines and returns the cost of sorting a relation, including+ * the cost of reading the input data.+ *+ * NOTE: some callers currently pass NIL for pathkeys because they+ * can't conveniently supply the sort keys. Since this routine doesn't+ * currently do anything with pathkeys anyway, that doesn't matter...+ * but if it ever does, it should react gracefully to lack of key data.+ * (Actually, the thing we'd most likely be interested in is just the number+ * of sort keys, which all callers *could* supply.)+ */+void+cost_sort(Path *path, PlannerInfo *root,+ List *pathkeys, Cost input_cost, double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples)++{+ Cost startup_cost;+ Cost run_cost;++ cost_full_sort(&startup_cost, &run_cost,+ input_cost,+ tuples, width, comparison_cost, sort_mem,+ limit_tuples);++ path->rows = tuples;
path->startup_cost = startup_cost;
path->total_cost = startup_cost + run_cost;
}
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.cindex 71b9d42c99..74799cd8fd 100644--- a/src/backend/optimizer/path/pathkeys.c+++ b/src/backend/optimizer/path/pathkeys.c@@ -334,6 +334,51 @@ pathkeys_contained_in(List *keys1, List *keys2)
return false;
}
++/*+ * pathkeys_common_contained_in+ * Same as pathkeys_contained_in, but also sets length of longest+ * common prefix of keys1 and keys2.+ */+bool+pathkeys_common_contained_in(List *keys1, List *keys2, int *n_common)+{+ int n = 0;+ ListCell *key1,+ *key2;++ forboth(key1, keys1, key2, keys2)+ {+ PathKey *pathkey1 = (PathKey *) lfirst(key1);+ PathKey *pathkey2 = (PathKey *) lfirst(key2);++ if (pathkey1 != pathkey2)+ {+ *n_common = n;+ return false;+ }+ n++;+ }++ *n_common = n;+ return (key1 == NULL);+}+++/*+ * pathkeys_common+ * Returns length of longest common prefix of keys1 and keys2.+ */+int+pathkeys_common(List *keys1, List *keys2)+{+ int n;++ (void) pathkeys_common_contained_in(keys1, keys2, &n);+ return n;+}++
/*
* get_cheapest_path_for_pathkeys
* Find the cheapest path (according to the specified criterion) that
@@ -1793,19 +1838,23 @@ right_merge_direction(PlannerInfo *root, PathKey *pathkey)
static int
pathkeys_useful_for_ordering(PlannerInfo *root, List *pathkeys)
{
+ int n_common_pathkeys;+
if (root->query_pathkeys == NIL)
return 0; /* no special ordering requested */
if (pathkeys == NIL)
return 0; /* unordered path */
- if (pathkeys_contained_in(root->query_pathkeys, pathkeys))- {- /* It's useful ... or at least the first N keys are */- return list_length(root->query_pathkeys);- }+ (void) pathkeys_common_contained_in(root->query_pathkeys, pathkeys,+ &n_common_pathkeys);- return 0; /* path ordering not useful */+ /*+ * Return the number of path keys in common, or 0 if there are none. Any+ * leading common pathkeys could be useful for ordering because we can use+ * the incremental sort.+ */+ return n_common_pathkeys;
}
/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.cindex fc25908dc6..1d7d4eb3e7 100644--- a/src/backend/optimizer/plan/createplan.c+++ b/src/backend/optimizer/plan/createplan.c@@ -98,6 +98,8 @@ static Plan *create_projection_plan(PlannerInfo *root,
int flags);
static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe);
static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags);
+static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root,+ IncrementalSortPath *best_path, int flags);
static Group *create_group_plan(PlannerInfo *root, GroupPath *best_path);
static Unique *create_upper_unique_plan(PlannerInfo *root, UpperUniquePath *best_path,
int flags);
@@ -244,6 +246,10 @@ static MergeJoin *make_mergejoin(List *tlist,
static Sort *make_sort(Plan *lefttree, int numCols,
AttrNumber *sortColIdx, Oid *sortOperators,
Oid *collations, bool *nullsFirst);
+static IncrementalSort *make_incrementalsort(Plan *lefttree,+ int numCols, int presortedCols,+ AttrNumber *sortColIdx, Oid *sortOperators,+ Oid *collations, bool *nullsFirst);
static Plan *prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
Relids relids,
const AttrNumber *reqColIdx,
@@ -258,6 +264,8 @@ static EquivalenceMember *find_ec_member_for_tle(EquivalenceClass *ec,
Relids relids);
static Sort *make_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
Relids relids);
+static IncrementalSort *make_incrementalsort_from_pathkeys(Plan *lefttree,+ List *pathkeys, Relids relids, int presortedCols);
static Sort *make_sort_from_groupcols(List *groupcls,
AttrNumber *grpColIdx,
Plan *lefttree);
@@ -460,6 +468,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
(SortPath *) best_path,
flags);
break;
+ case T_IncrementalSort:+ plan = (Plan *) create_incrementalsort_plan(root,+ (IncrementalSortPath *) best_path,+ flags);+ break;
case T_Group:
plan = (Plan *) create_group_plan(root,
(GroupPath *) best_path);
@@ -1994,6 +2007,32 @@ create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags)
return plan;
}
+/*+ * create_incrementalsort_plan+ *+ * Do the same as create_sort_plan, but create IncrementalSort plan.+ */+static IncrementalSort *+create_incrementalsort_plan(PlannerInfo *root, IncrementalSortPath *best_path,+ int flags)+{+ IncrementalSort *plan;+ Plan *subplan;++ /* See comments in create_sort_plan() above */+ subplan = create_plan_recurse(root, best_path->spath.subpath,+ flags | CP_SMALL_TLIST);+ plan = make_incrementalsort_from_pathkeys(subplan,+ best_path->spath.path.pathkeys,+ IS_OTHER_REL(best_path->spath.subpath->parent) ?+ best_path->spath.path.parent->relids : NULL,+ best_path->presortedCols);++ copy_generic_path_info(&plan->sort.plan, (Path *) best_path);++ return plan;+}+
/*
* create_group_plan
*
@@ -5088,17 +5127,24 @@ static void
label_sort_with_costsize(PlannerInfo *root, Sort *plan, double limit_tuples)
{
Plan *lefttree = plan->plan.lefttree;
- Path sort_path; /* dummy for result of cost_sort */-- cost_sort(&sort_path, root, NIL,- lefttree->total_cost,- lefttree->plan_rows,- lefttree->plan_width,- 0.0,- work_mem,- limit_tuples);- plan->plan.startup_cost = sort_path.startup_cost;- plan->plan.total_cost = sort_path.total_cost;+ Cost startup_cost,+ run_cost;++ /*+ * This function shouldn't have to deal with IncrementalSort plans because+ * they are only created from corresponding Path nodes.+ */+ Assert(IsA(plan, Sort));++ cost_full_sort(&startup_cost, &run_cost,+ lefttree->total_cost,+ lefttree->plan_rows,+ lefttree->plan_width,+ 0.0,+ work_mem,+ limit_tuples);+ plan->plan.startup_cost = startup_cost;+ plan->plan.total_cost = startup_cost + run_cost;
plan->plan.plan_rows = lefttree->plan_rows;
plan->plan.plan_width = lefttree->plan_width;
plan->plan.parallel_aware = false;
@@ -5677,9 +5723,12 @@ make_sort(Plan *lefttree, int numCols,
AttrNumber *sortColIdx, Oid *sortOperators,
Oid *collations, bool *nullsFirst)
{
- Sort *node = makeNode(Sort);- Plan *plan = &node->plan;+ Sort *node;+ Plan *plan;+ node = makeNode(Sort);++ plan = &node->plan;
plan->targetlist = lefttree->targetlist;
plan->qual = NIL;
plan->lefttree = lefttree;
@@ -5693,6 +5742,37 @@ make_sort(Plan *lefttree, int numCols,
return node;
}
+/*+ * make_incrementalsort --- basic routine to build an IncrementalSort plan node+ *+ * Caller must have built the sortColIdx, sortOperators, collations, and+ * nullsFirst arrays already.+ */+static IncrementalSort *+make_incrementalsort(Plan *lefttree, int numCols, int presortedCols,+ AttrNumber *sortColIdx, Oid *sortOperators,+ Oid *collations, bool *nullsFirst)+{+ IncrementalSort *node;+ Plan *plan;++ node = makeNode(IncrementalSort);++ plan = &node->sort.plan;+ plan->targetlist = lefttree->targetlist;+ plan->qual = NIL;+ plan->lefttree = lefttree;+ plan->righttree = NULL;+ node->presortedCols = presortedCols;+ node->sort.numCols = numCols;+ node->sort.sortColIdx = sortColIdx;+ node->sort.sortOperators = sortOperators;+ node->sort.collations = collations;+ node->sort.nullsFirst = nullsFirst;++ return node;+}+
/*
* prepare_sort_from_pathkeys
* Prepare to sort according to given pathkeys
@@ -6039,6 +6119,42 @@ make_sort_from_pathkeys(Plan *lefttree, List *pathkeys, Relids relids)
collations, nullsFirst);
}
+/*+ * make_incrementalsort_from_pathkeys+ * Create sort plan to sort according to given pathkeys+ *+ * 'lefttree' is the node which yields input tuples+ * 'pathkeys' is the list of pathkeys by which the result is to be sorted+ * 'relids' is the set of relations required by prepare_sort_from_pathkeys()+ * 'presortedCols' is the number of presorted columns in input tuples+ */+static IncrementalSort *+make_incrementalsort_from_pathkeys(Plan *lefttree, List *pathkeys,+ Relids relids, int presortedCols)+{+ int numsortkeys;+ AttrNumber *sortColIdx;+ Oid *sortOperators;+ Oid *collations;+ bool *nullsFirst;++ /* Compute sort column info, and adjust lefttree as needed */+ lefttree = prepare_sort_from_pathkeys(lefttree, pathkeys,+ relids,+ NULL,+ false,+ &numsortkeys,+ &sortColIdx,+ &sortOperators,+ &collations,+ &nullsFirst);++ /* Now build the Sort node */+ return make_incrementalsort(lefttree, numsortkeys, presortedCols,+ sortColIdx, sortOperators,+ collations, nullsFirst);+}+
/*
* make_sort_from_sortclauses
* Create sort plan to sort according to given sortclauses
@@ -6774,6 +6890,7 @@ is_projection_capable_path(Path *path)
case T_Hash:
case T_Material:
case T_Sort:
+ case T_IncrementalSort:
case T_Unique:
case T_SetOp:
case T_LockRows:
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex eb25c2f470..b2f4aaadb5 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -4869,7 +4869,7 @@ create_distinct_paths(PlannerInfo *root,
else
{
Size hashentrysize = hash_agg_entry_size(
- 0, cheapest_input_path->pathtarget->width, 0);+ 0, cheapest_input_path->pathtarget->width, 0);
/* Allow hashing only if hashtable is predicted to fit in work_mem */
allow_hash = (hashentrysize * numDistinctRows <= work_mem * 1024L);
@@ -4925,8 +4925,8 @@ create_distinct_paths(PlannerInfo *root,
* Build a new upperrel containing Paths for ORDER BY evaluation.
*
* All paths in the result must satisfy the ORDER BY ordering.
- * The only new path we need consider is an explicit sort on the- * cheapest-total existing path.+ * The only new paths we need consider is an explicit full or+ * incremental sort on the cheapest-total existing path.
*
* input_rel: contains the source-data Paths
* target: the output tlist the result Paths must emit
@@ -4965,29 +4965,60 @@ create_ordered_paths(PlannerInfo *root,
foreach(lc, input_rel->pathlist)
{
- Path *path = (Path *) lfirst(lc);+ Path *input_path = (Path *) lfirst(lc);+ Path *sorted_path = input_path;
bool is_sorted;
+ int presorted_keys;++ is_sorted = pathkeys_common_contained_in(root->sort_pathkeys,+ input_path->pathkeys, &presorted_keys);- is_sorted = pathkeys_contained_in(root->sort_pathkeys,- path->pathkeys);- if (path == cheapest_input_path || is_sorted)+ if (is_sorted)
{
- if (!is_sorted)+ /* Use the input path as is, but add a projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);+ }+ else+ {+ if (input_path == cheapest_input_path)
{
- /* An explicit sort here can take advantage of LIMIT */- path = (Path *) create_sort_path(root,- ordered_rel,- path,- root->sort_pathkeys,- limit_tuples);+ /*+ * Sort the cheapest input path. An explicit sort here can+ * take advantage of LIMIT.+ */+ sorted_path = (Path *) create_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ limit_tuples);+ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);+ }+ if (enable_incrementalsort && presorted_keys > 0)+ {+ /* Also consider incremental sort. */+ sorted_path = (Path *) create_incremental_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ presorted_keys,+ limit_tuples);++ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);
}
-- /* Add projection step if needed */- if (path->pathtarget != target)- path = apply_projection_to_path(root, ordered_rel,- path, target);-- add_path(ordered_rel, path);
}
}
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.cindex 3dcded506b..2b676bf406 100644--- a/src/backend/optimizer/plan/setrefs.c+++ b/src/backend/optimizer/plan/setrefs.c@@ -678,6 +678,7 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
case T_Material:
case T_Sort:
+ case T_IncrementalSort:
case T_Unique:
case T_SetOp:
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.cindex 3650e8329d..b02fcb9bfe 100644--- a/src/backend/optimizer/plan/subselect.c+++ b/src/backend/optimizer/plan/subselect.c@@ -2688,6 +2688,7 @@ finalize_plan(PlannerInfo *root, Plan *plan,
case T_Hash:
case T_Material:
case T_Sort:
+ case T_IncrementalSort:
case T_Unique:
case T_SetOp:
case T_Group:
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.cindex 9c8f3b1f0b..e15bc1baaa 100644--- a/src/backend/optimizer/util/pathnode.c+++ b/src/backend/optimizer/util/pathnode.c@@ -2750,6 +2750,57 @@ create_set_projection_path(PlannerInfo *root,
return pathnode;
}
+/*+ * create_incremental_sort_path+ * Creates a pathnode that represents performing an incremental sort.+ *+ * 'rel' is the parent relation associated with the result+ * 'subpath' is the path representing the source of data+ * 'pathkeys' represents the desired sort order+ * 'presorted_keys' is the number of keys by which the input path is+ * already sorted+ * 'limit_tuples' is the estimated bound on the number of output tuples,+ * or -1 if no LIMIT or couldn't estimate+ */+SortPath *+create_incremental_sort_path(PlannerInfo *root,+ RelOptInfo *rel,+ Path *subpath,+ List *pathkeys,+ int presorted_keys,+ double limit_tuples)+{+ IncrementalSortPath *sort = makeNode(IncrementalSortPath);+ SortPath *pathnode = &sort->spath;++ pathnode->path.pathtype = T_IncrementalSort;+ pathnode->path.parent = rel;+ /* Sort doesn't project, so use source path's pathtarget */+ pathnode->path.pathtarget = subpath->pathtarget;+ /* For now, assume we are above any joins, so no parameterization */+ pathnode->path.param_info = NULL;+ pathnode->path.parallel_aware = false;+ pathnode->path.parallel_safe = rel->consider_parallel &&+ subpath->parallel_safe;+ pathnode->path.parallel_workers = subpath->parallel_workers;+ pathnode->path.pathkeys = pathkeys;++ pathnode->subpath = subpath;++ cost_incremental_sort(&pathnode->path,+ root, pathkeys, presorted_keys,+ subpath->startup_cost,+ subpath->total_cost,+ subpath->rows,+ subpath->pathtarget->width,+ 0.0, /* XXX comparison_cost shouldn't be 0? */+ work_mem, limit_tuples);++ sort->presortedCols = presorted_keys;++ return pathnode;+}+
/*
* create_sort_path
* Creates a pathnode that represents performing an explicit sort.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.cindex af876d1f01..b6ce724557 100644--- a/src/backend/utils/misc/guc.c+++ b/src/backend/utils/misc/guc.c@@ -989,6 +989,15 @@ static struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {+ {"enable_incrementalsort", PGC_USERSET, QUERY_TUNING_METHOD,+ gettext_noop("Enables the planner's use of incremental sort steps."),+ NULL+ },+ &enable_incrementalsort,+ true,+ NULL, NULL, NULL+ },
{
{"enable_hashagg", PGC_USERSET, QUERY_TUNING_METHOD,
gettext_noop("Enables the planner's use of hashed aggregation plans."),
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.cindex d02e676aa3..583551d197 100644--- a/src/backend/utils/sort/tuplesort.c+++ b/src/backend/utils/sort/tuplesort.c@@ -125,6 +125,16 @@
#define PARALLEL_SORT(state) ((state)->shared == NULL ? 0 : \
(state)->worker >= 0 ? 1 : 2)
+/*+ * Initial size of memtuples array. We're trying to select this size so that+ * array doesn't exceed ALLOCSET_SEPARATE_THRESHOLD and so that the overhead of+ * allocation might possibly be lowered. However, we don't consider array sizes+ * less than 1024.+ *+ */+#define INITIAL_MEMTUPSIZE Max(1024, \+ ALLOCSET_SEPARATE_THRESHOLD / sizeof(SortTuple) + 1)+
/* GUC variables */
#ifdef TRACE_SORT
bool trace_sort = false;
@@ -241,6 +251,14 @@ struct Tuplesortstate
int64 allowedMem; /* total memory allowed, in bytes */
int maxTapes; /* number of tapes (Knuth's T) */
int tapeRange; /* maxTapes-1 (Knuth's P) */
+ int64 maxSpace; /* maximum amount of space occupied among sort+ * of groups, either in-memory or on-disk */+ bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk+ * space, false when it's value for in-memory+ * space */+ TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */+ MemoryContext maincontext; /* memory context for tuple sort metadata that+ * persists across multiple batches */
MemoryContext sortcontext; /* memory context holding most sort data */
MemoryContext tuplecontext; /* sub-context of sortcontext for tuple data */
LogicalTapeSet *tapeset; /* logtape.c object for tapes in a temp file */
@@ -591,6 +609,7 @@ struct Sharedsort
static Tuplesortstate *tuplesort_begin_common(int workMem,
SortCoordinate coordinate,
bool randomAccess);
+static void tuplesort_begin_batch(Tuplesortstate *state);
static void puttuple_common(Tuplesortstate *state, SortTuple *tuple);
static bool consider_abort_common(Tuplesortstate *state);
static void inittapes(Tuplesortstate *state, bool mergeruns);
@@ -647,6 +666,8 @@ static void worker_freeze_result_tape(Tuplesortstate *state);
static void worker_nomergeruns(Tuplesortstate *state);
static void leader_takeover_tapes(Tuplesortstate *state);
static void free_sort_tuple(Tuplesortstate *state, SortTuple *stup);
+static void tuplesort_free(Tuplesortstate *state);+static void tuplesort_updatemax(Tuplesortstate *state);
/*
* Special versions of qsort just for SortTuple objects. qsort_tuple() sorts
@@ -682,8 +703,8 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
bool randomAccess)
{
Tuplesortstate *state;
+ MemoryContext maincontext;
MemoryContext sortcontext;
- MemoryContext tuplecontext;
MemoryContext oldcontext;
/* See leader_takeover_tapes() remarks on randomAccess support */
@@ -691,31 +712,31 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
elog(ERROR, "random access disallowed under parallel sort");
/*
- * Create a working memory context for this sort operation. All data- * needed by the sort will live inside this context.+ * Memory context surviving tuplesort_reset. This memory context holds+ * data which is useful to keep while sorting multiple similar batches.
*/
- sortcontext = AllocSetContextCreate(CurrentMemoryContext,+ maincontext = AllocSetContextCreate(CurrentMemoryContext,
"TupleSort main",
ALLOCSET_DEFAULT_SIZES);
/*
- * Caller tuple (e.g. IndexTuple) memory context.- *- * A dedicated child context used exclusively for caller passed tuples- * eases memory management. Resetting at key points reduces- * fragmentation. Note that the memtuples array of SortTuples is allocated- * in the parent context, not this context, because there is no need to- * free memtuples early.+ * Create a working memory context for one sort operation. The content of+ * this context is deleted by tuplesort_reset.
*/
- tuplecontext = AllocSetContextCreate(sortcontext,- "Caller tuples",- ALLOCSET_DEFAULT_SIZES);+ sortcontext = AllocSetContextCreate(maincontext,+ "TupleSort sort",+ ALLOCSET_DEFAULT_SIZES);
/*
- * Make the Tuplesortstate within the per-sort context. This way, we+ * Additionally a working memory context for tuples is setup in+ * tuplesort_begin_batch.+ */++ /*+ * Make the Tuplesortstate within the per-sortstate context. This way, we
* don't need a separate pfree() operation for it at shutdown.
*/
- oldcontext = MemoryContextSwitchTo(sortcontext);+ oldcontext = MemoryContextSwitchTo(maincontext);
state = (Tuplesortstate *) palloc0(sizeof(Tuplesortstate));
@@ -724,11 +745,8 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
pg_rusage_init(&state->ru_start);
#endif
- state->status = TSS_INITIAL;
state->randomAccess = randomAccess;
- state->bounded = false;
state->tuples = true;
- state->boundUsed = false;
/*
* workMem is forced to be at least 64KB, the current minimum valid value
@@ -737,38 +755,21 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
* with very little memory.
*/
state->allowedMem = Max(workMem, 64) * (int64) 1024;
- state->availMem = state->allowedMem;
state->sortcontext = sortcontext;
- state->tuplecontext = tuplecontext;- state->tapeset = NULL;-- state->memtupcount = 0;+ state->maincontext = maincontext;
/*
* Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;
* see comments in grow_memtuples().
*/
- state->memtupsize = Max(1024,- ALLOCSET_SEPARATE_THRESHOLD / sizeof(SortTuple) + 1);-- state->growmemtuples = true;- state->slabAllocatorUsed = false;- state->memtuples = (SortTuple *) palloc(state->memtupsize * sizeof(SortTuple));-- USEMEM(state, GetMemoryChunkSpace(state->memtuples));-- /* workMem must be large enough for the minimal memtuples array */- if (LACKMEM(state))- elog(ERROR, "insufficient memory allowed for sort");-- state->currentRun = 0;+ state->memtupsize = INITIAL_MEMTUPSIZE;+ state->memtuples = NULL;
/*
- * maxTapes, tapeRange, and Algorithm D variables will be initialized by- * inittapes(), if needed+ * After all of the other non-parallel-related state, we setup all of the+ * state needed for each batch.
*/
-- state->result_tape = -1; /* flag that result tape has not been formed */+ tuplesort_begin_batch(state);
/*
* Initialize parallel-related state based on coordination information
@@ -802,6 +803,70 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
return state;
}
+static void+tuplesort_begin_batch(Tuplesortstate *state)+{+ MemoryContext oldcontext;++ oldcontext = MemoryContextSwitchTo(state->maincontext);++ /*+ * Caller tuple (e.g. IndexTuple) memory context.+ *+ * A dedicated child context used exclusively for caller passed tuples+ * eases memory management. Resetting at key points reduces+ * fragmentation. Note that the memtuples array of SortTuples is allocated+ * in the parent context, not this context, because there is no need to+ * free memtuples early.+ */+ state->tuplecontext = AllocSetContextCreate(state->sortcontext,+ "Caller tuples",+ ALLOCSET_DEFAULT_SIZES);++ state->status = TSS_INITIAL;+ state->bounded = false;+ state->boundUsed = false;++ state->availMem = state->allowedMem;++ state->tapeset = NULL;++ state->memtupcount = 0;++ /*+ * Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;+ * see comments in grow_memtuples().+ */+ state->growmemtuples = true;+ state->slabAllocatorUsed = false;+ if (state->memtuples != NULL && state->memtupsize != INITIAL_MEMTUPSIZE)+ {+ pfree(state->memtuples);+ state->memtuples = NULL;+ state->memtupsize = INITIAL_MEMTUPSIZE;+ }+ if (state->memtuples == NULL)+ {+ state->memtuples = (SortTuple *) palloc(state->memtupsize * sizeof(SortTuple));+ USEMEM(state, GetMemoryChunkSpace(state->memtuples));+ }++ /* workMem must be large enough for the minimal memtuples array */+ if (LACKMEM(state))+ elog(ERROR, "insufficient memory allowed for sort");++ state->currentRun = 0;++ /*+ * maxTapes, tapeRange, and Algorithm D variables will be initialized by+ * inittapes(), if needed+ */++ state->result_tape = -1; /* flag that result tape has not been formed */++ MemoryContextSwitchTo(oldcontext);+}+
Tuplesortstate *
tuplesort_begin_heap(TupleDesc tupDesc,
int nkeys, AttrNumber *attNums,
@@ -814,7 +879,7 @@ tuplesort_begin_heap(TupleDesc tupDesc,
MemoryContext oldcontext;
int i;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
AssertArg(nkeys > 0);
@@ -890,7 +955,7 @@ tuplesort_begin_cluster(TupleDesc tupDesc,
Assert(indexRel->rd_rel->relam == BTREE_AM_OID);
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -985,7 +1050,7 @@ tuplesort_begin_index_btree(Relation heapRel,
MemoryContext oldcontext;
int i;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -1063,7 +1128,7 @@ tuplesort_begin_index_hash(Relation heapRel,
randomAccess);
MemoryContext oldcontext;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -1106,7 +1171,7 @@ tuplesort_begin_datum(Oid datumType, Oid sortOperator, Oid sortCollation,
int16 typlen;
bool typbyval;
- oldcontext = MemoryContextSwitchTo(state->sortcontext);+ oldcontext = MemoryContextSwitchTo(state->maincontext);
#ifdef TRACE_SORT
if (trace_sort)
@@ -1223,17 +1288,19 @@ tuplesort_set_bound(Tuplesortstate *state, int64 bound)
state->sortKeys->abbrev_full_comparator = NULL;
}
+bool+tuplesort_used_bound(Tuplesortstate *state)+{+ return state->boundUsed;+}+
/*
- * tuplesort_end- *- * Release resources and clean up.+ * tuplesort_free
*
- * NOTE: after calling this, any pointers returned by tuplesort_getXXX are- * pointing to garbage. Be careful not to attempt to use or free such- * pointers afterwards!+ * Internal routine for freeing resources of tuplesort.
*/
-void-tuplesort_end(Tuplesortstate *state)+static void+tuplesort_free(Tuplesortstate *state)
{
/* context swap probably not needed, but let's be safe */
MemoryContext oldcontext = MemoryContextSwitchTo(state->sortcontext);
@@ -1291,10 +1358,104 @@ tuplesort_end(Tuplesortstate *state)
MemoryContextSwitchTo(oldcontext);
/*
- * Free the per-sort memory context, thereby releasing all working memory,- * including the Tuplesortstate struct itself.+ * Free the per-sort memory context, thereby releasing all working memory.+ */+ MemoryContextReset(state->sortcontext);+}++/*+ * tuplesort_end+ *+ * Release resources and clean up.+ *+ * NOTE: after calling this, any pointers returned by tuplesort_getXXX are+ * pointing to garbage. Be careful not to attempt to use or free such+ * pointers afterwards!+ */+void+tuplesort_end(Tuplesortstate *state)+{+ tuplesort_free(state);++ /*+ * Free the main memory context, including the Tuplesortstate struct+ * itself.
*/
- MemoryContextDelete(state->sortcontext);+ MemoryContextDelete(state->maincontext);+}++/*+ * tuplesort_updatemax+ *+ * Update maximum resource usage statistics.+ */+static void+tuplesort_updatemax(Tuplesortstate *state)+{+ int64 spaceUsed;+ bool isSpaceDisk;++ /*+ * Note: it might seem we should provide both memory and disk usage for a+ * disk-based sort. However, the current code doesn't track memory space+ * accurately once we have begun to return tuples to the caller (since we+ * don't account for pfree's the caller is expected to do), so we cannot+ * rely on availMem in a disk sort. This does not seem worth the overhead+ * to fix. Is it worth creating an API for the memory context code to+ * tell us how much is actually used in sortcontext?+ */+ if (state->tapeset)+ {+ isSpaceDisk = true;+ spaceUsed = LogicalTapeSetBlocks(state->tapeset) * BLCKSZ;+ }+ else+ {+ isSpaceDisk = false;+ spaceUsed = state->allowedMem - state->availMem;+ }++ /*+ * Sort evicts data to the disk when it didn't manage to fit those data to+ * the main memory. This is why we assume space used on the disk to be+ * more important for tracking resource usage than space used in memory.+ * Note that amount of space occupied by some tuple set on the disk might+ * be less than amount of space occupied by the same tuple set in the+ * memory due to more compact representation.+ */+ if ((isSpaceDisk && !state->isMaxSpaceDisk) ||+ (isSpaceDisk == state->isMaxSpaceDisk && spaceUsed > state->maxSpace))+ {+ state->maxSpace = spaceUsed;+ state->isMaxSpaceDisk = isSpaceDisk;+ state->maxSpaceStatus = state->status;+ }+}++/*+ * tuplesort_reset+ *+ * Reset the tuplesort. Reset all the data in the tuplesort, but leave the+ * meta-information in. After tuplesort_reset, tuplesort is ready to start+ * a new sort. This allows avoiding recreation of tuple sort states (and+ * save resources) when sorting multiple small batches.+ */+void+tuplesort_reset(Tuplesortstate *state)+{+ tuplesort_updatemax(state);+ tuplesort_free(state);++ /*+ * After we've freed up per-batch memory, re-setup all of the state common+ * to both the first batch and any subsequent batch.+ */+ tuplesort_begin_batch(state);++ state->lastReturnedTuple = NULL;+ state->slabMemoryBegin = NULL;+ state->slabMemoryEnd = NULL;+ state->slabFreeHead = NULL;
}
/*
@@ -2591,8 +2752,7 @@ mergeruns(Tuplesortstate *state)
* Reset tuple memory. We've freed all the tuples that we previously
* allocated. We will use the slab allocator from now on.
*/
- MemoryContextDelete(state->tuplecontext);- state->tuplecontext = NULL;+ MemoryContextResetOnly(state->tuplecontext);
/*
* We no longer need a large memtuples array. (We will allocate a smaller
@@ -2642,7 +2802,8 @@ mergeruns(Tuplesortstate *state)
* from each input tape.
*/
state->memtupsize = numInputTapes;
- state->memtuples = (SortTuple *) palloc(numInputTapes * sizeof(SortTuple));+ state->memtuples = (SortTuple *) MemoryContextAlloc(state->maincontext,+ numInputTapes * sizeof(SortTuple));
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
/*
@@ -3138,18 +3299,15 @@ tuplesort_get_stats(Tuplesortstate *state,
* to fix. Is it worth creating an API for the memory context code to
* tell us how much is actually used in sortcontext?
*/
- if (state->tapeset)- {+ tuplesort_updatemax(state);++ if (state->isMaxSpaceDisk)
stats->spaceType = SORT_SPACE_TYPE_DISK;
- stats->spaceUsed = LogicalTapeSetBlocks(state->tapeset) * (BLCKSZ / 1024);- }
else
- {
stats->spaceType = SORT_SPACE_TYPE_MEMORY;
- stats->spaceUsed = (state->allowedMem - state->availMem + 1023) / 1024;- }+ stats->spaceUsed = (state->maxSpace + 1023) / 1024;- switch (state->status)+ switch (state->maxSpaceStatus)
{
case TSS_SORTEDINMEM:
if (state->boundUsed)
diff --git a/src/include/executor/execdebug.h b/src/include/executor/execdebug.hindex 2e9920111f..4af6e0013d 100644--- a/src/include/executor/execdebug.h+++ b/src/include/executor/execdebug.h@@ -86,10 +86,12 @@
#define SO_nodeDisplay(l) nodeDisplay(l)
#define SO_printf(s) printf(s)
#define SO1_printf(s, p) printf(s, p)
+#define SO2_printf(s, p1, p2) printf(s, p1, p2)
#else
#define SO_nodeDisplay(l)
#define SO_printf(s)
#define SO1_printf(s, p)
+#define SO2_printf(s, p1, p2)
#endif /* EXEC_SORTDEBUG */
/* ----------------
diff --git a/src/include/executor/nodeIncrementalSort.h b/src/include/executor/nodeIncrementalSort.hnew file mode 100644index 0000000000..e62c02a4f3--- /dev/null+++ b/src/include/executor/nodeIncrementalSort.h@@ -0,0 +1,28 @@+/*-------------------------------------------------------------------------+ *+ * nodeIncrementalSort.h+ *+ * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group+ * Portions Copyright (c) 1994, Regents of the University of California+ *+ * src/include/executor/nodeIncrementalSort.h+ *+ *-------------------------------------------------------------------------+ */+#ifndef NODEINCREMENTALSORT_H+#define NODEINCREMENTALSORT_H++#include "access/parallel.h"+#include "nodes/execnodes.h"++extern IncrementalSortState *ExecInitIncrementalSort(IncrementalSort *node, EState *estate, int eflags);+extern void ExecEndIncrementalSort(IncrementalSortState *node);+extern void ExecReScanIncrementalSort(IncrementalSortState *node);++/* parallel instrumentation support */+extern void ExecIncrementalSortEstimate(IncrementalSortState *node, ParallelContext *pcxt);+extern void ExecIncrementalSortInitializeDSM(IncrementalSortState *node, ParallelContext *pcxt);+extern void ExecIncrementalSortInitializeWorker(IncrementalSortState *node, ParallelWorkerContext *pcxt);+extern void ExecIncrementalSortRetrieveInstrumentation(IncrementalSortState *node);++#endif /* NODEINCREMENTALSORT_H */diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.hindex 3d27d50f09..e6a9b67675 100644--- a/src/include/nodes/execnodes.h+++ b/src/include/nodes/execnodes.h@@ -1980,6 +1980,21 @@ typedef struct MaterialState
Tuplestorestate *tuplestorestate;
} MaterialState;
++/* ----------------+ * When performing sorting by multiple keys, it's possible that the input+ * dataset is already sorted on a prefix of those keys. We call these+ * "presorted keys".+ * PresortedKeyData represents information about one such key.+ * ----------------+ */+typedef struct PresortedKeyData+{+ FmgrInfo flinfo; /* comparison function info */+ FunctionCallInfo fcinfo; /* comparison function call info */+ OffsetNumber attno; /* attribute number in tuple */+} PresortedKeyData;+
/* ----------------
* Shared memory container for per-worker sort information
* ----------------
@@ -2008,6 +2023,68 @@ typedef struct SortState
SharedSortInfo *shared_info; /* one entry per worker */
} SortState;
+typedef struct IncrementalSortGroupInfo+{+ int64 groupCount;+ long maxDiskSpaceUsed;+ long totalDiskSpaceUsed;+ long maxMemorySpaceUsed;+ long totalMemorySpaceUsed;+ List *sortMethods;+} IncrementalSortGroupInfo;++typedef struct IncrementalSortInfo+{+ IncrementalSortGroupInfo fullsortGroupInfo;+ IncrementalSortGroupInfo prefixsortGroupInfo;+} IncrementalSortInfo;++/* ----------------+ * Shared memory container for per-worker incremental sort information+ * ----------------+ */+typedef struct SharedIncrementalSortInfo+{+ int num_workers;+ IncrementalSortInfo sinfo[FLEXIBLE_ARRAY_MEMBER];+} SharedIncrementalSortInfo;++/* ----------------+ * IncrementalSortState information+ * ----------------+ */+typedef enum+{+ INCSORT_LOADFULLSORT,+ INCSORT_LOADPREFIXSORT,+ INCSORT_READFULLSORT,+ INCSORT_READPREFIXSORT,+} IncrementalSortExecutionStatus;++typedef struct IncrementalSortState+{+ ScanState ss; /* its first field is NodeTag */+ bool bounded; /* is the result set bounded? */+ int64 bound; /* if bounded, how many tuples are needed */+ bool sort_Done; /* sort completed yet? */+ bool outerNodeDone; /* finished fetching tuples from outer node */+ int64 bound_Done; /* value of bound we did the sort with */+ IncrementalSortExecutionStatus execution_status;+ int64 n_fullsort_remaining;+ Tuplesortstate *fullsort_state; /* private state of tuplesort.c */+ Tuplesortstate *prefixsort_state; /* private state of tuplesort.c */+ /* the keys by which the input path is already sorted */+ PresortedKeyData *presorted_keys;++ IncrementalSortInfo incsort_info;++ /* slot for pivot tuple defining values of presorted keys within group */+ TupleTableSlot *group_pivot;+ TupleTableSlot *transfer_tuple;+ bool am_worker; /* are we a worker? */+ SharedIncrementalSortInfo *shared_info; /* one entry per worker */+} IncrementalSortState;+
/* ---------------------
* GroupState information
* ---------------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.hindex 8a76afe8cc..50b1ba5186 100644--- a/src/include/nodes/nodes.h+++ b/src/include/nodes/nodes.h@@ -74,6 +74,7 @@ typedef enum NodeTag
T_HashJoin,
T_Material,
T_Sort,
+ T_IncrementalSort,
T_Group,
T_Agg,
T_WindowAgg,
@@ -130,6 +131,7 @@ typedef enum NodeTag
T_HashJoinState,
T_MaterialState,
T_SortState,
+ T_IncrementalSortState,
T_GroupState,
T_AggState,
T_WindowAggState,
@@ -245,6 +247,7 @@ typedef enum NodeTag
T_ProjectionPath,
T_ProjectSetPath,
T_SortPath,
+ T_IncrementalSortPath,
T_GroupPath,
T_UpperUniquePath,
T_AggPath,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.hindex 0ceb809644..28d580dd3c 100644--- a/src/include/nodes/pathnodes.h+++ b/src/include/nodes/pathnodes.h@@ -1620,6 +1620,15 @@ typedef struct SortPath
Path *subpath; /* path representing input source */
} SortPath;
+/*+ * IncrementalSortPath+ */+typedef struct IncrementalSortPath+{+ SortPath spath;+ int presortedCols; /* number of presorted columns */+} IncrementalSortPath;+
/*
* GroupPath represents grouping (of presorted input)
*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.hindex 4869fe7b6d..fe4046b64b 100644--- a/src/include/nodes/plannodes.h+++ b/src/include/nodes/plannodes.h@@ -774,6 +774,17 @@ typedef struct Sort
bool *nullsFirst; /* NULLS FIRST/LAST directions */
} Sort;
++/* ----------------+ * incremental sort node+ * ----------------+ */+typedef struct IncrementalSort+{+ Sort sort;+ int presortedCols; /* number of presorted columns */+} IncrementalSort;+
/* ---------------
* group node -
* Used for queries with GROUP BY (but no aggregates) specified.
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.hindex 735ba09650..5725b4828e 100644--- a/src/include/optimizer/cost.h+++ b/src/include/optimizer/cost.h@@ -53,6 +53,7 @@ extern PGDLLIMPORT bool enable_indexonlyscan;
extern PGDLLIMPORT bool enable_bitmapscan;
extern PGDLLIMPORT bool enable_tidscan;
extern PGDLLIMPORT bool enable_sort;
+extern PGDLLIMPORT bool enable_incrementalsort;
extern PGDLLIMPORT bool enable_hashagg;
extern PGDLLIMPORT bool enable_hashagg_disk;
extern PGDLLIMPORT bool enable_groupingsets_hash_disk;
@@ -103,6 +104,15 @@ extern void cost_sort(Path *path, PlannerInfo *root,
List *pathkeys, Cost input_cost, double tuples, int width,
Cost comparison_cost, int sort_mem,
double limit_tuples);
+extern void cost_full_sort(Cost *startup_cost, Cost *run_cost,+ Cost input_total_cost, double tuples, int width,+ Cost comparison_cost, int sort_mem,+ double limit_tuples);+extern void cost_incremental_sort(Path *path,+ PlannerInfo *root, List *pathkeys, int presorted_keys,+ Cost input_startup_cost, Cost input_total_cost,+ double input_tuples, int width, Cost comparison_cost, int sort_mem,+ double limit_tuples);
extern void cost_append(AppendPath *path);
extern void cost_merge_append(Path *path, PlannerInfo *root,
List *pathkeys, int n_streams,
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.hindex e450fe112a..bcd08af753 100644--- a/src/include/optimizer/pathnode.h+++ b/src/include/optimizer/pathnode.h@@ -184,6 +184,12 @@ extern ProjectSetPath *create_set_projection_path(PlannerInfo *root,
RelOptInfo *rel,
Path *subpath,
PathTarget *target);
+extern SortPath *create_incremental_sort_path(PlannerInfo *root,+ RelOptInfo *rel,+ Path *subpath,+ List *pathkeys,+ int presorted_keys,+ double limit_tuples);
extern SortPath *create_sort_path(PlannerInfo *root,
RelOptInfo *rel,
Path *subpath,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.hindex 9ab73bd20c..85f5fe37ea 100644--- a/src/include/optimizer/paths.h+++ b/src/include/optimizer/paths.h@@ -188,6 +188,8 @@ typedef enum
extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
extern bool pathkeys_contained_in(List *keys1, List *keys2);
+extern bool pathkeys_common_contained_in(List *keys1, List *keys2, int *n_common);+extern int pathkeys_common(List *keys1, List *keys2);
extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
Relids required_outer,
CostSelector cost_criterion,
diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.hindex a2fdd3fcd3..0e9ab4e586 100644--- a/src/include/utils/tuplesort.h+++ b/src/include/utils/tuplesort.h@@ -215,6 +215,7 @@ extern Tuplesortstate *tuplesort_begin_datum(Oid datumType,
bool randomAccess);
extern void tuplesort_set_bound(Tuplesortstate *state, int64 bound);
+extern bool tuplesort_used_bound(Tuplesortstate *state);
extern void tuplesort_puttupleslot(Tuplesortstate *state,
TupleTableSlot *slot);
@@ -239,6 +240,8 @@ extern bool tuplesort_skiptuples(Tuplesortstate *state, int64 ntuples,
extern void tuplesort_end(Tuplesortstate *state);
+extern void tuplesort_reset(Tuplesortstate *state);+
extern void tuplesort_get_stats(Tuplesortstate *state,
TuplesortInstrumentation *stats);
extern const char *tuplesort_method_name(TuplesortMethod m);
diff --git a/src/test/isolation/expected/drop-index-concurrently-1.out b/src/test/isolation/expected/drop-index-concurrently-1.outindex 75dff56bc4..8e6adb66bb 100644--- a/src/test/isolation/expected/drop-index-concurrently-1.out+++ b/src/test/isolation/expected/drop-index-concurrently-1.out@@ -21,7 +21,7 @@ QUERY PLAN
Sort
Sort Key: id, data
- -> Seq Scan on test_dc+ -> Index Scan using test_dc_pkey on test_dc
Filter: ((data)::text = '34'::text)
step select2: SELECT * FROM test_dc WHERE data=34 ORDER BY id,data;
id data
diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.outnew file mode 100644index 0000000000..4f6f2288a3--- /dev/null+++ b/src/test/regress/expected/incremental_sort.out@@ -0,0 +1,1320 @@+-- When we have to sort the entire table, incremental sort will+-- be slower than plain sort, so it should not be used.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;+ QUERY PLAN +-----------------------------------+ Sort+ Sort Key: tenk1.four, tenk1.ten+ -> Sort+ Sort Key: tenk1.four+ -> Seq Scan on tenk1+(5 rows)++-- When there is a LIMIT clause, incremental sort is beneficial because+-- it only has to sort some of the groups, and not the entire table.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten+limit 1;+ QUERY PLAN +-----------------------------------------+ Limit+ -> Incremental Sort+ Sort Key: tenk1.four, tenk1.ten+ Presorted Key: tenk1.four+ -> Sort+ Sort Key: tenk1.four+ -> Seq Scan on tenk1+(7 rows)++-- When work_mem is not enough to sort the entire table, incremental sort+-- may be faster if individual groups still fit into work_mem.+set work_mem to '2MB';+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;+ QUERY PLAN +-----------------------------------+ Incremental Sort+ Sort Key: tenk1.four, tenk1.ten+ Presorted Key: tenk1.four+ -> Sort+ Sort Key: tenk1.four+ -> Seq Scan on tenk1+(6 rows)++reset work_mem;+-- TODO if an analyze happens here the plans might change; should we+-- solve by inserting extra rows or by adding a GUC that would somehow+-- forcing the time of plan we expect.+create table t(a integer, b integer);+-- A single large group tested around each mode transition point.+insert into t(a, b) select 1, i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 31;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+(31 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 32;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+(32 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 33;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+(33 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 65;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+ 1 | 34+ 1 | 35+ 1 | 36+ 1 | 37+ 1 | 38+ 1 | 39+ 1 | 40+ 1 | 41+ 1 | 42+ 1 | 43+ 1 | 44+ 1 | 45+ 1 | 46+ 1 | 47+ 1 | 48+ 1 | 49+ 1 | 50+ 1 | 51+ 1 | 52+ 1 | 53+ 1 | 54+ 1 | 55+ 1 | 56+ 1 | 57+ 1 | 58+ 1 | 59+ 1 | 60+ 1 | 61+ 1 | 62+ 1 | 63+ 1 | 64+ 1 | 65+(65 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 66;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+ 1 | 34+ 1 | 35+ 1 | 36+ 1 | 37+ 1 | 38+ 1 | 39+ 1 | 40+ 1 | 41+ 1 | 42+ 1 | 43+ 1 | 44+ 1 | 45+ 1 | 46+ 1 | 47+ 1 | 48+ 1 | 49+ 1 | 50+ 1 | 51+ 1 | 52+ 1 | 53+ 1 | 54+ 1 | 55+ 1 | 56+ 1 | 57+ 1 | 58+ 1 | 59+ 1 | 60+ 1 | 61+ 1 | 62+ 1 | 63+ 1 | 64+ 1 | 65+ 1 | 66+(66 rows)++delete from t;+-- An initial large group followed by a small group.+insert into t(a, b) select (case when i < 50 then 1 else 2 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 55;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 55;+ a | b +---+----+ 1 | 1+ 1 | 2+ 1 | 3+ 1 | 4+ 1 | 5+ 1 | 6+ 1 | 7+ 1 | 8+ 1 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 1 | 20+ 1 | 21+ 1 | 22+ 1 | 23+ 1 | 24+ 1 | 25+ 1 | 26+ 1 | 27+ 1 | 28+ 1 | 29+ 1 | 30+ 1 | 31+ 1 | 32+ 1 | 33+ 1 | 34+ 1 | 35+ 1 | 36+ 1 | 37+ 1 | 38+ 1 | 39+ 1 | 40+ 1 | 41+ 1 | 42+ 1 | 43+ 1 | 44+ 1 | 45+ 1 | 46+ 1 | 47+ 1 | 48+ 1 | 49+ 2 | 50+ 2 | 51+ 2 | 52+ 2 | 53+ 2 | 54+ 2 | 55+(55 rows)++-- Test EXPLAIN ANALYZE (text output) with only a fullsort group.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 55;+ QUERY PLAN +-------------------------------------------------------------------------------------------------+ Limit (actual rows=55 loops=1)+ -> Incremental Sort (actual rows=55 loops=1)+ Sort Key: t.a, t.b+ Presorted Key: t.a+ Full-sort Groups: 2 (Methods: quicksort, top-N heapsort) Memory: 27kB (avg), 27kB (max)+ -> Sort (actual rows=100 loops=1)+ Sort Key: t.a+ Sort Method: quicksort Memory: 30kB+ -> Seq Scan on t (actual rows=100 loops=1)+(9 rows)++explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 55;+ QUERY PLAN +-------------------------------------------------------------------+ [ ++ { ++ "Plan": { ++ "Node Type": "Limit", ++ "Parallel Aware": false, ++ "Actual Rows": 55, ++ "Actual Loops": 1, ++ "Plans": [ ++ { ++ "Node Type": "Incremental Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 55, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a", "t.b"], ++ "Presorted Key": ["t.a"], ++ "Full-sort Groups": { ++ "Group Count": 2, ++ "Sort Methods Used": ["quicksort", "top-N heapsort"],++ "Average Sort Space Used": 27, ++ "Maximum Sort Space Used": 27, ++ "Sort Space Type": "Memory" ++ }, ++ "Plans": [ ++ { ++ "Node Type": "Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 100, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a"], ++ "Sort Method": "quicksort", ++ "Sort Space Used": 30, ++ "Sort Space Type": "Memory", ++ "Plans": [ ++ { ++ "Node Type": "Seq Scan", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Relation Name": "t", ++ "Alias": "t", ++ "Actual Rows": 100, ++ "Actual Loops": 1 ++ } ++ ] ++ } ++ ] ++ } ++ ] ++ }, ++ "Triggers": [ ++ ] ++ } ++ ]+(1 row)++delete from t;+-- An initial small group followed by a large group.+insert into t(a, b) select (case when i < 5 then i else 9 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 70;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 70;+ a | b +---+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 9 | 5+ 9 | 6+ 9 | 7+ 9 | 8+ 9 | 9+ 9 | 10+ 9 | 11+ 9 | 12+ 9 | 13+ 9 | 14+ 9 | 15+ 9 | 16+ 9 | 17+ 9 | 18+ 9 | 19+ 9 | 20+ 9 | 21+ 9 | 22+ 9 | 23+ 9 | 24+ 9 | 25+ 9 | 26+ 9 | 27+ 9 | 28+ 9 | 29+ 9 | 30+ 9 | 31+ 9 | 32+ 9 | 33+ 9 | 34+ 9 | 35+ 9 | 36+ 9 | 37+ 9 | 38+ 9 | 39+ 9 | 40+ 9 | 41+ 9 | 42+ 9 | 43+ 9 | 44+ 9 | 45+ 9 | 46+ 9 | 47+ 9 | 48+ 9 | 49+ 9 | 50+ 9 | 51+ 9 | 52+ 9 | 53+ 9 | 54+ 9 | 55+ 9 | 56+ 9 | 57+ 9 | 58+ 9 | 59+ 9 | 60+ 9 | 61+ 9 | 62+ 9 | 63+ 9 | 64+ 9 | 65+ 9 | 66+ 9 | 67+ 9 | 68+ 9 | 69+ 9 | 70+(70 rows)++-- Test EXPLAIN ANALYZE (text output) with both fullsort and presorted groups.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 70;+ QUERY PLAN +---------------------------------------------------------------------------------+ Limit (actual rows=70 loops=1)+ -> Incremental Sort (actual rows=70 loops=1)+ Sort Key: t.a, t.b+ Presorted Key: t.a+ Full-sort Groups: 1 (Methods: quicksort) Memory: 28kB (avg), 28kB (max)+ Presorted Groups: 5 (Methods: quicksort) Memory: 25kB (avg), 25kB (max)+ -> Sort (actual rows=100 loops=1)+ Sort Key: t.a+ Sort Method: quicksort Memory: 30kB+ -> Seq Scan on t (actual rows=100 loops=1)+(10 rows)++explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 70;+ QUERY PLAN +---------------------------------------------------+ [ ++ { ++ "Plan": { ++ "Node Type": "Limit", ++ "Parallel Aware": false, ++ "Actual Rows": 70, ++ "Actual Loops": 1, ++ "Plans": [ ++ { ++ "Node Type": "Incremental Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 70, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a", "t.b"], ++ "Presorted Key": ["t.a"], ++ "Full-sort Groups": { ++ "Group Count": 1, ++ "Sort Methods Used": ["quicksort"], ++ "Average Sort Space Used": 28, ++ "Maximum Sort Space Used": 28, ++ "Sort Space Type": "Memory" ++ }, ++ "Presorted Groups": { ++ "Group Count": 5, ++ "Sort Methods Used": ["quicksort"], ++ "Average Sort Space Used": 25, ++ "Maximum Sort Space Used": 25, ++ "Sort Space Type": "Memory" ++ }, ++ "Plans": [ ++ { ++ "Node Type": "Sort", ++ "Parent Relationship": "Outer", ++ "Parallel Aware": false, ++ "Actual Rows": 100, ++ "Actual Loops": 1, ++ "Sort Key": ["t.a"], ++ "Sort Method": "quicksort", ++ "Sort Space Used": 30, ++ "Sort Space Type": "Memory", ++ "Plans": [ ++ { ++ "Node Type": "Seq Scan", ++ "Parent Relationship": "Outer",++ "Parallel Aware": false, ++ "Relation Name": "t", ++ "Alias": "t", ++ "Actual Rows": 100, ++ "Actual Loops": 1 ++ } ++ ] ++ } ++ ] ++ } ++ ] ++ }, ++ "Triggers": [ ++ ] ++ } ++ ]+(1 row)++delete from t;+-- Small groups of 10 tuples each tested around each mode transition point.+insert into t(a, b) select i / 10, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 31;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+(31 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 32;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+(32 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 33;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+ 3 | 33+(33 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 65;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+ 3 | 33+ 3 | 34+ 3 | 35+ 3 | 36+ 3 | 37+ 3 | 38+ 3 | 39+ 4 | 40+ 4 | 41+ 4 | 42+ 4 | 43+ 4 | 44+ 4 | 45+ 4 | 46+ 4 | 47+ 4 | 48+ 4 | 49+ 5 | 50+ 5 | 51+ 5 | 52+ 5 | 53+ 5 | 54+ 5 | 55+ 5 | 56+ 5 | 57+ 5 | 58+ 5 | 59+ 6 | 60+ 6 | 61+ 6 | 62+ 6 | 63+ 6 | 64+ 6 | 65+(65 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 66;+ a | b +---+----+ 0 | 1+ 0 | 2+ 0 | 3+ 0 | 4+ 0 | 5+ 0 | 6+ 0 | 7+ 0 | 8+ 0 | 9+ 1 | 10+ 1 | 11+ 1 | 12+ 1 | 13+ 1 | 14+ 1 | 15+ 1 | 16+ 1 | 17+ 1 | 18+ 1 | 19+ 2 | 20+ 2 | 21+ 2 | 22+ 2 | 23+ 2 | 24+ 2 | 25+ 2 | 26+ 2 | 27+ 2 | 28+ 2 | 29+ 3 | 30+ 3 | 31+ 3 | 32+ 3 | 33+ 3 | 34+ 3 | 35+ 3 | 36+ 3 | 37+ 3 | 38+ 3 | 39+ 4 | 40+ 4 | 41+ 4 | 42+ 4 | 43+ 4 | 44+ 4 | 45+ 4 | 46+ 4 | 47+ 4 | 48+ 4 | 49+ 5 | 50+ 5 | 51+ 5 | 52+ 5 | 53+ 5 | 54+ 5 | 55+ 5 | 56+ 5 | 57+ 5 | 58+ 5 | 59+ 6 | 60+ 6 | 61+ 6 | 62+ 6 | 63+ 6 | 64+ 6 | 65+ 6 | 66+(66 rows)++delete from t;+-- Small groups of only 1 tuple each tested around each mode transition point.+insert into t(a, b) select i, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 31;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+(31 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 32;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+(32 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 33;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+ 33 | 33+(33 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 65;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+ 33 | 33+ 34 | 34+ 35 | 35+ 36 | 36+ 37 | 37+ 38 | 38+ 39 | 39+ 40 | 40+ 41 | 41+ 42 | 42+ 43 | 43+ 44 | 44+ 45 | 45+ 46 | 46+ 47 | 47+ 48 | 48+ 49 | 49+ 50 | 50+ 51 | 51+ 52 | 52+ 53 | 53+ 54 | 54+ 55 | 55+ 56 | 56+ 57 | 57+ 58 | 58+ 59 | 59+ 60 | 60+ 61 | 61+ 62 | 62+ 63 | 63+ 64 | 64+ 65 | 65+(65 rows)++explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+ QUERY PLAN +---------------------------------+ Limit+ -> Incremental Sort+ Sort Key: t.a, t.b+ Presorted Key: t.a+ -> Sort+ Sort Key: t.a+ -> Seq Scan on t+(7 rows)++select * from (select * from t order by a) s order by a, b limit 66;+ a | b +----+----+ 1 | 1+ 2 | 2+ 3 | 3+ 4 | 4+ 5 | 5+ 6 | 6+ 7 | 7+ 8 | 8+ 9 | 9+ 10 | 10+ 11 | 11+ 12 | 12+ 13 | 13+ 14 | 14+ 15 | 15+ 16 | 16+ 17 | 17+ 18 | 18+ 19 | 19+ 20 | 20+ 21 | 21+ 22 | 22+ 23 | 23+ 24 | 24+ 25 | 25+ 26 | 26+ 27 | 27+ 28 | 28+ 29 | 29+ 30 | 30+ 31 | 31+ 32 | 32+ 33 | 33+ 34 | 34+ 35 | 35+ 36 | 36+ 37 | 37+ 38 | 38+ 39 | 39+ 40 | 40+ 41 | 41+ 42 | 42+ 43 | 43+ 44 | 44+ 45 | 45+ 46 | 46+ 47 | 47+ 48 | 48+ 49 | 49+ 50 | 50+ 51 | 51+ 52 | 52+ 53 | 53+ 54 | 54+ 55 | 55+ 56 | 56+ 57 | 57+ 58 | 58+ 59 | 59+ 60 | 60+ 61 | 61+ 62 | 62+ 63 | 63+ 64 | 64+ 65 | 65+ 66 | 66+(66 rows)++delete from t;+drop table t;diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.outindex fbc8d3ac6c..4425853572 100644--- a/src/test/regress/expected/partition_aggregate.out+++ b/src/test/regress/expected/partition_aggregate.out@@ -8,6 +8,8 @@ SET enable_partitionwise_aggregate TO true;
SET enable_partitionwise_join TO true;
-- Disable parallel plans.
SET max_parallel_workers_per_gather TO 0;
+-- Disable incremental sort, which can influence selected plans due to fuzz factor.+SET enable_incrementalsort TO off;
--
-- Tests for list partitioned tables.
--
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.outindex 715842b87a..a126f0ad61 100644--- a/src/test/regress/expected/sysviews.out+++ b/src/test/regress/expected/sysviews.out@@ -78,6 +78,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_hashagg | on
enable_hashagg_disk | on
enable_hashjoin | on
+ enable_incrementalsort | on
enable_indexonlyscan | on
enable_indexscan | on
enable_material | on
@@ -91,7 +92,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_seqscan | on
enable_sort | on
enable_tidscan | on
-(19 rows)+(20 rows)
-- Test that the pg_timezone_names and pg_timezone_abbrevs views are
-- more-or-less working. We can't test their contents in any great detail
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_scheduleindex d2b17dd3ea..175c1d5a49 100644--- a/src/test/regress/parallel_schedule+++ b/src/test/regress/parallel_schedule@@ -78,7 +78,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tidscan collate.icu.utf8+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tidscan collate.icu.utf8 incremental_sort
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_scheduleindex acba391332..2bcd994361 100644--- a/src/test/regress/serial_schedule+++ b/src/test/regress/serial_schedule@@ -88,6 +88,7 @@ test: select_distinct_on
test: select_implicit
test: select_having
test: subselect
+test: incremental_sort
test: union
test: case
test: join
diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sqlnew file mode 100644index 0000000000..9320a10b91--- /dev/null+++ b/src/test/regress/sql/incremental_sort.sql@@ -0,0 +1,88 @@+-- When we have to sort the entire table, incremental sort will+-- be slower than plain sort, so it should not be used.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;++-- When there is a LIMIT clause, incremental sort is beneficial because+-- it only has to sort some of the groups, and not the entire table.+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten+limit 1;++-- When work_mem is not enough to sort the entire table, incremental sort+-- may be faster if individual groups still fit into work_mem.+set work_mem to '2MB';+explain (costs off)+select * from (select * from tenk1 order by four) t order by four, ten;+reset work_mem;++-- TODO if an analyze happens here the plans might change; should we+-- solve by inserting extra rows or by adding a GUC that would somehow+-- forcing the time of plan we expect.+create table t(a integer, b integer);++-- A single large group tested around each mode transition point.+insert into t(a, b) select 1, i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+select * from (select * from t order by a) s order by a, b limit 31;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+select * from (select * from t order by a) s order by a, b limit 32;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+select * from (select * from t order by a) s order by a, b limit 33;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+select * from (select * from t order by a) s order by a, b limit 65;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+select * from (select * from t order by a) s order by a, b limit 66;+delete from t;++-- An initial large group followed by a small group.+insert into t(a, b) select (case when i < 50 then 1 else 2 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 55;+select * from (select * from t order by a) s order by a, b limit 55;+-- Test EXPLAIN ANALYZE (text output) with only a fullsort group.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 55;+explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 55;+delete from t;++-- An initial small group followed by a large group.+insert into t(a, b) select (case when i < 5 then i else 9 end), i from generate_series(1, 100) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 70;+select * from (select * from t order by a) s order by a, b limit 70;+-- Test EXPLAIN ANALYZE (text output) with both fullsort and presorted groups.+explain (analyze, costs off, summary off, timing off)+select * from (select * from t order by a) s order by a, b limit 70;+explain (analyze, costs off, summary off, timing off, format json)+select * from (select * from t order by a) s order by a, b limit 70;+delete from t;++-- Small groups of 10 tuples each tested around each mode transition point.+insert into t(a, b) select i / 10, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+select * from (select * from t order by a) s order by a, b limit 31;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+select * from (select * from t order by a) s order by a, b limit 32;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+select * from (select * from t order by a) s order by a, b limit 33;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+select * from (select * from t order by a) s order by a, b limit 65;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+select * from (select * from t order by a) s order by a, b limit 66;+delete from t;++-- Small groups of only 1 tuple each tested around each mode transition point.+insert into t(a, b) select i, i from generate_series(1, 70) n(i);+explain (costs off) select * from (select * from t order by a) s order by a, b limit 31;+select * from (select * from t order by a) s order by a, b limit 31;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 32;+select * from (select * from t order by a) s order by a, b limit 32;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 33;+select * from (select * from t order by a) s order by a, b limit 33;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 65;+select * from (select * from t order by a) s order by a, b limit 65;+explain (costs off) select * from (select * from t order by a) s order by a, b limit 66;+select * from (select * from t order by a) s order by a, b limit 66;+delete from t;++drop table t;diff --git a/src/test/regress/sql/partition_aggregate.sql b/src/test/regress/sql/partition_aggregate.sqlindex dcd6edbad2..6a8db29a07 100644--- a/src/test/regress/sql/partition_aggregate.sql+++ b/src/test/regress/sql/partition_aggregate.sql@@ -9,6 +9,8 @@ SET enable_partitionwise_aggregate TO true;
SET enable_partitionwise_join TO true;
-- Disable parallel plans.
SET max_parallel_workers_per_gather TO 0;
+-- Disable incremental sort, which can influence selected plans due to fuzz factor.+SET enable_incrementalsort TO off;
--
-- Tests for list partitioned tables.
--
2.21.1
[text/plain] v39-0004-fix.patch (6.9K, ../20200321005640.vrwcpyis6ahmspwt@development/5-v39-0004-fix.patch)
download | inline diff:
From d0ee7837d2e8137390f6e64188ecbd4a1addb11d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 16:55:28 +0100
Subject: [PATCH 4/8] fix
---
src/backend/commands/explain.c | 6 ++--
src/backend/executor/execProcnode.c | 11 ++++----
src/backend/optimizer/plan/planner.c | 41 +++++++++++++++++-----------
src/backend/utils/sort/tuplesort.c | 10 +++++--
src/include/nodes/plannodes.h | 1 -
5 files changed, 42 insertions(+), 27 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.cindex dd4600a214..0256dd42f1 100644--- a/src/backend/commands/explain.c+++ b/src/backend/commands/explain.c@@ -2767,7 +2767,7 @@ show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
foreach(methodCell, groupInfo->sortMethods)
{
- const *sortMethodName = tuplesort_method_name(methodCell->int_value);+ const char *sortMethodName = tuplesort_method_name(methodCell->int_value);
methodNames = lappend(methodNames, sortMethodName);
}
@@ -2776,7 +2776,7 @@ show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
if (groupInfo->maxMemorySpaceUsed > 0)
{
long avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;
- const *spaceTypeName;+ const char *spaceTypeName;
ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);
ExplainPropertyInteger("Maximum Sort Space Used", "kB",
@@ -2787,7 +2787,7 @@ show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
if (groupInfo->maxDiskSpaceUsed > 0)
{
long avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;
- const *spaceTypeName;+ const char *spaceTypeName;
ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);
ExplainPropertyInteger("Maximum Sort Space Used", "kB",
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.cindex d15a86a706..5662e7d742 100644--- a/src/backend/executor/execProcnode.c+++ b/src/backend/executor/execProcnode.c@@ -852,12 +852,13 @@ ExecSetTupleBound(int64 tuples_needed, PlanState *child_node)
else if (IsA(child_node, IncrementalSortState))
{
/*
- * If it is a Sort node, notify it that it can use bounded sort.+ * If it is an IncrementalSort node, notify it that it can use bounded+ * sort.
*
- * Note: it is the responsibility of nodeSort.c to react properly to- * changes of these parameters. If we ever redesign this, it'd be a- * good idea to integrate this signaling with the parameter-change- * mechanism.+ * Note: it is the responsibility of nodeIncrementalSort.c to react+ * properly to changes of these parameters. If we ever redesign this,+ * it'd be a good idea to integrate this signaling with the+ * parameter-change mechanism.
*/
IncrementalSortState *sortState = (IncrementalSortState *) child_node;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex b2f4aaadb5..0e01cf8cb1 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -4869,7 +4869,7 @@ create_distinct_paths(PlannerInfo *root,
else
{
Size hashentrysize = hash_agg_entry_size(
- 0, cheapest_input_path->pathtarget->width, 0);+ 0, cheapest_input_path->pathtarget->width, 0);
/* Allow hashing only if hashtable is predicted to fit in work_mem */
allow_hash = (hashentrysize * numDistinctRows <= work_mem * 1024L);
@@ -4932,6 +4932,9 @@ create_distinct_paths(PlannerInfo *root,
* target: the output tlist the result Paths must emit
* limit_tuples: estimated bound on the number of output tuples,
* or -1 if no LIMIT or couldn't estimate
+ *+ * XXX This only looks at sort_pathkeys. I wonder if it needs to look at the+ * other pathkeys (grouping, ...) like generate_useful_gather_paths.
*/
static RelOptInfo *
create_ordered_paths(PlannerInfo *root,
@@ -5002,23 +5005,29 @@ create_ordered_paths(PlannerInfo *root,
add_path(ordered_rel, sorted_path);
}
- if (enable_incrementalsort && presorted_keys > 0)- {- /* Also consider incremental sort. */- sorted_path = (Path *) create_incremental_sort_path(root,- ordered_rel,- input_path,- root->sort_pathkeys,- presorted_keys,- limit_tuples);- /* Add projection step if needed */- if (sorted_path->pathtarget != target)- sorted_path = apply_projection_to_path(root, ordered_rel,- sorted_path, target);+ /* With incremental sort disabled, don't build those paths. */+ if (!enable_incrementalsort)+ continue;- add_path(ordered_rel, sorted_path);- }+ /* Likewise, if the path can't be used for incremental sort. */+ if (!presorted_keys)+ continue;++ /* Also consider incremental sort. */+ sorted_path = (Path *) create_incremental_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ presorted_keys,+ limit_tuples);++ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);
}
}
diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.cindex 583551d197..77c15ebd78 100644--- a/src/backend/utils/sort/tuplesort.c+++ b/src/backend/utils/sort/tuplesort.c@@ -256,8 +256,8 @@ struct Tuplesortstate
bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk
* space, false when it's value for in-memory
* space */
- TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */- MemoryContext maincontext; /* memory context for tuple sort metadata that+ TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */+ MemoryContext maincontext; /* memory context for tuple sort metadata that
* persists across multiple batches */
MemoryContext sortcontext; /* memory context holding most sort data */
MemoryContext tuplecontext; /* sub-context of sortcontext for tuple data */
@@ -803,6 +803,9 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate,
return state;
}
+/*+ * XXX Missing comment.+ */
static void
tuplesort_begin_batch(Tuplesortstate *state)
{
@@ -1288,6 +1291,9 @@ tuplesort_set_bound(Tuplesortstate *state, int64 bound)
state->sortKeys->abbrev_full_comparator = NULL;
}
+/*+ * XXX Missing comment.+ */
bool
tuplesort_used_bound(Tuplesortstate *state)
{
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.hindex fe4046b64b..136d794219 100644--- a/src/include/nodes/plannodes.h+++ b/src/include/nodes/plannodes.h@@ -774,7 +774,6 @@ typedef struct Sort
bool *nullsFirst; /* NULLS FIRST/LAST directions */
} Sort;
-
/* ----------------
* incremental sort node
* ----------------
--
2.21.1
[text/plain] v39-0005-Consider-incremental-sort-paths-in-additional-places.patch (13.0K, ../20200321005640.vrwcpyis6ahmspwt@development/6-v39-0005-Consider-incremental-sort-paths-in-additional-places.patch)
download | inline diff:
From 1baba95ea4d800de7d9ffcd24f04cec279cde518 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 28 Jul 2019 15:59:05 +0200
Subject: [PATCH 5/8] Consider incremental sort paths in additional places
---
src/backend/optimizer/path/allpaths.c | 221 +++++++++++++++++++++++++-
src/backend/optimizer/plan/planner.c | 130 ++++++++++++++-
src/include/optimizer/paths.h | 2 +
3 files changed, 350 insertions(+), 3 deletions(-)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.cindex ccf46dd0aa..9a92948fe3 100644--- a/src/backend/optimizer/path/allpaths.c+++ b/src/backend/optimizer/path/allpaths.c@@ -556,7 +556,7 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
*/
if (rel->reloptkind == RELOPT_BASEREL &&
bms_membership(root->all_baserels) != BMS_SINGLETON)
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/* Now find the cheapest of the paths for this rel */
set_cheapest(rel);
@@ -2727,6 +2727,223 @@ generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows)
}
}
+/*+ * Find an equivalence class member expression, all of whose Vars, come from+ * the indicated relation.+ */+static 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;+}++/*+ * get_useful_pathkeys_for_relation+ * Determine which orderings of a relation might be useful.+ *+ * Getting data in sorted order can be useful either because the requested+ * order matches the final output ordering for the overall query we're+ * planning, or because it enables an efficient merge join. Here, we try+ * to figure out which pathkeys to consider.+ */+static List *+get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)+{+ List *useful_pathkeys_list = NIL;+ ListCell *lc;++ /*+ * Pushing the query_pathkeys to the remote server is always worth+ * considering, because it might let us avoid a local sort.+ */+ if (root->query_pathkeys)+ {+ bool query_pathkeys_ok = true;++ foreach(lc, root->query_pathkeys)+ {+ PathKey *pathkey = (PathKey *) lfirst(lc);+ EquivalenceClass *pathkey_ec = pathkey->pk_eclass;+ Expr *em_expr;++ /*+ * The planner and executor don't have any clever strategy for+ * taking data sorted by a prefix of the query's pathkeys and+ * getting it to be sorted by all of those pathkeys. We'll just+ * end up re-sorting the entire data set. So, unless we can push+ * down all of the query pathkeys, forget it.+ *+ * is_foreign_expr would detect volatile expressions as well, but+ * checking ec_has_volatile here saves some cycles.+ */+ if (pathkey_ec->ec_has_volatile ||+ !(em_expr = find_em_expr_for_rel(pathkey_ec, rel)))+ {+ query_pathkeys_ok = false;+ break;+ }+ }++ /*+ * This ends up allowing us to do incremental sort on top of an index+ * scan all parallelized under a gather merge node.+ */+ if (query_pathkeys_ok)+ useful_pathkeys_list = list_make1(list_copy(root->query_pathkeys));+ }++ return useful_pathkeys_list;+}++/*+ * generate_useful_gather_paths+ * Generate parallel access paths for a relation by pushing a Gather or+ * Gather Merge on top of a partial path.+ *+ * Unlike generate_gather_paths, this does not look only at pathkeys of the+ * input paths (aiming to preserve the ordering). It also considers ordering+ * that might be useful by nodes above the gather merge node, and tries to+ * add a sort (regular or incremental) to provide that.+ */+void+generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows)+{+ ListCell *lc;+ double rows;+ double *rowsp = NULL;+ List *useful_pathkeys_list = NIL;+ Path *cheapest_partial_path = NULL;++ /* If there are no partial paths, there's nothing to do here. */+ if (rel->partial_pathlist == NIL)+ return;++ /* Should we override the rel's rowcount estimate? */+ if (override_rows)+ rowsp = &rows;++ /* generate the regular gather merge paths */+ generate_gather_paths(root, rel, override_rows);++ /* when incremental sort is disabled, we're done */+ if (!enable_incrementalsort)+ return;++ /* consider incremental sort for interesting orderings */+ useful_pathkeys_list = get_useful_pathkeys_for_relation(root, rel);++ /* used for explicit sort paths */+ cheapest_partial_path = linitial(rel->partial_pathlist);++ /*+ * Consider incremental sort paths for each interesting ordering.+ *+ * XXX I wonder if we need to consider adding a projection here, as+ * create_ordered_paths does.+ */+ foreach(lc, useful_pathkeys_list)+ {+ List *useful_pathkeys = lfirst(lc);+ ListCell *lc2;+ bool is_sorted;+ int presorted_keys;++ foreach(lc2, rel->partial_pathlist)+ {+ Path *subpath = (Path *) lfirst(lc2);+ GatherMergePath *path;++ /* path has no ordering at all, can't use incremental sort */+ if (subpath->pathkeys == NIL)+ continue;++ is_sorted = pathkeys_common_contained_in(useful_pathkeys,+ subpath->pathkeys,+ &presorted_keys);++ if (is_sorted)+ {+ path = create_gather_merge_path(root, rel, subpath, rel->reltarget,+ subpath->pathkeys, NULL, rowsp);++ add_path(rel, &path->path);+ continue;+ }++ Assert(!is_sorted);++ /*+ * consider regular sort for cheapest partial path (for each+ * useful pathkeys)+ */+ if (cheapest_partial_path == subpath)+ {+ Path *tmp;++ tmp = (Path *) create_sort_path(root,+ rel,+ subpath,+ useful_pathkeys,+ -1.0);++ rows = tmp->rows * tmp->parallel_workers;++ path = create_gather_merge_path(root, rel,+ tmp,+ rel->reltarget,+ tmp->pathkeys,+ NULL,+ rowsp);++ add_path(rel, &path->path);++ /* Fall through */+ }++ /* Also consider incremental sort */+ if (presorted_keys > 0)+ {+ Path *tmp;++ tmp = (Path *) create_incremental_sort_path(root,+ rel,+ subpath,+ useful_pathkeys,+ presorted_keys,+ -1);++ path = create_gather_merge_path(root, rel,+ tmp,+ rel->reltarget,+ tmp->pathkeys,+ NULL,+ rowsp);++ add_path(rel, &path->path);+ }+ }+ }+}+
/*
* make_rel_from_joinlist
* Build access paths using a "joinlist" to guide the join path search.
@@ -2899,7 +3116,7 @@ standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels)
* once we know the final targetlist (see grouping_planner).
*/
if (lev < levels_needed)
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/* Find and save the cheapest paths for this rel */
set_cheapest(rel);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex 0e01cf8cb1..46dc355af3 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -6434,7 +6434,9 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
foreach(lc, input_rel->pathlist)
{
Path *path = (Path *) lfirst(lc);
+ Path *path_original = path;
bool is_sorted;
+ int presorted_keys;
is_sorted = pathkeys_contained_in(root->group_pathkeys,
path->pathkeys);
@@ -6493,6 +6495,80 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
Assert(false);
}
}
++ /*+ * Now we may consider incremental sort on this path, but only+ * when the path is not already sorted and when incremental sort+ * is enabled.+ */+ if (is_sorted || !enable_incrementalsort)+ continue;++ /* Restore the input path (we might have added Sort on top). */+ path = path_original;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* We've already skipped fully sorted paths above. */+ Assert(!is_sorted);++ /* no shared prefix, not point in building incremental sort */+ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ /* Now decide what to stick atop it */+ if (parse->groupingSets)+ {+ consider_groupingsets_paths(root, grouped_rel,+ path, true, can_hash,+ gd, agg_costs, dNumGroups);+ }+ else if (parse->hasAggs)+ {+ /*+ * We have aggregation, possibly with plain GROUP BY. Make+ * an AggPath.+ */+ add_path(grouped_rel, (Path *)+ create_agg_path(root,+ grouped_rel,+ path,+ grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_SIMPLE,+ parse->groupClause,+ havingQual,+ agg_costs,+ dNumGroups));+ }+ else if (parse->groupClause)+ {+ /*+ * We have GROUP BY without aggregation or grouping sets.+ * Make a GroupPath.+ */+ add_path(grouped_rel, (Path *)+ create_group_path(root,+ grouped_rel,+ path,+ parse->groupClause,+ havingQual,+ dNumGroups));+ }+ else+ {+ /* Other cases should have been handled above */+ Assert(false);+ }
}
/*
@@ -6819,7 +6895,9 @@ create_partial_grouping_paths(PlannerInfo *root,
foreach(lc, input_rel->partial_pathlist)
{
Path *path = (Path *) lfirst(lc);
+ Path *path_original = path;
bool is_sorted;
+ int presorted_keys;
is_sorted = pathkeys_contained_in(root->group_pathkeys,
path->pathkeys);
@@ -6854,6 +6932,56 @@ create_partial_grouping_paths(PlannerInfo *root,
NIL,
dNumPartialPartialGroups));
}
++ /*+ * Now we may consider incremental sort on this path, but only+ * when the path is not already sorted and when incremental sort+ * is enabled.+ */+ if (is_sorted || !enable_incrementalsort)+ continue;++ /* Restore the input path (we might have added Sort on top). */+ path = path_original;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* We've already skipped fully sorted paths above. */+ Assert(!is_sorted);++ /* no shared prefix, not point in building incremental sort */+ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ partially_grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ if (parse->hasAggs)+ add_partial_path(partially_grouped_rel, (Path *)+ create_agg_path(root,+ partially_grouped_rel,+ path,+ partially_grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_INITIAL_SERIAL,+ parse->groupClause,+ NIL,+ agg_partial_costs,+ dNumPartialPartialGroups));+ else+ add_partial_path(partially_grouped_rel, (Path *)+ create_group_path(root,+ partially_grouped_rel,+ path,+ parse->groupClause,+ NIL,+ dNumPartialPartialGroups));
}
}
@@ -7235,7 +7363,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
* one of the generated paths may turn out to be the cheapest one.
*/
if (rel->consider_parallel && !IS_OTHER_REL(rel))
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/*
* Reassess which paths are the cheapest, now that we've potentially added
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.hindex 85f5fe37ea..f6994779de 100644--- a/src/include/optimizer/paths.h+++ b/src/include/optimizer/paths.h@@ -54,6 +54,8 @@ extern RelOptInfo *standard_join_search(PlannerInfo *root, int levels_needed,
extern void generate_gather_paths(PlannerInfo *root, RelOptInfo *rel,
bool override_rows);
+extern void generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel,+ bool override_rows);
extern int compute_parallel_worker(RelOptInfo *rel, double heap_pages,
double index_pages, int max_workers);
extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
--
2.21.1
[text/plain] v39-0006-fix.patch (6.0K, ../20200321005640.vrwcpyis6ahmspwt@development/7-v39-0006-fix.patch)
download | inline diff:
From 5752bf071168a1dfd16fa3ae045182ccd3e5ee81 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 18:26:16 +0100
Subject: [PATCH 6/8] fix
---
src/backend/optimizer/path/allpaths.c | 62 +++++++++++++++++----------
1 file changed, 39 insertions(+), 23 deletions(-)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.cindex 9a92948fe3..6838a238cd 100644--- a/src/backend/optimizer/path/allpaths.c+++ b/src/backend/optimizer/path/allpaths.c@@ -2764,6 +2764,14 @@ find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
* order matches the final output ordering for the overall query we're
* planning, or because it enables an efficient merge join. Here, we try
* to figure out which pathkeys to consider.
+ *+ * This allows us to do incremental sort on top of an index scan under a gather+ * merge node, i.e. parallelized.+ *+ * XXX At the moment this can only ever return a list with a single element,+ * because it looks at query_pathkeys only. So we might return the pathkeys+ * directly, but it seems plausible we'll want to consider other orderings+ * in the future.
*/
static List *
get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
@@ -2772,8 +2780,8 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
ListCell *lc;
/*
- * Pushing the query_pathkeys to the remote server is always worth- * considering, because it might let us avoid a local sort.+ * Considering query_pathkeys is always worth it, because it might let us+ * avoid a local sort.
*/
if (root->query_pathkeys)
{
@@ -2786,14 +2794,9 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
Expr *em_expr;
/*
- * The planner and executor don't have any clever strategy for- * taking data sorted by a prefix of the query's pathkeys and- * getting it to be sorted by all of those pathkeys. We'll just- * end up re-sorting the entire data set. So, unless we can push- * down all of the query pathkeys, forget it.- *- * is_foreign_expr would detect volatile expressions as well, but- * checking ec_has_volatile here saves some cycles.+ * We can't use incremental sort for pathkeys containing volatile+ * expressions. We could walk the exppression itself, but checking+ * ec_has_volatile here saves some cycles.
*/
if (pathkey_ec->ec_has_volatile ||
!(em_expr = find_em_expr_for_rel(pathkey_ec, rel)))
@@ -2803,10 +2806,6 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
}
}
- /*- * This ends up allowing us to do incremental sort on top of an index- * scan all parallelized under a gather merge node.- */
if (query_pathkeys_ok)
useful_pathkeys_list = list_make1(list_copy(root->query_pathkeys));
}
@@ -2819,10 +2818,10 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
* Generate parallel access paths for a relation by pushing a Gather or
* Gather Merge on top of a partial path.
*
- * Unlike generate_gather_paths, this does not look only at pathkeys of the- * input paths (aiming to preserve the ordering). It also considers ordering- * that might be useful by nodes above the gather merge node, and tries to- * add a sort (regular or incremental) to provide that.+ * Unlike plain generate_gather_paths, this looks both at pathkeys of input+ * paths (aiming to preserve the ordering), but also considers ordering that+ * might be useful for nodes above the gather merge node, and tries to add+ * a sort (regular or incremental) to provide that.
*/
void
generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows)
@@ -2841,7 +2840,7 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
if (override_rows)
rowsp = &rows;
- /* generate the regular gather merge paths */+ /* generate the regular gather (merge) paths */
generate_gather_paths(root, rel, override_rows);
/* when incremental sort is disabled, we're done */
@@ -2851,7 +2850,7 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
/* consider incremental sort for interesting orderings */
useful_pathkeys_list = get_useful_pathkeys_for_relation(root, rel);
- /* used for explicit sort paths */+ /* used for explicit (full) sort paths */
cheapest_partial_path = linitial(rel->partial_pathlist);
/*
@@ -2880,6 +2879,14 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
subpath->pathkeys,
&presorted_keys);
+ /*+ * When the partial path is already sorted, we can just add a gather+ * merge on top, and we're done - no point in adding explicit sort.+ *+ * XXX Can't we skip this (maybe only for the cheapest partial path)+ * when the path is already sorted? Then it's likely duplicate with+ * the path created by generate_gather_paths.+ */
if (is_sorted)
{
path = create_gather_merge_path(root, rel, subpath, rel->reltarget,
@@ -2892,8 +2899,14 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
Assert(!is_sorted);
/*
- * consider regular sort for cheapest partial path (for each- * useful pathkeys)+ * Consider regular sort for the cheapest partial path (for each+ * useful pathkeys). We know the path is not sorted, because we'd+ * not get here otherwise.+ *+ * XXX This is not redundant with the gather merge path created in+ * generate_gather_paths, because that merely preserves ordering of+ * the cheapest partial path, while here we add an explicit sort to+ * get match the useful ordering.
*/
if (cheapest_partial_path == subpath)
{
@@ -2919,7 +2932,10 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r
/* Fall through */
}
- /* Also consider incremental sort */+ /*+ * Consider incremental sort, but only when the subpath is already+ * partially sorted on a pathkey prefix.+ */
if (presorted_keys > 0)
{
Path *tmp;
--
2.21.1
[text/plain] v39-0007-A-couple-more-places-for-incremental-sort.patch (9.3K, ../20200321005640.vrwcpyis6ahmspwt@development/8-v39-0007-A-couple-more-places-for-incremental-sort.patch)
download | inline diff:
From 2e6cbe5bc169ebe1e9971b5256003d02349ad1ce Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Sun, 28 Jul 2019 16:03:56 +0200
Subject: [PATCH 7/8] A couple more places for incremental sort
---
src/backend/optimizer/geqo/geqo_eval.c | 2 +-
src/backend/optimizer/plan/planner.c | 220 ++++++++++++++++++++++++-
2 files changed, 217 insertions(+), 5 deletions(-)
diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.cindex 6d897936d7..ff33acc7b6 100644--- a/src/backend/optimizer/geqo/geqo_eval.c+++ b/src/backend/optimizer/geqo/geqo_eval.c@@ -274,7 +274,7 @@ merge_clump(PlannerInfo *root, List *clumps, Clump *new_clump, int num_gene,
* grouping_planner).
*/
if (old_clump->size + new_clump->size < num_gene)
- generate_gather_paths(root, joinrel, false);+ generate_useful_gather_paths(root, joinrel, false);
/* Find and save the cheapest paths for this joinrel */
set_cheapest(joinrel);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex 46dc355af3..2880fcabe8 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -5080,6 +5080,67 @@ create_ordered_paths(PlannerInfo *root,
add_path(ordered_rel, path);
}
++ /*+ * Consider incremental sort with a gather merge on partial paths.+ *+ * XXX This is probably duplicate with the paths we already generate+ * in generate_useful_gather_paths in apply_scanjoin_target_to_paths.+ */+ if (enable_incrementalsort)+ {+ ListCell *lc;++ foreach(lc, input_rel->partial_pathlist)+ {+ Path *input_path = (Path *) lfirst(lc);+ Path *sorted_path = input_path;+ bool is_sorted;+ int presorted_keys;+ double total_groups;++ /*+ * We don't care if this is the cheapest partial path - we+ * can't simply skip it, because it may be partially sorted in+ * which case we want to consider incremental sort on top of+ * it (instead of full sort, which is what happens above).+ */++ is_sorted = pathkeys_common_contained_in(root->sort_pathkeys,+ input_path->pathkeys,+ &presorted_keys);++ /* Ignore already sorted paths */+ if (is_sorted)+ continue;++ if (presorted_keys == 0)+ continue;++ /* Since we have presorted keys, consider incremental sort. */+ sorted_path = (Path *) create_incremental_sort_path(root,+ ordered_rel,+ input_path,+ root->sort_pathkeys,+ presorted_keys,+ limit_tuples);+ total_groups = input_path->rows *+ input_path->parallel_workers;+ sorted_path = (Path *)+ create_gather_merge_path(root, ordered_rel,+ sorted_path,+ sorted_path->pathtarget,+ root->sort_pathkeys, NULL,+ &total_groups);++ /* Add projection step if needed */+ if (sorted_path->pathtarget != target)+ sorted_path = apply_projection_to_path(root, ordered_rel,+ sorted_path, target);++ add_path(ordered_rel, sorted_path);+ }+ }
}
/*
@@ -6580,12 +6641,18 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
foreach(lc, partially_grouped_rel->pathlist)
{
Path *path = (Path *) lfirst(lc);
+ Path *path_original = path;+ bool is_sorted;+ int presorted_keys;++ is_sorted = pathkeys_contained_in(root->group_pathkeys,+ path->pathkeys);
/*
* Insert a Sort node, if required. But there's no point in
* sorting anything but the cheapest path.
*/
- if (!pathkeys_contained_in(root->group_pathkeys, path->pathkeys))+ if (!is_sorted)
{
if (path != partially_grouped_rel->cheapest_total_path)
continue;
@@ -6616,6 +6683,56 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
parse->groupClause,
havingQual,
dNumGroups));
++ /*+ * Now we may consider incremental sort on this path, but only+ * when the path is not already sorted and when incremental+ * sort is enabled.+ */+ if (is_sorted || !enable_incrementalsort)+ continue;++ /* Restore the input path (we might have added Sort on top). */+ path = path_original;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* We've already skipped fully sorted paths above. */+ Assert(!is_sorted);++ /* no shared prefix, not point in building incremental sort */+ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ if (parse->hasAggs)+ add_path(grouped_rel, (Path *)+ create_agg_path(root,+ grouped_rel,+ path,+ grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_FINAL_DESERIAL,+ parse->groupClause,+ havingQual,+ agg_final_costs,+ dNumGroups));+ else+ add_path(grouped_rel, (Path *)+ create_group_path(root,+ grouped_rel,+ path,+ parse->groupClause,+ havingQual,+ dNumGroups));
}
}
}
@@ -6887,6 +7004,60 @@ create_partial_grouping_paths(PlannerInfo *root,
dNumPartialGroups));
}
}
++ /*+ * Also consider incremental sort on all partially sorted paths.+ */+ if (enable_incrementalsort)+ {+ foreach(lc, input_rel->pathlist)+ {+ Path *path = (Path *) lfirst(lc);+ bool is_sorted;+ int presorted_keys;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ /* Ignore already sorted paths */+ if (is_sorted)+ continue;++ if (presorted_keys == 0)+ continue;++ /* Since we have presorted keys, consider incremental sort. */+ path = (Path *) create_incremental_sort_path(root,+ partially_grouped_rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ if (parse->hasAggs)+ add_path(partially_grouped_rel, (Path *)+ create_agg_path(root,+ partially_grouped_rel,+ path,+ partially_grouped_rel->reltarget,+ parse->groupClause ? AGG_SORTED : AGG_PLAIN,+ AGGSPLIT_INITIAL_SERIAL,+ parse->groupClause,+ NIL,+ agg_partial_costs,+ dNumPartialGroups));+ else+ add_path(partially_grouped_rel, (Path *)+ create_group_path(root,+ partially_grouped_rel,+ path,+ parse->groupClause,+ NIL,+ dNumPartialGroups));+ }+ }+
}
if (can_sort && cheapest_partial_path != NULL)
@@ -6951,10 +7122,10 @@ create_partial_grouping_paths(PlannerInfo *root,
/* We've already skipped fully sorted paths above. */
Assert(!is_sorted);
- /* no shared prefix, not point in building incremental sort */
if (presorted_keys == 0)
continue;
+ /* Since we have presorted keys, consider incremental sort. */
path = (Path *) create_incremental_sort_path(root,
partially_grouped_rel,
path,
@@ -7079,10 +7250,11 @@ create_partial_grouping_paths(PlannerInfo *root,
static void
gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel)
{
+ ListCell *lc;
Path *cheapest_partial_path;
/* Try Gather for unordered paths and Gather Merge for ordered ones. */
- generate_gather_paths(root, rel, true);+ generate_useful_gather_paths(root, rel, true);
/* Try cheapest partial path + explicit Sort + Gather Merge. */
cheapest_partial_path = linitial(rel->partial_pathlist);
@@ -7108,6 +7280,46 @@ gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel)
add_path(rel, path);
}
++ if (!enable_incrementalsort)+ return;++ /* also consider incremental sort on partial paths, if enabled */+ foreach(lc, rel->partial_pathlist)+ {+ Path *path = (Path *) lfirst(lc);+ bool is_sorted;+ int presorted_keys;+ double total_groups;++ is_sorted = pathkeys_common_contained_in(root->group_pathkeys,+ path->pathkeys,+ &presorted_keys);++ if (is_sorted)+ continue;++ if (presorted_keys == 0)+ continue;++ path = (Path *) create_incremental_sort_path(root,+ rel,+ path,+ root->group_pathkeys,+ presorted_keys,+ -1.0);++ path = (Path *)+ create_gather_merge_path(root,+ rel,+ path,+ rel->reltarget,+ root->group_pathkeys,+ NULL,+ &total_groups);++ add_path(rel, path);+ }
}
/*
@@ -7209,7 +7421,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
* paths by doing it after the final scan/join target has been
* applied.
*/
- generate_gather_paths(root, rel, false);+ generate_useful_gather_paths(root, rel, false);
/* Can't use parallel query above this level. */
rel->partial_pathlist = NIL;
--
2.21.1
[text/plain] v39-0008-fix.patch (2.1K, ../20200321005640.vrwcpyis6ahmspwt@development/9-v39-0008-fix.patch)
download | inline diff:
From 59e9a858763a274405b5015344526092c6e55af5 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Thu, 19 Mar 2020 18:48:19 +0100
Subject: [PATCH 8/8] fix
---
src/backend/optimizer/plan/planner.c | 16 +++++++---------
1 file changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.cindex 2880fcabe8..b4763e79f8 100644--- a/src/backend/optimizer/plan/planner.c+++ b/src/backend/optimizer/plan/planner.c@@ -5100,17 +5100,17 @@ create_ordered_paths(PlannerInfo *root,
double total_groups;
/*
- * We don't care if this is the cheapest partial path - we- * can't simply skip it, because it may be partially sorted in- * which case we want to consider incremental sort on top of- * it (instead of full sort, which is what happens above).+ * We don't care if this is the cheapest partial path - we can't+ * simply skip it, because it may be partially sorted in which+ * case we want to consider adding incremental sort (instead of+ * full sort, which is what happens above).
*/
is_sorted = pathkeys_common_contained_in(root->sort_pathkeys,
input_path->pathkeys,
&presorted_keys);
- /* Ignore already sorted paths */+ /* No point in adding incremental sort on fully sorted paths. */
if (is_sorted)
continue;
@@ -7005,9 +7005,7 @@ create_partial_grouping_paths(PlannerInfo *root,
}
}
- /*- * Also consider incremental sort on all partially sorted paths.- */+ /* Consider incremental sort on all partial paths, if enabled. */
if (enable_incrementalsort)
{
foreach(lc, input_rel->pathlist)
@@ -7122,10 +7120,10 @@ create_partial_grouping_paths(PlannerInfo *root,
/* We've already skipped fully sorted paths above. */
Assert(!is_sorted);
+ /* no shared prefix, not point in building incremental sort */
if (presorted_keys == 0)
continue;
- /* Since we have presorted keys, consider incremental sort. */
path = (Path *) create_incremental_sort_path(root,
partially_grouped_rel,
path,
--
2.21.1
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: tomas.vondra@2ndquadrant.com, jtc331@gmail.com, michael@paquier.xyz, rafia.pghackers@gmail.com, pg@bowt.ie, simon@2ndquadrant.com, shaun.thomas@2ndquadrant.com, 9erthalion6@gmail.com, a.korotkov@postgrespro.ru, tgl@sss.pgh.pa.us, pgsql-hackers@lists.postgresql.org, alvherre@2ndquadrant.com
Subject: Re: [PATCH] Incremental sort (was: PoC: Partial sort)
In-Reply-To: <20200321005640.vrwcpyis6ahmspwt@development>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox