成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

你確定讀懂了PostgreSQL執行計劃嗎?

數據庫 PostgreSQL
在執行任何 SQL 語句之前,PostgreSQL 優化器都會為它創建一個執行計劃(Query Plan)。執行計劃描述了 SQL 語句的具體實現步驟,例如使用全表掃描還是索引查找的方式獲取表中的數據,連接查詢使用 Nested Loop Join、Hash Join 還是 Sort Merge Join 算法,以及連接的順序等等。

在執行任何 SQL 語句之前,PostgreSQL 優化器都會為它創建一個執行計劃(Query Plan)。執行計劃描述了 SQL 語句的具體實現步驟,例如使用全表掃描還是索引查找的方式獲取表中的數據,連接查詢使用 Nested Loop Join、Hash Join 還是 Sort Merge Join 算法,以及連接的順序等等。

當我們遇到慢查詢等性能問題時,通常可以先查看 SQL 語句的執行計劃,因此本文給大家詳細介紹一下如何獲取并解讀 PostgreSQL 執行計劃。

獲取執行計劃

PostgreSQL 提供了 EXPLAIN 語句,可以很方便地獲取 SQL 語句的執行計劃。EXPLAIN 語句的基本語法如下:

EXPLAIN statement;

我們首先創建初始化數據:

CREATE TABLE test(
  id INTEGER PRIMARY KEY,
  vc VARCHAR(100),
  vn NUMERIC,
  vd DATE,
  other char(100) DEFAULT 'N/A' NOT NULL
);

INSERT INTO test (id, vc, vn, vd)
SELECT id, 's'||random(), 100*random(),'2024-01-01'::date+(100*random())::int 
FROM GENERATE_SERIES(1, 10000) id;

ANALYZE test;

最后的 ANALYZE 命令是為了收集表的統計信息,幫助查詢優化器做出合理的選擇。

提示:PostgreSQL 優化器需要知道最新的數據庫統計信息(pg_statistic)才能選擇合適的執行計劃,通常 autovacuum 后臺守護進程會定期更新統計信息。但是,如果某個表近期執行了大量數據更新,我們可以執行 ANALYZE 命令更新它的統計信息。

以下是一個簡單的 EXPLAIN 示例:

EXPLAIN SELECT * FROM test;

QUERY PLAN                                                |
----------------------------------------------------------+
Seq Scan on test  (cost=0.00..323.00 rows=10000 width=141)|

PostgreSQL 執行計劃結果包含幾部分內容:操作(Seq Scan on test)、成本(cost)、預估返回的行數(rows)以及預估每行數據的平均寬度(width),單位為字節。

其中,最重要的信息是成本,它的單位一般是磁盤頁讀取次數。成本包含兩個數字,分別代表返回第一行數據之前的啟動成本和返回全部結果的總成本。對于大多數查詢而言,我們需要關注總成本;但是某些情況下(例如 EXISTS 子查詢),查詢計劃器會選擇最小的啟動成本,因為執行器只需要獲取一行數據。另外,如果我們使用了 LIMIT 子句限制返回的行數,查詢計劃器會基于兩個成本計算一個合適的中間值。

EXPLAIN 語句還支持一些選項,其中需要重點注意的一個選項就是 ANALYZE,因為它不僅顯示預估的執行計劃,還會實際執行相應的語句并且返回執行時間統計。例如:

EXPLAIN ANALYZE
SELECT * FROM test;

QUERY PLAN                                                                                              |
--------------------------------------------------------------------------------------------------------+
Seq Scan on test  (cost=0.00..323.00 rows=10000 width=141) (actual time=0.021..1.374 rows=10000 loops=1)|
Planning Time: 0.083 ms                                                                                 |
Execution Time: 1.890 ms                                                                                |

可以看出,執行計劃結果中增加了實際運行時間(actual time)統計,包括每個操作節點消耗的時間(毫秒)、返回的數據行數以及執行的次數。Planning Time 是生成執行計劃的時間;Execution Time 是執行語句的實際時間,不包括 Planning Time。ANALYZE 選項通常可以用于檢查查詢計劃器的評估是否準確。

雖然 ANALYZE 選項忽略了 SELECT 語句返回的結果,但是對于 INSERT、UPDATE、DELETE 等語句,它仍然會修改表中的數據,為了避免這種副作用,我們可以在事務內部獲取執行計劃,然后回滾事務:

BEGIN;
EXPLAIN ANALYZE ...;
ROLLBACK;

其他 EXPLAIN 選項的介紹可以參考下文。

解讀執行計劃

PostgreSQL 執行計劃的結構是一棵由計劃節點組成的樹,EXPLAIN 命令的每一行對應一個節點。

每一行節點除了匯總信息之外,還可能包含縮進行,顯示了完成該節點的底層操作。節點的執行順序按照縮進來判斷,縮進越多的越先執行,同樣縮進的從上至下執行。第一行顯示了預估的總成本,它也是優化器最小化的目標。

