このテーブル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;
関数score_rank()
は、text
スコアと追加されたPK integer
からid
を生成します。並べ替えには役立ちません。完全に交換してください。まったく必要ないのではないでしょうか。代わりに、2つの列score
およびid
をsortingに直接使用します。
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_freqs
、elem_count_histogram
、pg_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 ここ-いくつかのテストで...
このステートメントの問題は、クエリプランナーに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
の必要性を回避し、INSERT
とUPDATE
の間に入力を正規化することを試みることができます。
別の方法は次のとおりです。
_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
条件またはテーブルコンテンツの場合、パフォーマンスが低下する可能性があることに注意してください。