web-dev-qa-db-ja.com

count / sortを使用したpostgresクエリのパフォーマンスが遅い

別の開発者からDjango/postgresアプリを継承しましたが、クエリの多くが非常に遅いことがわかりました。これまで行われていないデータを正しく整理/正規化するのに多くの時間を費やしてきましたが、多くの助けにはなりましたが、クエリによってはまだ数分かかる場合があります。

特に、特定の基準に従って画像の数を表示することにより、データを要約するためにいくつかのクエリを実行します。これの一例を以下に示します-ハッシュタグによる上位画像。

クエリは、画像の数が最も多いハッシュタグを返します。画像とハッシュタグの関係は、結合テーブル全体の多対多の関係です。このクエリに適用できる他のさまざまなWHERE基準がありますが、これは最も単純な例です-少なくとも、作成されたタイムスタンプとquery_idフィールドでフィルタリングします。

pgバージョンPostgreSQL 9.6.11 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.3 20140911 (Red Hat 4.8.3-9), 64-bit

hardware:AWS RDS Postgres db.t2.largeが最近db.t2.mediumからアップグレードされ、多くのクエリで条件の再確認ステップが削除されたようです

クエリ

SELECT
    base_hashtag.*,
    COUNT(DISTINCT base_image.id)
FROM
    base_hashtag
    JOIN base_imagehashtag ON base_hashtag.id = base_imagehashtag.hashtag_id
    JOIN base_image ON base_imagehashtag.image_id = base_image.id
WHERE
    base_image.query_id = '566591d4-33a3-4a96-a2d9-99e7bd625c18'
    AND base_image.created > 1548979200
    AND base_image.created < 1551398400
    AND base_hashtag.id <> 1
GROUP BY
    base_hashtag.id
ORDER BY
    base_hashtag.count DESC
LIMIT
    40
;

テーブル定義:以下は、クエリ例のテーブルフィールドの簡略化されたサブセットです。潜在的に冗長なインデックスがいくつかありますが、それらは選択したパフォーマンスに影響を与えるべきではないと私は思っています。

- Table: public.base_image

