web-dev-qa-db-ja.com

時々遅い大きなテーブルでクエリをデバッグする

Postgresデータベースに裏打ちされたWeb APIを使用しており、パフォーマンスは通常非常に良好です。データベースとアプリケーションの両方のパフォーマンスを全体として監視しています。ほとんどのクエリ(およびそのためのAPI呼び出し)は100ミリ秒未満で完了しますが、場合によっては異常値があります。

ちょうど今日、API呼び出しに5,000ミリ秒以上かかり、ウォッチドッグによって終了されたというアラートを受け取りました。ログを掘り下げてから、基になるPostgresクエリが完了するまでに13かかりました(すべてが非同期であるため、APIリクエストが終了した場合でも、SQLクエリは続行されます)。

これは非常に異例であり、問​​題のあるクエリを手動で実行しても、そのようなひどいタイミングを再現することができません。それは私のために(分析を説明すると)985msで完了まで実行されます。

私の質問

これがなぜ起こったのかについての理論を定式化するために次に何を見るべきかわかりません。それは起こりませんそれは頻繁に起こりません1日に数千の類似したイベントから1日に1回か2回だけですが、それは煩わしいほど頻繁に起こります。何が欠けていますか?これをデバッグするために次に行うべきステップは何ですか?私はDBAの出身ではないので、これはおそらくばかげた質問です。

いくつかの簡単な背景と私が試したこと

これはすべて、AmazonのRDSでホストされ、m3.xlarge、プロビジョニングされたIOPS(2,000)でPostgres 9.4を実行しています。

私のテーブルの1つ、それを "詳細"と呼ぶことにしましょう。500万行近くを含み、1日25,000レコードの割合で増加しています。このテーブルは更新または削除されることはなく、挿入と選択のみが行われますが、アプリケーションの「コア」を表しています。関心のあるほとんどすべてのものがこのテーブルから読み取られます。

この特定のケースでは、このクエリにはいくつかのパラメーター(たとえば、下部の日付とID)があり、かなり大きなデータセットを参照していることを知っています。私はすでに、この特定のシナリオを985ミリ秒から20に下げるこのクエリの大幅に改善されたバージョンを開発しました。ただし、実行に1秒もかからないクエリが他にもあることを懸念しています。私、時々13秒以上かかります。

テーブル

まあ、一種の...もっと多くの列が含まれていますが、クエリにないか、インデックスがない列を削除しました。以下のクエリで使用されているすべての列、またはインデックスが添付された列が残っています。

_CREATE TABLE "public"."details" (
    "value" numeric,
    "created_at" timestamp(6) WITH TIME ZONE NOT NULL,
    "updated_at" timestamp(6) WITH TIME ZONE NOT NULL,
    "effective_date" timestamp(6) WITH TIME ZONE,
    "submission_date" timestamp(6) WITH TIME ZONE,
    "id" uuid NOT NULL,
    "form_id" uuid,
    "order_id" uuid,
    "invoice_id" uuid,
    "customer_id" uuid,
    "seq" int8 NOT NULL DEFAULT nextval('details_seq_seq'::regclass),
    "client_day" date,
    CONSTRAINT "details_pkey" PRIMARY KEY ("id") NOT DEFERRABLE INITIALLY IMMEDIATE,
    CONSTRAINT "details_id_key" UNIQUE ("id") NOT DEFERRABLE INITIALLY IMMEDIATE
)
WITH (OIDS=FALSE);
ALTER TABLE "public"."details" OWNER TO "user";
CREATE UNIQUE INDEX  "details_id_key" ON "public"."details" USING btree("id" "pg_catalog"."uuid_ops" ASC NULLS LAST);
CREATE INDEX  "details_pkey" ON "public"."details" USING btree("id" "pg_catalog"."uuid_ops" ASC NULLS LAST);
CREATE INDEX  "client_day_details" ON "public"."details" USING btree(client_day "pg_catalog"."date_ops" ASC NULLS LAST);
CREATE INDEX  "created_at_details" ON "public"."details" USING btree(created_at "pg_catalog"."timestamptz_ops" ASC NULLS LAST);
CREATE INDEX  "effective_date_details" ON "public"."details" USING btree(effective_date "pg_catalog"."timestamptz_ops" ASC NULLS LAST);
CREATE INDEX  "form_id_details" ON "public"."details" USING btree(form_id "pg_catalog"."uuid_ops" ASC NULLS LAST);
CREATE INDEX  "order_id_details" ON "public"."details" USING btree(order_id "pg_catalog"."uuid_ops" ASC NULLS LAST);
CREATE INDEX  "customer_id_details" ON "public"."details" USING btree(customer_id "pg_catalog"."uuid_ops" ASC NULLS LAST);
CREATE INDEX  "seq_updated_at_effective_date_details" ON "public"."details" USING btree(seq "pg_catalog"."int8_ops" ASC NULLS LAST, updated_at "pg_catalog"."timestamptz_ops" ASC NULLS LAST, effective_date "pg_catalog"."timestamptz_ops" ASC NULLS LAST);
ALTER TABLE "public"."details" CLUSTER ON "seq_updated_at_effective_date_details";
CREATE INDEX  "invoice_id_details" ON "public"."details" USING btree(invoice_id "pg_catalog"."uuid_ops" ASC NULLS LAST);
CREATE INDEX  "updated_attribute_vals" ON "public"."details" USING btree(updated_at "pg_catalog"."timestamptz_ops" ASC NULLS LAST);
_

