web-dev-qa-db-ja.com

PostgreSQLでGINインデックスを使用するときにORDER BYソートを高速化する方法は?

私はこのようなテーブルを持っています:

CREATE TABLE products (
  id serial PRIMARY KEY, 
  category_ids integer[],
  published boolean NOT NULL,
  score integer NOT NULL,
  title varchar NOT NULL);

製品は複数のカテゴリに属する​​ことができます。 category_ids列は、すべての製品のカテゴリのIDのリストを保持します。

典型的なクエリは次のようになります(常に単一のカテゴリを検索します):

SELECT * FROM products WHERE published
  AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;

スピードアップするには、次のインデックスを使用します。

CREATE INDEX idx_test1 ON products
  USING GIN (category_ids gin__int_ops) WHERE published;

これは、1つのカテゴリに製品が多すぎない限り、非常に役立ちます。そのカテゴリに属する​​製品はすぐに除外されますが、難しい方法(インデックスなし)で実行する必要がある並べ替え操作があります。

btree_gin拡張機能をインストールして、次のように複数列のGINインデックスを作成できるようにします。

CREATE INDEX idx_test2 ON products USING GIN (
  category_ids gin__int_ops, score, title) WHERE published;

しかしPostgresはそれをソートに使用したくないです。クエリでDESC指定子を削除しても、.

タスクを最適化するための代替アプローチは大歓迎です。


追加情報:

  • PostgreSQL 9.4、intarray拡張あり
  • 製品の総数は現在26万ですが、大幅に増加すると予想されています(最大1,000万、これはマルチテナントのeコマースプラットフォームです)
  • カテゴリー1..10000あたりの製品(最大10万まで成長する可能性があります)、平均は100未満ですが、製品の数が多いこれらのカテゴリーは、より多くのリクエストを引き付ける傾向があります

次のクエリプランは、小規模なテストシステムから取得したものです(選択したカテゴリで4680製品、表で合計20万製品)。

