分享到

簡介

管理效能是在使用資料庫支援的應用程式時,一項持續進行的工作。執行緩慢的查詢可能會導致逾時、降低使用者體驗、使用更多資源,甚至可能影響您的預算,具體取決於您支付資料庫費用的方式。這些問題使得了解資料庫的效能特性變得非常重要,以便您可以識別和修復有問題的查詢。

在本指南中,我們將討論識別 PostgreSQL 資料庫中效能不佳查詢的不同方法。之後,我們將討論您可以使用的不同技術來修復慢速查詢,以維持 PostgreSQL 的效能。

檢查作用中查詢和程序

嘗試追蹤效能低落的查詢時,第一個要檢查的地方是目前作用中查詢和程序的清單。PostgreSQL 透過 pg_stat_activity 檢視表提供此資料。

pg_stat_activity 檢視表是 PostgreSQL 累積統計系統中可用的檢視表之一。它包含每個伺服器程序的列,可協助您檢閱每個程序目前正在執行的工作。

若要顯示檢視表中的所有資訊,請輸入

SELECT * FROM pg_stat_activity \gx
-[ RECORD 1 ]----+-------------------------------
datid |
datname |
pid | 1963
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.083043+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | AutoVacuumMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | autovacuum launcher
-[ RECORD 2 ]----+-------------------------------
datid |
datname |
pid | 1965
leader_pid |
usesysid | 10
usename | postgres
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.083926+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | LogicalLauncherMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | logical replication launcher
-[ RECORD 3 ]----+-------------------------------
datid | 13921
datname | postgres
pid | 836027
leader_pid |
usesysid | 10
usename | postgres
application_name | psql
client_addr |
client_hostname |
client_port | -1
backend_start | 2022-11-06 20:20:18.273218+01
xact_start | 2022-11-06 20:39:01.207078+01
query_start | 2022-11-06 20:39:01.207078+01
state_change | 2022-11-06 20:39:01.207088+01
wait_event_type |
wait_event |
state | active
backend_xid |
backend_xmin | 762
query_id |
query | select * from pg_stat_activity
backend_type | client backend
-[ RECORD 4 ]----+-------------------------------
datid |
datname |
pid | 1961
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.082354+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | BgWriterHibernate
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | background writer
-[ RECORD 5 ]----+-------------------------------
datid |
datname |
pid | 1960
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.082065+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | CheckpointerMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | checkpointer
-[ RECORD 6 ]----+-------------------------------
datid |
datname |
pid | 1962
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.082653+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | WalWriterMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | walwriter

注意: 使用 \gx 行終止序列而不是傳統的分號 (;) 會告知 PostgreSQL 對目前的查詢使用擴展輸出模式。這會垂直顯示每個記錄的欄和關聯值,而不是水平顯示,這在某些情況下可以提高可讀性。

輸出中有許多欄位在尋找較慢的查詢時可能會有所幫助。其中一些最相關的欄位包括

  • state:程序的目前狀態。列為 active 的列目前正在執行。其他狀態包括 idle(程序正在等待新的用戶端命令)、idle in transaction(程序正在交易內容中等待命令)以及 idle in transaction (aborted)(語句導致錯誤的交易)。
  • query:最近執行的查詢。對於作用中程序,這將是目前正在執行的查詢。
  • usename:與程序關聯的使用者名稱。
  • application_name:連線到程序的應用程式名稱。
  • datname:使用者連線到的資料庫名稱。
  • wait_event:程序正在等待的事件名稱(如果有的話)。如果程序具有 active 狀態且存在 wait_event,則表示查詢目前被系統的其他部分封鎖。
  • wait_event_type:程序正在等待的事件類別。
  • pid:程序的程序 ID。
  • query_start:對於作用中查詢,目前查詢開始的時間戳記。
  • xact_start:如果程序正在執行交易,則目前交易開始的時間戳記。

我們可以依與目前內容相關的任何欄位篩選查詢。一種有用的模式是使用 age() 函數來計算查詢已執行多長時間。例如

SELECT
age(clock_timestamp(), query_start),
usename,
datname,
query
FROM pg_stat_activity
WHERE
state != 'idle'
AND query NOT ILIKE '%pg_stat_activity%'
ORDER BY age desc;

這將顯示非閒置查詢的執行時間、使用者名稱、資料庫和查詢文字。我們將結果從執行時間最長到最短的查詢排序,並從結果中排除此特定查詢。

同樣地,您可以看到所有非閒置但確實有等待事件的程序

SELECT
usename,
datname,
query,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE
state != 'idle'
AND query wait_event != ''

這可以幫助您查看目前由於系統的其他部分(例如,鎖定爭用)而未進展的查詢。

檢查其他系統統計資訊

雖然 pg_stat_activity 檢視表可能會提供您識別較慢查詢所需的大部分資訊,但查看其他系統統計資訊也可能很有用,以協助識別其他最佳化目標。

檢視資料庫統計資訊

pg_stat_database 表格包含每個資料庫的統計資訊

SELECT * FROM pg_stat_database \gx
. . .
-[ RECORD 2 ]------------+------------------------------
datid | 13921
datname | postgres
numbackends | 1
xact_commit | 266
xact_rollback | 9
blks_read | 229
blks_hit | 11263
tup_returned | 118708
tup_fetched | 3563
tup_inserted | 0
tup_updated | 0
tup_deleted | 0
conflicts | 0
temp_files | 0
temp_bytes | 0
deadlocks | 0
checksum_failures |
checksum_last_failure |
blk_read_time | 0
blk_write_time | 0
session_time | 5303626.534
active_time | 200.906
idle_in_transaction_time | 0
sessions | 2
sessions_abandoned | 0
sessions_fatal | 0
sessions_killed | 0
stats_reset | 2022-11-06 20:20:18.279798+01
. . .

對於我們的目的而言,一些有趣的欄位包括

  • blks_read:資料庫中讀取的磁碟區塊數。
  • blks_hit:在緩衝快取中找到磁碟區塊的次數(避免從磁碟進行慢速讀取)。
  • xact_commit:已提交的交易數。
  • xact_rollback:已回滾的交易數。

Data Egret 團隊在其部落格上所示,您可以使用這些原始值來計算有趣的統計資訊,例如快取命中率

SELECT
datname,
100 * blks_hit / (blks_hit + blks_read) as cache_hit_ratio
FROM
pg_stat_database
WHERE
(blks_hit + blks_read) > 0;
datname | cache_hit_ratio
-----------+-----------------
| 99
postgres | 98
template1 | 99
(3 rows)

這可能是寶貴的資訊,可以幫助您評估是否可以從向資料庫叢集新增 RAM 中受益,以便可以有效地快取最常見的查詢。

檢視表格統計資訊

另一個有用的檢視表系列是 pg_stat_all_tablespg_stat_user_tablespg_stat_sys_tablespg_stat_all_tables 檢視表顯示所有資料庫的存取統計資訊,而其他兩個檢視表則根據表格是使用者表格還是系統表格來篩選表格。

SELECT * FROM pg_stat_all_tables \gx
. . .
-[ RECORD 104 ]-----+------------------------
relid | 1262
schemaname | pg_catalog
relname | pg_database
seq_scan | 5168
seq_tup_read | 20655
idx_scan | 20539
idx_tup_fetch | 20539
n_tup_ins | 0
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 0
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0
autoanalyze_count | 0

這些檢視表中的一些有趣欄位包括

  • seq_scan:在表格上執行的循序掃描次數。
  • seq_tup_read:從循序掃描傳回的列數。
  • idx_scan:針對表格執行的索引掃描次數。
  • idx_tup_fetch:透過索引擷取的列數。

這些欄位中的數字可以幫助您評估索引的效能,以及您執行的查詢是否有效地使用它們。如果您發現表格有許多循序掃描,您可能會受益於建立額外的索引,供您最常見的查詢使用。

檢視索引命中次數

如果您需要更多關於目前擁有的索引的資訊,您可以查看 pg_stat_all_indexespg_stat_user_indexespg_stat_sys_indexes 檢視表

SELECT * FROM pg_stat_all_indexes \gx
. . .
-[ RECORD 6 ]-+----------------------------------------------
relid | 1249
indexrelid | 2659
schemaname | pg_catalog
relname | pg_attribute
indexrelname | pg_attribute_relid_attnum_index
idx_scan | 822
idx_tup_read | 1670
idx_tup_fetch | 1670
. . .

這些檢視表為您提供每個索引使用頻率的資訊。idx_scan 欄顯示索引已掃描的次數。idx_tup_read 欄顯示掃描傳回的項目數,而 idx_tup_fetch 顯示索引掃描傳回的總列數。

此資訊對於幫助您了解何時擁有查詢未使用的索引非常有用。一旦您識別出這些索引,您可以重寫查詢以利用索引,或者您可以移除未使用的索引以提高寫入效能。

檢視鎖定資訊

您收集到的一些關於慢速查詢的資訊可能指向鎖定問題。您可以透過查詢 pg_locks 檢視表來找到更多關於目前持有的所有鎖定的資訊

