Skip to content

Commit 096a063

Browse files
danolivoCommitfest Bot
authored and
Commitfest Bot
committed
Improve group number estimation.
Estimating GROUP BY x optimisation employs a distinct statistic for column x. But if we have an expression like 'x=y' somewhere down the query tree, the number of different values can't be more than the smaller distinct value on columns 'x' and 'y'. That means it is possible to correct the estimation with knowledge provided by the equivalence class. In this commit, the estimate_num_groups routine is changed to include PathKey nodes in the presortedExprs list. With the PathKey node, we can pass through its equivalence class members and correct the distinct estimation. To avoid multiple calls on statistic tuples, the em_ndistinct cache field is introduced.
1 parent f056f75 commit 096a063

File tree

6 files changed

+180
-32
lines changed

6 files changed

+180
-32
lines changed

src/backend/optimizer/path/costsize.c

+9-31
Original file line numberDiff line numberDiff line change
@@ -2008,13 +2008,12 @@ cost_incremental_sort(Path *path,
20082008
run_cost,
20092009
input_run_cost = input_total_cost - input_startup_cost;
20102010
double group_tuples,
2011-
input_groups;
2011+
input_groups,
2012+
estimated_groups;
20122013
Cost group_startup_cost,
20132014
group_run_cost,
20142015
group_input_run_cost;
20152016
List *presortedExprs = NIL;
2016-
ListCell *l;
2017-
bool unknown_varno = false;
20182017

20192018
Assert(presorted_keys > 0 && presorted_keys < list_length(pathkeys));
20202019

@@ -2025,9 +2024,6 @@ cost_incremental_sort(Path *path,
20252024
if (input_tuples < 2.0)
20262025
input_tuples = 2.0;
20272026

2028-
/* Default estimate of number of groups, capped to one group per row. */
2029-
input_groups = Min(input_tuples, DEFAULT_NUM_DISTINCT);
2030-
20312027
/*
20322028
* Extract presorted keys as list of expressions.
20332029
*
@@ -2050,33 +2046,15 @@ cost_incremental_sort(Path *path,
20502046
* anyway - from that standpoint the DEFAULT_NUM_DISTINCT is defensive
20512047
* while maintaining lower startup cost.
20522048
*/
2053-
foreach(l, pathkeys)
2054-
{
2055-
PathKey *key = (PathKey *) lfirst(l);
2056-
EquivalenceMember *member = (EquivalenceMember *)
2057-
linitial(key->pk_eclass->ec_members);
2058-
2059-
/*
2060-
* Check if the expression contains Var with "varno 0" so that we
2061-
* don't call estimate_num_groups in that case.
2062-
*/
2063-
if (bms_is_member(0, pull_varnos(root, (Node *) member->em_expr)))
2064-
{
2065-
unknown_varno = true;
2066-
break;
2067-
}
2049+
presortedExprs = list_copy_head(pathkeys, presorted_keys);
20682050

2069-
/* expression not containing any Vars with "varno 0" */
2070-
presortedExprs = lappend(presortedExprs, member->em_expr);
2071-
2072-
if (foreach_current_index(l) + 1 >= presorted_keys)
2073-
break;
2074-
}
2075-
2076-
/* Estimate the number of groups with equal presorted keys. */
2077-
if (!unknown_varno)
2078-
input_groups = estimate_num_groups(root, presortedExprs, input_tuples,
2051+
estimated_groups = estimate_num_groups(root, presortedExprs, input_tuples,
20792052
NULL, NULL);
2053+
if (estimated_groups > 0.0)
2054+
input_groups = estimated_groups;
2055+
else
2056+
/* Default estimate of number of groups, capped to one group per row. */
2057+
input_groups = Min(input_tuples, DEFAULT_NUM_DISTINCT);
20802058

20812059
group_tuples = input_tuples / input_groups;
20822060
group_input_run_cost = input_run_cost / input_groups;

src/backend/optimizer/path/equivclass.c

+1
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ add_eq_member(EquivalenceClass *ec, Expr *expr, Relids relids,
525525
em->em_datatype = datatype;
526526
em->em_jdomain = jdomain;
527527
em->em_parent = parent;
528+
em->em_ndistinct = -1.0;
528529

529530
if (bms_is_empty(relids))
530531
{

src/backend/utils/adt/selfuncs.c

+86-1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ static bool get_actual_variable_endpoint(Relation heapRel,
214214
MemoryContext outercontext,
215215
Datum *endpointDatum);
216216
static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
217+
static EquivalenceMember *identify_sort_ecmember(PlannerInfo *root,
218+
EquivalenceClass *ec);
217219

218220

219221
/*
@@ -3468,12 +3470,34 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
34683470
i = 0;
34693471
foreach(l, groupExprs)
34703472
{
3471-
Node *groupexpr = (Node *) lfirst(l);
3473+
Node *node = (Node *) lfirst(l);
3474+
Node *groupexpr;
34723475
double this_srf_multiplier;
34733476
VariableStatData vardata;
34743477
List *varshere;
34753478
ListCell *l2;
34763479

3480+
/* Find an expression beforehand */
3481+
if (IsA(node, PathKey))
3482+
{
3483+
PathKey *key = (PathKey *) node;
3484+
EquivalenceMember *em = identify_sort_ecmember(root, key->pk_eclass);
3485+
3486+
/*
3487+
* Check if the expression contains Var with "varno 0" so that we
3488+
* don't call estimate_num_groups in that case.
3489+
*/
3490+
if (bms_is_member(0, pull_varnos(root, (Node *) em->em_expr)))
3491+
{
3492+
/* Return 'unknown' value */
3493+
return 0.0;
3494+
}
3495+
3496+
groupexpr = (Node *) em->em_expr;
3497+
}
3498+
else
3499+
groupexpr = node;
3500+
34773501
/* is expression in this grouping set? */
34783502
if (pgset && !list_member_int(*pgset, i++))
34793503
continue;
@@ -8404,3 +8428,64 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
84048428

84058429
*indexPages = index->pages;
84068430
}
8431+
8432+
/*
8433+
* Find suitable member of the equivalence class.
8434+
* Passing through the list of EC members find the member with minimum of
8435+
* distinct values. Cache estimated number of distincts in the em_ndistinct
8436+
* field of each member.
8437+
*/
8438+
static EquivalenceMember *
8439+
identify_sort_ecmember(PlannerInfo *root, EquivalenceClass *ec)
8440+
{
8441+
EquivalenceMember *candidate = linitial(ec->ec_members);
8442+
8443+
if (root == NULL)
8444+
/* Fast path */
8445+
return candidate;
8446+
8447+
foreach_node(EquivalenceMember, em, ec->ec_members)
8448+
{
8449+
VariableStatData vardata;
8450+
8451+
if (em->em_ndistinct == 0.)
8452+
/* Nothing helpful */
8453+
continue;
8454+
8455+
if (em->em_is_child || em->em_is_const || bms_is_empty(em->em_relids) ||
8456+
bms_is_member(0, em->em_relids))
8457+
{
8458+
em->em_ndistinct = 0.;
8459+
continue;
8460+
}
8461+
8462+
if (em->em_ndistinct < 0.)
8463+
{
8464+
bool isdefault = true;
8465+
double ndist = 0.;
8466+
8467+
/* Let's check candidate's ndistinct value */
8468+
examine_variable(root, (Node *) em->em_expr, 0, &vardata);
8469+
if (HeapTupleIsValid(vardata.statsTuple))
8470+
ndist = get_variable_numdistinct(&vardata, &isdefault);
8471+
ReleaseVariableStats(vardata);
8472+
8473+
if (isdefault)
8474+
{
8475+
em->em_ndistinct = 0.;
8476+
continue;
8477+
}
8478+
8479+
em->em_ndistinct = ndist;
8480+
}
8481+
8482+
Assert(em->em_ndistinct > 0.);
8483+
8484+
if (candidate->em_ndistinct == 0. ||
8485+
em->em_ndistinct < candidate->em_ndistinct)
8486+
candidate = em;
8487+
}
8488+
8489+
Assert(candidate != NULL);
8490+
return candidate;
8491+
}

src/include/nodes/pathnodes.h

+2
Original file line numberDiff line numberDiff line change
@@ -1477,6 +1477,8 @@ typedef struct EquivalenceMember
14771477
JoinDomain *em_jdomain; /* join domain containing the source clause */
14781478
/* if em_is_child is true, this links to corresponding EM for top parent */
14791479
struct EquivalenceMember *em_parent pg_node_attr(read_write_ignore);
1480+
1481+
double em_ndistinct; /* cached value of ndistinct: 0- default value or 'unknown'; -1 - not defined yet */
14801482
} EquivalenceMember;
14811483

14821484
/*

src/test/regress/expected/incremental_sort.out

+51
Original file line numberDiff line numberDiff line change
@@ -1722,3 +1722,54 @@ order by t1.four, t1.two limit 1;
17221722
-> Seq Scan on tenk1 t2
17231723
(12 rows)
17241724

1725+
-- Check:
1726+
-- commuting of sides in an expression doesn't influence cost estimation
1727+
CREATE TABLE sort_ndist_t1 (x numeric, y numeric);
1728+
CREATE TABLE sort_ndist_t2 (x numeric, y numeric);
1729+
INSERT INTO sort_ndist_t1 (x,y)
1730+
SELECT gs%10, gs%10 FROM generate_series(1,1E4) AS gs;
1731+
INSERT INTO sort_ndist_t2 (x,y)
1732+
SELECT gs, gs FROM generate_series(1,1E4) AS gs;
1733+
CREATE INDEX t1_idx ON sort_ndist_t1 (x);
1734+
CREATE INDEX t2_idx ON sort_ndist_t2 (x);
1735+
VACUUM ANALYZE sort_ndist_t1, sort_ndist_t2;
1736+
SET enable_hashjoin = 'off';
1737+
-- Having lots of duplicates after the join it is more effective to use plain
1738+
-- Sort instead of incremental sort: with small number of groups we do the same
1739+
-- stuff like Sort but with extra penalty.
1740+
EXPLAIN (COSTS OFF)
1741+
SELECT t1.x, t1.y FROM sort_ndist_t1 t1, sort_ndist_t2 t2
1742+
WHERE t1.x=t2.x
1743+
ORDER BY t1.x,t1.y;
1744+
QUERY PLAN
1745+
--------------------------------------------------------------------
1746+
Sort
1747+
Sort Key: t1.x, t1.y
1748+
-> Nested Loop
1749+
-> Seq Scan on sort_ndist_t1 t1
1750+
-> Memoize
1751+
Cache Key: t1.x
1752+
Cache Mode: logical
1753+
-> Index Only Scan using t2_idx on sort_ndist_t2 t2
1754+
Index Cond: (x = t1.x)
1755+
(9 rows)
1756+
1757+
EXPLAIN (COSTS OFF) -- the plan must be the same as above
1758+
SELECT t1.x, t1.y FROM sort_ndist_t1 t1, sort_ndist_t2 t2
1759+
WHERE t2.x=t1.x
1760+
ORDER BY t1.x,t1.y;
1761+
QUERY PLAN
1762+
--------------------------------------------------------------------
1763+
Sort
1764+
Sort Key: t1.x, t1.y
1765+
-> Nested Loop
1766+
-> Seq Scan on sort_ndist_t1 t1
1767+
-> Memoize
1768+
Cache Key: t1.x
1769+
Cache Mode: logical
1770+
-> Index Only Scan using t2_idx on sort_ndist_t2 t2
1771+
Index Cond: (x = t1.x)
1772+
(9 rows)
1773+
1774+
RESET enable_hashjoin;
1775+
DROP TABLE sort_ndist_t1, sort_ndist_t2;

src/test/regress/sql/incremental_sort.sql

+31
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,34 @@ explain (costs off)
298298
select * from
299299
(select * from tenk1 order by four) t1 join tenk1 t2 on t1.four = t2.four and t1.two = t2.two
300300
order by t1.four, t1.two limit 1;
301+
302+
-- Check:
303+
-- commuting of sides in an expression doesn't influence cost estimation
304+
CREATE TABLE sort_ndist_t1 (x numeric, y numeric);
305+
CREATE TABLE sort_ndist_t2 (x numeric, y numeric);
306+
307+
INSERT INTO sort_ndist_t1 (x,y)
308+
SELECT gs%10, gs%10 FROM generate_series(1,1E4) AS gs;
309+
INSERT INTO sort_ndist_t2 (x,y)
310+
SELECT gs, gs FROM generate_series(1,1E4) AS gs;
311+
CREATE INDEX t1_idx ON sort_ndist_t1 (x);
312+
CREATE INDEX t2_idx ON sort_ndist_t2 (x);
313+
VACUUM ANALYZE sort_ndist_t1, sort_ndist_t2;
314+
315+
SET enable_hashjoin = 'off';
316+
317+
-- Having lots of duplicates after the join it is more effective to use plain
318+
-- Sort instead of incremental sort: with small number of groups we do the same
319+
-- stuff like Sort but with extra penalty.
320+
EXPLAIN (COSTS OFF)
321+
SELECT t1.x, t1.y FROM sort_ndist_t1 t1, sort_ndist_t2 t2
322+
WHERE t1.x=t2.x
323+
ORDER BY t1.x,t1.y;
324+
325+
EXPLAIN (COSTS OFF) -- the plan must be the same as above
326+
SELECT t1.x, t1.y FROM sort_ndist_t1 t1, sort_ndist_t2 t2
327+
WHERE t2.x=t1.x
328+
ORDER BY t1.x,t1.y;
329+
330+
RESET enable_hashjoin;
331+
DROP TABLE sort_ndist_t1, sort_ndist_t2;

0 commit comments

Comments
 (0)