ユースケース開発SlateトラブルシューティングPostgres のクエリの最適化

注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。

Postgres のクエリを最適化する

このページでは、Postgres で最適なパフォーマンスを得るためのクエリのチューニングに焦点を当てます。通常は、オントロジーの機能を基盤として Slate アプリケーションを構築し、オブジェクトセットや Actions などの機能を使用してデータを読み書きすることを推奨します。

応答性の高いアプリケーションを設計するには、慎重な計画と検討が必要です。データモデル、クエリ構造、依存関係グラフは、それぞれアプリケーションのパフォーマンスと使いやすさにおいて重要な役割を果たします。このガイドでは、Slate で記述できる PostgreSQL クエリのパフォーマンスチューニングに焦点を当て、パフォーマンスに関する経験則、チューニングの余地を特定する方法、パフォーマンスを改善するための推奨事項を紹介します。

クエリのパフォーマンスチューニングは、反復的なプロセスです。「正しい」結果を得るために、同じ要求をさまざまな方法で記述できます。

パフォーマンスに関する経験則

PostgreSQL クエリの目標実行時間は500 ms以下です。対象のデータセットが1,000万件未満のレコードを含む場合、ほとんどのユースケースでこの目標を達成できるはずですが、データモデルの設計時には慎重な計画と検討が必要です。

クエリのパフォーマンスを評価するとき、特に新しいデータセットを使用する場合は、統計を収集して分析する前に、必ずクエリを複数回実行してください。これにより、データセットが適切にキャッシュされ、パフォーマンスが想定される使用状況により近いものになります。PostgreSQL のキャッシュについて詳しく知りたい場合は、次のリンクを参照してください。

EXPLAIN の解説

EXPLAIN は、クエリ実行計画を返すために Postgres が提供する、特に便利なコマンドです。Postgres が受け取るすべてのリクエストに対してクエリ計画が作成されます。この計画では、クエリ構造とデータの特性を使用して、リクエストを処理する最も速い方法を決定します。このセクションでは、まず、このガイド全体で参照する EXPLAIN コマンドを説明します。

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**)

重要な理由:WIDTH を使用すると、非常に大きなプロパティを持つ行や、多数の列を含む行を出力するノードを特定できます。

EXPLAIN ANALYZE

ノード

上記を参照してください。

実際の時間

_コスト_と同様に、最初の数値は起動に必要な実際の時間で、単位はミリ秒 (ms) です。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 は、特定の処理に期待される時間よりも長くかかっている_理由_を理解するための手がかりになります。

ループ回数

ノードの合計実行回数を示します。表示される実際の時間と行数の値は、1回の実行あたりの平均値です。LOOPS の値に実際の時間を掛けると、ノードで費やされた合計時間を算出できます。

(actual time=10.313..12.530 rows=150 **loops=10**)

操作(プランノード)を理解する

スキャン

  • sequential scans(シーケンシャルスキャン):Seq Scan 操作は、ディスクに保存されたリレーション(テーブル)全体をスキャンします(TABLE ACCESS FULL と同様です)。リレーションのスキーマ、サイズ、制約、インデックスの有無にかかわらず、リレーションに対してシーケンシャルスキャンを実行することは常に可能です。
    • シーケンシャルスキャンには次の特性があります。
      • 起動が高速です(シーケンシャル I/O はランダムアクセスよりもはるかに高速です)。
      • 各ブロックは1回だけ読み取られます。
      • 順序付けされていない出力を生成します。
  • index scans:Index Scan は B ツリーを走査し、リーフノードをたどって一致するすべてのエントリーを見つけ、対応するテーブルデータを取得します。INDEX RANGE SCAN に続いて TABLE ACCESS BY INDEX ROWID 操作を実行するのと同様です。
    • Index Scan には次の特性があります。
      • ランダムアクセスはシーケンシャル I/O よりもはるかに低速です。
      • インデックスにアクセスするために追加の I/O が必要です。
      • 同じブロックを複数回読み取る可能性があります。
      • 順序付けされた出力を生成します。
  • bitmap index/heap scans:通常の Index Scan は、インデックスからタプルポインターを1つずつ取得し、その都度テーブル内の該当タプルにアクセスします。ビットマップスキャンは、インデックスからすべてのタプルポインターを一度に取得し、メモリー内の「ビットマップ」データ構造を使って並べ替えてから、タプルの物理的な場所の順序でテーブルのタプルにアクセスします。
    • ビットマップインデックス/ヒープスキャンには次の特性があります。
      • インデックスの選択性を活用したシーケンシャル I/O です。
      • すべてのインデックスタプルを読み取って並べ替えるため、起動が低速です。
      • IN 演算子や =ANY(array) 演算子、および選択性の低いインデックススキャンでよく選択されます。
      • 複数のインデックスを組み合わせられます。
      • 順序付けされていない出力を生成します。
  • index only:Index Only Scan は B ツリーを走査し、リーフノードをたどって一致するすべてのエントリーを見つけます。インデックスにはクエリを満たすためのすべての列が含まれているため、テーブルへのアクセスは不要です(ただし、MVCC の可視性情報は例外です)。

