web-dev-qa-db-ja.com

数百万行のカウントクエリを高速化

製品でいっぱいのデータベースを想定しています。商品は1つのコレクションに属することができ、ユーザーによって作成されます。データベースの大まかなスケール:

  • 製品:52.000.000
  • コレクション:9.000.000
  • ユーザー:およそ9.000.000

ユーザーが持っている製品+コレクションの量、および各コレクション内の製品の量を取得しようとしています(この情報は、x日間すべて生成され、ElasticSearchで索引付けされるはずです)。

ユーザークエリについては、私は現在次のようなことをしています:

_      SELECT
        users.*,
        (SELECT
          count(*)
        FROM
          products product
        WHERE
          product.user_id = user.id
        ) AS product_count,
        (SELECT
          count(*)
        FROM
          collections collection
        WHERE
          collection.user_id = user.id
        ) AS collection_count
      FROM
        users user
_

すべての* _idフィールドにインデックスが付けられます。 Explain(analyze、verbose)を使用(削除された機密情報):

_ Limit  (cost=0.00..156500.97 rows=100 width=41) (actual time=0.064..28345.363 rows=100 loops=1)
   Output: (...), ((SubPlan 1)), ((SubPlan 2))
   ->  Seq Scan on public.users user  (cost=0.00..14549429167.11 rows=9296702 width=41) (actual time=0.064..28345.241 rows=100 loops=1)
         Output: (...), (SubPlan 1), (SubPlan 2)
         SubPlan 1
           ->  Aggregate  (cost=1415.84..1415.85 rows=1 width=0) (actual time=261.101..261.102 rows=1 loops=100)
                 Output: count(*)
                 ->  Bitmap Heap Scan on public.products product  (cost=7.32..1414.95 rows=355 width=0) (actual time=0.282..260.767 rows=382 loops=100)
                       Output: (...)
                       Recheck Cond: (product.user_id = user.id)
                       Heap Blocks: exact=32882
                       ->  Bitmap Index Scan on products_user_id_index  (cost=0.00..7.23 rows=355 width=0) (actual time=0.165..0.165 rows=382 loops=100)
                             Index Cond: (product.user_id = user.id)
         SubPlan 2
           ->  Aggregate  (cost=149.13..149.14 rows=1 width=0) (actual time=22.333..22.333 rows=1 loops=100)
                 Output: count(*)
                 ->  Index Only Scan using collections_user_id_index on public.collections collection  (cost=0.43..149.02 rows=44 width=0) (actual time=0.610..22.300 rows=28 loops=100)
                       Output: collection.user_id
                       Index Cond: (collection.user_id = user.id)
                       Heap Fetches: 2850
 Planning time: 0.214 ms
 Execution time: 28345.508 ms
_

読み取りクエリのタイミングをとるとき:

  • 制限1:0.695ms
  • 制限10:10434ms
  • LIMIT 100:150471ms

数行以上を取得するとクエリ時間が非常に遅くなるので、これを少し高速化できるかどうか疑問に思っています。

DBマシンを強化する場合、CPUを追加すると役立つでしょうか? AFAIK postgresは複数のコアでクエリを実行しないため、それがどれだけ役立つかわかりません。

(これも少し関連していますが、コレクションのcount()がインデックスのみのスキャンを使用するのに、製品はビットマップヒープスキャンを使用するのはなぜですか?)

3
dvcrn

すべてまたはほとんどのユーザーの数値を計算している間、much相関サブクエリの代わりに、結合する前にプレーンサブクエリを使用してユーザーごとのカウントを集計する方が効率的です。

SELECT u.*, p.product_count, c.collection_count
FROM   users u
LEFT   JOIN (
   SELECT user_id AS id, count(*) AS product_count
   FROM   products
   GROUP  BY 1
   ) p USING (id)
LEFT   JOIN (
   SELECT user_id  As id, count(*) AS collection_count
   FROM   collections
   GROUP  BY 1
   ) c USING (id);

EXPLAIN出力に表示されるインデックスのみのスキャンとビットマップインデックスは、行の小さなサブセット(LIMIT 100)。あなたのテストはこの点で誤解を招くものです。すべて(またはほとんど)のユーザーの数値を計算している間、インデックスは役に立ちません。順次スキャンはより高速になります。

Bitmap Heap Scanご覧のとおり、ビットマップインデックススキャンに必要な2番目の手順のみです。これは、100行だけの場合の驚きです。テーブルの統計が古くなっている、または100人のユーザーが関連する製品を多く持っているか、productsの行が高度にクラスター化されている(物理的に、つまり、1人のユーザーの複数の行が同じまたは少数のデータページに存在することを意味します)。 Postgresは、データページごとに複数の行が見つかると予想される場合にのみ、ビットマップインデックススキャンに切り替えます。これは、100ユーザーと52.000.000製品(rows=355が期待され、rows=382が見つかりました)。

関連:

4