web-dev-qa-db-ja.com

タイムスタンプの範囲(2列)でのクエリの最適化

Ubuntu 12.04でPostgreSQL 9.1を使用しています。

時間範囲内のレコードを選択する必要があります。私のテーブルtime_limitsには2つのtimestampフィールドと1つのintegerプロパティがあります。実際のテーブルには、このクエリに関係しない追加の列があります。

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

このテーブルには、約2Mのレコードが含まれています。

次のようなクエリには膨大な時間がかかりました。

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

だから私は別のインデックスを追加してみました-PKの逆:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

パフォーマンスが向上したという印象を受けました。テーブルの中央にあるレコードにアクセスする時間は、より合理的であるようです。40〜90秒程度です。

ただし、時間範囲の中央の値の場合は、まだ数十秒です。テーブルの最後をターゲットにすると(年代順で)、さらに2倍になります。

このクエリプランを取得するために、初めてexplain analyzeを試しました。

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

depesz.comの結果を参照してください。

検索を最適化するにはどうすればよいですか? id_phi0に設定されると、2つのタイムスタンプ列のスキャンにすべての時間が費やされていることがわかります。そして、私はタイムスタンプの大きなスキャン(60K行!)を理解していません。彼らは主キーでインデックス付けされていませんか?idx_inversed追加しましたか?

タイムスタンプタイプから他のタイプに変更する必要がありますか?

GistインデックスとGINインデックスについて少し読んだ。カスタムタイプの特定の条件でより効率的になる可能性があることを収集します。それは私のユースケースにとって実行可能なオプションですか?

105

Postgres 9.1以降の場合:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

ほとんどの場合、インデックスのソート順はほとんど関係ありません。 Postgresは実質的に高速で逆方向にスキャンできます。ただし、複数の列に対する範囲クエリの場合、hugeの違いが生じる可能性があります。密接に関連:

あなたのクエリを考えてみましょう:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

インデックスの最初の列id_phiのソート順は関係ありません。equality=)がチェックされるため、最初に来るはずです。 正解です。この関連回答の詳細:

Postgresはすぐにid_phi = 0にジャンプし、一致するインデックスの次の2つの列を検討できます。これらは、逆ソート順の範囲条件<=>=)で照会されます。私のインデックスでは、条件を満たす行が最初に来ます。 Bツリーインデックスで可能な限り最速の方法1

  • start_date_time <= somethingが必要です:インデックスは最初に最も早いタイムスタンプを持っています。
    • 該当する場合は、列3も確認してください。
      最初の行が適格でなくなるまで再帰します(超高速)。
  • end_date_time >= somethingが必要です:インデックスには最初に最新のタイムスタンプがあります。
    • 条件を満たしている場合は、最初の行が取得しないまで(超高速で)行をフェッチし続けます。
      列2の次の値に進みます..

Postgresは前方または後方にスキャンできます。あなたがインデックスを持っていた方法では、最初の2つの列に一致するall行を読み取り、次に3番目の列にfilterを読み取る必要があります。マニュアルの章 IndexesおよびORDER BY を必ずお読みください。それはあなたの質問にかなりよく合います。

最初の2つの列で一致する行の数は?
テーブルの時間範囲の開始に近いstart_date_timeがあるのはごくわずかです。ただし、ほぼすべて行の最後にid_phi = 0が付いています。そのため、パフォーマンスは開始時間が遅くなると低下します。

プランナー見積もり

プランナーは、サンプルクエリのrows=62682を見積もります。それらのうち、適格なものはありません(rows=0)。テーブルの統計ターゲットを増やすと、より適切な見積もりが得られる可能性があります。 2.000.000行の場合...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

...支払うかもしれません。またはそれ以上。この関連回答の詳細:

id_phi(少数の個別値のみ、均等に分散)の場合は必要ありませんが、タイムスタンプ(個別値の多く、不均一に分布)の場合は必要だと思います。
インデックスの改善についても、それほど重要ではないと思います。