クエリ

_SELECT
    "count_pages"(
        array_to_json(array_agg(t)),
        '{"limit":500,"mode":"u"}'
    ) :: text as json
from
    (
        SELECT
            "t1"."seq"
        FROM
            (
                (
                    "details" "t1"
                    JOIN "orders" "j1" ON "j1"."id" = "t1"."order_id"
                )
                JOIN "invoices" "j2" ON "j2"."id" = "j1"."invoice_id"
            )
        JOIN "accounts" "j3" ON "j3"."id" = "j2"."account_id"
        WHERE
            (
                "j3"."customer_id" = '3e6ec3ac-fcce-4698-b1a6-87140e1197ec'
                AND "j3"."id" = ANY(
                    '{"9ee9979d-bd3f-40fd-932a-b7e3c1a4b046", "a1a695f3-eee5-4654-a5f5-967192a5781b", "0b118f5e-d1a8-42d4-8c1f-719180a44b89"}'
                )
            )
        AND(
            "t1"."effective_date" >= '2016-01-28T14:56:31.000Z'
            AND "t1"."updated_at" >= '2016-02-07T21:29:50.000Z'
        )
        ORDER BY
            "t1"."seq" ASC
    ) t
_

_EXPLAIN ANALYZE_

_Aggregate  (cost=23500.37..23500.63 rows=1 width=32) (actual time=985.927..985.928 rows=1 loops=1)
  ->  Subquery Scan on t  (cost=23499.87..23500.28 rows=33 width=32) (actual time=940.274..962.487 rows=7166 loops=1)
        ->  Sort  (cost=23499.87..23499.95 rows=33 width=8) (actual time=940.248..947.794 rows=7166 loops=1)
              Sort Key: t1.seq
              Sort Method: quicksort  Memory: 528kB
              ->  Nested Loop  (cost=5.19..23499.04 rows=33 width=8) (actual time=1.964..929.479 rows=7166 loops=1)
                    ->  Nested Loop  (cost=4.76..193.87 rows=182 width=16) (actual time=0.293..11.758 rows=854 loops=1)
                          ->  Nested Loop  (cost=4.47..74.20 rows=24 width=16) (actual time=0.210..1.294 rows=85 loops=1)
                                ->  Seq Scan on accounts j3  (cost=0.00..6.64 rows=1 width=16) (actual time=0.074..0.132 rows=3 loops=1)
                                      Filter: ((customer_id = '3e6ec3ac-fcce-4698-b1a6-87140e1197ec'::uuid) AND (id = ANY ('{9ee9979d-bd3f-40fd-932a-b7e3c1a4b046,a1a695f3-eee5-4654-a5f5-967192a5781b,0b118f5e-d1a8-42d4-8c1f-719180a44b89}'::uuid[])))
                                      Rows Removed by Filter: 102
                                ->  Bitmap Heap Scan on invoices j2  (cost=4.47..67.31 rows=25 width=32) (actual time=0.062..0.294 rows=28 loops=3)
                                      Recheck Cond: (account_id = j3.id)
                                      Heap Blocks: exact=64
                                      ->  Bitmap Index Scan on account_id_invoices  (cost=0.00..4.47 rows=25 width=0) (actual time=0.043..0.043 rows=28 loops=3)
                                            Index Cond: (account_id = j3.id)
                          ->  Index Scan using invoice_id_orders on orders j1  (cost=0.29..4.91 rows=8 width=32) (actual time=0.020..0.098 rows=10 loops=85)
                                Index Cond: (invoice_id = j2.id)
                    ->  Index Scan using order_id_details on details t1  (cost=0.43..128.04 rows=1 width=24) (actual time=0.054..1.054 rows=8 loops=854)
                          Index Cond: (order_id = j1.id)
                          Filter: ((effective_date >= '2016-01-28 14:56:31+00'::timestamp with time zone) AND (updated_at >= '2016-02-07 21:29:50+00'::timestamp with time zone))
                          Rows Removed by Filter: 494
