web-dev-qa-db-ja.com

範囲型の完全な等価性が原因で発生した不良クエリプランを処理するには

tstzrange変数の正確な等価が必要な更新を実行しています。約100万行が変更され、クエリには約13分かかります。 EXPLAIN ANALYZEの結果は here で確認できますが、実際の結果はクエリプランナーが推定した結果とは大きく異なります。問題は、t_rangeのインデックススキャンで単一の行が返されることを期待していることです。

これは、範囲タイプの統計が他のタイプの統計とは異なる方法で格納されるという事実に関連しているようです。列のpg_statsビューを見ると、n_distinctは-1であり、他のフィールド(most_common_valsmost_common_freqsなど)は空です。

ただし、t_rangeのどこかに統計が保存されている必要があります。まったく同じではなくt_rangeで「within」を使用する非常に類似した更新は、実行に約4分かかり、大幅に異なるクエリプランを使用します( here を参照)。一時テーブルのすべての行と履歴テーブルのかなりの部分が使用されるため、2番目のクエリプランは私にとって意味があります。さらに重要なことに、クエリプランナーはt_rangeのフィルターに対してほぼ正しい行数を予測します。

t_rangeの分布は少し変わっています。このテーブルを使用して別のテーブルの履歴状態を格納しています。他のテーブルへの変更は大きなダンプで一度に発生するため、t_rangeの明確な値は多くありません。 t_rangeの一意の値のそれぞれに対応するカウントは次のとおりです。

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

上記の異なるt_rangeのカウントは完了しているため、カーディナリティは約300万です(そのうちの約100万は、いずれかの更新クエリによって影響を受けます)。

クエリ1のパフォーマンスがクエリ2よりもはるかに低いのはなぜですか?私の場合、クエリ2は良い代替品ですが、正確な範囲の等価性が本当に必要な場合、どうすればPostgresがよりスマートなクエリプランを使用できるようになりますか?

テーブル定義インデックス付き(無関係な列を削除):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" Gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

クエリ1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

クエリ2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1は999753行を更新し、Q2は999753 + 36791 = 1036544を更新します(つまり、一時テーブルでは、時間範囲条件に一致するすべての行が更新されます)。

私は @ ypercubeのコメント に応じてこのクエリを試しました:

クエリ3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

クエリプランと結果( こちら を参照)は、前の2つのケースの中間(〜6分)でした。

2016/02/05編集

1.5年後にはデータにアクセスできなくなったので、同じ構造(インデックスなし)でカーディナリティが類似したテストテーブルを作成しました。 jjanesの回答 更新に使用される一時テーブルの順序が原因である可能性があると提案されました。 (Amazon RDSを使用して)track_io_timingにアクセスできないため、仮説を直接テストできませんでした。

  1. 全体的な結果ははるかに高速でした(数倍)。これは アーウィンの答え と一致するインデックスの削除によるものだと思います。

  2. このテストケースでは、クエリ1と2は両方ともマージ結合を使用したため、基本的に同じ時間かかりました。つまり、Postgresがハッシュ結合を選択する原因となったものをトリガーできなかったため、そもそもPostgresがパフォーマンスの悪いハッシュ結合を選択した理由が明確ではありません。

28
abeboparebop

実行計画における時間の最大の違いは、最上位ノードであるUPDATE自体です。これは、更新中にほとんどの時間がIOになることを示唆しています。_track_io_timing_をオンにしてEXPLAIN (ANALYZE, BUFFERS)でクエリを実行することでこれを確認できます

異なるプランは、異なる順序で更新される行を提示しています。 1つは_trip_id_の順序であり、もう1つは一時テーブルに物理的に存在する順序です。

更新されるテーブルは、物理的な順序がtrip_id列と関連付けられているようであり、この順序で行を更新すると、効率的なIO先読み/順次読み取りのパターンが生成されます。一時テーブルは多くのランダム読み取りにつながるようです。

一時テーブルを作成したステートメントに_order by trip_id_を追加できれば、問題が解決する可能性があります。

