假设,我们有三张表,一个视图,每张表都有索引:
CREATE TABLE a (aid int, ...); -- 一亿行
CREATE TABLE b (bid int, ...); -- 二亿行
CREATE TABLE c (cid int, ...); -- 三亿行
CREATE INDEX idx_a ON a (aid);
CREATE INDEX idx_b ON b (bid);
CREATE INDEX idx_c ON c (cid);
CREATE VIEW v AS SELECT * FROM a, b WHERE aid = bid;
假设用户想运行下面的查询。优化器会做什么?它的选择是什么?
SELECT *
FROM v, c
WHERE v.aid = c.cid AND cid = 4;
在看真是的优化器如何处理之前,先看看优化器有什么选项。
假设优化器从头开始,计算视图的输出。连接一亿行和两亿行有什么好办法?
一个办法是使用嵌套循环。原理很简单,下面是伪代码:
for x in table1:
for y in table2:
if x.field == y.field
issue row
else
keep doing
嵌套循环常用于一方很小,数据不多的时候。而我们的例子,要处理那么多数据,显然不会这样处理。
可以使用下面的策略,解决我们的问题:
← Hash join
← Sequentialscan table 1
← Sequentialscan table 2
双方都被hash,然后比较hash key,返回join结果。问题是所有的值都需要做hash,并暂存在某处。
最后是归并连接-使用排序列表连接返回值。如果要连接的双方都已经排好序了,系统能从顶部取一些行,如果匹配就返回。所以,最重要的是数据要先排序。计划是这样的:
← Merge join
← Sort table 1
← Sequential scan table 1
← Sort table 2
← Sequential scan table 2
连接的一方或者双方可以使用来自计划的底层的排序数据。如果直接访问表,索引是一个很好的选择,但是前提是返回的数据比整个表小很多。否则,我们的开销会增加很多-先读整个索引,再读整个表。如果返回集是表的很大一部分,顺序扫描更高效-尤其是以主键顺序访问的时候。
使用归并连接,可以处理很多数据。但是,数据必须先排序,或者从索引读数据。
排序成本是_O(n * log(n))_。因此,为3亿行数据排序也是没什么吸引力。
这几种连接都支持并行版本。
既然几种连接都不可行。可以考虑使用逻辑转换加速查询。要执行下列几步:
SELECT * FROM
(SELECT * FROM a, b WHERE aid = bid) AS v, c
WHERE v.aid = c.cid AND cid = 4;
该视图是内联的,转换成了子查询。这样,为进一步的优化打开了大门。
SELECT * FROM a, b, c WHERE a.aid = c.cid AND aid = bid AND cid = 4;
消除子查询以后,变成了这样的。现在是一个普通连接。
下来增加等式约束(equality constraints)-检测其他约束,连接选项和过滤等。让我们仔细观察一下这个查询:如果aid = cid而且aid = bid,我们知道bid = cid。如果cid = 4,则其他几个也等于4:
SELECT *
FROM a, b, c
WHERE a.aid = c.cid
AND aid = bid
AND cid = 4
AND bid = cid
AND aid = 4
AND bid = 4
这几列都可以使用索引,就不使用可怕的视图了。PostgreSQL选择从索引检索几行以后,使用什么连接都可以了。
现在,所有的形式转换都结束了,PostgreSQL会执行详尽的搜索。它会试各种可能的计划,使用最廉价的方案。PostgreSQL还要决定最好的连接顺序。原始查询中,连接顺序是A → B和A → C。等式约束以后,可能先连接B → C,然后再连接A。优化器对所有的选项都持开放的态度。
PostgreSQL可能选择这样的执行计划:
postgres=# explain SELECT * FROM v, c WHERE v.aid = c.cid AND cid = 4;
QUERY PLAN
----------------------------------------------------------------
Nested Loop (cost=1.71..17.78 rows=1 width=12)
-> Nested Loop (cost=1.14..9.18 rows=1 width=8)
-> Index Only Scan using idx_a on a
(cost=0.57..4.58 rows=1 width=4)
Index Cond: (aid = 4)
-> Index Only Scan using idx_b on b
(cost=0.57..4.59 rows=1 width=4)
Index Cond: (bid = 4)
-> Index Only Scan using idx_c on c
(cost=0.57..8.59 rows=1 width=4)
Index Cond: (cid = 4)
(8 rows)
PostgreSQL会使用三个索引,使用嵌套循环连接数据。
你看到了,PostgreSQL可以为查询提速。PostgreSQL够聪明,但是也需要聪明的用户。用户做了愚蠢的事情,也会影响优化效果。我们先删掉视图:
DROP VIEW v;
然后重新增加视图:
CREATE VIEW v AS SELECT * FROM a, b WHERE aid = bid
OFFSET 0;
视图的逻辑和之前的一样。但是,优化器会用不同的方式处理。每个非0的OFFSET,会改变结果,因此,视图不得不被计算。
postgres=# EXPLAIN SELECT * FROM v, c WHERE v.aid = c.cid AND cid = 4;
QUERY PLAN
----------------------------------------------------------------
Nested Loop (cost=120.71..7949879.40 rows=1 width=12)
-> Subquery Scan on v
(cost=120.13..7949874.80 rows=1 width=8)
Filter: (v.aid = 4)
-> Merge Join (cost=120.13..6699874.80
rows=100000000 width=8)
Merge Cond: (a.aid = b.bid)
-> Index Only Scan using idx_a on a
(cost=0.57..2596776.57 rows=100000000
width=4)
-> Index Only Scan using idx_b on b
(cost=0.57..5193532.33 rows=199999984
width=4)
-> Index Only Scan using idx_c on c
(cost=0.57..4.59 rows=1 width=4)
Index Cond: (cid = 4)
(9 rows)
只看看预测的成本-从两位数飙升到惊人的数字。显然,查询性能非常糟糕。
PostgreSQL还有很多优化措施,比如常量折叠(constant folding)-把表达式转换成常量:
postgres=# explain SELECT * FROM a WHERE aid = 3 + 1;
QUERY PLAN
--------------------------------------------------------------------
Index Only Scan using idx_a on a (cost=0.43..8.45 rows=1 width=4)
Index Cond: (aid = 4)
(2 行记录)
你看到了,PostgreSQL会查找4,因为aid有索引,PostgreSQL使用了索引扫描。因为我们的表只有一列,只访问索引就可以返回结果。
如果表达式在左边怎么办?
postgres=# explain SELECT * FROM a WHERE aid - 1 = 3;
QUERY PLAN
-----------------------------------------------------------------------
Gather (cost=1000.00..23350.00 rows=10000 width=4)
Workers Planned: 2
-> Parallel Seq Scan on a (cost=0.00..21350.00 rows=4167 width=4)
Filter: ((aid - 1) = 3)
(4 行记录)
就成顺序扫描了。
PostgreSQL可以内联静态SQL函数。用来减少调用的函数数量,提高速度。
我们先增加一个函数:
postgres=# CREATE OR REPLACE FUNCTION ld(int)
RETURNS numeric AS
$$
SELECT log(2, $1);
$$
LANGUAGE 'sql' IMMUTABLE;
CREATE FUNCTION
该函数用来计算以2为底的对数。
postgres=# SELECT ld(1024);
ld
---------------------
10.0000000000000000
(1 行记录)
为了演示工作原理,我们清空表,插入1000条记录,以加速索引的速度。
postgres=# TRUNCATE a;
TRUNCATE TABLE
postgres=# INSERT INTO a SELECT generate_series(1, 10000);
INSERT 0 10000
postgres=# CREATE INDEX idx_ld ON a (ld(aid));
CREATE INDEX
然后,看一下执行计划:
postgres=# EXPLAIN SELECT * FROM a WHERE ld(aid) = 10;
QUERY PLAN
-------------------------------------------------------------------
Index Scan using idx_ld on a (cost=0.29..8.30 rows=1 width=4)
Index Cond: (log('2'::numeric, (aid)::numeric) = '10'::numeric)
(2 行记录)
可以看到,优化器使用log函数代替了ld函数。和下面的查询等价:
postgres=# EXPLAIN SELECT * FROM a WHERE log(2, aid) = 10;
QUERY PLAN
-------------------------------------------------------------------
Index Scan using idx_ld on a (cost=0.29..8.30 rows=1 width=4)
Index Cond: (log('2'::numeric, (aid)::numeric) = '10'::numeric)
(2 行记录)
join pruning的意思是删除不必要的连接。比如一些中间件或者ORM产生的SQL语句,删除连接,可以减轻负担。
我们先建表:
postgres=# CREATE TABLE x (id int, PRIMARY KEY (id));
CREATE TABLE
postgres=# CREATE TABLE y (id int, PRIMARY KEY (id));
CREATE TABLE
postgres=# INSERT INTO x SELECT generate_series(1, 1000);
INSERT 0 1000
postgres=# INSERT INTO y SELECT generate_series(1, 1000);
INSERT 0 1000
执行下面的查询:
postgres=# EXPLAIN SELECT *
FROM x LEFT JOIN y ON (x.id = y.id)
WHERE x.id = 3;
QUERY PLAN
---------------------------------------------------------------------------
Nested Loop Left Join (cost=0.55..16.60 rows=1 width=8)
Join Filter: (x.id = y.id)
-> Index Only Scan using x_pkey on x (cost=0.28..8.29 rows=1 width=4)
Index Cond: (id = 3)
-> Index Only Scan using y_pkey on y (cost=0.28..8.29 rows=1 width=4)
Index Cond: (id = 3)
(6 行记录)
PostgreSQL直接连接了这些表。我们修改一下SQL-不再选择所有的列,而只选择连接左边表的所有的列:
postgres=# EXPLAIN SELECT x.*
FROM x LEFT JOIN y ON (x.id = y.id)
WHERE x.id = 3;
QUERY PLAN
---------------------------------------------------------------------
Index Only Scan using x_pkey on x (cost=0.28..8.29 rows=1 width=4)
Index Cond: (id = 3)
(2 行记录)
PostgreSQL不再连接,直接从x内部扫描。这么做,有两条理由:
集合操作允许把多个查询的结果组合成一个结果集。包括UNION、INTERSECT和EXCEPT。PostgreSQL为这些操作做了很多优化工作。
优化器会把限制下放到集合操作中。看下面的例子:
postgres=# EXPLAIN SELECT * FROM
(
SELECT aid AS xid FROM a
UNION ALL
SELECT bid FROM b
) AS y
WHERE xid = 3;
QUERY PLAN
--------------------------------------------------------------------------
Append (cost=0.29..16.61 rows=2 width=4)
-> Index Only Scan using idx_a on a (cost=0.29..8.30 rows=1 width=4)
Index Cond: (aid = 3)
-> Index Only Scan using idx_b on b (cost=0.28..8.29 rows=1 width=4)
Index Cond: (bid = 3)
(5 行记录)
可以看到,唯一的限制在子查询外。但是,PostgreSQL把过滤条件下沉,xid = 3的条件给了aid和bid,这样,两张表都可以使用索引了。
注意,UNION和UNION ALL不同:UNION ALL盲目地加上该数据,并传给两张表的结果,而UNION会过滤掉重复的内容:
postgres=# EXPLAIN SELECT * FROM
(
SELECT aid AS xid FROM a
UNION
SELECT bid FROM b
) AS y
WHERE xid = 3;
QUERY PLAN
--------------------------------------------------------------------------------------
Unique (cost=16.64..16.65 rows=2 width=4)
-> Sort (cost=16.64..16.64 rows=2 width=4)
Sort Key: a.aid
-> Append (cost=0.29..16.63 rows=2 width=4)
-> Index Only Scan using idx_a on a (cost=0.29..8.30 rows=1 width=4)
Index Cond: (aid = 3)
-> Index Only Scan using idx_b on b (cost=0.28..8.29 rows=1 width=4)
Index Cond: (bid = 3)
(8 行记录)
PostgreSQL不得不在Append节点上增加一个Sort节点,确保重复的数据能被过滤掉。
EXPLAIN ANALYZE会执行查询,返回计划,包括实际的运行时信息。下面是一个例子:
postgres=# EXPLAIN ANALYZE SELECT * FROM
(
SELECT * FROM b LIMIT 1000000
) AS b
ORDER BY cos(bid);
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=145669.91..148169.91 rows=1000000 width=12) (actual time=635.926..778.192 rows=1000000 loops=1)
Sort Key: (cos((b.bid)::double precision))
Sort Method: external merge Disk: 25496kB
-> Subquery Scan on b (cost=0.00..28921.57 rows=1000000 width=12) (actual time=19.451..317.650 rows=1000000 loops=1)
-> Limit (cost=0.00..13921.57 rows=1000000 width=4) (actual time=19.445..194.510 rows=1000000 loops=1)
-> Seq Scan on b b_1 (cost=0.00..314160.80 rows=22566480 width=4) (actual time=19.444..140.805 rows=1000000 loops=1)
Planning Time: 0.154 ms
Execution Time: 830.165 ms
(8 行记录)
这个计划看着有点可怕,不要惊慌,我们一步一步分析。读计划的时候,要从里向外读。我们的这个例子,从顺序扫描b开始。这里实际上有两块信息:成本块和实际的时间块,成本块包含估计,实际时间块显示实际的执行时间。这里的顺序扫描执行了140.805毫秒。
然后,数据被传给Limit节点,它确保没有太多的数据。注意,每个执行阶段也会向我们显示涉及的行的数量。你看到了,
通过Limit,确保PostgreSQL只得到100万条记录。实际的运行时间是317毫秒。最后,数据被排序,这要花费很多时间。查看计划,可以知道时间都流失在什么地方了。这个例子,顺序扫描花了一些时间,但是,这个阶段很难加速。然后,排序花了太多的时间。
排序是可以加速的,以后再讲。
EXPLAIN的输出可以提供更多信息。比如我们使用这些选项的时候:
postgres=# EXPLAIN (analyze, verbose, costs, timing, buffers)
SELECT * FROM a ORDER BY random();
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
Sort (cost=834.39..859.39 rows=10000 width=12) (actual time=7.079..7.980 rows=10000 loops=1)
Output: aid, (random())
Sort Key: (random())
Sort Method: quicksort Memory: 853kB
Buffers: shared hit=45
-> Seq Scan on public.a (cost=0.00..170.00 rows=10000 width=12) (actual time=0.021..2.189 rows=10000 loops=1)
Output: aid, random()
Buffers: shared hit=45
Planning Time: 0.095 ms
Execution Time: 9.232 ms
(10 行记录)
analyze是实际上执行查询,verbose会增加更多的信息,costs会显示成本信息,timing提供运行时数据,让我们观察时间都花在什么地方了,buffers很有启发性,表示执行该计划,需要访问多少个buffers。
看计划的时候,要问自己两个问题:
比如前面的例子,顺序扫描完成时是2.189毫秒,排序完成时是7.980毫秒。所以,排序用了将近6毫秒,占了本次查询的大部分时间。
想解决慢查询,无法提供一般性的建议,这是因为影响的因素太多了。
但是:要确保估计值和实际值比较接近。有时候,因为估计值的原因(比如系统统计信息没及时更新),优化器会作出糟糕的决定。一般来说,autovacuum期间会做优化器统计。
看下面的例子:
postgres=# CREATE TABLE t_estimate AS
SELECT * FROM generate_series(1, 10000) AS id;
SELECT 10000
然后增加优化器统计:
postgres=# ANALYZE t_estimate;
ANALYZE
我们现在看看估计:
postgres=# EXPLAIN ANALYZE SELECT * FROM t_estimate WHERE cos(id) < 4;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------
Seq Scan on t_estimate (cost=0.00..220.00 rows=3333 width=4) (actual time=0.017..2.753 rows=10000 loops=1)
Filter: (cos((id)::double precision) < '4'::double precision)
Planning Time: 0.101 ms
Execution Time: 3.309 ms
(4 行记录)
很多时候,PostgreSQL可能很好地不能处理WHERE,这是因为它只有列的统计,而没有表达式的统计。这里,我们看到了对WHERE子句返回的数据的严重低估。
当然,也可能高估数据量:
postgres=# EXPLAIN ANALYZE SELECT * FROM t_estimate WHERE cos(id) > 4;
QUERY PLAN
---------------------------------------------------------------------------------------------------------
Seq Scan on t_estimate (cost=0.00..220.00 rows=3333 width=4) (actual time=1.953..1.953 rows=0 loops=1)
Filter: (cos((id)::double precision) > '4'::double precision)
Rows Removed by Filter: 10000
Planning Time: 0.066 ms
Execution Time: 1.970 ms
(5 行记录)
幸运的是,可以解决此问题:
postgres=# CREATE INDEX idx_cosine ON t_estimate (cos(id));
CREATE INDEX
postgres=# ANALYZE t_estimate;
ANALYZE
再看看执行计划:
postgres=# EXPLAIN ANALYZE SELECT * FROM t_estimate WHERE cos(id) > 4;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Index Scan using idx_cosine on t_estimate (cost=0.29..8.30 rows=1 width=4) (actual time=0.007..0.007 rows=0 loops=1)
Index Cond: (cos((id)::double precision) > '4'::double precision)
Planning Time: 0.292 ms
Execution Time: 0.027 ms
(4 行记录)
错误估计时常发生,比如经常被低估的一个问题是跨列相关(cross-column correlation)。比如这样一个例子,20%的人喜欢滑雪,20%的人来自非洲。如果想要非洲滑雪者的数量,计算一下是20% * 20%=4%。但是,非洲很少下雪,所以滑雪者的数量应该很少。PostgreSQL的统计不跨列,所以会导致比较差的结果。
从PostgreSQL 10.0开始,有了多变量统计,可以一劳永逸地解决此问题。
很多时候,内存和缓存能导致意外的行为。
看个例子:
postgres=# CREATE TABLE t_random AS
SELECT * FROM generate_series(1, 10000000) AS id ORDER BY random();
SELECT 10000000
postgres=# ANALYZE t_random;
ANALYZE
我们查询一小部分记录:
postgres=# EXPLAIN (analyze true, buffers true, costs true, timing true)
SELECT * FROM t_random WHERE id < 1000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..97431.21 rows=1000 width=4) (actual time=0.902..225.346 rows=999 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=2176 read=42072
-> Parallel Seq Scan on t_random (cost=0.00..96331.21 rows=417 width=4) (actual time=1.160..219.202 rows=333 loops=3)
Filter: (id < 1000)
Rows Removed by Filter: 3333000
Buffers: shared hit=2176 read=42072
Planning Time: 0.070 ms
Execution Time: 225.431 ms
(10 行记录)
检查数据前,确保执行两次查询。看起来,应该使用索引。实际上,PostgreSQL找到2176个缓冲,而从操作系统读取了42072个缓冲。如果你够幸运,操作系统的缓冲命中率高,你的查询就快。如果命中率低,就需要从磁盘读数据。所以,执行时间会剧烈波动。查询从缓存读取的速度是从磁盘随机读速度的100倍。
让我们想像一个简单的例子。假如我们有一个电话系统,保存了100亿条记录。数据流增长得很快,而用户想查询这些数据。如果你有100亿行,只有部分数据能在内存找到,因此大部分来自磁盘。
比如,我们执行这样一条查询:
SELECT * FROM data WHERE phone_number = '+12345678';
你的通话记录会分散在各处-如果你挂断一个电话,再接起下一个,同时有数以万计的人都在这样做,所以,你的两条通话之间可能保存到同一个块的几率基本上是0。所以,如果想查询某电话的5000条通话记录,可能要访问5000个块。每个块需要5毫秒,一共就要执行25秒。当然,如果缓存命中率高一点,你的查询能执行得快一些。