postgresql执行计划讲解及优化

news/2025/1/20 0:38:17/文章来源:https://www.cnblogs.com/Vincent-yuan/p/18680584
1.EXPLAIN语法讲解
PostgreSQL中EXPLAIN命令的语法格式如下:
 
EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
 
该命令的可选项“options”如下:
ANALYZE [ boolean ]
VERBOSE [ boolean ]
COSTS [ boolean ]
BUFFERS [ boolean ]
FORMAT { TEXT | XML | JSON | YAML }
 
ANALYZE选项通过实际执行SQL来获得SQL命令的实际执行计划。ANALYZE选项查看到的执行计划因为真正被执行过,所以可以看到执行计划每一步耗费了多长时间,以及它实际返回的行数。
注意 加上ANALYZE选项后是真正执行实际的SQL命令,如果SQL语句是一个插入、删除、更新或CREATE TABLE AS语句(这些语句会修改数据库),为了不影响实际数据,可以把EXPLAIN ANALYZE放到一个事务中,执行完后即回滚事务,命令如下:
BEGIN;
EXPLAIN ANALYZE ...;
ROLLBACK;
 
VERBOSE选项显示计划的附加信息,如计划树中每个节点输出的各个列,如果触发器被触发,还会输出触发器的名称。该选项的值默认为“FALSE”。
COSTS选项显示每个计划节点的启动成本和总成本,以及估计行数和每行宽度。该选项的值默认为“TRUE”。
BUFFERS选项显示缓冲区使用的信息。该参数只能与ANALYZE参数一起使用。显示的缓冲区信息包括共享块读和写的块数、本地块读和写的块数,以及临时块读和写的块数。默认值为 FALSE。
FORMAT选项指定输出格式,输出格式可以是TEXT、XML、JSON或者YAML。默认值为TEXT。
2.EXPLAIN输出结果讲解
 
osdba=# explain select * from testtab01;
                           QUERY PLAN
---------------------------------------------------------------
  Seq Scan on testtab01 (cost=0.00..184.00 rows=10000 width=36)
(1 row)
 
上面的运行结果中“Seq Scan on testtab01”表示顺序扫描表“testtab01”,顺序扫描也就是全表扫描,即从头到尾地扫描表。后面的内容“(cost=0.00..184.00 rows=10000 width=36)”可以分为以下3个部分。
cost=0.00..184.00:“cost=”后面有两个数字,中间由“..”分隔,第一个数字“0.00”表示启动的成本,也就是说,返回第一行需要多少cost值;第二个数字表示返回所有数据的成本。
rows=10000:表示会返回10000行。
• width=36:表示每行平均宽度为36字节。
关于cost
成本“cost”用于描述SQL命令的执行代价,默认情况下,不同操作的cost值如下:
• 顺序扫描一个数据块,cost值定为“1”。
• 随机扫描一个数据块,cost值定为“4”。
• 处理一个数据行的CPU代价,cost值定为“0.01”。
• 处理一个索引行的CPU代价,cost值定为“0.005”。
• 每个操作符的CPU代价为“0.0025”。
根据上面的操作类型,PostgreSQL可以智能地计算出一个SQL命令的执行代价。
3.EXPLAIN使用示例
联合使用analyze选项和buffers选项,通过实际执行来查看实际的代价和缓冲区命中的情况,命令如下:
osdba=# explain (analyze true,buffers true ) select * from testtab03;
                                                        QUERY PLAN
--------------------------------------------------------------------------------
  Seq Scan on testtab03 (cost=0.00..474468.18 rows=26170218 width=36) (actual time=0.498..8543.701 rows=10000000 loops=1)
    Buffers: shared hit=16284 read=196482 written=196450
  Total runtime: 9444.707 ms
(3 rows)
 
因为加了buffers选项,执行计划的结果中就会出现一行“Buffers:shared hit=16284 read=196482 written=196450”,其中“shared hit=16284”表示在共享内存中直接读到16284个块,从磁盘中读到196482块,写磁盘196450块。有人可能会问,SELECT为什么会写?这是因为共享内存中有脏块,从磁盘中读出的块必须把内存中的脏块挤出内存,所以产生了很多的写。
 
来看下面这个“create table as”的执行计划:
osdba=# explain create table testtab04 as select * from testtab03 limit 100000;
                                 QUERY PLAN
-----------------------------------------------------------------------------
  Limit (cost=0.00..3127.66 rows=100000 width=142)
    -> Seq Scan on testtab03 (cost=0.00..312766.02 rows=10000002 width=142)
(2 rows)
insert语句的执行计划:
osdba=# explain insert into testtab04 select * from testtab03 limit 100000;
                                    QUERY PLAN
--------------------------------------------------------------------------------
  Insert on testtab04 (cost=0.00..4127.66 rows=100000 width=142)
    -> Limit (cost=0.00..3127.66 rows=100000 width=142)
      -> Seq Scan on testtab03 (cost=0.00..312766.02 rows=10000002 width=142)
(3 rows)
 
删除语句的执行计划如下:
osdba=# explain delete from testtab04;
                            QUERY PLAN
-------------------------------------------------------------------
  Delete on testtab04 (cost=0.00..22.30 rows=1230 width=6)
    -> Seq Scan on testtab04 (cost=0.00..22.30 rows=1230 width=6)
(2 rows)
 
更新语句的执行计划如下:
osdba=# explain update testtab04 set note='bbbbbbbbbbbbbbbb';
                             QUERY PLAN
--------------------------------------------------------------------
  Update on testtab04 (cost=0.00..22.30 rows=1230 width=10)
    -> Seq Scan on testtab04 (cost=0.00..22.30 rows=1230 width=10)
(2 rows)
 
4.EXPLAIN输出结果查询方式
全表扫描
全表扫描在PostgreSQL中也称顺序扫描(Seq Scan),全表扫描就是把表中的所有数据块从头到尾读一遍,然后从中找到符合条件的数据块。
全表扫描在EXPLAIN命令的输出结果中用“Seq Scan”表示,示例如下:
osdba=# EXPLAIN SELECT * FROM testtab01;
                            QUERY PLAN
---------------------------------------------------------------
  Seq Scan on testtab01 (cost=0.00..2754.05 rows=151905 width=36)
(1 row)
 
索引扫描
索引通常是为了加快查询数据的速度而增加的。索引扫描,就是在索引中找出需要的数据行的物理位置,然后再到表的数据块中把相应的数据读出来的过程。
索引扫描在EXPLAIN命令的输出结果中用“Index Scan”表示,示例如下:
 
osdba=# EXPLAIN SELECT * FROM testtab01 where id=1000;
                                    QUERY PLAN
--------------------------------------------------------------------------------
  Index Scan using idx_testtab01_id on testtab01 (cost=0.29..8.31 rows=1 width=70)
    Index Cond: (id = 1000)
(2 rows)
位图扫描
位图扫描也是走索引的一种方式。方法是扫描索引,把满足条件的行或块在内存中建一个位图,扫描完索引后,再根据位图到表的数据文件中把相应的数据读出来。如果走了两个索引,可以把两个索引形成的位图通过AND或OR计算合并成一个,再到表的数据文件中把数据读出来。
当执行计划的结果行数很多时会走这种扫描,如非等值查询、IN子句或有多个条件都可以走不同的索引时。
 
下面是非等值的一个示例:
osdba=# explain select * from testtab02 where id2 >10000;
                                       QUERY PLAN
--------------------------------------------------------------------------------
  Bitmap Heap Scan on testtab02 (cost=18708.13..36596.06 rows=998155 width=16)
    Recheck Cond: (id2 > 10000)
    -> Bitmap Index Scan on idx_testtab02_id2 (cost=0.00..18458.59 rows=998155 width=0)
      Index Cond: (id2 > 10000)
(4 rows)
 
在位图扫描中可以看到,“Bitmap Index Scan”先在索引中找到符合条件的行,然后在内存中创建位图,再到表中扫描,也就是我们看到的“Bitmap Heap Scan”。
大家还会看到“Recheck Cond:(id2>10000)”,这是因为多版本的原因,从索引中找出的行从表中读出后还需要再检查一下条件。
下面是一个因为IN子句走位图索引的示例:
osdba=# explain select * from testtab02 where id1 in (2,4,6,8);
                                   QUERY PLAN
---------------------------------------------------------------------------------
  Bitmap Heap Scan on testtab02 (cost=17.73..33.47 rows=4 width=16)
    Recheck Cond: (id1 = ANY ('{2,4,6,8}'::integer[]))
    -> Bitmap Index Scan on idx_testtab02_id1 (cost=0.00..17.73 rows=4 width=0)
      Index Cond: (id1 = ANY ('{2,4,6,8}'::integer[]))
(4 rows)
 
下面是走两个索引后将位图进行BitmapOr运算的示例:
osdba=# explain select * from testtab02 where id2 >10000 or id1 <200000;
                                          QUERY PLAN
----------------------------------------------------------------------------------
  Bitmap Heap Scan on testtab02 (cost=20854.46..41280.46 rows=998446 width=16)
    Recheck Cond: ((id2 > 10000) OR (id1 < 200000))
    -> BitmapOr (cost=20854.46..20854.46 rows=1001000 width=0)
      -> Bitmap Index Scan on idx_testtab02_id2 (cost=0.00..18458.59 rows=998155 width=0)
        Index Cond: (id2 > 10000)
      -> Bitmap Index Scan on idx_testtab02_id1 (cost=0.00..1896.65 rows=102430 width=0)
          Index Cond: (id1 < 200000)
(7 rows)
 
在上面的执行计划中,可以看到BitmapOr操作,即使用OR运算合并两个位图。
这里有个疑问,or到底会使索引失效吗?
待验证。
条件过滤
条件过滤,一般就是在WHERE子句上加过滤条件,当扫描数据行时会找出满足过滤条件的行。条件过滤在执行计划中显示为“Filter”,示例如下:
osdba=# EXPLAIN SELECT * FROM testtab01 where id<1000 and note like 'asdk%';
                                     QUERY PLAN
--------------------------------------------------------------------------------
  Index Scan using idx_testtab01_id on testtab01 (cost=0.29..48.11 rows=1 width=70)
    Index Cond: (id < 1000)
    Filter: (note ~~ 'asdk%'::text)
 
如果条件的列上有索引,可能会走索引而不走过滤。
嵌套循环连接
嵌套循环连接(NestLoop Join)是在两个表做连接时最朴素的一种连接方式。在嵌套循环中,内表被外表驱动,外表返回的每一行都要在内表中检索找到与它匹配的行,因此整个查询返回的结果集不能太大(大于1万不适合),要把返回子集较小的表作为外表,而且在内表的连接字段上要有索引,否则速度会很慢。
执行的过程如下:确定一个驱动表(Outer Table),另一个表为Inner Table,驱动表中的每一行与Inner Table表中的相应记录Join类似一个嵌套的循环。适用于驱动表的记录集比较小(<10000)而且Inner Table表有有效的访问方法(Index)。
散列连接
优化器使用两个表中较小的表,利用连接键在内存中建立散列表,然后扫描较大的表并探测散列表,找出与散列表匹配的行。
这种方式适用于较小的表可以完全放于内存中的情况,这样总成本就是访问两个表的成本之和。但是如果表很大,不能完全放入内存,优化器会将它分割成若干不同的分区,把不能放入内存的部分写入磁盘的临时段,此时要有较大的临时段从而尽量提高I/O的性能。
下面就是一个散列连接(Hash Join)的例子:
osdba=# explain select a.id,b.id,a.note from testtab01 a, testtab02 b where a.id=b.id and b.id<=1000000;
                                           QUERY PLAN
--------------------------------------------------------------------------------
  Hash Join (cost=20000041250.75..20000676975.71 rows=999900 width=93)
    Hash Cond: (a.id = b.id)
    -> Seq Scan on testtab01 a (cost=10000000000.00..10000253847.55 rows=10000055 width=89)
    -> Hash (cost=10000024846.00..10000024846.00 rows=999900 width=4)
      -> Seq Scan on testtab02 b (cost=10000000000.00..10000024846.00 rows=999900 width=4)
        Filter: (id <= 1000000)
(6 rows)
 
先看表大小,命令如下:
osdba=# select pg_relation_size('testtab01');
  pg_relation_size
------------------
    1260314624
(1 row)
 
osdba=# select pg_relation_size('testtab02');
  pg_relation_size
------------------
    101138432
(1 row)
 
因为表“'testtab01”大于“'testtab02”,所以Hash Join是先在较小的表“testtab02”上建立散列表,然后扫描较大的表“testtab01”并探测散列表,找出与散列表匹配的行。
合并连接
通常情况下,散列连接的效果比合并连接要好,然而如果源数据上有索引,或者结果已经被排过序,此时执行排序合并连接不需要再进行排序,合并连接的性能会优于散列连接。
下面的示例中,表“testtab01”的“id”字段上有索引,表“testtab02”的“id”字段上也有索引,这时从索引扫描的数据已经排好序了,就可以直接进行合并连接(Merge Join):
osdba=# explain select a.id,b.id,a.note from testtab01 a, testtab02 b where a.id=b.id and b.id<=100000;
                                              QUERY PLAN
--------------------------------------------------------------------------------
  Merge Join (cost=1.47..47922.57 rows=99040 width=93)
    Merge Cond: (a.id = b.id)
    -> Index Scan using idx_testtab01_id on testtab01 a (cost=0.43..413538.43 rows=10000000 width=89)
    -> Index Only Scan using idx_testtab02_id on testtab02 b (cost=0.42..4047.63 rows=99040 width=4)
      Index Cond: (id <= 100000)
(5 rows)
 
4数据库优化
比较常用的是下面两种优化思路。
第一种:把一些无用的步骤或作用不大的步骤去掉就是一种优化。
第二种:要么换到更快的硬件(这里暂不介绍),要么最有效的方法是优化算法,如让SQL走到更优的执行计划上。
在数据库优化中,主要有以下优化指标。
• 响应时间:衡量数据库系统与用户交互时多久能够发出响应。
• 吞吐量:衡量在单位时间内可以完成的数据库任务。
数据库优化工作中,第一项就是确定优化目标。
性能目标:如CPU利用率或IOPS需要降到多少。
• 响应时间:需要从多少毫秒降到多少毫秒。
• 吞吐量:每秒处理的SQL数或QPS需要提高到多少。
4.1 数据库的逻辑结构优化
主要是表和索引的优化思路和方法。
1 调整表的fillfactor参数
fillfactor:填充因子是一个从10到100的整数,用于设置在插入数据时,在一个数据块中填充百分之多少的空间后就不再填充了,另一部分空间预留作更新时使用。比如,设置为“60”,则表示向一个数据块中插入的数据占用60%的空间后,就不再向该数据块中插入数据。而保留的这40%的空间,就是为了更新数据时使用。
对于更新频繁的表需要设置一个较小的fillfactor值。
调整方法如下:
alter table test01 set (fillfactor=80);
索引的优化
索引是一种从表中快速检索出较少行的有效方式,如果需要从表中检索出较少的行,我们需要考虑的是在查询条件上建索引,这样可以利用索引把所需要的数据快速检索出来。我们需要在哪些情况下建索引呢?下面的规则可以指导索引的创建:
特别小的表可以没有索引,但超过300行的表就应该有索引。
• 经常与其他表进行连接的表,在连接字段上应该建立索引。
• 经常出现在WHERE子句中的字段,特别是大表的字段,应该建立索引。
• 经常出现在ORDER BY子句中的字段,应该建索引。
• 经常出现在GROUP BY子句中的字段,考虑建索引。
• 对于查询中很少使用的列不应该创建索引。
• 索引应该建在选择性高的字段上。
• 索引应该建在小字段上,对于大的文本字段甚至超长字段,建议不要建索引,如果建也建议建哈希索引。
• 复合索引的建立需要仔细进行分析,尽量考虑用单字段索引代替。索引的几个字段是否经常同时以AND方式出现在WHERE子句中,单字段查询是否极少甚至没有?如果是,则可以建立复合索引,否则尽量考虑单字段索引;如果复合索引中包含的字段经常单独出现在WHERE子句中,则分解为多个单字段索引。
• 如果建了(A、B)两个字段上的组合索引,通常就不要再建A字段的单字段索引了。
• 正确选择复合索引中的主列字段(第一个列),一般是选择性较好的字段作为第一个列。
• 如果复合索引所包含的字段超过3个,那么要仔细考虑其必要性,尽量减少复合的字段。
• 频繁进行数据操作的表,不要建立太多的索引。
• 删除无用的索引,这些索引除了会导致更新的代价变大外,还可能产生错误的执行计划。
通常一个表的索引数最好不要超过6个,若太多则应考虑将一些不常使用的列上建的索引删除。
SQL的优化
下面列出了一些SQL语句优化技巧,可以在实际使用中灵活应用:
• 通常应该尽量避免全表扫描和排序操作,所以考虑在查询条件的列和ORDER BY涉及的列上建立索引。
• 如果经常进行一些范围查询,可以考虑使用“CLUSTER table_name USING index_name”让表中行的物理存储顺序与索引的顺序一致,以提高查询效率。
• 应尽量避免在WHERE子句中对字段进行函数或表达式操作,因为这会导致走不到索引。
• 通常用EXISTS代替IN是一个好的选择:“select * from a where col in(select col from b)”用“select * from a where exists(select 1 from b where b.col=a.col)”替换。
• 只含数值信息的字段尽量不要设计为字符型,而应该设计成数值型,因为这会降低查询和连接的性能,并会增加存储开销。
• 数值类型的字段尽量设计为int或bigint类型而不应该设计成numeric型,因为int和bigint类型的效率更高。只有int或bigint的范围不能表示时,才使用numeric类型。
• 如果明知两个结果集没有重复记录,则应该使用UNION ALL而不是UNION合并两个结果集。
• 最好不要使用“*”返回所有表的所有列,如“select * from t”,应用具体的字段列表代替“*”,不要返回用不到的任何字段。
• 表的别名(Alias)的技巧是当在SQL语句中连接多个表时,请尽量使用表的别名,并把别名前缀于每个Column上,这样可以减少解析的时间并减少那些由列名歧义引起的语法错误。
• 尽量将数据的处理工作放在服务器上完成,以减少网络开销,适度地使用存储过程,以减少客户端与数据库的交互次数。
• 使用COPY导入数据,比一条条地INSERT要快得多,也比“INSERT t values(),(),(),....”这样的批量插入快。
• 在存储过程中,能够用SQL语句实现的就不要用循环去实现。
• 在存储过程或事务中更新多张表时,应该总是以相同的顺序去更新,这样可以避免死锁。
• varchar(n)和text类型没有性能差异,只是varchar(n)对输入文本的长度有限制。
• 建议使用timestamp with time zone类型,而不要用timestamp without time zone类型,这是为了避免时间函数对于不同时区的时间点返回值不同,为业务的国际化扫清障碍。
PostgreSQL数据库对于INSERT、UPDATE和DELETE语句可以通过RETURING返回行的值,可以避免二次查询,从而提高性能。
INSERT INTO test(t) VALUES('11111') RETURNING id, tm;
 
PostgreSQL中没有Oracle、MSSQL的MERGE INTO语法,但是它的ON DUPLICATE KEY UPDATE语法可以实现类似的功能,通过该功能可以实现MERGE功能,从而提高SQL的效率。
INSERT INTO blog_pv(blog_id, pv) VALUES(998, 1) ON CONFLICT(blog_id) DO UPDATE SET pv=blog_pv.pv+1;
 
有时我们需要从一张大表中随机抽取一些数据。
一般人可能会用limit语句来获取数据,命令如下:
select * from test01 limit 1000;
 
但这样可能随机性不好,也有人用下面的SQL语句来实现:
select * from test01 where id%1000 = 1;
 
但上面的SQL语句会走全表扫描,效率不高,其实PostgreSQL提供了数据抽样的语法,可以直接抽样大表的数据:
SELECT * FROM test01 TABLESAMPLE SYSTEM(0.1);
 
“TABLESAMPLE SYSTEM(0.1)”的含义是随机找一些数据块进行抽样,抽样比例是0.1%。
 
 
 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/871941.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

XML+propties

txt文件,properties(属性)文件,XMl文件txt与properties与XML的区别当这些文件存储单个关系数据时, 普通文件 无法存储 关系数据,而properties属性文件以键值对形式存储就很方便,XML文件也可以 见图1 但储存多个用户就不行了,XML更适合, 见图2properties集合properties …

一条SQL更新语句是如何执行的?

与查询流程不同的是,更新流程中会涉及两个重要的模块: (i)redo_log模块(InnoDB中的日志模块):在 MySQL 里也有这么个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MyS…

大模型分布式训练并行技术(五)-序列并行

p { font-size: 12pt; line-height: 2 !important } 参考资料 大模型分布式训练并行技术(五)-序列并行 详解MegatronLM序列模型并行训练(Sequence Parallel)一、序列并行(Colossal-AI)背景 Colossal-AI 序列并行诞生的背景是 self-attention 的内存需求是输入长度(sequenc…

1.匿名内部类

使用场景不用多创建类,来使用其方法定义 匿名内部类的语法比较奇特,匿名内部类既是一个类的定义,同时他本身也是一个对象, 所以子类继承抽象类, 实现类实现接口,需要节省内存不创建类,从而创建匿名内部类 例子使用当你的才华配不上你的野心,努力的时候到了!

Arrays 排序

正常来说 Arrays可以用于数组排序, 但如果数组里面是引用类型地址就会报错,这时候, 就需要加个功能(实现接口/继承接口) Comparable接口来定引用类型对象的排序规则(以..属性值进行排序)正常Arrays.sort( 数组对象)进行排序时 , 会在排序的时候将数组对象进行调用comparato方…

Spring Boot 自动配置原理详解

引言 Spring Boot 的一大亮点是它能够自动配置(Auto-Configuration)Spring应用程序,极大地简化了Spring应用的创建过程。开发者只需添加所需的依赖,Spring Boot就会根据这些依赖和一些预设条件自动装配相应的组件,从而减少了大量样板代码的编写。 第三方组件的集成方式 对…

传奇

毋庸置疑,很多人的心里,都有一个传奇、传奇3,80后、90后,甚至70后尤甚。当然也包括我。主要当然因我曾经是盛大游戏传奇工作室研发团队的一员,且是盛大传奇3项目部的第一个程序技术人员,内心对传奇、传奇3的感情非同一般。因工作等原因,我早已不再从事传奇类游戏的开发研…

常用工具

类似gdb的bash调试工具bashdb: https://sourceforge.net/projects/bashdb/files/bashdb/ 非常好用,结合vscode bashdb(bash debug) shift + command + d,配置 .vscode/launch.json

《CPython Internals》阅读笔记:p232-p249

《CPython Internals》学习第 13天,p232-p249 总结,总计 18 页。 一、技术总结 无。 二、英语总结(生词:1) 1.overhead (1)overhead: over-("above") + head(“top part, uppermost section”) overhead的字面意思是:above the head,后来演变成"represent …

2025春秋杯冬季赛MISC部分题目复现

简单算术 异或直接得出flagfind_me 进游戏用fill指令把命令方块填充掉然后切创造或者观察找将文件解压出来后看结构是MC的存档文件,于是用MC跑,提示要找雪屋,在附近雪屋的箱子里有给压缩包密码 解压后得到:unai?535.0a20[189.[4049[ax30[e.j60xaj91x8+随波逐流一把梭音频的秘密…

CogAgent: A Visual Language Model for GUI Agents

CogAgent: 利用VLM操作GUI。主要内容 提出了一个18B的VLM模型CogAgent(CogVLM的新版本),旨在提高对于GUI的理解、导航和交互能力。利用高分辨率和低分辨率编码器适应不同分辨率的输入,在9个VQA benchmarks上取得了sota。同时,CogAgent利用截屏输入,在PC和安卓GUI导航任务…

【原创】MAC OS 本地搭建部署 dify

一、什么是 dify?Dify 是一个开源的大语言模型(LLM)应用开发平台,融合了后端即服务(Backend as Service, BaaS)和 LLMOps 理念,旨在简化和加速生成式AI应用的创建和部署。它支持多种大型语言模型(如OpenAI的GPT系列、Claude3等),并提供强大的数据集管理功能、可视化的…