共有

はじめに

データベースを利用したアプリケーションでは、パフォーマンス管理は継続的なタスクです。実行が遅いクエリは、タイムアウトを引き起こし、ユーザーエクスペリエンスを低下させ、より多くのリソースを消費し、データベースの支払い方法によっては予算に影響を与えることさえあります。これらの問題があるため、問題のあるクエリを特定して修正できるように、データベースのパフォーマンス特性を理解することが重要です。

このガイドでは、PostgreSQLデータベースでパフォーマンスの低いクエリを特定するさまざまな方法について説明します。その後、PostgreSQLのパフォーマンスを維持するために遅いクエリを修正するために使用できるさまざまなテクニックについて議論します。

アクティブなクエリとプロセスを確認する

パフォーマンスの低いクエリを特定しようとするときに最初に確認すべき場所は、現在アクティブなクエリとプロセスのリストです。PostgreSQLは、pg_stat_activityビューでこのデータを利用可能にしています。

pg_stat_activityビューは、PostgreSQLの累積統計システムで利用可能なビューの1つです。これはサーバープロセスごとに1行を含み、各プロセスが現在何に取り組んでいるかを確認するのに役立ちます。

ビュー内のすべての情報を表示するには、次のように入力します

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:プロセスがトランザクションを実行している場合、現在のトランザクションが開始されたときのタイムスタンプ。

現在のコンテキストに関連する任意の列でクエリをフィルタリングできます。役立つパターンの1つは、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_tables、およびpg_stat_sys_tablesがあります。pg_stat_all_tablesビューはすべてのデータベースのアクセス統計を表示し、他の2つのビューはテーブルがユーザーテーブルかシステムテーブルかに基づいてフィルタリングします。

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_indexes、およびpg_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:ロックが付与されたかどうかを示すブール値。

スロークエリロギングを有効にする

長時間実行されるクエリに関する情報をより簡単に検索する方法の1つは、スロークエリロギングを有効にすることです。スロークエリロギングを有効にすると、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_descおよびextra_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

ジャスティン・エリングウッド

ジャスティンは2013年からデータベース、Linux、インフラストラクチャ、開発者ツールについて執筆しています。現在は妻と2匹のウサギとともにベルリンに住んでいます。彼は通常、三人称で書く必要がないため、関係者全員にとって安心です。
© . All rights reserved.