PostgreSQL / データの読み取りとクエリ
PostgreSQLパフォーマンス最適化入門
はじめに
データベースを利用したアプリケーションでは、パフォーマンス管理は継続的なタスクです。実行が遅いクエリは、タイムアウトを引き起こし、ユーザーエクスペリエンスを低下させ、より多くのリソースを消費し、データベースの支払い方法によっては予算に影響を与えることさえあります。これらの問題があるため、問題のあるクエリを特定して修正できるように、データベースのパフォーマンス特性を理解することが重要です。
このガイドでは、PostgreSQLデータベースでパフォーマンスの低いクエリを特定するさまざまな方法について説明します。その後、PostgreSQLのパフォーマンスを維持するために遅いクエリを修正するために使用できるさまざまなテクニックについて議論します。
Prisma ORMのPostgreSQLコネクタを使用すると、JavaScriptおよびTypeScriptアプリケーションからPostgreSQLデータベースを直接管理できます。
既存のプロジェクトにPrismaを追加する方法、またはゼロから始める方法を学びましょう。
CLIから本番環境対応のPrisma PostgresデータベースでPrisma ORMをすばやくセットアップできます。
npx prisma@latest init --db
その後、このガイドに従って完全なTypeScriptアプリケーションを構築してください。
アクティブなクエリとプロセスを確認する
パフォーマンスの低いクエリを特定しようとするときに最初に確認すべき場所は、現在アクティブなクエリとプロセスのリストです。PostgreSQLは、pg_stat_activity
ビューでこのデータを利用可能にしています。
pg_stat_activity
ビューは、PostgreSQLの累積統計システムで利用可能なビューの1つです。これはサーバープロセスごとに1行を含み、各プロセスが現在何に取り組んでいるかを確認するのに役立ちます。
ビュー内のすべての情報を表示するには、次のように入力します
SELECT * FROM pg_stat_activity \gx
-[ RECORD 1 ]----+-------------------------------datid |datname |pid | 1963leader_pid |usesysid |usename |application_name |client_addr |client_hostname |client_port |backend_start | 2022-11-01 11:03:44.083043+01xact_start |query_start |state_change |wait_event_type | Activitywait_event | AutoVacuumMainstate |backend_xid |backend_xmin |query_id |query |backend_type | autovacuum launcher-[ RECORD 2 ]----+-------------------------------datid |datname |pid | 1965leader_pid |usesysid | 10usename | postgresapplication_name |client_addr |client_hostname |client_port |backend_start | 2022-11-01 11:03:44.083926+01xact_start |query_start |state_change |wait_event_type | Activitywait_event | LogicalLauncherMainstate |backend_xid |backend_xmin |query_id |query |backend_type | logical replication launcher-[ RECORD 3 ]----+-------------------------------datid | 13921datname | postgrespid | 836027leader_pid |usesysid | 10usename | postgresapplication_name | psqlclient_addr |client_hostname |client_port | -1backend_start | 2022-11-06 20:20:18.273218+01xact_start | 2022-11-06 20:39:01.207078+01query_start | 2022-11-06 20:39:01.207078+01state_change | 2022-11-06 20:39:01.207088+01wait_event_type |wait_event |state | activebackend_xid |backend_xmin | 762query_id |query | select * from pg_stat_activitybackend_type | client backend-[ RECORD 4 ]----+-------------------------------datid |datname |pid | 1961leader_pid |usesysid |usename |application_name |client_addr |client_hostname |client_port |backend_start | 2022-11-01 11:03:44.082354+01xact_start |query_start |state_change |wait_event_type | Activitywait_event | BgWriterHibernatestate |backend_xid |backend_xmin |query_id |query |backend_type | background writer-[ RECORD 5 ]----+-------------------------------datid |datname |pid | 1960leader_pid |usesysid |usename |application_name |client_addr |client_hostname |client_port |backend_start | 2022-11-01 11:03:44.082065+01xact_start |query_start |state_change |wait_event_type | Activitywait_event | CheckpointerMainstate |backend_xid |backend_xmin |query_id |query |backend_type | checkpointer-[ RECORD 6 ]----+-------------------------------datid |datname |pid | 1962leader_pid |usesysid |usename |application_name |client_addr |client_hostname |client_port |backend_start | 2022-11-01 11:03:44.082653+01xact_start |query_start |state_change |wait_event_type | Activitywait_event | WalWriterMainstate |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()
関数を使用してクエリの実行時間を計算することです。例えば
SELECTage(clock_timestamp(), query_start),usename,datname,queryFROM pg_stat_activityWHEREstate != 'idle'AND query NOT ILIKE '%pg_stat_activity%'ORDER BY age desc;
これにより、アイドル状態ではないクエリの実行時間、ユーザー名、データベース、およびクエリテキストが表示されます。結果は実行時間の長いものから短いものの順に並べ替えられ、この特定のクエリは結果から除外されます。
同様に、アイドル状態ではないが待機イベントを持つすべてのプロセスを表示できます。
SELECTusename,datname,query,wait_event_type,wait_eventFROM pg_stat_activityWHEREstate != 'idle'AND query wait_event != ''
これは、システム内の他の部分(例えば、ロック競合)のために現在進行していないクエリを確認するのに役立ちます。
その他のシステム統計を確認する
pg_stat_activity
ビューは、遅いクエリを特定するために必要な情報のほとんどを提供するでしょうが、最適化のための追加ターゲットを特定するのに役立つ他のシステム統計も確認すると役立つ場合があります。
データベース統計の表示
pg_stat_database
テーブルには、各データベースに関する統計が含まれています。
SELECT * FROM pg_stat_database \gx
. . .-[ RECORD 2 ]------------+------------------------------datid | 13921datname | postgresnumbackends | 1xact_commit | 266xact_rollback | 9blks_read | 229blks_hit | 11263tup_returned | 118708tup_fetched | 3563tup_inserted | 0tup_updated | 0tup_deleted | 0conflicts | 0temp_files | 0temp_bytes | 0deadlocks | 0checksum_failures |checksum_last_failure |blk_read_time | 0blk_write_time | 0session_time | 5303626.534active_time | 200.906idle_in_transaction_time | 0sessions | 2sessions_abandoned | 0sessions_fatal | 0sessions_killed | 0stats_reset | 2022-11-06 20:20:18.279798+01. . .
私たちの目的のために興味深い列には、次のようなものがあります
blks_read
:データベース内で読み取られたディスクブロックの数。blks_hit
:代わりにバッファキャッシュ内でディスクブロックが見つかった回数(ディスクからの遅い読み取りを回避)。xact_commit
:コミットされたトランザクションの数。xact_rollback
:ロールバックされたトランザクションの数。
Data Egretチームがブログで示しているように、これらの生の値を使用してキャッシュヒット率などの興味深い統計を計算できます。
SELECTdatname,100 * blks_hit / (blks_hit + blks_read) as cache_hit_ratioFROMpg_stat_databaseWHERE(blks_hit + blks_read) > 0;
datname | cache_hit_ratio-----------+-----------------| 99postgres | 98template1 | 99(3 rows)
これは、最も一般的なクエリを効果的にキャッシュできるように、データベースクラスターにRAMを追加することでメリットが得られるかどうかを評価するのに役立つ貴重な情報です。
テーブル統計の表示
他に役立つビュー群として、pg_stat_all_tables
、pg_stat_user_tables
、およびpg_stat_sys_tables
があります。pg_stat_all_tables
ビューはすべてのデータベースのアクセス統計を表示し、他の2つのビューはテーブルがユーザーテーブルかシステムテーブルかに基づいてフィルタリングします。
SELECT * FROM pg_stat_all_tables \gx
. . .-[ RECORD 104 ]-----+------------------------relid | 1262schemaname | pg_catalogrelname | pg_databaseseq_scan | 5168seq_tup_read | 20655idx_scan | 20539idx_tup_fetch | 20539n_tup_ins | 0n_tup_upd | 0n_tup_del | 0n_tup_hot_upd | 0n_live_tup | 0n_dead_tup | 0n_mod_since_analyze | 0n_ins_since_vacuum | 0last_vacuum |last_autovacuum |last_analyze |last_autoanalyze |vacuum_count | 0autovacuum_count | 0analyze_count | 0autoanalyze_count | 0
これらのビューにおける興味深い列には、次のようなものがあります
seq_scan
:テーブルに対して実行されたシーケンシャルスキャンの数。seq_tup_read
:シーケンシャルスキャンから返された行の数。idx_scan
:テーブルに対して実行されたインデックススキャンの数。idx_tup_fetch
:インデックスを介して取得された行の数。
これらの列の数値は、インデックスのパフォーマンスと、実行しているクエリによって効果的に使用されているかどうかを評価するのに役立ちます。テーブルに多数のシーケンシャルスキャンがある場合は、最も一般的なクエリで使用できる追加のインデックスを作成するとメリットが得られるでしょう。
インデックスヒットの表示
現在持っているインデックスに関する詳細情報が必要な場合は、pg_stat_all_indexes
、pg_stat_user_indexes
、およびpg_stat_sys_indexes
ビューを参照できます。
SELECT * FROM pg_stat_all_indexes \gx
. . .-[ RECORD 6 ]-+----------------------------------------------relid | 1249indexrelid | 2659schemaname | pg_catalogrelname | pg_attributeindexrelname | pg_attribute_relid_attnum_indexidx_scan | 822idx_tup_read | 1670idx_tup_fetch | 1670. . .
これらは、各インデックスがどれくらいの頻度で使用されているかに関する情報を提供します。idx_scan
列はインデックスがスキャンされた回数を示します。idx_tup_read
列はスキャンによって返されたエントリの数を示し、idx_tup_fetch
はインデックススキャンによって返された行の総数を示します。
この情報は、クエリで使われていないインデックスがあるかどうかを理解するのに役立ちます。それらのインデックスを特定したら、インデックスを活用するようにクエリを書き換えるか、未使用のインデックスを削除して書き込みパフォーマンスを向上させることができます。
ロック情報の表示
収集した遅いクエリに関する情報の一部は、ロックの問題を示しているかもしれません。pg_locks
ビューをクエリすることで、現在保持されているすべてのロックに関する詳細情報を確認できます。
SELECT * FROM pg_locks \gx
-[ RECORD 1 ]------+----------------locktype | relationdatabase | 13921relation | 12290page |tuple |virtualxid |transactionid |classid |objid |objsubid |virtualtransaction | 3/3920pid | 967262mode | AccessShareLockgranted | tfastpath | twaitstart |-[ RECORD 2 ]------+----------------locktype | virtualxiddatabase |relation |page |tuple |virtualxid | 3/3920transactionid |classid |objid |objsubid |virtualtransaction | 3/3920pid | 967262mode | ExclusiveLockgranted | tfastpath | twaitstart |
この出力は、PostgreSQL内のすべてのロックに関する情報を提供します。これは、個別のプロセスが同じオブジェクトの制御を要求したときに発生する競合問題を診断するのに役立ちます。
問題のあるロックを調査するのに役立つ可能性のある列には、次のようなものがあります
locktype
:ロック可能なオブジェクトの種類database/relation/page/tuple
:ロックされたアイテムのオブジェクトID。データベースとリレーションについては、pg_database
とpg_class
で相互参照できます。mode
:実装または要求されたロックモード。granted
:ロックが付与されたかどうかを示すブール値。
スロークエリロギングを有効にする
長時間実行されるクエリに関する情報をより簡単に検索する方法の1つは、スロークエリロギングを有効にすることです。スロークエリロギングを有効にすると、PostgreSQLは指定された時間を超えて実行されるクエリを自動的に記録します。これにより、調査時に実行されていないスロークエリに関する情報を収集できます。
PostgreSQLがすでにスロークエリをログに記録しているか確認する
最初にすべきことは、スロークエリロギングの現在の状態を確認することです。スロークエリロギングがすでに有効になっている場合は、何もする必要はありません。
スロークエリロギングが有効になっているかどうかは、次のように入力して確認できます
SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------name | log_min_duration_statementsetting | -1unit | mscategory | Reporting and Logging / When to Logshort_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 | superuservartype | integersource | defaultmin_val | -1max_val | 2147483647enumvals |boot_val | -1reset_val | -1sourcefile |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_statementsetting | 5000unit | mscategory | Reporting and Logging / When to Logshort_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 | superuservartype | integersource | configuration filemin_val | -1max_val | 2147483647enumvals |boot_val | -1reset_val | 5000sourcefile | /etc/postgresql/14/main/postgresql.confsourceline | 506pending_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 settingsRole | 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_statementsetting | 5000unit | mscategory | Reporting and Logging / When to Logshort_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 | superuservartype | integersource | configuration filemin_val | -1max_val | 2147483647enumvals |boot_val | -1reset_val | 5000sourcefile | /etc/postgresql/14/main/postgresql.confsourceline | 506pending_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 helloprismaSELECT 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 postgresSELECT pg_sleep(4);
新しいログ行は記録されないはずです。
まとめ
この記事では、PostgreSQLが提供するパフォーマンス情報を表示し、理解する方法について説明しました。この情報を表示することで、システムリソース、クエリパターン、設定におけるさまざまなボトルネックを把握できます。パフォーマンスが低下した場合は、PostgreSQLが提供する情報を確認して、問題のある動作の調査を開始できます。
また、スロークエリロギングを使用して、どのクエリがシステムリソースを占有し、予想よりも長く実行されているかを正確に特定する方法についても説明しました。このデータを記録し、結果のログを評価することで、追加のインデックス、異なるクエリ構造、またはより効率的なクエリ設計が必要な場所を特定するのに役立ちます。これらの高コストな操作を識別する方法を知ることは、より機能的なデータベースバックエンドアプリケーションを実行するための最初のステップです。
PostgreSQLデータベースコネクタは、JavaScriptおよびTypeScriptアプリケーションからPostgreSQLデータベースを管理するのに役立ちます。既存のプロジェクトにPrismaを追加する方法、またはゼロからPrismaを始める方法を学びましょう。