web-dev-qa-db-ja.com

ORDER BY句によってクエリのパフォーマンスが低下する

コンテキスト:

PostgreSQL 10では、usersテーブルに3667438レコードがあり、usersテーブルにはsocialというJSONBがあります。通常、計算された関数の出力にインデックスを付ける方法を使用するため、情報を単一のインデックスに集約できます。 engagement(social)関数の出力は倍精度数値型です。

問題:

問題のある節はORDER BY engagement(social) DESC NULLS LASTであり、このデータにはbtreeインデックスidx_in_social_engagement with DESC NULLS LASTも添付されています。

高速クエリ:

EXPLAIN ANALYZE
SELECT  "users".* FROM "users"
WHERE (follower_count(social) < 500000)
AND (engagement(social) > 0.03)
AND (engagement(social) < 0.25)
AND (peemv(social) < 533)
ORDER BY "users"."created_at" ASC
LIMIT 12 OFFSET 0;

Limit  (cost=0.43..52.25 rows=12 width=1333) (actual time=0.113..1.625 
rows=12 loops=1)
   ->  Index Scan using created_at_idx on users  (cost=0.43..7027711.55 rows=1627352 width=1333) (actual time=0.112..1.623 rows=12 loops=1)
         Filter: ((follower_count(social) < 500000) AND (engagement(social) > '0.03'::double precision) AND (engagement(social) <  '0.25'::double precision) AND (peemv(social) > '0'::double precision) AND (peemv(social) < '533'::double precision))
         Rows Removed by Filter: 8
 Planning time: 0.324 ms
 Execution time: 1.639 ms

遅いクエリ:

EXPLAIN ANALYZE 
SELECT  "users".* FROM "users" 
WHERE (follower_count(social) < 500000) 
AND (engagement(social) > 0.03) 
AND (engagement(social) < 0.25) 
AND (peemv(social) > 0.0) 
AND (peemv(social) < 533) 
ORDER BY engagement(social) DESC NULLS LAST, "users"."created_at" ASC 
LIMIT 12 OFFSET 0;

Limit  (cost=2884438.00..2884438.03 rows=12 width=1341) (actual time=68011.728..68011.730 rows=12 loops=1)
->  Sort  (cost=2884438.00..2888506.38 rows=1627352 width=1341) (actual time=68011.727..68011.728 rows=12 loops=1)
        Sort Key: (engagement(social)) DESC NULLS LAST, created_at
        Sort Method: top-N heapsort  Memory: 45kB
        ->  Index Scan using idx_in_social_engagement on users  (cost=0.43..2847131.26 rows=1627352 width=1341) (actual time=0.082..67019.102 rows=1360633 loops=1)
            Index Cond: ((engagement(social) > '0.03'::double precision) AND (engagement(social) < '0.25'::double precision))
            Filter: ((follower_count(social) < 500000) AND (peemv(social) > '0'::double precision) AND (peemv(social) < '533'::double precision))
            Rows Removed by Filter: 85580
Planning time: 0.312 ms
Execution time: 68011.752 ms

各行に格納されているすべてのデータが必要なため、選択は*で行われます。

更新:

CREATE INDEX idx_in_social_engagement on influencers USING BTREE ( engagement(social) DESC NULLS LAST)

正確なインデックス定義

4
Imanol Y.

あなたの_ORDER BY_句はオンです:

_engagement(social) DESC NULLS LAST, "users"."created_at" ASC
_

しかし、私はあなたのインデックスがちょうど上にあると思います:

_engagement(social) DESC NULLS LAST
_

したがって、インデックスは_ORDER BY_を完全にサポートできません。

JSONBまたは式インデックスを使用せずに同じ問題を再現できます。 _ORDER BY_のboth列に複合式インデックスを作成することで、状況を救うことができる場合があります。

PostgreSQLプランナーが無限に賢い場合、既存のインデックスを効率的に使用できる可能性があります。残りのすべてのフィルター要件を満たす12のタプルを収集するまで、engagement(social) DESC NULLS LASTを行進させる必要があります。次に、12番目のタプルでengagement(social)に関連付けられた(および他の基準を満たした)残りのタプルをすべて収集するまで、そのインデックスを行進し続けます。次に、収集されたすべてのタプルを_ORDER BY_全体で再ソートし、_LIMIT 12_をその拡張および再ソートされたセットに適用する必要があります。しかし、PostgreSQLプランナーは無限に賢いわけではありません。

7
jjanes

ここでの原因は、JSONB列の統計が不足していることだと思います。 PostgresはJSONB列の統計を保持せず、代わりにハードコードされた推定値を使用します。これらの推定値が非常に離れている場合、それが起こる可能性が非常に高い場合、これはあなたのケースのようなひどいクエリプランにつながる可能性があります。

良い計画では、Postgresは最初にデータを注文し、次にそれをフィルタリングします。これは、フィルターで多くの行が削除されない場合、LIMIT句と並べ替えられた列のインデックスを含むクエリでは非常に高速です。インデックスは順序付けされているため、行を順番に取得するのは非常に安価です。 limit句は、それを満たすために十分になるまで数行をフェッチするだけでよいことを意味します。

しかし、フィルターが多くの行を除外する場合、例えば0.1%だけが一致する場合、最初に並べ替えると、ほとんどすべてのテーブルを調べて十分な行を見つける必要があります。これは、ほとんどすべての行をフィルターで除外するためです。この場合、最初にインデックスでフィルタリングしてからソートする方がはるかに高速です。これはあなたの悪い計画がすることであり、明らかにそれはあなたのデータによく適合しません。

断然最良のオプションは、ここで使用する値を独自の列に入れることです。 JSONBは一部の用途に非常に役立ちますが、柔軟性が必要ない場合は、従来のリレーショナルな方法がはるかに優れています。

関数インデックスは統計を提供しますが、これはクエリに役立ちます。あなたの場合、ソーシャル列全体を関数に渡しているため、これはうまく機能せず、そこから良い統計を作成できないと思います。ソーシャルカラムのエンゲージメントキーでのみインデックスを試すことができます。これにより、Postgresに、より優れたクエリプランに必要な統計情報が提供される可能性があります。それを行う方法の例については、 JSONB統計に関する私の質問 を参照してください。

1
Mad Scientist