執行計劃最底層的節點是掃描節點,通常用于從表中返回原始數據。我們就從簡單的單表訪問開始。

單表訪問

對于不同的表訪問方法,存在以下不同的掃描節點:

  • 順序掃描(適用于返回大部分數據行)
  • 索引掃描(適用于返回很少數據行)
  • 位圖索引掃描(適用于返回較多數據行)

順序掃描就是全表掃描,它會依次讀取整個表中的數據。如果查詢條件字段沒有索引,一般需要執行順序掃描,例如:

EXPLAIN 
SELECT *
FROM test
WHERE vd = '2024-01-01'::date;

QUERY PLAN                                             |
-------------------------------------------------------+
Seq Scan on test  (cost=0.00..348.00 rows=59 width=141)|
  Filter: (vd = '2024-01-01'::date)                    |

順序掃描對應的操作名稱為 Seq Scan,通常意味著我們需要基于查詢條件字段創建索引,從而通過索引優化查詢。

索引掃描意味著遍歷索引的 B-樹葉子節點,找到所有滿足條件的索引項,然后通過索引指針讀取表中的數據。例如:

EXPLAIN 
SELECT *
FROM test
WHERE id = 1000;

QUERY PLAN                                                            |
----------------------------------------------------------------------+
Index Scan using test_pkey on test  (cost=0.29..8.30 rows=1 width=141)|
  Index Cond: (id = 1000)                                             |

如果我們需要查詢的字段都可以通過索引獲取,PostgreSQL 可以使用僅索引掃描(Index-Only Scan)技術優化查詢。例如:

CREATE INDEX idx_test_vn ON test(vn,id);

EXPLAIN 
SELECT vn, id
FROM test
WHERE vn = 1000;

QUERY PLAN                                                                  |
----------------------------------------------------------------------------+
Index Only Scan using idx_test_vn on test  (cost=0.29..4.30 rows=1 width=16)|
  Index Cond: (vn = '1000'::numeric)                                        |

索引 idx_test_vn 包含了 vn 字段和 id 字段,查詢語句不需要訪問表中的數據即可返回查詢結果。

提示:PostgreSQL 提供了覆蓋索引(Covering Index),可以進一步實現 Index-Only Scan 優化。另外,Index-Only Scan 優化需要滿足一個條件:MVCC 可見性,因為索引中并沒有存儲數據的可見性信息,只有表的元組中存儲了該信息。

索引掃描每次找到一個滿足條件的索引項時,都會基于元組指針再次訪問表中的數據(回表),這是一種隨機 IO。如果索引掃描只返回很少的數據行,它是一個很好的訪問方法。但是如果掃描索引返回的數據行比較多,大量的隨機回表會導致性能下降;一個優化的方法就是把回表的隨機 IO 變成順序 IO,為此 PostgreSQL 引入了位圖索引掃描。

位圖索引掃描(Bitmap Index Scan)的原理是一次掃描索引獲取所有滿足條件的元組指針,然后在內存中基于“位圖”數據結構進行排序,最后按照元組指針的物理順序訪問表(Bitmap Heap Scan)中的數據。例如:

CREATE INDEX idx_test_vd ON test(vd);

EXPLAIN 
SELECT *
FROM test
WHERE vd = '2024-01-01'::date;

QUERY PLAN                                                               |
-------------------------------------------------------------------------+
Bitmap Heap Scan on test  (cost=4.75..139.99 rows=60 width=141)          |
  Recheck Cond: (vd = '2024-01-01'::date)                                |
  ->  Bitmap Index Scan on idx_test_vd  (cost=0.00..4.74 rows=60 width=0)|
        Index Cond: (vd = '2024-01-01'::date)                            |

該查詢語句返回 60 行數據,使用索引掃描的話,還需要 60 次回表。因此,PostgreSQL 選擇了位圖索引的訪問方法。

Recheck Cond 發生在回表階段,因為如果基于元組構建位圖導致位圖過大,就會基于數據頁(Page)構建位圖(有損方式),也就是只記錄了哪些數據頁包含了所需的數據行,所以在讀取數據頁之后需要再次檢查具體的元組。對于無損方式構建的位圖,也會出現 Recheck Cond 節點,但是并不執行檢查操作。

位圖索引掃描更常見的一種情況是查詢條件組合使用了多個索引時,例如:

EXPLAIN 
SELECT *
FROM test
WHERE vn = 1000 OR vd = '2024-01-01'::date;

QUERY PLAN                                                                     |
-------------------------------------------------------------------------------+
Bitmap Heap Scan on test  (cost=9.06..146.25 rows=61 width=141)                |
  Recheck Cond: ((vn = '1000'::numeric) OR (vd = '2024-01-01'::date))          |
  ->  BitmapOr  (cost=9.06..9.06 rows=61 width=0)                              |
        ->  Bitmap Index Scan on idx_test_vn  (cost=0.00..4.29 rows=1 width=0) |
              Index Cond: (vn = '1000'::numeric)                               |
        ->  Bitmap Index Scan on idx_test_vd  (cost=0.00..4.74 rows=60 width=0)|
              Index Cond: (vd = '2024-01-01'::date)                            |

