web-dev-qa-db-ja.com

数百万行のPostgresQLデータベースからデータを取得すると、非常に時間がかかります

私は、ユーザーがユーザーとして登録し、ブッククラブを作成し、他の人(メンバー)を参加させるシステムに取り組んでいます。ユーザーとメンバーはすべて、クラブに本を追加したり、他のメンバーが追加した本に投票したりできます。最近、データベースのパフォーマンスを確認するために大量のデータを追加しようとしましたが、実際に気に入ったデータを実際に取得するには時間がかかることがわかりました。投票とそれに投票した会員の名前を含め、クラブのすべての本を入手したいと思います。

私のデータベースダイアグラム(dbdiagram.ioで作成 チェックアウト

Database diagram 煩わしさなくデータベースを自由にクエリするために、データ構造を見ることでGraphQLバックエンドを作成できるオープンソースサービス Hasura を使用することにしました(私はPostgresQLを使用しています) )。次のクエリを使用して、必要なデータを取得します。

_query GetBooksOfClubIncludingVotesAndMemberName {
  books(
    where: {
      club_id: {_eq: "3"}, 
      state:{_eq: 0 }
    }, 
    order_by: [
      { fallback : asc },
      { id: asc }
    ]
  ) {
    id
    isbn
    state
    votes {
      member {
        id
        name
      }
    }
  }    
}
_

もちろん、このクエリはSQLステートメントに変換されます

_SELECT
  coalesce(
    json_agg(
      "root"
      ORDER BY
        "root.pg.fallback" ASC NULLS LAST,
        "root.pg.id" ASC NULLS LAST
    ),
    '[]'
  ) AS "root"
FROM
  (
    SELECT
      row_to_json(
        (
          SELECT
            "_8_e"
          FROM
            (
              SELECT
                "_0_root.base"."id" AS "id",
                "_0_root.base"."isbn" AS "isbn",
                "_7_root.ar.root.votes"."votes" AS "votes"
            ) AS "_8_e"
        )
      ) AS "root",
      "_0_root.base"."id" AS "root.pg.id",
      "_0_root.base"."fallback" AS "root.pg.fallback"
    FROM
      (
        SELECT
          *
        FROM
          "public"."books"
        WHERE
          (
            (("public"."books"."club_id") = (('3') :: bigint))
            AND (("public"."books"."state") = (('0') :: smallint))
          )
      ) AS "_0_root.base"
      LEFT OUTER JOIN LATERAL (
        SELECT
          coalesce(json_agg("votes"), '[]') AS "votes"
        FROM
          (
            SELECT
              row_to_json(
                (
                  SELECT
                    "_5_e"
                  FROM
                    (
                      SELECT
                        "_4_root.ar.root.votes.or.member"."member" AS "member"
                    ) AS "_5_e"
                )
              ) AS "votes"
            FROM
              (
                SELECT
                  *
                FROM
                  "public"."votes"
                WHERE
                  (("_0_root.base"."id") = ("book_id"))
              ) AS "_1_root.ar.root.votes.base"
              LEFT OUTER JOIN LATERAL (
                SELECT
                  row_to_json(
                    (
                      SELECT
                        "_3_e"
                      FROM
                        (
                          SELECT
                            "_2_root.ar.root.votes.or.member.base"."id" AS "id",
                            "_2_root.ar.root.votes.or.member.base"."name" AS "name"
                        ) AS "_3_e"
                    )
                  ) AS "member"
                FROM
                  (
                    SELECT
                      *
                    FROM
                      "public"."members"
                    WHERE
                      (
                        ("_1_root.ar.root.votes.base"."member_id") = ("id")
                      )
                  ) AS "_2_root.ar.root.votes.or.member.base"
              ) AS "_4_root.ar.root.votes.or.member" ON ('true')
          ) AS "_6_root.ar.root.votes"
      ) AS "_7_root.ar.root.votes" ON ('true')
    ORDER BY
      "root.pg.fallback" ASC NULLS LAST,
      "root.pg.id" ASC NULLS LAST
  ) AS "_9_root";
_

_EXPLAIN ANALYZE_を使用してこのステートメントを実行すると、完了までに約9217ミリ秒かかったことがわかります。以下の分析応答を確認してください

_                                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=12057321.11..12057321.15 rows=1 width=32) (actual time=9151.967..9151.967 rows=1 loops=1)
   ->  Sort  (cost=12057312.92..12057313.38 rows=182 width=37) (actual time=9151.856..9151.865 rows=180 loops=1)
         Sort Key: books.fallback, books.id
         Sort Method: quicksort  Memory: 72kB
         ->  Nested Loop Left Join  (cost=66041.02..12057306.09 rows=182 width=37) (actual time=301.721..9151.490 rows=180 loops=1)
               ->  Index Scan using book_club on books  (cost=0.43..37888.11 rows=182 width=42) (actual time=249.506..304.469 rows=180 loops=1)
                     Index Cond: (club_id = '3'::bigint)
                     Filter: (state = '0'::smallint)
               ->  Aggregate  (cost=66040.60..66040.64 rows=1 width=32) (actual time=49.134..49.134 rows=1 loops=180)
                     ->  Nested Loop Left Join  (cost=0.72..66040.46 rows=3 width=32) (actual time=0.037..49.124 rows=3 loops=180)
                           ->  Index Only Scan using member_book on votes  (cost=0.43..66021.32 rows=3 width=8) (actual time=0.024..49.104 rows=3 loops=180)
                                 Index Cond: (book_id = books.id)
                                 Heap Fetches: 540
                           ->  Index Scan using members_pkey on members  (cost=0.29..6.38 rows=1 width=36) (actual time=0.005..0.005 rows=1 loops=540)
                                 Index Cond: (id = votes.member_id)
                                 SubPlan 2
                                   ->  Result  (cost=0.00..0.04 rows=1 width=32) (actual time=0.000..0.000 rows=1 loops=540)
                     SubPlan 3
                       ->  Result  (cost=0.00..0.04 rows=1 width=32) (actual time=0.000..0.000 rows=1 loops=540)
               SubPlan 1
                 ->  Result  (cost=0.00..0.04 rows=1 width=32) (actual time=0.001..0.002 rows=1 loops=180)
 Planning Time: 0.788 ms
 JIT:
   Functions: 32
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 4.614 ms, Inlining 52.818 ms, Optimization 113.442 ms, Emission 81.939 ms, Total 252.813 ms
 Execution Time: 9217.899 ms
(27 rows)
_

次のテーブルサイズ:

_   relname    | rowcount
--------------+----------
 books        |  1153800
 members      |    19230
 votes        |  3461400
 clubs        |     6410
 users        |        3
_

これには時間がかかりすぎます。以前の設計では、インデックスがなかったため、さらに遅くなりました。インデックスを追加しましたが、それだけ長く待たなければならないという事実にはまだ満足していません。データ構造などについて改善できる点はありますか?

[〜#〜] edit [〜#〜]同じ選択ステートメントですが、推奨されるようにEXPLAIN (ANALYZE, BUFFERS)を使用しています:

_                                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=12057321.11..12057321.15 rows=1 width=32) (actual time=8896.202..8896.202 rows=1 loops=1)
   Buffers: shared hit=2392279 read=9470
   ->  Sort  (cost=12057312.92..12057313.38 rows=182 width=37) (actual time=8896.097..8896.106 rows=180 loops=1)
         Sort Key: books.fallback, books.id
         Sort Method: quicksort  Memory: 72kB
         Buffers: shared hit=2392279 read=9470
         ->  Nested Loop Left Join  (cost=66041.02..12057306.09 rows=182 width=37) (actual time=222.978..8895.801 rows=180 loops=1)
               Buffers: shared hit=2392279 read=9470
               ->  Index Scan using book_club on books  (cost=0.43..37888.11 rows=182 width=42) (actual time=174.471..214.000 rows=180 loops=1)
                     Index Cond: (club_id = '3'::bigint)
                     Filter: (state = '0'::smallint)
                     Buffers: shared hit=113 read=9470
               ->  Aggregate  (cost=66040.60..66040.64 rows=1 width=32) (actual time=48.211..48.211 rows=1 loops=180)
                     Buffers: shared hit=2392166
                     ->  Nested Loop Left Join  (cost=0.72..66040.46 rows=3 width=32) (actual time=0.028..48.202 rows=3 loops=180)
                           Buffers: shared hit=2392166
                           ->  Index Only Scan using member_book on votes  (cost=0.43..66021.32 rows=3 width=8) (actual time=0.018..48.187 rows=3 loops=180)
                                 Index Cond: (book_id = books.id)
                                 Heap Fetches: 540
                                 Buffers: shared hit=2390546
                           ->  Index Scan using members_pkey on members  (cost=0.29..6.38 rows=1 width=36) (actual time=0.004..0.004 rows=1 loops=540)
                                 Index Cond: (id = votes.member_id)
                                 Buffers: shared hit=1620
                                 SubPlan 2
                                   ->  Result  (cost=0.00..0.04 rows=1 width=32) (actual time=0.000..0.000 rows=1 loops=540)
                     SubPlan 3
                       ->  Result  (cost=0.00..0.04 rows=1 width=32) (actual time=0.000..0.000 rows=1 loops=540)
               SubPlan 1
                 ->  Result  (cost=0.00..0.04 rows=1 width=32) (actual time=0.008..0.008 rows=1 loops=180)
 Planning Time: 0.400 ms
 JIT:
   Functions: 32
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 2.060 ms, Inlining 9.923 ms, Optimization 94.927 ms, Emission 68.793 ms, Total 175.702 ms
 Execution Time: 8898.360 ms
