JOIN 查询的行估计
Row estimation for JOIN queries
PostgreSQL 如何估计 JOIN 查询中的行数,例如:
EXPLAIN
SELECT *
FROM R, S
WHERE (R.StartTime < S.EndTime) AND (S.StartTime < R.EndTime);
假设涉及到的数据类型是timestamp with time time zone
(其实并不重要,我们会看到),join选择性估计函数可以用:
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 没有任何交叉table 的统计数据可以使它不那么粗糙。
手册中有一章恰好解决了您的问题:
其中包括对 Laurenz 提供的内容的解释。
但这还不是全部。我们还需要基础 table 的行数(基数)。 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
更多详情:
- Fast way to discover the row count of a table in PostgreSQL
例子
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 行。(因为这些是 临时 tables,它们不在 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 估计 rows=12031
table s
。一个很好的估计,算法有效。
由于 table 的物理大小不会自动缩小,因此删除行更容易导致估计值丢失。在主要 DELETE
之后 VACUUM ANALYZE
是个好主意。甚至 VACUUM FULL ANALYZE
。参见:
Postgres 期望 rows=6683889
,这符合我们的期望(根据 Laurenz 的解释):
SELECT 5000 * 12031 * 0.3333333333333333^2 -- 6683888.89
更好的查询
您的示例查询只是一个示例。但它恰好是一个糟糕的,因为同样可以通过 范围类型 和运算符更有效地实现。特别是 tstzrange
and &&
:
&&
的选择性?
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 << 0.333!而且通常更现实。
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
这是宾果游戏!
并且可以通过索引有效地支持此查询,从而改变游戏规则...
PostgreSQL 如何估计 JOIN 查询中的行数,例如:
EXPLAIN
SELECT *
FROM R, S
WHERE (R.StartTime < S.EndTime) AND (S.StartTime < R.EndTime);
假设涉及到的数据类型是timestamp with time time zone
(其实并不重要,我们会看到),join选择性估计函数可以用:
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 没有任何交叉table 的统计数据可以使它不那么粗糙。
手册中有一章恰好解决了您的问题:
其中包括对 Laurenz 提供的内容的解释。
但这还不是全部。我们还需要基础 table 的行数(基数)。 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
更多详情:
- Fast way to discover the row count of a table in PostgreSQL
例子
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 行。(因为这些是 临时 tables,它们不在 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 估计 rows=12031
table s
。一个很好的估计,算法有效。
由于 table 的物理大小不会自动缩小,因此删除行更容易导致估计值丢失。在主要 DELETE
之后 VACUUM ANALYZE
是个好主意。甚至 VACUUM FULL ANALYZE
。参见:
Postgres 期望 rows=6683889
,这符合我们的期望(根据 Laurenz 的解释):
SELECT 5000 * 12031 * 0.3333333333333333^2 -- 6683888.89
更好的查询
您的示例查询只是一个示例。但它恰好是一个糟糕的,因为同样可以通过 范围类型 和运算符更有效地实现。特别是 tstzrange
and &&
:
&&
的选择性?
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 << 0.333!而且通常更现实。
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
这是宾果游戏!
并且可以通过索引有效地支持此查询,从而改变游戏规则...