咱们先建表,插入数据:node
postgres=# CREATE TABLE t_test (id serial, name text); CREATE TABLE postgres=# INSERT INTO t_test (name) SELECT 'hans' postgres-# FROM generate_series(1, 2000000); INSERT 0 2000000 postgres=# INSERT INTO t_test (name) SELECT 'paul' postgres-# FROM generate_series(1, 2000000); INSERT 0 2000000
这样,咱们有了400万行数据:web
postgres=# SELECT name, count(*) FROM t_test GROUP BY 1; name | count ------+--------- hans | 2000000 paul | 2000000 (2 行记录)
咱们执行一个简单的查询:sql
postgres=# \timing 启用计时功能. postgres=# SELECT * FROM t_test WHERE id = 432332; id | name --------+------ 432332 | hans (1 行记录) 时间:111.051 ms
上面的例子,timing命令会告诉psql,显示查询的运行时间。注意,这不是实际的执行时间,而是由psql测量的时间。这个很短的查询,也应该考虑网络传输时间。网络
前面的例子,读400万行花了100多毫秒。从性能上看,这是一场灾难。可使用EXPLAIN命令,看看哪里有问题。
session
当你感受某查询的性能不够好,可使用EXPLAIN揭示性能问题。数据结构
postgres=# EXPLAIN SELECT * FROM t_test WHERE id = 432332; QUERY PLAN ------------------------------------------------------------------------- Gather (cost=1000.00..43455.43 rows=1 width=9) Workers Planned: 2 -> Parallel Seq Scan on t_test (cost=0.00..42455.33 rows=1 width=9) Filter: (id = 432332) (4 行记录) 时间:0.499 ms
在这个执行计划列表中,你看到了什么?PostgreSQL中,一个SQL语句的执行分四个阶段:架构
EXPLAIN的目的是看规划器准备如何高效地查询。在个人例子里,PostgreSQL使用并行的顺序扫描。这意味着两个workers会协做,共同处理过滤条件。经过聚合节点(gather node,从PostgreSQL 9.6开始支持,这是并行查询架构的一部分)将各部分结果联合到一块儿。你还能够看到PostgreSQL但愿在计划的每一个阶段返回多少行。
PostgreSQL 9.6中,并行workers的数量由表的大小决定。小表不会使用并行,由于开销很大。
也不是必须并行:并发
postgres=# SET max_parallel_workers_per_gather TO 0; SET
固然,这样作只影响当前session。也能够修改postgresql.conf中的配置,可是,强烈建议不要这么作。dom
若是只使用一个CPU,执行计划看起来像这样的:svg
postgres=# EXPLAIN SELECT * FROM t_test WHERE id = 432332; QUERY PLAN ---------------------------------------------------------- Seq Scan on t_test (cost=0.00..71622.00 rows=1 width=9) Filter: (id = 432332) (2 行记录)
PostgreSQL会使用该过滤条件,顺序读整个表。预计要花费71622 penalty point-这是一个抽象的概念,须要比较执行查询的不一样方式。PostgreSQL会决定使用看起来成本最低的执行计划。那为何是71622点呢?
postgres=# SELECT pg_relation_size('t_test') / 8192.0; ?column? -------------------- 21622.000000000000 (1 行记录)
pg_relation_size函数返回表占用空间的字节数。能够看到,该表由21622个块组成。根据这个成本模型,PostgreSQL会计算顺序读每一个块的成本。
影响它的配置参数是:
postgres=# SHOW seq_page_cost; seq_page_cost --------------- 1 (1 行记录)
可是,从磁盘读每一个块并非咱们作的所有。还要应用过滤条件,要把数据送给CPU。有两个参数说明这些成本:
postgres=# SHOW cpu_tuple_cost; cpu_tuple_cost ---------------- 0.01 (1 行记录) postgres=# SHOW cpu_operator_cost; cpu_operator_cost ------------------- 0.0025 (1 行记录)
因此,是这样计算的:
postgres=# SELECT 21622*1 + 4000000*0.01 + 4000000*0.0025; ?column? ------------ 71622.0000 (1 行记录)
你已经看到了,这是计划的成本。成本由CPU部分和I/O部分组成,最后计算出一个数。还有,这个成本和实际执行没有关系,这只是一个估计。
固然,这个简明的例子还列出了一些参数。PostgreSQL也有一些针对索引相关操做的参数:
若是使用了并行查询,还有更多的成本参数:
启用不少进程扫描大表一般不是个好主意。
读整张表来查找一条记录,更不是个好主意。
postgres=# CREATE INDEX idx_id ON t_test (id); CREATE INDEX postgres=# \timing 启用计时功能. postgres=# SELECT * FROM t_test WHERE id = 43242; id | name -------+------ 43242 | hans (1 行记录) 时间:1.621 ms
PostgreSQL使用Lehman-Yao的高并发b-tree建标准索引。Lehman-Yao容许你在同一时刻在同一个索引上执行不少操做(读和写)。
可是,索引不是免费的:
postgres=# \di+ 关联列表 架构模式 | 名称 | 类型 | 拥有者 | 数据表 | 大小 | 描述 ----------+--------+------+----------+--------+-------+------ public | idx_id | 索引 | postgres | t_test | 86 MB | (1 行记录)
咱们的400万行记录的索引,吃掉了86MB磁盘空间。并且会致使写表变慢-由于索引是同步的。
B-tree索引不仅是能够用来找记录,排序的时候也能用:
postgres=# EXPLAIN SELECT * FROM t_test ORDER BY id DESC LIMIT 10; QUERY PLAN ----------------------------------------------------------------------------------------------- Limit (cost=0.43..0.74 rows=10 width=9) -> Index Scan Backward using idx_id on t_test (cost=0.43..125505.43 rows=4000000 width=9) (2 行记录)
能够看到,索引能以正确的顺序返回数据,而不用扫描表。
除了ORDER BY,min和max函数也须要排序,索引也能提高他们的性能:
postgres=# explain SELECT min(id), max(id) FROM t_test; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Result (cost=0.93..0.94 rows=1 width=8) InitPlan 1 (returns $0) -> Limit (cost=0.43..0.46 rows=1 width=4) -> Index Only Scan using idx_id on t_test (cost=0.43..135505.43 rows=4000000 width=4) Index Cond: (id IS NOT NULL) InitPlan 2 (returns $1) -> Limit (cost=0.43..0.46 rows=1 width=4) -> Index Only Scan Backward using idx_id on t_test t_test_1 (cost=0.43..135505.43 rows=4000000 width=4) Index Cond: (id IS NOT NULL) (9 行记录)
PostgreSQL中,b-tree能以正常顺序读,也能以反序读。能够把b-tree当作一个排序列表。因此,天然地,最小值在前面,最大值在后面。
PostgreSQL容许一个查询内使用多个索引。甚至一个索引能够被屡次使用:
postgres=# explain SELECT * FROM t_test WHERE id = 30 OR id = 50; QUERY PLAN --------------------------------------------------------------------------- Bitmap Heap Scan on t_test (cost=8.88..16.85 rows=2 width=9) Recheck Cond: ((id = 30) OR (id = 50)) -> BitmapOr (cost=8.88..8.88 rows=2 width=0) -> Bitmap Index Scan on idx_id (cost=0.00..4.44 rows=1 width=0) Index Cond: (id = 30) -> Bitmap Index Scan on idx_id (cost=0.00..4.44 rows=1 width=0) Index Cond: (id = 50) (7 行记录)
位图扫描和位图索引不一样。
PostgreSQL的位图扫描是,扫描第一个索引,收集包含数据的块列表。而后,下一个索引扫描块列表……本例中使用的OR,这些列表被统一成一个包含数据的大的块列表。使用这个列表,扫描表检索这些块。
问题是PostgreSQL检索了比须要的更多的数据。咱们的例子要找两行,可是,位图扫描可能返回几个块。所以,执行器从新检查,过滤这些记录,来知足条件。
AND条件,或者AND和OR的混合条件,也使用位图扫描。不过,若是PostgreSQL看到AND条件,它不必定强迫本身使用位图扫描。PostgreSQL优化器会比较不一样计划的成本。
何时优化器会选择位图扫描呢?我以为有两种用例:
PostgreSQL 10.0,支持并行的位图堆扫描。
使用索引,并不老是能提升性能。
在深刻以前,再回顾一下咱们使用的数据结构-只有两个不一样的名字和惟一的ID。
postgres=# \d t_test 数据表 "public.t_test" 栏位 | 类型 | Collation | Nullable | Default ------+---------+-----------+----------+------------------------------------ id | integer | | not null | nextval('t_test_id_seq'::regclass) name | text | | | 索引: "idx_id" btree (id)
目前,id列有索引。下一步,咱们要查询name列,因而在name列也加一个索引。
postgres=# CREATE INDEX idx_name ON t_test (name); CREATE INDEX Time: 3191.722 ms (00:03.192)
如今看看是否正确地使用了索引:
postgres=# EXPLAIN SELECT * FROM t_test WHERE name = 'hans2'; QUERY PLAN ----------------------------------------------------------------------- Index Scan using idx_name on t_test (cost=0.43..8.45 rows=1 width=9) Index Cond: (name = 'hans2'::text) (2 行记录)
PostgreSQL决定使用索引。大多数用户指望这样。可是,查询条件是“hans2”,而hans2并不存在,查询计划完美地反映了这一点,rows=1,说明优化器只但愿查询返回很小的子集。
表里没这一行,可是PostgreSQL不会估计0行,这是由于它会使随后的估计变得更加困难。
让咱们看看,若是查询更多的数据会发生什么:
postgres=# EXPLAIN SELECT * FROM t_test postgres-# WHERE name = 'hans' OR name = 'paul'; QUERY PLAN ---------------------------------------------------------------- Seq Scan on t_test (cost=0.00..81622.00 rows=3000091 width=9) Filter: ((name = 'hans'::text) OR (name = 'paul'::text)) (2 行记录)
PostgreSQL采用了顺序扫描。为何呢?系统为何忽略了索引?理由是简单的:hans和paul组成了整个表(检查系通通计)。没有理由读索引以及整个表。
若是行的数量少,PostgreSQL再次考虑位图扫描和索引扫描:
postgres=# EXPLAIN SELECT * FROM t_test WHERE name = 'hans2' OR name = 'paul2'; QUERY PLAN ----------------------------------------------------------------------------- Bitmap Heap Scan on t_test (cost=8.88..12.89 rows=1 width=9) Recheck Cond: ((name = 'hans2'::text) OR (name = 'paul2'::text)) -> BitmapOr (cost=8.88..8.88 rows=1 width=0) -> Bitmap Index Scan on idx_name (cost=0.00..4.44 rows=1 width=0) Index Cond: (name = 'hans2'::text) -> Bitmap Index Scan on idx_name (cost=0.00..4.44 rows=1 width=0) Index Cond: (name = 'paul2'::text) (7 行记录)