查詢首先基于 idx_test_vn 以及 idx_test_vd 進行了位圖索引掃描,然后進行了位圖合并(BitmapOr),最后基于位圖結果進行回表。

位圖索引掃描存在一個副作用,就是查詢結果不再按照索引順序返回,無法通過索引優化 ORDER BY。例如:

EXPLAIN
SELECT *
FROM test
WHERE vd BETWEEN '2024-01-01'::date AND '2024-01-31'::date
ORDER BY vd;

QUERY PLAN                                                                           |
-------------------------------------------------------------------------------------+
Sort  (cost=485.23..492.65 rows=2966 width=141)                                      |
  Sort Key: vd                                                                       |
  ->  Bitmap Heap Scan on test  (cost=46.69..314.18 rows=2966 width=141)             |
        Recheck Cond: ((vd >= '2024-01-01'::date) AND (vd <= '2024-01-31'::date))    |
        ->  Bitmap Index Scan on idx_test_vd  (cost=0.00..45.94 rows=2966 width=0)   |
              Index Cond: ((vd >= '2024-01-01'::date) AND (vd <= '2024-01-31'::date))|

查詢計劃中增加了額外的排序節點(Sort)。

提示:位圖索引掃描增加了內存和 CPU 的消耗,但是會減少磁盤 IO。

除了表之外,還有一些特殊的數據源(例如 VALUES 子句和 FROM 子句中的集合函數)擁有特殊的掃描類型。例如:

EXPLAIN
SELECT *
FROM (VALUES(1,'sql')) t(id,v);

QUERY PLAN                               |
-----------------------------------------+
Result  (cost=0.00..0.01 rows=1 width=36)|

EXPLAIN
SELECT *
FROM pg_catalog.generate_series(1, 100);

QUERY PLAN                                                          |
--------------------------------------------------------------------+
Function Scan on generate_series  (cost=0.00..1.00 rows=100 width=4)|

多表連接

如果查詢涉及多表連接操作,執行計劃中的掃描節點之上將會顯示額外的 Join 節點。通常連接操作一次連接兩個表,如果查詢包含多個連接操作,按照順序進行連接,前兩個表連接的中間結果和下一個表進行連接。

PostgreSQL 實現了以下三種連接算法:

  • 嵌套循環(Nested Loop)
  • 哈希連接(Hash Join)
  • 排序合并(Merge Join)

嵌套循環連接類似于編程語言中的嵌套 for 循環,首先從外部表(驅動表)中獲取滿足條件的數據,然后為每一行數據遍歷一次內部表(被驅動表),獲取所有匹配的數據。下圖演示了嵌套循環連接的執行過程:

以下查詢將 test 和它自己進行交叉連接:

EXPLAIN
SELECT *
FROM test t1
CROSS JOIN test t2;

QUERY PLAN                                                               |
-------------------------------------------------------------------------+
Nested Loop  (cost=0.00..1250671.00 rows=100000000 width=282)            |
  ->  Seq Scan on test t1  (cost=0.00..323.00 rows=10000 width=141)      |
  ->  Materialize  (cost=0.00..373.00 rows=10000 width=141)              |
        ->  Seq Scan on test t2  (cost=0.00..323.00 rows=10000 width=141)|

PostgreSQL 選擇了嵌套循環算法實現以上連接查詢,其中 Materialize 說明 t2 的掃描結果進行了緩存,極大地減少了磁盤訪問次數。

哈希連接使用其中一個表中滿足條件的記錄創建哈希表,然后掃描另一個表進行匹配。哈希連接的執行過程如下圖所示:

以下查詢仍然使用 test 進行自連接,但是指定了連接條件

EXPLAIN
SELECT *
FROM test t1
JOIN test t2 ON t1.vc=t2.vc;

QUERY PLAN                                                               |
-------------------------------------------------------------------------+
Hash Join  (cost=448.00..908.50 rows=10000 width=282)                    |
  Hash Cond: ((t1.vc)::text = (t2.vc)::text)                             |
  ->  Seq Scan on test t1  (cost=0.00..323.00 rows=10000 width=141)      |
  ->  Hash  (cost=323.00..323.00 rows=10000 width=141)                   |
        ->  Seq Scan on test t2  (cost=0.00..323.00 rows=10000 width=141)|

PostgreSQL 選擇了哈希連接算法實現以上連接查詢,并且使用 t2 表的數據創建哈希表。

排序合并連接先將兩個數據源按照連接字段進行排序(Sort),然后合并兩個已經排序的集合,返回滿足連接條件的結果。排序合并連接的執行過程如下圖所示:

