PostgreSQL教程(十七):SQL语言(十)之性能提示

一、使用EXPLAIN

PostgreSQL为每个收到查询产生一个查询计划 。 选择正确的计划来匹配查询结构和数据的属性对于好的性能来说绝对是最关键的,因此系统包含了一个复杂的规划器来尝试选择好的计划。 你可以使用EXPLAIN命令察看规划器为任何查询生成的查询计划。 阅读查询计划是一门艺术,它要求一些经验来掌握,但是本节只试图覆盖一些基础。

本节中的例子都是从开发源代码的回归测试数据库中抽取出来的,并且在此之前做过一次VACUUM ANALYZE。你应该能够在自己尝试这些例子时得到相似的结果,但是你的估计代价和行计数可能会小幅变化,因为ANALYZE的统计信息是随机采样而不是精确值,并且代价也与平台有某种程度的相关性。

这些例子使用EXPLAIN的默认"text"输出格式,这种格式紧凑并且便于人类阅读。如果你想把EXPLAIN的输出交给一个程序做进一步分析,你应该使用它的某种机器可读的输出格式(XML、JSON 或 YAML)。

1.1 EXPAIN 基础

查询计划的结构是一个计划结点的树。最底层的结点是扫描结点:它们从表中返回未经处理的行。 不同的表访问模式有不同的扫描结点类型:顺序扫描、索引扫描、位图索引扫描。 也还有不是表的行来源,例如VALUES子句和FROM中返回集合的函数,它们有自己的结点类型。如果查询需要连接、聚集、排序、或者在未经处理的行上的其它操作,那么就会在扫描结点之上有其它额外的结点来执行这些操作。 并且,做这些操作通常都有多种方法,因此在这些位置也有可能出现不同的结点类型。 EXPLAIN给计划树中每个结点都输出一行,显示基本的结点类型和计划器为该计划结点的执行所做的开销估计。 第一行(最上层的结点)是对该计划的总执行开销的估计;计划器试图最小化的就是这个数字。

这里是一个简单的例子,只是用来显示输出看起来是什么样的:

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

由于这个查询没有WHERE子句,它必须扫描表中的所有行,因此计划器只能选择使用一个简单的顺序扫描计划。被包含在圆括号中的数字是(从左至右):

  • 估计的启动开销。在输出阶段可以开始之前消耗的时间,例如在一个排序结点里执行排序的时间。

  • 估计的总开销。这个估计值基于的假设是计划结点会被运行到完成,即所有可用的行都被检索。不过实际上一个结点的父结点可能很快停止读所有可用的行(见下面的LIMIT例子)。

  • 这个计划结点输出行数的估计值。同样,也假定该结点能运行到完成。

  • 预计这个计划结点输出的行平均宽度(以字节计算)。

开销是用规划器的开销参数所决定的捏造单位来衡量的。传统上以取磁盘页面为单位来度量开销; 也就是seq_page_cost 将被按照习惯设为1.0,其它开销参数将相对于它来设置。 本节的例子都假定这些参数使用默认值。

有一点很重要:一个上层结点的开销包括它的所有子结点的开销。还有一点也很重要:这个开销只反映规划器关心的东西。特别是这个开销没有考虑结果行传递给客户端所花费的时间,这个时间可能是实际花费时间中的一个重要因素;但是它被规划器忽略了,因为它无法通过修改计划来改变(我们相信,每个正确的计划都将输出同样的行集)。

行数值有一些小技巧,因为它不是计划结点处理或扫描过的行数,而是该结点发出的行数。这通常比被扫描的行数少一些, 因为有些被扫描的行会被应用于此结点上的任意WHERE子句条件过滤掉。 理想中顶层的行估计会接近于查询实际返回、更新、删除的行数。

回到我们的例子:

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

这些数字的产生非常直接。如果你执行:

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

你会发现tenk1有358个磁盘页面和10000行。 开销被计算为 (页面读取数*seq_page_cost)+(扫描的行数*cpu_tuple_cost)。默认情况下,seq_page_cost是1.0,cpu_tuple_cost是0.01, 因此估计的开销是 (358 * 1.0) + (10000 * 0.01) = 458。

现在让我们修改查询并增加一个WHERE条件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=7001 width=244)
   Filter: (unique1 < 7000)

请注意EXPLAIN输出显示WHERE子句被当做一个"过滤器"条件附加到顺序扫描计划结点。 这意味着该计划结点为它扫描的每一行检查该条件,并且只输出通过该条件的行。因为WHERE子句的存在,估计的输出行数降低了。不过,扫描仍将必须访问所有 10000 行,因此开销没有被降低;实际上开销还有所上升(准确来说,上升了 10000 * cpu_operator_cost)以反映检查WHERE条件所花费的额外 CPU 时间。

这条查询实际选择的行数是 7000,但是估计的rows只是个近似值。如果你尝试重复这个试验,那么你很可能得到略有不同的估计。 此外,这个估计会在每次ANALYZE命令之后改变, 因为ANALYZE生成的统计数据是从该表中随机采样计算的。

现在,让我们把条件变得更严格:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.07..229.20 rows=101 width=244)
   Recheck Cond: (unique1 < 100)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
         Index Cond: (unique1 < 100)

这里,规划器决定使用一个两步的计划:子计划结点访问访问一个索引来找出匹配索引条件的行的位置,然后上层计划结点实际地从表中取出那些行。独立地抓取行比顺序地读取它们的开销高很多,但是不是所有的表页面都被访问,这么做实际上仍然比一次顺序扫描开销要少(使用两层计划的原因是因为上层规划结点把索引标识出来的行位置在读取之前按照物理位置排序,这样可以最小化单独抓取的开销。结点名称里面提到的"位图"是执行该排序的机制)。

现在让我们给WHERE子句增加另一个条件:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.04..229.43 rows=1 width=244)
   Recheck Cond: (unique1 < 100)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
         Index Cond: (unique1 < 100)

新增的条件stringu1 = 'xxx'减少了估计的输出行计数, 但是没有减少开销,因为我们仍然需要访问相同的行集合。 请注意,stringu1子句不能被应用为一个索引条件,因为这个索引只是在unique1列上。 它被用来过滤从索引中检索出的行。因此开销实际上略微增加了一些以反映这个额外的检查。

在某些情况下规划器将更倾向于一个"simple"索引扫描计划:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42;

                                 QUERY PLAN
