注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。
このページでは、Postgres でクエリのパフォーマンスを最適化するためのチューニングに焦点を当てています。一般的には、データの読み書きには オブジェクトセット や Actions のようなオントロジー機能を利用して、Slate アプリケーションを構築することをお勧めします。
応答性の高いアプリケーションを設計するには、慎重な計画と考慮が必要です。データモデル、クエリ構造、依存関係グラフは、それぞれアプリケーションのパフォーマンスと使いやすさに重要な役割を果たします。このガイドでは、Slate で記述できる PostgreSQL クエリのパフォーマンスチューニングの側面に焦点を当てており、パフォーマンスのヒューリスティックス、チューニングの機会を特定する方法、パフォーマンスを向上させるための提案を含んでいます。
クエリのパフォーマンスチューニングは反復的なプロセスです。同じ要求を「正しい」答えにするために、多くの異なる方法で書くことができます。
PostgreSQL クエリの目標実行時間は <= 500ms です。これは、対象となるデータセットが 10m レコード未満の場合、ほとんどのユースケースで可能ですが、データモデルの設計時には慎重な計画と考慮が必要です。
特に新しいデータセットでクエリパフォーマンスを評価する際は、統計を収集して分析する前にクエリを複数回実行することを確認してください。これにより、データセットが適切にキャッシュされ、パフォーマンスが予想される使用状況により一致するようになります。PostgreSQL のキャッシュについて詳しく知りたい場合は、以下のリンクを参照してください:
EXPLAIN は、Postgres が提供するクエリ実行計画を返す特に有用なコマンドです。Postgres が受け取るすべての要求に対してクエリプランが作成され、クエリ構造とデータの特性を使用して要求を最速で処理する方法を決定します。このガイド全体で参照されるため、最初に EXPLAIN コマンドを簡単にレビューします。
ノードは作業の論理単位、つまりクエリ評価のステップと考えることができます。ノードは逆グラフとして返され、応答の最初の行が最後の作業単位を示します。各ノードは ->
で始まります。
-> Index Scan using event_type_idx on event (...)
なぜこれが重要なのか: ノードはクエリ実行計画における非効率な操作を特定し、パフォーマンスチューニングの優先順位を付けるのに役立ちます。
最初の数値はスタートアップコスト(最初のレコードを取得する時間)です。2番目の数値はスタートアップから完了までのノード全体を処理するコストです。
(**cost=86.83..4577.07** rows=2368 width=10)
コストは、Postgres クエリプランナーがオブジェクト(一般的にはテーブル)統計に基づいて生成する推定値です。この数値は実際の実行時間を示すものではありませんが、実行時間と直接相関するはずです。
コストは、順次フェッチ、非順次(ランダム)フェッチ、行の処理、演算子(関数)の処理、インデックスエントリの処理など、いくつかの作業コンポーネントの組み合わせです。このコストは I/O および CPU 活動を表し、数値が大きいほど、Postgres がタスクを完了するために必要な作業量が多いことを示します。Postgres クエリオプティマイザはコストに基づいてどの実行プランを使用するかを決定します。
このプランノードが出力する予定の行数の推定値です。
(cost=86.83..4577.07 **rows=2368** width=10)
なぜこれが重要なのか: ROWS は、大量のデータを返しているか、期待通りに動作していないノードを特定するのに役立ちます。
このノードが出力する行の推定平均サイズ(バイト単位)です。
(cost=86.83..4577.07 rows=2368 **width=10**)
なぜこれが重要なのか: 幅 は、非常に大きなプロパティを持つ行や、多数の列を出力するノードを特定するのに役立ちます。
コスト と同様に、最初の数値はスタートアップに必要な実際の時間(ミリ秒単位)です。2番目の数値はスタートアップから完了までのノード全体を処理する実際の時間です。
(**actual time=10.313..12.530** rows=4857 loops=1)
名前が示すように、実際の時間はステートメントの実行によって取得されます。キーワード ANALYZE
は、実行プランを表示するのと同時にクエリを実行するよう Postgres に指示します。クエリのタイムアウトに悩んでいる場合、ANALYZE
を削除するとクエリプランだけが返され、クエリを実行するよりもかなり高速になります。
なぜこれが重要なのか: これは、パフォーマンスの問題を引き起こしているノードや操作を特定する最も明確な指標です。
このノードが出力する予定の行数の推定値です。
(actual time=10.313..12.530 **rows=4857** loops=1)
なぜこれが重要なのか: ROWS は、特定の操作が予想以上に時間がかかる理由についてのコンテキストを提供するのに役立ちます。
ノードの実行回数の合計を報告します。表示される実際の時間および行の値は、実行ごとの平均値です。ループ値に実際の時間を掛けると、ノードに費やされた総時間が得られます。
(actual time=10.313..12.530 rows=150 **loops=10**)
順次スキャン
(seq scan): Seq Scan 操作は、ディスクに格納されているリレーション(テーブル)全体をスキャンします(TABLE ACCESS FULL のように)。順次スキャンはリレーションのスキーマ、サイズ、制約、インデックスの有無に関係なく、常に可能です。
インデックススキャン
: Index Scan は B ツリーの走査を実行し、リーフノードを通じてすべての一致するエントリを見つけ、対応するテーブルデータを取得します。これは、INDEX RANGE SCAN に続いて TABLE ACCESS BY INDEX ROWID 操作を行うようなものです。
ビットマップインデックス/ヒープスキャン
: 通常の Index Scan は、インデックスからタプルポインタを1つずつ取得し、すぐにそのタプルをテーブルで訪れます。ビットマップスキャンは、インデックスからすべてのタプルポインタを一度に取得し、インメモリの "ビットマップ" データ構造を使用してそれらをソートし、その後、物理タプル位置の順序でテーブルタプルを訪れます。
インデックスのみ
: Index Only Scan は、B ツリーの走査を実行し、リーフノードを通じてすべての一致するエントリを見つけます。インデックスにクエリを満たすためのすべての列が含まれているため、テーブルアクセスは必要ありません(MVCC 可視性情報を除く)。結合操作は通常、一度に2つのテーブルのみを処理します。クエリが2つ以上のテーブルの結合を含む場合、結合は順次実行されます:最初に2つのテーブルで、その後中間結果と次のテーブルで、というように。この文脈での "テーブル" という用語は、中間結果も意味する場合があります。
ネストループ
: 1つのテーブルから結果を取得し、最初のテーブルの各行に対してもう1つのテーブルをクエリすることで2つのテーブルを結合します。
マージ結合
: (ソート)マージ結合は、2つのソートされたリストをジッパーのように組み合わせます。結合の両側は事前にソートされている必要があります。
ハッシュ結合
: ハッシュ結合は、結合の片側から候補レコードをハッシュテーブルにロードし(プランで Hash とマークされています)、結合のもう一方の側からの各レコードに対してハッシュテーブルをプローブします。
GroupAggregate
: GROUP BY 句に従ってプリソートされたセットを集計します。この操作は大量のデータをバッファリングしません。HashAggregate
: 一時的なハッシュテーブルを使用してレコードをグループ化します。HashAggregate 操作はプリソートされたデータセットを必要とせず、大量のメモリを使用して中間結果を実体化します(パイプライン化されていません)。出力は意味のある順序にはなりません。Unique
WindowAgg
: ウィンドウ関数の使用を示します。Sort
work_mem
に収まる場合、クイックソートが使用できます。Limit
DISTINCT
は PostgreSQL の古い部分から来ており、重複レコードを識別するための効率の低い方法を使用しています。DISTINCT
の使用を避けてください。a.id = b.person_id
)を使用するようにします。これにより、ハッシュ結合ではなくネストループ結合などのより効率的な結合技術が使用できるようになります。EXISTS
を使用してください。これは少なくとも1つの一致が見つかった時点で処理を停止します。Deeper into Postgres 9.5 - New Group By Options for Aggregation ↗ を参照してください。
UNION は重複レコードを排除します。これにはテーブルのソートが必要です。
リクエストに関連するコストを考慮して、UNION の使用を避けることを強くお勧めします。これが必要な場合は
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14
SELECT relname, -- テーブル名 seq_scan - idx_scan AS too_much_seq, -- シーケンシャルスキャン数からインデックススキャン数を差し引いた値(インデックスが足りていない可能性がある場合、この値は大きくなります) CASE WHEN seq_scan - coalesce(idx_scan, 0) > 0 THEN 'missing index?' -- シーケンシャルスキャンの数がインデックススキャンの数よりも多い場合、インデックスが不足している可能性があります ELSE 'OK' -- インデックスは適切に設定されています END, pg_relation_size(relname::regclass) AS rel_size, -- テーブルのサイズ(バイト単位) seq_scan, -- シーケンシャルスキャンの回数 idx_scan -- インデックススキャンの回数 FROM pg_stat_all_tables -- すべてのテーブルの統計情報を取得 WHERE schemaname = 'public' -- スキーマ名が'public'のテーブルのみを対象 AND pg_relation_size(relname::regclass) > 80000 -- テーブルのサイズが80000バイトより大きいもののみを対象 ORDER BY too_much_seq DESC; -- シーケンシャルスキャンとインデックススキャンの差(too_much_seq)が大きい順に並べ替える
Copied!1 2 3 4 5 6 7
SELECT indexrelid::regclass as index, -- indexrelidをregclassとしてindexという名前で取得 relid::regclass as table, -- relidをregclassとしてtableという名前で取得 'DROP INDEX ' || indexrelid::regclass || ';' as drop_statement -- 'DROP INDEX '文字列とindexrelid::regclassを結合してdrop_statementという名前で取得 FROM pg_stat_user_indexes -- pg_stat_user_indexesテーブルからデータを取得 JOIN pg_index USING (indexrelid) -- pg_indexテーブルとindexrelidを用いて結合 WHERE idx_scan = 0AND indisunique is false; -- idx_scanが0でかつindisuniqueがfalseのデータを選択
インデックスのみのスキャン ↗ の適切な使用法と利点について詳しく読む。