PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(一)|学习笔记

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,企业版 4核16GB
推荐场景:
HTAP混合负载
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: 快速学习PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(一)

开发者学堂课程【PostgreSQL快速入门: PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(一)】学习笔记与课程紧密联系,让用户快速学习知识

课程地址https://developer.aliyun.com/learning/course/16/detail/91


PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(一)

 

内容介绍:

一、PostgreSQL 的查询优化

二、EXPLAIN 语法

三、EXPLAIN 输出的含义

一、PostgreSQL 的查询优化

1、本节课讲解 Postgre 的查询优化,这章节涉及到explain SQL 分析工具的使用,理解 explain 的代价计算原理,explain 在输出的时候会有成本的输出,Postgre 优化器会根据不同的路径计算出来的成本,选择成本最优的执行计划。

会根据数据库的硬件环境校准代价因子,不同的硬件环境默认的 Postgre 的代价因子可能不合适。

2、默认的代价因子是 Planner Cost Constanta 等,

image.png

seq_page_cost =1.0 是全表扫描的单个扫描块的成本,它是一个成本基数,是1.0。random_page_cost =1.0 是随机扫描的,如果走索引扫描10个块,它的成本就是10乘以代价因子。cpu_tuple_cost =0.01指每取出一条记录,cpu 的开销是多少。

cpu_index_tuple_cost 走索引的话,索引扫描每一个 tuple 额外的开销是多少。cpu_operator_cost 如果有过滤,扫描十条记录只需要里面的某些条件,假如只需要一条,cpu_operator_cost 指的是过滤这十记录带来的额外开销,operator 跟函数有关系。effective_cache_size = 128000MB 跟代价因子没有关系,是一个倾向性的东西。

比如他配置的是128G,就是告诉 Posrgre 整个系统有128G 的剩余内存,考虑查询这些数据块在 cache 里面,这样 random_page_cost 的成本会更低,就是离散扫描的成本会更低

对于不同硬件,这些成本硬置需要调整。比如 CPU 很调暗,这些就要调低一些,或者使用的硬盘是强悍的硬盘,或者 SSD,那么它连续扫描和随机扫描的成本,可以设置成一样的。

3、还要讲的有 explain 输出的含义,比如组合行,节点处理,合并连接,哈希连接等,并且可以结合 explain 的输出优化 SQL。

 

二、EXPLAIN 语法

1、explain 后面可以带 option,比如说analyze analyze 在 explain 的时候真正执行 statement,就是下面的SQL语句。

EXPLAIN [(option [.….])] statement

EXPLAIN[ANALYZE][VERBOSE] statement

以统计得到时间以及 buffer 的信息,如果要统计 buffer 和 timing,analyze 必须打开。analyze 打开一定要注意 analyze 是真正执行这条 SQL。

比如 explain analyze 一个 update 语句,会 update  一个数据,一般在调教好之后就不用在 analyze,直接 explain 就可以。

2verbose 指的是输出详细信息;cost 是输出成本,默认打开;buffer 指的是输出查询消耗的 shared buffer 和local buffer 的信息,包括命中、未命中、脏、写; timing 指输出 explain statement 真实的执行时间FORMAT { TEXT | XML | JSON | YAML } 是输出格式,格式可以是 text、xml、json 或者 ymal 格式。

 

三、EXPLAIN 输出的含义

1、例子讲解

digoal=# explain (analyzc, verbose, costs, buffers, timing) select count(*) from tbl_cost_align

QUERY PLAN

Aggregate(cost=220643.00.220643.01 rows= width=0)(actual time=4637.754.4637.754 rows= loops=)

Output: count(*)--这个节点的输出,聚合,输出第一行前的开销是220643.00.

--聚合的开销=220643.00-195393.00

Buffers: shared hit=4925read=89468-这个节点以及下级节点的BUFFER统计项

>Seq Scan on postgres.tbl_cost_align(cost=0.00.195393.00

rows=10100000 width=0)(actual timc=00183119.291 rows-10100000loops=1)--这个节点的路径(全表扫描)

--0.00表示输出第一行前的成本,如这里输出第一行前不需要排序为0.00.后面是这个节点真实的时间

Output: id,info,crt_time --这个节点输出的列

Buffers: shared hit=4925 read=89468 这个节点的shared buffer命中4925个page,从磁盘读取89468个page(如果 shared buffer够大,第二次执行的时候应该全部hit.)

Total runtime: 4637.805 ms总的执行时间

(7 rows)

(1)例子是 explain(analyze、verbose、costs、buffers、timing) 全部选上,输出是非常详细的,但是 analyze 会执行这条 SQL 语句,最终得到的 actual time 指的是真正执行这条 SQL 语句select count(*) from tbl_cost_align得到的时间。