-----------------------------------------------------------------------------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.29..8.30 rows=1 width=244)
   Index Cond: (unique1 = 42)

在这类计划中,表行被按照索引顺序取得,这使得读取它们开销更高,但是其中有一些是对行位置排序的额外开销。你很多时候将在只取得一个单一行的查询中看到这种计划类型。它也经常被用于拥有匹配索引顺序的ORDER BY子句的查询中,因为那样就不需要额外的排序步骤来满足ORDER BY

如果在WHERE引用的多个行上有独立的索引,规划器可能会选择使用这些索引的一个 AND 或 OR 组合:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
               Index Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0)
               Index Cond: (unique2 > 9000)

但是这要求访问两个索引,所以与只使用一个索引并把其他条件作为过滤器相比,它不一定能胜出。如果你变动涉及到的范围,你将看到计划也会相应改变。

下面是一个例子,它展示了LIMIT的效果:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Limit  (cost=0.29..14.48 rows=2 width=244)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..71.27 rows=10 width=244)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)

这是和上面相同的查询,但是我们增加了一个LIMIT这样不是所有的行都需要被检索,并且规划器改变了它的决定。注意索引扫描结点的总开销和行计数显示出好像它会被运行到完成。但是,限制结点在检索到这些行的五分之一后就会停止,因此它的总开销只是索引扫描结点的五分之一,并且这是查询的实际估计开销。之所以用这个计划而不是在之前的计划上增加一个限制结点是因为限制无法避免在位图扫描上花费启动开销,因此总开销会是超过那种方法(25个单位)的某个值。

让我们尝试连接两个表,使用我们已经讨论过的列:

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..118.62 rows=10 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244)
         Index Cond: (unique2 = t1.unique2)

在这个计划中,我们有一个嵌套循环连接结点,它有两个表扫描作为输入或子结点。该结点的摘要行的缩进反映了计划树的结构。连接的第一个(或"outer")子结点是一个与前面见到的相似的位图扫描。它的开销和行计数与我们从SELECT ... WHERE unique1 < 10得到的相同,因为我们将WHERE子句unique1 < 10用在了那个结点上。t1.unique2 = t2.unique2子句现在还不相关,因此它不影响 outer 扫描的行计数。嵌套循环连接结点将为从 outer 子结点得到的每一行运行它的第二个(或"inner")子结点。当前 outer 行的列值可以被插入 inner 扫描。这里,来自 outer 行的t1.unique2值是可用的,所以我们得到的计划和开销与前面见到的简单SELECT ... WHERE t2.unique2 = *constant*情况相似(估计的开销实际上比前面看到的略低,是因为在t2上的重复索引扫描会利用到高速缓存)。循环结点的开销则被以 outer 扫描的开销为基础设置,外加对每一个 outer 行都要进行一次 inner 扫描 (10 * 7.87),再加上用于连接处理一点 CPU 时间。

在这个例子里,连接的输出行计数等于两个扫描的行计数的乘积,但通常并不是所有的情况中都如此, 因为可能有同时提及两个表的 额外WHERE子句,并且因此它只能被应用于连接点,而不能影响任何一个输入扫描。这里是一个例子:

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred;

                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..49.46 rows=33 width=488)
   Join Filter: (t1.hundred < t2.hundred)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Materialize  (cost=0.29..8.51 rows=10 width=244)
         ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..8.46 rows=10 width=244)
               Index Cond: (unique2 < 10)

条件t1.hundred < t2.hundred不能在tenk2_unique2索引中被测试,因此它被应用在连接结点。这缩减了连接结点的估计输出行计数,但是没有改变任何输入扫描。

注意这里规划器选择了"物化"连接的 inner 关系,方法是在它的上方放了一个物化计划结点。这意味着t2索引扫描将只被做一次,即使嵌套循环连接结点需要读取其数据十次(每个来自 outer 关系的行都要读一次)。物化结点在读取数据时将它保存在内存中,然后在每一次后续执行时从内存返回数据。

在处理外连接时,你可能会看到连接计划结点同时附加有"连接过滤器"和普通"过滤器"条件。连接过滤器条件来自于外连接的ON子句,因此一个无法通过连接过滤器条件的行也能够作为一个空值扩展的行被发出。但是一个普通过滤器条件被应用在外连接条件之后并且因此无条件移除行。在一个内连接中这两种过滤器类型没有语义区别。

如果我们把查询的选择度改变一点,我们可能得到一个非常不同的连接计划:

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Hash Join  (cost=230.47..713.98 rows=101 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)
   ->  Hash  (cost=229.20..229.20 rows=101 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
                     Index Cond: (unique1 < 100)

这里规划器选择了使用一个哈希连接,在其中一个表的行被放入一个内存哈希表,在这之后其他表被扫描并且为每一行查找哈希表来寻找匹配。同样要注意缩进是如何反映计划结构的:tenk1上的位图扫描是哈希结点的输入,哈希结点会构造哈希表。然后哈希表会返回给哈希连接结点,哈希连接结点将从它的 outer 子计划读取行,并为每一个行搜索哈希表。

另一种可能的连接类型是一个归并连接,如下所示:

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Merge Join  (cost=198.11..268.19 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)
         Filter: (unique1 < 100)
   ->  Sort  (cost=197.83..200.33 rows=1000 width=244)
         Sort Key: t2.unique2
         ->  Seq Scan on onek t2  (cost=0.00..148.00 rows=1000 width=244)

归并连接要求它的输入数据被按照连接键排序。在这个计划中,tenk1数据被使用一个索引扫描排序,以便能够按照正确的顺序来访问行。但是对于onek则更倾向于一个顺序扫描和排序,因为在那个表中有更多行需要被访问(对于很多行的排序,顺序扫描加排序常常比一个索引扫描好,因为索引扫描需要非顺序的磁盘访问)。

一种查看变体计划的方法是强制规划器丢弃它认为开销最低的任何策略,这可以使用启用/禁用标志实现(这是一个野蛮的工具,但是很有用)。例如,如果我们并不认同在前面的例子中顺序扫描加排序是处理表onek的最佳方法,我们可以尝试:

SET enable_sort = off;

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Merge Join  (cost=0.56..292.65 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)
         Filter: (unique1 < 100)
   ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..224.79 rows=1000 width=244)

