os: centos 7.4
postgresql: 9.6.8
explain 是 postgresql 查看執行計划最直接的方式。
explain 語法
EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
這里 option可以是:
ANALYZE [ boolean ]
VERBOSE [ boolean ]
COSTS [ boolean ]
BUFFERS [ boolean ]
TIMING [ boolean ]
FORMAT { TEXT | XML | JSON | YAML }
explain 查看
現在有 user,role,user_role 三個表的關聯,來獲取登錄用戶的角色權限。
如下
mondb=# explain
SELECT tu.user_id,
tu.user_first_name::text || tu.user_last_name::text AS user_name,
tr.role_id,
tr.role_name
FROM t_base_user tu,
t_base_role tr,
t_base_user_role tur
WHERE 1 = 1
AND tu.user_id = tur.user_id
AND tr.role_id = tur.role_id
AND tu.user_status = '1'
AND tr.role_status = '1'
ORDER BY tr.role_id,
tu.user_id;
QUERY PLAN
---------------------------------------------------------------------------------------
Sort (cost=3.94..3.97 rows=9 width=57)
Sort Key: tr.role_id, tu.user_id -> Hash Join (cost=2.41..3.80 rows=9 width=57)
Hash Cond: ((tur.role_id)::text = (tr.role_id)::text) -> Hash Join (cost=1.24..2.48 rows=9 width=19)
Hash Cond: ((tur.user_id)::text = (tu.user_id)::text) -> Seq Scan on t_base_user_role tur (cost=0.00..1.11 rows=11 width=9) -> Hash (cost=1.12..1.12 rows=9 width=16) -> Seq Scan on t_base_user tu (cost=0.00..1.12 rows=9 width=16)
Filter: ((user_status)::text = '1'::text) -> Hash (cost=1.09..1.09 rows=7 width=19) -> Seq Scan on t_base_role tr (cost=0.00..1.09 rows=7 width=19)
Filter: ((role_status)::text = '1'::text)
(13 rows)
mondb=#
以這一行為例:-> Seq Scan on t_base_user_role tur (cost=0.00..1.11 rows=11 width=9) ,說下每行后面類似 (cost=0.00..1.11 rows=11 width=9) 所代表的含義。
有這個輸出是因為 option 的 COSTS 默認為 true,即 explain 等價於 explain (COSTS true)
那這個 COSTS 的具體含義是:包括每一個計划結點的估計啟動和總代價,以及估計的行數和每行的寬度。這個參數默認被設置為TRUE。
顯示中最重要的部分是估計出的語句執行代價,它是計划器對於該語句要運行多久的猜測(以任意的代價單位度量,但是習慣上表示取磁盤頁面的次數)。
事實上會顯示兩個數字:在第一行能被返回前的啟動代價,以及返回所有行的總代價。
對於大部分查詢來說總代價是最重要的,但是在一些情景中(如EXISTS中的子查詢),計划器將選擇更小的啟動代價來代替最小的總代價(因為因為執行器將在得到一行后停止)。此外,如果你用一個LIMIT子句限制返回行的數量,計划器會在終端代價之間做出適當的插值來估計到底哪個計划是真正代價最低的。
cost
總時間(單位:毫秒)
cost=0.00..1.11 是怎么計算出來的?估計很多人都想搞清楚。
其中:
0.00代表估計的啟動開銷。在輸出階段可以開始之前消耗的時間,例如在一個排序結點里執行排序的時間。
1.11代表估計的總開銷。這個估計值基於的假設是計划結點會被運行到完成,即所有可用的行都被檢索。實際上一個結點的父結點可能很快停止讀所有可用的行
這里涉及了postgresql 幾個參數:
seq_page_cost: 連續塊掃描操作的單個塊的cost. 例如全表掃描
random_page_cost: 隨機塊掃描操作的單個塊的cost. 例如索引掃描
cpu_tuple_cost: 處理每條記錄的CPU開銷(tuple:關系中的一行記錄)
cpu_index_tuple_cost:掃描每個索引條目帶來的CPU開銷
cpu_operator_cost: 操作符或函數帶來的CPU開銷.(需要注意函數以及操作符對應的函數的三態, 執行計划會根據三態做優化, 關系到多條記錄時三態對應的調用次數是需要關心的)
查看當前設置
mondb=# select name,setting from pg_settings ps where ps.name in ('seq_page_cost','random_page_cost','cpu_tuple_cost','cpu_index_tuple_cost','cpu_operator_cost');
name | setting
----------------------+---------
cpu_index_tuple_cost | 0.005
cpu_operator_cost | 0.0025
cpu_tuple_cost | 0.01
random_page_cost | 4
seq_page_cost | 1
(5 rows)
總成本=seq_page_cost*relpages+cpu_tuple_cost*reltuples
mondb=# select relpages,reltuples from pg_class where relname='t_base_user_role';
relpages | reltuples ----------+-----------
1 | 11
(1 row)
帶入公式:
1(seq_page_cost)*1(relpages)+0.01(cpu_tuple_cost)*11(reltuples)=1.11
有過濾條件的公式,比如 >=,多了一個 cpu_operation_cost 的成本。
1(seq_page_cost)*1(relpages)+0.01(cpu_tuple_cost)*11(reltuples)+0.0025(cpu_operation_cost)*11(reltuples)
rows
計划結點輸出行數的估計值。同樣,也假定該結點能運行到完成。
rows=11
mondb=# select reltuples from pg_class where relname='t_base_user_role';
reltuples -----------
11
(1 row)
width
計划結點輸出的行平均寬度(以字節計算)
width=9
mondb=# select sum(length(user_id)), sum(length(role_id)),
(sum(length(user_id)) + sum(length(role_id))) /count(1)
from t_base_user_role;
sum | sum | ?column? -----+-----+----------
59 | 30 | 8
(1 row)
explain analyze
analyze 代表語句真實執行了。所以會有類似 (actual time=0.263..0.265 rows=7 loops=1)的輸出
mondb=# explain analyze
SELECT tu.user_id,
tu.user_first_name::text || tu.user_last_name::text AS user_name,
tr.role_id,
tr.role_name
FROM t_base_user tu,
t_base_role tr,
t_base_user_role tur
WHERE 1 = 1
AND tu.user_id = tur.user_id
AND tr.role_id = tur.role_id
AND tu.user_status = '1'
AND tr.role_status = '1'
ORDER BY tr.role_id,
tu.user_id;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Sort (cost=3.94..3.97 rows=9 width=57) (actual time=0.263..0.265 rows=7 loops=1)
Sort Key: tr.role_id, tu.user_id
Sort Method: quicksort Memory: 25kB -> Hash Join (cost=2.41..3.80 rows=9 width=57) (actual time=0.194..0.214 rows=7 loops=1)
Hash Cond: ((tur.role_id)::text = (tr.role_id)::text) -> Hash Join (cost=1.24..2.48 rows=9 width=19) (actual time=0.063..0.076 rows=7 loops=1)
Hash Cond: ((tur.user_id)::text = (tu.user_id)::text) -> Seq Scan on t_base_user_role tur (cost=0.00..1.11 rows=11 width=9) (actual time=0.010..0.011 rows=11 loops=1) -> Hash (cost=1.12..1.12 rows=9 width=16) (actual time=0.029..0.029 rows=7 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on t_base_user tu (cost=0.00..1.12 rows=9 width=16) (actual time=0.013..0.020 rows=7 loops=1)
Filter: ((user_status)::text = '1'::text) -> Hash (cost=1.09..1.09 rows=7 width=19) (actual time=0.063..0.063 rows=7 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on t_base_role tr (cost=0.00..1.09 rows=7 width=19) (actual time=0.034..0.041 rows=7 loops=1)
Filter: ((role_status)::text = '1'::text)
Planning time: 0.607 ms
Execution time: 0.351 ms
(18 rows)
explain (analyze true)
mondb=# explain (analyze true)
SELECT tu.user_id,
tu.user_first_name::text || tu.user_last_name::text AS user_name,
tr.role_id,
tr.role_name
FROM t_base_user tu,
t_base_role tr,
t_base_user_role tur
WHERE 1 = 1
AND tu.user_id = tur.user_id
AND tr.role_id = tur.role_id
AND tu.user_status = '1'
AND tr.role_status = '1'
ORDER BY tr.role_id,
tu.user_id;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Sort (cost=3.94..3.97 rows=9 width=57) (actual time=0.120..0.120 rows=7 loops=1)
Sort Key: tr.role_id, tu.user_id
Sort Method: quicksort Memory: 25kB
-> Hash Join (cost=2.41..3.80 rows=9 width=57) (actual time=0.077..0.091 rows=7 loops=1)
Hash Cond: ((tur.role_id)::text = (tr.role_id)::text)
-> Hash Join (cost=1.24..2.48 rows=9 width=19) (actual time=0.031..0.040 rows=7 loops=1)
Hash Cond: ((tur.user_id)::text = (tu.user_id)::text)
-> Seq Scan on t_base_user_role tur (cost=0.00..1.11 rows=11 width=9) (actual time=0.004..0.005 rows=11 loops=1)
-> Hash (cost=1.12..1.12 rows=9 width=16) (actual time=0.016..0.016 rows=7 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
-> Seq Scan on t_base_user tu (cost=0.00..1.12 rows=9 width=16) (actual time=0.007..0.011 rows=7 loops=1)
Filter: ((user_status)::text = '1'::text)
-> Hash (cost=1.09..1.09 rows=7 width=19) (actual time=0.030..0.030 rows=7 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
-> Seq Scan on t_base_role tr (cost=0.00..1.09 rows=7 width=19) (actual time=0.015..0.021 rows=7 loops=1)
Filter: ((role_status)::text = '1'::text)
Planning time: 0.443 ms
Execution time: 0.187 ms
(18 rows)
從輸出上來看 explain analyze 等價於 explain (analyze true),那為什么會有兩種表現形式?很容易讓人困惑。
對於 EXPLAIN [ ANALYZE ] [ VERBOSE ] statement ,只有 ANALYZE 和 VERBOSE 選項能被指定,並且必須按照上述的順序,不要把選項列表放在圓括號內。在PostgreSQL 9.0 之前,只支持沒有圓括號的語法。
我們期望所有新的選項將只在圓括號語法中支持。
個人感覺沒有放到圓括號的 analyze 是為了兼容之前的老版本。
官方文檔要好好讀!!!
官方文檔要好好讀!!!
官方文檔要好好讀!!!
參考
http://www.postgres.cn/docs/9.6/sql-explain.html
http://www.postgres.cn/docs/9.6/using-explain.html