暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB PostgreSQL版索引预读原理介绍

PolarDB 2025-03-27
253

PolarDB PostgreSQL版索引预读原理介绍

关于 PolarDB PostgreSQL 版

PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。

索引相关背景介绍

在典型的TP业务中,通常会对频繁查找的列创建索引以避免对表的全表扫描。通过索引树可以精确的定位到所需的数据位于哪些数据页面上,仅需几个IO和内存跳转就能定位到所需的数据。

然而,当需要扫描的数据量较多时,比如扫描数据量在1000多行时,可能需要1000次读IO,这个过程中IO占整个SQL的执行时间超过99%。

通常新的业务数据会插入到比较新的Heap页面中,也就是Heap页面中的数据是没有顺序的,也就意味着传统的依靠文件系统的预读无法发挥作用。

本功能在索引扫描的算法中,通过叶子页面中存储的行指针,定位当前SQL接下来需要读取的Heap表页面号,并提前预读以优化索引扫描的IO性能。

索引扫描IndexScan的IO模型

image.png

如上图所示,PG索引树的叶子节点存储了指向Heap表的行指针itup。在进行索引扫描IndexScan时:

  1. 首先先从索引树ROOT页面往下查找到具体的叶子节点页面;

  2. 从叶子节点页面读取到多个itup指针;

  3. 逐个通过itup指针定位Heap表的页面号,并从存储上读取Heap表页面到Buffer中进行后续处理;

上述过程也称为回表,如下图所示,这个过程是串行化的:

先读取索引树的数据页面;

再读取n个Heap表页面;

Heap表页面在存储上的排列是无规律近似随机的,传统的依靠文件系统的预读机制无法发挥预取的作用。

image.png

索引扫描IndexOnlyScan的IO模型

当索引树上的索引列是SQL中涉及到的列的超集时,也就是仅通过扫描索引树就能查询到SQL中的全部信息时,会使用IndexOnlyScan仅对索引树进行扫描,避免回表产生的额外IO。

如下图,PG中的索引树是BLink树,每层的节点之间通过指针互相连接。当进行索引扫描时,首先定位到某个叶子节点,当扫描完该节点时,再通过该节点页面存储的next指针得到下一个节点的页面号,并读取到Buffer中进行处理。

索引树的页面在存储上的排列也是无规律近似随机的,该过程只能通过当前节点的next指针找到下一个页面号。因此,传统的依靠文件系统的预读机制也无法发挥预取的作用。

image.png

PolarDB-PG的索引预读原理介绍

索引扫描IndexScan的预读原理

如下图所示,在索引扫描的算法中,在读取到索引树的叶子页面,通过叶子页面能够得到多个指向Heap表的行指针,从行指针中提取出接下来要读取的Heap表页面号,并逐个通过异步IO提前预读。

image.png

将原来的串行读取优化为异步读取,大大提升了IO的效果。如下图所示:

image.png

索引扫描IndexOnlyScan的预读原理

如下图所示,当进行索引扫描时,首先定位到某个叶子节点,过程中记录父节点,通过父节点存储的指针提取出该叶子节点的一系列兄弟节点,对这些叶子页面逐个通过异步IO进行预读。

image.png

使用指南

  • 索引预读的场景

目前仅支持对用户的业务表索引,且索引类型是BTree索引支持预读。

  • IndexScan预读

IndexScan预读功能默认关闭,参数名为polar_enable_aio_prefetch和polar_enable_aio_index_prefetch

  • 开启IndexScan索引预读的功能:
set polar_enable_aio_prefetch = 1;
set polar_enable_aio_index_prefetch = 1;

  • 关闭IndexScan索引预读的功能:
set polar_create_index_prefetcher = 0;
set polar_enable_aio_index_prefetch = 0;

  • IndexOnlyScan预读

IndexScan预读功能默认关闭,参数名为polar_enable_aio_prefetch和polar_enable_aio_index_only_prefetch

  • 开启IndexScanOnly索引预读的功能:
set polar_enable_aio_prefetch = 1;
set polar_enable_aio_index_only_prefetch = 1;

  • 关闭IndexScanOnly索引预读的功能:
set polar_enable_aio_prefetch = 0;
set polar_enable_aio_index_only_prefetch = 0;

性能对比

数据准备

create table t001(id int, age int, msg text);
insert into t001 select random() * 1000000, random() * 10000, md5(random()::text) from generate_series(1, 1000000) as i;
create index t001_id on t001(id);

注意:在进行下面的索引扫描测试前,可以通过读取其他表以便将表t001和t001_id的数据页面换出,仅测试发生了IO时的性能。

默认索引扫描

可以看出使用了索引扫描,命中了1125次IO,读取了3914次IO,总执行时间449ms。

postgres=# explain (analyze, timing, buffers) select avg(age) from t001 where id < 5000;
                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=16519.65..16519.66 rows=1 width=32) (actual time=449.597..449.599 rows=1 loops=1)
   Buffers: shared hit=1125 (main=1125 vm=0 fsm=0) read=3914 (main=3914 vm=0 fsm=0)
   ->  Index Scan using t001_id on t001  (cost=0.42..16506.70 rows=5180 width=4) (actual time=0.222..447.084 rows=5025 loops=1)
         Index Cond: (id < 5000)
         Buffers: shared hit=1125 (main=1125 vm=0 fsm=0) read=3914 (main=3914 vm=0 fsm=0)
 Planning:
   Buffers: shared hit=1 (main=0 vm=1 fsm=0) read=3 (main=3 vm=0 fsm=0)
 Planning Time: 0.779 ms
 Execution Time: 449.698 ms

开启索引扫描预读

使用上述方法开启IndexScan的预读后,可以看出同样是使用了索引扫描,实际都发生了3914次IO,3914的读取中有3767次是通过异步IO提前预读上来的。总体共有4892次命中,比未开启预读的命中次数1125正好多出了3767次IO,而IndexScan的关键路径上仅读取了147次IO。

总执行时间88ms。



postgres=# explain (analyze, timing, buffers) select avg(age) from t001 where id < 5000;
                                                                          QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=16519.65..16519.66 rows=1 width=32) (actual time=88.738..88.740 rows=1 loops=1)
   Buffers: shared hit=4892 (main=4892 vm=0 fsm=0) read=147 (main=147 vm=0 fsm=0) waitio=928 aiohit=1104 aioinitiate=3767 aiowakeup=658 waittime=49.634
   Prefetches: hit=1104 initiate=3767 rounds=13
   ->  Index Scan using t001_id on t001  (cost=0.42..16506.70 rows=5180 width=4) (actual time=0.233..86.946 rows=5025 loops=1)
         Index Cond: (id < 5000)
         Buffers: shared hit=4892 (main=4892 vm=0 fsm=0) read=147 (main=147 vm=0 fsm=0) waitio=928 aiohit=1104 aioinitiate=3767 aiowakeup=658 waittime=49.634
         Prefetches: hit=1104 initiate=3767 rounds=13
 Planning:
   Buffers: shared hit=1 (main=0 vm=1 fsm=0) read=3 (main=3 vm=0 fsm=0)
 Planning Time: 1.342 ms
 Execution Time: 88.831 ms
(11 rows)



文章转载自PolarDB,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论