这显示规划器认为用索引扫描来排序onek的开销要比用顺序扫描加排序的方式高大约12%。当然,下一个问题是是否真的是这样。我们可以通过使用EXPLAIN ANALYZE来仔细研究一下,如下文所述。

1.2 EXPAIN ANALYZE

可以通过使用EXPLAINANALYZE选项来检查规划器估计值的准确性。通过使用这个选项,EXPLAIN会实际执行该查询,然后显示真实的行计数和在每个计划结点中累计的真实运行时间,还会有一个普通EXPLAIN显示的估计值。例如,我们可能得到这样一个结果:

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                                           QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..118.62 rows=10 width=488) (actual time=0.128..0.377 rows=10 loops=1)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244) (actual time=0.057..0.121 rows=10 loops=1)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.024..0.024 rows=10 loops=1)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244) (actual time=0.021..0.022 rows=1 loops=10)
         Index Cond: (unique2 = t1.unique2)
 Planning time: 0.181 ms
 Execution time: 0.501 ms

注意"actual time"值是以毫秒计的真实时间,而cost估计值被以捏造的单位表示,因此它们不大可能匹配上。在这里面要查看的最重要的一点是估计的行计数是否合理地接近实际值。在这个例子中,估计值都是完全正确的,但是在实际中非常少见。

在某些查询计划中,可以多次执行一个子计划结点。例如,inner 索引扫描可能会因为上层嵌套循环计划中的每一个 outer 行而被执行一次。在这种情况下,loops值报告了执行该结点的总次数,并且 actual time 和行数值是这些执行的平均值。这是为了让这些数字能够与开销估计被显示的方式有可比性。将这些值乘上loops值可以得到在该结点中实际消耗的总时间。在上面的例子中,我们在执行tenk2的索引扫描上花费了总共 0.220 毫秒。

在某些情况中,EXPLAIN ANALYZE会显示计划结点执行时间和行计数之外的额外执行统计信息。例如,排序和哈希结点提供额外的信息:

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;

                                                                 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=717.34..717.59 rows=101 width=488) (actual time=7.761..7.774 rows=100 loops=1)
   Sort Key: t1.fivethous
   Sort Method: quicksort  Memory: 77kB
   ->  Hash Join  (cost=230.47..713.98 rows=101 width=488) (actual time=0.711..7.427 rows=100 loops=1)
         Hash Cond: (t2.unique2 = t1.unique2)
         ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244) (actual time=0.007..2.583 rows=10000 loops=1)
         ->  Hash  (cost=229.20..229.20 rows=101 width=244) (actual time=0.659..0.659 rows=100 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 28kB
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244) (actual time=0.080..0.526 rows=100 loops=1)
                     Recheck Cond: (unique1 < 100)
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.049..0.049 rows=100 loops=1)
                           Index Cond: (unique1 < 100)
 Planning time: 0.194 ms
 Execution time: 8.008 ms

排序结点显示使用的排序方法(尤其是,排序是在内存中还是磁盘上进行)和需要的内存或磁盘空间量。哈希结点显示了哈希桶的数量和批数,以及被哈希表所使用的内存量的峰值(如果批数超过一,也将会涉及到磁盘空间使用,但是并没有被显示)。

另一种类型的额外信息是被一个过滤器条件移除的行数:

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;

                                               QUERY PLAN
---------------------------------------------------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=7000 width=244) (actual time=0.016..5.107 rows=7000 loops=1)
   Filter: (ten < 7)
   Rows Removed by Filter: 3000
 Planning time: 0.083 ms
 Execution time: 5.905 ms

这些值对于被应用在连接结点上的过滤器条件特别有价值。只有在至少有一个被扫描行或者在连接结点中一个可能的连接对被过滤器条件拒绝时,"Rows Removed"行才会出现。

一个与过滤器条件相似的情况出现在"有损"索引扫描中。例如,考虑这个查询,它搜索包含一个指定点的多边形:

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                              QUERY PLAN
------------------------------------------------------------------------------------------------------
 Seq Scan on polygon_tbl  (cost=0.00..1.05 rows=1 width=32) (actual time=0.044..0.044 rows=0 loops=1)
   Filter: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Filter: 4
 Planning time: 0.040 ms
 Execution time: 0.083 ms

规划器认为(非常正确)这个采样表太小不值得劳烦一次索引扫描,因此我们得到了一个普通的顺序扫描,其中的所有行都被过滤器条件拒绝。但是如果我们强制使得一次索引扫描可以被使用,我们看到:

SET enable_seqscan TO off;

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=32) (actual time=0.062..0.062 rows=0 loops=1)
   Index Cond: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Index Recheck: 1
 Planning time: 0.034 ms
 Execution time: 0.144 ms

这里我们可以看到索引返回一个候选行,然后它会被索引条件的重新检查拒绝。这是因为一个 GiST 索引对于多边形包含测试是 "有损的":它确实返回覆盖目标的多边形的行,然后我们必须在那些行上做精确的包含性测试。

EXPLAIN有一个BUFFERS选项可以和ANALYZE一起使用来得到更多运行时统计信息:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                                           QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244) (actual time=0.323..0.342 rows=10 loops=1)
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   Buffers: shared hit=15
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0) (actual time=0.309..0.309 rows=0 loops=1)
         Buffers: shared hit=7
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)
               Index Cond: (unique1 < 100)
               Buffers: shared hit=2
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.227..0.227 rows=999 loops=1)
               Index Cond: (unique2 > 9000)
               Buffers: shared hit=5
 Planning time: 0.088 ms
 Execution time: 0.423 ms

BUFFERS提供的数字帮助我们标识查询的哪些部分是对 I/O 最敏感的。

记住因为EXPLAIN ANALYZE实际运行查询,任何副作用都将照常发生,即使查询可能输出的任何结果被丢弃来支持打印EXPLAIN数据。如果你想要分析一个数据修改查询而不想改变你的表,你可以在分析完后回滚命令,例如:

BEGIN;

EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;

                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Update on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=14.628..14.628 rows=0 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=0.101..0.439 rows=100 loops=1)
         Recheck Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1)
               Index Cond: (unique1 < 100)
 Planning time: 0.079 ms
 Execution time: 14.727 ms

ROLLBACK;

