web-dev-qa-db-ja.com

WHERE、ORDER BY、LIMITを使用したこのクエリがなぜ遅いのですか?

このテーブルposts_lists

テーブル "public.posts_lists" 
列|タイプ|照合|ヌル可能|デフォルト
 ------------ + ------------------------ + ------ ----- + ---------- + --------- 
 id |キャラクター変化(20)| | nullではない| 
 user_id |キャラクター変化(20)| | | 
タグ| jsonb | | | 
スコア|整数| | | 
 created_at |整数| | | 
インデックス:
 "tmp_posts_lists_pkey1" PRIMARY KEY、btree(id)
 "tmp_posts_lists_idx_create_at1532588309" btree(created_at)
 "tmp_posts_lists_ids_idcore_descore_descore_desc153" :: text)DESC)
 "tmp_posts_lists_idx_tags1532588309" gin(jsonb_array_lower(tags))
 "tmp_posts_lists_idx_user_id1532588309" btree(user_id)

タグによるリストの取得はfast

EXPLAIN ANALYSE
SELECT * FROM posts_lists
WHERE jsonb_array_lower(tags) ? lower('Qui');
 posts_listsのビットマップヒープスキャン(コスト= 1397.50..33991.24行= 10000幅= 56)(実際の時間= 0.110..0.132行= 2ループ= 1)
条件を再確認:(jsonb_array_lower(タグ)? 'qui' :: text)
ヒープブロック:正確= 2 
-> tmp_posts_lists_idx_tags1532588309のビットマップインデックススキャン(コスト= 0.00..1395.00行= 10000幅= 0)(実際の時間= 0.010..0.010 rows = 2 loops = 1)
 Index Cond:(jsonb_array_lower(tags)? 'qui' :: text)
計画時間:0.297 ms 
実行時間:0.157 ms 

スコア順に並べられたリストを取得し、100に制限します-fast

EXPLAIN ANALYSE
SELECT *
FROM posts_lists
ORDER BY score_rank(score, id) DESC
LIMIT 100;
制限(コスト= 0.56..12.03行= 100幅= 88)(実際の時間= 0.074..0.559行= 100ループ= 1)
-> tmp_posts_lists_idx_score_desc1532588309を使用して、posts_lists(コスト= 0.56..1146999.15 rows = 10000473 width = 88)(実際の時間= 0.072..0.535 rows = 100 loops = 1)
計画時間:0.586 ms 
実行時間:0.714 ms 

しかし、上記の2つのクエリを組み合わせると非常に遅い

EXPLAIN ANALYSE
SELECT * FROM posts_lists
WHERE jsonb_array_lower(tags) ? lower('Qui')
ORDER BY score_rank(score, id) DESC
LIMIT 100;
制限(コスト= 0.56..33724.60行= 100幅= 88)(実際の時間= 2696.965..493476.142行= 2ループ= 1)
-> tmp_posts_lists_idx_score_desc1532588309を使用したインデックススキャン(posts_lists(コスト= 0.56..3372404.39 rows = 10000 width = 88)(実際の時間= 2696.964..493476.139 rows = 2 loops = 1)
フィルター:(jsonb_array_lower(tags)? 'qui' :: text)
フィルターによって削除された行:9999998 
計画時間:0.426 ms 
実行時間:493476.190 ms 

なぜ?クエリの効率を向上させる方法は?

上記で使用した2つの関数の定義:

create or replace function score_rank(score integer, id text)
  returns text as $$
select case when score < 0
  then '0' || lpad((100000000 + score) :: text, 8, '0') || id
       else '1' || lpad(score :: text, 8, '0') || id
       end
$$
language sql immutable;


create or replace function jsonb_array_lower(arr jsonb)
  returns jsonb as $$
SELECT jsonb_agg(lower(elem))
FROM jsonb_array_elements_text(arr) elem
$$
language sql immutable;
2
robxyy

並べ替えとページング

関数score_rank()は、textスコアと追加されたPK integerからidを生成します。並べ替えには役立ちません。完全に交換してください。まったく必要ないのではないでしょうか。代わりに、2つの列scoreおよびidsortingに直接使用します。

SELECT *
FROM   posts_lists
ORDER  BY score DESC, id DESC
LIMIT  100;

インデックスtmp_posts_lists_idx_score_desc1532588309を、(score DESC, id DESC)のより小さく、より速く、より安価に維持でき、より用途の広いインデックスに置き換えます。

また、行値の比較を使用して、この複数列のインデックスに基づいてpaginationを効率的に作成することもできます。見る:

後で、base256などで文字列を連結する新しい関数について説明しました。このような巧妙な策略によってパフォーマンスが向上することはありません。 integerでの並べ替えは、Postgresでの文字列での並べ替えよりも高速です。 varchar(20)の代わりにinteger(またはbigint)を使用すると、実際には複数の方法で問題が発生します。

統計とクエリプラン(別名:なぜですか?)

主な問題は、jsonb列にネストされた値の統計の欠如です。結果としてPostgresは時々述語jsonb_array_lower(tags) ? lower('Qui')の選択性を誤って判断し、不適切なクエリプランを選択します。 LIMIT 2を使用した例では、クエリプランナーのロジックを次のように説明できます。これを"Plan 1"と呼びましょう。

スコアが最も高いのは2行だけですか?スコアが高い順にposts_lists_idx_score_descをスキャンしてみましょう。運が良ければ、すぐに結果が出ます!

これは、少なくともある程度一般的なタグを使用するほとんどの場合に妥当な計画です。しかし、タグ「qui」は非常にまれで、スコアも低いことがわかりました。最悪のケース。 Postgresは、2を維持するためだけに400万行近くをスキャンすることになります。膨大な時間の無駄:

Rows Removed by Filter: 3847383

クエリプランナーがそのタグが実際にどれほどまれであるかを知っている場合、それはposts_lists_idx_tagsの2番目の例で見られるような他のインデックスLIMIT 100から始まります-これを"Plan 2"と呼びましょう:

一致する行を検索し、スコアでソートして上位Nを取得します。

プラン1は、小さいLIMITの方が有利であり、タグの頻度が高いです。 (そして、条件を満たす行が偶然上にソートされる場合。)
プラン2は、大きいLIMITの方が有利で、タグの使用頻度は低くなります。

Postgresは現在、jsonbのようなドキュメントタイプのネストされた値に関する統計を持っていません。また、結合された周波数はまったくありません。見る:

他に何をする場合でも、必ず最新バージョンのPostgresを実行してください。プランナーはリリースごとに賢くなっています。

代替案

1.Postgres配列text[])およびjsonb列の代わりに配列演算子を使用してsome統計を作成するというアイデアも考えられます最も一般的な要素の場合。 システムビューmost_common_elemsの列most_common_elem_freqselem_count_histogrampg_stats

