web-dev-qa-db-ja.com

SELECTクエリで使用されていないインデックス

Postgres 9.4.1に次の形式で約325万行のテーブルがあります

CREATE TABLE stats
(
    id serial NOT NULL,
    type character varying(255) NOT NULL,
    "references" jsonb NOT NULL,
    path jsonb,
    data jsonb,
    "createdAt" timestamp with time zone NOT NULL,
    CONSTRAINT stats_pkey PRIMARY KEY (id)
)
WITH (
    OIDS=FALSE
);

typeは、50文字以下の単純な文字列です。

references列は、キー値のリストを持つオブジェクトです。基本的に、単純なキー値の任意のリストであり、1レベルだけの深さで、値は常に文字列です。かもしれない

{
    "fruit": "Plum"
    "car": "toyota"
}

それとも

{
    "project": "2532"
}

createdAtタイムスタンプは、常にデータベースから生成されるわけではありません(ただし、値が指定されていない場合、デフォルトで生成されます)

現在、テストデータのみを含むテーブルを使用しています。このデータでは、すべての行にprojectキーが参照として含まれています。したがって、プロジェクトキーを持つ行は325万行あります。 project参照には、正確に400,000の異なる値があります。 typeフィールドには5つの異なる値しかありませんが、これは本番環境では数百以下になるでしょう。

したがって、次のクエリをすばやく実行できるようにテーブルにインデックスを付けようとしています。

SELECT
  EXTRACT(Epoch FROM (MAX("createdAt") - MIN("createdAt"))) 
FROM
  stats
WHERE
  stats."references"::jsonb ? 'project' AND
  (
    stats."type" = 'event1' OR
    (
      stats."type" = 'event2' AND
      stats."createdAt" > '2015-11-02T00:00:00+08:00' AND
      stats."createdAt" < '2015-12-03T23:59:59+08:00'
    )
  )
GROUP BY stats."references"::jsonb->> 'project'

クエリは、同じ参照を持つ2つの統計行に基づいて、2つのイベント間の時間距離を返します。この場合はprojectです。 typeと選択されたreferenceの値ごとに1つの行しかありませんが、返される結果が0である行がない場合もあります(これは、後で別の部分で平均化されます)より大きなクエリ)。

createdAttype列とreferences列にインデックスを作成しましたが、クエリ実行プランが代わりにフルスキャンを実行しているようです。

インデックス

CREATE INDEX "stats_createdAt_references_type_idx"
    ON stats
    USING btree
    ("createdAt", "references", type COLLATE pg_catalog."default");

実行計画:

 HashAggregate  (cost=111188.31..111188.33 rows=1 width=38) 
                (actual time=714.499..714.499 rows=0 loops=1)
   Group Key: ("references" ->> 'project'::text)
      ->  Seq Scan on stats  (cost=0.00..111188.30 rows=1 width=38) 
                             (actual time=714.498..714.498 rows=0 loops=1)
          Filter: (
              (("references" ? 'project'::text) 
               AND ((type)::text = 'event1'::text)) OR 
              (((type)::text = 'event2'::text) 
               AND ("createdAt" > '2015-11-02 05:00:00+13'::timestamp with time zone) 
               AND ("createdAt" < '2015-12-04 04:59:59+13'::timestamp with time zone)))

Rows Removed by Filter: 3258680
Planning time: 0.163 ms
Execution time: 714.534 ms

インデックス作成とクエリの実行計画にまったくこだわっていないので、誰かが私を正しい方向に向けることができればすばらしいでしょう。

編集

Erwinによって指摘されたように、正しいインデックスがあったとしても、クエリから返されたテーブルの部分が非常に大きいため、テーブルスキャンが引き続き発生するように見えます。これは、このデータセットの場合、これが私が取得できる最速のクエリ時間であることを意味しますか?プロジェクト参照なしで60Mの無関係な行を追加した場合、それはインデックスを使用する可能性があります(正しいインデックスがある場合)が、データを追加してクエリを高速化できる方法がわかりません。多分私は何かが欠けています。

5
James Hay

あなたの現在の説明によるとインデックスはあなたの現在のクエリで(もしあったとしても)多く助けにはなりません。

したがって、プロジェクトキーを持つ行は325万行あります。

これはtotal行数でもあるため、この述語はtrue for(ほぼ)every row ...であり、まったく選択的ではありません。ただし、jsonb列_"references"_に役立つインデックスはありません。 _("createdAt", "references", type)_のbtreeインデックスに含めても意味がありません。

次のように、_"reference"_に一般的に役立つGINインデックスがあったとしても、

_CREATE INDEX stats_references_gix ON stats USING gin ("references");
_

... Postgresにはまだ jsonb列内の個々のキーに関する有用な統計はありません。


typeには5つの異なる値しかありません

クエリは、1つのタイプのすべてと別のタイプの不明な部分を選択します。これはすべての行の推定20-40%です。シーケンシャルスキャンが最も確実な方法です。 全行の約5%以下の場合、インデックスは意味を持ち始めます。

テストするには、セッションでのデバッグ用を設定して、可能なインデックスを強制できます。

_SET enable_seqscan = off;
_

リセット:

_RESET enable_seqscan;
_

遅いクエリが表示されます...


プロジェクト値でグループ化します。

_GROUP BY "references"->> 'project'_

そして:

プロジェクト参照には正確に400,000の異なる値があります。

これは、プロジェクトあたり平均で8行です。 LATERALサブクエリでプロジェクトごとに最小値と最大値のみを選択した場合、値の頻度に応じて、すべての行の推定3〜20%を取得する必要があります...

このインデックスを試してください。今あるものよりも意味があります。

_CREATE INDEX stats_special_idx ON stats (type, ("references" ->> 'project'), "createdAt")
WHERE "references" ? 'project';
_

Postgresは引き続きシーケンシャルスキャンにフォールバックする可能性があります...

正規化されたスキーマ/より選択的な基準/最小と最大のみを選択するよりスマートなクエリ_"createdAt"_ ...

クエリ

私はあなたのクエリを次のように書きます:

_SELECT EXTRACT(Epoch FROM (MAX("createdAt") - MIN("createdAt"))) 
FROM   stats
WHERE  "references" ? 'project'
AND   (type = 'event1' OR
       type = 'event2'
   AND "createdAt" >= '2015-11-02 00:00:00+08:00'  -- I guess you want this
   AND "createdAt" <  '2015-12-04 00:00:00+08:00'
      )
GROUP  BY "references"->> 'project';  -- don't cast
_

ノート

  • ここにキャストしないでください:

    _stats."references"::jsonb ? 'project'_

    列はすでにjsonbであり、何も得られません。述語was selectiveの場合、インデックスの使用はキャストによって禁止されている可能性があります。

  • _"createdAt"_の述語は、おそらく下限と上限でincorrectです。丸一日を含めるには、提案された代替案を検討してください。

  • references予約語 であるため、常に二重引用符で囲む必要があります。識別子として使用しないでください。 _"createdAt"_のような二重引用符で囲まれたCaMeLケース名の場合も同様です。許可されていますが、エラーが発生しやすく、不要な複雑化が生じます。

  • type

    type character varying(255) NOT NULL,

    タイプは、50文字以下の単純なストリングです。

    Typeフィールドには5つの異なる値しかありません。これは、本番環境では数百以下になるでしょう。

    これはどれも意味がないようです。

    • varchar(255)自体は、ほとんど意味がありません 。 255文字は、Postgresでは重要ではない任意の制限です。
    • 50文字以下の場合、255の制限はさらに意味がありません。
    • 適切に正規化された設計では、小さなinteger列_type_id_(小さなtypeテーブルを参照)があり、行ごとに4バイトしか占有せず、インデックスが小さくて高速になります。
  • 理想的には、projectテーブルにすべてのプロジェクトをリストし、statsに別の短整数FK列_project_id_を記述します。このようなクエリを高速化します。そして、選択的な基準については、提案された正規化がなくてもmuchより高速なクエリが可能です。これらの線に沿って:

  • ユーザーごとに最新のレコードを取得するためにGROUP BYクエリを最適化する

3