CLUSTER/pg_repack

さらに高速にしたい場合は、テーブル内の行の物理的な順序を合理化できます。テーブルを短期間だけ(たとえば、営業時間外に)ロックして、テーブルを書き換え、インデックスに従って行を並べ替える余裕がある場合:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

同時アクセスの場合、 pg_repack を検討してください。これにより、排他ロックなしで同じことができます。

どちらの方法でも、テーブルから読み取る必要のあるブロックが少なくなり、すべてが事前にソートされるという効果があります。これは、物理的なソート順序を断片化するテーブルへの書き込みにより、時間とともに悪化する1回限りの影響です。

Postgres 9.2+の要点インデックス

1 pg 9.2+では、別の、おそらくより高速なオプションがあります: 範囲列の要点インデックス。

  • timestamptimestamp with time zoneには組み込みの範囲タイプがあります: tsrangetstzrange 。 btreeインデックスは通常、id_phiのような追加のinteger列の方が高速です。メンテナンスも小さくて安価です。ただし、結合されたインデックスを使用すると、クエリはおそらく全体的に高速になります。

  • テーブル定義を変更するか、 expression index を使用します。

  • 手元にある複数列のGistインデックスについては、追加のモジュール btree_Gist がインストールされ(データベースごとに1回)、integerを含む演算子クラスを提供する必要もあります。

三連勝! A複数列の機能要旨インデックス

CREATE EXTENSION IF NOT EXISTS btree_Gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING Gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

クエリで "contains range"演算子@> を使用してください:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Postgres 9.3以降のSP-Gistインデックス

SP-Gistこの種類のクエリでは、インデックスがさらに高速になる可能性があります-exceptそれ、 マニュアルの引用

現在、Bツリー、Gist、GIN、およびBRINインデックスタイプのみが複数列インデックスをサポートしています。

Postgres 12でも同様です。
(tsrange(...))spgistインデックスを(id_phi)の2番目のbtreeインデックスと組み合わせる必要があります。オーバーヘッドが追加されたため、これが競合できるかどうかはわかりません。
tsrange列のみのベンチマークを使用した関連する回答:

177

アーウィンの答えはすでに包括的ですが、

タイムスタンプの範囲タイプは、Jeff DavisのTemporal拡張機能を備えたPostgreSQL 9.1で使用できます: https://github.com/jeff-davis/PostgreSQL-Temporal

注:機能は限られています(タイムスタンプを使用し、 '[)'スタイルのオーバーラップはafaikのみが可能です)。また、PostgreSQL 9.2にアップグレードする理由は他にもたくさんあります。

5
nathan-m

複数列のインデックスを別の順序で作成してみてください。

primary key(id_phi, start_date_time,end_date_time);

同様の question も投稿しましたが、これも複数列インデックスのインデックスの順序に関連しています。重要なのは、最初に最も制限的な条件を使用して検索スペースを削減することです。

編集:私の間違い。これで、すでにこのインデックスが定義されていることがわかります。

3
jap1968

なんとか急増(1秒から70msへ)

多くの測定値と多くのレベル(l列)(30秒、1分、1時間など)の集計を含むテーブルがあります。2つの範囲バインド列があります:開始の$sおよび終了の$e

2つのマルチカラムインデックスを作成しました。1つは開始用、もう1つは終了用です。

選択クエリを調整しました。開始境界が指定された範囲内にある範囲を選択します。さらに、それらの終了境界が指定された範囲内にある範囲を選択します。

Explainは、インデックスを効率的に使用した2つの行ストリームを示しています。

インデックス:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

クエリを選択:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

説明:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

秘訣は、計画ノードに必要な行のみが含まれていることです。以前はall points from some point in time to the very endを選択したため、計画ノードに数千の行があり、次のノードは不要な行を削除しました。

1
borovsky