PostgreSQL 并行写入堆表,如何保证时序线性存储 - BRIN索引优化
背景
BRIN索引的原理很简单,每个数据块(或者每一段连续的数据块)记录了其中的元数据(最大值,最小值,平均值,COUNT,AVG,NULL值个数等),当每个数据块存储的数据范围错开(边界清晰)时,块级索引的效率就非常高。
例如测试表
create table test(id int, info text);
-- 对ID创建BRIN索引
create index idx_test_id on test using brin(id);
ID边界清晰的例子:
数据块1:ID值范围 1-100
数据块2:ID值范围 101-200
数据块3:ID值范围 301-300
......
ID边界模糊的例子:
数据块1:ID值范围 1-30 , 60-100
数据块2:ID值范围 31-49, 101-130
数据块3:ID值范围 49-59, 130-159, 160-200
......
边界清晰,指不同的数据块没有交集,或者少量交集,而边界模糊,指数据块的交集很多,数据存储散乱。
为什么边界模糊性能不好呢?
例如
select * from test where id between 1 and 100;
这条SQL,在边界清晰时,搜索1号数据块即可。
而在边界模糊时,需要搜索1,2,3个数据块。
在实际业务中,我们可能会使用自增序列,作为时序字段,当串行(单进程)写入时,序列值和行号(堆表物理存储)线性相关,而当并行写入时,情况可能会变得糟糕(仅仅当使用了大步调时)。
怎么优化呢?
自增序列,并行写入的问题
1、串行写入
create table test(id serial8, info text);
insert into test (info) select 'test' from generate_series(1,1000000);
线性相关性 = 1
postgres=# analyze test;
ANALYZE
postgres=# select correlation from pg_stats where tablename='test' and attname='id';
correlation
-------------
1
(1 row)
BRIN性能
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where id between 1000 and 10000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.test (cost=6.85..5761.69 rows=9264 width=13) (actual time=0.134..2.703 rows=9001 loops=1)
Output: id, info
Recheck Cond: ((test.id >= 1000) AND (test.id <= 10000))
Rows Removed by Index Recheck: 14679
Heap Blocks: lossy=128
Buffers: shared hit=130
-> Bitmap Index Scan on idx_test_id (cost=0.00..4.53 rows=23256 width=0) (actual time=0.026..0.026 rows=1280 loops=1)
Index Cond: ((test.id >= 1000) AND (test.id <= 10000))
Buffers: shared hit=2
Planning time: 0.085 ms
Execution time: 3.188 ms
(11 rows)
2、并行写入
postgres=# drop table test;
DROP TABLE
postgres=# create table test(id serial8, info text);
CREATE TABLE
vi test.sql
insert into test (info) values ('test');
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 400 -j 400 -t 2500
线性相关性,略差,但依旧很好
postgres=# create index idx_test_id on test using brin(id);
CREATE INDEX
postgres=# analyze test;
ANALYZE
postgres=# select correlation from pg_stats where tablename='test' and attname='id';
correlation
-------------
0.998394
(1 row)
-- 偏差
postgres=# select *,id-rn from (select ctid,id,row_number() over(order by ctid) rn from test) t where id<>rn;
ctid | id | rn | ?column?
------------+---------+---------+----------
(0,3) | 4 | 3 | 1
(0,4) | 3 | 4 | -1
(0,28) | 29 | 28 | 1
(0,29) | 28 | 29 | -1
(0,39) | 40 | 39 | 1
(0,40) | 39 | 40 | -1
(0,66) | 67 | 66 | 1
(0,67) | 66 | 67 | -1
(0,110) | 111 | 110 | 1
(0,111) | 110 | 111 | -1
(1,1) | 189 | 186 | 3
(1,2) | 190 | 187 | 3
(1,3) | 191 | 188 | 3
(1,4) | 192 | 189 | 3
(1,5) | 193 | 190 | 3
(1,6) | 194 | 191 | 3
(1,7) | 195 | 192 | 3
(1,8) | 196 | 193 | 3
(1,9) | 197 | 194 | 3
(1,10) | 198 | 195 | 3
(1,11) | 199 | 196 | 3
(1,12) | 200 | 197 | 3
(1,13) | 201 | 198 | 3
(1,14) | 202 | 199 | 3
(1,15) | 203 | 200 | 3
(1,16) | 204 | 201 | 3
(1,17) | 205 | 202 | 3
(1,18) | 206 | 203 | 3
(1,19) | 207 | 204 | 3
(1,20) | 209 | 205 | 4
(1,21) | 210 | 206 | 4
(1,22) | 212 | 207 | 5
(1,23) | 220 | 208 | 12
(1,24) | 223 | 209 | 14
(1,25) | 226 | 210 | 16
(1,26) | 229 | 211 | 18
BRIN性能
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where id between 1000 and 10000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.test (cost=6.83..5952.29 rows=9204 width=13) (actual time=0.073..2.687 rows=9001 loops=1)
Output: id, info
Recheck Cond: ((test.id >= 1000) AND (test.id <= 10000))
Rows Removed by Index Recheck: 14679
Heap Blocks: lossy=128
Buffers: shared hit=130
-> Bitmap Index Scan on idx_test_id (cost=0.00..4.53 rows=22764 width=0) (actual time=0.033..0.033 rows=1280 loops=1)
Index Cond: ((test.id >= 1000) AND (test.id <= 10000))
Buffers: shared hit=2
Planning time: 0.086 ms
Execution time: 3.185 ms
(11 rows)
3、并行写入,放大序列步调,散列问题出现
postgres=# drop table test;
DROP TABLE
postgres=# create table test(id serial8, info text);
CREATE TABLE
postgres=# alter sequence test_id_seq cache 10000;
ALTER SEQUENCE
vi test.sql
insert into test (info) values ('test');
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 400 -j 400 -t 2500
线性相关性,直线下降
postgres=# create index idx_test_id on test using brin(id);
CREATE INDEX
postgres=# analyze test;
ANALYZE
postgres=# select correlation from pg_stats where tablename='test' and attname='id';
correlation
-------------
0.571033
(1 row)
-- 偏差非常明显
postgres=# select *,id-rn from (select ctid,id,row_number() over(order by ctid) rn from test) t where id<>rn;
ctid | id | rn | ?column?
------------+---------+---------+----------
(0,6) | 10001 | 6 | 9995
(0,7) | 6 | 7 | -1
(0,8) | 7 | 8 | -1
(0,9) | 10002 | 9 | 9993
(0,10) | 20001 | 10 | 19991
(0,11) | 8 | 11 | -3
(0,12) | 10003 | 12 | 9991
(0,13) | 9 | 13 | -4
(0,14) | 20002 | 14 | 19988
(0,15) | 10004 | 15 | 9989
(0,16) | 10 | 16 | -6
(0,17) | 20003 | 17 | 19986
(0,18) | 10005 | 18 | 9987
(0,19) | 11 | 19 | -8
(0,20) | 20004 | 20 | 19984
(0,21) | 10006 | 21 | 9985
(0,22) | 12 | 22 | -10
(0,23) | 20005 | 23 | 19982
(0,24) | 10007 | 24 | 9983
(0,25) | 13 | 25 | -12
(0,26) | 20006 | 26 | 19980
(0,27) | 14 | 27 | -13
(0,28) | 20007 | 28 | 19979
(0,29) | 10008 | 29 | 9979
(0,30) | 15 | 30 | -15
(0,31) | 20008 | 31 | 19977
(0,32) | 10009 | 32 | 9977
(0,33) | 20009 | 33 | 19976
(0,34) | 16 | 34 | -18
(0,35) | 30001 | 35 | 29966
(0,36) | 20010 | 36 | 19974
(0,37) | 10010 | 37 | 9973
(0,38) | 17 | 38 | -21
(0,39) | 20011 | 39 | 19972
(0,40) | 20012 | 40 | 19972
(0,41) | 10011 | 41 | 9970
(0,42) | 18 | 42 | -24
(0,43) | 30002 | 43 | 29959
(0,44) | 20013 | 44 | 19969
(0,45) | 19 | 45 | -26
(0,46) | 10012 | 46 | 9966
(0,47) | 30003 | 47 | 29956
(0,48) | 20014 | 48 | 19966
(0,49) | 20 | 49 | -29
(0,50) | 30004 | 50 | 29954
(0,51) | 10013 | 51 | 9962
(0,52) | 20015 | 52 | 19963
(0,53) | 21 | 53 | -32
(0,54) | 30005 | 54 | 29951
(0,55) | 10014 | 55 | 9959
(0,56) | 20016 | 56 | 19960
(0,57) | 22 | 57 | -35
(0,58) | 20017 | 58 | 19959
BRIN性能
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where id between 1000 and 10000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.test (cost=5.11..6244.85 rows=2222 width=13) (actual time=7.104..43.755 rows=1501 loops=1)
Output: id, info
Recheck Cond: ((test.id >= 1000) AND (test.id <= 10000))
Rows Removed by Index Recheck: 448419
Heap Blocks: lossy=2432
Buffers: shared hit=2440 -- 散列,BRIN索引混淆了,导致扫描了大量的数据块。
-> Bitmap Index Scan on idx_test_id (cost=0.00..4.56 rows=38916 width=0) (actual time=0.071..0.071 rows=24320 loops=1)
Index Cond: ((test.id >= 1000) AND (test.id <= 10000))
Buffers: shared hit=8
Planning time: 0.128 ms
Execution time: 43.867 ms
(11 rows)
并发时序线性优化
1、使用单步序列。单步序列不会因为并发增加而导致离散度增加。
2、使用系统时间字段,系统时间与单步序列效果一样,即使并发写入,也不会导致离散度增加。
使用连续自增值的字段,创建BRIN索引就是很靠谱的。