3

postgresqlとpsycopg2を使用して小さな在庫システムを構築しました。コンテンツの集計された要約/レポートを作成したい場合を除いて、すべてがうまく機能しますが、count()と並べ替えのためにパフォーマンスが非常に悪くなります。

DBスキーマは次のとおりです。

CREATETABLEホスト
((
        idシリアル主キー、
        名前VARCHAR(255)
);
CREATETABLEアイテム
((
        idシリアル主キー、
        説明テキスト
);
CREATE TABLE host_item
((
        idシリアル主キー、
        host INTEGER REFERENCES hosts(id)ON DELETE CASCADE ON UPDATE CASCADE、
        item INTEGER REFERENCES items(id)ON DELETE CASCADE ON UPDATECASCADE
);

他にもいくつかのフィールドがありますが、それらは関係ありません。

2つの異なるレポートを抽出したい:-アイテム数が多いすべてのホストのリスト、カウントの高いものから低いものへ-ホストの数が多いすべてのアイテムのリスト、カウントの高いものから低いものへの順序

私は目的のために2つのクエリを使用しました:

ホスト数のあるアイテム:

SELECT i.id、i.description、COUNT(hi.id)AS count
アイテムからi
LEFT JOIN host_item AS hi
オン(i.id = hi.item)
GROUP BY i.id
ORDER BY count DESC
制限10;

アイテム数のあるホスト:

SELECT h.id、h.name、COUNT(hi.id)AS count
ホストからASh
LEFT JOIN host_item AS hi
オン(h.id = hi.host)
GROUP BY h.id
ORDER BY count DESC
制限10;

問題は、データを返す前にクエリが5〜6秒間実行されることです。これはWebベースのアプリケーションであるため、6秒は受け入れられません。データベースには、約5万のホスト、1000のアイテム、および400 000のホスト/アイテムの関係が大量に存在し、アプリケーションを使用する場合(または使用する場合)に大幅に増加する可能性があります。

遊んだ後、「ORDER BY count DESC」の部分を削除することで、両方のクエリが遅延なく即座に実行されることがわかりました(クエリを完了するのに20ミリ秒未満)。

これらのクエリを最適化して、遅延なく結果を並べ替えることができる方法はありますか?さまざまなインデックスを試していましたが、カウントが計算されるので、これにインデックスを利用することができます。postgresqlでのcount()の実行が遅いことを読みましたが、問題の原因となっている並べ替えです...

私の現在の回避策は、上記のクエリを1時間ごとのジョブとして実行し、結果を新しいテーブルに入れて、カウント列にインデックスを付けてすばやく検索することです。

私はPostgresql9.2を使用しています。

更新:注文どおりのクエリプラン:)

EXPLAIN ANALYZE
SELECT h.id, h.name, COUNT(hi.id) AS count
FROM hosts AS h
LEFT JOIN host_item AS hi
ON (h.id=hi.host)
GROUP BY h.id
ORDER BY count DESC
LIMIT 10;


 Limit  (cost=699028.97..699028.99 rows=10 width=21) (actual time=5427.422..5427.424 rows=10 loops=1)
   ->  Sort  (cost=699028.97..699166.44 rows=54990 width=21) (actual time=5427.415..5427.416 rows=10 loops=1)
         Sort Key: (count(hi.id))
         Sort Method: top-N heapsort  Memory: 25kB
         ->  GroupAggregate  (cost=613177.95..697840.66 rows=54990 width=21) (actual time=3317.320..5416.440 rows=54990 loops=1)
               ->  Merge Left Join  (cost=613177.95..679024.94 rows=3653163 width=21) (actual time=3317.267..5025.999 rows=3653163 loops=1)
                     Merge Cond: (h.id = hi.host)
                     ->  Index Scan using hosts_pkey on hosts h  (cost=0.00..1779.16 rows=54990 width=17) (actual time=0.012..15.693 rows=54990 loops=1)
                     ->  Materialize  (cost=613177.95..631443.77 rows=3653163 width=8) (actual time=3317.245..4370.865 rows=3653163 loops=1)
                           ->  Sort  (cost=613177.95..622310.86 rows=3653163 width=8) (actual time=3317.199..3975.417 rows=3653163 loops=1)
                                 Sort Key: hi.host
                                 Sort Method: external merge  Disk: 64288kB
                                 ->  Seq Scan on host_item hi  (cost=0.00..65124.63 rows=3653163 width=8) (actual time=0.006..643.257 rows=3653163 loops=1)
 Total runtime: 5438.248 ms





EXPLAIN ANALYZE
SELECT h.id, h.name, COUNT(hi.id) AS count
FROM hosts AS h
LEFT JOIN host_item AS hi
ON (h.id=hi.host)
GROUP BY h.id
LIMIT 10;


 Limit  (cost=0.00..417.03 rows=10 width=21) (actual time=0.136..0.849 rows=10 loops=1)
   ->  GroupAggregate  (cost=0.00..2293261.13 rows=54990 width=21) (actual time=0.134..0.845 rows=10 loops=1)
         ->  Merge Left Join  (cost=0.00..2274445.41 rows=3653163 width=21) (actual time=0.040..0.704 rows=581 loops=1)
               Merge Cond: (h.id = hi.host)
               ->  Index Scan using hosts_pkey on hosts h  (cost=0.00..1779.16 rows=54990 width=17) (actual time=0.015..0.021 rows=11 loops=1)
               ->  Index Scan Backward using idx_host_item_host on host_item hi  (cost=0.00..2226864.24 rows=3653163 width=8) (actual time=0.005..0.438 rows=581 loops=1)
 Total runtime: 1.143 ms

更新:この質問に対するすべての回答は、Postgresがどのように機能するかを学び理解するのに非常に役立ちます。この問題に対する明確な解決策はないようですが、あなたが提供してくれたすべての優れた回答に本当に感謝しています。Postgresqlでの今後の作業でそれらを使用します。たくさんの人に感謝します!

4

4 に答える 4

3

@GordonLinoff が言うように、関連するデータベースに関係なく、これらのクエリは遅くなりますが、その理由を知っておくと役に立ちます。データベースがこのクエリを実行する方法を検討してください。

SELECT table1.*, count(*)
FROM table1
JOIN table2 ON table2.id1 = table1.id
GROUP BY table1.id

table2ほとんどの行のデータが含まれており、両方のテーブルが自明でないサイズであると仮定するとtable1、リレーショナル データベースは次のようになる傾向があります。

  • をスキャンtable2し、 の集計を計算して、結果セットid1を生成します。{ id1, count }
  • スキャンしtable1ます。
  • ハッシュ結合。

を追加しても追加ORDER BY countしなくても、作業量は実質的に変わりません。まだ 2 つのテーブル スキャンと がありJOIN、ソート ステップが追加されただけです。にインデックスを追加しようとするかもしれませんがtable2 (id1)、改善できるのは集計手順だけです。2 つのテーブル全体を読み取る代わりに、1 つのテーブル全体とインデックス全体を読み取ることになります。喜び。

一方または両方のテーブルのインデックスを使用してほとんどの行を考慮から除外できる場合は、必ずそうしてください。そうしないと、操作は常に 2 回のスキャンに要約され、データ セットが大きくなるにつれて、パフォーマンスが低下します。

ちなみに、これはORDER BYクエリで the を削除した場合の効果です。句を残すことで、LIMITPostgreSQL に最初の N 行のみに関心があることを伝えました。これは、から N 行を選択table1し、ネストされたループを実行できることを意味します。table2内のこれらの N 行のそれぞれについて、その ID のインデックスを使用してその特定の ID を検索しtable1ますcount(*)table2これにより、はるかに高速になります。ほとんどを除外しましたtable2

アプリケーションで通常、関連付けられたレコードの数が必要な場合、通常の解決策は自分でカウンターを維持することです。1 つの規則 ( Railsや他のいくつかの ORMでネイティブにサポートされています) は、 にtable2_count列を追加することtable1です。このカウンターにインデックスを付けると、ORDER BY ... LIMITクエリのパフォーマンスが大幅に向上します。

ツールがそのままではこれを実行できない場合、またはさまざまなツール セットを使用してこのデータベースを操作している場合は、トリガーを使用することをお勧めします。@GordonLinoff が示唆しているように、これを別のサマリー テーブルに入れることができますJOINtable2_count最初に列を追加し、table1パフォーマンス測定が勝利を示した場合にのみ分割することをお勧めします。

于 2012-10-16T16:52:28.780 に答える
3

@Gordon と @willglynn は、クエリが遅い理由について多くの有用な背景を提供してくれました。

回避策は、カウンターをテーブルに追加し、itemsそれらhostsを最新の状態に保つトリガーを追加することです-書き込み操作のコストはそれほど大きくありません。
または、あなたのようにマテリアライズドビューを使用してください。私はそれを選ぶかもしれません。

そのためには、これらのクエリを定期的に実行する必要があり、改善することができます。最初のものを次のように書き換えます。

SELECT id, i.description, hi.ct
FROM   items i
JOIN  (
    SELECT item AS id, count(*) AS ct
    FROM   host_item
    GROUP  BY item
    ORDER  BY ct DESC
    LIMIT  10
    ) hi USING (id);
  • itemstableのほとんどの行に対して table に行がある場合は、host_item最初に集計してから を集計する方が高速JOINです。@willglynn の推測に反して、これは Postgres 9.1 では自動的に最適化されません。

  • count(*)count(col)プリンシパルよりも高速でありcol、NULL にすることはできませんが同等です。(ALEFT JOINは NULL 値を導入する可能性があります。)

  • に簡略LEFT JOINJOIN。少なくとも 10 個の異なるホストが常に存在すると想定しても問題ありません。元のクエリにはあまり関係ありませんが、これは必須です。

  • テーブルのインデックスは役に立たず、残りhost_itemは PK でカバーされます。items

おそらくまだあなたのケースには十分ではありませんが、Postgres 9.1 での私のテストでは、このフォームは2 倍以上高速です。9.2 に変換する必要がEXPLAIN ANALYZEありますが、念のためにテストしてください。

于 2012-10-17T00:55:50.873 に答える
2

作成したクエリは、どのデータベースでも遅くなります。のないクエリとの比較order byは興味深いものです。速度の戻りは、インデックスが関与していることを示唆しています。その場合、インデックスからカウントを見つけることができます。

より公正な比較は、order byandlimit句なしのクエリと比較することです。そうすれば、. を使用したバージョンと同様に、すべての行が生成されorder byます。基本的に、データベース エンジンは、上位 10 行を見つけるためにすべての行を評価する必要があります。オプティマイザーは、データを並べ替える必要があるか、または他の方法を使用する必要があるかを決定します。

いくつかのオプションがあります。1 つ目は、Postgres 固有のパラメーターを変更することで、クエリのパフォーマンスを高速化できるかどうかを確認することです。たとえば、ページ キャッシュが小さすぎて拡張できる可能性があります。または、役立つ並べ替え最適化パラメーターがあるかもしれません。

第二に、定期的に実行されるジョブによって構築されたサマリーテーブルを提案することができます。少し古いデータが問題にならない場合は、これで問題ありません。

3 番目に、サマリー テーブルを作成できますが、ジョブではなくトリガーを使用してデータを入力します。データが変更されたら、さまざまなカウントを更新します。

第 4 に、他のアプローチを試すこともできます。たとえば、おそらく PostgresCOUNT(*) over ()は集計よりもウィンドウ関数を最適化します。row_number()または、集計された結果を最適化して、 order by. または、10 ではなく 1 つの値だけで生活できる場合は、MAX()それで十分です。

于 2012-10-16T16:15:45.570 に答える
1

Based on the posted plans your row-count estimates are fine and the plans look vaguely sane. Your main issue is the big sort, probably necessitated by the ORDER BY:

Sort Method: external merge Disk: 64288kB

That's going to hurt even if you have fast storage. If you're on a single hard drive or (worse) a RAID5 array, that's going to be very, very slow. That sort goes away with Erwin's updated query, but increasing work_mem is still likely to gain you some performance.

You should increase work_mem, either for this query or (less) globally to get much better performance. Try:

SET work_mem = '100MB';
SELECT your_query

and see what difference it makes.

You may also want to play with the random_page_cost and seq_page_cost parameters to see if a different balance produces cost estimates that're a better match for your environment and thus causes the planner to choose a quicker query. For relatively small amounts of data like this where most of it will be cached in RAM I'd start with something like random_page_cost = 0.22 and seq_page_cost = 0.2. You can use SET for them like you do work_mem, eg:

SET work_mem = '100MB';
SET random_page_cost = 0.22;
SET seq_page_Cost = 0.2;
SELECT your_query

Do not set work_mem that high if you're setting it in postgresql.conf and you have lots of active connections, as it's per-sort not per-query, so some queries could use several times work_mem and just a couple at once could bring the system to memory exhaustion; you need to set it low enough that each connection in max_connections can be using 2x or 3x work_mem without your system running out of memory. You can set it per-transaction with SET LOCAL, per-user with ALTER USER ... SET, per-database with ALTER DATABASE ... SET or globally in postgresql.conf.

See:

于 2012-10-17T07:56:00.330 に答える