結合

結合操作は通常、一度に2個のテーブルのみを処理します。クエリで3個以上のテーブルを結合する場合、結合は順次実行されます。まず2個のテーブルを結合し、次にその中間結果と次のテーブルを結合する、という流れです。そのため、結合のコンテキストでは、「テーブル」という用語が「中間結果」を意味することもあります。

  • nested loop:一方のテーブルから結果を取得し、そのテーブルの各行についてもう一方のテーブルにクエリを実行することで、2個のテーブルを結合します。
    • 一般的に、最も性能の低い結合方式です。
    • 最初のレコードを高速に生成します。
    • 2番目の子ノードが低速な場合、性能が低下する可能性があります。
    • CROSS JOIN を実行できる唯一の結合です。
    • 不等価の結合条件に対応できる唯一の結合です。
  • merge join:(ソート)マージ結合は、並べ替え済みの2つのリストをファスナーのように組み合わせます。結合の両側を事前に並べ替えておく必要があります。
    • 等価の結合条件でのみ使用できます。
    • 一般的に、大規模なデータセットで最も高い性能を発揮します。
    • 順序付けされた入力が必要であり、低速な並べ替えやインデックススキャンが必要になる場合があります。
    • すべてのインデックスタプルを読み取って並べ替えるため、起動が低速です。
  • hash joins:ハッシュ結合は、結合の片側の候補レコードをハッシュテーブル(プラン内で Hash と指定されます)に読み込み、もう一方の側の各レコードについてそのハッシュテーブルを検索します。
    • 等価の結合条件でのみ使用できます。
    • 一般的に、大きなテーブルと小さなテーブルを結合する場合に最も高い性能を発揮します。
    • ハッシュ化可能なデータ型にのみ対応します。
    • 小さい方のテーブルをハッシュ化するため、起動が低速です。
    • テーブルの統計情報が古く不正確な場合、性能が低下します。

集計

  • GroupAggregate:事前に並べ替えられたセットを GROUP BY 句に従って集計します。この操作では大量のデータをバッファーに保持しません。
  • HashAggregate:一時的なハッシュテーブルを使ってレコードをグループ化します。HashAggregate 操作では、事前に並べ替えられたデータセットは不要です。その代わり、大量のメモリーを使用して中間結果を実体化します(パイプライン処理は行いません)。出力には意味のある順序付けは行われません。
  • Unique
  • WindowAgg:ウィンドウ関数が使用されていることを示します。

その他

  • Sort
    • ORDER BY、DISTINCT、GROUP BY、UNION、およびマージ結合で発生します。
    • 起動にかなりの時間がかかります。
    • 並べ替えが work_mem 内に収まる場合、クイックソートを使用できます。
    • 並べ替えがメモリー内に収まらない場合、ディスクに書き出されて一時ファイルが使用されるため、コストが非常に高くなる可能性があります。
  • Limit
    • LIMIT と OFFSET の両方を処理します。
    • WHERE 句がない場合、min() と max() に使用できます。
    • OFFSET によってスキップされるレコードも生成/実体化されますが、結果が返される前に破棄されます。
    • 子スキャンのコストは依然として全体のコストになります。
    • 並べ替えと件数制限を組み合わせると、最適化された並べ替え方式を使用できます。