以下查詢使用主鍵 id 字段進行連接,并且按照 t1 的主鍵進行排序:

EXPLAIN
SELECT *
FROM test t1
JOIN test t2 ON t1.id=t2.id
ORDER BY t1.id;

QUERY PLAN                                                                           |
-------------------------------------------------------------------------------------+
Merge Join  (cost=0.57..1142.57 rows=10000 width=282)                                |
  Merge Cond: (t1.id = t2.id)                                                        |
  ->  Index Scan using test_pkey on test t1  (cost=0.29..496.29 rows=10000 width=141)|
  ->  Index Scan using test_pkey on test t2  (cost=0.29..496.29 rows=10000 width=141)|

PostgreSQL 選擇了排序合并連接算法實現以上連接查詢,它可以避免額外的排序操作。

集合運算

集合運算符(UNION、INTERSECT、EXCEPT)用于將多個查詢語句的結果進行并集、交集、差集運算,它們也會在執行計劃中顯示單獨的節點。例如:

EXPLAIN 
SELECT * 
FROM test t1
UNION ALL
SELECT *
FROM test t2;

QUERY PLAN                                                         |
-------------------------------------------------------------------+
Append  (cost=0.00..746.00 rows=20000 width=141)                   |
  ->  Seq Scan on test t1  (cost=0.00..323.00 rows=10000 width=141)|
  ->  Seq Scan on test t2  (cost=0.00..323.00 rows=10000 width=141)|

其中,Append 節點意味著將兩個查詢的結果追加合并成一個結果。

以下是一個 INTERSECT 示例:

EXPLAIN
SELECT * 
FROM test t1
INTERSECT   
SELECT *
FROM test t2;

QUERY PLAN                                                                                              |
--------------------------------------------------------------------------------------------------------+
SetOp Intersect  (cost=8324.77..8624.77 rows=10000 width=666)                                           |
  ->  Sort  (cost=8324.77..8374.77 rows=20000 width=666)                                                |
        Sort Key: "*SELECT* 1".id, "*SELECT* 1".vc, "*SELECT* 1".vn, "*SELECT* 1".vd, "*SELECT* 1".other|
        ->  Append  (cost=0.00..946.00 rows=20000 width=666)                                            |
              ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..423.00 rows=10000 width=145)               |
                    ->  Seq Scan on test t1  (cost=0.00..323.00 rows=10000 width=141)                   |
              ->  Subquery Scan on "*SELECT* 2"  (cost=0.00..423.00 rows=10000 width=145)               |
                    ->  Seq Scan on test t2  (cost=0.00..323.00 rows=10000 width=141)                   |

其中,SetOp Intersect 節點代表了并集運算,它由一個 Append 節點和 Sort 節點組成,因為 INTERSECT 運算符需要去除重復記錄。

最后是一個 EXCEPT 示例:

EXPLAIN
SELECT * 
FROM test t1
EXCEPT    
SELECT *
FROM test t2;

QUERY PLAN                                                                                              |
--------------------------------------------------------------------------------------------------------+
SetOp Except  (cost=8324.77..8624.77 rows=10000 width=666)                                              |
  ->  Sort  (cost=8324.77..8374.77 rows=20000 width=666)                                                |
        Sort Key: "*SELECT* 1".id, "*SELECT* 1".vc, "*SELECT* 1".vn, "*SELECT* 1".vd, "*SELECT* 1".other|
        ->  Append  (cost=0.00..946.00 rows=20000 width=666)                                            |
              ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..423.00 rows=10000 width=145)               |
                    ->  Seq Scan on test t1  (cost=0.00..323.00 rows=10000 width=141)                   |
              ->  Subquery Scan on "*SELECT* 2"  (cost=0.00..423.00 rows=10000 width=145)               |
                    ->  Seq Scan on test t2  (cost=0.00..323.00 rows=10000 width=141)                   |

其中,SetOp Except 節點表示差集運算,同樣由一個 Append 節點和 Sort 節點組成。

排序分組

排序(ORDER BY)和分組(GROUP BY)也是查詢語句中常見的操作,它們都有專門的節點類型。例如:

EXPLAIN
SELECT *
FROM test 
ORDER BY vd;

QUERY PLAN                                                      |
----------------------------------------------------------------+
Sort  (cost=987.39..1012.39 rows=10000 width=141)               |
  Sort Key: vd                                                  |
  ->  Seq Scan on test  (cost=0.00..323.00 rows=10000 width=141)|

雖然 vd 字段存在索引,但是查詢需要返回全部數據,PostgreSQL 還是選擇了全表掃描加排序(Sort)的方式。

如果索引能夠同時完成數據過濾(WHERE)和排序,執行計劃中就不會出現 Sort 節點。例如:

EXPLAIN
SELECT *
FROM test 
WHERE vn = 1000
ORDER BY id;