SELECT * FROM pg_locks \gx
-[ RECORD 1 ]------+----------------
locktype | relation
database | 13921
relation | 12290
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 3/3920
pid | 967262
mode | AccessShareLock
granted | t
fastpath | t
waitstart |
-[ RECORD 2 ]------+----------------
locktype | virtualxid
database |
relation |
page |
tuple |
virtualxid | 3/3920
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 3/3920
pid | 967262
mode | ExclusiveLock
granted | t
fastpath | t
waitstart |

輸出將提供關於 PostgreSQL 中所有鎖定的資訊。這可以幫助您診斷在不同程序請求控制相同物件時可能發生的爭用問題。

一些可能有助於您調查有問題鎖定的欄位包括

  • locktype可鎖定物件的類型
  • database/relation/page/tuple:鎖定項目的物件 ID。對於資料庫和關聯,這些可以在 pg_databasepg_class 中交叉參照。
  • mode:已實作或請求的鎖定模式
  • granted:一個布林值,表示是否已授予鎖定。

啟用慢速查詢記錄

尋找有關長時間執行查詢資訊的一種更簡單方法是啟用慢速查詢記錄。啟用慢速查詢記錄可讓 PostgreSQL 自動記錄任何執行時間超過給定時間量的查詢。這可讓您收集有關目前未在調查時執行的慢速查詢的資訊。

檢查 PostgreSQL 是否已記錄慢速查詢

您應該做的第一件事是驗證慢速查詢記錄的目前狀態。如果已啟用慢速查詢記錄,則您無需執行任何操作。

您可以透過輸入以下內容來檢查是否已啟用慢速查詢記錄

SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------
name | log_min_duration_statement
setting | -1
unit | ms
category | Reporting and Logging / When to Log
short_desc | Sets the minimum execution time above which all statements will be logged.
extra_desc | Zero prints all queries. -1 turns this feature off.
context | superuser
vartype | integer
source | default
min_val | -1
max_val | 2147483647
enumvals |
boot_val | -1
reset_val | -1
sourcefile |
sourceline |
pending_restart | f

如果您檢查 short_descextra_desc 欄位的值,您將找到允許我們評估目前是否已啟用記錄的資訊。我們可以發現目前啟用慢速查詢記錄,因為 setting 欄目前設定為 -1

現在您已知道目前狀態,您可以根據需要變更它。

設定 PostgreSQL 以記錄慢速查詢

在我們繼續之前,務必注意,雖然慢速查詢記錄非常有用,但它可能會對效能產生額外的影響。PostgreSQL 必須執行額外的操作來計時每個查詢,並將結果記錄到日誌中。這可能會影響效能並意外填滿硬碟空間。

在所有時間都記錄慢速查詢可能不是一個好主意。相反地,當您主動調查問題時啟用此功能,並在完成後停用它。

全域記錄慢速查詢

考慮到這一點,您可以透過修改 PostgreSQL 伺服器的組態檔來全域設定慢速查詢記錄。您也可以互動式地修改這些值,但在組態中設定良好的預設值將使稍後更容易進行互動式調整。

開啟 PostgreSQL 的組態檔。您可以透過輸入以下內容來找到目前組態檔的位置

SHOW config_file;
config_file
-----------------------------------------
/etc/postgresql/14/main/postgresql.conf
(1 row)

在檔案內,搜尋 log_min_duration_statement 設定。如果我們上面的範例輸出值是從組態檔讀取的,則會將其設定為 -1,以指示目前已停用該功能。還有許多其他相關設定,您可以根據需要進行調整

. . .
# Query logging configuration
#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements
# and their durations, > 0 logs only a sample of
# statements running at least this number
# of milliseconds;
# sample fraction is determined by log_statement_sample_rate
#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding
# log_min_duration_sample to be logged;
# 1.0 logs all such statements, 0.0 never logs
#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements
# are logged regardless of their duration; 1.0 logs all
# statements from all transactions, 0.0 never logs
. . .

目前,log_min_duration_statement 設定已註解掉,其目前值設定為 -1 以表示預設值。其他設定在檔案中都有很好的註解,可讓您對超過最小值的語句進行取樣,而不是記錄所有語句。最後一個設定可讓您對交易中發生的語句進行取樣。

您可以透過取消註解 log_min_duration_statement 並將其設定為另一個值來開啟長時間查詢記錄。例如,我們可以將其設定為 5 秒,以記錄任何執行時間超過該時間的語句

log_min_duration_statement = 5s

儲存檔案後,您可以透過在 PostgreSQL 中輸入以下內容,從 PostgreSQL 重新載入伺服器