Limit  (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
  ->  Sort  (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
        Sort Key: score, title
        Sort Method: quicksort  Memory: 928kB
        ->  Bitmap Heap Scan on products  (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
              Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
              Heap Blocks: exact=3441
              ->  Bitmap Index Scan on idx_test2  (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
                    Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms

注#1:82 msはそれほど恐ろしく見えないかもしれませんが、それはソートバッファーがメモリに収まるためです。製品テーブルからすべての列を選択すると(SELECT * FROM ...で実際には約60列あります)、Sort Method: external merge Disk: 5696kBに移動して実行時間が倍になります。そして、それは4680製品のみです。

アクションポイント#1(メモ#1から):メモリフットプリントを削減ソート操作を行うため、少し高速化するために、フェッチ、ソートするのが賢明です。最初に製品IDを制限し、次に完全なレコードをフェッチします。

SELECT * FROM products WHERE id IN (
  SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
  ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;

これにより、Sort Method: quicksort Memory: 903kBに戻り、4680製品では約80ミリ秒になります。それでも、製品数が10万に増えると遅くなる可能性があります。

13

私は多くの実験を行いました、そしてここに私の発見があります。

GINと並べ替え

現在、GINインデックス(バージョン9.4以降) 注文を支援できません

現在PostgreSQLでサポートされているインデックスタイプのうち、Bツリーのみがソートされた出力を生成できます。他のインデックスタイプは、指定された実装依存の順序で一致する行を返します。

work_mem

この構成パラメーター を指摘してくれたChrisに感謝します。デフォルトは4MBで、レコードセットが大きい場合はwork_memから適切な値(EXPLAIN ANALYSE)はソート操作を大幅にスピードアップできます。

ALTER SYSTEM SET work_mem TO '32MB';

変更を有効にするためにサーバーを再起動し、再確認します。

SHOW work_mem;

元のクエリ

データベースに650kの製品を追加しました。いくつかのカテゴリでは最大40kの製品を保持しています。 published句を削除してクエリを少し簡略化しました。

SELECT * FROM products WHERE category_ids @> ARRAY [248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000;

Limit  (cost=2435.62..2435.62 rows=1 width=1390) (actual time=1141.254..1141.256 rows=10 loops=1)
  ->  Sort  (cost=2434.00..2435.62 rows=646 width=1390) (actual time=1115.706..1140.513 rows=30010 loops=1)
        Sort Key: score, title
        Sort Method: external merge  Disk: 29656kB
        ->  Bitmap Heap Scan on products  (cost=17.01..2403.85 rows=646 width=1390) (actual time=11.831..25.646 rows=41666 loops=1)
              Recheck Cond: (category_ids @> '{248688}'::integer[])
              Heap Blocks: exact=6471
              ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=10.140..10.140 rows=41666 loops=1)
                    Index Cond: (category_ids @> '{248688}'::integer[])
Planning time: 0.288 ms
Execution time: 1146.322 ms

ご覧のようにwork_memでは不十分だったため、Sort Method: external merge Disk: 29656kB(ここでの数値は概算値であり、メモリ内クイックソートには32MBを少し超える量が必要です)。

メモリのフットプリントを削減

並べ替えに完全なレコードを選択せず​​、IDを使用し、並べ替え、オフセット、制限を適用して、必要な10レコードだけをロードします。

SELECT * FROM products WHERE id in (
  SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 30000
) ORDER BY score DESC, title;

Sort  (cost=2444.10..2444.11 rows=1 width=1390) (actual time=707.861..707.862 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2436.05..2444.09 rows=1 width=1390) (actual time=707.764..707.803 rows=10 loops=1)
        ->  HashAggregate  (cost=2435.63..2435.64 rows=1 width=4) (actual time=707.744..707.746 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2435.62..2435.62 rows=1 width=72) (actual time=707.732..707.734 rows=10 loops=1)
                    ->  Sort  (cost=2434.00..2435.62 rows=646 width=72) (actual time=704.163..706.955 rows=30010 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: quicksort  Memory: 7396kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=11.587..35.076 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.883..9.883 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 0.682 ms
Execution time: 707.973 ms

注意 Sort Method: quicksort Memory: 7396kB。結果ははるかに良いです。

JOINと追加のBツリーインデックス

Chrisがアドバイスしたように、追加のインデックスを作成しました:

CREATE INDEX idx_test7 ON products (score DESC, title);

最初に私はこのように参加してみました:

SELECT * FROM products NATURAL JOIN
  (SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 30000) c
ORDER BY score DESC, title;

クエリプランは少し異なりますが、結果は同じです:

Sort  (cost=2444.10..2444.11 rows=1 width=1390) (actual time=700.747..700.747 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2436.05..2444.09 rows=1 width=1390) (actual time=700.651..700.690 rows=10 loops=1)
        ->  HashAggregate  (cost=2435.63..2435.64 rows=1 width=4) (actual time=700.630..700.630 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2435.62..2435.62 rows=1 width=72) (actual time=700.619..700.619 rows=10 loops=1)
                    ->  Sort  (cost=2434.00..2435.62 rows=646 width=72) (actual time=697.304..699.868 rows=30010 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: quicksort  Memory: 7396kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=10.796..32.258 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.234..9.234 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 1.015 ms
Execution time: 700.918 ms

さまざまなオフセットと製品カウントで遊んでいたため、PostgreSQLに追加のBツリーインデックスを使用させることができませんでした。

だから私は古典的な方法で行き、junction tableを作成しました:

CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id FROM products;
CREATE INDEX idx_prodcats_cat_prod_id ON prodcats (category_id, product_id);

SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 30000;

Limit  (cost=122480.06..122480.09 rows=10 width=1390) (actual time=1290.360..1290.362 rows=10 loops=1)
  ->  Sort  (cost=122405.06..122509.00 rows=41574 width=1390) (actual time=1264.250..1289.575 rows=30010 loops=1)
        Sort Key: p.score, p.title
        Sort Method: external merge  Disk: 29656kB
        ->  Merge Join  (cost=50.46..94061.13 rows=41574 width=1390) (actual time=117.746..182.048 rows=41666 loops=1)
              Merge Cond: (p.id = c.product_id)
              ->  Index Scan using products_pkey on products p  (cost=0.42..90738.43 rows=646067 width=1390) (actual time=0.034..116.313 rows=210283 loops=1)
              ->  Index Only Scan using idx_prodcats_cat_prod_id on prodcats c  (cost=0.43..1187.98 rows=41574 width=4) (actual time=0.022..7.137 rows=41666 loops=1)
                    Index Cond: (category_id = 248688)
                    Heap Fetches: 0
Planning time: 0.873 ms
Execution time: 1294.826 ms

それでもBツリーインデックスを使用していないため、結果セットは適合しませんでしたwork_mem、したがって結果は良くありません。

しかし、状況によっては、多数の製品小さなオフセットを持つことで、PostgreSQLはBツリーインデックスを使用することを決定します:

SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 300;

Limit  (cost=3986.65..4119.51 rows=10 width=1390) (actual time=264.176..264.574 rows=10 loops=1)
  ->  Nested Loop  (cost=0.98..552334.77 rows=41574 width=1390) (actual time=250.378..264.558 rows=310 loops=1)
        ->  Index Scan using idx_test7 on products p  (cost=0.55..194665.62 rows=646067 width=1390) (actual time=0.030..83.026 rows=108037 loops=1)
        ->  Index Only Scan using idx_prodcats_cat_prod_id on prodcats c  (cost=0.43..0.54 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=108037)
              Index Cond: ((category_id = 248688) AND (product_id = p.id))
              Heap Fetches: 0
Planning time: 0.585 ms
Execution time: 264.664 ms

ここでのBツリーインデックスは直接的な結果を生成しないため、これは実際には非常に論理的です。シーケンシャルスキャンのガイドとしてのみ使用されます。

GINクエリと比較してみましょう。

SELECT * FROM products WHERE id in (
  SELECT id FROM products WHERE category_ids @> ARRAY[248688]
  ORDER BY score DESC, title LIMIT 10 OFFSET 300
) ORDER BY score DESC, title;

Sort  (cost=2519.53..2519.55 rows=10 width=1390) (actual time=143.809..143.809 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2435.14..2519.36 rows=10 width=1390) (actual time=143.693..143.736 rows=10 loops=1)
        ->  HashAggregate  (cost=2434.71..2434.81 rows=10 width=4) (actual time=143.678..143.680 rows=10 loops=1)
              Group Key: products_1.id
              ->  Limit  (cost=2434.56..2434.59 rows=10 width=72) (actual time=143.668..143.670 rows=10 loops=1)
                    ->  Sort  (cost=2433.81..2435.43 rows=646 width=72) (actual time=143.642..143.653 rows=310 loops=1)
                          Sort Key: products_1.score, products_1.title
                          Sort Method: top-N heapsort  Memory: 68kB
                          ->  Bitmap Heap Scan on products products_1  (cost=17.01..2403.85 rows=646 width=72) (actual time=11.625..31.868 rows=41666 loops=1)
                                Recheck Cond: (category_ids @> '{248688}'::integer[])
                                Heap Blocks: exact=6471
                                ->  Bitmap Index Scan on idx_products_category_ids_gin  (cost=0.00..16.85 rows=646 width=0) (actual time=9.916..9.916 rows=41666 loops=1)
                                      Index Cond: (category_ids @> '{248688}'::integer[])
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
              Index Cond: (id = products_1.id)
Planning time: 0.630 ms
Execution time: 143.921 ms

GINの結果ははるかに優れています。製品の数とオフセットのさまざまな組み合わせを確認しましたが、どんな状況でもジャンクションテーブルアプローチの方が優れていました

リアルインデックスの力

PostgreSQLがソートにインデックスを完全に利用するために、すべてのクエリのWHEREパラメータとORDER BYパラメータは、単一のBツリーインデックスに存在する必要があります。これを行うために、製品からジャンクションテーブルにソートフィールドをコピーしました。

CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id, score, title FROM products;
CREATE INDEX idx_prodcats_1 ON prodcats (category_id, score DESC, title, product_id);

SELECT * FROM products WHERE id in (SELECT product_id FROM prodcats WHERE category_id=248688 ORDER BY score DESC, title LIMIT 10 OFFSET 30000) ORDER BY score DESC, title;

Sort  (cost=2149.65..2149.67 rows=10 width=1390) (actual time=7.011..7.011 rows=10 loops=1)
  Sort Key: products.score, products.title
  Sort Method: quicksort  Memory: 35kB
  ->  Nested Loop  (cost=2065.26..2149.48 rows=10 width=1390) (actual time=6.916..6.950 rows=10 loops=1)
        ->  HashAggregate  (cost=2064.83..2064.93 rows=10 width=4) (actual time=6.902..6.904 rows=10 loops=1)
              Group Key: prodcats.product_id
              ->  Limit  (cost=2064.02..2064.71 rows=10 width=74) (actual time=6.893..6.895 rows=10 loops=1)
                    ->  Index Only Scan using idx_prodcats_1 on prodcats  (cost=0.56..2860.10 rows=41574 width=74) (actual time=0.010..6.173 rows=30010 loops=1)
                          Index Cond: (category_id = 248688)
                          Heap Fetches: 0
        ->  Index Scan using products_pkey on products  (cost=0.42..8.45 rows=1 width=1390) (actual time=0.003..0.003 rows=1 loops=10)
              Index Cond: (id = prodcats.product_id)
Planning time: 0.318 ms
Execution time: 7.066 ms

そして、これは、選択したカテゴリに多数の製品があり、オフセットが大きい場合の最悪のシナリオです。 offset = 300の場合、実行時間はちょうど0.5 msです。

残念ながら、このような接合表を維持するには、追加の作業が必要です。これは、インデックス付きマテリアライズドビューを介して実行できますが、データがほとんど更新されない場合にのみ役立ちます。そのようなマテリアライズドビューの更新は非常に重い操作になるためです。

だから私は今までGINインデックスを使い続け、work_memとメモリフットプリントクエリの削減。

10

ここでは、パフォーマンスの向上に役立ついくつかの簡単なヒントを示します。私はあなたの側でほとんど苦労しない最も簡単なヒントから始めて、最初の後でより難しいヒントに移ります。

1. _work_mem_

したがって、説明プラン__Sort Method: external merge Disk: 5696kB_で報告されたソートは6 MB未満しか消費していませんが、ディスクに溢れています。 _work_mem_ファイルの_postgresql.conf_設定を大きくして、ソートがメモリに収まるようにする必要があります。

EDIT:さらに、詳細な検査により、インデックスを使用して、基準に一致する_catgory_ids_を確認した後、ビットマップインデックススキャン関連するヒープページ内から行を読み取るときに、「不可逆」になり、状態を再確認する必要があります。 postgresql.orgのこの投稿 を参照してください。 :P重要な点は、あなたの_work_mem_が低すぎることです。サーバーのデフォルト設定を調整していないと、うまく機能しません。

この修正により、基本的に時間をかける必要がなくなります。 _postgresql.conf_が1つ変更されました。その他のヒントについては、この パフォーマンスチューニングページ を参照してください。

2.スキーマの変更

したがって、スキーマ設計で_category_ids_を整数配列に非正規化することを決定しました。これにより、GINまたはGistインデックスを使用して高速アクセスを強制することができます。私の経験では、GINインデックスの選択はGistよりも読み取りの方が速いため、その場合は正しい選択をしました。ただし、GINはソートされていないインデックスです。等価述語は簡単にチェックできますが、_WHERE >_、_WHERE <_、_ORDER BY_などの操作はインデックスによって促進されません。

まともなアプローチは、多対多を指定するために使用される bridge table/junction table を使用して設計を正規化することです。データベース内の多くの関係。

この場合、多くのカテゴリと対応する整数の_category_id_ sのセットがあり、多くの製品とそれに対応する_product_id_ sがあります。 _category_id_ sの整数配列である製品テーブルの列の代わりに、この配列列をスキーマから削除し、次のようにテーブルを作成します

_CREATE TABLE join_products_categories (product_id int, category_id int);
_

次に、ブリッジテーブルの2つの列にBツリーインデックスを生成できます。

_CREATE INDEX idx_products_in_join_table ON join_products_categories (product_id);
CREATE INDEX idx_products_in_join_table ON join_products_categories (category_id);
_

私の控えめな意見ですが、これらの変更はあなたに大きな違いをもたらすかもしれません。少なくとも、まず_work_mem_を変更してみてください。

がんばって!

編集:

ソートを支援する追加のインデックスを作成します

したがって、時間の経過とともに製品ラインが拡大した場合、特定のクエリは多くの結果(数千、数万?)を返す可能性がありますが、それでも製品ライン全体のごく一部にすぎない場合があります。これらの場合、メモリ内でソートを行うとソートにかなりのコストがかかる可能性がありますが、適切に設計されたインデックスを使用してソートを支援できます。

インデックスとORDER BYについて説明しているPostgreSQLの公式ドキュメントを参照してください。

_ORDER BY_要件に一致するインデックスを作成する場合

_CREATE INDEX idx_product_sort ON products (score DESC, title);
_

その後、Postgresは最適化し、インデックスを使用するか、明示的なソートを実行する方が費用対効果が高いかどうかを判断します。 Postgresがインデックスを使用することは保証されないことに注意してください。パフォーマンスを最適化し、インデックスを使用するか、明示的にソートするかを選択します。このインデックスを作成する場合は、それを監視して、その作成を正当化するのに十分に使用されているかどうかを確認し、ほとんどの並べ替えが明示的に行われている場合は削除します。

それでも、この時点で、「費用対効果の最大の」改善はおそらくより多くの_work_mem_を使用することになりますが、インデックスがソートをサポートできる場合があります。

4
Chris