【发布时间】:2021-12-02 20:16:57
【问题描述】:
PostgreSQL如何估计JOIN查询中的行数如:
EXPLAIN
SELECT *
FROM R, S
WHERE (R.StartTime < S.EndTime) AND (S.StartTime < R.EndTime);
【问题讨论】:
标签: postgresql statistics histogram sql-execution-plan
PostgreSQL如何估计JOIN查询中的行数如:
EXPLAIN
SELECT *
FROM R, S
WHERE (R.StartTime < S.EndTime) AND (S.StartTime < R.EndTime);
【问题讨论】:
标签: postgresql statistics histogram sql-execution-plan
手册中有一个章节准确地解决了您的问题:
解释 Laurenz 提供的内容等。
但这还不是全部。我们还需要基础表的行数(cardinalities)。 Postgres 使用在src/backend/utils/adt/plancat.c 中定义的estimate_rel_size():
/*
* estimate_rel_size - estimate # pages and # tuples in a table or index
*
* We also estimate the fraction of the pages that are marked all-visible in
* the visibility map, for use in estimation of index-only scans.
*
* If attr_widths isn't NULL, it points to the zero-index entry of the
* relation's attr_widths[] cache; we fill this in if we have need to compute
* the attribute widths for estimation purposes.
*/
void
estimate_rel_size(Relation rel, int32 *attr_widths,
BlockNumber *pages, double *tuples, double *allvisfrac)
...
这是重现计算的最小 SQL 查询(忽略一些极端情况):
SELECT (reltuples / relpages * (pg_relation_size(oid) / 8192))::bigint
FROM pg_class
WHERE oid = 'mytable'::regclass; -- your table here
更多细节:
CREATE TEMP TABLE r(id serial, start_time timestamptz, end_time timestamptz);
CREATE TEMP TABLE s(id serial, start_time timestamptz, end_time timestamptz);
INSERT INTO r(start_time, end_time)
SELECT now(), now() -- actual values don't matter for this particular case
FROM generate_series (1, 5000);
INSERT INTO s(start_time, end_time)
SELECT now(), now()
FROM generate_series (1, 10000);
VACUUM r, s; -- set reltuples & relpages in pg_class
-- add 2000 rows to S
INSERT INTO s(start_time, end_time)
SELECT now(), now()
FROM generate_series (1, 2000);
pg_class 仍然有 5000 和 10000 个 reltuples,但我们知道 R 和 S 中有 5000 和 12000 行。(因为这些是 临时表,它们没有被 autovacuum 覆盖,所以数字永远不会自动更新。)检查:
SELECT relname, reltuples, relpages -- 5000 | 10000
FROM pg_class c
WHERE c.oid IN ('pg_temp.r'::regclass, 'pg_temp.s'::regclass);
SELECT count(*) FROM r; -- 5000
SELECT count(*) FROM s; -- 12000
查询计划:
EXPLAIN
SELECT *
FROM r, s
WHERE (r.start_time < s.end_time) AND (s.start_time < r.end_time);
'Nested Loop (cost=0.00..1053004.31 rows=6683889 width=40)'
' Join Filter: ((r.start_time < s.end_time) AND (s.start_time < r.end_time))'
' -> Seq Scan on s (cost=0.00..197.31 rows=12031 width=20)'
' -> Materialize (cost=0.00..107.00 rows=5000 width=20)'
' -> Seq Scan on r (cost=0.00..82.00 rows=5000 width=20)'
'JIT:'
' Functions: 6'
' Options: Inlining true, Optimization true, Expressions true, Deforming true'
Postgres 为表 s 估计 rows=12031。一个很好的估计,该算法有效。
由于表的物理大小不会自动缩小,因此删除行更容易放弃估计。在主要的DELETE 之后发送VACUUM ANALYZE 是个好主意。甚至VACUUM FULL ANALYZE。见:
Postgres 期望 rows=6683889,这符合我们的期望(根据 Laurenz 的解释):
SELECT 5000 * 12031 * 0.3333333333333333^2 -- 6683888.89
您的示例查询就是这样:一个示例。但它恰好是一个糟糕的,因为同样可以通过 范围类型 和更有效的运算符来实现。特别是tstzrange 和&&:
&& 的选择性?
SELECT oprjoin -- areajoinsel
FROM pg_operator
WHERE oprname = '&&'
AND oprleft = 'anyrange'::regtype
AND oprright = 'anyrange'::regtype;
`src/backend/utils/adt/geoselfuncs.c中的源码:
Datum
areajoinsel(PG_FUNCTION_ARGS)
{
PG_RETURN_FLOAT8(0.005);
}
更多选择性0.005
EXPLAIN
SELECT *
FROM r, s
WHERE tstzrange(r.start_time, r.end_time) && tstzrange(s.start_time, s.end_time);
恰好是完全等价的,因为tstzrange 默认包括下限和不包括上限。我得到了这个查询计划:
'Nested Loop (cost=0.00..1203391.81 rows=300775 width=40)'
' Join Filter: (tstzrange(r.start_time, r.end_time) && tstzrange(s.start_time, s.end_time))'
' -> Seq Scan on s (cost=0.00..197.31 rows=12031 width=20)'
' -> Materialize (cost=0.00..107.00 rows=5000 width=20)'
' -> Seq Scan on r (cost=0.00..82.00 rows=5000 width=20)'
'JIT:'
' Functions: 6'
' Options: Inlining true, Optimization true, Expressions true, Deforming true'
我们的期望:
SELECT 5000 * 12031 * 0.005 -- 300775.000
这是一个宾果游戏!
并且可以通过索引有效地支持此查询,从而改变游戏规则...
【讨论】:
假设涉及的数据类型是timestamp with time time zone(但这并不重要,正如我们将看到的那样),可以通过以下方式找到连接选择性估计函数:
SELECT oprjoin
FROM pg_operator
WHERE oprname = '<'
AND oprleft = 'timestamptz'::regtype
AND oprright = 'timestamptz'::regtype;
oprjoin
═════════════════
scalarltjoinsel
(1 row)
该函数定义在src/backend/utils/adt/selfuncs.c:
/*
* scalarltjoinsel - Join selectivity of "<" for scalars
*/
Datum
scalarltjoinsel(PG_FUNCTION_ARGS)
{
PG_RETURN_FLOAT8(DEFAULT_INEQ_SEL);
}
这在src/include/utils/selfuncs.h 中定义为
/* default selectivity estimate for inequalities such as "A < b" */
#define DEFAULT_INEQ_SEL 0.3333333333333333
所以,听起来很简单,PostgreSQL 将估计一个不等式连接条件将过滤掉三分之二的行。由于有两个这样的条件,选择性成倍增加,PostgreSQL会估计结果的行数是
(#rows in R) * (#rows in S) / 9
到目前为止,PostgreSQL 还没有任何跨表统计数据可以让这变得不那么粗糙。
【讨论】: