PostgreSQL优化器之从一个关于扫描方式选择引发的思考

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: # 一个关于PostgreSQL使用组合索引的问题 近期阅读了《数据库查询优化器的艺术》这本书,对PG和Mysql优化器技术的轮廓有了一定了解。在阅读的过程中,因为知识背景和书本身的表述问题产生了许多困惑,这里就分享对其中一个困惑的探索过程作为看完书的总结。 在这本书的第十八章,关于PG和Mysql的优化器对于索引的优化能力对比中的一段让我困惑不已。如图一所示,单独使用组合索引的后半部分作为查

一个关于PostgreSQL使用组合索引的问题

近期阅读了《数据库查询优化器的艺术》这本书,对PG和Mysql优化器技术的轮廓有了一定了解。在阅读的过程中,因为知识背景和书本身的表述问题产生了许多困惑,这里就分享对其中一个困惑的探索过程作为看完书的总结。

在这本书的第十八章,关于PG和Mysql的优化器对于索引的优化能力对比中的一段让我困惑不已。如图一所示,单独使用组合索引的后半部分作为查询条件使用了顺序扫描,而再加上一个与索引无关的过滤条件,扫描方式竟然就变成了BitMap Index Scan。

考虑到该书针对的PG是9.2.3,这么个老版本,难道是这是两个人写的代码,一个优化了一个忘记了?但是,仔细观察这两个执行计划的代价发现了一丝不对劲,因为两者代价的差值是32.13-32.09=0.04。这个值太小了,而且是加了个过滤条件将预测到结果集行数从9降到3产生的。直观上感觉这个过滤条件能带来的裁剪效果应该不止这么点,所以很可能是索引扫描本身代价过高拖累了谓词优化效果。
image.png

图一:书中执行计划

PostgreSQL中的组合索引

为了分析这个问题,首先了解一下组合索引的大体结构,即怎么样才能只使用组合索引的后半部分完成查询。目前以及有很多讲索引结构的文章,想详细了解的话推荐德歌的《深入浅出PostgreSQL B-Tree索引结构》,本文就简要概括一下。

PG默认创建的索引都是B-Tree索引,整体上索引文件和表一样是块页式存储,元信息存储在根页,所以查询的时候也是从根页一层一层的找到实际存储地址。如果没有指定组合索引的开头一列,那么可以基于B-Tree层层过滤,把所有满足后半部分值要求的索引页都读取到内存,这时很容易就读取了整个索引文件,当数据量大的时候代价较高。

上文提及的问题中的索引文件结构如图二所示,其中meta page中记录了root page是1,然后下面的root page详情则直接指明了到具体heap page的偏移量以及每一项索引的取值。因此,问题中的索引扫描至少要读取两个索引页,同时在root page的data中顺序查找满足条件的索引,如果有多级的话能够过滤掉一些一定不包含结果的索引项。
image.png

图二:表的索引结构

什么是BitMap Heap Scan

图二的执行计划中可以看到有个BitMap Index Scan,看起来似乎是基于位图索引的扫描。然而,PG默认创建的索引是基于B-tree,同时也并不支持位图索引,不过PG会在索引创建时建立位图表(Bitmap Heap Table)。有一段描述BitMap Heap Scan的话:


A plain indexscan fetches one tuple-pointer at a time from the index, 
and immediately visits that tuple in the table.  A bitmap scan fetches 
all the tuple-pointers from the index in one go, sorts them using an 
in-memory "bitmap" data structure, and then visits the table tuples in 
physical tuple-location order

上面的意思是说,普通的索引扫描一次读一条索引项,而BitMap Heap Scan一次性将满足条件的索引项全部取出,并在内存中进行排序, 然后根据取出的索引项访问表数据。由于BitMap Heap Scan会输出整个数据块的数据,因此需要recheck,对输出的索引项进行过滤。就我个人理解,BitMap Heap Scan就是针对有多个索引项满足条件时,通过饱和式的索引页读取结合排序大幅减少随机读取,提升I/O效率。

问题的真相

在理解完背景知识之后,开始正式探索真相。首先本地复现书中的实验,具体SQL语句如下:


create table tkey(key_pk int, key_part1 int, key_part2 int, nokey int, primary key(key_pk));
create index tkey_multi_k on tkey(key_part1,key_part2);
insert into tkey values(1,1,1,1);
insert into tkey values(2,2,1,1);
insert into tkey values(3,2,2,1);
insert into tkey values(4,3,1,1);
insert into tkey values(5,3,2,1);
insert into tkey values(6,3,3,1);
insert into tkey values(7,4,1,1);
insert into tkey values(8,4,2,1);
insert into tkey values(9,4,3,NULL);

基于PG 9.2.3本地分别执行explain select * from tkey where key_part2=2;explain select * from tkey where key_part2=2 and nokey>2;两条SQL语句查看执行计划,如下图三所示。从图中可以看到,Seq Scan算子的代价较书中少了0.01,其他的与书中一致,完成实验复现。

image.png

图三:实验复现

为了证实可以使用基于索引的扫描,使用set session enable_seqscan=false;将顺序扫描禁止,再查询执行计划,得到结果如图四所示。在关闭顺序索引后,两条查询最终都使用了BitMap Index Scan,但是实际估算出来的代价比顺序查找还低了0.05。懵逼!玄学?

image.png

图四:关闭顺序扫描后的查询计划

结合书上第七章的源码分析,PG选择扫描方式的时候会依次搜索顺序扫描、索引扫描和tid扫描。针对索引扫描找到⁨postgresql-9.2.3⁩/⁨src⁩/backend/⁨optimizer⁩/⁨path/indexpath.c:create_index_paths⁩函数,然后这个会调用/Users/yijiang/workspace/postgresql-9.2.3/src/backend/optimizer/util/pathnode.c:add_path函数添加具体查询计划。add_path函数在添加路径时会对比新路径和已有的老路径,判断是不是有添加的必要,以及是否要删掉已经搜索出来的老路径。具体对比项包括代价、排序方式和依赖的上游信息量。其中最重要的一点就是代价对比,在源码中有一段:


/*
 * Do a fuzzy cost comparison with 1% fuzziness limit. (XXX does this
 * percentage need to be user-configurable?)
 */
costcmp = compare_path_costs_fuzzily(new_path, old_path, 1.01);

这个一句的意思是忽略1%以内的代价差进行对比,也就是实验中的顺序扫描方法和自身差异在0.32以内的计划,其代价都认为是一样的。所以仅低0.05的BitMap Index Scan被认为与顺序扫描代价一致,同时排序方式一致,且都没有上游依赖项。最终,在PG 9.2.3看来这两个扫描方式一致,所以新的路径就不用加了,最终索引扫描因为先来后到的问题被踢出局。至此,上文中的现象都得到了解释。

由于PG 9.2.3版本比较太老,所以基于更新的PG 10.5再进行一轮实验,看看是否有什么差异。实验结果如图五(a)所示,发现两个查询都变成BitMap Index Scan,同时代价也比之前估算的更高了一些。在看关闭索引扫描后的图五(b),此时代价高了超过1%,说明在新版的PG对顺序扫描的代价计算进行了修订,同时也意味着查询优化器给出的代价看似精确,但也仅仅是一次估算,需要不断的调整。

image.png

图五:基于PG 10.5的实验结果

因此,显示的执行计划与预期不一致,并一定某个优化规则不支持,而是经过了复杂的可行解搜索,发现了朴素的做法实际代价更靠谱,本质上还是代价权衡的结果。

讨论

上文中关于代价对比有个忽略1%误差的设定,本人猜测是考虑到代价估算本身就是基于经验的启发式计算,同时也很难基于数值完全刻画出具体算子的执行代价,因为越是复杂的计算越是难以估计其资源消耗(如上文中PG 10.5的索引扫描代价估计)。因此,估算的代价本身就有一定误差,而在大数据的场景下误差会被放大,所以设定1%作为误差范围,同时优先搜索简单路径,尽可能尝试朴素做法,达到提升查询效率的目的。

参考文献

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
存储 关系型数据库 PostgreSQL
PostgreSQL表扫描方法解析
PostgreSQL表扫描方法解析
114 0
|
存储 SQL Oracle
PostgreSQL扫描方法综述
PostgreSQL扫描方法综述
120 0
|
SQL 存储 Oracle
PostgreSQL 分页, offset, 返回顺序, 扫描方法原理(seqscan, index scan, index only scan, bitmap scan, parallel xx scan),游标
PostgreSQL 分页, offset, 返回顺序, 扫描方法原理(seqscan, index scan, index only scan, bitmap scan, parallel xx scan),游标
3825 0
|
SQL 存储 弹性计算
PostgreSQL 分页, offset, 返回顺序, 扫描方法原理(seqscan, index scan, index only scan, bitmap scan, parallel xx scan),游标
标签 PostgreSQL , 数据离散性 , 扫描性能 , 重复扫 , bitmap index scan , 排序扫描 , 扫描方法 , 顺序 背景 一个这样的问题: 为什么select x from tbl offset x limit x; 两次查询连续的OFFSET,会有重复数据呢? select ctid,* from tbl where ... offset 0 li
2066 0
|
SQL 机器学习/深度学习 存储
【重新发现PostgreSQL之美】- 38 肝者,将军之官,谋虑出焉. 优化器
大家好,这里是重新发现PostgreSQL之美 - 38 肝者,将军之官,谋虑出焉. 优化器
|
SQL Oracle 关系型数据库
PostgreSQL JOIN limit 优化器 成本计算 改进 - mergejoin startup cost 优化
标签 PostgreSQL , join , limit , startup cost , cbo , 优化器改进 背景 PostgreSQL limit N的成本估算,是通过计算总成本A,以及估算得到的总记录数B得到: (N/B)*A 大概意思就是占比的方法计算 对于单表查询...
1218 0
|
算法 关系型数据库 C语言
PostgreSQL 当有多个索引可选时,优化器如何选择
标签 PostgreSQL , 索引 , 复合索引 , 选择 , 成本 , 优化器 背景 当一个表有很多索引时,并且一个QUERY可以使用到其中的多个索引时,数据库会如何做出选择?最终选择哪个,或者哪几个索引呢? 《PostgreSQL 多查询条件,多个索引的选择算法与问题诊断方法》 选择单个索引时,PATH可以选择index scan , index only scan, bitmap scan。
3069 0
|
关系型数据库 物联网 PostgreSQL
PostgreSQL技术周刊第16期:PostgreSQL 优化器代码概览
PostgreSQL(简称PG)的开发者们:云栖社区已有5000位PG开发者,发布了3000+PG文章(文章列表),沉淀了700+的PG精品问答(问答列表)。 PostgreSQL技术周刊会为大家介绍最新的PG技术与动态、预告活动、最热问答、直播教程等,欢迎大家订阅PostgreSQL技术周刊。
3542 0
|
SQL 算法 关系型数据库
PostgreSQL 优化器代码概览
## 简介 PostgreSQL 的开发源自上世纪80年代,它最初是 Michael Stonebraker 等人在美国国防部支持下创建的POSTGRE项目。上世纪末,Andrew Yu 等人在它上面搭建了第一个SQL Parser,这个版本称为Postgre95,也是加州大学伯克利分校版本的PostgreSQL的基石[1]。
1642 0