一、執行計划
1.執行計划是什么:
一條sql以最快最低消耗獲取出所需數據的一個執行過程。
SQL 是一種“描述型”語言。與“過程型”語言不同,用戶在使用 SQL 時,只描述了“要做什么”,而不是“怎么做”。因此,數據庫在接收到 SQL 查詢時,必須為其生成一個“執行計划”。執行計划可以告訴我們SQL如何使用索引,連接查詢的執行順序,查詢的數據行數等
執行計划本質上是由物理操作符構成的一棵執行樹。物理操作符一般對應一個關系操作,如表掃描、聯接、聚合、排序等。執行計划通過將不同的物理操作符按照一定的先后順序組織在一棵執行樹中,最終完成該 SQL 查詢。
2.執行計划的作用:
在sql優化過程中,查看sql是如何執行的,究竟要涉及多少行、使用哪些索引、運行時間等
二、查看執行計划sql語法
1.語法
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語句;
-
verbose:顯示額外的信息,尤其是計划樹中每個節點的字段列表,schema識別表和函數名稱。總是打印統計數據中顯示的每個觸發器的名字;
-
costs:包括每個計划節點的啟動成本預估和總成本的消耗,也包括行數和行寬度的預估;
-
buffers:使用信息,特別包括共享塊命中、讀、臟和寫的次數,本地塊命中、讀、臟和寫,臨時塊讀和寫的次數;
-
timing:在輸出中包含實際啟動時間和每個節點花費的時間,重復讀系統塊在某些系統上會顯著的減緩查詢的速度,只在ANALYZE也啟用的時候使用;
-
format:聲明輸出格式,可以為TEXT、XML、JSON 或 YAML,默認 text;
2.示例
2.1. EXPLAIN
EXPLAIN SELECT * FROM t2
執行結果:
Seq Scan on t2 (cost=0.00..11.10 rows=110 width=670)
2.2 EXPLAIN ANALYZE:
EXPLAIN ANALYZE SELECT * FROM t2
執行結果:
Seq Scan on t2 (cost=0.00..11.10 rows=110 width=670) (actual time=0.009..0.011 rows=14 loops=1) Planning Time: 0.091 ms Execution Time: 0.055 ms
2.3. EXPLAIN ANALYZE VERBOSE
EXPLAIN ANALYZE VERBOSE SELECT * FROM t2
執行結果:
Seq Scan on public.t2 (cost=0.00..11.10 rows=110 width=670) (actual time=0.029..0.030 rows=14 loops=1)
Output: id, name, age, address
Planning Time: 0.038 ms Execution Time: 0.040 ms
3執行計划各部分的含義:
一個較為復雜的例子:
CREATE TABLE "public"."t2" ( "id" int4 NOT NULL DEFAULT nextval('t2_id_seq'::regclass), "name" varchar(64) COLLATE "pg_catalog"."default", "age" int4, "address" varchar(255) COLLATE "pg_catalog"."default", CONSTRAINT "t2_pkey" PRIMARY KEY ("id") ); create table t3(id int,info text); create table t4(id int,info text); CREATE INDEX t3_id_index ON t3 (id); insert into t3 select generate_series(1,100000),'bill'||generate_series(1,100000); insert into t4 select generate_series(1,100000),'bill'||generate_series(1,100000); CREATE INDEX t4_id_index ON t4 (id); explain ANALYZE select t4.id, sum(t4.id) from t4 inner join (select t3.id, t3.info from t3 WHERE t3.id > 687) tt on (tt.id=t4.id) where tt.info like '%bill%' AND t4.id in (SELECT id FROM t2) GROUP BY t4.id ORDER BY t4.id
執行計划:
GroupAggregate (cost=0.81..43.78 rows=264 width=12) (actual time=0.316..0.317 rows=0 loops=1) Group Key: t4.id -> Merge Join (cost=0.81..39.82 rows=264 width=4) (actual time=0.315..0.316 rows=0 loops=1) Merge Cond: (t3.id = t2.id) -> Merge Join (cost=0.62..8265.90 rows=99270 width=8) (actual time=0.278..0.278 rows=1 loops=1) Merge Cond: (t4.id = t3.id) -> Index Only Scan using t4_id_index on t4 (cost=0.29..3148.29 rows=100000 width=4) (actual time=0.013..0.170 rows=688 loops=1) Heap Fetches: 688 -> Index Scan using t3_id_index on t3 (cost=0.29..3626.89 rows=99270 width=4) (actual time=0.077..0.077 rows=1 loops=1) Index Cond: (id > 687) Filter: (info ~~ '%bill%'::text) -> Index Only Scan using t2_pkey on t2 (cost=0.15..12.14 rows=266 width=4) (actual time=0.009..0.025 rows=266 loops=1) Heap Fetches: 0 Planning Time: 107.100 ms Execution Time: 0.502 ms
3.1.運算類型:
| 執行計划運算類型 | 操作說明 | 是否有啟動時間 |
|---|---|---|
| Seq Scan | 掃描表 | 無啟動時間 |
| Index Scan | 索引掃描 | 無啟動時間 |
| Bitmap Index Scan | 索引掃描 | 有啟動時間 |
| Bitmap Heap Scan | 索引掃描 | 有啟動時間 |
| Subquery Scan | 子查詢 | 無啟動時間 |
| Tid Scan | ctid = …條件 | 無啟動時間 |
| Function Scan | 函數掃描 | 無啟動時間 |
| Nested Loop | 循環結合 | 無啟動時間 |
| Merge Join | 合並結合 | 有啟動時間 |
| Hash Join | 哈希結合 | 有啟動時間 |
| Sort | 排序,ORDER BY操作 | 有啟動時間 |
| Hash | 哈希運算 | 有啟動時間 |
| Result | 函數掃描,和具體的表無關 | 無啟動時間 |
| Unique | DISTINCT,UNION操作 | 有啟動時間 |
| Limit | LIMIT,OFFSET操作 | 有啟動時間 |
| Aggregate | count, sum,avg, stddev集約函數 | 有啟動時間 |
| Group | GROUP BY分組操作 | 有啟動時間 |
| Append | UNION操作 | 無啟動時間 |
| Materialize | 子查詢 | 有啟動時間 |
| SetOp | INTERCECT,EXCEPT | 有啟動時 |
3.2計划結果說明
1. cost
含義:這個計划節點的預計的啟動開銷和總開銷
詳細描述:啟動開銷是指一個計划節點在返回結果之前花費的開銷,如果是在一個排序節點里,那就是指執行排序花費的開銷。 總開銷是指一個計划節點從開始到運行完成,即所有可用行被檢索完后,總共花費的開銷。實際上,一個節點的父節點可能會在子節點返回一部分結果后,停止繼續讀取剩余的行,如Limit節點。
2. rows
含義:這個計划節點的預計輸出行數
詳細描述:在帶有ANALYZE選項時,SQL語句會實際運行,這時一個計划節點的代價輸出會包含兩部分,前面部分是預計的代價,后面部分是實際的代價。前面部分中rows是指預計輸出行數,后面部分是指實際輸出行數。如果中間節點返回的數據量過大,最終返回的數據量很小,或許可以考慮將中間節點以下的查詢修改成物化視圖的形式。
3. width
含義:這個計划節點返回行的預計平均寬度(以字節計算)
詳細描述:如果一個掃描節點返回行的平均寬度明顯小於子節點返回行的平均寬度,說明從子節點讀取的大部分數據是無用的,或許應該考慮一下調整SQL語句或表的相關設計,比如讓執行計划盡量選擇Index Only Scan,或者對表進行垂直拆分。
4. actual time
含義:這個計划節點的實際啟動時間和總運行時間
詳細描述:啟動時間是指一個計划節點在返回第一行記錄之前花費的時間。 總運行時間是指一個計划節點從開始到運行完成,即所有可用行被檢索完后,總共花費的時間。
5. loops
含義:這個計划節點的實際重啟次數
詳細描述:如果一個計划節點在運行過程中,它的相關參數值(如綁定變量)發生了變化,就需要重新運行這個計划節點。
6. Filter
含義:這個掃描節點的過濾條件
詳細描述:對於一個表的掃描節點,如果相關的條件表達式不能對應到表上的某個索引,可能需要分析一下具體的原因和影響,比如該表相關的字段在表達式中需要進行隱式類型轉換,那么即使在該字段上存在索引,也不可能被使用到。如:((b.intcol)::numeric > 99.0)
7. Index Cond
含義:這個索引掃描節點的索引匹配條件
詳細描述:說明用到了表上的某個索引。
8. Rows Removed by Filter
含義:這個掃描節點通過過濾條件過濾掉的行數
詳細描述:如果一個掃描節點的實際輸出行數明顯小於通過過濾條件過濾掉的行數,說明這個計划節點在運行過程中的大量計算是無用的,或者說是沒有實際產出的,那么這個SQL語句或者表的相關設計可能不是特別好。
三、執行計划中掃描方式
1順序掃描Seq Scan
順序掃描實際上就是全表掃描,沒有任何可用索引或者不適合走索引的情況下的一種查詢行為
2.索引掃描Index Scan(包含回表過程)
EXPLAIN ANALYZE SELECT * FROM t2 WHERE id =3
執行結果 :
Index Scan using t2_pkey on t2 (cost=0.14..8.16 rows=1 width=670) (actual time=0.015..0.016 rows=1 loops=1) Index Cond: (id = 3) Planning Time: 0.066 ms Execution Time: 0.032 ms
說明:
回表:就是掃描索引獲取到需要查詢的行地址信息后,根據行地址信息,定位到對應行並獲取這些行數據的過程
3. 索引掃描Index Only Scan (不包含回表過程)
EXPLAIN ANALYZE SELECT id FROM t2 WHERE id =3
執行結果:
Index Only Scan using t2_pkey on t2 (cost=0.14..8.16 rows=1 width=4) (actual time=0.039..0.041 rows=1 loops=1) Index Cond: (id = 3) Heap Fetches: 1 Planning Time: 0.059 ms Execution Time: 0.054 ms
說明:
不需要回表的原因:相對於上面的sql語句,這里只查詢了id, 而id正好就是查詢時要掃描的索引的字段,索引中已經包含了id值,索引不需要再回表獲取行數據。
4.位圖索引掃描Bitmap Index Scan
四、表與表之間的連接處理方式
包括三種:loop join,merge join,hash join這三種join方式
當查詢涉及兩個以上的表時,最終結果必須由一個連接步驟樹構成,每個連接步驟有兩個輸入。規划器會檢查不同可能的連接序列來找到代價最小的那一個。
1.loop join
對左表中找到的每一行都要掃描右表一次。這種策略最容易實現但是可能非常耗時(不過,如果右表可以通過索引掃描,這將是一個不錯的策略。因為可以用左表當前行的值來作為右表上索引掃描的鍵)。
示例:
create table t3(id int,info text); create table t4(id int,info text); CREATE INDEX t3_id_index ON t3 (id); insert into t3 select generate_series(1,100000),'bill'||generate_series(1,100000); insert into t4 select generate_series(1,100000),'bill'||generate_series(1,100000); explain select * from t4 join t3 on (t3.id=t4.id) where t4.info='bill';
執行結果:
Nested Loop (cost=0.29..1799.32 rows=1 width=26) -> Seq Scan on t4 (cost=0.00..1791.00 rows=1 width=13) Filter: (info = 'bill'::text) -> Index Scan using t3_id_index on t3 (cost=0.29..8.31 rows=1 width=13) Index Cond: (id = t4.id)
說明:
- t3.id有索引,因此這里首先全表掃描t4, 獲取到t4的每一個id后再t3表中通過索引掃描獲取到匹配項
- 適合處理兩個較小的結果集的場景
2.merge join:
在連接開始之前,每一個表都按照連接的列排好序。然后兩個表會被並行掃描,匹配的行被整合成連接行。由於這種連接中每個表只被掃描一次。它所要求的排序可以通過一個顯式的排序步驟得到,或使用一個連接鍵上的索引按適當順序掃描關系得到。
示例:
create table t3(id int,info text); create table t4(id int,info text); CREATE INDEX t3_id_index ON t3 (id); insert into t3 select generate_series(1,100000),'bill'||generate_series(1,100000); insert into t4 select generate_series(1,100000),'bill'||generate_series(1,100000); explain select * from t4 inner join t3 on (t3.id=t4.id)
執行結果:
Merge Join (cost=8627.84..367318.87 rows=23603259 width=72) Merge Cond: (t3.id = t4.id) -> Index Scan using t3_id_index on t3 (cost=0.29..4298.90 rows=68707 width=36) -> Materialize (cost=8627.55..8971.08 rows=68707 width=36) -> Sort (cost=8627.55..8799.32 rows=68707 width=36) Sort Key: t4.id -> Seq Scan on t4 (cost=0.00..1228.07 rows=68707 width=36) JIT: Functions: 7 Options: Inlining false, Optimization false, Expressions true, Deforming true
說明:
- 上面執行計划中t3.id有索引,所以直接使用索引進行掃描,t4.id沒有索引,因此進行全表掃描進行排序
- 適合處理兩個有序結果集的場景,或者jion雙方本身存在一致的索引鍵
3.hash join:
右表(outer)會先被掃描並且被載入到一個哈希表,使用連接列作為哈希鍵。接下來左表被掃描,掃描中找到的每一行的連接屬性值被用作哈希鍵在哈希表中查找匹配的行。
示例:
create table t3(id int,info text); create table t4(id int,info text); CREATE INDEX t3_id_index ON t3 (id); insert into t3 select generate_series(1,100000),'bill'||generate_series(1,100000); insert into t4 select generate_series(1,100000),'bill'||generate_series(1,100000); CREATE INDEX t4_id_index ON t4 (id); explain select * from t4 inner join t3 on (t3.id=t4.id) where t3.id <400
執行結果:
Hash Join (cost=26.83..1947.01 rows=418 width=26) Hash Cond: (t4.id = t3.id) -> Seq Scan on t4 (cost=0.00..1541.00 rows=100000 width=13) -> Hash (cost=21.61..21.61 rows=418 width=13) -> Index Scan using t3_id_index on t3 (cost=0.29..21.61 rows=418 width=13) Index Cond: (id < 400)
說明:
- 使用索引掃描t3表並計算HASH值,然后全表掃描t4,將t4的id作為hash的key值找到與t3的匹配行
