web-dev-qa-db-ja.com

結果がなく、LIMITが指定されている場合、SELECTが非常に遅くなる

最終的な結果の数が0の場合にインデックスを使用しないため、SELECTクエリが非常に遅いという問題が発生していますandLIMIT句が指定されています。

結果の数が0より大きい場合、Postgresはインデックスを使用し、結果は約1ミリ秒で返されます。私の知る限り、これは常に正しいようです。

結果の数が0で、LIMITが使用されていない場合、Postgresはインデックスを使用し、結果は約1msで返されます

結果の数が0で、LIMITが指定されている場合、Postgresは順次スキャンを実行し、結果は約13,000ミリ秒かかります。

最後のケースでPostgreSQLがインデックスを使用しないのはなぜですか?

カーディナリティ:

合計で2,100万行。
〜300万行WHERE related_id=1
〜300万行WHERE related_id=1 AND platform=p1
2行WHERE related_id=1 AND platform=p2
0行WHERE related_id=1 AND platform=p3
〜800万行WHERE platform=p2

Postgresバージョン:9.4.6

テーブルスキーマ:

CREATE TYPE platforms AS ENUM ('p1', 'p2', 'p3');

CREATE TABLE mytable (
    id bigint NOT NULL DEFAULT nextval('mytable_sq'::regclass),
    related_id integer NOT NULL,
    platform platforms NOT NULL DEFAULT 'default'::platforms,
    name character varying(200) NOT NULL,
    CONSTRAINT mytable_pkey PRIMARY KEY (id),
    CONSTRAINT mytable_related_id_fkey FOREIGN KEY (related_id)
         REFERENCES related (id)
);

CREATE INDEX related_id__platform__index ON mytable (related_id, platform);
CREATE UNIQUE INDEX some_other_index ON mytable (related_id, lower(name::text));

クエリと計画:

このクエリは0行を返します。

EXPLAIN ANALYZE
SELECT * FROM mytable
WHERE related_id=1 AND platform='p2'::platforms
LIMIT 20;

 Limit  (cost=0.00..14.07 rows=20 width=737) (actual time=12863.465..12863.465 rows=0 loops=1)
    ->  Seq Scan on mytable  (cost=0.00..1492790.47 rows=2122653 width=737) (actual time=12863.464..12863.464 rows=0 loops=1)
          Filter: ((related_id = 1) AND (platform = 'p2'::platforms))
          Rows Removed by Filter: 21076656
 Planning time: 3.540 ms
 Execution time: 12868.190 ms

このクエリも0行を返します。

EXPLAIN ANALYZE
SELECT * FROM mytable
WHERE related_id=1 AND platform='p2'::platforms;

 Bitmap Heap Scan on mytable  (cost=60533.63..1295799.94 rows=2122653 width=737) (actual time=0.890..0.890 rows=0 loops=1)
 Recheck Cond: ((related_id = 1) AND (platform = 'p2'::platforms))
  ->  Bitmap Index Scan on related_id__platform__index  (cost=0.00..60002.97 rows=2122653 width=0) (actual time=0.888..0.888 rows=0 loops=1)
         Index Cond: ((related_id = 1) AND (platform = 'p2'::platforms))
 Planning time: 0.827 ms
 Execution time: 1.104 ms

このクエリは20行を返します(LIMITがない場合、200万行以上になります):

EXPLAIN ANALYZE
SELECT * FROM mytable
WHERE related_id=1 AND platform='p1'::platforms
LIMIT 20;

 Limit  (cost=0.44..70.95 rows=20 width=737) (actual time=0.759..0.995 rows=20 loops=1)
   ->  Index Scan using related_id__platform__index on mytable  (cost=0.44..1217669.26 rows=345388 width=737) (actual time=0.759..0.993 rows=20 loops=1)
         Index Cond: ((related_id = 1) AND (platform = 'p1'::platforms))
 Planning time: 5.776 ms
 Execution time: 2.476 ms

このクエリは2行を返します。

