扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
PostgreSQL命令 EXPLAIN ANALYZE 是日常工作中了解和优化SQL查询过程所用到的最强大工具,后接如 SELECT ... , UPDATE ... 或者 DELETE ... 等SQL语句,命令执行后并不返回数据,而是输出查询计划,详细说明规划器通过何种方式来执行给定的SQL语句。
创新互联专注于眉县网站建设服务及定制,我们拥有丰富的企业做网站经验。 热诚为您提供眉县营销型网站建设,眉县网站制作、眉县网页设计、眉县网站官网定制、重庆小程序开发服务,打造眉县网络公司原创品牌,更为您提供眉县网站排名全网营销落地服务。
下面是从 Postgres Using EXPLAIN 提取的查询:
它生成的查询计划:
Postgres构建了一个规划节点的树结构,以表示所采取的不同操作,其中root根和每个 - 指向其中一个操作。在某些情况下, EXPLAIN ANALYZE 会提供除执行时间和行数之外的额外执行统计信息,例如上面例子中的 Sort 及 Hash 。除第一个没有 - 的行之外的任何行都是诸如此类的信息,因此查询的结构是:
每个树分支代表子动作,从里到外以确定哪个是“第一个”发生(尽管同一级别的节点顺序可能不同)。
在 tenk_unique1 索引上执行的第一个操作是 Bitmap Index Scan :
这对应于SQL WHERE t1.unique1 100 。Postgres查找与条件 unique1 100 匹配的行位置。此处不会返回行数据本身。成本估算 (cost=0.00..5.04 rows=101 width=0) 意味着Postgres预期将“花费” 任意计算单位的 5.04 来找到这些行。0.00是此节点开始工作的成本(在这种情况下,即为查询的启动时间)。 rows 是此索引扫描将返回的预估行数, width 是这些返回行的预估大小(以字节为单位)(0是因为这里只关心位置,而不是行数据的内容)。
因为使用了 ANALYZE 选项运行 EXPLAIN ,所以查询被实际执行并捕获了计时信息。 (actual time=0.049..0.049 rows=100 loops=1) 表示索引扫描执行了1次( loops 值),结果返回了100行,实际时间是0 ..如果节点执行了多次,实际时间是每次迭代的平均值,可以将该值乘以循环次数以获取实际时间。基于成本的最小/最大时间的概念,范围值也可能会有所不同。通过这些值,我们可以为该查询生成一个成本比率,每个成本单位为0.049ms / 5.04单位≈0.01ms/单位。
索引扫描的结果将传递给 Bitmap Heap Scan 操作。在此节点中,Postgres将获取别名为t1的tenk1表中行的位置,根据 unique1 100 条件筛选并获取行。
当乘以之前计算的0.01值时,我们可以得到成本预期的大概时间(229.20 - 5.07)*0.01≈2.24ms,同时每行实际时间为除以4后的结果:0.526ms。这可能是因为成本估算是取的上限而不是取所有需读取的行,也或者因为Recheck条件总是生效。
和表顺序读取行(a Seq Scan )相比, Bitmap Index Scan 和 Bitmap Heap Scan 关联操作成本要昂贵得多,但是因为在这种情况下只需要访问相对较少的行,所以关联操作最终会变得更快。通过在获取行之前将行按照物理顺序排序来进一步加速,这会将单独获取的成本降到最低。节点名称中的“Bitmap”完成了排序操作。
表扫描的结果(tenk1表中满足 unique1 100 条件的那些行)将在读取时被插入到内存的哈希表中。正如我们从成本中看到的那样,这根本不需要时间。
哈希节点包括散列桶(hash buckets)和批次数(batches)相关的信息,以及内存使用峰值情况。如果批次 1,则还会包括未显示的磁盘使用信息。内存占用在100行* 244字节= 24.4 kB时是有意义的,它非常接近28kB,我们假定这是哈希键本身所占用的内存。
接下来,Postgres从别名为t2的tenk2表读取所有的10000行,并根据tenk1表行的Hash检查它们。散列连接意味着将一个表的行输入到内存中的散列(先前的操作中已构建),之后扫描另一个表的行,并根据散列表探测其值以进行匹配。在第二行可以看到“匹配”的条件, Hash Cond: (t2.unique2 = t1.unique2) 。请注意,因为查询是从tenk1和tenk2中选择所有值,所以在散列连接期间每行的宽度加倍。
现在已经收集了满足条件的所有行,可以对结果集进行排序 Sort Key: t1.fivethous 。
Sort节点包含排序算法 quicksort 相关的信息 ,排序是在内存中还是在磁盘上完成(这将极大地影响速度),以及排序所需的内存/磁盘空间量。
熟悉如何解读查询计划会非常有助于优化查询。例如,Seq Scan节点通常表示添加索引的必要性,读取速度可能要快得多。
翻译并编辑,原文出处:
一、使用EXPLAIN:
PostgreSQL为每个查询都生成一个查询规划,因为选择正确的查询路径对性能的影响是极为关键的。PostgreSQL本身已经包含了一个规划器用于寻找最优规划,我们可以通过使用EXPLAIN命令来查看规划器为每个查询生成的查询规划。
PostgreSQL中生成的查询规划是由1到n个规划节点构成的规划树,其中最底层的节点为表扫描节点,用于从数据表中返回检索出的数据行。然而,不同
的扫描节点类型代表着不同的表访问模式,如:顺序扫描、索引扫描,以及位图索引扫描等。如果查询仍然需要连接、聚集、排序,或者是对原始行的其它操作,那
么就会在扫描节点"之上"有其它额外的节点。并且这些操作通常都有多种方法,因此在这些位置也有可能出现不同的节点类型。EXPLAIN将为规划树中的每
个节点都输出一行信息,显示基本的节点类型和规划器为执行这个规划节点计算出的预计开销值。第一行(最上层的节点)是对该规划的总执行开销的预计,这个数
值就是规划器试图最小化的数值。
这里有一个简单的例子,如下:
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1;
QUERY PLAN
-------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244)
EXPLAIN引用的数据是:
1). 预计的启动开销(在输出扫描开始之前消耗的时间,比如在一个排序节点里做排续的时间)。
2). 预计的总开销。
3). 预计的该规划节点输出的行数。
4). 预计的该规划节点的行平均宽度(单位:字节)。
这里开销(cost)的计算单位是磁盘页面的存取数量,如1.0将表示一次顺序的磁盘页面读取。其中上层节点的开销将包括其所有子节点的开销。这里的输出
行数(rows)并不是规划节点处理/扫描的行数,通常会更少一些。一般而言,顶层的行预计数量会更接近于查询实际返回的行数。
现在我们执行下面基于系统表的查询:
复制代码 代码如下:
SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';
从查询结果中可以看出tenk1表占有358个磁盘页面和10000条记录,然而为了计算cost的值,我们仍然需要知道另外一个系统参数值。
复制代码 代码如下:
postgres=# show cpu_tuple_cost;
cpu_tuple_cost
----------------
0.01
(1 row)
cost = 358(磁盘页面数) + 10000(行数) * 0.01(cpu_tuple_cost系统参数值)
下面我们再来看一个带有WHERE条件的查询规划。
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 7000;
QUERY PLAN
------------------------------------------------------------
Seq Scan on tenk1 (cost=0.00..483.00 rows=7033 width=244)
Filter: (unique1 7000)
EXPLAIN的输出显示,WHERE子句被当作一个"filter"应用,这表示该规划节点将扫描表中的每一行数据,之后再判定它们是否符合过滤的条
件,最后仅输出通过过滤条件的行数。这里由于WHERE子句的存在,预计的输出行数减少了。即便如此,扫描仍将访问所有10000行数据,因此开销并没有
真正降低,实际上它还增加了一些因数据过滤而产生的额外CPU开销。
上面的数据只是一个预计数字,即使是在每次执行ANALYZE命令之后也会随之改变,因为ANALYZE生成的统计数据是通过从该表中随机抽取的样本计算的。
如果我们将上面查询的条件设置的更为严格一些的话,将会得到不同的查询规划,如:
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 100;
QUERY PLAN
------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=2.37..232.35 rows=106 width=244)
Recheck Cond: (unique1 100)
- Bitmap Index Scan on tenk1_unique1 (cost=0.00..2.37 rows=106 width=0)
Index Cond: (unique1 100)
这里,规划器决定使用两步规划,最内层的规划节点访问一个索引,找出匹配索引条件的行的位置,然后上层规划节点再从表里读取这些行。单独地读取数据行比顺
序地读取它们的开销要高很多,但是因为并非访问该表的所有磁盘页面,因此该方法的开销仍然比一次顺序扫描的开销要少。这里使用两层规划的原因是因为上层规
划节点把通过索引检索出来的行的物理位置先进行排序,这样可以最小化单独读取磁盘页面的开销。节点名称里面提到的"位图(bitmap)"是进行排序的机
制。
现在我们还可以将WHERE的条件设置的更加严格,如:
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 3;
QUERY PLAN
------------------------------------------------------------------------------
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..10.00 rows=2 width=244)
Index Cond: (unique1 3)
在该SQL中,表的数据行是以索引的顺序来读取的,这样就会令读取它们的开销变得更大,然而事实上这里将要获取的行数却少得可怜,因此没有必要在基于行的物理位置进行排序了。
现在我们需要向WHERE子句增加另外一个条件,如:
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 3 AND stringu1 = 'xxx';
QUERY PLAN
------------------------------------------------------------------------------
Index Scan using tenk1_unique1 on tenk1 (cost=0.00..10.01 rows=1 width=244)
Index Cond: (unique1 3)
Filter: (stringu1 = 'xxx'::name)
新增的过滤条件stringu1 = 'xxx'只是减少了预计输出的行数,但是并没有减少实际开销,因为我们仍然需要访问相同数量的数据行。而该条件并没有作为一个索引条件,而是被当成对索引结果的过滤条件来看待。
如果WHERE条件里有多个字段存在索引,那么规划器可能会使用索引的AND或OR的组合,如:
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 100 AND unique2 9000;
QUERY PLAN
-------------------------------------------------------------------------------------
Bitmap Heap Scan on tenk1 (cost=11.27..49.11 rows=11 width=244)
Recheck Cond: ((unique1 100) AND (unique2 9000))
- BitmapAnd (cost=11.27..11.27 rows=11 width=0)
- Bitmap Index Scan on tenk1_unique1 (cost=0.00..2.37 rows=106 width=0)
Index Cond: (unique1 100)
- Bitmap Index Scan on tenk1_unique2 (cost=0.00..8.65 rows=1042 width=0)
Index Cond: (unique2 9000)
这样的结果将会导致访问两个索引,与只使用一个索引,而把另外一个条件只当作过滤器相比,这个方法未必是更优。
现在让我们来看一下基于索引字段进行表连接的查询规划,如:
复制代码 代码如下:
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 100 AND t1.unique2 = t2.unique2;
QUERY PLAN
--------------------------------------------------------------------------------------
Nested Loop (cost=2.37..553.11 rows=106 width=488)
- Bitmap Heap Scan on tenk1 t1 (cost=2.37..232.35 rows=106 width=244)
Recheck Cond: (unique1 100)
- Bitmap Index Scan on tenk1_unique1 (cost=0.00..2.37 rows=106 width=0)
Index Cond: (unique1 100)
- Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.00..3.01 rows=1 width=244)
Index Cond: ("outer".unique2 = t2.unique2)
从查询规划中可以看出(Nested
Loop)该查询语句使用了嵌套循环。外层的扫描是一个位图索引,因此其开销与行计数和之前查询的开销是相同的,这是因为条件unique1
100发挥了作用。 这个时候t1.unique2 =
t2.unique2条件子句还没有产生什么作用,因此它不会影响外层扫描的行计数。然而对于内层扫描而言,当前外层扫描的数据行将被插入到内层索引扫描
中,并生成类似的条件t2.unique2 = constant。所以,内层扫描将得到和EXPLAIN SELECT * FROM tenk2
WHERE unique2 = 42一样的计划和开销。最后,以外层扫描的开销为基础设置循环节点的开销,再加上每个外层行的一个迭代(这里是 106
* 3.01),以及连接处理需要的一点点CPU时间。
如果不想使用嵌套循环的方式来规划上面的查询,那么我们可以通过执行以下系统设置,以关闭嵌套循环,如:
复制代码 代码如下:
SET enable_nestloop = off;
EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 100 AND t1.unique2 = t2.unique2;
QUERY PLAN
------------------------------------------------------------------------------------------
Hash Join (cost=232.61..741.67 rows=106 width=488)
Hash Cond: ("outer".unique2 = "inner".unique2)
- Seq Scan on tenk2 t2 (cost=0.00..458.00 rows=10000 width=244)
- Hash (cost=232.35..232.35 rows=106 width=244)
- Bitmap Heap Scan on tenk1 t1 (cost=2.37..232.35 rows=106 width=244)
Recheck Cond: (unique1 100)
- Bitmap Index Scan on tenk1_unique1 (cost=0.00..2.37 rows=106 width=0)
Index Cond: (unique1 100)
这个规划仍然试图用同样的索引扫描从tenk1里面取出符合要求的100行,并把它们存储在内存中的散列(哈希)表里,然后对tenk2做一次全表顺序扫
描,并为每一条tenk2中的记录查询散列(哈希)表,寻找可能匹配t1.unique2 =
t2.unique2的行。读取tenk1和建立散列表是此散列联接的全部启动开销,因为我们在开始读取tenk2之前不可能获得任何输出行。
此外,我们还可以用EXPLAIN ANALYZE命令检查规划器预估值的准确性。这个命令将先执行该查询,然后显示每个规划节点内实际运行时间,以及单纯EXPLAIN命令显示的预计开销,如:
复制代码 代码如下:
EXPLAIN ANALYZE SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 100 AND t1.unique2 = t2.unique2;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=2.37..553.11 rows=106 width=488) (actual time=1.392..12.700 rows=100 loops=1)
- Bitmap Heap Scan on tenk1 t1 (cost=2.37..232.35 rows=106 width=244) (actual time=0.878..2.367 rows=100 loops=1)
Recheck Cond: (unique1 100)
- Bitmap Index Scan on tenk1_unique1 (cost=0.00..2.37
rows=106 width=0) (actual time=0.546..0.546 rows=100 loops=1)
Index Cond: (unique1 100)
- Index Scan using tenk2_unique2 on tenk2 t2
(cost=0.00..3.01 rows=1 width=244) (actual time=0.067..0.078 rows=1
loops=100)
Index Cond: ("outer".unique2 = t2.unique2)
Total runtime: 14.452 ms
注意"actual time"数值是以真实时间的毫秒来计算的,而"cost"预估值是以磁盘页面读取数量来计算的,所以它们很可能是不一致的。然而我们需要关注的只是两组数据的比值是否一致。
在一些查询规划里,一个子规划节点很可能会运行多次,如之前的嵌套循环规划,内层的索引扫描会为每个外层行执行一次。在这种情况下,"loops"将报告
该节点执行的总次数,而显示的实际时间和行数目则是每次执行的平均值。这么做的原因是令这些真实数值与开销预计显示的数值更具可比性。如果想获得该节点所
花费的时间总数,计算方式是用该值乘以"loops"值。
EXPLAIN ANALYZE显示的"Total runtime"包括执行器启动和关闭的时间,以及结果行处理的时间,但是它并不包括分析、重写或者规划的时间。
如果EXPLAIN命令仅能用于测试环境,而不能用于真实环境,那它就什么用都没有。比如,在一个数据较少的表上执行EXPLAIN,它不能适用于数量很
多的大表,因为规划器的开销计算不是线性的,因此它很可能对大些或者小些的表选择不同的规划。一个极端的例子是一个只占据一个磁盘页面的表,在这样的表
上,不管它有没有索引可以使用,你几乎都总是得到顺序扫描规划。规划器知道不管在任何情况下它都要进行一个磁盘页面的读取,所以再增加几个磁盘页面读取用
以查找索引是毫无意义的。
二、批量数据插入:
有以下几种方法用于优化数据的批量插入。
1. 关闭自动提交:
在批量插入数据时,如果每条数据都被自动提交,当中途出现系统故障时,不仅不能保障本次批量插入的数据一致性,而且由于有多次提交操作的发生,整个插入效
率也会受到很大的打击。解决方法是,关闭系统的自动提交,并且在插入开始之前,显示的执行begin
transaction命令,在全部插入操作完成之后再执行commit命令提交所有的插入操作。
2. 使用COPY:
使用COPY在一条命令里装载所有记录,而不是一系列的INSERT命令。COPY命令是为装载数量巨大的数据行优化过的,它不像INSERT命令那样灵
活,但是在装载大量数据时,系统开销也要少很多。因为COPY是单条命令,因此在填充表的时就没有必要关闭自动提交了。
3. 删除索引:
如果你正在装载一个新创建的表,最快的方法是创建表,用COPY批量装载,然后创建表需要的任何索引。因为在已存在数据的表上创建索引比维护逐行增加要快。当然在缺少索引期间,其它有关该表的查询操作的性能将会受到一定的影响,唯一性约束也有可能遭到破坏。
4. 删除外键约束:
和索引一样,"批量地"检查外键约束比一行行检查更加高效。因此,我们可以先删除外键约束,装载数据,然后在重建约束。
5. 增大maintenance_work_mem:
在装载大量数据时,临时增大maintenance_work_mem系统变量的值可以改进性能。这个系统参数可以提高CREATE
INDEX命令和ALTER TABLE ADD FOREIGN KEY命令的执行效率,但是它不会对COPY操作本身产生多大的影响。
6. 增大checkpoint_segments:
临时增大checkpoint_segments系统变量的值也可以提高大量数据装载的效率。这是因为在向PostgreSQL装载大量数据时,将会导致
检查点操作(由系统变量checkpoint_timeout声明)比平时更加频繁的发生。在每次检查点发生时,所有的脏数据都必须flush到磁盘上。
通过提高checkpoint_segments变量的值,可以有效的减少检查点的数目。
7. 事后运行ANALYZE:
在增加或者更新了大量数据之后,应该立即运行ANALYZE命令,这样可以保证规划器得到基于该表的最新数据统计。换句话说,如果没有统计数据或者统计数据太过陈旧,那么规划器很可能会选择一个较差的查询规划,从而导致查询效率过于低下。
客户新上线了一套监控系统,可以监控到所有的执行慢的SQL,监控到有个批量任务导入大量数据后,进行索引创建。耗时需要几分钟,虽然不影响业务,但是需要整改。
这种问题的处理思路,都是大拆小,搞并发。
在测试环境,创建一个大表进行测试,创建大量的假数据。
创建表
随机字符串生成函数
生成大量的数据
经测试发现这种方法创建数据太慢了,改成使用COPY的方式创建数据。
排查发现random_string效率太低,生成一条数据接近1ms
重新创建表
写程序创建8600万条数据放在test.csv中
导入大量数据
测试基准数据
\timing 开启计时
耗时532824.434 ms
耗时385838.893 ms,提升 38%的性能,非常不错,但是远远不够。
仍然会出发告警。
重新创建表
创建分区
并发创建INDEX,并记录每个分区索引创建的开始时间和结束时间;
耗时 = 最大结束时间 - 最小开始时间 = 137 s,速度提升接近4倍。
顺序创建INDEX,并记录每个分区索引创建的开始时间和结束时间;
耗时 = 每个索引的耗时相加 = 457358.14 ms,速度提升 16.5%
顺序创建INDEX,优化并发
耗时 = 每个索引的耗时相加 = 292027.642 ms, 速度提升接近两倍。
在开启了并发参数的情况下,如果再叠加并发分区INDEX创建,会不会有惊喜呢?
并发创建INDEX,并记录每个分区索引创建的开始时间和结束时间;
耗时 = 最大结束时间 - 最小开始时间 = 141 s,速度还不如默认并发参数下的表现。应该是资源发生争抢导致的,通过系统监控发现CPU已经打满了。
分区并发是目前能想到的最优化手段了。
还需要结合查询的情况进行分析,分区会带来一点点的性能下降是否影响也需要考虑一下。
分区时目前能避开监控报警的唯一手段了,另外还钻了监控报警的空子。
客户的监控是基于单条语句的,单个分区的最大创建时间为47s,控制在分钟以内了。
并行查询使用多个后台进程,但后端进程基本上处理连接的客户端发出的所有查询。改后端有五个子系统组成。
解析器生成一个解析树,后续子系统可以从纯文本的 SQL 语句中读取该解析树。
如下面的查询:
解析树是其根节点是定义在 parsenodes.h中的 [SelectStmt](javascript:void(0))结构的树。
SELECT 查询的元素和解析树的相应元素编号相同。例如,(1) 是第一个目标列表的一个项目,它是表的“id”列,(4) 是 WHERE 子句,依此类推。
由于解析器在生成解析树时只检查输入的语法,因此只有在查询中出现语法错误时才会返回错误。
解析器不检查输入查询的语义。例如,即使查询包含不存在的表名,解析器也不会返回错误。语义检查由分析器/分析器完成。
分析器运行由解析器生成的解析树的语义分析并生成查询树。
查询树的根是定义在 parsenodes.h中的 [查询](javascript:void(0))结构;此结构包含其相应查询的元数据,例如此命令的类型(SELECT、INSERT 或其他)和几个叶子;每个叶子形成一个列表或树,并保存各个特定子句的数据。
述查询树简述如下。
重写器是实现 规则系统 的系统,必要时根据存储在 pg_rules系统目录中的规则变换查询树。
PostgreSQL 中的视图 是使用规则系统实现的。当视图由 CREATE VIEW 命令定义时,相应的规则会自动生成并存储在目录中。
假设已经定义了以下视图,并且对应的规则存储在 pg_rules 系统目录中。
当发出包含如下所示视图的查询时,解析器将创建解析树,如图所示。
在这个阶段,重写器将范围表节点处理为子查询的解析树,即对应的视图,存储在 pg_rules 中。
计划器从重写器接收查询树并生成可以由执行器最有效地处理的(查询)计划树。
PostgreSQL 中的计划器是基于纯成本优化的;它不支持基于规则的优化和提示。这个规划器是 RDBMS 中最复杂的子系统
与其他 RDBMS 一样,PostgreSQL 中的 EXPLAIN命令显示计划树本身。 如下所示。
他对应的计划树:
每个计划节点都有执行器需要处理的信息,单表查询的情况下,执行器从计划树的末端到根进行处理。
PostgreSQL 的查询优化是基于成本的。成本是无量纲值,它们不是绝对的绩效指标,而是比较运营相对绩效的指标。成本由 costsize.c 中定义的函数估算。执行器执行的所有操作都有相应的成本函数。例如,顺序扫描和索引扫描的成本分别由 cost_seqscan() 和 cost_index() 估算。
有三种成本,启动成本,执行成本以及总成本。其中总成本 = 启动成本 + 执行成本。
顺序扫描的成本由 cost_seqscan() 函数估算。
其中 seq_page_cost 、 cpu_tuple_cost 和 cpu_operator_cost 在 postgresql.conf 文件中设置,默认值分别为 1.0 、 0.01 和 0.0025 ,Ntuple和Npage分别是该表的所有元组和所有页的编号。
从运行成本估算可以看出,PostgreSQL 假设所有页面都将从存储中读取;也就是说,PostgreSQL 不考虑扫描的页面是否在共享缓冲区中。
虽然 PostgreSQL 支持 一些索引方法 ,例如 BTree、 GiST 、 GIN 和 BRIN ,但索引扫描的成本是使用常见的成本函数估算的:cost_index()。
索引扫描的启动成本是读取索引页以访问目标表中第一个元组的成本,它由以下等式定义:
Hindex是索引树的高度。
索引扫描的运行成本是表和索引的 cpu 成本和 IO(输入/输出)成本之和:
前三个成本定义如下:
其中 cpu_index_tuple_cost 和 random_page_cost 在 postgresql.conf 文件中设置(默认分别为 0.005 和 4.0); qual_op_cost粗略来说就是指数的评估成本,值为0.0025。选择性选择性是指定WHERE子句对索引的搜索范围的比例;它是一个从 0 到 1 的浮点数
查询谓词的选择率是通过直方图界值与高频值估计的,这些信息都储存在系统目录pg_staticstics中,并可通过pg_stats视图查询。
表中的每一列的高频值都在pg_stats视图的most_common_vals和most_common_freqs中成对存储。
排序路径会在排序操作中被使用。排序操作包括order by、归并连接的预处理操作,以及其他函数。函数cost_sort()用于估计排序操作的代价。如果能在工作内存中放下所有元组,那么排序操作会选用快速排序算法。否则就会创建临时文件,使用文件归并排序算法。
排序路径的启动代价就是对目标表的排序代价,因此代价就是O(Nsort) * Log 2 (Nsort),这里Nsort就是带排序的元组数。排序路径的运行代价就是读取已经排序好的元组的代价,因此代价就是O(Nsort)。
PostgreSQL中的计划器会执行三个步骤:
访问路径是估算代价时的处理单元。比如顺序扫描、索引扫描、排序,以及各种连接操作都有其对应的路径。访问路径只在计划器创建查询计划树的时候使用。最忌本的访问路径数据结构就是relation.h中定义的path结构体,相当于顺序扫描。所有其他的路径访问都基于该结构。
在创建计划树之前,计划器将线对PlannerInfo中的查询书进行一些预处理。预处理有很多步骤,本节值讨论和单表查询处理相关的主要步骤。
计划器对所有可能的访问路径进行代价估计,然后选择代价最小的那个。
在最后一步中,计划器按照代价最小的路径生成一颗计划树。
计划树的根节点是定义在plannodes.h中的Plannedstmt结构,包含19个字段,其中有4个代表性字段:
计划树包含各式各样的计划节点。PlanNode是所有计划节点的基类,其他计划节点都会包含PlanNode结构。比如顺序扫描节点SeqScanNode包含一个PlanNode和一个整型变量scanrelid。PlanNode包含14个字段,下面是7个代表性字段:
在单表查询的例子中,执行器从计划树中取出计划节点,按照自底向上的顺序进行处理,并调用节点相应的处理函数。
每个计划节点都有相应的函数,用于执行节点对应的操作。这些函数在src/backend/executor目录中。
理解执行器如何工作的最好方式,就是阅读explain命令的输出。
我们可以自底向上阅读explain的结果,来看一看执行器是如何工作的。
第六行:首先,执行器通过nodeSeqscan.c中定义的函数执行顺序扫描操作。
第四行:然后,执行器通过nodeSort.c中定义的函数,对顺序扫描的结果进行排序。
执行器在处理查询时会使用工作内存和临时缓冲区,两者都在内存中分配。如果查询无法在内存中完成,就会用到临时文件。
使用带有Analyze选项的explain,待解释的命令会真正执行,并显示实际结果行数、实际执行时间和实际内存使用量。
在第6行,explain命令显示执行器使用了10000KB的临时文件。临时文件会被临时创建在base/pg_tmp子目录中,并遵循如下命令规则:{“pgsql_tmp”}+ {创建本文件的postgres进程pid}.{从0开始的序列号}
比如,临时文件pgsql_tmp8903.5是pid为8903的postgres进程创建的第6个临时文件。
PostgreSQL中支持三种连接操作,分别是嵌套循环连接,归并连接和散列连接。在pg中,嵌套循环连接和归并连接有几种变体。
这三种连接方式都支持pg中所有的连接操作,注入inner join、 left/right outer join、 full outer join等。
循环嵌套连接不需要任何启动代价,因此:start-up cost = 0
运行代价和内外表尺寸的乘积成比例,即run cost是O(Nouter * Ninner), Nouter和Ninner分别是外表和内表的元组条数。run cost的定义如下:
Couter和Cinner分别是内表和外表顺序扫描的代价。
循环嵌套连接的代价总会被估计,但实际中很少会使用这种连接操作,因为它有几种更高效的变体。
在上面描述的循环嵌套连接中,每当读取一条外表中的元组时,都需要扫描内标中的所有元组。位每条外表元组对内标做全表扫描,这一过程代价高昂,pg支持一种物化嵌套循环连接,可以减少内标全表扫描的代价。
在运行嵌套循环连接之前,执行器会使用临时元组存储模块对内表进行一次扫描,将内表元组加载到工作或临时文件中。在处理内表元组时,临时元组存储比缓冲区管理器更为高效,特别是当所有的元组都能放入工作内存中。
qg内部提供了临时元组存储的模块,可用于各种操作,如五花膘、创建混合散列连接的批次等。该模块包含一系列函数,都在tuplestore.c中。这些函数用于从工作内存或临时文件读写元组。该工作内存还是临时文件取决于待存储元组的总数。
上面显示了执行器要进行的操作,执行器对这些计划节点的处理过程如下:
第7行:执行器使用顺序扫描,物化内部表tbl_b。
第4行:执行器执行嵌套循环连接操作,外表是tbl_a,内表是物化的tbl_b。
如果内表上有索引,且该索引能用于搜索满足连接条件的元组,那么计划器在外外表的每条元组搜索内标中的匹配元组时,会考虑使用索引进行直接搜索,以替代顺序扫描。这种变体叫做索引嵌套循环连接,如下图所示。虽然这种变体叫做“索引嵌套循环连接”,但是谁该算法基本上只需要在外表上循环一次,因此连接操作的执行非常高效。
与嵌套循环连接不同的是,归并连接只能用于自然连接与等值连接。
函数initial_cost_merge_join()和final_cost_merge_join()用于估计归并连接的代价。
归并连接的启动成本是内表与外表排序成本之和,因此其启动成本为:
这里Nouter和Ninner分别是外表和内标的元素条数,而运行代价是O(Nouter + Ninner)。
下图是归并连接的示意图。
如果所有元组都可以存储在内存中,那么排序操作就能在内存中进行,否则就是用临时文件。
第9行:执行器对内表tbl_b进行排序,使用顺序扫描(第11行)。
第6行:执行器对外表tbl_a进行排序,使用顺序扫描(第8行)。
第4行:执行器执行归并连接操作,外表是排序好的tbl_a,内表是排好序的tbl_b。
与嵌套循环连接类似,归并连接还支持物化归并连接,物化内表,使内表扫描更为高效。
下面是物化归并连接的explain结果,很容易发现,与普通归并连接的差异是第9行:Materialize。
与归并连接类似,hash连接只能用于自然连接与等值连接。
PostgreSQL中的散列连接的行为因表的大小而异。如果布标足够小(确切的说,内表大小不超过工作内存的25%),那么hash连接就是简单的两阶段内存hash连接,否则将会使用带倾斜批次的混合hash连接。
内存中的hash连接是在work_mem中处理的,在pg中,散列表区域被称作处理批次。一个批处理批次会有多个散列槽,内部称其为桶,桶的数量由nodeHash.c中定义的ExecChooseHashTableSize()函数所确定。桶的数量是2的整数次幂。
内存散列连接有两个阶段,分别是构建阶段和探测阶段。在构建阶段,内存表中的所有元组都会被插入到处理批次中;在探测阶段每条腕表元组都会与处理批次中的内表元组比较,如果满足连接条件,则将两条元组连接起来。
当内表的元组无法全部存储在工作内表中的单个处理批次时,pg使用带倾斜批次的混合散列连接算法,该算法时混合散列连接诶的一种变体。
在第一个构建和探测阶段postgresql准备多个批次,宇通的数目类似,处理批次的数据由函数ExecChooseHashTableSize()决定,也就是2的整数次幂。工作内存中智慧分配一个处理批次,而其他批次都以临时文件的形式创建。属于这些批次的元组将通过临时元组存储功能被写入到相应的文件中。
为了获取最佳计划树,计划器必须考虑各个索引与各种连接方法之间的所有可能组合。如果表的数量超过某个水平,该过程的代价就会因为组合爆炸而变得非常昂贵,以至于根本不可行。
如果表的数量小于12张,计划器可以使用动态规划来获取最佳计划。
本节简单介绍了PG查询逻辑优化中的子查询链接(subLink),以EXISTS子链接为例介绍了子查询链接上拉主函数处理逻辑以及使用gdb跟踪分析。
上一节介绍了ANY子链接,本节介绍了EXISTS子链接.
为便于方便解析,根据日志分析,得出查询树如下图所示:
convert_EXISTS_sublink_to_join函数源码:
相关数据结构
1、Var
XX_one_pos
依赖的函数
simplify_EXISTS_query
OffsetVarNodes
IncrementVarSublevelsUp
IncrementVarSublevelsUp
pull_varnos
contain_vars_of_level
query_tree_walker
OffsetVarNodes_walker
query_or_expression_tree_walker
pull_varnos_walker
contain_vars_of_level_walker
expression_tree_walker
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流