約50億行のPostgreSQLテーブルがあり、適切なインデックスが見つからず、特定のLIMIT
操作で主キースキャンを実行するという厄介な習慣が生じています。
この問題は通常、ORDER BY .. LIMIT ..
句(Djangoページネーションの一般的なパターン))に現れます。ここで、LIMIT
は、インデックスに一致する結果の比較的小さなサブセットです。極端な例はこれです:
SELECT * FROM mcqueen_base_imagemeta2
WHERE image_id IN ( 123, ... )
ORDER BY id DESC
LIMIT 1;
このIN
句の項目は〜20で、image_id
のインデックスと一致する行の合計は16です。
EXPLAIN
は、image_id
インデックスがなく、代わりに5B行のPKスキャンを実行することを示しています。
制限(cost = 0.58..4632.03 rows = 1 width = 28) -> mcqueen_base_imagemeta2でmcqueen_base_imagemeta2_pkeyを使用して後方にインデックススキャン(cost = 0.58..364597074.75 rows = 78722 width = 28) フィルター: (image_id = ANY( '{123、...}' :: bigint []))
LIMIT
を2
に増やすと、期待どおりに機能します。
制限(コスト= 7585.92..7585.93行= 2幅= 28) ->ソート(コスト= 7585.92..7782.73行= 78722幅= 28) ソートキー:id DESC -> mcqueen_base_imagemeta2でmcqueen_base_imagemeta2_image_id_616fe89cを使用したインデックススキャン(cost = 0.58..6798.70 rows = 78722 width = 28) Index Cond:(image_id = ANY( '{123、...}' :: bigint [ ]))
これは、インデックスが〜3000行と一致し、制限が100に設定されているクエリでも発生するため、現実の世界では簡単に何かが発生しますREST APIページネーション。
テーブルの定義は次のとおりです。
mcqueen=# \d mcqueen_base_imagemeta2
Table "public.mcqueen_base_imagemeta2"
Column | Type | Modifiers
-------------------+--------------------------+----------------------------------------------------------------------
id | bigint | not null default nextval('mcqueen_base_imagemeta2_id_seq'::regclass)
created_at | timestamp with time zone | not null
image_id | bigint | not null
key_id | smallint | not null
source_version_id | smallint | not null
Indexes:
"mcqueen_base_imagemeta2_pkey" PRIMARY KEY, btree (id)
"mcqueen_base_imagemeta2_image_id_616fe89c" btree (image_id)
"mcqueen_base_imagemeta2_key_id_a4854581" btree (key_id)
"mcqueen_base_imagemeta2_source_version_id_f9b0513e" btree (source_version_id)
Foreign-key constraints:
"mcqueen_base_imageme_image_id_616fe89c_fk_mcqueen_b" FOREIGN KEY (image_id) REFERENCES mcqueen_base_image(id) DEFERRABLE INITIALLY DEFERRED
"mcqueen_base_imageme_key_id_a4854581_fk_mcqueen_b" FOREIGN KEY (key_id) REFERENCES mcqueen_base_metakey(id) DEFERRABLE INITIALLY DEFERRED
"mcqueen_base_imageme_source_version_id_f9b0513e_fk_mcqueen_b" FOREIGN KEY (source_version_id) REFERENCES mcqueen_base_metasourceversion(id) DEFERRABLE INITIALLY DEFERRED
私はチューニングに関しては初心者ですが、統計のデフォルトはそのテーブルのサイズに達していないため、PKスキャンはインデックススキャンよりも高速であると単純に考えています。
78722が見つかると考えていますが、実際には16が見つかるため、いくつかの悪い計画につながる可能性があります。
In-listの値が統計テーブルのMCVリストに存在しない場合、n_distinct値を使用して頻度を推測します。これはおそらくかなりずれています(そのことについての私の質問には答えませんでした)。これを行う方法は、MCV頻度リストでカバーされていないタプルの数を取得し、MCVリストにリストされていない個別の値の数で割ります。つまり、基本的にはntuples * (1-sum of MCF) / (n_distinct - length of MCF)
です。この簡略化された式はNULLを無視します。
@ErwinBrandstetterが示唆するように、統計のサンプルサイズを増やすことでMCVリストのサイズを増やすことで、状況を改善できる可能性があります。これにより、n_distinct推定の精度も向上する可能性があります。しかし、60億行の場合、サンプルサイズを十分に増やすことができない場合があります。また、同じページで発生する可能性が高い重複する値とともにimage_idがまとめられている場合、PostgreSQLで使用されるサンプリング方法は、n_distinctの計算に関してかなり偏っており、これはサンプルサイズを増やすだけで修正に抵抗します。
これを修正するより簡単な方法は、n_distinctを手動で修正することです。
alter table mcqueen_base_imagemeta2 alter column image_id set (n_distinct=1000000000);
analyze mcqueen_base_imagemeta2;
この方法では、ANALYZEに必要な時間やストレージは増えませんが、サンプルサイズを増やす方法と同じように、成功する可能性が高くなります。
LIMIT 1
の場合、PostgresはORDER BY
をサポートするインデックスをトラバースする方が高速であると推定し、最初の行が見つかるまでフィルタリングを続けることができます。これは、数行以上が適格であり、そのうちの1つがORDER BY
に従って早期にポップアップする限り、高速です。ただし、適格な行が早期にポップアップしない場合は(非常に)遅くなり、適格な行がまったくない場合は最悪のシナリオになります。小さなLIMIT
についても同様です。
Postgresは最も一般的な値(MCVリスト)に関する統計を収集しますが、最も一般的な値については収集しません-明白な理由により、これは多すぎて役に立たないでしょう。また、デフォルトでは、列間の相関に関する統計はありません。 (ID番号は通常無相関であるため、手動で作成できますが、いずれにしてもユースケースに適合しません。)
したがって、Postgresは一般的な推定に基づいて決定を行う必要があります。あるインデックスから別のインデックスに切り替えるスイートスポットを特定するのは非常に困難です。これはさらに難しくなりますが、多くのアイテムを含むimage_id IN (123, ... )
のような述語では、ほとんどの場合、ほとんどまたはまったくないか、存在しません。しかし、リストに十分な数を入れると、Postgresは最終的に他のインデックスをたどると最初のヒットがより早く見つかると予想します。
より大きな統計ターゲットを使用すると、状況を多少改善できる可能性があります。
ALTER TABLE mcqueen_base_imagemeta2 ALTER image_id SET STATISTICS 2000;
これにより(とりわけ)列のMCVリストのサイズが増加し、より一般的な(より少ない)値の特定に役立ちます。しかし、これは問題の一般的な解決策ではなく、ANALYZE
とクエリの計画が少し高くなります。関連:
最新バージョン(まもなくPostgres 12になる)にアップグレードすると、一般的なパフォーマンスが向上し、プランナーがよりスマートになります。
カーディナリティー、値の頻度、アクセスパターンなどに応じて、回避策にはさまざまな手法があります... ORDER BY
インデックスを完全に無効にする Laurenzの例 isone根本的な回避策-長いリストで逆効果または、非常に一般的なimage_id
、ORDER BY
インデックスは、実際にははるかに高速です。
関連:
50億行、フィルターリストの約20 image_id
、小さいLIMIT
のように、指定された数値でうまく機能するはずです。 LIMIT 1
と短いリストの場合に最も効率的ですが、小さいLIMIT
と管理しやすいリストのサイズに適しています。
SELECT m.*
FROM unnest( '{123, ...}'::bigint[]) i(image_id)
CROSS JOIN LATERAL (
SELECT m.id
FROM mcqueen_base_imagemeta2 m
WHERE m.image_id = i.image_id
ORDER BY m.id DESC
LIMIT 1 -- or N
) m
ORDER BY id DESC
LIMIT 1; -- or N
リストをarrayおよびunnest()
として提供します。または、VALUES
式を使用します。関連:
(image_id, id DESC)
のマルチカラムインデックスでこれをサポートすることが不可欠です。
可能性があります次に、mcqueen_base_imagemeta2_image_id_616fe89c
だけで既存のインデックス(image_id)
を削除します。見る:
これにより、image_id
ごとに1つの非常に高速なインデックス(のみ)スキャンが実行されます。そして、最後の(非常に)安価な並べ替え手順です。
image_id
ごとにN行をフェッチすることで、外部クエリで必要なすべての行が確保されます。If単一のimage_id
あたりより少ない行のみが結果に含まれる可能性があるというメタ知識がある場合、それに応じてネストされたLIMIT
を減らすことができます。
(Djangoページネーションの一般的なパターン)
LIMIT
とOFFSET
による改ページ調整?最初のページは問題ありませんが、その後は悪い考えです。
簡単な解決策は、ORDER BY
条件。セマンティクスは変更されませんが、PostgreSQLはインデックスを使用できなくなります。
SELECT * FROM mcqueen_base_imagemeta2
WHERE image_id IN ( 123, ... )
ORDER BY id + 0 DESC
LIMIT 1;