前言
執行計畫代表此次查詢要怎麼樣的演算法查詢我們的資料,而成本是決定使用哪個執行計畫的重要因素
在 postgreSQL DB query optimizer 會選擇成本最低的執行計劃,當作查詢資料使用算法
在 Sql-sevrer 成本計算封裝在程式內部,我們無法透過一些因子來調整,但 postgreSQL 可以
我認為 query optimizer 判斷成本概念有點類似 google map 在找尋最佳路徑
我對於資料庫有定義一個,地圖理論來說明 RDBMS 執行計畫相關的事情
本篇成本因子效能調教會涉 Linux kernel systemtap ,國中數學,postgreSQL 運作模型,篇幅可能會有點多且複雜
但我認為本篇學會可以對於 query optimizer 有更進一步了解
預設成本因子潛在問題
下面我使用一個例子來
我們利用一樣的查詢 explain (analyze,verbose,costs,buffers,timing) select id from tbl where id > 70000;
查看使用 table scan
& index scan only
執行計畫預估值跟實際查詢差異.
1 | postgres=# explain (analyze,verbose,costs,buffers,timing) select info from tbl where info ='girl'; |
發現明明查詢語法都一樣,只是透過 Setting 發現明明是 Index Only Scan
執行實際時間成本比較低,但 query optimizer 卻選擇 table scan,會造成上面問題是因為成本因子沒有調教所造成
這也是我一開始說成本因子的重要性
另外上面 cost 代表含意也不明,只是一個數字,首先我們要先知道成本是一個概念,重點我們是要看哪種成本?
像成本有分 時間成本,金錢成本…..都是不同維度的成本
我們的做法會把成本校準成 時間成本 也就是實際執行時間和預估執行成本校準
調教成本因子前置作業
下載 systemtap
我是在 AWS EC2 建立一個 ubuntu Linux server.所以目前我操作步驟是針對 ubuntu Linux 說明
systemtap 這個工具可以監測 Linux 核心底層 本次我會藉由他來幫我們了解實際查詢平均 IO 讀取時間
透過 uname 命令查看 kernel 版本
1 | root:# uname -r |
下載 systemtap 需要特別注意使用 Linux kernel 版本還有平台
因為 systemtap dbgsym 對於 kernel 版本需要對應一致,像我機器版本是5.4.0-1045-aws
所以會需要下載5.4.0-1045-aws
版本
我們需要在 update list 新增含有 dbgsym 下載路徑,所以可以跑下面語法
1 | codename=$(lsb_release -c | awk '{print $2}') |
如果下載完畢會顯示下載完畢的資訊如下
1 | root/pg_log# sudo apt-get install linux-image-$(uname -r)-dbgsym -y |
安裝完後使用 stap
應該可以看到類似下面提示
1 | root:/pg_log# stap |
調教成本因子
首先我們要先知道成本是一個概念,重點我們是要看哪種成本?
像成本有分 時間成本,金錢成本…..都是不同維度的成本
我們的做法會把成本校準成 時間成本 也就是實際執行時間和預估執行成本校準
在執行計畫中 QO 會利用 成本 (cost) 來判斷使用哪個執行計畫,而成本 (cost)是透過公式來計算出來 costsize.c
在公式中有幾個變數可以讓我們來調整執行計畫成本估算,就是所謂的成本因子
postgreSQL 預設給的成本因子並不能符合所有機器算法.不同的硬體環境CPU性能,IO性能各不相同,預設的成本因子可能不適合當前硬體.
EX: 同樣硬體設備,但 CPU 2 core 和 32 core 跑起來查詢時間就會有差異
我們在SET ENABLE_SEQSACN = OFF;
在計算成本時 SEQSACN 起始成本會是1.0e10
本次我們要調教的成本因子有下面五個,關於每個成本的意涵我都有說明
- seq_page_cost : 1 – Table Scan 每個 Block 成本因子
- random_page_cost : 4 – Index Scan Block 成本因子
- cpu_tuple_cost : 0.01 – CPU 處理每個 tuple 成本因子
- cpu_index_tuple_cost : 0.005 – Index scan tuple 成本因子
- cpu_operator_cost : 0.0025 – 操作符或函数成本因子
下面這張表是調教成本因子 sample data
1 | create table tbl (id int,val int, info text, create_time timestamp); |
成本因子說明
校準方法我們利用一元一次方程式 + 基準點校準每個因子
seq_page_cost和cpu_tuple_cost的校準
seq_page_cost通過stap測試得到.
cpu_tuple_cost通過公式得到.
使用 stap 監聽 process
我們後面就可以利用 stap
來監聽我們 postgreSQL Client 連接的 porcess,這邊有兩個參數我們需要替換
1 | stap -e ' |
- postgreSQL Bin Path:利用此指令
ps auxw | grep postgres | grep -- -
查看 postgres DB process 啟動路徑 & postgres.conf 位置 - postgreSQL pid: 透過
select pg_backend_pid();
可以查詢到資訊
透過 ps auxw | grep postgres | grep -- -
命令可以查找到 /usr/lib/postgresql/14/bin/postgres
執行程式路徑
1 | root@:/pg_log# ps auxw | grep postgres | grep -- - |
1 | postgres=# select pg_backend_pid(); |
依照上面參數我們替換後的樣板變成
1 | stap -e ' |
替換完畢後我們可以嘗試使用 root 執行上面命令,執行成功應該會出現類似下面訊息,代表我們已經成功監聽了
1 | Pass 1: parsed user script and 476 library scripts using 103952virt/90464res/7496shr/82980data kb, in 240usr/60sys/485real ms. |
後面會需要重複幾次上面操作,為了怕篇幅太長我只會貼上替換後的樣版
校準 seq_page_cost && cpu_tuple_cost
因為上面有說我們要校準時間成本
公式:實際執行時間 = (search blocks from disk) * (seq_page_cost) + (search rows) * (cpu_tuple_cost)
seq_page_cost = stap 監聽平均執行 IO 來當作成本因子,所以得到 seq_page_cost 後我們就可以求出 cpu_tuple_cost
.
實際執行時間
、search blocks from disk
、search rows
都可以透過執行計畫取得,所以上面公式變成一元一次方程式
在一個視窗開啟 stap 後,我們就可以進行查詢下面語法取得執行計畫,會發現 cost 跟實際執行時間差異很大(沒關係我們要調教)
1 | postgres=# explain (analyze,verbose,costs,buffers,timing) select * from tbl; |
後面在 stap 那個視窗會出現下面資訊
1 | query__start explain (analyze,verbose,costs,buffers,timing) select * from tbl;pid:58916 |
有了執行計畫跟平均執行 IO 就可以套入我們的公式了
6662 是奈秒(ns)我們需要轉換成毫秒(ms)
公式:實際執行時間 = (search blocks from disk) * (seq_page_cost) + (search rows) * (cpu_tuple_cost)
- search blocks from disk:154640
- seq_page_cost:6662 ns = 0.006662 ms
- search rows:15000000
- 實際執行時間 : 14004.456 - 0.736 = 14003.72
- cpu_tuple_cost = 0.0008649005546666667
14003.72= 154640 * 0.006662 + 15000000 * cpu_tuple_cost
所以我們可以得到
- cpu_tuple_cost = 0.0008649005546666667;
- seq_page_cost = 0.006662;
每次執行完為了精準度,我們需要清除cache並重新啟動 postgreSQL 清除 shared buffer 資料,讓每次資料都可以從 disk 撈取,所以都會執行下面兩個語法
1 | root:/pg_log# sync; echo 3 > /proc/sys/vm/drop_caches |
清除 cache & postgresql restart 後我們利用校準後的因子來查詢是否有效
1 | postgres=# SET cpu_tuple_cost = 0.0008649005546666667; |
能看到預估時間成本跟實際時間成本已經很接近了
因為使用 stap 會有一些時間消耗,可以多取幾次找到平均值這裡就不多說了
取得 random_page_cost 以及 cpu_index_tuple_cost , cpu_operator_cost
前面我們調教兩個成本因子,接下來要接續計算 random_page_cost 以及 cpu_index_tuple_cost , cpu_operator_cost
- cpu_tuple_cost = 0.0008649005546666667;
- seq_page_cost = 0.006662;
本次要利用 Index Scan 公式還計算剩下因子
最終執行時間成本 = (search index block) * random_page_cost + cpu_tuple_cost * (tuple search row) + cpu_index_tuple_cost * (tuple search row) + cpu_operator_cost * x
我們利用設定把讓執行計畫查詢 force 成 Index Scan,利用得到 stap & 執行計畫資料進行優化
1 | set random_page_cost=1; |
psql 執行如下
1 | postgres=# select pg_backend_pid(); |
stap 結果如下
1 | Pass 4: using cached /root/.systemtap/cache/66/stap_66dcbdbebf362f424bfd78405d2e262b_16109.ko |
再來就是把資訊套入公式中
最終執行時間成本 = (search index block) * random_page_cost + cpu_tuple_cost * (tuple search row) + cpu_index_tuple_cost * (tuple search row) + cpu_operator_cost * x
- 最終執行時間成本 = 3948.019
- cpu_tuple_cost = 0.0008649005546666667
- tuple search row = 9406
- 執行平均 IO 420006 ns = 0.420006 ms (random_page_cost)
套用進公式時會發現還是有許多未知數
3948.019 = (search index block) * 0.420006 + 0.0008649005546666667 * 9406 + cpu_index_tuple_cost * 9406 + cpu_operator_cost * x
search index block 有兩種方式可以查看第一個是利用
bt_page_items
了解範圍查詢有多少 block,另一種是利用基準點來估算出數值 (二元一次聯立方程式)
取得 search index block
我們先退出來 清除 cache & restart postgreSQL,執行下面語法
其中有一個地方不一樣
set random_page_cost=2;
,因為我們要利用 (二元一次聯立方程式) 求出我們的數值
1 | set random_page_cost=2; |
利用上面查詢資訊如下
1 | postgres=# set random_page_cost=2; |
這邊左邊的成本利用 估算時間 因為估算時間公式都一樣是上面我寫的那個 這次唯一有變動的部分只有
1 | 34131.99 = (search index block) * 1 + 9406 + cpu_index_tuple_cost * 9406 + cpu_operator_cost * x` |
這樣我們就可以取得 (search index block)
資訊,同理我們可以利用上面手法接著得到公式中 x
1 | set random_page_cost=1; |
我們把 set cpu_operator_cost=2;
其他一樣設定成1,下去做比較
1 | postgres=# set random_page_cost=1; |
計算步驟跟上面差不多,我就直接帶到核心計算公式
1 | 42844.99 - 34131.99 = x |
自此我們就把所有資訊都取得了
調教最終數值
- 最終執行時間成本 = 3948.019
- cpu_tuple_cost = 0.0008649005546666667
- tuple search row = 9406
- 執行平均 IO 420006 ns = 0.420006 ms (random_page_cost)
- x = 8713
- search index block = 8341
3948.019 = 8341 * 0.420006 + 0.0008649005546666667 * 9406 + cpu_index_tuple_cost * 9406 + cpu_operator_cost * 8713
把數值套用在上面公式,有人會說還有兩個變數怎麼解這個公式?
因為在建議設定值 cpu_operator_cost : cpu_index_tuple_cost = 1 : 2
所以我們可以把公式變成
1 | 3948.019 = 8341 * 0.420006 + 0.0008649005546666667 * 9406 + cpu_index_tuple_cost * 9406 + cpu_index_tuple_cost * 2 * 8713 |
最終調教因子結果
- cpu_index_tuple_cost : 0.016272126542292986
- cpu_operator_cost:0.03254425308458597
- cpu_tuple_cost = 0.0008649005546666667
- seq_page_cost = 0.006662;
- random_page_cost = 0.420006
最後我們可以利用調教完的因子進行查詢
1 | --index scan test |
發現預估成本時間和實際成本時間已經貼近許多了 (因為在使用 stap 時 cpu 會有額外消耗),所以建議在調教效能要找一台同規格且完全沒事情的機器來處理不然數值會有偏差
table scan & index scan 已經貼近真實時間很多了
1 | postgres=# set enable_seqscan=off; set enable_bitmapscan=off; explain (analyze,verbose,costs,buffers,timing) select * from tbl where id > 1998760000; |
小結
終於寫完這篇文章了,耗時許多且來來回回很多步驟要處理
假如對於上面說的覺得太複雜,可以參考網路上別人較為通用的設定值
Tuning RelStorage and parameters of PostgreSQL on Plone site
1 | set cpu_tuple_cost = 0.0030; |
建議如果有能力還是自己 tuning,因為這樣會更準確
此文作者:Daniel Shih(石頭)
此文地址: https://isdaniel.github.io/postgresql-cost-factor-tuning/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!