Recheck Cond filter IO\CPU放大 原理与优化CASE - 含 超级大表 不包含(反选) SQL优化

本文涉及的产品
云原生数据库 PolarDB 分布式版,标准版 2核8GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS PostgreSQL Serverless,0.5-4RCU 50GB 3个月
推荐场景:
对影评进行热评分析
简介:

标签

PostgreSQL , 全文检索 , 数组 , 不包含 , not in , bitmap scan filter , toast 切片存储 , except , IO , cpu , 放大


背景

在阅读本文之前,先提几个问题:

1、用了索引是不是就不需要访问表了?

2、用了索引是不是就不需要进行二次判断了?

第一个问题,只有一种情况用了索引是不需要访问表的,Index Only Scan,并且对应堆表行号对应的HEAP块中没有不可见TUPLE(访问表的VM文件得到)。否则都需要回表,访问堆表的tuple head(infomask掩码),并判断行版本获得可见性。注意回表并不需要访问COLUMN的VALUE,只需要访问堆表中TUPLE的HEAD,取其掩码来判断可见性。

第二个问题,实际上是索引精确性的问题,对于精准索引的INDEX SCAN,是不需要RECHECK的,例如B-TREE索引。但是这里指的是index scan和index only scan,并不是bitmap scan。bitmap scan实际上只是定位到了BLOCK,并没有定位到item,也就是说,需要recheck。因此什么时候recheck,取决于索引的精确性 以及是否使用了bitmapcan。对于不lossy index,必然是要recheck的,对于bitmap scan也必然是需要recheck的。

recheck有哪些开销?

recheck需要取得HEAP TABLE的对应被过滤列的VALUE,进行recheck。

好的,接下来谈一下recheck引入的隐含问题:

recheck过多,就会导致CPU使用偏高,同时响应变慢,毋庸置疑。

recheck 引入的开销举例

例子1 - bitmap scan带来的recheck

bitmap scan将数据收敛到了被命中的BLOCK,并执行顺序的HEAP扫描。这样减少了数据块的随机性,但是引入了一个问题,RECHECK的问题。

postgres=# explain (analyze,verbose,timing,costs,buffers) select count(*) from a where id>10 and id<10000;  
                                                         QUERY PLAN                                                            
-----------------------------------------------------------------------------------------------------------------------------  
 Aggregate  (cost=10840.18..10840.19 rows=1 width=8) (actual time=4.238..4.238 rows=1 loops=1)  
   Output: count(*)  
   Buffers: shared hit=97 read=27  
   ->  Bitmap Heap Scan on public.a  (cost=132.77..10815.80 rows=9750 width=0) (actual time=1.082..3.056 rows=9989 loops=1)  
         Output: id, info, crt_time  
	 -- 这条就是heap scan的时候的recheck  
         Recheck Cond: ((a.id > 10) AND (a.id < 10000))  
         Heap Blocks: exact=94  
         Buffers: shared hit=97 read=27  
         ->  Bitmap Index Scan on a_pkey  (cost=0.00..130.34 rows=9750 width=0) (actual time=1.060..1.060 rows=9989 loops=1)  
               Index Cond: ((a.id > 10) AND (a.id < 10000))  
               Buffers: shared hit=3 read=27  
 Planning time: 0.148 ms  
 Execution time: 4.276 ms  
(13 rows)  

同样的SQL,使用index scan就不需要recheck。

postgres=# explain (analyze,verbose,timing,costs,buffers) select count(*) from a where id>10 and id<10000;  
                                                          QUERY PLAN                                                             
-------------------------------------------------------------------------------------------------------------------------------  
 Aggregate  (cost=344.41..344.42 rows=1 width=8) (actual time=3.493..3.493 rows=1 loops=1)  
   Output: count(*)  
   Buffers: shared hit=124  
   ->  Index Scan using a_pkey on public.a  (cost=0.43..320.04 rows=9750 width=0) (actual time=0.020..2.280 rows=9989 loops=1)  
         Output: id, info, crt_time  
         Index Cond: ((a.id > 10) AND (a.id < 10000))  
         Buffers: shared hit=124  
 Planning time: 0.156 ms  
 Execution time: 3.528 ms  
(9 rows)  

例子2 - 大字段recheck带来的性能问题

大字段recheck是很危险的,特别是命中的数据量所在的字段占用空间非常庞大时。

例如一个100万的表,但是其中一个JSON字段是1MB,那么如果这个JSON字段需要被recheck的话,即使涉及的数据只有1000条,也需要1GB的扫描量。

postgres=# create table t_big(id int, info tsvector);  
CREATE TABLE  
  
  
create or replace function gen_rand_tsvector(int,int) returns tsvector as $$    
  select array_to_tsvector(array_agg((random()*$1)::int::text)) from generate_series(1,$2);    
$$ language sql strict;   
  
  
insert into t_big select 1 , gen_rand_tsvector(15000, 20000) from generate_series(1,100000);  
postgres=# create index idx_t_big on t_big using gin (info);  
CREATE INDEX  

下面的QUERY复合条件的数据块太多,需要扫描大字段,导致了超长时间的查询。

postgres=#  explain (analyze,verbose,timing,costs,buffers) select id from t_big where info @@ to_tsquery('! -1');  
                                                              QUERY PLAN                                                                 
---------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.t_big  (cost=234779.77..261535.52 rows=99500 width=4) (actual time=6445.766..6460.928 rows=100000 loops=1)  
   Output: id  
   Recheck Cond: (t_big.info @@ to_tsquery('! -1'::text))  
   Heap Blocks: exact=637  
   Buffers: shared hit=165720  
   ->  Bitmap Index Scan on idx_t_big  (cost=0.00..234754.90 rows=99500 width=0) (actual time=6445.690..6445.690 rows=100000 loops=1)  
         Index Cond: (t_big.info @@ to_tsquery('! -1'::text))  
         Buffers: shared hit=165083  
 Planning time: 0.166 ms  
 Execution time: 6468.692 ms  
(10 rows)  

后面谈优化方法。

例子3 - lossy index 引入的recheck

brin索引, bloom索引都属于lossy索引,索引本身决定了在输入条件后,只能将数据缩小到一个范围,然后再通过recheck来决定tuple是否符合条件。

create table t_brin (id int, info text);  
insert into t_brin select generate_series(1,10000000), 'test';  
  
create index idx_t_brin on t_brin using brin(id);  
  
explain (analyze,verbose,timing,costs,buffers) select count(*) from t_brin where id between 1 and 100;  
postgres=# explain (analyze,verbose,timing,costs,buffers) select count(*) from t_brin where id between 1 and 100;  
                                                           QUERY PLAN                                                             
--------------------------------------------------------------------------------------------------------------------------------  
 Aggregate  (cost=21314.01..21314.02 rows=1 width=8) (actual time=2.685..2.685 rows=1 loops=1)  
   Output: count(*)  
   Buffers: shared hit=137  
   ->  Bitmap Heap Scan on public.t_brin  (cost=4.83..21314.01 rows=1 width=0) (actual time=0.165..2.669 rows=100 loops=1)  
         Output: id, info  
         -- 这里出现了recheck  
	 Recheck Cond: ((t_brin.id >= 1) AND (t_brin.id <= 100))  
         Rows Removed by Index Recheck: 23580  
         Heap Blocks: lossy=128  
         Buffers: shared hit=137  
         ->  Bitmap Index Scan on idx_t_brin  (cost=0.00..4.83 rows=23641 width=0) (actual time=0.148..0.148 rows=1280 loops=1)  
               Index Cond: ((t_brin.id >= 1) AND (t_brin.id <= 100))  
               Buffers: shared hit=9  
 Planning time: 0.211 ms  
 Execution time: 2.739 ms  
(14 rows)  

切片存储-TOAST

这里为什么要提到切片存储,因为我接下来的优化方法,如果没有切片存储,那么就不成立。

切片存储指对超过BLOCK 1/4大小的变长记录,存储到切片中,而在TUPLE中仅仅使用TOAST 指针来引用,从而减少TUPLE本身的大小。

那么扫描全表的行号时,实际上,如果记录数本身不多,只是列比较大时,实际上全表扫描取行号很快。

如何优化大字段 recheck带来的问题

前面举的例子,当字段很大时,recheck很耗费资源。

假设满足条件的数据太多,导致recheck很久。应该如何优化呢?

select * from tbl where id not in (....);  
  
select * from tbl where ts @@ to_tsquery('! china');  

既然这些条件的结果很多,那么说明相反的结果就很少,这些条件相反的条件来查询(走index scan,避免recheck),另外再使用全量数据(全表扫,不recheck,所以不需要扫描大字段),except来排他。

except方法优化

1、找出所有符合条件的数据的ID或行号

2、找出(满足条件)相反的ID或行号

小心去重(加上PK,或行号,避免去重)

例子,我们使用tsvector存储了大量的文本向量信息,每个字段1MB,有几百万记录,然后有一个NOT IN的查询,这个查询筛选得到的结果过多。

由于tsvector的索引GIN扫描用到了bitmapscan,当记录数很多时,需要RECHECK的记录就越多,并且这部分是空间占用的大坨。

最终会导致搜索很慢。

优化方法:

既然按所需条件复合条件的记录过多,导致RECHECK部分的时间变慢,那么我们可以使用反条件,减少复合条件的记录减少RECHECK的耗时。

同时使用全表扫得到CTID或PK(因为全表扫不需要扫TOAST),然后再EXCEPT它。最后根据PK或CTID得到索要的记录。

explain (analyze,verbose,timing,costs,buffers)  
select id from t_big where ctid = any (                 -- 通过行号来定位使用except得到的符合条件的记录  
array  
(  
    select ctid from t_big   -- 这个是全表的ctid(行号)  
  except   
    select ctid from t_big where info @@ to_tsquery('-1')    -- 这里就是反条件得到的ctid(行号)   
)  
);  