正如在这个例子中所看到的,当查询是一个INSERTUPDATEDELETE命令时,应用表更改的实际工作由顶层插入、更新或删除计划结点完成。这个结点之下的计划结点执行定位旧行以及/或者计算新数据的工作。因此在上面,我们看到我们已经见过的位图表扫描,它的输出被交给一个更新结点,更新结点会存储被更新过的行。还有一点值得注意的是,尽管数据修改结点可能要可观的运行时间(这里,它消耗最大份额的时间),规划器当前并没有对开销估计增加任何东西来说明这些工作。这是因为这些工作对每一个正确的查询计划都得做,所以它不影响计划的选择。

当一个UPDATE或者DELETE命令影响继承层次时, 输出可能像这样:

EXPLAIN UPDATE parent SET f2 = f2 + 1 WHERE f1 = 101;
                                    QUERY PLAN
-----------------------------------------------------------------------------------
 Update on parent  (cost=0.00..24.53 rows=4 width=14)
   Update on parent
   Update on child1
   Update on child2
   Update on child3
   ->  Seq Scan on parent  (cost=0.00..0.00 rows=1 width=14)
         Filter: (f1 = 101)
   ->  Index Scan using child1_f1_key on child1  (cost=0.15..8.17 rows=1 width=14)
         Index Cond: (f1 = 101)
   ->  Index Scan using child2_f1_key on child2  (cost=0.15..8.17 rows=1 width=14)
         Index Cond: (f1 = 101)
   ->  Index Scan using child3_f1_key on child3  (cost=0.15..8.17 rows=1 width=14)
         Index Cond: (f1 = 101)

在这个例子中,更新节点需要考虑三个子表以及最初提到的父表。因此有四个输入 的扫描子计划,每一个对应于一个表。为清楚起见,在更新节点上标注了将被更新 的相关目标表,显示的顺序与相应的子计划相同(这些标注是从 PostgreSQL 9.5 开始新增的,在以前的版本中读者必须通过 观察子计划才能知道这些目标表)。

EXPLAIN ANALYZE显示的 Planning time是从一个已解析的查询生成查询计划并进行优化 所花费的时间,其中不包括解析和重写。

EXPLAIN ANALYZE显示的Execution time包括执行器的启动和关闭时间,以及运行被触发的任何触发器的时间,但是它不包括解析、重写或规划的时间。如果有花在执行BEFORE执行器的时间,它将被包括在相关的插入、更新或删除结点的时间内;但是用来执行AFTER 触发器的时间没有被计算,因为AFTER触发器是在整个计划完成后被触发的。在每个触发器(BEFOREAFTER)也被独立地显示。注意延迟约束触发器将不会被执行,直到事务结束,并且因此根本不会被EXPLAIN ANALYZE考虑。

1.3 警告

在两种有效的方法中EXPLAIN ANALYZE所度量的运行时间可能偏离同一个查询的正常执行。首先,由于不会有输出行被递交给客户端,网络传输开销和 I/O 转换开销没有被包括在内。其次,由EXPLAIN ANALYZE所增加的度量开销可能会很可观,特别是在操作系统调用gettimeofday()很慢的机器上。你可以使用pg_test_timing工具来度量在你的系统上的计时开销。

EXPLAIN结果不应该被外推到与你实际测试的非常不同的情况。例如,一个很小的表上的结果不能被假定成适合大型表。规划器的开销估计不是线性的,并且因此它可能为一个更大或更小的表选择一个不同的计划。一个极端例子是,在一个只占据一个磁盘页面的表上,你将几乎总是得到一个顺序扫描计划,而不管索引是否可用。规划器认识到它在任何情况下都将采用一次磁盘页面读取来处理该表,因此用额外的页面读取去查看一个索引是没有价值的(我们已经在前面的polygon_tbl例子中见过)。

在一些情况中,实际的值和估计的值不会匹配得很好,但是这并非错误。一种这样的情况发生在计划结点的执行被LIMIT或类似的效果很快停止。例如,在我们之前用过的LIMIT查询中:

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                                          QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.29..14.71 rows=2 width=244) (actual time=0.177..0.249 rows=2 loops=1)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..72.42 rows=10 width=244) (actual time=0.174..0.244 rows=2 loops=1)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)
         Rows Removed by Filter: 287
 Planning time: 0.096 ms
 Execution time: 0.336 ms

索引扫描结点的估计开销和行计数被显示成好像它会运行到完成。但是实际上限制结点在得到两个行之后就停止请求行,因此实际的行计数只有 2 并且运行时间远低于开销估计所建议的时间。这并非估计错误,这仅仅一种估计值和实际值显示方式上的不同。

归并连接也有类似的现象。如果一个归并连接用尽了一个输入并且其中的最后一个键值小于另一个输入中的下一个键值,它将停止读取另一个输入。在这种情况下,不会有更多的匹配并且因此不需要扫描第二个输入的剩余部分。这会导致不读取一个子结点的所有内容,其结果就像在LIMIT中所提到的。另外,如果 outer (第一个)子结点包含带有重复键值的行,inner(第二个)子结点会被倒退并且被重新扫描来找能匹配那个键值的行。EXPLAIN ANALYZE会统计相同 inner 行的重复发出,就好像它们是真实的额外行。当有很多 outer 重复时,对 inner 子计划结点所报告的实际行计数会显著地大于实际在 inner 关系中的行数。

由于实现的限制,BitmapAnd 和 BitmapOr 结点总是报告它们的实际行计数为零。

通常,EXPLAIN输出将显示查询规划器生成的每个计划节点的详细情况。不过,有一些情况中执行器能够确定特定的节点不是必需的。当前,唯一支持这种行动的节点类型是Append节点。这种节点类型有能力丢弃掉确定不会产生查询所需记录的子节点。可以通过EXPLAIN输出中的"Subplans Removed"属性的存在确定已经被移除的节点。

二、规划器使用的统计信息

2.1 单列统计信息

如我们在上一节所见,查询规划器需要估计一个查询要检索的行数,这样才能对查询计划做出好的选择。 本节对系统用于这些估计的统计信息进行一个快速的介绍。

统计信息的一个部分就是每个表和索引中的项的总数,以及每个表和索引占用的磁盘块数。这些信息保存在pg_class表的reltuplesrelpages列中。 我们可以用类似下面的查询查看这些信息:

SELECT relname, relkind, reltuples, relpages
FROM pg_class
WHERE relname LIKE 'tenk1%';

       relname        | relkind | reltuples | relpages
