範囲タイプの正確な等価性に起因する不適切なクエリプランの処理方法


28

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

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

ただし、t_rangeどこかに統計が保存されている必要があります。完全に同等ではなくt_rangeで「within」を使用する非常に類似した更新の実行には約4分かかり、実質的に異なるクエリプランを使用します(こちらを参照)。一時テーブルのすべての行と履歴テーブルのかなりの部分が使用されるため、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上記のdistinctのカウントは完了しているため、カーディナリティは〜3Mです(このうち〜1Mは、いずれかの更新クエリの影響を受けます)。

クエリ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の答えは、原因が更新に使用される一時テーブルの順序である可能性があることを提案しました。track_io_timing(Amazon RDSを使用して)アクセスできないため、仮説を直接テストできませんでした。

  1. 全体的な結果は非常に高速でした(数倍)。これは、アーウィンの答えと一致するインデックスの削除が原因であると推測しています

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


1
等号条件(a = b)を2つの「含む」条件に変換した場合はどうなります(a @> b AND b @> a)か?計画は変わりますか?
ypercubeᵀᴹ

@ypercube:計画は大幅に変更されますが、まだ最適ではありません-私の編集#2を参照してください。
アベボパレボップ14

1
別のアイデアは(lower(t_range),upper(t_range))、平等をチェックするため、通常のbtreeインデックスを追加することです。
ypercubeᵀᴹ

回答:


9

実行計画における時間の最大の違いは、最上位ノードである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は、範囲での等価結合の選択性を見積もるのに悪い仕事をしているのは正しいです。ただし、これは基本的な問題に正接するだけです。更新の選択部分に対するより効率的なクエリは、誤って行をより適切な順序で更新プロパティに送り込む可能性がありますが、そうであればほとんどが運にかかっています。


残念ながらtrack_io_timing、を変更することはできません(1年半が経過しているため!)、元のデータにアクセスできなくなりました。ただし、同じスキーマと同様のサイズ(数百万行)でテーブルを作成し、2つの異なる更新を実行して理論をテストしました。1つは元のテーブルと同じように、もう1つはソートされたものです準ランダムに。残念ながら、2つの更新にはほぼ同じ時間がかかり、更新テーブルの順序がこのクエリに影響を与えないことを意味します。
アベボパレボップ

7

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

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

単純なクエリ1をそのままにしてください。シンプルで根本的な解決策、の前にインデックス削除することUPDATEです。他の目的で必要な場合は、の後に再作成してくださいUPDATE。これは、大きなの間にインデックスを維持するよりも高速ですUPDATE

UPDATEすべての行の3分の1 については、他のすべてのインデックスも同様に削除し、UPDATE。唯一の欠点は、追加の特権とテーブルの排他ロックが必要なことです(を使用する場合は短時間だけCREATE INDEX CONCURRENTLY)。

@ypercubeの GiSTインデックスの代わりにbtreeを使用するという考え方は、原則としては良いようです。しかしではない(非インデックスはそもそも任意良好ではない)、そして全ての行の3分の1 ではないにだけ(lower(t_range),upper(t_range))ので、tstzrange離散範囲の種類ではないです。

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

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

組み込みの範囲タイプ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_traveledと同じ値を既に持つことができる場合tt.shape_dist_traveled(およびUPDATE同様のトリガーの副作用に依存しない場合)、空の更新を除外することでクエリを高速化できます。

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

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

VACUUM FULLいくつかの無効なタプル(またはによって予約されたスペースFILLFACTOR)はUPDATEパフォーマンスに有益であるため、あなたにとっては有害です。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.