PostgreSQLは、UPDATE操作を計画するときにIOの順序付けを考慮に入れません)(SELECT操作とは異なり、それらを考慮に入れます)。PostgreSQLがより賢い場合は、 1つのプランがより効率的な順序を生成するか、更新とその子ノードの間に明示的なソートノードを挿入して、更新がctidの順序で行をフィードするようにします。

PostgreSQLが範囲の等値結合の選択性を推定するのに不十分な仕事をしているのは正しいです。ただし、これはあなたの根本的な問題に正接するだけです。更新の選択部分に対するより効率的なクエリでは、誤って行をupdate-properに適切な順序でフィードする可能性がありますが、そうである場合、ほとんどが運に任されています。

9
jjanes

等価述語の選択性がtstzrange列のGistインデックスによって根本的に過大評価される理由は正確にはわかりません。それ自体は興味深いままですが、特定のケースとは無関係のようです。

UPDATEは既存のすべての3M行の3分の1(!)を変更するため、インデックスはまったく役に立ちません。逆に、テーブルに加えてインデックスを段階的に更新すると、UPDATEにかなりのコストがかかります。

単純なQuery 1にしてください。単純で根本的なsolutionは、UPDATEの前にdrop the indexすることです。他の目的で必要な場合は、UPDATEの後に再作成してください。これは、UPDATEが大きいときにインデックスを維持するよりも高速です。

すべての行の3分の1のUPDATEの場合、他のすべてのインデックスも削除して、UPDATEの後に再作成すると、おそらく費用がかかります。唯一の欠点:追加の特権とテーブルの排他ロックが必要です( CREATE INDEX CONCURRENTLY を使用する場合は、ほんの少しの間)。

@ ypercubeのアイデア Gistインデックスの代わりにbtreeを使用することは、原則として良いようです。ただし、notは、すべての行の3分の1(最初はインデックスが適切でない場合)であり、not(lower(t_range),upper(t_range))tstzrangeは離散範囲型ではないため。

ほとんどの離散範囲型には標準形があり、これにより「等価」の概念が単純になります。標準形の値の下限と上限で定義されます。 ドキュメント:

離散範囲タイプには、要素タイプの望ましいステップサイズを認識するcanonicalization関数が必要です。正規化関数は、範囲型の同等の値を変換して、同一の表現、特に一貫性のある範囲または排他的な範囲を持つようにします。正規化関数が指定されていない場合、実際には同じ値のセットを表す可能性があっても、異なるフォーマットの範囲は常に等しくないものとして扱われます。

組み込みの範囲型int4rangeint8range、およびdaterangeはすべて、下限を含み、上限を除外する正規形式を使用します。つまり、[)です。ただし、ユーザー定義の範囲タイプは他の規則を使用できます。

これは、tstzrangeには当てはまりません。この場合、上限と下限の包含性が等しいかどうかを考慮する必要があります。可能なbtreeインデックスは次のようにする必要があります。

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

また、クエリはWHERE句で同じ式を使用する必要があります。

textにキャストされた値全体を単にインデックス付けしたくなるかもしれません: (cast(t_range AS text)) -しかし、IMMUTABLE値のテキスト表現は現在のtimestamptz設定に依存するため、この式はtimezoneではありません。正規形式を生成するIMMUTABLEラッパー関数に追加のステップを追加し、それに関数インデックスを作成する必要があります...

追加の手段/代替案

更新された行のいくつかに対してshape_dist_traveledtt.shape_dist_traveledと同じ値を既に持っている可能性がある場合(およびUPDATEのようなトリガーの副作用に依存していない場合...)、空の更新を除外してクエリを高速化します。

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

もちろん、パフォーマンスの最適化に関するすべての一般的なアドバイスが適用されます。 Postgres Wikiは良い出発点です。

一部のデッドタプル(またはFILLFACTORによって予約されているスペース)はUPDATEのパフォーマンスに有益であるため、VACUUM FULLは有害です。

更新された行の数が多く、余裕がある場合(同時アクセスや他の依存関係がない場合)、所定の場所で更新するのではなく、完全に新しいテーブルを書き込む方がより高速になる可能性があります。この関連する回答の手順:

7