EXPLAIN分析pgsql的性能


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更有效
缺點 當索引丟失或者查詢條件限制不夠時,效率很低;當表的紀錄數多時,效率低。 為建立哈希表,需要大量內存。第一次的結果返回較慢。 所有的表都需要排序。它為最優化的吞吐量而設計,並且在結果沒有全部找到前不返回數據。

參考

【EXPLAIN】http://www.postgres.cn/docs/9.5/sql-explain.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM