Postgresql排序與limit組合場景效能極限優化詳解
1 構造測試資料
create table tbl(id int,num int,arr int[]); create index idx_tbl_arr on tbl using gin (arr); create or replace function gen_rand_arr() returns int[] as $$ select array(select (1000*random())::int from generate_series(1,64)); $$ language sql strict; insert into tbl select generate_series(1,3000000),(10000*random())::int,gen_rand_arr(); insert into tbl select generate_series(1,500),array[350,514,213,219,528,753,270,321,413,424,524,435,546,765,234,345,131,351];
2 查詢走GIN索引
測試場景的限制GIN索引查詢速度是很快的, 在實際生產中,可能出現使用gin索引後,查詢速度依然很高的情況,特點就是執行計劃中Bitmap Heap Scan佔用了大量時間,Bitmap Index Scan大部分標記的塊都被過濾掉了。
這種情況是很常見的,一般的btree索引可以cluster來重組資料,但是gin索引是不支援cluster的,一般的gin索引列都是陣列型別。所以當出現數據非常分散的情況時,bitmap index scan會標記大量的塊,後面recheck的成本非常高,導致gin索引查詢慢。
我們接著來看這個例子
explain analyze select * from tbl where arr @> array[350,270] order by num desc limit 20; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=2152.02..2152.03 rows=1 width=40) (actual time=57.665..57.668 rows=20 loops=1) -> Sort (cost=2152.02..2152.03 rows=1 width=40) (actual time=57.664..57.665 rows=20 loops=1) Sort Key: num Sort Method: top-N heapsort Memory: 27kB -> Bitmap Heap Scan on tbl (cost=2148.00..2152.01 rows=1 width=40) (actual time=57.308..57.581 rows=505 loops=1) Recheck Cond: (arr @> '{350,270}'::integer[]) Heap Blocks: exact=493 -> Bitmap Index Scan on idx_tbl_arr (cost=0.00..2148.00 rows=1 width=0) (actual time=57.248..57.248 rows=505 loops=1) Index Cond: (arr @> '{350,270}'::integer[]) Planning time: 0.050 ms Execution time: 57.710 ms
可以看到當前執行計劃是依賴gin索引掃描的,但gin索引出現效能問題時我們如何來優化呢?
3 排序limit組合場景優化
SQL中的排序與limit組合是一個很典型的索引優化創景。我們知道btree索引在記憶體中是有序的,通過遍歷btree索引可以直接拿到sort後的結果,這裡組合使用limit後,只需要遍歷btree的一部分節點然後按照其他條件recheck就ok了。
我們來看一下優化方法:
create index idx_tbl_num on tbl(num); analyze tbl; set enable_seqscan = off; set enable_bitmapscan = off; postgres=# explain analyze select * from tbl where arr @> array[350,270] order by num desc limit 10; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------ Limit (cost=0.43..571469.93 rows=1 width=287) (actual time=6.300..173.949 rows=10 loops=1) -> Index Scan Backward using idx_tbl_num on tbl (cost=0.43..571469.93 rows=1 width=287) (actual time=6.299..173.943 rows=10 loops=1) Filter: (arr @> '{350,270}'::integer[]) Rows Removed by Filter: 38399 Planning time: 0.125 ms Execution time: 173.972 ms (6 rows) Time: 174.615 ms postgres=# cluster tbl using idx_tbl_num; CLUSTER Time: 124340.276 ms postgres=# explain analyze select * from tbl where arr @> array[350,270] order by num desc limit 10; QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.43..563539.77 rows=1 width=287) (actual time=1.145..34.602 rows=10 loops=1) -> Index Scan Backward using idx_tbl_num on tbl (cost=0.43..563539.77 rows=1 width=287) (actual time=1.144..34.601 rows=10 loops=1) Filter: (arr @> '{350,270}'::integer[]) Rows Removed by Filter: 38399 Planning time: 0.206 ms Execution time: 34.627 ms (6 rows)
本例的測試場景構造可能沒有最大程度的體現問題,不過可以看出cluster後走btree索引可以很穩定的達到34ms左右。
在gin效能存在問題的時候,這類limit + order by的SQL語句不妨常識強制(pg_hint_plan)走一下btree索引,可能有意想不到的效果。
4 高併發場景下的gin索引查詢效能下降
GIN索引為PostgreSQL資料庫多值型別的倒排索引,一條記錄可能涉及到多個GIN索引中的KEY,所以如果寫入時實時合併索引,會導致IO急劇增加,寫入RT必然增加。為了提高寫入吞吐,PG允許使用者開啟GIN索引的延遲合併技術,開啟後,資料會先寫入pending list,並不是直接寫入索引頁,當pending list達到一定大小,或者autovacuum 對應表時,會觸發pending list合併到索引的動作。
查詢時,如果有未合併到索引中的PENDING LIST,那麼會查詢pending list,同時查詢索引也的資訊。
如果寫入量很多,pending list非常巨大,合併(autovacuum worker做的)速度跟不上時,會導致通過GIN索引查詢時查詢效能下降。
create extension pageinspect ; SELECT * FROM gin_metapage_info(get_raw_page('idx_tbl_arr',0)); -- 如果很多條記錄在pending list中,查詢效能會下降明顯。 -- vacuum table,強制合併pending list vacuum tbl;
第4部分引用https://github.com/digoal/blog/blob/master/201809/20180919_02.md
補充:PostgreSQL -- 效能優化的小方法
一、回收磁碟空間
在PostgreSQL中,使用delete和update語句刪除或更新的資料行並沒有被實際刪除,而只是在舊版本資料行的實體地址上將該行的狀態置為已刪除或已過期。因此當資料表中的資料變化極為頻繁時,那麼在一段時間之後該表所佔用的空間將會變得很大,然而資料量卻可能變化不大。要解決該問題,需要定期對資料變化頻繁的資料表執行VACUUM操作。現在新版PostgreSQL是自動執行VACUUM的
使用VACUUM和VACUUM FULL命令回收磁碟空間
postgres=# vacuum arr_test;
postgres=# vacuum full arr_test;
建立測試資料:
postgres=# create table arr (id serial,value int,age int) #建立測試表 postgres=# insert into arr (value,age) select generate_series(1,1000000) as value,(random()*(10^2))::integer; #插入100W測試資料 postgres=# select pg_relation_size('arr'); #查看錶大小 pg_relation_size ------------------ 44285952 (1 row) postgres=# delete from arr where id<300000; #刪除299999條資料 DELETE 299999 postgres=# select pg_relation_size('arr'); #再次查看錶大小,沒有變化 pg_relation_size ------------------ 44285952 (1 row) postgres=# vacuum full arr; #vacuum表,再次查看錶大小,明顯變小了 VACUUM postgres=# select pg_relation_size('arr'); pg_relation_size ------------------ 30998528 (1 row) postgres=# update arr set age=10000 where id>=300000 and id<600000; #更新30W條資料 UPDATE 300000 postgres=# select pg_relation_size('arr'); #查看錶大小,明顯再次增大 pg_relation_size ------------------ 44285952 (1 row)
二、重建索引
在PostgreSQL中,為資料更新頻繁的資料表定期重建索引(REINDEX INDEX)是非常有必要的。
對於B-Tree索引,只有那些已經完全清空的索引頁才會得到重複使用,對於那些僅部分空間可用的索引頁將不會得到重用,如果一個頁面中大多數索引鍵值都被刪除,只留下很少的一部分,那麼該頁將不會被釋放並重用。
在這種極端的情況下,由於每個索引頁面的利用率極低,一旦資料量顯著增加,將會導致索引檔案變得極為龐大,不僅降低了查詢效率,而且還存在整個磁碟空間被完全填滿的危險。
對於重建後的索引還存在另外一個性能上的優勢,因為在新建立的索引上,邏輯上相互連線的頁面在物理上往往也是連在一起的,這樣可以提高磁碟頁面被連續讀取的機率,從而提高整個操作的IO效率
postgres=# REINDEX INDEX testtable_idx;
三、重新收集統計資訊
PostgreSQL查詢規劃器在選擇最優路徑時,需要參照相關資料表的統計資訊用以為查詢生成最合理的規劃。這些統計是通過ANALYZE命令獲得的,你可以直接呼叫該命令,或者把它當做VACUUM命令裡的一個可選步驟來呼叫,如VACUUM ANAYLYZE table_name,該命令將會先執行VACUUM再執行ANALYZE。與回收空間(VACUUM)一樣,對資料更新頻繁的表保持一定頻度的ANALYZE,從而使該表的統計資訊始終處於相對較新的狀態,這樣對於基於該表的查詢優化將是極為有利的。然而對於更新並不頻繁的資料表,則不需要執行該操作。
我們可以為特定的表,甚至是表中特定的欄位執行ANALYZE命令,這樣我們就可以根據實際情況,只對更新比較頻繁的部分資訊執行ANALYZE操作,這樣不僅可以節省統計資訊所佔用的空間,也可以提高本次ANALYZE操作的執行效率。
這裡需要額外說明的是,ANALYZE是一項相當快的操作,即使是在資料量較大的表上也是如此,因為它使用了統計學上的隨機取樣的方法進行行取樣,而不是把每一行資料都讀取進來並進行分析。因此,可以考慮定期對整個資料庫執行該命令。
事實上,我們甚至可以通過下面的命令來調整指定欄位的抽樣率
如:
ALTER TABLE testtable ALTER COLUMN test_col SET STATISTICS 200
注意:該值的取值範圍是0--1000,其中值越低取樣比例就越低,分析結果的準確性也就越低,但是ANALYZE命令執行的速度卻更快。如果將該值設定為-1,那麼該欄位的取樣比率將恢復到系統當前預設的取樣值,我們可以通過下面的命令獲取當前系統的預設取樣值。
postgres=# show default_statistics_target; default_statistics_target --------------------------- 100 (1 row)
從上面的結果可以看出,該資料庫的預設取樣值為100(10%)。
postgresql 效能優化
一、排序:
1. 儘量避免
2. 排序的資料量儘量少,並保證在記憶體裡完成排序。
(至於具體什麼資料量能在記憶體中完成排序,不同資料庫有不同的配置:
oracle是sort_area_size;
postgresql是work_mem (integer),單位是KB,預設值是4MB。
mysql是sort_buffer_size 注意:該引數對應的分配記憶體是每連線獨佔!
)
二、索引:
1. 過濾的資料量比較少,一般來說<20%,應該走索引。20%-40% 可能走索引也可能不走索引。> 40% ,基本不走索引(會全表掃描)
2. 保證值的資料型別和欄位資料型別要一致。
3. 對索引的欄位進行計算時,必須在運算子右側進行計算。也就是 to_char(oc.create_date,‘yyyyMMdd')是沒用的
4. 表字段之間關聯,儘量給相關欄位上新增索引。
5. 複合索引,遵從最左字首的原則,即最左優先。(單獨右側欄位查詢沒有索引的)
三、連線查詢方式:
1、hash join
放記憶體裡進行關聯。
適用於結果集比較大的情況。
比如都是200000資料
2、nest loop
從結果1 逐行取出,然後與結果集2進行匹配。
適用於兩個結果集,其中一個數據量遠大於另外一個時。
結果集一:1000
結果集二:1000000
四、多表聯查時:
在多表聯查時,需要考慮連線順序問題。
1、當postgresql中進行查詢時,如果多表是通過逗號,而不是join連線,那麼連線順序是多表的笛卡爾積中取最優的。如果有太多輸入的表, PostgreSQL規劃器將從窮舉搜尋切換為基因概率搜尋,以減少可能性數目(樣本空間)。基因搜尋花的時間少, 但是並不一定能找到最好的規劃。
2、對於JOIN,LEFT JOIN / RIGHT JOIN 會一定程度上指定連線順序,但是還是會在某種程度上重新排列:FULL JOIN 完全強制連線順序。如果要強制規劃器遵循準確的JOIN連線順序,我們可以把執行時引數join_collapse_limit設定為 1
五、PostgreSQL提供了一些效能調優的功能:
優化思路:
0、為每個表執行 ANALYZE
。然後分析 EXPLAIN (ANALYZE,BUFFERS) sql。
1、對於多表查詢,檢視每張表資料,然後改進連線順序。
2、先查詢那部分是重點語句,比如上面SQL,外面的巢狀層對於優化來說沒有意義,可以去掉。
3、檢視語句中,where等條件子句,每個欄位能過濾的效率。找出可優化處。
比如oc.order_id = oo.order_id是關聯條件,需要加索引
oc.op_type = 3 能過濾出1/20的資料,
oo.event_type IN (…) 能過濾出1/10的資料,
這兩個是優化的重點,也就是實現確保op_type與event_type已經加了索引,其次確保索引用到了。
優化方案:
a) 整體優化:
1、使用EXPLAIN
EXPLAIN命令可以檢視執行計劃,這個方法是我們最主要的除錯工具。
2、及時更新執行計劃中使用的統計資訊
由於統計資訊不是每次操作資料庫都進行更新的,一般是在 VACUUM 、 ANALYZE 、 CREATE INDEX等DDL執行的時候會更新統計資訊,
因此執行計劃所用的統計資訊很有可能比較舊。 這樣執行計劃的分析結果可能誤差會變大。
以下是表tenk1的相關的一部分統計資訊。
SELECT relname,relkind,reltuples,relpages FROM pg_class WHERE relname LIKE 'tenk1%'; relname | relkind | reltuples | relpages ----------------------+---------+-----------+---------- tenk1 | r | 10000 | 358 tenk1_hundred | i | 10000 | 30 tenk1_thous_tenthous | i | 10000 | 30 tenk1_unique1 | i | 10000 | 30 tenk1_unique2 | i | 10000 | 30 (5 rows)
其中 relkind是型別,r是自身表,i是索引index;reltuples是專案數;relpages是所佔硬碟的塊數。
估計成本通過 (磁碟頁面讀取【relpages】*seq_page_cost)+(行掃描【reltuples】*cpu_tuple_cost)計算。
預設情況下, seq_page_cost是1.0,cpu_tuple_cost是0.01。
3、使用臨時表(with)
對於資料量大,且無法有效優化時,可以使用臨時表來過濾資料,降低資料數量級。
4、對於會影響結果的分析,可以使用 begin;…rollback;來回滾。
b) 查詢優化:
1、明確用join來關聯表,確保連線順序
一般寫法:SELECT * FROM a,b,c WHERE a.id = b.id AND b.ref = c.id;
如果明確用join的話,執行時候執行計劃相對容易控制一些。
例子:
SELECT * FROM a CROSS JOIN b CROSS JOIN c WHERE a.id = b.id AND b.ref = c.id;
SELECT * FROM a JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);
c) 插入更新優化
1、關閉自動提交(autocommit=false)
如果有多條資料庫插入或更新等,最好關閉自動提交,這樣能提高效率
2、多次插入資料用copy命令更高效
我們有的處理中要對同一張表執行很多次insert操作。這個時候我們用copy命令更有效率。因為insert一次,其相關的index都要做一次,比較花費時間。
3、臨時刪除index【具體可以檢視Navicat表資料生成sql的語句,就是先刪再建的】
有時候我們在備份和重新匯入資料的時候,如果資料量很大的話,要好幾個小時才能完成。這個時候可以先把index刪除掉。匯入後再建index。
4、外來鍵關聯的刪除
如果表的有外來鍵的話,每次操作都沒去check外來鍵整合性。因此比較慢。資料匯入後再建立外來鍵也是一種選擇。
d) 修改引數:
介紹幾個重要的
1、增加maintenance_work_mem引數大小
增加這個引數可以提升CREATE INDEX和ALTER TABLE ADD FOREIGN KEY的執行效率。
2、增加checkpoint_segments引數的大小
增加這個引數可以提升大量資料匯入時候的速度。
3、設定archive_mode無效
這個引數設定為無效的時候,能夠提升以下的操作的速度
?CREATE TABLE AS SELECT
?CREATE INDEX
?ALTER TABLE SET TABLESPACE
?CLUSTER等。
4、autovacuum相關引數
autovacuum:預設為on,表示是否開起autovacuum。預設開起。特別的,當需要凍結xid時,儘管此值為off,PG也會進行vacuum。
autovacuum_naptime:下一次vacuum的時間,預設1min。 這個naptime會被vacuum launcher分配到每個DB上。autovacuum_naptime/num of db。
log_autovacuum_min_duration:記錄autovacuum動作到日誌檔案,當vacuum動作超過此值時。 “-1”表示不記錄。“0”表示每次都記錄。
autovacuum_max_workers:最大同時執行的worker數量,不包含launcher本身。
autovacuum_work_mem :每個worker可使用的最大記憶體數。
autovacuum_vacuum_threshold :預設50。與autovacuum_vacuum_scale_factor配合使用, autovacuum_vacuum_scale_factor預設值為20%。當update,delete的tuples數量超過autovacuum_vacuum_scale_factor *table_size+autovacuum_vacuum_threshold時,進行vacuum。如果要使vacuum工作勤奮點,則將此值改小。
autovacuum_analyze_threshold :預設50。與autovacuum_analyze_scale_factor配合使用。
autovacuum_analyze_scale_factor :預設10%。當update,insert,delete的tuples數量超過autovacuum_analyze_scale_factor *table_size+autovacuum_analyze_threshold時,進行analyze。
autovacuum_freeze_max_age:200 million。離下一次進行xid凍結的最大事務數。
autovacuum_multixact_freeze_max_age:400 million。離下一次進行xid凍結的最大事務數。
autovacuum_vacuum_cost_delay :如果為-1,取vacuum_cost_delay值。
autovacuum_vacuum_cost_limit :如果為-1,到vacuum_cost_limit的值,這個值是所有worker的累加值。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援我們。如有錯誤或未考慮完全的地方,望不吝賜教。