QUERY PLAN                                                              |
------------------------------------------------------------------------+
Index Scan using idx_test_vn on test  (cost=0.29..8.30 rows=1 width=141)|
  Index Cond: (vn = '1000'::numeric)                                    |

索引 idx_test_vn 包含了 vn 以及 id 字段。

PostgreSQL 實現了兩種分組算法:哈希聚合算法以及排序聚合算法。

哈希聚合算法使用一個臨時哈希表對數據進行分組聚合,完成數據哈希之后的結果就是分組結果。例如:

EXPLAIN
SELECT vc,count(*)
FROM test 
GROUP BY vc;

QUERY PLAN                                                     |
---------------------------------------------------------------+
HashAggregate  (cost=373.00..473.00 rows=10000 width=28)       |
  Group Key: vc                                                |
  ->  Seq Scan on test  (cost=0.00..323.00 rows=10000 width=20)|

vc 字段沒有索引,PostgreSQL 選擇了哈希聚合算法(HashAggregate)。

排序聚合算法首先將數據按照分組字段進行排序,將每個組內的數據都排列到一起,然后進行聚合操作。例如:

EXPLAIN
SELECT vc,count(*)
FROM test 
GROUP BY vc
ORDER BY vc;

QUERY PLAN                                                           |
---------------------------------------------------------------------+
GroupAggregate  (cost=987.39..1162.39 rows=10000 width=28)           |
  Group Key: vc                                                      |
  ->  Sort  (cost=987.39..1012.39 rows=10000 width=20)               |
        Sort Key: vc                                                 |
        ->  Seq Scan on test  (cost=0.00..323.00 rows=10000 width=20)|

考慮到查詢結果還需要進行排序,PostgreSQL 選擇了排序聚合算法(Sort + GroupAggregate)。

排序聚合算法還可以基于索引避免排序操作,例如:

EXPLAIN
SELECT vn,count(*)
FROM test 
GROUP BY vn
ORDER BY vn;

QUERY PLAN                                                                              |
----------------------------------------------------------------------------------------+
GroupAggregate  (cost=0.29..504.29 rows=10000 width=20)                                 |
  Group Key: vn                                                                         |
  ->  Index Only Scan using idx_test_vn on test  (cost=0.29..354.29 rows=10000 width=12)|

vn 字段存在索引,因此執行計劃中只有 GroupAggregate 節點,而沒有 Sort 節點。

限制結果

Top-N 查詢和分頁查詢通常只需要返回有限數量的結果,例如:

EXPLAIN ANALYZE
SELECT *
FROM test 
ORDER BY vn 
FETCH FIRST 5 ROWS ONLY;

QUERY PLAN                                                                                                                     |
-------------------------------------------------------------------------------------------------------------------------------+
Limit  (cost=0.29..0.91 rows=5 width=141) (actual time=0.013..0.017 rows=5 loops=1)                                            |
  ->  Index Scan using idx_test_vn on test  (cost=0.29..1246.20 rows=10000 width=141) (actual time=0.012..0.015 rows=5 loops=1)|
Planning Time: 0.084 ms                                                                                                        |
Execution Time: 0.030 ms                                                                                                       |

執行計劃中的 Limit 節點表示 PostgreSQL 在獲取足夠數據行之后停止底層操作,索引掃描(Index Scan)不僅避免了排序操作,而且只需要掃描 5 個索引條目(actual time=0.012…0.015 rows=5 loops=1)就可以終止掃描,這種優化技術被稱為管道(pipelined)操作。

Limit 操作的性能依賴于底層操作的實現,如果底層執行的是非管道操作,例如:

EXPLAIN ANALYZE 
SELECT *
FROM test 
ORDER BY vc 
FETCH FIRST 5 ROWS ONLY;

QUERY PLAN                                                                                                          |
--------------------------------------------------------------------------------------------------------------------+
Limit  (cost=489.10..489.11 rows=5 width=141) (actual time=3.361..3.362 rows=5 loops=1)                             |
  ->  Sort  (cost=489.10..514.10 rows=10000 width=141) (actual time=3.360..3.360 rows=5 loops=1)                    |
        Sort Key: vc                                                                                                |
        Sort Method: top-N heapsort  Memory: 27kB                                                                   |
        ->  Seq Scan on test  (cost=0.00..323.00 rows=10000 width=141) (actual time=0.015..0.529 rows=10000 loops=1)|
Planning Time: 0.124 ms                                                                                             |
Execution Time: 3.384 ms                                                                                            |

vc 字段沒有索引,所以需要執行額外的排序(Sort)。排序可能導致明顯的性能問題,因為 Limit 節點需要等待所有數據(actual time=0.015…0.529 rows=10000 loops=1)完成排序之后才能返回數據。

訪問謂詞與過濾謂詞

對于 WHERE 子句(謂詞),PostgreSQL 提供了三種不同的實現方法:

  • 索引訪問謂詞
  • 索引過濾謂詞
  • 表級過濾謂詞

