EXPLAIN分析pgsql的性能
前言
對於pgsql中查詢性能的分析,好像不想mysql中那么簡單。當然pgsql中也是通過EXPLAIN進行分析,那么就來認真中結下pgsql中explain的使用。
EXPLAIN命令
EXPLAIN -- 顯示一個語句的執行計划
EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
這里 option可以是:
ANALYZE [ boolean ]
VERBOSE [ boolean ]
COSTS [ boolean ]
BUFFERS [ boolean ]
TIMING [ boolean ]
FORMAT { TEXT | XML | JSON | YAML }
ANALYZE選項通過實際執行的sql來獲取相應的計划。這個是真正執行的,多以可以真是的看到執行計划花費了多少的時間,還有它返回的行數。
當然對於分析插入更新的語句,我們我們是可以把ANALYZE放到事物里面的,分析后之后回滾。
BEGIN;
EXPLAIN ANALYZE ...;
ROLLBACK;
命令詳解
VERBOSE選項用於顯示計划的附加信息。這些附加的信息有:計划中每個節點輸出的各個列,如果觸發器被觸發,還會輸出觸發器的名稱。該選項默認為FALSE。
COSTS選項顯示每個計划節點的啟動成本和總成本,以及估計行數和每行寬度。該選項默認是TRUE。
BUFFERS選項顯示關於緩存區的信息。該選項只能與ANALYZE參數一起使用。顯示的緩存區信息包括共享快塊,本地塊和臨時讀和寫的塊數。共享塊、本地塊和臨時塊分別包含表和索引、臨時快和臨時索引、以及在排序和物化計划中使用的磁盤塊。上層節點顯示出來的數據塊包含其所有子節點使用的塊數。該選項默認為FALSE。
EXPLAIN輸出結果展示
explain select * from test1
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Seq Scan on test1 (cost=0.00..146666.56 rows=7999956 width=33)
Seq Scan
表示的是全表掃描,就是從頭掃尾掃描一遍表里面的數據。
(cost=0.00..146666.56 rows=7999956 width=33)
的內容可以分成三部分
- cost=0.00..146666.56 cost后面有兩個數字,中間使用
..
分割,第一個數字0.00
表示啟動的成本,也就是返回第一行需要多少cost值;第二行表示返回所有數據的成本。 - rows=7999956:表示會返回7999956行
- width=33:表示每行的數據寬度為33字節
其中cost
描述的是一個sql
執行的代價。
analyze
通過analyze
可以看到更加精確的執行計划。
explain analyze select * from test1
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Seq Scan on test1 (cost=0.00..146666.56 rows=7999956 width=33) (actual time=0.012..1153.317 rows=8000001 loops=1)
Planning time: 0.049 ms
Execution time: 1637.480 ms
加了analyze
可以看到實際的啟動時間,(actual time=0.012..1153.317 rows=8000001 loops=1)
其中:
- actual time=0.012..1153.317:
0.012
表示的是啟動的時間,..
后面的時間表示返回所有行需要的時間 - rows=8000001:表示返回的行數
buffers
通過使用buffers來查看緩存區命中的情況
explain (analyze,buffers) select * from test1
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Seq Scan on test1 (cost=0.00..146666.56 rows=7999956 width=33) (actual time=0.013..1166.464 rows=8000001 loops=1)
Buffers: shared hit=777 read=65890
Planning time: 0.049 ms
Execution time: 1747.163 ms
其中會多出一行Buffers: shared hit=777 read=65890
。
- shared hit=777:表示的在共享內存里面直接讀到
777
個塊; - read=65890:表示從磁盤中讀了
65890
塊 - written: 表示寫磁盤共
xx
塊
通過上面數字進行分析,我們可以知道那些部分是對I/O
最敏感的。
全表掃描
全表掃描在pgsql
中叫做順序掃描(seq scan
),全表掃描就是把表的的所有的數據從頭到尾讀取一遍,然后從數據塊中找到符合條件的數據塊。
索引掃描
索引是為了加快數據查詢的速度索引而增加的(Index Scan
)。索引掃描也就是我們的查詢條件使用到了我們創建的索引,當然什么是索引,自行查閱資料吧。
位圖掃描
位圖掃描也是走索引的一種方式。方法是掃描索引,那滿足條件的行或者塊在內存中建立一個位圖,掃描完索引后,再根據位圖到表的數據文件中把相應的數據讀出來。如果走了兩個索引,可以把兩個索引進行’and‘或‘or’計算,合並到一個位圖,再到表的數據文件中把數據讀出來。
條件過濾
就是在where后面加上過濾條件,掃描數據行,會找出滿足條件過濾的行。條件過濾執行計划中顯示為‘Filter’。
Nestloop join
對於被連接的數據子集較小的情況,Nested Loop
是個較好的選擇。Nested Loop
是連表查詢最朴素的一種連接方式。在嵌套循環的時候,內表被外表驅動,外表中返回的每一行,都要在內表中檢索找尋和它匹配的行。整個查詢返回的結果集不能太大(>10000不適合),要把返回子集比較小的表作為外表,同時內表中連接查詢的字段,最好能命中索引,不然會有性能問題。
執行的過程:確定一個驅動表(outer table),另一個表為inner table,驅動表中的每一行數據會去inner表中,查找檢索數據。注意,驅動表的每一行都去inner表中檢索,索引驅動表的數據不能太大。對於inner表中的數據就沒有限制了,只要創建的索引合適,inner表中數據的大小對查詢的性能影響不大。
測試下:
創建數據表
create table test1
(
id bigserial not null
constraint test1_pk
primary key,
name text not null,
category_id bigint not null
);
create index test1_category_id_index
on test1 (category_id);
create table test2
(
id bigserial not null
constraint test2_pk
primary key,
name text not null,
category_id bigint not null
);
create index test2_category_id_index
on test2 (category_id);
插入數據,test1插入8000000條,test2插入6000000條
do $$
declare
v_idx integer := 1;
begin
while v_idx < 8000000 loop
v_idx = v_idx+1;
insert into test1 (name, category_id) values ( random()*(20000000000000000-10)+10,random()*(8000000-10)+10);
end loop;
end $$;
do $$
declare
v_idx integer := 1;
begin
while v_idx < 6000000 loop
v_idx = v_idx+1;
insert into test2 (name, category_id) values ( random()*(20000000000000000-10)+10,random()*(6000000-10)+10);
end loop;
end $$;
驗證下查詢
explain select a.id,b.id from test1 a,test2 b where a.category_id=b.category_id and a.id<10000
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Gather (cost=1170.58..67037.87 rows=14666 width=16)
Workers Planned: 2
-> Nested Loop (cost=170.58..64571.27 rows=6111 width=16)
-> Parallel Bitmap Heap Scan on test1 a (cost=170.15..24939.15 rows=3748 width=16)
Recheck Cond: (id < 10000)
-> Bitmap Index Scan on test1_pk (cost=0.00..167.90 rows=8996 width=0)
Index Cond: (id < 10000)
-> Index Scan using test2_category_id_index on test2 b (cost=0.43..10.55 rows=2 width=16)
Index Cond: (category_id = a.category_id)
可以看到當有高選擇性索引或進行限制性,查詢優化器會自動選擇Nested Loop
Hash join
優化器使用兩個表較小的表,並利用連接鍵在內存中建立散列表,然后掃描較大的表並探測散列表,找出與散列表匹配的列。
這種方式適用於較小的表可以完全放入到內存中,這樣總成本就是訪問兩個表的成本之和。但是如果表都很大,不能放入到內存中,優化器會將它分割成若干個不同的分區,把不能放入到內存的部分寫入到臨時段。此時要求有較大的臨時段從而盡量提高I/O 的性能。它能夠很好的工作於沒有索引的大表和並行查詢的環境中,並提供最好的性能。
優化器會自動選擇較小的表,建立散列表,然后掃描另個較大的表。Hash Join
只能應用於等值連接(如WHERE A.COL3 = B.COL4),這是由Hash的特點決定的。
測試下
drop index test1_category_id_index;
drop index test2_category_id_index;
刪除關聯查詢的字段的索引
explain select a.id,b.id from test1 a,test2 b where a.category_id=b.category_id and a.id<10000
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Hash Join (cost=214297.39..248684.97 rows=14666 width=16)
Hash Cond: (a.category_id = b.category_id)
-> Index Scan using test1_pk on test1 a (cost=0.43..335.86 rows=8996 width=16)
Index Cond: (id < 10000)
-> Hash (cost=109999.98..109999.98 rows=5999998 width=16)
-> Seq Scan on test2 b (cost=0.00..109999.98 rows=5999998 width=16)
因為沒有索引了,並且查詢的條數可以完全放入到內存里面,所以查詢優化器就選擇使用Hash join
了,對於選擇那個表建立散列表,要看查詢的條件。如上面的限制條件a.id<10000
,限制了a表查詢的數據條數,那么a表條數較少,然后就在a表建立散列表,然后掃描b表。
Merge Join
通常情況下散列連接的效果比合並連接的效果好,如果源數據上有索引,或者結果已經排過序,在執行順序合並連接時就不需要排序了,這時合並連接的性能會優於散列連接。
Merge join
的操作步驟:
1' 對連接的每個表做table access full;
2' 對table access full的結果進行排序;
3' 進行merge join對排序結果進行合並。
Merge Join
可適於於非等值Join(>,<,>=,<=,但是不包含!=,也即<>)
explain select a.id,b.id from test1 a,test2 b where a.category_id=b.category_id
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Merge Join (cost=2342944.84..2575285.64 rows=13041968 width=16)
Merge Cond: (b.category_id = a.category_id)
-> Sort (cost=1005574.67..1020574.66 rows=5999998 width=16)
Sort Key: b.category_id
-> Seq Scan on test2 b (cost=0.00..124999.98 rows=5999998 width=16)
Filter: (id < 100000000)
-> Materialize (cost=1337364.94..1377364.72 rows=7999956 width=16)
-> Sort (cost=1337364.94..1357364.83 rows=7999956 width=16)
Sort Key: a.category_id
-> Seq Scan on test1 a (cost=0.00..146666.56 rows=7999956 width=16)
category_id
上面是沒有索引的,這時候查詢選擇了Merge Join
,上面的Sort Key: a.category_id
,就是對a表的category_id
字段排序。
Nested Loop,Hash JOin,Merge Join對比
類別 | Nested Loop | Hash Join | Merge Join |
---|---|---|---|
使用條件 | 任何條件 | 等值連接(=) | 等值或非等值連接(>,<,=,>=,<=),‘<>’除外 |
相關資源 | CPU、磁盤I/O | 內存、臨時空間 | 內存、臨時空間 |
特點 | 當有高選擇性索引或進行限制性搜索時效率比較高,能夠快速返回第一次的搜索結果。 | 當缺乏索引或者索引條件模糊時,Hash Join比Nested Loop有效。通常比Merge Join快,如果有索引,或者結果已經被排序了,這時候Merge Join的查詢更快。在數據倉庫環境下,如果表的紀錄數多,效率高。 | 當缺乏索引或者索引條件模糊時,Merge Join比Nested Loop有效。非等值連接時,Merge Join比Hash Join更有效 |
缺點 | 當索引丟失或者查詢條件限制不夠時,效率很低;當表的紀錄數多時,效率低。 | 為建立哈希表,需要大量內存。第一次的結果返回較慢。 | 所有的表都需要排序。它為最優化的吞吐量而設計,並且在結果沒有全部找到前不返回數據。 |