(35 rows)
_

EDIT 2:_select * from pg_prepared_xacts;_および_select * from pg_stat_activity;_を回答で提案されているように使用します。最初のステートメントは行を表示せず、2番目のステートメントでは古いxact_start時間に気づかなかったため、これは以前(昨日)に_VACUUM FULL votes_を実行した後で行われました。残念ながら_VACUUM FULL votes_を実行しても問題は解決しません。

ステートメントの出力:

_booky=# select * from pg_prepared_xacts;
 transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)

booky=# select * from pg_stat_activity;
 datid  | datname | pid | usesysid | usename  | application_name | client_addr | client_hostname | client_port |         backend_start         |          xact_start           |          query_start          |         state_change          | wait_event_type |     wait_event      | state  | backend_xid | backend_xmin |              query              |         backend_type
--------+---------+-----+----------+----------+------------------+-------------+-----------------+-------------+-------------------------------+-------------------------------+-------------------------------+-------------------------------+-----------------+---------------------+--------+-------------+--------------+---------------------------------+------------------------------
        |         |  31 |          |          |                  |             |                 |             | 2020-04-05 08:41:47.959657+00 |                               |                               |                               | Activity        | AutoVacuumMain      |        |             |              |                                 | autovacuum launcher
        |         |  33 |       10 | postgres |                  |             |                 |             | 2020-04-05 08:41:47.959964+00 |                               |                               |                               | Activity        | LogicalLauncherMain |        |             |              |                                 | logical replication launcher
 169575 | booky   |  48 |       10 | postgres | psql             |             |                 |          -1 | 2020-04-05 10:05:20.847798+00 | 2020-04-05 10:07:47.534381+00 | 2020-04-05 10:07:47.534381+00 | 2020-04-05 10:07:47.534382+00 |                 |                     | active |             |     15265333 | select * from pg_stat_activity; | client backend
        |         |  29 |          |          |                  |             |                 |             | 2020-04-05 08:41:47.959346+00 |                               |                               |                               | Activity        | BgWriterHibernate   |        |             |              |                                 | background writer
        |         |  28 |          |          |                  |             |                 |             | 2020-04-05 08:41:47.959688+00 |                               |                               |                               | Activity        | CheckpointerMain    |        |             |              |                                 | checkpointer
        |         |  30 |          |          |                  |             |                 |             | 2020-04-05 08:41:47.959501+00 |                               |                               |                               | Activity        | WalWriterMain       |        |             |              |                                 | walwriter
(6 rows)
_
1
Jason

@Lennartのおかげで、問題を修正したように見えるINDEXを追加しました。約8900ミリ秒から35ミリ秒になりました。

作成するインデックス:

CREATE INDEX IX1_VOTES ON VOTES (book_id, member_id)
0
Jason
_  ->  Index Only Scan using member_book on votes  (cost=0.43..66021.32 rows=3 width=8) (actual time=0.024..49.104 rows=3 loops=180)
         Index Cond: (book_id = books.id)
         Heap Fetches: 540
_

49.104 * 180 = 8839、これは実質的にすべての時間です。ほとんどの場合、この時間はIOになり、テーブルからランダムなページを読み取ります(_track_io_timings_をオンにしてからEXPLAIN (ANALYZE, BUFFERS)を実行すると、その決定的な答え)。

「投票」をバキュームしてヒープフェッチを取り除くと、ほぼ確実に問題が解決します。

_   ->  Index Only Scan using member_book on votes  (cost=0.43..66021.32 rows=3 width=8) (actual time=0.018..48.187 rows=3 loops=180)
           Index Cond: (book_id = books.id)
           Heap Fetches: 540
           Buffers: shared hit=2390546
_

これがVACUUMの実行後に行われた場合は、おそらく長時間実行されているトランザクションが開いたままになっているため、VACUUMが効果的に機能しなくなっています。また、540行を取得するために2,390,546バッファをヒットすることは、信じられないほど奇妙に思えます。繰り返しになりますが、これは、長時間オープンのトランザクションが原因で、インデックスやテーブルに大規模な膨張が発生している可能性があります。

_select * from pg_prepared_xacts;_は行を表示しますか? _select * from pg_stat_activity_は_xact_start_の古い時間を示していますか?どちらでもない場合は、_VACUUM FULL votes_を実行して、問題が解決するかどうかを確認できますか?

0
jjanes