EXPLAIN ANALYZE
SELECT * FROM mytable
WHERE related_id=1 AND platform='p3'::platforms LIMIT 20;

 Limit  (cost=0.44..80.37 rows=20 width=737) (actual time=0.014..0.016 rows=2 loops=1)
   ->  Index Scan using related_id__platform__index on mytable  (cost=0.44..99497.62 rows=24894 width=737) (actual time=0.014..0.015 rows=2 loops=1)
         Index Cond: ((related_id = 1) AND (platform = 'p3'::platforms))
 Planning time: 0.972 ms
 Execution time: 0.123 ms
6
Tim Martin

Postgresはクエリ内の述語の組み合わせの頻度を推定する悪い仕事をします:

SELECT * FROM tbl
WHERE  related_id = 1 AND platform = 'p2'::platforms
LIMIT  20;

述語のそれぞれは、それ自体ではあまり選択的ではありません-その情報はPostgresに利用可能です(「最も一般的な値」)-統計が最新であると仮定します:

合計で2,100万行。
〜300万行WHERE related_id=1
〜800万行WHERE platform=p2

IOW、〜7行ごとに1番目のフィルターを通過し、〜3行ごとに2番目のフィルターを通過します。 Postgresは(素朴な)計算を行い、およそ20行ごとに条件が満たされることを期待しています。 ORDER BYがないので、any20の条件を満たす行が実行されます。最速の方法は、テーブルを順次スキャンして、400行以下で実行することです。数ページのデータのみで、非常に安価です。

anyインデックスを使用すると追加のコストが追加され、Postgresはインデックステーブルをスキャンする必要があります。 (例外:インデックスのみのスキャン。ここではSELECT *では実行できません)。それは、Postgresがそれ以外の場合はそれがより高価であると推定するのに十分な追加のページを読み取る必要がある場合にのみ支払うでしょう。小さいLIMITは順次スキャンされますが、大きい(またはない)LIMITはビットマップインデックススキャンです。

残念ながら、述語の組み合わせは、予想外にまれであることが判明しました。 Postgresは、条件を満たす行を2つだけ見つけるために、テーブル全体をスキャンする必要があります。 (インデックスは実際にははるかに安いとにかく)

2行WHERE related_id=1 AND platform=p2

複数の列の値の結合頻度notPostgresで使用できます。考えてみてください。そのような統計を収集すると、すぐに手に負えなくなります。

この特定の場合のための非常にシンプルで効率的なソリューション部分インデックスを作成します

CREATE INDEX related_id_1_platform_2_idx ON tbl (id)
WHERE  related_id = 1 AND platform = 'p2'::platforms;

この超小型インデックス(2行)はクエリに完全に一致するだけでなく、特定の組み合わせ(pg_class.reltuplesのエントリ)でカウントの見積もりを利用できるようになります。実際のインデックス列はほとんどこの列とは無関係です。小さな列を選択してください。通常はPKにするのが最適です。

2つの述語のいずれかが変更される可能性がある場合は、より一般的なアプローチがあります。たとえば、related_id = 1が安定した状態であれば、次のように作成します。

CREATE INDEX related_id_1_idx ON tbl (platform)
WHERE  related_id = 1;

インデックス列が再び関連しています。 Postgresはfunctionalインデックスのインデックス列の完全な統計のみを収集するため、これではスケールをチップするのに十分でない場合があります(それ以外の場合は、基になるテーブルの統計に依存します)。私が提案する:

CREATE INDEX related_id_1_func_idx ON tbl ((platform::text::platforms))  -- double parens!
WHERE  related_id = 1;

括弧の余分なペアに注意してください-キャストの省略形の構文上の必要性。
platform::text::platformsは実際には何も変更しません-enumtextにキャストして戻します。ただし、Postgresは(推定)新しい値に関する完全な統計を収集します。

これで(ANALYZE tblの後)、related_id = 1platform最も一般的な値を含む完全な統計が得られます。

確認する:

SELECT *
FROM   pg_stats
WHERE  schemaname = 'public'  -- actual schema
AND    tablename  = 'related_id_1_func_idx';  -- actual idx name

そしてPostgresはあなたのケースのインデックスを選択する必要があります-クエリで同じ式を繰り返す場合そう:

SELECT ...
WHERE related_id = 1 AND platform::text::platforms = 'p2'::platforms;

関連:

Postgres統計の最も一般的な値について:

8