CREATE TABLE public.base_image
(
    id integer NOT NULL DEFAULT nextval('base_image_id_seq'::regclass),
    image_url character varying(100000) COLLATE pg_catalog."default",
    username character varying(100000) COLLATE pg_catalog."default",
    created integer,
    location_id character varying(10000000) COLLATE pg_catalog."default",
    query_id uuid,
    CONSTRAINT base_image_pkey PRIMARY KEY (id),
    CONSTRAINT base_image_query_id_b2da2903_fk_base_query_pk_id FOREIGN KEY (query_id)
        REFERENCES public.base_query (pk_id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        DEFERRABLE INITIALLY DEFERRED
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

CREATE INDEX base_image_0bbeda9c
    ON public.base_image USING btree
    (query_id)
    TABLESPACE pg_default;

CREATE INDEX base_image_created_idx
    ON public.base_image USING btree
    (created)
    TABLESPACE pg_default;

CREATE INDEX base_image_created_imageid_queryid
    ON public.base_image USING btree
    (id, created, query_id)
    TABLESPACE pg_default;

CREATE INDEX base_image_id_idx
    ON public.base_image USING btree
    (id)
    TABLESPACE pg_default;

CREATE INDEX base_image_id_query_id_idx
    ON public.base_image USING btree
    (id, query_id)
    TABLESPACE pg_default;

CREATE UNIQUE INDEX base_image_query_id_id_idx
    ON public.base_image USING btree
    (query_id, id)
    TABLESPACE pg_default;

CREATE INDEX base_image_query_id_idx
    ON public.base_image USING btree
    (query_id)
    TABLESPACE pg_default;

-- Table: public.base_imagehashtag

CREATE TABLE public.base_imagehashtag
(
    id integer NOT NULL DEFAULT nextval('base_imagehashtag_id_seq'::regclass),
    hashtag_id integer NOT NULL,
    image_id integer NOT NULL,
    CONSTRAINT base_imagehashtag_pkey PRIMARY KEY (id),
    CONSTRAINT base_imagehashtag_image_id_96133dcc_uniq UNIQUE (image_id, hashtag_id),
    CONSTRAINT base_imagehashtag_hashtag_id_0d819bb9_fk_base_hashtag_id FOREIGN KEY (hashtag_id)
        REFERENCES public.base_hashtag (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        DEFERRABLE INITIALLY DEFERRED,
    CONSTRAINT base_imagehashtag_image_id_79e99aa4_fk_base_image_id FOREIGN KEY (image_id)
        REFERENCES public.base_image (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
        DEFERRABLE INITIALLY DEFERRED
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

CREATE INDEX base_imagehashtag_e4858d5c
    ON public.base_imagehashtag USING btree
    (hashtag_id)
    TABLESPACE pg_default;

CREATE INDEX base_imagehashtag_f33175e6
    ON public.base_imagehashtag USING btree
    (image_id)
    TABLESPACE pg_default;

CREATE INDEX base_imagehashtag_imageid_hashtag_id
    ON public.base_imagehashtag USING btree
    (hashtag_id, image_id)
    TABLESPACE pg_default;

-- Table: public.base_hashtag

CREATE TABLE public.base_hashtag
(
    id integer NOT NULL DEFAULT nextval('base_hashtag_id_seq'::regclass),
    name character varying(255) COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT base_hashtag_pkey PRIMARY KEY (id),
    CONSTRAINT base_hashtag_name_key UNIQUE (name)
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

CREATE INDEX base_hashtag_name_a8f89285_like
    ON public.base_hashtag USING btree
    (name COLLATE pg_catalog."default" varchar_pattern_ops)
    TABLESPACE pg_default;

カーディナリティ:おおよそ

  • base_image:23M
  • base_hashtag:5M
  • base_imagehashtag:211M

クエリプラン

Limit  (cost=7895851.20..7895851.30 rows=40 width=36) (actual time=188165.607..188165.641 rows=40 loops=1)
   Buffers: shared hit=137658 read=1129357, temp read=652059 written=652045
   ->  Sort  (cost=7895851.20..7904963.31 rows=3644846 width=36) (actual time=188165.605..188165.618 rows=40 loops=1)
         Sort Key: (count(base_hashtag.*)) DESC
         Sort Method: top-N heapsort  Memory: 28kB
         Buffers: shared hit=137658 read=1129357, temp read=652059 written=652045
         ->  GroupAggregate  (cost=7434552.18..7780638.93 rows=3644846 width=36) (actual time=178023.985..188051.536 rows=290908 loops=1)
               Group Key: base_hashtag.id
               Buffers: shared hit=137658 read=1129357, temp read=652059 written=652045
               ->  Merge Join  (cost=7434552.18..7716854.12 rows=3644846 width=68) (actual time=178023.656..186172.736 rows=3812014 loops=1)
                     Merge Cond: (base_hashtag.id = base_imagehashtag.hashtag_id)
                     Buffers: shared hit=137658 read=1129357, temp read=652059 written=652045
                     ->  Index Scan using base_hashtag_pkey on base_hashtag  (cost=0.43..205295.87 rows=5894881 width=64) (actual time=0.014..1961.031 rows=4341714 loops=1)
                           Filter: (id <> 1)
                           Rows Removed by Filter: 1
                           Buffers: shared hit=39911
                     ->  Materialize  (cost=7433940.57..7452164.81 rows=3644847 width=8) (actual time=177776.569..181299.695 rows=4055074 loops=1)
                           Buffers: shared hit=97747 read=1129357, temp read=652059 written=652045
                           ->  Sort  (cost=7433940.57..7443052.69 rows=3644847 width=8) (actual time=177776.566..179584.317 rows=4055074 loops=1)
                                 Sort Key: base_imagehashtag.hashtag_id
                                 Sort Method: external merge  Disk: 71336kB
                                 Buffers: shared hit=97747 read=1129357, temp read=652059 written=652045
                                 ->  Hash Join  (cost=1201261.72..6937033.15 rows=3644847 width=8) (actual time=46237.509..174816.562 rows=4055074 loops=1)
                                       Hash Cond: (base_imagehashtag.image_id = base_image.id)
                                       Buffers: shared hit=97747 read=1129357, temp read=631968 written=631954
                                       ->  Seq Scan on base_imagehashtag  (cost=0.00..3256836.88 rows=211107488 width=8) (actual time=0.103..72452.659 rows=211056752 loops=1)
                                             Buffers: shared hit=16405 read=1129357
                                       ->  Hash  (cost=1194732.58..1194732.58 rows=397931 width=4) (actual time=1284.293..1284.294 rows=261208 loops=1)
                                             Buckets: 131072  Batches: 8  Memory Usage: 2174kB
                                             Buffers: shared hit=81342, temp written=667
                                             ->  Bitmap Heap Scan on base_image  (cost=180976.73..1194732.58 rows=397931 width=4) (actual time=950.553..1191.553 rows=261208 loops=1)
                                                   Recheck Cond: ((created > 1548979200) AND (created < 1551398400) AND (query_id = '566591d4-33a3-4a96-a2d9-99e7bd625c18'::uuid))
                                                   Rows Removed by Index Recheck: 179762
                                                   Heap Blocks: exact=8033 lossy=43452
                                                   Buffers: shared hit=81342
                                                   ->  BitmapAnd  (cost=180976.73..180976.73 rows=397931 width=0) (actual time=948.947..948.947 rows=0 loops=1)
                                                         Buffers: shared hit=29857
                                                         ->  Bitmap Index Scan on base_image_created_idx  (cost=0.00..32677.36 rows=1488892 width=0) (actual time=171.198..171.198 rows=1484511 loops=1)
                                                               Index Cond: ((created > 1548979200) AND (created < 1551398400))
                                                               Buffers: shared hit=5154
                                                         ->  Bitmap Index Scan on base_image_query_id_idx  (cost=0.00..148100.16 rows=6159946 width=0) (actual time=760.218..760.219 rows=6274189 loops=1)
                                                               Index Cond: (query_id = '566591d4-33a3-4a96-a2d9-99e7bd625c18'::uuid)
                                                               Buffers: shared hit=24703
 Planning time: 0.689 ms
 Execution time: 188176.901 ms

ご覧のとおり、クエリの完了には数分かかる場合があります。アプリレベルでキャッシュを使用しますが、ユーザーが一意のクエリを送信することが多いため、これは少ししか役に立ちません。これらのタイプのクエリの多くは、データベースに大きな負荷をかけるダッシュボードに入力するために同時に発生します。私は即時の応答を期待していませんが、分ではなく秒に削減できることを望んでいます。

結果を並べ替えて最高になる前に、すべてのハッシュタグカウントを実行していると想定しているため、このクエリには大量のデータが含まれていると思います。

Postgresを調整する方法についてはたくさんの情報があることはわかっていますが、誰かがこの設定で明らかに非効率的なものを見て、正しい方向に向けて実験を開始できるようになることを望んでいます。

編集

Enable_seqscan = offを設定した同じクエリ

    Limit  (cost=8386776.05..8386776.15 rows=40 width=36) (actual time=75981.675..75981.712 rows=40 loops=1)
   Buffers: shared hit=43238 read=414995, temp read=20088 written=20088
   ->  Sort  (cost=8386776.05..8395626.09 rows=3540015 width=36) (actual time=75981.674..75981.687 rows=40 loops=1)
         Sort Key: (count(base_hashtag.*)) DESC
         Sort Method: top-N heapsort  Memory: 28kB
         Buffers: shared hit=43238 read=414995, temp read=20088 written=20088
         ->  GroupAggregate  (cost=7941630.37..8274877.45 rows=3540015 width=36) (actual time=64678.453..75862.227 rows=290908 loops=1)
               Group Key: base_hashtag.id
               Buffers: shared hit=43238 read=414995, temp read=20088 written=20088
               ->  Merge Join  (cost=7941630.37..8212927.18 rows=3540015 width=68) (actual time=64678.087..73943.416 rows=3812014 loops=1)
                     Merge Cond: (base_imagehashtag.hashtag_id = base_hashtag.id)
                     Buffers: shared hit=43238 read=414995, temp read=20088 written=20088
                     ->  Sort  (cost=7939229.85..7948079.89 rows=3540015 width=8) (actual time=64541.756..66215.773 rows=4055074 loops=1)
                           Sort Key: base_imagehashtag.hashtag_id
                           Sort Method: external merge  Disk: 71344kB
                           Buffers: shared hit=35902 read=375789, temp read=20088 written=20088
                           ->  Merge Join  (cost=34646.33..7457355.98 rows=3540015 width=8) (actual time=58023.450..61805.075 rows=4055074 loops=1)
                                 Merge Cond: (base_image.id = base_imagehashtag.image_id)
                                 Buffers: shared hit=35902 read=375789
                                 ->  Index Only Scan using base_image_country_created_imageid_query_id on base_image  (cost=0.56..268172.32 rows=384264 width=4) (actual time=217.707..832.100 rows=261208 loops=1)
                                       Index Cond: ((query_id = '566591d4-33a3-4a96-a2d9-99e7bd625c18'::uuid) AND (created > 1548979200) AND (created < 1551398400))
                                       Heap Fetches: 0
                                       Buffers: shared hit=35724 read=9321
                                 ->  Index Only Scan using base_imagehashtag_image_id_96133dcc_uniq on base_imagehashtag  (cost=0.57..6621360.73 rows=212655744 width=8) (actual time=3.366..34853.999 rows=113968400 loops=1)
                                       Heap Fetches: 0
                                       Buffers: shared hit=178 read=366468
                     ->  Index Scan using base_hashtag_pkey on base_hashtag  (cost=0.43..205820.87 rows=5910767 width=64) (actual time=0.041..3822.781 rows=7862820 loops=1)
                           Filter: (id <> 1)
                           Rows Removed by Filter: 1
                           Buffers: shared hit=7336 read=39206
 Planning time: 0.609 ms
 Execution time: 75991.874 ms

編集2

Enable_mergejoin = offおよびenable_hashjoin = offを使用した同じクエリ

Limit  (cost=44144721.02..44144721.12 rows=40 width=36) (actual time=25487.908..25487.942 rows=40 loops=1)
   Buffers: shared hit=17297009 read=83, temp read=88580 written=88580
   ->  Sort  (cost=44144721.02..44154290.01 rows=3827596 width=36) (actual time=25487.906..25487.915 rows=40 loops=1)
         Sort Key: (count(base_hashtag.*)) DESC
         Sort Method: top-N heapsort  Memory: 28kB
         Buffers: shared hit=17297009 read=83, temp read=88580 written=88580
         ->  GroupAggregate  (cost=43947180.16..44023732.08 rows=3827596 width=36) (actual time=21791.076..25377.264 rows=290908 loops=1)
               Group Key: base_hashtag.id
               Buffers: shared hit=17297009 read=83, temp read=88580 written=88580
               ->  Sort  (cost=43947180.16..43956749.15 rows=3827596 width=68) (actual time=21790.924..23315.027 rows=3812014 loops=1)
                     Sort Key: base_hashtag.id
                     Sort Method: external merge  Disk: 259024kB
                     Buffers: shared hit=17297009 read=83, temp read=88580 written=88580
                     ->  Nested Loop  (cost=1.56..43214685.68 rows=3827596 width=68) (actual time=2.965..17525.982 rows=3812014 loops=1)
                           Buffers: shared hit=17297009 read=83
                           ->  Nested Loop  (cost=1.13..15358809.52 rows=3827597 width=8) (actual time=2.508..3727.578 rows=4055074 loops=1)
                                 Buffers: shared hit=1060857 read=42
                                 ->  Index Only Scan using base_image_queryid_created_id on base_image  (cost=0.56..17668.67 rows=417605 width=4) (actual time=0.018..85.571 rows=261208 loops=1)
                                       Index Cond: ((query_id = '566591d4-33a3-4a96-a2d9-99e7bd625c18'::uuid) AND (created > 1548979200) AND (created < 1551398400))
                                       Heap Fetches: 0
                                       Buffers: shared hit=4205
                                 ->  Index Only Scan using base_imagehashtag_image_id_96133dcc_uniq on base_imagehashtag  (cost=0.57..28.74 rows=800 width=8) (actual time=0.002..0.006 rows=16 loops=261208)
                                       Index Cond: (image_id = base_image.id)
                                       Heap Fetches: 0
                                       Buffers: shared hit=1056652 read=42
                           ->  Index Scan using base_hashtag_pkey on base_hashtag  (cost=0.43..7.27 rows=1 width=64) (actual time=0.002..0.002 rows=1 loops=4055074)
                                 Index Cond: (id = base_imagehashtag.hashtag_id)
                                 Filter: (id <> 1)
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=16236152 read=41
 Planning time: 0.507 ms
 Execution time: 25527.201 ms
2
TimSpEdge

キャッシュされたクエリ結果を使用するつもりなので、1つの可能性は、すべてのbase_image.query_idの値を一度に事前計算するマテリアライズドビューを作成することです。それらすべての値を一度に計算することは、単一の値に対してそれを実行することよりも多くの時間を費やすべきではないようです。しかし、これは、base_image.createdが比較される値が安定しているか、少なくとも予測可能なパターンに従う場合にのみ機能します。だから次のようなもの:

_create materialized view foobar as SELECT
    base_image.query_id,
    base_hashtag.*,
    COUNT(DISTINCT base_image.id) as dist_count
FROM
    base_hashtag
    JOIN base_imagehashtag ON base_hashtag.id = base_imagehashtag.hashtag_id
    JOIN base_image ON base_imagehashtag.image_id = base_image.id
WHERE
    base_image.created > 1548979200
    AND base_image.created < 1551398400
    AND base_hashtag.id <> 1
GROUP BY
    base_image.query_id,
    base_hashtag.id
_

クエリ自体に固執するために、work_memがこのタイプのクエリには低すぎるようです。これは両方で示されます

バケット:131072バッチ:8メモリ使用量:2174kB

そして

ヒープブロック:exact = 8033lossy = 43452

単一のバッチでハッシュを実行できるようにwork_memを増やす余裕があり、ビットマップスキャンに損失のあるブロックがない場合、それはいくつかの助けになるでしょう。 (ただし、ビットマップスキャンはそもそもボトルネックの一部ではないため、大きな助けにはなりません)。

インデックスon base_image (query_id, created, id)は、BitmapAndを回避し、インデックスのみのスキャンを実行することで少し役立つ場合がありますが、「base_image」からデータを取得するのが最も遅い手順ではないため、これは非常に役立ちます。

大規模な「base_imagehashtag上のSeq Scan」を削除せずに大幅な改善を実現できるとは思いません。 _set enable_seqscan=off_の後にクエリのEXPLAIN(ANALYZE、BUFFERS)を確認して、それが何を選択するかを確認します。たとえそれが遅くなったとしても、それが何をしているかを見るといくつかの手掛かりが得られるかもしれません。

また、9.6からv11にアップグレードすると、クエリの並列実行が可能になります。効率は向上しませんが、同時に複数のクエリがない場合はレイテンシが向上します。 db.t2.largeを使用しますが、それはあまり役に立ちません。私の経験では、この状況ではハイパースレッディングは役に立たないので、2 vCPUはおそらく1 CPUを意味するだけなので、並列化するものはありません。


Enable_seqscan = offで実行した場合の実行を見ると、テーブル「base_imagehashtag」全体のスキャンから、インデックスの1つ全体のスキャンに切り替わります。そしてそれは改善でしたが、今ではそのインデックス全体をスキャンすることがボトルネックになっているため、大きな改善ではありません。

インデックスのマージ結合ではなく、そのインデックスに対するネストされたループが見られることを望んでいました。 _set enable_mergejoin=off_の後に別のexplain (analyze, buffers)を表示できますか?それはenable_seqscanよりも私が見たかったものをより的確にターゲットとするでしょう。ネストされたループは、コールドキャッシュで実行するとパフォーマンスが低下する危険がありますが、ホットキャッシュでのパフォーマンスははるかに優れている可能性があります。インデックス全体をスキャンすることはまだ非常に遅く、ボトルネックであるため、ハードウェアのアップグレードまたはデータ表現のリファクタリングのいずれかに頼る前に、入れ子になったループが唯一残されたオプションだと思います。

本番環境で180秒プランではなく75秒プランを使用する場合は、いくつかの方法があります。 1つは、アプリケーション/フレームワーク/抽象化レイヤーがこの柔軟性を提供する場合、クエリを実行する直前にenable_seqscan = offを設定し、後でリセットすることです。根本的な問題を修正するのは良いことですが、時にはあなたはあなたの戦いを選び、より簡単な方法を取り除かなければならないことがあります。

疑わしい行の1つは次の行です。

-> base_imagehashtagのbase_imagehashtag_image_id_96133dcc_uniqを使用したインデックスのみのスキャン(コスト= 0.57..6621360.73行= 212655744幅= 8)(実際の時間= 3.366..34853.999行= 13968400ループ= 1)

なぜそれが期待した半分の行数を見つけるのですか?この見積もりが正しい場合、プランナはこのプランをより早く見つけることができるため、トリックに頼らずにこのプランを使用する必要があります。あなたの統計は最新のものですか?ここで私が考えることができる他の唯一のことは、指定された「query_id」と「created」の値に対するbase_image_country_created_imageid_query_idのスキャンは、base_image.id値の全範囲の下半分からのbase_image.idの値のみを返したため、マージ結合の他の半分は早く終了しました。しかし、これが事実である場合、プランナーにもっと良い仕事をさせるためにその事実を利用する方法がわかりません。さらに、実際のパフォーマンスは、クエリで使用された正確なパラメーター値に依存します。

Seqスキャンよりもフルインデックススキャンを推奨する別の方法は、「c​​pu_Tuple_cost」と比較して「cpu_index_Tuple_cost」の値を下げることだと思います。とにかく、 "cpu_index_Tuple_cost"のデフォルト設定はとにかく高いと思われています。計画を切り替えるには、「cpu_index_Tuple_cost」をどれだけ下げる必要がありますか?

1
jjanes