用t_big那个例子,性能飙升

                                                                         QUERY PLAN                                                                           
------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Tid Scan on public.t_big  (cost=12023.32..12035.42 rows=10 width=4) (actual time=105.696..129.313 rows=100000 loops=1)  
   Output: t_big.id  
   TID Cond: (t_big.ctid = ANY ($0))  
   Buffers: shared hit=100640  
   InitPlan 1 (returns $0)  
     ->  SetOp Except  (cost=11520.81..12023.31 rows=100000 width=10) (actual time=62.363..93.300 rows=100000 loops=1)  
           Output: "*SELECT* 1".ctid, (0)  
           Buffers: shared hit=640  
           ->  Sort  (cost=11520.81..11772.06 rows=100500 width=10) (actual time=62.359..70.542 rows=100000 loops=1)  
                 Output: "*SELECT* 1".ctid, (0)  
                 Sort Key: "*SELECT* 1".ctid  
                 Sort Method: quicksort  Memory: 7760kB  
                 Buffers: shared hit=640  
                 ->  Append  (cost=0.00..3170.85 rows=100500 width=10) (actual time=0.012..46.268 rows=100000 loops=1)  
                       Buffers: shared hit=640  
                       ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..2637.00 rows=100000 width=10) (actual time=0.012..30.602 rows=100000 loops=1)  
                             Output: "*SELECT* 1".ctid, 0  
                             Buffers: shared hit=637  
                             ->  Seq Scan on public.t_big t_big_1  (cost=0.00..1637.00 rows=100000 width=6) (actual time=0.011..13.605 rows=100000 loops=1)  
                                   Output: t_big_1.ctid  
                                   Buffers: shared hit=637  
                       ->  Subquery Scan on "*SELECT* 2"  (cost=19.73..533.85 rows=500 width=10) (actual time=0.047..0.047 rows=0 loops=1)  
                             Output: "*SELECT* 2".ctid, 1  
                             Buffers: shared hit=3  
                             ->  Bitmap Heap Scan on public.t_big t_big_2  (cost=19.73..528.85 rows=500 width=6) (actual time=0.045..0.045 rows=0 loops=1)  
                                   Output: t_big_2.ctid  
                                   Recheck Cond: (t_big_2.info @@ to_tsquery('-1'::text))  
                                   Buffers: shared hit=3  
                                   ->  Bitmap Index Scan on idx_t_big  (cost=0.00..19.60 rows=500 width=0) (actual time=0.042..0.042 rows=0 loops=1)  
                                         Index Cond: (t_big_2.info @@ to_tsquery('-1'::text))  
                                         Buffers: shared hit=3  
 Planning time: 0.239 ms  
 Execution time: 137.356 ms  
(33 rows)  

小结

本文讲述了recheck的原理,以及在什么情况下RECHECK可能引发较大性能问题。如何避免等方法。

参考

《全文检索 不包含 优化 - 阿里云RDS PostgreSQL最佳实践》

《HTAP数据库 PostgreSQL 场景与性能测试之 26 - (OLTP) NOT IN、NOT EXISTS 查询》

《PostgreSQL 空间切割(st_split)功能扩展 - 空间对象网格化 (多边形GiST优化)》

《PostgreSQL 空间st_contains,st_within空间包含搜索优化 - 降IO和降CPU(bound box) (多边形GiST优化)》

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
3天前
|
SQL 缓存 监控
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
|
12天前
|
SQL 存储 缓存
如何优化SQL查询性能?
【10月更文挑战第28天】如何优化SQL查询性能?
52 10
|
11天前
|
SQL 存储 缓存
SQL Server 数据太多如何优化
11种优化方案供你参考,优化 SQL Server 数据库性能得从多个方面着手,包括硬件配置、数据库结构、查询优化、索引管理、分区分表、并行处理等。通过合理的索引、查询优化、数据分区等技术,可以在数据量增大时保持较好的性能。同时,定期进行数据库维护和清理,保证数据库高效运行。
|
24天前
|
SQL 资源调度 分布式计算
如何让SQL跑快一点?(优化指南)
这篇文章主要探讨了如何在阿里云MaxCompute(原ODPS)平台上对SQL任务进行优化,特别是针对大数据处理和分析场景下的性能优化。
|
20天前
|
人工智能 Cloud Native Java
云原生技术深度解析:从IO优化到AI处理
【10月更文挑战第24天】在当今数字化时代,云计算已经成为企业IT架构的核心。云原生作为云计算的最新演进形态,旨在通过一系列先进的技术和实践,帮助企业构建高效、弹性、可观测的应用系统。本文将从IO优化、key问题解决、多线程意义以及AI处理等多个维度,深入探讨云原生技术的内涵与外延,并结合Java和AI技术给出相应的示例。
75 1
|
1月前
|
SQL 监控 数据库
慢SQL对数据库写入性能的影响及优化技巧
在数据库管理系统中,慢SQL(即执行缓慢的SQL语句)不仅会影响查询性能,还可能对数据库的写入性能产生显著的不利影响
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
4月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
3月前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
44 2