索引訪問謂詞(Index Access Predicate)指定了索引葉子節點遍歷的開始和結束條件。例如:

EXPLAIN ANALYZE 
SELECT *
FROM test 
WHERE id BETWEEN 100 AND 120;

QUERY PLAN                                                                                                        |
------------------------------------------------------------------------------------------------------------------+
Index Scan using test_pkey on test  (cost=0.29..8.71 rows=21 width=141) (actual time=0.007..0.011 rows=21 loops=1)|
  Index Cond: ((id >= 100) AND (id <= 120))                                                                       |
Planning Time: 0.133 ms                                                                                           |
Execution Time: 0.024 ms                                                                                          |

其中,Index Cond 表示索引掃描時基于該條件開始和結束,所以它屬于訪問謂詞。

索引過濾謂詞(Index Filter Predicate)在遍歷索引葉子節點時用于判斷是否返回該索引項,但是不會用于判斷遍歷的開始和結束條件,也就不會縮小索引掃描的范圍。例如:

CREATE INDEX idx_test_vdvc ON test(vd, vc);

EXPLAIN ANALYZE 
SELECT *
FROM test 
WHERE vd BETWEEN '2024-01-01'::date AND '2024-01-02'::date AND vc = 'xxx';

QUERY PLAN                                                                                                          |
--------------------------------------------------------------------------------------------------------------------+
Index Scan using idx_test_vdvc on test  (cost=0.29..9.36 rows=1 width=141) (actual time=0.024..0.024 rows=0 loops=1)|
  Index Cond: ((vd >= '2024-01-01'::date) AND (vd <= '2024-01-02'::date) AND ((vc)::text = 'xxx'::text))            |
Planning Time: 0.124 ms                                                                                             |
Execution Time: 0.040 ms                                                                                            |

idx_test_vdvc 索引基于 vd 和 vc 兩個字段,但是查詢條件中只有 vd 用于決定索引遍歷的開始條件和結束條件,vc 字段只能用于判斷是否返回該索引項。因為 vd 是范圍條件,導致索引節點中的 vc 字段不再具體順序性。PostgreSQL 執行計劃沒有區分索引訪問謂詞和索引過濾謂詞,統一顯示為 Index Cond。

注意:索引過濾謂詞看似使用了索引,但是隨著數據量的增長可能導致性能明顯下降,因為它沒有減少索引掃描的范圍。

對于以上查詢語句,如果我們創建 idx_test_vdvc 索引時把 vc 字段放在最前面,就可以充分利用索引優化性能,因為此時所有的謂詞都是所有訪問謂詞。

表級過濾謂詞(Table Level Filter Predicate)是指謂詞中的非索引字段在表級別進行判斷,這意味著數據庫需要讀取表中的數據然后再應用該條件。例如:

EXPLAIN ANALYZE 
SELECT *
FROM test 
WHERE id = 100 AND other = 'N/A';

QUERY PLAN                                                                                                      |
----------------------------------------------------------------------------------------------------------------+
Index Scan using test_pkey on test  (cost=0.29..8.30 rows=1 width=141) (actual time=0.019..0.020 rows=1 loops=1)|
  Index Cond: (id = 100)                                                                                        |
  Filter: (other = 'N/A'::bpchar)                                                                               |
Planning Time: 0.103 ms                                                                                         |
Execution Time: 0.037 ms                                                                                        |

查詢使用了主鍵索引掃描(Index Scan),其中 id 是索引訪問謂詞(Index Cond),other 是表級過濾謂詞(Filter)。

提示:一般來說,對于相同的查詢語句,訪問謂詞的性能好于索引過濾謂詞,索引過濾謂詞的性能好于表級過濾謂詞。

輸出參數

最后我們介紹一下 EXPLAIN 語句的完整語法:

EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement

其中 option 可以為以下選項之一:

    ANALYZE [ boolean ]
    VERBOSE [ boolean ]
    COSTS [ boolean ]
    SETTINGS [ boolean ]
    GENERIC_PLAN [ boolean ]
    BUFFERS [ boolean ]
    WAL [ boolean ]
    TIMING [ boolean ]
    SUMMARY [ boolean ]
    FORMAT { TEXT | XML | JSON | YAML }

其中,ANALYZE 和 VERBOSE 選項支持兩種指定方法;其他選項需要使用括號包含,多個選項使用逗號進行分隔。

statement 可以是以下語句之一:SELECT、INSERT、UPDATE、DELETE、MERGE、VALUES、EXECUTE、DECLARE、CREATE TABLE AS、CREATE MATERIALIZED VIEW AS。

boolean 用于啟用或者禁用相關選項。TRUE、ON 或者 1 表示啟用,FALSE、OFF 或者 0 表示禁用。如果忽略了 boolean 設置,默認為啟用。

ANALYZE

ANALYZE 選項不僅顯示預估的執行計劃,還會實際執行相應的語句,并且返回執行時間和其他信息統計。該選項默認為 FALSE。