クエリチューニングのベストプラクティス

SELECT

  • 表示に必要な属性のみを含めます。これにより、リクエストを処理するために取得するページ数を制限できます。

DISTINCT

  • DISTINCT は PostgreSQL のコードベースの古い部分に由来し、一意のレコードを識別するために効率の低い方法を使用しています。
  • 可能であれば、GROUP BY またはサブクエリを使用して DISTINCT の使用を避けます。

JOIN

  • テーブルを結合する際は、ON 句で単純な等価条件(a.id = b.person_id など)を使用するようにします。これにより、より効率的な結合方式(Nested Loop Join ではなく Hash Join など)を使用できます。
  • 可能であれば、サブクエリを JOIN 文に変換します。通常、これによりオプティマイザーが意図を理解し、より適切なプランを選択できる可能性があります。
  • JOIN を適切に使用します。結果が重複するという理由だけで GROUP BY や DISTINCT を使用していませんか?通常、これは JOIN の使用方法が不適切であることを示しており、コストの増加につながる可能性があります。
  • 実行プランで Hash Join を使用している場合、テーブルサイズの推定が誤っていると非常に低速になる可能性があります。そのため、バキューム戦略を見直して、テーブルの統計情報が正確であることを確認してください。
  • 可能であれば、相関サブクエリを避けます。クエリのコストが大幅に増加する可能性があります。
  • 条件に基づいて行の存在を確認する場合は、EXISTS を使用します。EXISTS は「短絡評価」を行うためです(一致する行が1つ以上見つかった時点で処理を停止します)。

WHERE 句

  • 可能であれば、LIKE を避けます。
  • IN() 文に大きなリストを渡すことは避けます。代わりに、JOIN 条件の使用や、除外に基づくロジックへの変更を検討します。
  • WHERE 句での関数呼び出しを避けます。

GROUP BY と GROUPING SET

PostgreSQL の GROUPING SETS ドキュメント ↗を参照してください。

UNION と UNION ALL の比較

UNION は重複レコードを除去します。このため、対象のテーブルを並べ替える必要があります。

リクエストに伴うコストを考慮し、UNION の使用を避けることを強く推奨します。UNION が必要になるケースはごく少ないはずですが、必要な場合は、Slate からクエリを実行する前に Transforms レイヤーで処理するべきです。

UNION ALL は重複を除去せず、一方のテーブルの行をもう一方のテーブルに効率的に追加します。

よくある間違い:UNION と UNION ALL の比較 ↗を参照してください。

インデックス

次の条件では、インデックスの追加を検討します。

  • 小さなテーブルである場合や、クエリが行の5%を超える数を取得する場合を除き、シーケンシャルスキャン(seq scan)をなくします。
  • 複数列のインデックスを使用する場合は、含める列を定義する順序に注意します。
  • よく使用する列に対して、選択性の高いインデックスを使用します。

不足しているインデックスを特定する

Copied!
1 2 3 4 5 6 7 8 9 10 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_tablesWHERE schemaname = 'public'AND pg_relation_size(relname::regclass) > 80000 ORDER BY too_much_seq DESC;

未使用のインデックスを特定する

Copied!
1 2 3 4 5 6 7 SELECT indexrelid::regclass as index, relid::regclass as table, 'DROP INDEX ' || indexrelid::regclass || ';' as drop_statement FROM pg_stat_user_indexes JOIN pg_index USING (indexrelid) WHERE idx_scan = 0AND indisunique is false;

インデックスオンリースキャン(カバリングインデックス)

インデックスオンリースキャン ↗の適切な使用方法と利点について、詳しくご覧ください。