SELECT pg_reload_conf();

您可以透過再次檢查目前值來驗證伺服器是否正在使用您的新設定

SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------
name | log_min_duration_statement
setting | 5000
unit | ms
category | Reporting and Logging / When to Log
short_desc | Sets the minimum execution time above which all statements will be logged.
extra_desc | Zero prints all queries. -1 turns this feature off.
context | superuser
vartype | integer
source | configuration file
min_val | -1
max_val | 2147483647
enumvals |
boot_val | -1
reset_val | 5000
sourcefile | /etc/postgresql/14/main/postgresql.conf
sourceline | 506
pending_restart | f

現在,setting 欄位設定為 5000,而 unit 欄位設定為 ms,表示我們的 5 秒設定已轉換為 5000 毫秒並已套用。sourcefile 行也確認此值是從我們修改的組態檔中讀取的。

每個資料庫記錄慢速查詢

嘗試偵測慢速查詢時的另一個選項是將慢速查詢記錄限制為特定資料庫。雖然 log_min_duration_statement 可以全域設定,正如我們在上一節中所示,但它也可以在資料庫層級設定。

若要為單一資料庫開啟慢速查詢記錄,請使用 ALTER DATABASE 命令

ALTER DATABASE helloprisma SET 'log_min_duration_statement' = 2000;
ALTER DATABASE

請注意,與全域設定不同,當使用 ALTER DATABASE 命令時,該值必須是不帶單位的整數,表示時間(以毫秒為單位)。

我們可以透過查詢每個資料庫的角色設定來驗證設定是否已套用

\drds
List of settings
Role | Database | Settings
------+-------------+-------------------------------
| helloprisma | log_min_duration_statement=2000
(1 row)

我們可以驗證這是否沒有干擾我們先前設定為 5 秒閾值的全域設定

SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------
name | log_min_duration_statement
setting | 5000
unit | ms
category | Reporting and Logging / When to Log
short_desc | Sets the minimum execution time above which all statements will be logged.
extra_desc | Zero prints all queries. -1 turns this feature off.
context | superuser
vartype | integer
source | configuration file
min_val | -1
max_val | 2147483647
enumvals |
boot_val | -1
reset_val | 5000
sourcefile | /etc/postgresql/14/main/postgresql.conf
sourceline | 506
pending_restart | f

測試慢速查詢記錄

透過發出一個超過最小記錄持續時間的語句來測試設定

SELECT pg_sleep(10);
pg_sleep
----------
(1 row)

檢查日誌,您應該會找到指示發生長時間執行查詢的語句

2022-11-11 17:58:04.719 CET [1121088] postgres@postgres STATEMENT: select sleep(10);
2022-11-11 17:58:42.635 CET [1121088] postgres@postgres LOG: duration: 10017.171 ms statement: select pg_sleep(10);

由於我們對全域限制和特定表格有不同的閾值,因此我們可以透過使用應觸發其中一個但不觸發另一個的查詢時間來測試是否正確套用每個閾值。

例如,我們可以連線到具有較低閾值的資料庫並休眠 4 秒,這應該會觸發日誌行

\c helloprisma
SELECT pg_sleep(4);

我們的日誌顯示

2022-11-13 14:46:07.361 CET [1252789] postgres@helloprisma STATEMENT: alter database helloprisma set log_min_duration_statement=2s;
2022-11-13 14:53:05.027 CET [1309069] postgres@helloprisma LOG: duration: 4022.546 ms statement: select pg_sleep(4);

現在,我們可以切換到另一個僅受全域設定影響的資料庫。相同的休眠語句不應觸發日誌行

\c postgres
SELECT pg_sleep(4);

不應記錄新的日誌行。

結論

在本文中,我們介紹了如何檢視和理解 PostgreSQL 提供的某些效能資訊。檢視此資訊可讓您深入了解系統資源、查詢模式、組態設定中的不同瓶頸。當您遇到效能緩慢時,您可以檢查 PostgreSQL 提供的資訊,開始調查有問題的行為。

我們也討論了如何使用慢速查詢記錄來精確指出哪些查詢佔用系統資源,以及執行時間超出預期。記錄此資料並評估產生的日誌可以幫助您識別可能需要額外索引、不同查詢結構或更有效率的查詢設計的位置。了解如何識別這些耗費資源的操作是朝向執行更具功能性的資料庫支援應用程式的第一步。

關於作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年以來一直撰寫關於資料庫、Linux、基礎架構和開發人員工具的文章。他目前與妻子和兩隻兔子住在柏林。他通常不必以第三人稱寫作,這對所有相關方來說都是一種解脫。