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

3 minute read

背景

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

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优化)》

Flag Counter

digoal’s 大量PostgreSQL文章入口