阅读完需:约 26 分钟
http://postgres.cn/docs/15/using-explain.html
在使用数据库时,经常会有这样的疑问:“我的表对应字段已经创建了索引,为什么这个SQL 语句执行还是这么慢?” 虽然数据库SQL 执行慢有很多原因,但是对于PostgreSQL DBA 来说,好像有个共识,遇到用户慢SQL优化的问题,先拿EXPLAIN 命令查看下对应的查询计划,从而可以快速定位慢在哪里。
- index scan、index only scan、bitmap index scan 有啥区别?
- 为什么明明有建立索引,但PG就是不用呢?
- 执行计划中有子查询就一定不好吗?
- hash join、merge join、nestloop join有啥区别?
EXPLAIN 语法
在PostgreSQL 中,EXPLAIN 命令可以输出SQL 语句的查询计划,具体语法如下:
EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
这里 option可以是:
ANALYZE [ boolean ]
VERBOSE [ boolean ]
COSTS [ boolean ]
SETTINGS [ boolean ]
BUFFERS [ boolean ]
WAL [ boolean ]
TIMING [ boolean ]
SUMMARY [ boolean ]
FORMAT { TEXT | XML | JSON | YAML }
-
ANALYZE
选项为TRUE 会实际执行SQL,并获得相应的查询计划,默认为FALSE。如果优化一些修改数据的SQL 需要真实的执行但是不能影响现有的数据,可以放在一个事务中,分析完成后可以直接回滚。 -
VERBOSE
选项为TRUE 会显示查询计划的附加信息,默认为FALSE。附加信息包括查询计划中每个节点(后面具体解释节点的含义)输出的列(Output),表的SCHEMA 信息,函数的SCHEMA 信息,表达式中列所属表的别名,被触发的触发器名称等。 -
COSTS
选项为TRUE 会显示每个计划节点的预估启动代价(找到第一个符合条件的结果的代价)和总代价,以及预估行数和每行宽度,默认为TRUE。 -
BUFFERS
选项为TRUE 会显示关于缓存的使用信息,默认为FALSE。该参数只能与ANALYZE 参数一起使用。缓冲区信息包括共享块(常规表或者索引块)、本地块(临时表或者索引块)和临时块(排序或者哈希等涉及到的短期存在的数据块)的命中块数,更新块数,挤出块数。 -
TIMING
选项为TRUE 会显示每个计划节点的实际启动时间和总的执行时间,默认为TRUE。该参数只能与ANALYZE 参数一起使用。因为对于一些系统来说,获取系统时间需要比较大的代价,如果只需要准确的返回行数,而不需要准确的时间,可以把该参数关闭。 -
SUMMARY
选项为TRUE 会在查询计划后面输出总结信息,例如查询计划生成的时间和查询计划执行的时间。当ANALYZE 选项打开时,它默认为TRUE。 -
FORMAT
指定输出格式,默认为TEXT。各个格式输出的内容都是相同的,其中XML | JSON | YAML 更有利于我们通过程序解析SQL 语句的查询计划
查询计划类似于一棵有节点的树,执行和阅读的顺序是自底而上。计划中的每个节点表示一个操作,例如表扫描、表连接、聚集或者排序。阅读的顺序是从底向上:每个节点会把结果输出给直接在它上面的节点。一个计划中的底层节点通常是表扫描操作:顺序扫描表、通过索引或者位图索引扫描表等。如果该查询要求那些行上的连接、聚集、排序或者其他操作,就会有额外的节点在扫描节点上面负责执行这些操作。
EXPLAIN的输出对于查询计划中的每个节点都显示为一行并显示该节点类型和下面的执行的代价估计:
- cost:以磁盘页面获取为单位度量。1.0等于一次顺序磁盘页面读取。第一个估计是得到第一行的启动代价,第二个估计是得到所有行的总代价。
- rows:这个计划节点输出的总行数。这个数字根据条件的过滤因子会小于被该计划节点处理或者扫描的行数。最顶层节点的是估算的返回、更新或者删除的行数。
- width:这个计划节点输出的所有行的总字节数。
需要注意以下两点:
- 一个节点的代价包括其子节点的代价。最顶层计划节点有对于该计划估计的总执行代价。这是优化器估算出来的最小的数字。
- 代价只反映了在数据库中执行的时间,并没有计算在数据库执行之外的时间,例如将结果行传送到客户端花费的时间。
EXPLAIN SELECT * FROM names WHERE name = 'Joelle';
QUERY PLAN
------------------------------------------------------------
Gather Motion 4:1 (slice1) (cost=0.00..20.88 rows=1 width=13)
-> Seq Scan on 'names' (cost=0.00..20.88 rows=1 width=13)
Filter: name::text ~~ 'Joelle'::text
查询优化器会顺序扫描names表,对每一行检查WHERE语句中的filter条件,只输出满足该条件的行。 扫描操作的结果被传递给一个Gather Motion操作。Gather Motion是Segment把所有行发送给Master节点。在这个例子中,有4个Segment节点会并行执行,并向Master节点发送数据。这个计划估计的启动代价是00.00(没有代价)而总代价是20.88次磁盘页面获取。优化器估计这个查询将返回一行数据。
当EXPLAIN 命令中ANALYZE 选项为on时,会在代价估计信息之后输出真实执行信息,包括:
- actual time 执行时间,格式为xxx…xxx,在… 之前的是该节点实际的启动时间,即找到符合该节点条件的第一个结果实际需要的时间,在…之后的是该节点实际的执行时间
- rows 指的是该节点实际的返回行数
- loops 指的是该节点实际的重启次数。如果一个计划节点在运行过程中,它的相关参数值(如绑定变量)发生了变化,就需要重新运行这个计划节点。
看过例子后,再详细了解一下,执行计划生成的原理。一条SQL从输入到执行完毕,大致会经历如下三个步骤
- 语法分析:词法分析、语法分析、语义分析
- 查询优化:基于规则的优化、基于代价的优化
- 查询执行、数据存取
Explain的输出,是查询优化的最终结果。而查询优化又被分为两个步骤
- 基于规则的优化:即逻辑优化,通过对关系代数表达式进行逻辑上的等价变换,以获得性能更好的计划,比如谓词下推(将上层的过滤条件下推到下层)。
- 基于代价的优化:即物理优化,逻辑优化后,实际的查询路径还是有很多种,PG建立了代价计算模型,计算所有可能路径的代价,选出最优路径。
比如select * from users where id = 1,在扫描方式上,有顺序扫描、索引扫描等。在不同的数据量情况下,按顺序扫描和索引扫描的代价可能是不一样的,因此它的执行计划可能会随数据量的变化而变化。
逻辑优化(基于规则的优化)
基于逻辑的等价变换,可对原始的SQL语句进行优化。逻辑优化的基本原则——将复杂逻辑变为简单逻辑。具体做的工作,大致是提升子查询、表达式预处理、having子句条件下推、group by冗余字段消除、谓词(过滤条件)下推、外连接消除等。
子查询提升
子查询可分为如下两类
- 相关子查询:子查询中引用外层表的列属性,导致外层表的每一条记录,子查询都需要重新执行一次
- 非相关子查询:子查询是独立的,与外层表没有直接的关联,子查询单独执行一次,外层表可以重复利用其结果
通常来说,相关子查询会被提升,非相关子查询由于其本来就只执行一次,因此没有太大必要提升。
-- 相关子查询举例
explain select *, (select label_id from content_to_label where content_id = content.id) as label_id from content;
HotSpotIntrinsicCandidate
Seq Scan on content (cost=0.00..3694.61 rows=1151 width=743)
SubPlan 1
-> Seq Scan on content_to_label (cost=0.00..3.10 rows=3 width=4)
Filter: (content_id = content.id)
-- 非相关子查询举例
explain select *, (select label_id from content_to_label limit 1) as label_id from content;
Seq Scan on content (cost=0.02..126.53 rows=1151 width=743)
InitPlan 1 (returns $0)
-> Limit (cost=0.00..0.02 rows=1 width=4)
-> Seq Scan on content_to_label (cost=0.00..2.68 rows=168 width=4)
相关子查询又可以依据子查询出现的位置分为如下两种——子查询和子连接,他们都能够被提升。
- 子查询语句:出现在FROM关键字后的是子查询语句
- 子连接语句:出现在WHERE/ON等约束条件或SELECT子句中的是子连接语句
提升子连接
子连接是子查询的一种特殊情况,由于它常出现在条件中,因此通常伴随ANY、EXISTS、NOT EXISTS、IN、NOT IN等关键字,PG会对他们尝试做提升,比如
-- 查询那些打过标签的文章
explain select * from content where exists(select 1 from content_to_label where content_id = content.id);
-- 在不优化的情况下,exists子查询会像上面所示,真的是子查询。但这里优化器将子查询做了提升,提升后变成连接,通过将内表hash化,降低算法复杂度
Hash Join (cost=4.43..134.62 rows=59 width=739)
Hash Cond: (content.id = content_to_label.content_id)
-> Seq Scan on content (cost=0.00..126.51 rows=1151 width=739)
-> Hash (cost=3.69..3.69 rows=59 width=4)
-> HashAggregate (cost=3.10..3.69 rows=59 width=4)
Group Key: content_to_label.content_id
-> Seq Scan on content_to_label (cost=0.00..2.68 rows=168 width=4)
能够被提升还有一个前提条件是子查询必须足够简单,上面同样的SQL,子查询投影改成聚集函数,就无法提升
explain select * from content where exists(select sum(content_id) from content_to_label where content_id = content.id);
Seq Scan on content (cost=0.00..3714.75 rows=576 width=739)
Filter: (SubPlan 1)
SubPlan 1
-> Aggregate (cost=3.11..3.12 rows=1 width=8)
-> Seq Scan on content_to_label (cost=0.00..3.10 rows=3 width=4)
Filter: (content_id = content.id)
提升子查询
出现在表位置的子查询,也能够提升,如下
explain select * from content left join (select *, 1 from content_to_label) ctl on content.id = ctl.content_id;
Hash Right Join (cost=140.90..144.02 rows=1151 width=759)
Hash Cond: (content_to_label.content_id = content.id)
-> Seq Scan on content_to_label (cost=0.00..2.68 rows=168 width=20)
-> Hash (cost=126.51..126.51 rows=1151 width=739)
-> Seq Scan on content (cost=0.00..126.51 rows=1151 width=739)
预处理表达式
即将能够事先处理的表达式处理掉,比如常量结算、约束条件的逻辑简化等
- 常量简化即直接计算出SQL中的常量表达式
-- 可以直接计算出101
explain select * from content where id < 1 + 100
Bitmap Heap Scan on content (cost=5.08..123.47 rows=104 width=739)
Recheck Cond: (id < 101)
-> Bitmap Index Scan on content_pkey (cost=0.00..5.06 rows=104 width=0)
Index Cond: (id < 101)
- 谓词规范化(规约、拉平、提取公共项)
- 对无用的约束条件去除
-- false对于或运算是没用的,会被优化器直接去除
explain select * from content where id < 100 or false
Bitmap Heap Scan on content (cost=5.08..123.45 rows=103 width=739)
Recheck Cond: (id < 100)
-> Bitmap Index Scan on content_pkey (cost=0.00..5.05 rows=103 width=0)
Index Cond: (id < 100)
- 约束条件会被拉平
-- 约束条件进行了无谓的括号,会被拉平
explain select * from content where id < 100 or (id < 1000 or id > 2000)
Seq Scan on content (cost=0.00..135.14 rows=978 width=739)
Filter: ((id < 100) OR (id < 1000) OR (id > 2000))
- 约束条件经过逻辑运算后,会被提取公共项
-- 约束条件提取公共项,只剩下id > 1 and id < 2
explain select * from content where (id > 1 and id < 2 and id < 100) or (id > 1 and id < 2);
Index Scan using content_pkey on content (cost=0.28..8.30 rows=1 width=739)
Index Cond: ((id > 1) AND (id < 2))
处理HAVING子句
HAVING子句的优化主要是将部分条件转变为普通的过滤条件,从而减少原始数据的大小。
-- 统计发过发过10篇以上内容且用户id>10的用户
explain select "authorId" from content group by "authorId" having count(1) > 10 and "authorId" > 100;
HashAggregate (cost=132.85..133.50 rows=65 width=4)
Group Key: "authorId"
Filter: (count(1) > 10)
-> Seq Scan on content (cost=0.00..129.39 rows=692 width=4)
Filter: ("authorId" > 100)
GroupBy键值消除
GroupBy子句需要借助排序或哈希实现,如果能减少它后面的字段,就能降低损耗。典型的是如果group by中出现了一个主键和多个不相关的字段,则仅保留主键即可,因为主键唯一不可重复,没有必要再对其他字段进行排序或hash操作了
explain select * from content group by id, type, "authorId";
HashAggregate (cost=129.39..140.90 rows=1151 width=739)
Group Key: id
-> Seq Scan on content (cost=0.00..126.51 rows=1151 width=739)
谓词下推
谓词即筛选条件,将上层的条件下推到基层进行扫描,会带来性能上的提升
-- 过滤条件下推
explain select * from content left join content_to_label ctl on content.id = ctl.content_id where content.id < 100;
Hash Right Join (cost=125.48..128.60 rows=103 width=755)
Hash Cond: (ctl.content_id = content.id)
-> Seq Scan on content_to_label ctl (cost=0.00..2.68 rows=168 width=16)
-> Hash (cost=124.19..124.19 rows=103 width=739)
-> Bitmap Heap Scan on content (cost=5.08..124.19 rows=103 width=739)
Recheck Cond: (id < 100)
-> Bitmap Index Scan on content_pkey (cost=0.00..5.05 rows=103 width=0)
Index Cond: (id < 100)
-- 连接条件下推
explain select * from content left join content_to_label ctl on ctl.content_id < 100;
Nested Loop Left Join (cost=0.00..145.23 rows=1161 width=755)
-> Seq Scan on content (cost=0.00..127.61 rows=1161 width=739)
-> Materialize (cost=0.00..3.10 rows=1 width=16)
-> Seq Scan on content_to_label ctl (cost=0.00..3.10 rows=1 width=16)
Filter: (content_id < 100)
外连接消除
如果两个表是内连接,则他们之间的顺序可以任意交换,会方便谓词下推。而对于外连接,则不会那么方便。如果能够将外连接转换为内连接,则查询过程会简化。能够被转换为内连接的情况如下
-- 常规左外连接
explain select * from content left outer join content_to_label ctl on content.id = ctl.content_id;
Hash Right Join (cost=140.90..144.02 rows=1151 width=755)
Hash Cond: (ctl.content_id = content.id)
-> Seq Scan on content_to_label ctl (cost=0.00..2.68 rows=168 width=16)
-> Hash (cost=126.51..126.51 rows=1151 width=739)
-> Seq Scan on content (cost=0.00..126.51 rows=1151 width=739)
-- 做左外连接,条件上限制可空侧的表格,消除外连接
explain select * from content left outer join content_to_label ctl on content.id = ctl.content_id where ctl.content_id is not null;
Hash Join (cost=140.90..144.02 rows=168 width=755)
Hash Cond: (ctl.content_id = content.id)
-> Seq Scan on content_to_label ctl (cost=0.00..2.68 rows=168 width=16)
Filter: (content_id IS NOT NULL)
-> Hash (cost=126.51..126.51 rows=1151 width=739)
-> Seq Scan on content (cost=0.00..126.51 rows=1151 width=739)
逻辑优化的具体项目当然不止上面列举这几项,但从这几项中也能够看出逻辑优化的思路,即消除多余的逻辑操作,尽量从逻辑上降低实际查询时需要处理的数据量。同时我们也能够看到,子查询不一定不好,只要能够被合理提升、或是非关联子查询,对性能的影响不一定会很大;而你写的哪些看起来有些多余的部分,只要合理,也可能会被在逻辑优化中消除。
物理优化(基于代价的优化)
逻辑优化后得到新的查询树,PG会针对查询树生成各种不同的查询路径,并通过查询代价模型计算比较并得出查询代价最低的路径作为最终的查询计划。
cost计算模型
再数据库运行的实际环境中,用户的硬件环境天差地别,CPU频率、内存大小、磁盘介质等都会直接影响实际执行效率,因此代价估算无法十分准确。比较实际的是依据一套基本靠谱的模型,选出出各路径中代价相对最小的那一个作为最终结果。PG设定了一个相对单位1作为查询预估的最小单位,针对每种路径节点给与不同的单位数量,最终计算总代价的单位数量。
具体来说,代价计算要考虑的点主要如下
IO基准代价
从磁盘中读取数据以page为单位,而顺序读取比随机读取省时。PG默认顺序读取一个页面的代价为1,随机读取一个页面的代价为4
CPU基准代价
从页面中提取出具体的记录
对投影和约束条件中的表达式的计算成本
在PG中相关参数如下
- seq_page_cost:顺序扫描一个页面的IO成本,默认1.0
- random_page_cost:随即扫描一个页面的IO成本,默认4.0
- cpu_tuple_cost:获取一个元组(即记录)的CPU成本,默认0.01
- cpu_index_tuple_cost:通过索引获取一个元组的CPU成本,默认0.005
- cpu_operator_cost:执行一个表达式运算的CPU成本,默认0.0025
- parallel_setup_cost:并行查询时进程间通讯的初始化成本,默认1000.0
- parallel_tuple_cost:并行查询进行间投递元组的成本,默认0.1
查询树
扫描
顺序扫描即数据从头到尾扫描,适合连续的数据读取;不适合分布随机的数据。
索引扫描
Index Scan
先扫描索引,再根据得到的key获取具体数据,涉及到随机读,因此如果索引扫描得到的数据量过大时,大量随机读也会带来很大的性能损失。
-- 通过索引获取少量数据
explain select * from content where id = 100;
Index Scan using content_pkey on content (cost=0.28..8.29 rows=1 width=739)
Index Cond: (id = 100)
Index Only Scan
,获取的目标字段就是索引的字段,因此仅扫描索引即可,即所谓的索引覆盖
-- 直接获取索引的列
explain select id from content where id = 100;
Index Only Scan using content_pkey on content (cost=0.28..8.29 rows=1 width=4)
Index Cond: (id = 100)
位图扫描
针对Index Scan产生的随机读的优化,将索引读取到的key映射到位图,再扫描位图获取连续的key,将随机读变成连续读。出现位图扫描的地方一般会被分为两个步骤:索引映射到位图、通过位图扫描堆,即如下两个节点同时出现
BitmapHeap Scan、BitmapIndex Scan
-- 通过索引获取较多数据
explain select * from content where id < 100;
Bitmap Heap Scan on content (cost=5.08..124.19 rows=103 width=739)
Recheck Cond: (id < 100)
-> Bitmap Index Scan on content_pkey (cost=0.00..5.05 rows=103 width=0)
Index Cond: (id < 100)
上面的查询首先扫描索引,存储到位图(Bitmap Index),然后再通过位图扫描堆(Bitmap Head Scan),扫描时会再次检查条件(Recheck)
一般来说索引覆盖 > 索引扫描 > 索引+位图 > 顺序扫描,但也并非绝对。
如果优化器认为序列扫描的代价小于索引扫描,肯定是使用索引。
比如同一个语句,根据条件范围的不同,也会选择不同的扫描方式。
上面展示了id=100时使用索引扫描;id<100时使用索引+位图扫描;这里展示id>100时的结果
-- 由于表中数据量超过500,这里的约束条件选择了绝大多数记录,因此使用顺序扫描会更划算(索引扫描带来的随机读抵消了它带来的优点)
explain select id from content where id > 100;
Seq Scan on content (cost=0.00..130.51 rows=1058 width=4)
Filter: (id > 100)
连接
Nestlooped Join
即最普通的连接,笛卡尔积,算法复杂度为O(MxN),M和N是待连接表的记录数
explain select * from content, content_to_label ctl;
Nested Loop (cost=0.00..2568.81 rows=195048 width=755)
-> Seq Scan on content (cost=0.00..127.61 rows=1161 width=739)
-> Materialize (cost=0.00..3.52 rows=168 width=16)
-> Seq Scan on content_to_label ctl (cost=0.00..2.68 rows=168 width=16)
Hash Join
对内表先提取一个hash表,对外表的每条记录,都通过hash很快地找到内表记录,从而加快连接速度。哈希连接复杂度为O(M+N)
explain select * from content, content_to_label ctl where content.id = ctl.content_id;
Hash Join (cost=142.12..145.24 rows=168 width=755)
Hash Cond: (ctl.content_id = content.id)
-> Seq Scan on content_to_label ctl (cost=0.00..2.68 rows=168 width=16)
-> Hash (cost=127.61..127.61 rows=1161 width=739)
-> Seq Scan on content (cost=0.00..127.61 rows=1161 width=739)
该计划将content当作内表,先顺序扫描,再提取hash(Hash),将content_to_label当作外表,最后进行哈希连接,连接时会检查连接条件
Merge Join
即归并连接,仅当两个候选表有序时才会使用归并连接。
explain select * from (select * from content order by id) content, (select * from content_to_label order by content_id) ctl where content.id = ctl.content_id order by ctl.content_id;
Merge Join (cost=201.57..217.40 rows=168 width=755)
Merge Cond: (content.id = content_to_label.content_id)
-> Sort (cost=186.71..189.61 rows=1161 width=739)
Sort Key: content.id
-> Seq Scan on content (cost=0.00..127.61 rows=1161 width=739)
-> Materialize (cost=8.89..11.41 rows=168 width=16)
-> Sort (cost=8.89..9.31 rows=168 width=16)
Sort Key: content_to_label.content_id
-> Seq Scan on content_to_label (cost=0.00..2.68 rows=168 width=16)
如上,先对两个备选表排序,然后将排序key作为连接条件,优化器选择了归并连接。
如果待连接表本来就有序,那Merge Join会更好,否则Hash Join好,出现NestLoop Join是绝对要避免的
类别 | Nested Loop | Hash Join | Merge Join |
---|---|---|---|
使用条件 | 任何条件 | 等值连接(=) | 等值或非等值连接(>,<,=,>=,<=),‘<>’除外 |
相关资源 | CPU、磁盘I/O | 内存、临时空间 | 内存、临时空间 |
特点 | 当有高选择性索引或进行限制性搜索时效率比较高,能够快速返回第一次的搜索结果。 | 当缺乏索引或者索引条件模糊时,Hash Join比Nested Loop有效。通常比Merge Join快,如果有索引,或者结果已经被排序了,这时候Merge Join的查询更快。在数据仓库环境下,如果表的纪录数多,效率高。 | 当缺乏索引或者索引条件模糊时,Merge Join比Nested Loop有效。非等值连接时,Merge Join比Hash Join更有效 |
缺点 | 当索引丢失或者查询条件限制不够时,效率很低;当表的纪录数多时,效率低。 | 为建立哈希表,需要大量内存。第一次的结果返回较慢。 | 所有的表都需要排序。它为最优化的吞吐量而设计,并且在结果没有全部找到前不返回数据。 |
Explain案例
常用explain + analyze同时查看计划成本和实际执行时间,注意analyze会导致SQL的确实执行。由于参数越多,出现的噪音越大,因此我的常用的习惯是先explain,再加上analyze查看具体情况。优化的思路是先找有没有一眼就看起来低效的节点,如Nestloop Join,无索引导致的Seq Scan等,其次重点关注cost耗费较多的节点。如下方性能损耗最大的是对content表的顺序扫描。
explain select * from notification
left join content on notification.target_content_id = content.id
left join comments on notification.target_comment_id = comments.id
where (notification.target_content_id is not null and content.state = 'normal')
or (notification.target_comment_id is not null and comments.state = 'normal');
-- explain的命令执行结果如下:一棵树的文本输出形式
Hash Left Join (cost=147.29..175.52 rows=437 width=928)
Hash Cond: (notification.target_comment_id = comments.id)
-> Hash Join (cost=139.84..166.91 rows=437 width=855)
Hash Cond: (notification.target_content_id = content.id)
-> Seq Scan on notification (cost=0.00..25.22 rows=701 width=116)
Filter: (target_content_id IS NOT NULL)
-> Hash (cost=130.51..130.51 rows=746 width=739)
-> Seq Scan on content (cost=0.00..130.51 rows=746 width=739)
Filter: (state = 'normal'::state_enum)
-> Hash (cost=4.98..4.98 rows=198 width=73)
-> Seq Scan on comments (cost=0.00..4.98 rows=198 width=73)
解释
-- 左连接,连接方式为hash,(代价估计:开始-结束为149.58..178.62,预估结果行数530,宽度为928)
Hash Left Join (cost=147.29..175.52 rows=437 width=928)
-- 该层的连接条件
Hash Cond: (notification.target_comment_id = comments.id)
-- 哈希连接
-> Hash Join (cost=139.84..166.91 rows=437 width=855)
-- 该层的连接条件
Hash Cond: (notification.target_content_id = content.id)
-- 对notification顺序扫描
-> Seq Scan on notification (cost=0.00..25.22 rows=701 width=116)
-- 扫描条件为target_content_id不为null
Filter: (target_content_id IS NOT NULL)
-- 对下层结果做hash
-> Hash (cost=130.51..130.51 rows=746 width=739)
-- 顺序扫描content
-> Seq Scan on content (cost=0.00..130.51 rows=746 width=739)
-- 顺序扫描的过滤条件
Filter: (state = 'normal'::state_enum)
-- 对下层结果做hash
-> Hash (cost=4.98..4.98 rows=198 width=73)
-- 顺序扫描comments
-> Seq Scan on comments (cost=0.00..4.98 rows=198 width=73)
加上analyze的版本,可以发现多了每个步骤实际执行的时间、各步骤的实际情况(如hash所生成的bucket数量、内存消耗等)
Hash Left Join (cost=147.29..175.52 rows=437 width=928) (actual time=0.734..1.287 rows=571 loops=1)
Hash Cond: (notification.target_comment_id = comments.id)
-> Hash Join (cost=139.84..166.91 rows=437 width=855) (actual time=0.626..0.999 rows=571 loops=1)
Hash Cond: (notification.target_content_id = content.id)
-> Seq Scan on notification (cost=0.00..25.22 rows=701 width=116) (actual time=0.009..0.159 rows=727 loops=1)
Filter: (target_content_id IS NOT NULL)
Rows Removed by Filter: 21
-> Hash (cost=130.51..130.51 rows=746 width=739) (actual time=0.609..0.609 rows=732 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 573kB
-> Seq Scan on content (cost=0.00..130.51 rows=746 width=739) (actual time=0.003..0.351 rows=732 loops=1)
Filter: (state = 'normal'::state_enum)
Rows Removed by Filter: 423
-> Hash (cost=4.98..4.98 rows=198 width=73) (actual time=0.101..0.101 rows=232 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 31kB
-> Seq Scan on comments (cost=0.00..4.98 rows=198 width=73) (actual time=0.003..0.034 rows=232 loops=1)
Planning time: 0.402 ms
Execution time: 1.368 ms