摘要: 標籤PostgreSQL , 多列彙總, gin , btree , n_distinct , 選擇性, 如何選擇索引方法(hash,btree,gin,gist,brin), 如何優化索引, 關聯性幕後在廣告產業,精准行銷是一個較熱的話題,之前寫過一個案例,如何使用PostgreSQL的array類型和GIN索引即時圈人的場景。
幕後
在廣告產業,精准行銷是一個較熱的話題,之前寫過一個案例,如何使用PostgreSQL的array類型和GIN索引即時圈人的場景。
《萬億級行銷(圈人)邁入毫秒年齡 - 萬億user_tags級即時推薦系統資料庫設計》
使用以上方法,程式需要作出一些調整(當然,如果用戶原本就是PostgreSQL技術棧,改動量會很少),改動量舉例
假設用戶使用了多個列來表示不同的屬性,每個屬性對應一些TAG取值空間。
create table user_tags(uid int8 primary key, lab1 int, lab2 text, lab3 timestamp, lab4 text, lab5 interval, lab6 json);
用戶原有的圈人、維度統計查詢可能是這樣的
select * from user_tags where lab1 ? xxx and lab2 ? xxx or lab4 ? xxx;
select xx, count(*) from user_tags where lab1 ? xxx and lab2 ? xxx or lab4 ? xxx group by xxx;
由於屬性取值空間可能連續,使用《萬億級行銷(圈人)邁入毫秒年齡 - 萬億user_tags級即時推薦系統資料庫設計》提到的方法,需要建立標籤庫,將資料階梯化,查詢也要進行轉換。
例如between and這種連續查詢需要轉換為in的散列查詢。使得程式設計更加複雜,(雖然這樣也許可以將效能最大化)。
那麼PostgreSQL有沒有什麼折中的辦法呢?
當然有,一切辦法都是為懶人準備的,懶人推動了社會的進步。
如果你閱讀一下這些文件,你會發現PG裡面辦法還是蠻多的。
1、使用bitmapand,bitmapor+任意索引,解決圈人問題。
《多欄位,任意群組條件查詢(0建模) - 毫秒級即時圈人 最佳實踐》
2、使用varbitx外掛程式,解決圈人問題。
《阿裡雲RDSfor PostgreSQL varbitx外掛程式與即時畫像套用場景介紹》
接下來針對有連續查詢,等值查詢多種群組查詢的圈人場景,我們來看看如何解決。
建模和測試
構建一張TAG表
postgres=# create table tbl_label(uid int primary key, c1 int, c2 text, c3 numeric, c4 timestamp, c5 interval, c6 int);
CREATE TABLE
Time: 31.145 ms
插入一批資料
postgres=# insert into tbl_label select id,
random()*10000, md5(random()::text),
10000*random(), clock_timestamp(),
(random()*1000::int||' hour')::interval,
random()*99999
from generate_series(1,10000000) t(id);
INSERT 0 10000000
資料樣式
postgres=# select * from tbl_label limit 10;
uid |c1|c2|c3|c4|c5|c6
-----+------+----------------------------------+------------------+----------------------------+------------------+-------
1 | 1692 | 505662aa4a6b33e1775cea660063ba58 | 9761.26249413937 | 2017-06-12 18:38:57.515097 | 322:06:55.266882 | 67699
2 | 8611 | a60d564b7f4d58029dfd5e16f0457305 | 1003.07232700288 | 2017-06-12 18:38:57.515282 | 780:59:39.081975 | 89283
3 |290 | f226358e08372d4b79c8ecdd27172244 | 8240.20517989993 | 2017-06-12 18:38:57.515296 | 261:29:59.658099 | 87975
4 | 7829 | 32bc5d97731ddaf2c1630218e43d1e85 | 9061.87939457595 | 2017-06-12 18:38:57.515303 | 760:47:18.267513 | 76409
5 | 7735 | 3813b4bcdaadc21a55da143f6aceeac9 | 6651.74870751798 | 2017-06-12 18:38:57.515309 | 512:45:50.116217 | 11252
6 | 9945 | ff72917169cdea9225e429e438f22586 | 2114.50539063662 | 2017-06-12 18:38:57.515316 | 63:30:34.15014| 33288
7 | 9144 | 7cf4067f22c5edbb1fc4e08ecee7242c | 5662.74457611144 | 2017-06-12 18:38:57.515322 | 890:30:28.360096 | 55484
8 | 2433 | 8ac9732bec2b1c175483c16e82467653 | 9184.17258188128 | 2017-06-12 18:38:57.515328 | 343:34:40.88581| 53265
9 | 8113 | 2dd724e82dc7c2a15dfda45f6a41cd53 | 5094.92502082139 | 2017-06-12 18:38:57.515334 | 635:16:39.096908 | 63410
10 | 3893 | b8abdb00228f09efb04c1e2a8a022c22 | 6397.02362008393 | 2017-06-12 18:38:57.51534| 295:26:24.752753 | 17894
(10 rows)
剖析表的統計資訊
postgres=# analyze tbl_label ;
ANALYZE
查看每列的散列程度
n_distinct解釋
-1表示唯一,也就是說這個列的每一行都不一樣.
>=1時,表示這個列有多少唯一值.
<1時,表示這個列的唯一值數量/總數.
correlation解釋
表示該列與資料堆隱藏的線性關聯性, 1表示正向完全相關。越接近0表示資料散發越離散。<0表示反向相關。
uid是自增的, c4是時間遞增的,所以都是1,完全相關。
postgres=# select tablename,attname,n_distinct,correlation from pg_stats where tablename='tbl_label';
tablename | attname | n_distinct | correlation
-----------+---------+------------+-------------
tbl_label | uid|-1 |1
tbl_label | c1|10018 |0.00431651
tbl_label | c2|-0.957505 | -0.00796595
tbl_label | c3|-1 |0.00308372
tbl_label | c4|-1 |1
tbl_label | c5|-1 | 0.000382809
tbl_label | c6|100688 |0.00156045
(7 rows)
針對以上統計資訊,對於唯一列,建立btree索引,對於鬆散列,建立gin索引(倒排),以達到最好的效果。
為了讓普通類型支援gin,需要建立btree_gin外掛程式
postgres=# create extension btree_gin;
CREATE EXTENSION
建立c1,c6的gin複合索引
postgres=# set maintenance_work_mem ='32GB';
SET
Time: 0.168 ms
postgres=# create index idx_tbl_label_1 on tbl_label using gin(c1,c6);
CREATE INDEX
Time: 1504.542 ms (00:01.505)
查詢測試,查詢c1,c6的任意群組,效果非常棒。
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_label where c1 between 1 and 100;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_label(cost=125.76..8759.97 rows=10074 width=80) (actual time=40.856..50.480 rows=9922 loops=1)
Output: uid, c1, c2, c3, c4, c5, c6
Recheck Cond: ((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100))
Heap Blocks: exact=7222
Buffers: shared hit=7982
->Bitmap Index Scan on idx_tbl_label_1(cost=0.00..123.24 rows=10074 width=0) (actual time=39.773..39.773 rows=9922 loops=1)
Index Cond: ((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100))
Buffers: shared hit=760
Planning time: 0.105 ms
Execution time: 51.043 ms
(10 rows)
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_label where c1 between 1 and 100 or c6=100;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_label(cost=134.36..8799.70 rows=10085 width=80) (actual time=41.133..50.187 rows=9932 loops=1)
Output: uid, c1, c2, c3, c4, c5, c6
Recheck Cond: (((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100)) OR (tbl_label.c6 = 100))
Heap Blocks: exact=7228
Buffers: shared hit=7992
->BitmapOr(cost=134.36..134.36 rows=10085 width=0) (actual time=40.045..40.045 rows=0 loops=1)
Buffers: shared hit=764
->Bitmap Index Scan on idx_tbl_label_1(cost=0.00..123.24 rows=10074 width=0) (actual time=40.031..40.031 rows=9922 loops=1)
Index Cond: ((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100))
Buffers: shared hit=760
->Bitmap Index Scan on idx_tbl_label_1(cost=0.00..6.08 rows=11 width=0) (actual time=0.012..0.012 rows=10 loops=1)
Index Cond: (tbl_label.c6 = 100)
Buffers: shared hit=4
Planning time: 0.125 ms
Execution time: 50.758 ms
(15 rows)
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_label where c1 between 1 and 100 and c6=100;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_label(cost=22.50..24.02 rows=1 width=80) (actual time=36.193..36.193 rows=0 loops=1)
Output: uid, c1, c2, c3, c4, c5, c6
Recheck Cond: ((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100) AND (tbl_label.c6 = 100))
Buffers: shared hit=763
->Bitmap Index Scan on idx_tbl_label_1(cost=0.00..22.50 rows=1 width=0) (actual time=36.190..36.190 rows=0 loops=1)
Index Cond: ((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100) AND (tbl_label.c6 = 100))
Buffers: shared hit=763
Planning time: 0.115 ms
Execution time: 36.226 ms
(9 rows)
建立其他列的btree索引,因為其他列的n_distinct表明這些列基本唯一,所以我們建立btree索引,可以精准的進行尋找。
對於線性關聯性好的列,建立brin索引。後面會講到索引的原理和選擇。
postgres=# create index idx_tbl_label2 on tbl_label using btree(c2);
CREATE INDEX
Time: 1388.756 ms (00:01.389)
postgres=# create index idx_tbl_label3 on tbl_label using btree(c3);
CREATE INDEX
Time: 1028.865 ms (00:01.029)
多列群組查詢,效果非常好
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_label where c1 between 1 and 100 and c6=100 and c2='abc';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_tbl_label2 on public.tbl_label(cost=0.42..3.45 rows=1 width=80) (actual time=0.032..0.032 rows=0 loops=1)
Output: uid, c1, c2, c3, c4, c5, c6
Index Cond: (tbl_label.c2 = 'abc'::text)
Filter: ((tbl_label.c1 >= 1) AND (tbl_label.c1 <= 100) AND (tbl_label.c6 = 100))
Buffers: shared read=3
Planning time: 0.248 ms
Execution time: 0.056 ms
(7 rows)
多個索引通過bitmapAnd, bitmapOr對資料進行遮罩,大幅升階任意條件查詢的效能。原理如下
《多欄位,任意群組條件查詢(0建模) - 毫秒級即時圈人 最佳實踐》
那麼應該如何選擇索引呢?後面會講到。
贈送彩蛋
實際上前面用到的是GIN多列複合索引,還有一種方法,將多列轉換為陣列,然後建立陣列索引(PostgreSQL 運算式索引。)。
1、如何將多列轉換為陣列?
postgres=# create or replace function to_array(VARIADIC anyarray) returns anyarray as $$
select $1;
$$ language sql strict immutable;
CREATE FUNCTION
例子
postgres=# select to_array('a'::text,'b','c');
to_array
----------
{a,b,c}
(1 row)
postgres=# select to_array(now(),clock_timestamp());
to_array
-------------------------------------------------------------------
{"2017-06-12 17:50:47.992274+08","2017-06-12 17:50:47.992489+08"}
(1 row)
postgres=# select to_array(1,2,3);
to_array
----------
{1,2,3}
(1 row)
2、陣列運算式索引
例子
create index idx_tbl_label_combin on tbl_label using gin (to_array(c1,c6));
當列的類型不一致時,可以轉換為一致的,然後建立運算式索引,類型轉換可能需要使用immutable函數,如果沒有則需要自建immutable轉換函式,也很簡單
postgres=# create index idx_tbl_label_combin1 on tbl_label using gin (to_array('c1:'||c1,'c6:'||c6));
3、如何點擊陣列運算式索引
查詢準則與索引中的運算式一致,即可點擊。
例子
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_label where to_array(c1,c6) && array[1,2];
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_label(cost=840.56..86397.30 rows=99750 width=80) (actual time=0.777..4.030 rows=2254 loops=1)
Output: uid, c1, c2, c3, c4, c5, c6
Recheck Cond: (ARRAY[tbl_label.c1, tbl_label.c6] && '{1,2}'::integer[])
Heap Blocks: exact=2242
Buffers: shared hit=2251
->Bitmap Index Scan on idx_tbl_label_combin(cost=0.00..815.62 rows=99750 width=0) (actual time=0.465..0.465 rows=2254 loops=1)
Index Cond: (ARRAY[tbl_label.c1, tbl_label.c6] && '{1,2}'::integer[])
Buffers: shared hit=9
Planning time: 0.361 ms
Execution time: 4.176 ms
(10 rows)
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl_label where to_array('c1:'||c1,'c6:'||c6) && array['c1:1'];
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.tbl_label(cost=422.00..54015.43 rows=50000 width=80) (actual time=0.331..1.888 rows=1021 loops=1)
Output: uid, c1, c2, c3, c4, c5, c6
Recheck Cond: (ARRAY[('c1:'::text || (tbl_label.c1)::text), ('c6:'::text || (tbl_label.c6)::text)] && '{c1:1}'::text[])
Heap Blocks: exact=1019
Buffers: shared hit=1024
->Bitmap Index Scan on idx_tbl_label_combin1(cost=0.00..409.50 rows=50000 width=0) (actual time=0.195..0.195 rows=1021 loops=1)
Index Cond: (ARRAY[('c1:'::text || (tbl_label.c1)::text), ('c6:'::text || (tbl_label.c6)::text)] && '{c1:1}'::text[])
Buffers: shared hit=5
Planning time: 0.173 ms
Execution time: 1.972 ms
(10 rows)
小結
1、什麼時候選擇btree
btree索引適合選擇性好的列(n_distinct很大,或者=-1),唯一值比例越高越適合btree。
2、什麼時候選擇gin
與btree相反,選擇性越差,採用GIN索引效率越高。
另外GIN的倒排屬性,還特別適合多實值型別的元素群組查詢,例如陣列、全文檢索索引類型、TOKEN類型、等等。
同時GIN索引介面是開放的,使用者可以根據資料特徵,自訂GIN索引。支援其他的資料類型,例如影像特徵值相似查詢,文字的相似性查詢等。
3、什麼時候選擇gist
GIST是PG的一種通用索引介面,適合各種資料類型,特別適合異構的類型,例如幾何類型,空間類型,範圍類型等。
GIST索引的原理可參考
《從難纏的柔邊查詢聊開 -PostgreSQL獨門絕招之一 GIN , GiST , SP-GiST , RUM 索引原理與技術幕後》
4、什麼時候選擇hash
如何用好只有等值查詢,並且被索引的列長度很長,可能超過資料庫block的1/3時,建議使用hash索引。 PG 10 hash索引會產生WAL,確保了強固,同時支援流複製。
PG10 以前的組建,不建議使用hash index,crash後需要rebuild,不支援流複製。
5、什麼時候選擇brin
當資料與堆隱藏線性關聯性很好時,可以採用BRIN索引。
BRIN是塊級索引,隱藏每個(或者每一段連續的)資料區塊的原子資訊(最大值,最小值,平均值,空值比例,COUNT等)。
特別適合範圍掃描。
不同的索引方法支援什麼類型的查詢?
1、btree
適合排序、>=, <=, =, in, >,< 等查詢。
2、HASH
適合=查詢。
3、GIN
不同的資料類型,調整不同的查詢需求。
例如數群組類型,適合 交集,包含等。
4、GIST
不同的資料類型,調整不同的查詢需求。
例如空間類型,適合,距離排序,KNN,包含,交集,左,右等。
5、BRIN
適合範圍查詢,=查詢。
如何優化索引效率
前面的方法告訴你應該如何選擇索引,但是沒有提索引本身的優化,實際上資料散發會影響索引的效率。
例如
《索引順序掃描引發的堆掃描IO放大背後的統計學原理與解決辦法 - PostgreSQL index scan enlarge heap page scanswhen index and column correlation small.》
因此,根據索引的掃描特點,對資料進行重散發,可以大幅度優化索引查詢的效率。
例如bitmap index scan(按BLOCKID順序讀取)就是PostgreSQL用於減少離散IO的手段。
1、btree資料散發優化
線性相關越好,掃描或返回多條資料的效率越高。
2、hash資料散發優化
線性相關越好,掃描或返回多條資料的效率越高。
3、gin資料散發優化
如果是普通類型,則線性相關越好,掃描或返回多條資料的效率越高。
如果是多實值型別(如陣列、全文檢索索引、TOKENs),則元素越暫留(元素群集,橫坐標為行號,縱坐標為元素值,資料散發越暫留),效率越高。
元素暫留通常不好實現,但是我們可以有暫留方法來聚集資料,1. 根據元素的出現頻率進行排序重組,當用戶搜尋高頻詞時,掃描的塊更少,減少IO放大。2. 根據(被搜尋元素的次數*點擊條數)的值進行排序,按排在最前的元素進行聚集,逐級聚集。
(以上方法可能比較燒腦,下次發一篇文件專門講GIN的資料重組優化)
《索引掃描優化之 -GIN資料重組優化(按元素彙總) 想像在玩多階魔方》
4、gist資料散發優化
如果是普通類型,則線性相關越好,掃描或返回多條資料的效率越高。
如果是空間類型,則元素越暫留(例如資料按geohash連續散發),效率越高。
5、brin資料散發優化
線性相關越好,掃描或返回多條資料的效率越高。
6、多列複合索引資料散發優化
對於多列符合索引,則看索引的類型,要求與前面一樣。
增加一個,多個列的線性關聯性越好,效能越好。
多列線性關聯性計算方法如下
《PostgreSQL計算 任意類型 欄位之間的線性關聯性》
資料散發還有一個好處,對於列隱藏,可以大幅升階壓縮比
《一個簡單演算法可以說明物聯網,金融 用戶 節約98%的資料存放區成本(PostgreSQL,Greenplum幫你做到)》
參考
《阿裡雲RDSfor PostgreSQL varbitx外掛程式與即時畫像套用場景介紹》
《多欄位,任意群組條件查詢(0建模) - 毫秒級即時圈人 最佳實踐》
《PostgreSQLGIN 單列叢集索引 套用》
《寶劍贈英雄 - 任意群組欄位等效查詢, 探探PostgreSQL多列展開式B樹(GIN)》
《PostgreSQLGIN索引實現原理》
《從難纏的柔邊查詢聊開 -PostgreSQL獨門絕招之一 GIN , GiST , SP-GiST , RUM 索引原理與技術幕後》
《PostgreSQL9.3 pg_trgm imporve support multi-bytes char and gist,gin index for reg-expsearch》
《萬億級行銷(圈人)邁入毫秒年齡 - 萬億user_tags級即時推薦系統資料庫設計》
相關產品:
1.物件隱藏OSS
2.巨量資料計算服務(MaxCompute)
3.資料管理
4.雲端服務器ECS