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


12

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

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製品、表で合計200k製品)。

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ミリ秒はそれほど恐ろしく見えないかもしれませんが、それはソートバッファーがメモリに収まるためです。製品テーブルからすべての列を選択するSELECT * FROM ...と(実際には約60列あります)、Sort Method: external merge Disk: 5696kB実行時間が2倍になります。そして、それは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万に増えると遅くなる可能性があります。


このページ:hlinnaka.iki.fi/2014/03/28/…ソートにbtree_ginを使用できないというコメントがあります。
Mladen Uzelac 2015年

はい、オプションを追加できるようにタイトルを書き換えました。
Yaroslav Stavnichiy 2015年

あなたはいつも単一のカテゴリーを探していますか?そして、いくつかのより基本的な情報を提供してください:Postgresバージョン、カーディナリティ、カテゴリごとの行(最小/平均/最大)。postgresql-performanceのタグ情報の指示を検討してください。And:scoreNULLにすることもできますがscore DESC、でではなくで並べ替えますscore DESC NULLS LAST。どちらかが正しくないようです...
Erwin Brandstetter

必要に応じて追加情報を追加しました。私は常に単一のカテゴリーを探しています。そしてscore実際にはNOT NULL-テーブルの定義を修正しました。
Yaroslav Stavnichiy 2015年

回答:


9

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

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ツリーインデックスを使用できませんでした。

だから私は古典的な方法で行き、ジャンクションテーブルを作成しました:

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です。

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

したがって、これまでのところ、work_memメモリフットプリントのクエリを増やしたり減らしたりしながら、GINインデックスを使用しています。


postgresql.confの一般設定を変更するために再起動する必要ありませリロードで十分です。また、マルチユーザー環境でグローバルに高すぎる設定(低すぎないこと)に対して警告します。より多くのクエリが必要なクエリがある場合は、- を使用するセッションのみ、またはを使用するトランザクションのみを高く設定します。参照:dba.stackexchange.com/a/48633/3684work_memwork_memwork_memSETSET LOCAL
Erwin Brandstetter 2018

すばらしい答えです。特に、ディスク->インメモリソート操作、すばやい変更のための大きな変更に感謝します。
Ricardo Villamil

4

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

1。 work_mem

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

編集:さらに、さらに詳しく調べたところ、インデックスを使用しcatgory_idsて基準に一致するものを確認した後、ビットマップインデックススキャンは強制的に「不可逆」になり、関連するヒープページ内から行を読み取るときに条件を再確認する必要があることがわかりました。 私が与えたよりも良い説明については、postgresql.orgのこの投稿参照してください。:P主なポイントは、あなたのwork_mem方法が低すぎることです。サーバーのデフォルト設定を調整していないと、うまく機能しません。

この修正により、基本的に時間をかける必要がなくなります。に1つ変更するとpostgresql.conf、オフになります。その他のヒントについては、このパフォーマンス調整ページを参照してください。

2.スキーマの変更

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

まともなアプローチは、データベースで多対多の関係を指定するために使用されるブリッジテーブル/ジャンクションテーブルを使用して設計を正規化することです。

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

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がインデックスを使用する保証ないことに注意してください。パフォーマンスを最適化し、インデックスを使用するか、明示的にソートするかを選択します。このインデックスを作成する場合は、作成を正当化するのに十分に使用されているかどうかを監視し、ほとんどのソートが明示的に行われている場合は削除します。

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


ジャンクションテーブルを使用してGINを回避することも考えていました。しかし、それがソートにどのように役立つかを指定していません。それは役に立たないと思います。GINクエリを介して収集された一連の製品IDを使用して製品テーブルを結合しようとしましたが、これは提供している結合と非常によく似ており、その操作ではスコアとタイトルにBツリーインデックスを使用できませんでした。多分私は間違ったインデックスを構築しました。詳しく説明していただけますか。
Yaroslav Stavnichiy 2015年

申し訳ありませんが、おそらく私は十分に明確に説明していませんでした。work_mem構成の変更は、「ディスクでのソート」の問題と、再確認条件の問題の修正として意図されていました。製品の数が増えると、ソートするために追加のインデックスが必要になる場合があります。詳しくは、上記の編集内容をご覧ください。
Chris
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.