explain 输出的时候是从最里层往上层节点看,在执行的时候是从最里层的节点开始。例子里最里层的节点是 Seq Scan,Seq Scan 全表扫描的意思,cost=0.00.195393.00 是从代价因子计算出来的成本,actual timc=00183119.291 是真实的执行时间,rows=10100000是返回的,这个表有一千零十万行,全表扫描消耗的支持时间总共是三秒钟。

0.018指的是在输出第一行的时候带来的开销,buffer 指的是 shared hit,指的是在 shared buffer 里面命中的,read 在外面读的,就是 shared buffer 以外的读。

(2)read 可能来自两个地方,一个是来自 OS cache,另一个是可能来自于真正的物理盘。因为 littles 超系统成有 cache,在读89468数据块的时候可能是在 OS cache 里面命中可能是在物理磁盘上读。但是对于 Postgre 来说,只要不在 Postgre 的 shared buffer 里面认为是从外面读的,不管是不是在 oscacheshared buffer 是在数据库的参数文件里的 shared buffer 配置。对于这个 SQL 语句,有4925个数据块在 shared buffer 里面命中,剩下的都是从外面读的。

(3)Output 表示 postgres.tbl_cost_align 节点的输出,输出了三个字段 id、info、crt_time,表示全表扫描的时候把这三个字段全部输出。

Aggregate 是一个聚合节点,聚合来自 count(*),全表扫描是扫描 tbl_cost_align 表 。聚合真正的输出时间是抛去全表扫描的时间加上聚合速度第一行之前带来的额外开销,真正聚合的时间是开销了4637毫秒减去3119毫秒,差不多1.5秒的样子,把这一千零十万条记录聚合成一条记录

真正输出的话时间是一个很短的时间,因为只有一条记录存储 count,count 就一个数字,这个输出的时间很短,row 是真正输出的行有多少,actual time 后面也是真正输出的行有多少条。聚合的开销从 cost=220643.00 来看,这是从代价因子里面计算出来的 cost,这个 cost 减去下一个节点总的 cost,相当于聚合带来的开销。

2、组合行集例子

digoal=# explain (analyze, verbose, costs, buffers, timing) select 1 union select1;  --union去重复,所以有sort节点

QUERY PLAN

Unique (cost=0.05..0.06 rows=2 width=0)(actual time=0.049..0.051 rows=1 loops=1)

Output:(1)

Buffers: shared hit=3

>Sort (cost=0.05..0.06 rows=2 width=0) (actual time=0.047..0.047 rows=2 loops=1)

Output:(1)

Sort Key:(1)

Sort Method:quicksort Memory: 25kB

Buffers: shared hit=3

>Append(cost=0.00..0.04 rows=2 width=0)(actual time=0.006..0.007rows=2 loops=1)

->Result(cost=0.00..0.01rows=1width=0)(actualtime=0.0030.003 rows=1 loops=1)

Output:l

->Result(cost=0.00..0.01rows=1 width=0)(actualtime=0.000.0.000 rows=1 loops=1)

Output: 1

Total runtime: 0.136 ms

(14 rows)

1)有两个查询把结果union,或者是 unit oral,union 有一个去重,就是排序的过程。真正执行 analyze select 1 union select1,最底层的节点是->,->Result(cost=0.00..0.01rows=1

width=0)(actualtime=0.0030.003 rows=1 loops=1) select1 的第一个节点->Result(cost=0.00..0.01rows=1 width=0)

(actualtime=0.000.0.000 rows=1 loops=1) 是select 1的第二个节点。这两个节点会做一个 append 操作,append 是合并,相当于 select 1 合并 select 1。然后到下面排序节点,

image.png

因为要去重,所以有个排序的过程。

(2)排序用到 quicksort Memory,就是在内存里面快速排序,消耗25KB。shared hit 是三个;sort key 是1,就是第一个字段做排序;output 也是1,得到排序之后,会有一个去重的操作(unique),最后输出的是一条记录,排序的时候总共有两条记录在排序(rows=2 loops=1)。Append 是一个组合行列合并的过程,也是从两个节点的一条记录合并成两条记录。

3、嵌套连接例子

(1)nested loop join: The right relation is scanned once for every row fo ound in the left relation.This strategyis easy to implement but can be very time consuming. (However, if the right relation can be scanned with an index scan, this can be a good strategy.It is possible to use values from the current row of the left relation as keys for the index scan of the right.)

连接有三种连接,分别是嵌套连接、哈希连接和合并连接,合并连接是marge join,哈希连接是哈希 join。嵌套连接叫 nested loop join,整个过程是左表扫描一次,右表根据左表每输出一条记录,跟右表做 join 的匹配,相当于是左表有十条记录,右表需要扫描十次,最终结果集再合并输出。这种表适合右表的关联列发生在唯一键值列或者主键列上的情况,在右表做 join 的时候,右表上有一个索引,如果没有索引右表每一次都是全表扫描,性能开销是非常大的。

(2)digoal=# explain (analyze, verbose, costs, buffers, timing) select f.* from f,p where f.p_id=p.id and f.p_id<10;

QUERY PLAN

Nested Loop(cost=0.57..22.29 rows-9 width=49)(actual time=0.011..0.042 rows=9 loops=1)

Output: f.id, f.p_id, f.info, f.crt_time

Buffers: shared hit=3l

-> Index Scan using idx_f_I on postgres.f(cost=0.29..2.45 rows=9 width=49) (actual time=0.005..0.010 rows=9 loops=1)--左表

Output: fid, f.p_id, f.info, f.crt_time

Index Cond: (f.p_id <10)

Buffers: shared hit=4

->Index Only Scan using idx_pkey on postgres.p(cost=0.29..2.19

rows= width=4)(actual time=0.002.0.003rows= loops=9)--右表

Output: p.id

Index Cond:(p.id=f.p_id)

Heap Fetches: 9

Buffers: shared hit=27

Total runtime: 0.072 ms

(13 rows)

假如有两个表,一个f表,一个p表,这两个表都有索引。f表的 pid 上有索引, p表 id 上有索引。右表是f表,它循环了9次,左表f表。这个执行计划中右表是p表,因为p表上是PK,是一个唯一键值,左输出9行,每一行右表都需要做一个条件(index cond),从左表输出9行,右表会循环九次,最终输出。Output 指的是输出的一些字段。shared hit,这里没有 read,表示所有的都是在 shared buffer 里面命中。

3、哈希连接例子

(1)hash join: the right relation is first scanned and loaded into a hash table,using its j oin attributes as hash keys. Next the left relation is scanned and the appropriate values of every row found are used as hash keys to locate the matching rows in the table.

哈希连接指的是右表扫描加载到内存 HASH 表,hash key 是一个 join 列。假如a表的id列和b表的id列做 join,如果右表是b表的话,那么这个哈希列就是b表的id列。然后左表扫描,并与内存中的 HASH 表进行关联,输出最终结果。

(2)digoal=# explain (analyze, verbose, costs, buffers, timing) selget f.* from fp where f.p_id=p.id and f.p_id<10;

Hash Join (cost=2.56234.15 rows=9 width=49)(actual time=6.047.3.905 rows=9 loops=1)

Output: f.id, fp_id, f.info, f.crt_time

Hash Cond:(p.id=f.p_id)--HASH join key,fp_id.

Buffers: shared hit=98

->Seq Scan on postgres.p(cost=0.00.194.00rows=10000

width=4)(actual time=0.014.2.016 rows=10000 loops=1)-左表

Output: p.id,p.info,p.crt_time

Buffers: shared hit=94

-> Hash(cost=2.45..2.45 rows=9 width=49) (actual time=0.017..0.017 rows=9 loops= 1)

Output: f.id, f.p_id, f.info, f.crt_time

Buckets: 1024 Batches:1 Memory Usage:1kB--右表加载到内存,hash keyjoin key fp_id

Buffers: shared hit=4

->Index Scan using idx_f_1 on postgres.f(cost=0.29..2.45

rows=9width=49)(actual time=0.00..0.012 rows=9 loops=1)--右表

Output: f.id, fp_id, f.info, f.crt_time

Index Cond:(f.p_id<10)

Buffers: shared hit=4

Total runtime: 3.954 ms

(16 rows)

同样是这个查询selget f.* from fp where f.p_id=p.id and f.p_id<10;)

但是把 nested local scan 禁掉,把 enable_nestloop 关掉,强制它走 hash join。走 hash join 能看到右表是扫描,然后加载到 hash 上。右表扫描然后加载到内存中,相当于走 idx_f_1 这个索引,把数据取出之后,做了 hash 的节点,相当于把取出的九条记录放到哈希内存表中,它的内存开销是1KB,buckets 是1024,batches 是1同样也是命中四个块,indx scan 相当于是 hash root 下一个 root。左表是p表,左表根据哈希出来九条的记录依次做一个匹配,然后在输出,节点就是 hash join。HASH join key 是 f.p_id,

就是两个关联的右表的字段,右表是f,所以字段是 p_id,就是 hash p。hash cond,hash 条件是 p.id= f.p_id。

4、合并连接例子

(1)merge join: Fach relation is sorted on the join attributes before the join starts.Then the two relations are scanned in parallel,and matching rows are combined to form join rows.This kind of join is more attractive because each relation has to be scanned only once.The required sorting might be achieved either by an explicit sort step, or by scanning the relation in the proper order using an index on the join key.

这个连接要做两次排序,如果 join 的列有索引,可以不需要额外排序的过程;如果没有索引,则需要额外的排序过程做哈希 join。首先两个 join 的表根据 join key 进行排序,如果两个列都有索引,则没有这个排序的过程,然后根据 join key 排序顺序并行扫描两个表进行匹配输出最终结果这种情况适合大表,并且索引列进行关联的情况,这两个大表只有 merge join 条件,其它条件都没有的情况下,走 merge join 带来的开销比较少。

(2)Digoal=# explain (analyze, verbose, costs, buffers, timing) select f.* from f.p where f.p_id=p.id and f.p_id<10;

QUERY PLAN

Merge Join(cost=0.57..301.85 rows=9 width=49)(actual time=0.030.0.049rows=9 loops=1)

Output: f.id,fp_id,f.info, f.crt_time

Merge Cond:(f.p_id=p.id)

Buffers: shared hit=8

->Index Scan using idx_f_1 on postgres.f(cost=0.29..2.45 rows=9 width=49)(actual time=0.005..0.012 rows=9 loops=1)

Output: f.id,f.p_id,f.info, f.crt_time

Index Cond:(f.p id<10)

Buffers: shared hit=4

->Index Only Scan using P_pkey on postgres.p (cost=0.29.274.29 rows=10000 width=4)(actual time=0.0170.022 rows=10 loops=1)

Output: p.id

Heap Fetches: 10

Buffers: shared hit=4

Total runtime:0.118 ms

(13 rows)

nestloop 和 hashjoin 都关掉,最后只允许 mergejoin。 还是这个查询语句(select f.* from f.p where f.p_id=p.id and f.p_id<10;

),走了两个索引,分别是 p_pkey 和 idx_f_1,都是两个键值,merge 条件是两个字段 f.p_id=p.id,这两个表的两个字段都有索引。mergejoin 成本是301,hashjoin 成本是234,nested loopjoin 成本是22.29,如果这三个join都允许的情况下,肯定是走嵌套循环。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
2月前
|
SQL 关系型数据库 分布式数据库
在PolarDB中,行数评估是通过对表的统计数据、基数估计以及算子代价模型来进行估算的。
【2月更文挑战第14天】在PolarDB中,行数评估是通过对表的统计数据、基数估计以及算子代价模型来进行估算的。
113 1
|
10月前
|
SQL 索引 OceanBase
OBCP第四章 SQL调优-局部索引与全局索引
OBCP第四章 SQL调优-局部索引与全局索引
95 0
|
2月前
|
缓存 关系型数据库 MySQL
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
142 0
|
23小时前
|
SQL 存储 监控
MSSQL性能调优深度解析:索引策略优化、SQL语句精炼与并发管理技巧
在Microsoft SQL Server(MSSQL)的性能调优领域,索引策略的优化、SQL语句的精炼以及高效的并发管理技巧是提升数据库性能不可或缺的三大方面
|
23小时前
|
SQL 运维 监控
MSSQL性能调优深度解析:索引精细调整、SQL查询优化与并发控制策略
在Microsoft SQL Server(MSSQL)的运维实践中,性能调优是确保数据库高效、稳定运行的核心任务
|
23小时前
|
SQL 监控 数据库
MSSQL性能调优深度探索:索引精细调整、SQL重写优化与智能并发控制
在Microsoft SQL Server(MSSQL)的日常管理和维护中,性能调优是一项至关重要的任务
|
SQL 存储 算法
PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(二)|学习笔记
快速学习PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(二)
345 0
PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(二)|学习笔记
|
SQL 算法 Cloud Native
数据库内核那些事|细说PolarDB优化器查询变换 - join消除篇
数据库的查询优化器是整个系统的"大脑",一条SQL语句执行是否高效在不同的优化决策下可能会产生几个数量级的性能差异,因此优化器也是数据库系统中最为核心的组件和竞争力之一。阿里云瑶池旗下的云原生数据库PolarDB MySQL版作为领先的云原生数据库,希望能够应对广泛用户场景、承接各类用户负载,助力企业数据业务持续在线、数据价值不断放大,因此对优化器能力的打磨是必须要做的工作之一。 本系列将从PolarDB for MySQL的查询变换能力开始,介绍我们在这个优化器方向上逐步积累的一些工作。
11364 0
|
SQL 移动开发 关系型数据库
PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(三)|学习笔记
快速学习PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(三)
819 0
PostgreSQL 执行计划,成本公式解说,代价因子校准,自动跟踪SQL执行计划(三)|学习笔记