一方面,為了測量執行計劃中每個節點的執行時成本,當前 EXPLAIN ANALYZE 的實現在執行計劃中增加了一些分析開銷,因此執行 EXPLAIN ANALYZE 命令有時候會導致查詢比正常運行花費的時間明顯更長。具體的分析開銷取決于查詢語句以及數據庫運行的平臺,有可能查詢節點每次執行只需要很短的時間,但是操作系統獲取時間的調用反而更慢,可以使用 pg_test_timing 工具測量系統的計時開銷。

另一方面, EXPLAIN ANALYZE 不需要將查詢結果發送到客戶端,因此沒有包含網絡傳輸和轉換成本。

VERBOSE

VERBOSE 選項用于在執行計劃中顯示額外的信息。例如:

EXPLAIN VERBOSE 
SELECT *
FROM test;

QUERY PLAN                                                        |
------------------------------------------------------------------+
Seq Scan on emerald.test  (cost=0.00..323.00 rows=10000 width=141)|
  Output: id, vc, vn, vd, other                                   |

以上 EXPLAIN VERBOSE 顯示了順序掃描節點輸出的字段列表(Output),以及包含模式名限定的表名(emerald.test)。

對于不同的操作節點,VERBOSE 選項還會顯示其他額外信息。該選項默認禁用。

COSTS

COSTS 選項用于輸出每個計劃節點的預估啟動成本和總成本,以及預估行數和平均長度。該選項默認啟用。例如:

EXPLAIN (COSTS)
SELECT *
FROM test;

QUERY PLAN                                                |
----------------------------------------------------------+
Seq Scan on test  (cost=0.00..323.00 rows=10000 width=141)|

SETTINGS

SETTINGS 選項用于顯示配置參數,尤其是影響查詢計劃的非默認設置的參數。該選項默認禁用。例如:

EXPLAIN (SETTINGS)
SELECT *
FROM test;

QUERY PLAN                                                |
----------------------------------------------------------+
Seq Scan on test  (cost=0.00..323.00 rows=10000 width=141)|
Settings: search_path = 'hrdb, public, "$user"'           |

GENERIC_PLAN

PostgreSQL 16 版本增加了 GENERIC_PLAN 選項,可以為預編譯語句 生成通用執行計劃,這種執行計劃不依賴于綁定變量(例如 $1、$2等)的值。例如:

EXPLAIN (GENERIC_PLAN)
SELECT *
FROM test
WHERE vn = $1;

QUERY PLAN                                                              |
------------------------------------------------------------------------+
Index Scan using idx_test_vn on test  (cost=0.29..8.30 rows=1 width=141)|
  Index Cond: (vn = $1)                                                 |

GENERIC_PLAN 選項默認禁用,而且不能和 ANALYZE 選項一起使用,因為 ANALYZE 需要執行語句。

另外,預編譯語句也可能使用定制執行計劃,也就是使用綁定變量的具體值創建執行計劃。例如:

PREPARE query_test(numeric)
AS 
SELECT *
FROM test
WHERE vn = $1;

EXPLAIN EXECUTE query_test(10);

QUERY PLAN                                                              |
------------------------------------------------------------------------+
Index Scan using idx_test_vn on test  (cost=0.29..8.30 rows=1 width=141)|
  Index Cond: (vn = '10'::numeric)                                                 |


DEALLOCATE query_test;

索引掃描的訪問謂詞中使用了具體的參數值(10)。

提示:運行時參數 plan_cache_mode 決定了預編譯語句使用通用執行計劃還是定制執行計劃。

BUFFERS

BUFFERS 選項用于顯示緩沖區使用情況,默認禁用。例如:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM test
WHERE id = 1000;

QUERY PLAN                                                                                                      |
----------------------------------------------------------------------------------------------------------------+
Index Scan using test_pkey on test  (cost=0.29..8.30 rows=1 width=141) (actual time=0.030..0.032 rows=1 loops=1)|
  Index Cond: (id = 1000)                                                                                       |
  Buffers: shared hit=3                                                                                         |
Planning Time: 0.266 ms                                                                                         |
Execution Time: 0.071 ms                                                                                        |

其中,shared hit 表示共享塊命中。

具體來說,BUFFERS 選項顯示的信息包括共享內存塊命中(hit)、讀取(read)、標記臟塊(dirtied)以及寫入(written)數量,本地內存塊命中(hit)、讀取(read)、標記臟塊(dirtied)以及寫入(written)數量,臨時內存塊的讀取(read)和寫入(written)數量。如果啟用了服務器參數 track_io_timing ,還會顯示讀寫數據文件塊和臨時文件塊的時間(毫秒)。

其中,一次命中意味著避免了一次磁盤讀取,因為所需數據塊已經存在緩存中。共享內存塊包含了普通表和索引的緩存數據,本地內存塊包含了臨時表和索引的緩存數據;臨時內存塊包含了排序、哈希、物化節點等操作使用的臨時數據。