Planning time: 5.103 ms
Execution time: 986.798 ms
_

EXPLAIN (ANALYZE, BUFFERS)

_Aggregate  (cost=23500.68..23500.95 rows=1 width=32) (actual time=332.305..332.306 rows=1 loops=1)
  Buffers: shared hit=246886
  ->  Subquery Scan on t  (cost=23500.18..23500.60 rows=33 width=32) (actual time=289.528..315.790 rows=8413 loops=1)
        Buffers: shared hit=246886
        ->  Sort  (cost=23500.18..23500.27 rows=33 width=8) (actual time=289.507..298.363 rows=8413 loops=1)
              Sort Key: t1.seq
              Sort Method: quicksort  Memory: 779kB
              Buffers: shared hit=246886
              ->  Nested Loop  (cost=5.19..23499.35 rows=33 width=8) (actual time=0.275..277.738 rows=8413 loops=1)
                    Buffers: shared hit=246886
                    ->  Nested Loop  (cost=4.76..193.87 rows=182 width=16) (actual time=0.091..5.067 rows=854 loops=1)
                          Buffers: shared hit=1115
                          ->  Nested Loop  (cost=4.47..74.20 rows=24 width=16) (actual time=0.076..0.566 rows=85 loops=1)
                                Buffers: shared hit=77
                                ->  Seq Scan on accounts j3  (cost=0.00..6.64 rows=1 width=16) (actual time=0.038..0.065 rows=3 loops=1)
                                      Filter: ((customer_id = '3e6ec3ac-fcce-4698-b1a6-87140e1197ec'::uuid) AND (id = ANY ('{9ee9979d-bd3f-40fd-932a-b7e3c1a4b046,a1a695f3-eee5-4654-a5f5-967192a5781b,0b118f5e-d1a8-42d4-8c1f-719180a44b89}'::uuid[])))
                                      Rows Removed by Filter: 102
                                      Buffers: shared hit=5
                                ->  Bitmap Heap Scan on invoices j2  (cost=4.47..67.31 rows=25 width=32) (actual time=0.025..0.090 rows=28 loops=3)
                                      Recheck Cond: (account_id = j3.id)
                                      Heap Blocks: exact=64
                                      Buffers: shared hit=72
                                      ->  Bitmap Index Scan on account_id_invoices  (cost=0.00..4.47 rows=25 width=0) (actual time=0.016..0.016 rows=28 loops=3)
                                            Index Cond: (account_id = j3.id)
                                            Buffers: shared hit=8
                          ->  Index Scan using invoice_id_orders on orders j1  (cost=0.29..4.91 rows=8 width=32) (actual time=0.006..0.029 rows=10 loops=85)
                                Index Cond: (invoice_id = j2.id)
                                Buffers: shared hit=1038
                    ->  Index Scan using order_id_details on details t1  (cost=0.43..128.04 rows=1 width=24) (actual time=0.015..0.296 rows=10 loops=854)
                          Index Cond: (order_id = j1.id)
                          Filter: ((effective_date >= '2016-01-28 14:56:31+00'::timestamp with time zone) AND (updated_at >= '2016-02-07 21:29:50+00'::timestamp with time zone))
                          Rows Removed by Filter: 494
                          Buffers: shared hit=245771
Planning time: 0.897 ms
Execution time: 333.020 ms
_

以下は、「イベント」中のグラフ/チャートです。

RDS

read operations

新しい遺物

transactions

9
Jason Whitehorn

テーブル定義

最初にぶら下がっている果物:UNIQUE制約_details_id_key_は、リソースの浪費です。これは、既存のPK _details_pkey_に役立つものは何も追加しません。

これらのノイズの多いDDLステートメントはどこで入手しましたか?冗長なデフォルト句はすべてビューを曇らせます。ノイズをトリミングした後:

_CREATE TABLE public.details (
   value numeric,
   created_at timestamptz NOT NULL,
   updated_at timestamptz NOT NULL,
   effective_date timestamptz,
   submission_date timestamptz,
   id uuid NOT NULL,
   form_id uuid,
   order_id uuid,
   invoice_id uuid,
   customer_id uuid,
   seq int8 NOT NULL DEFAULT nextval('details_seq_seq'::regclass),
   client_day date,
   CONSTRAINT details_pkey PRIMARY KEY (id),
   CONSTRAINT details_id_key UNIQUE (id)  -- total waste
);_

クエリ

私はあなたのクエリにいくつかのことを言うでしょう:

  • json_agg(t)の代わりに array_to_json(array_agg(t)) を使用しますか?
  • なぜordersに参加するのですか? invoicesからdetailsに直接参加できますか?

    _JOIN invoices j2 ON j2.id = t1.invoice_id
    _