Postgresが一部の星座のより良いクエリプランを生成するのに役立ちますが、特効薬ではありません。まず、最も一般的な要素のみが保存されます。 Postgresはまだ最も希少な要素について知りません。

2.またはnormalizedbデザインを作成し、tagsを行ごとに1つのタグを付けて別の1:n tableに移動します。タグごとに行オーバーヘッドが追加されるため、ディスクフットプリントが増加します。 (しかし、タグを変更すると、テーブルの膨張が少なくなるため、はるかに安くなります。)タグが安定している場合は、posts_listsと新しいテーブルtagsの間の完全なn:m関係を検討してください。多くの一般的なタグについても、これは少し小さいです。そして、それは「クリーン」な方法です。より詳細な統計があり、不良クエリプランが少なくなるはずです。

3.Postgres 10以降、to_tsvector()の値を処理するjson(b)のバリアントがあります。したがって、text search indexを作成して、テキスト検索演算子を操作するのは簡単です。

インデックス:

CREATE INDEX posts_lists_idx_tags_fts ON posts_lists USING gin (to_tsvector('simple', tags));

クエリ:

SELECT * FROM posts_lists
WHERE  to_tsvector('simple', tags) @@ to_tsquery('simple', 'qui') -- text search is case insensitive
ORDER  BY score DESC, id DESC
LIMIT  2;

必ずsimple辞書を使用してください。他のほとんどの辞書に組み込まれているステミングは必要ありません。

テキスト検索関数は小文字の出力を生成します。すべて設計上大文字小文字を区別しません。元の関数jsonb_array_lower()のように処理する必要はありません。

4.jsonbインデックスを使いながら、より専門的なjsonb_path_ops演算子クラスを試してください:

CREATE INDEX ON posts_lists USING gin (jsonb_array_lower(tags) jsonb_path_ops);

クエリ:

WHERE  jsonb_array_lower(tags) @> '["qui"]'

マニュアル:

jsonb_path_ops演算子クラスは、@>演算子を使用したクエリのみをサポートしますが、デフォルトの演算子クラスjsonb_opsよりも優れたパフォーマンス上の利点があります。 jsonb_path_opsインデックスは通常、同じデータのjsonb_opsインデックスよりもはるかに小さく、特にデータに頻繁に出現するキーがクエリに含まれている場合、検索の具体性が向上します。したがって、検索操作は通常、デフォルトの演算子クラスよりもパフォーマンスが向上します。

しかし、私はしないあなたの特定のケースに多くを期待します。

5.手続き型ソリューションと組み合わせて、「グラニュール」インデックスのレジームを使用します。見る:

db <> fiddle ここ-いくつかのテストで...

0

このステートメントの問題は、クエリプランナーにjsonb_array_lower(tags)の使用可能な統計がないことです。

最初の説明に見られるように:

_(cost=0.00..1395.00 rows=10000 width=0) (actual time=0.010..0.010 rows=2 loops=1)
_

プランナは、フィルタjsonb_array_lower(tags) ? lower('Qui')を使用すると10.000行が返されることを期待しますが、返される行は2つだけです。

最後のステートメントでも同じことが起こります。この欠落した情報のため、プランナーは、インデックス_tmp_posts_lists_idx_score_desc1532588309_のスキャンがより効率的であると想定しています。

lowerの必要性を回避し、INSERTUPDATEの間に入力を正規化することを試みることができます。

別の方法は次のとおりです。

_WITH c AS (
    SELECT id FROM posts_lists
    WHERE jsonb_array_lower(tags) ? lower('Qui')
)
SELECT * FROM posts_lists l, c
WHERE l.id = c.id
ORDER BY score_rank(score, id) DESC
LIMIT 100;
_

このステートメントはCTEを最適化フェンスとして使用しますが、他のWHERE条件またはテーブルコンテンツの場合、パフォーマンスが低下する可能性があることに注意してください。

2
Thomas Berger