----------------------+---------+-----------+----------
 tenk1                | r       |     10000 |      358
 tenk1_hundred        | i       |     10000 |       30
 tenk1_thous_tenthous | i       |     10000 |       30
 tenk1_unique1        | i       |     10000 |       30
 tenk1_unique2        | i       |     10000 |       30
(5 rows)

这里我们可以看到tenk1包含 10000 行, 它的索引也有这么多行,但是索引远比表小得多(不奇怪)。

出于效率考虑,reltuplesrelpages不是实时更新的 ,因此它们通常包含有些过时的值。它们被VACUUMANALYZE和几个 DDL 命令(例如CREATE INDEX)更新。一个不扫描全表的VACUUMANALYZE操作(常见情况)将以它扫描的部分为基础增量更新reltuples计数,这就导致了一个近似值。在任何情况中,规划器将缩放它在pg_class中找到的值来匹配当前的物理表尺寸,这样得到一个较紧的近似。

大多数查询只是检索表中行的一部分,因为它们有限制要被检查的行的WHERE子句。 因此规划器需要估算WHERE子句的选择度 ,即符合WHERE子句中每个条件的行的比例。 用于这个任务的信息存储在pg_statistic系统目录中。 在pg_statistic中的项由ANALYZEVACUUM ANALYZE命令更新, 并且总是近似值(即使刚刚更新完)。

除了直接查看pg_statistic之外, 手工检查统计信息的时候最好查看它的视图pg_statspg_stats被设计为更容易阅读。 而且,pg_stats是所有人都可以读取的,而pg_statistic只能由超级用户读取(这样可以避免非授权用户从统计信息中获取一些其他人的表的内容的信息。pg_stats视图被限制为只显示当前用户可读的表)。例如,我们可以:

SELECT attname, inherited, n_distinct,
       array_to_string(most_common_vals, E'\n') as most_common_vals
FROM pg_stats
WHERE tablename = 'road';

 attname | inherited | n_distinct |          most_common_vals
---------+-----------+------------+------------------------------------
 name    | f         |  -0.363388 | I- 580                        Ramp+
         |           |            | I- 880                        Ramp+
         |           |            | Sp Railroad                       +
         |           |            | I- 580                            +
         |           |            | I- 680                        Ramp
 name    | t         |  -0.284859 | I- 880                        Ramp+
         |           |            | I- 580                        Ramp+
         |           |            | I- 680                        Ramp+
         |           |            | I- 580                            +
         |           |            | State Hwy 13                  Ramp
(2 rows)

注意,这两行显示的是相同的列,一个对应开始于road表(inherited=t)的完全继承层次, 另一个只包括road表本身(inherited=f)。

ANALYZEpg_statistic中存储的信息量(特别是每个列的most_common_vals中的最大项数和histogram_bounds数组)可以用ALTER TABLE SET STATISTICS命令为每一列设置, 或者通过设置配置变量default_statistic_target进行全局设置。 目前的默认限制是 100 个项。提升该限制可能会让规划器做出更准确的估计(特别是对那些有不规则数据分布的列), 其代价是在pg_statistic中消耗了更多空间,并且需要略微多一些的时间来计算估计数值。 相比之下,比较低的限制可能更适合那些数据分布比较简单的列。

2.2 扩展统计信息

常常可以看到由于查询子句中用到的多个列相互关联而运行着糟糕的执行计划的慢查询。规划器通常会假设多个条件是彼此独立的,这种假设在列值相互关联的情况下是不成立的。由于常规的统计信息天然的针对个体列的性质,它们无法捕捉到跨列关联的知识。不过,PostgreSQL有能力计算多元统计信息,它能捕捉这类信息。

由于可能的列组合数非常巨大,所以不可能自动计算多元统计信息。可以创建扩展统计信息对象 (更常被称为统计信息对象)来指示服务器获得跨感兴趣列集合的统计信息。

统计信息对象可以使用CREATE STATISTICS命令创建。这样一个对象的创建仅仅是创建了一个目录项来表示对统计信息有兴趣。实际的数据收集是由ANALYZE(或者是一个手工命令,或者是后台的自动分析)执行的。收集到的值可以在pg_statistic_ext目录中看到。

ANALYZE基于它用来计算常规单列统计信息的表行样本来计算扩展统计信息。由于样本的尺寸会随着表或者表列的统计信息目标(如前一节所述)增大而增加,更大的统计信息目标通常将会导致更准确的扩展统计信息,同时也会导致更多花在计算扩展统计信息之上的时间。

下面的小节介绍当前支持的扩展统计信息类型。

2.2.1 函数依赖

最简单的一类扩展统计信息跟踪函数依赖 ,这是在数据库范式定义中使用的概念。如果列a的值的知识足以决定列b的值,即不会有两个行具有相同的a值但是有不同的b值,我们就说列b函数依赖于列a。在一个完全规范化的数据库中,函数依赖应该仅存在于主键和超键上。不过,实际上很多数据集合会由于各种原因无法被完全规范化,常见的例子是为了性能而有意地反规范化。即使在一个完全规范化的数据库中,也会有某些列之间的部分关联,这些可以表达成部分函数依赖。

函数依赖的存在直接影响了特定查询中估计的准确性。如果一个查询包含独立列和依赖列上的条件,依赖列上的条件不会进一步降低结果的尺寸。但是如果没有函数依赖的知识,查询规划器将假定条件是独立的,导致对结果尺寸的低估。

要告知规划器有关函数依赖的信息,ANALYZE可以收集跨列依赖的测度。评估所有列组之间的依赖程度可能会昂贵到不可实现,因此数据收集被限制为针对那些在一个统计信息对象中一起出现的列组(用dependencies选项定义)。建议只对强相关的列组创建dependencies统计信息,以避免ANALYZE以及后期查询规划中不必要的开销。

这里是一个收集函数依赖统计信息的例子:

CREATE STATISTICS stts (dependencies) ON zip, city FROM zipcodes;

ANALYZE zipcodes;

SELECT stxname, stxkeys, stxdependencies
  FROM pg_statistic_ext
  WHERE stxname = 'stts';
 stxname | stxkeys |             stxdependencies               
---------+---------+------------------------------------------
 stts    | 1 5     | {"1 => 5": 1.000000, "5 => 1": 0.423130}
(1 row)

这里可以看到列1(邮编)完全决定列5(城市),因此系数为1.0,而城市仅决定42%的邮编,意味着有很多城市(58%)有多个邮编。