次に、(質問で)count_pages()の定義とその他の詳細を尋ねます。しかし、あなたの声明を考えると:

私はすでに、この特定のシナリオを985ミリ秒から20に下げるこのクエリの大幅に改良されたバージョンを開発しました。

...時間を無駄にしているだけかもしれません。むしろ、クエリの改善されたバージョンに基づいて別の質問をしてください-それでも必要な場合。

クエリプランが間違っている

合計時間の大部分は、Postgresが両方のクエリプランの深刻な過小評価された行数に基づいて計画を立てているネストされたループステップの1つに費やされます。

   ->ネストされたループ(cost = 5.19..23499.04 行= 33 幅= 8)
(実際の時間= 1.964..929.479 rows = 7166 loops = 1)
 
->入れ子ループ(cost = 5.19..23499.35 行= 33 幅= 8)
(実際の時間= 0.275..277.738 rows = 8413 loops = 1)

これがdetailsordersの結合です。 (私はあなたがクエリでordersを必要とすることを確信しさえしていませんまったく。) また、このネストされたループに至るまでのすべてのステップで、推定値が低くなります。

Postgresがそれだけ多くの行を期待していることを知っている場合、おそらく merge joinまたはハッシュjoinを選択します

実際の行数は2番目のクエリプランで増加していますが、見積もりは増加していません。これは、ANALYZEを十分に実行していないか、大きなINSERTでデータを追加しているだけで、まだテーブル統計には反映されていないことを示しています。クエリ内のこれらの2つの述語は、このような状況の典型的な容疑者です。

_AND t1.effective_date >= '2016-01-28T14:56:31.000Z'
AND t1.updated_at     >= '2016-02-07T21:29:50.000Z'
_

newer行を常に追加すると思いますが、autovacuumは最新の行の最新の統計に追いついていませんが、クエリはそれらだけに焦点を当てています行。これは、時間の経過とともに、または特に大きなINSERTの後にエスカレートする可能性があります。

大きなテーブルの統計

autovacuumは、挿入/更新/削除された行の割合に基づいています。デフォルト設定は少し特別な状況の罠にすることができます一定の速度で成長する大きなテーブル特にほとんどの場合、最新の追加がクエリに関連しています。

1日あたり500万行、25,000行。 10日後、autovacuum "レコン":
新しい行は5%のみ、デフォルトの_autovacuum_analyze_scale_factor_は10%です。 ANALYZEを実行する必要はありません

マニュアル:

_autovacuum_analyze_scale_factor_(_floating point_)

ANALYZEをトリガーするかどうかを決定するときに_autovacuum_analyze_threshold_に追加するテーブルサイズの割合を指定します。デフォルトは0.1(テーブルサイズの10%)です。このパラメーターは、_postgresql.conf_ファイルまたはサーバーのコマンドラインでのみ設定できます。ただし、テーブルストレージパラメータを変更することで、個々のテーブルの設定を上書きできます。

大胆な強調鉱山。 他のautovacuum設定もお読みください!

よりアグレッシブな設定 -少なくとも重要なテーブルdetailsについてはお勧めします。 テーブルごとの設定が可能です:

特定のテーブルやテーブルの特定の列でANALYZEを実行することは可能であるため、アプリケーションで必要な場合は、一部の統計を他の統計よりも頻繁に更新する柔軟性があります。

テーブルには Storage Parameters を使用します。例:

_ALTER TABLE public.details SET (autovacuum_analyze_scale_factor = 0.001);
_

関連:

そして大きなINSERTの後に手動でANALYZEを手動で実行新しいデータを使用して、新しく追加された行を含む(おそらく)負荷の高いクエリをすぐにフォローアップします。 マニュアルをもう一度引用:

デッド行を削除するために、アクティブな本番データベースを頻繁に(少なくとも毎晩)バキュームすることをお勧めします。多数の行を追加または削除した後、影響を受けるテーブルに対して_VACUUM ANALYZE_コマンドを発行することをお勧めします。これにより、すべての最近の変更の結果でシステムカタログが更新され、PostgreSQLクエリプランナーがクエリの計画においてより適切な選択を行えるようになります。

autovacuumの詳細:

インデックス

それほど重要ではありませんが、複数列のインデックスも検討します。

_CREATE INDEX details_order_id_timestamps ON public.details
   (order_id, effective_date DESC NULLS LAST, updated_at DESC NULLS LAST);
_

また、インデックスのみのスキャンも表示されません。これは、VACUUMが十分に実行されていない( ここでマニュアルを比較 )か、カバーするインデックスがないか、またはその両方を指している可能性があります。多くのインデックスのうちどれが実際に使用され、どれが欠落しているのかを確認するのにお金がかかることがあります。

5