臟塊的數量表示之前未改動,但是當前查詢修改的數據塊;寫入塊的數量表示之前被標記為臟塊,同時在當前查詢處理過程總被后臺進程刷新到磁盤的數據塊。上層節點顯示的數量包含了子節點的數量,對于 TEXT 輸出格式,只顯示非零數據值。

WAL

WAL 選項用于顯示有關預寫式日志記錄生成的信息。具體來說,包括記錄數、全頁鏡像數(fpi)以及生成的 WAL(字節)。如果 FORMAT 選項的值為 TEXT(默認值),只顯示非零信息。該選項只能在啟用 ANALYZE 選項時使用,默認為禁用。

TIMING

TIMING 選項用于顯示每個計劃節點的啟用時間和完成時間(毫秒),該選項只能在啟用 ANALYZE 選項時使用,默認為啟用。

某些平臺上重復讀取系統時間可能會明顯影響查詢性能,如果只關注實際返回的行數,可以在啟用 ANALYZE 選項時將該選項禁用。即使關閉了節點的計時功能,整個語句的運行時間仍然會統計并顯示。

SUMMARY

SUMMARY 選項用于在執行計劃之后顯示匯總信息(例如總的時間消耗)。如果啟用了 ANALYZE 選項,默認顯示匯總信息;否則默認不會顯示匯總信息。

對于 EXPLAIN EXECUTE 語句,Planning time 包含了從緩存中獲取執行計劃或者重新計劃消耗的時間。

FORMAT

FORMAT 選項用于指定執行計劃的輸出格式,可以使用 TEXT、XML、JSON 或者 YAML 格式。默認輸出格式為 TEXT,其他格式輸出的內容和 TEXT 格式相同,只是更方便程序處理。例如:

EXPLAIN (FORMAT JSON)
SELECT *
FROM test;

[
  {
    "Plan": {
      "Node Type": "Seq Scan",
      "Parallel Aware": false,
      "Async Capable": false,
      "Relation Name": "test",
      "Alias": "test",
      "Startup Cost": 0.00,
      "Total Cost": 323.00,
      "Plan Rows": 10000,
      "Plan Width": 141
    }
  }
]


責任編輯:華軒 來源: SQL編程思想
相關推薦

2022-02-15 07:36:21

SQLEXPLAIN數據庫

2021-04-24 12:01:08

MySQL數據庫Mysql執行計劃

2011-09-14 17:03:17

數據庫執行計劃解析

2015-04-22 14:17:45

SQL SERVERMSSQL SERVE緩沖區

2021-03-17 09:35:51

MySQL數據庫explain

2023-09-21 10:55:51

MysqlSQL語句

2021-05-28 10:46:36

MySQL執行計劃

2009-11-13 16:28:02

Oracle生成執行計

2024-09-12 15:16:14

2022-08-08 08:03:44

MySQL數據庫CBO

2022-08-15 15:09:26

SQL數據庫MySQL

2010-04-16 09:27:18

Ocacle執行計劃

2009-11-10 16:00:05

Oracle執行計劃

2021-09-07 10:43:25

EverDB分布式執行

2020-09-15 08:44:57

MySQL慢日志SQL

2021-02-20 08:40:19

HiveExplain底層

2009-11-18 17:05:47

捕獲Oracle SQ

2020-10-16 09:40:18

順序Spring AOPHTTP

2022-12-13 08:36:42

D-SMARTOracle數據庫

2022-11-02 15:28:55

MySQL執行計劃B+樹
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品日本一区二区在线播放 | 日韩中文在线视频 | 91久久久www播放日本观看 | 国产精品一级在线观看 | 一级做a毛片 | 91亚洲国产 | 亚洲欧美一区二区三区1000 | 亚洲一区精品在线 | 日本一区二区三区在线观看 | 欧美videosex性极品hd | 久久久一区二区三区四区 | 国产精品一区在线 | 久久一视频 | 国产中文字幕在线观看 | 亚洲精品一区二区网址 | 特黄毛片 | 欧美在线一区二区三区 | 亚洲天堂av在线 | 亚洲综合无码一区二区 | 欧美情趣视频 | 国产精品99久久久久久人 | 成人精品一区二区三区 | 亚洲精品高清视频 | 天天干天天玩天天操 | 少妇精品久久久久久久久久 | 国产一二三区精品视频 | 久久久久国产精品一区二区 | 亚洲黄色av| 国产目拍亚洲精品99久久精品 | 黄色片亚洲 | 久国产视频 | 日韩网站在线观看 | 亚洲九九精品 | 国产馆 | 99精品国自产在线观看 | 午夜精品一区二区三区在线视频 | 亚洲免费福利视频 | 国产中文一区二区三区 | 日韩午夜网站 | 国产激情视频在线 | 99综合|