在为涉及函数依赖列的查询计算选择度时,规划器会使用依赖系数来调整针对条件的选择度估计,这样就不会产生低估。

2.2.1.1. 函数依赖的限制

当前只有在考虑简单等值条件(将列与常量值比较)时,函数依赖才适用。不会使用它们来改进比较两个列或者比较列和表达式的等值条件的估计,也不会用它们来改进范围子句、LIKE或者任何其他类型的条件。

在用函数依赖估计时,规划器假定在涉及的列上的条件是兼容的并且因此是冗余的。如果它们是不兼容的,正确的估计将是零行,但是那种可能性不会被考虑。例如,给定一个这样的查询

SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '94105';

规划器将会忽视city子句,因为它不改变选择度,这是正确的。不过,即便真地只有零行满足下面的查询,规划器也会做出同样的假设

SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '90210';

不过,函数依赖统计信息无法提供足够的信息来排除这种情况。

在很多实际情况中,这种假设通常是能满足的。例如,在应用程序中可能有一个GUI仅允许选择兼容的城市和邮编值用在查询中。但是如果不是这样,函数依赖可能就不是一个可行的选项。

2.2.2. 多元可区分值计数

单列统计信息存储每一列中可区分值的数量。在组合多个列(例如GROUP BY a, b)时,如果规划器只有单列统计数据,则对可区分值数量的估计常常会错误,导致选择不好的计划。

为了改进这种估计,ANALYZE可以为列组收集可区分值统计信息。和以前一样,为每一种可能的列组合做这件事情是不切实际的,因此只会为一起出现在一个统计信息对象(用ndistinct选项定义)中的列组收集数据。将会为列组中列出的列的每一种可能的组合都收集数据。

继续之前的例子,ZIP代码表中的可区分值计数可能像这样:

CREATE STATISTICS stts2 (ndistinct) ON zip, state, city FROM zipcodes;

ANALYZE zipcodes;

SELECT stxkeys AS k, stxndistinct AS nd
  FROM pg_statistic_ext
  WHERE stxname = 'stts2';
-[ RECORD 1 ]--------------------------------------------------------
k  | 1 2 5
nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178}
(1 row)

这表示有三种列组合有33178个可区分值:ZIP代码和州、ZIP代码和城市、ZIP代码+城市+周(事实上对于表中给定的一个唯一的ZIP代码,它们本来就应该是相等的)。另一方面,城市和州的组合只有27435个可区分值。

建议只对实际用于分组的列组合以及分组数错误估计导致了糟糕计划的列组合创建ndistinct统计信息对象。否则,ANALYZE循环只会被浪费。

三、用显示JOIN子句控制规划器

我们可以在一定程度上用显式JOIN语法控制查询规划器。要明白为什么需要它,我们首先需要一些背景知识。

在一个简单的连接查询中,例如:

SELECT * FROM a, b, c WHERE a.id = b.id AND b.ref = c.id;

规划器可以自由地按照任何顺序连接给定的表。例如,它可以生成一个使用WHERE条件a.id = b.id连接 A 到 B 的查询计划,然后用另外一个WHERE条件把 C 连接到这个连接表。或者它可以先连接 B 和 C 然后再连接 A 得到同样的结果。 或者也可以连接 A 到 C 然后把结果与 B 连接 --- 不过这么做效率不好,因为必须生成完整的 A 和 C 的迪卡尔积,而在WHERE子句中没有可用条件来优化该连接(PostgreSQL执行器中的所有连接都发生在两个输入表之间, 所以它必须以这些形式之一建立结果)。 重要的一点是这些不同的连接可能性给出在语义等效的结果,但在执行开销上却可能有巨大的差别。 因此,规划器会对它们进行探索并尝试找出最高效的查询计划。

当一个查询只涉及两个或三个表时,那么不需要考虑很多连接顺序。但是可能的连接顺序数随着表数目的增加成指数增长。 当超过十个左右的表以后,实际上根本不可能对所有可能性做一次穷举搜索,甚至对六七个表都需要相当长的时间进行规划。 当有太多的输入表时,PostgreSQL规划器将从穷举搜索切换为一种遗传 概率搜索,它只需要考虑有限数量的可能性(切换的阈值用geqo_threshold运行时参数设置)。遗传搜索用时更少,但是并不一定会找到最好的计划。

当查询涉及外连接时,规划器比处理普通(内)连接时拥有更小的自由度。例如,考虑:

SELECT * FROM a LEFT JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);

尽管这个查询的约束表面上和前一个非常相似,但它们的语义却不同, 因为如果 A 里有任何一行不能匹配 B 和 C的连接表中的行,它也必须被输出。因此这里规划器对连接顺序没有什么选择:它必须先连接 B 到 C,然后把 A 连接到该结果上。 相应地,这个查询比前面一个花在规划上的时间更少。在其它情况下,规划器就有可能确定多种连接顺序都是安全的。例如,给定:

SELECT * FROM a LEFT JOIN b ON (a.bid = b.id) LEFT JOIN c ON (a.cid = c.id);

将 A 首先连接到 B 或 C 都是有效的。当前,只有FULL JOIN完全约束连接顺序。大多数涉及LEFT JOINRIGHT JOIN的实际情况都在某种程度上可以被重新排列。

显式连接语法(INNER JOINCROSS JOIN或无修饰的JOIN)在语义上和FROM中列出输入关系是一样的, 因此它不约束连接顺序。

即使大多数类型的JOIN并不完全约束连接顺序,但仍然可以指示PostgreSQL查询规划器将所有JOIN子句当作有连接顺序约束来对待。例如,这里的三个查询在逻辑上是等效的:

SELECT * FROM a, b, c WHERE a.id = b.id AND b.ref = c.id;
SELECT * FROM a CROSS JOIN b CROSS JOIN c WHERE a.id = b.id AND b.ref = c.id;
SELECT * FROM a JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);

但如果我们告诉规划器遵循JOIN的顺序,那么第二个和第三个还是要比第一个花在规划上的时间少。 这个效果对于只有三个表的连接而言是微不足道的,但对于数目众多的表,可能就是救命稻草了。

要强制规划器遵循显式JOIN的连接顺序, 我们可以把运行时参数join_collapse_limit设置为 1(其它可能值在下文讨论)。

你不必为了缩短搜索时间来完全约束连接顺序,因为可以在一个普通FROM列表里使用JOIN操作符。例如,考虑:

SELECT * FROM a CROSS JOIN b, c, d, e WHERE ...;

如果设置join_collapse_limit = 1,那么这就强迫规划器先把 A 连接到 B, 然后再连接到其它的表上,但并不约束它的选择。在这个例子中,可能的连接顺序的数目减少了 5 倍。

按照这种方法约束规划器的搜索是一个有用的技巧,不管是对减少规划时间还是对引导规划器生成好的查询计划。 如果规划器按照默认选择了一个糟糕的连接顺序,你可以通过JOIN语法强迫它选择一个更好的顺序 --- 假设你知道一个更好的顺序。我们推荐进行实验。

一个非常相近的影响规划时间的问题是把子查询压缩到它们的父查询中。例如,考虑:

SELECT *
FROM x, y,
    (SELECT * FROM a, b, c WHERE something) AS ss
WHERE somethingelse;

这种情况可能在使用包含连接的视图时出现;该视图的SELECT规则将被插入到引用视图的地方,得到与上文非常相似的查询。 通常,规划器会尝试把子查询压缩到父查询里,得到:

SELECT * FROM x, y, a, b, c WHERE something AND somethingelse;

这样通常会生成一个比独立的子查询更好些的计划(例如,outer 的WHERE条件可能先把 X 连接到 A 上,这样就消除了 A 中的许多行, 因此避免了形成子查询的全部逻辑输出)。但是同时,我们增加了规划的时间; 在这里,我们用五路连接问题替代了两个独立的三路连接问题。这样的差别是巨大的,因为可能的计划数的是按照指数增长的。 如果有超过from_collapse_limitFROM项将会导致父查询,规划器将尝试通过停止提升子查询来避免卡在巨大的连接搜索问题中。你可以通过调高或调低这个运行时参数在规划时间和计划的质量之间取得平衡。

from_collapse_limit和join_collapse_limit的命名相似,因为它们做的几乎是同一件事:一个控制规划器何时将把子查询"平面化",另外一个控制何时把显式连接平面化。通常,你要么把join_collapse_limit设置成和from_collapse_limit一样(这样显式连接和子查询的行为类似), 要么把join_collapse_limit设置为 1(如果你想用显式连接控制连接顺序)。 但是你可以把它们设置成不同的值,这样你就可以细粒度地调节规划时间和运行时间之间的平衡。

四、填充一个数据库

第一次填充数据库时可能需要插入大量的数据。本节包含一些如何让这个处理尽可能高效的建议。

4.1 禁用自动提交

在使用多个INSERT时,关闭自动提交并且只在最后做一次提交(在普通 SQL 中,这意味着在开始发出BEGIN并且在结束时发出COMMIT。某些客户端库可能背着你就做了这些,在这种情况下你需要确定在你需要做这些时该库确实帮你做了)。如果你允许每一个插入都被独立地提交,PostgreSQL要为每一个被增加的行做很多工作。在一个事务中做所有插入的一个额外好处是:如果一个行的插入失败则所有之前插入的行都会被回滚,这样你不会被卡在部分载入的数据中。

4.2 使用 COPY

使用COPY在一条命令中装载所有记录,而不是一系列INSERT命令。 COPY命令是为装载大量行而优化过的; 它没INSERT那么灵活,但是在大量数据装载时导致的负荷也更少。 因为COPY是单条命令,因此使用这种方法填充表时无须关闭自动提交。

如果你不能使用COPY,那么使用PREPARE来创建一个预备INSERT语句也有所帮助,然后根据需要使用EXECUTE多次。这样就避免了重复分析和规划INSERT的负荷。不同接口以不同的方式提供该功能, 可参阅接口文档中的"预备语句"。

请注意,在载入大量行时,使用COPY几乎总是比使用INSERT快, 即使使用了PREPARE并且把多个插入被成批地放入一个单一事务。

同样的事务中,COPY比更早的CREATE TABLETRUNCATE命令更快。 在这种情况下,不需要写 WAL,因为在一个错误的情况下,包含新载入数据的文件不管怎样都将被移除。不过,只有当wal_level设置为minimal(此时所有的命令必须写 WAL)时才会应用这种考虑。

4.3 移除索引

如果你正在载入一个新创建的表,最快的方法是创建该表,用COPY批量载入该表的数据,然后创建表需要的任何索引。在已存在数据的表上创建索引要比在每一行被载入时增量地更新它更快。

如果你正在对现有表增加大量的数据,删除索引、载入表然后重新创建索引可能是最好的方案。 当然,在缺少索引的期间,其它数据库用户的数据库性能将会下降。 我们在删除唯一索引之前还需要仔细考虑清楚,因为唯一约束提供的错误检查在缺少索引的时候会丢失。

4.4 移除外键约束

和索引一样,"成批地"检查外键约束比一行行检查效率更高。 因此,先删除外键约束、载入数据然后重建约束会很有用。 同样,载入数据和约束缺失期间错误检查的丢失之间也存在平衡。

更重要的是,当你在已有外键约束的情况下向表中载入数据时, 每个新行需要一个在服务器的待处理触发器事件(因为是一个触发器的触发会检查行的外键约束)列表的条目。载入数百万行会导致触发器事件队列溢出可用内存, 造成不能接受的交换或者甚至是命令的彻底失败。因此在载入大量数据时,可能需要(而不仅仅是期望)删除并重新应用外键。如果临时移除约束不可接受,那唯一的其他办法可能是就是将载入操作分解成更小的事务。

4.5 增加 maintenance_work_mem

在载入大量数据时,临时增大​​​​​​maintenance_work_mem配置变量可以改进性能。这个参数也可以帮助加速CREATE INDEX命令和ALTER TABLE ADD FOREIGN KEY命令。 它不会对COPY本身起很大作用,所以这个建议只有在你使用上面的一个或两个技巧时才有用。

4.6 增加 max_wal_size

临时增大​​​​​​​max_wal_size配置变量也可以让大量数据载入更快。 这是因为向PostgreSQL中载入大量的数据将导致检查点的发生比平常(由checkpoint_timeout配置变量指定)更频繁。无论何时发生一个检查点时,所有脏页都必须被刷写到磁盘上。 通过在批量数据载入时临时增加max_wal_size,所需的检查点数目可以被缩减。

4.7 禁用 WAL 归档和流复制

当使用 WAL 归档或流复制向一个安装中载入大量数据时,在录入结束后执行一次新的基础备份比处理大量的增量 WAL 数据更快。为了防止载入时记录增量 WAL,通过将wal_level设置为minimal、将archive_mode设置为off以及将max_wal_senders设置为零来禁用归档和流复制。 但需要注意的是,修改这些设置需要重启服务。

除了避免归档器或 WAL 发送者处理 WAL 数据的时间之外,这样做将实际上使某些命令更快, 因为它们被设计为在wal_levelminimal时完全不写 WAL (通过在最后执行一个fsync而不是写 WAL,它们能以更小地代价保证崩溃安全)。这适用于下列命令:

  • CREATE TABLE AS SELECT

  • CREATE INDEX(以及类似 ALTER TABLE ADD PRIMARY KEY的变体)

  • ALTER TABLE SET TABLESPACE

  • CLUSTER

  • COPY FROM,当目标表已经被创建或者在同一个事务的早期被截断

4.8 事后运行ANALYZE

不管什么时候你显著地改变了表中的数据分布后,我们都强烈推荐运行ANALYZE。着包括向表中批量载入大量数据。运行ANALYZE(或者VACUUM ANALYZE)保证规划器有表的最新统计信息。 如果没有统计数据或者统计数据过时,那么规划器在查询规划时可能做出很差劲决定,导致在任意表上的性能低下。需要注意的是,如果启用了 autovacuum 守护进程,它可能会自动运行ANALYZE。

4.9 关于pg_dump的一些注记

pg_dump生成的转储脚本自动应用上面的若干个(但不是全部)技巧。 要尽可能快地载入pg_dump转储,你需要手工做一些额外的事情(请注意,这些要点适用于恢复 一个转储,而不是创建它的时候。同样的要点也适用于使用psql载入一个文本转储或用pg_restore从一个pg_dump归档文件载入)。

默认情况下,pg_dump使用COPY,并且当它在生成一个完整的模式和数据转储时, 它会很小心地先装载数据,然后创建索引和外键。因此在这种情况下,一些指导方针是被自动处理的。你需要做的是:

  • maintenance_work_memmax_wal_size设置适当的(即比正常值大的)值。

  • 如果使用 WAL 归档或流复制,在转储时考虑禁用它们。在载入转储之前,可通过将archive_mode设置为off、将wal_level设置为minimal以及将max_wal_senders设置为零(在录入dump前)来实现禁用。 之后,将它们设回正确的值并执行一次新的基础备份。

  • 采用pg_dump和pg_restore的并行转储和恢复模式进行实验并且找出要使用的最佳并发任务数量。通过使用-j选项的并行转储和恢复应该能为你带来比串行模式高得多的性能。

  • 考虑是否应该在一个单一事务中恢复整个转储。要这样做,将-1--single-transaction命令行选项传递给psql或pg_restore。 当使用这种模式时,即使是一个很小的错误也会回滚整个恢复,可能会丢弃已经处理了很多个小时的工作。根据数据间的相关性, 可能手动清理更好。如果你使用一个单一事务并且关闭了 WAL 归档,COPY命令将运行得最快。

  • 如果在数据库服务器上有多个 CPU 可用,可以考虑使用pg_restore的--jobs选项。这允许并行数据载入和索引创建。

  • 之后运行ANALYZE

一个只涉及数据的转储仍将使用COPY,但是它不会删除或重建索引,并且它通常不会触碰外键。 因此当载入一个只有数据的转储时,如果你希望使用那些技术,你需要负责删除并重建索引和外键。在载入数据时增加max_wal_size仍然有用,但是不要去增加maintenance_work_mem;不如说在以后手工重建索引和外键时你已经做了这些。并且不要忘记在完成后执行ANALYZE。

你可以通过使用--disable-triggers选项的方法获得禁用外键的效果 --- 不过你要意识到这么做是消除(而不只是推迟)外键验证。因此如果你使用该选项,就可能插入坏数据。

五、非持久设置

持久性是数据库的一个保证已提交事务的记录的特性(即使是发生服务器崩溃或断电)。 然而,持久性会明显增加数据库的负荷,因此如果你的站点不需要这个保证,PostgreSQL可以被配置成运行更快。在这种情况下,你可以调整下列配置来提高性能。除了下面列出的,在数据库软件崩溃的情况下也能保证持久性。当这些设置被使用时,只有突然的操作系统停止会产生数据丢失或损坏的风险。

  • 将数据库集簇的数据目录放在一个内存支持的文件系统上(即RAM磁盘)。这消除了所有的数据库磁盘 I/O,但将数据存储限制到可用的内存量(可能有交换区)。

  • 关闭fsync;不需要将数据刷入磁盘。

  • 关闭synchronous_commit;可能不需要在每次提交时 强制把WAL写入磁盘。这种设置可能会在 数据库崩溃时带来事务丢失的风险(但是没有数据破坏)。

  • 关闭full_page_writes;不需要警惕部分页面写入。

  • 增加max_wal_size和checkpoint_timeout; 这会降低检查点的频率,但会 增加/pg_wal的存储要求。

  • 创建 不做日志的表来避免WAL写入,不过这会让表在崩溃时不安全。

相关推荐
伏虎山真人13 分钟前
开源数据库 - mysql - mysql-server-8.4(gtid主主同步+ keepalived热切换)部署方案
数据库·mysql·开源
FIN技术铺3 小时前
Redis集群模式之Redis Sentinel vs. Redis Cluster
数据库·redis·sentinel
CodingBrother4 小时前
MySQL 中的 `IN`、`EXISTS` 区别与性能分析
数据库·mysql
代码小鑫5 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
小小不董5 小时前
Oracle OCP认证考试考点详解082系列16
linux·运维·服务器·数据库·oracle·dba
甄臻9245 小时前
Windows下mysql数据库备份策略
数据库·mysql
内蒙深海大鲨鱼5 小时前
qt之ui开发
数据库·qt·ui
不爱学习的YY酱5 小时前
【计网不挂科】计算机网络第一章< 概述 >习题库(含答案)
java·数据库·计算机网络
Mephisto.java5 小时前
【大数据学习 | HBASE高级】storeFile文件的合并
大数据·sql·oracle·json·hbase·database
这样の我5 小时前
hbase集成phoenix
大数据·数据库·hbase