web-dev-qa-db-ja.com

タイムスタンプの範囲(1列)でのクエリの最適化

HerokuでPostgres 9.3を使用しています。

毎日多くの挿入と更新が行われる100万以上のレコードを含む「トラフィック」テーブルがあります。このテーブル全体でさまざまな時間範囲でSUM操作を実行する必要があります。これらの呼び出しには最大40秒かかることがあり、それを改善する方法についての提案を聞きたいです。

このテーブルには次のインデックスが設定されています。

CREATE INDEX idx_traffic_partner_only ON traffic (dt_created) WHERE campaign_id IS NULL AND uuid_self <> uuid_partner;

SELECTステートメントの例を次に示します。

SELECT SUM("clicks") AS clicks, SUM("impressions") AS impressions
FROM "traffic"
WHERE "uuid_self" != "uuid_partner"
AND "campaign_id" is NULL
AND "dt_created" >= 'Sun, 29 Mar 2015 00:00:00 +0000'
AND "dt_created" <= 'Mon, 27 Apr 2015 23:59:59 +0000' 

そして、これはEXPLAIN ANALYZEです:

Aggregate  (cost=21625.91..21625.92 rows=1 width=16) (actual time=41804.754..41804.754 rows=1 loops=1)
  ->  Index Scan using idx_traffic_partner_only on traffic  (cost=0.09..20085.11 rows=308159 width=16) (actual time=1.409..41617.976 rows=302392 loops=1)
      Index Cond: ((dt_created >= '2015-03-29'::date) AND (dt_created <= '2015-04-27'::date))
Total runtime: 41804.893 ms

http://explain.depesz.com/s/gGA

この質問はSEの別の質問とよく似ていますが、1つの質問は2つの列のタイムスタンプ範囲にまたがるインデックスを使用し、そのクエリのインデックスプランナーにはかなりずれた見積もりがありました。主な提案は、並べ替えられた複数列のインデックスを作成することでしたが、単一列のインデックスの場合はあまり効果がありません。他の提案はCLUSTER/pg_repackとGistインデックスを使用することでしたが、通常のインデックスを使用するより良い解決策があるかどうかを確認したいので、まだ試していません。

タイムスタンプの範囲(2列)でのクエリの最適化

参考までに、DBでは使用されていない次のインデックスを試してみました。

INDEX idx_traffic_2 ON traffic (campaign_id, uuid_self, uuid_partner, dt_created);
INDEX idx_traffic_3 ON traffic (dt_created);
INDEX idx_traffic_4 ON traffic (uuid_self);
INDEX idx_traffic_5 ON traffic (uuid_partner);

[〜#〜] edit [〜#〜]:EXPLAIN(ANALYZE、VERBOSE、COSTS、BUFFERS)を実行し、結果は次のとおりです。

Aggregate  (cost=20538.62..20538.62 rows=1 width=8) (actual time=526.778..526.778 rows=1 loops=1)
  Output: sum(clicks), sum(impressions)
  Buffers: shared hit=47783 read=29803 dirtied=4
  I/O Timings: read=184.936
  ->  Index Scan using idx_traffic_partner_only on public.traffic  (cost=0.09..20224.74 rows=313881 width=8) (actual time=0.049..431.501 rows=302405 loops=1)
      Output: id, uuid_self, uuid_partner, impressions, clicks, dt_created... (other fields redacted)
      Index Cond: ((traffic.dt_created >= '2015-03-29'::date) AND (traffic.dt_created <= '2015-04-27'::date))
      Buffers: shared hit=47783 read=29803 dirtied=4
      I/O Timings: read=184.936
Total runtime: 526.881 ms

http://explain.depesz.com/s/7Gu6

テーブル定義:

CREATE TABLE traffic (
    id              serial,
    uuid_self       uuid not null,
    uuid_partner    uuid not null,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
)

idは主キーで、uuid_self、uuid_partner、campaign_idはすべて外部キーです。 dt_updatedフィールドはpostgres関数で更新されます。

8
Evan Appleby

ここでvery奇妙な2つのこと:

  1. クエリは、100万行以上のテーブルから30万行を選択します。 30%(または5%を超えるもの-行のサイズやその他の要因によって異なります)の場合、通常はインデックスを使用してもまったく効果がありません。 順次スキャンが表示されます。

    例外はインデックスのみのスキャンですが、ここには表示されません。複数列のインデックス @ Craigの提案 が最良のオプションですifインデックスのみのスキャンが得られます。あなたが述べたような多くの更新では、これはうまくいかないかもしれません、その場合、追加の列なしで、そしてあなたがすでに持っているインデックスだけである方が良いでしょう。あなたができるかもしれませんmakeより積極的なautovacuum設定 でテーブルのために働きます。個々のテーブルのパラメータを調整できます。

  2. Postgresがインデックスを使用するつもりである間、私は確かにその多くの行のビットマップインデックススキャンを見ると期待します、notプレーンインデックススキャン。これは通常、行のlowパーセンテージに適しています。 Postgresがデータページごとに複数のヒットを予期すると(テーブルの統計から判断すると)、通常はビットマップインデックススキャンに切り替わります。

それから判断すると、あなたのコスト設定が不十分である(そしておそらくテーブル統計も)のではないかと思います。 random_page_cost および/または cpu_index_Tuple_costtoo lowseq_page_costに対して設定した可能性があります。リンクをたどってマニュアルを読んでください。

コールドキャッシュはコメントで調べたように大きな要因であるという観察にも適合します。誰も長い間触れていないテーブル(の一部)にアクセスしているか、キャッシュが(まだ)設定されていないテストシステムで実行していますか?
それ以外の場合は、RAMを使用して、関連するデータのほとんどをDBにキャッシュできません。そのため、データが存在する場合、ランダムアクセスはシーケンシャルアクセスよりもはるかにコストがかかります。キャッシュ。実際の状況によっては、より良いクエリプランを取得するために調整が必要になる場合があります。

最初の読み取りのみの応答が遅い場合、他の1つの要因について言及する必要があります:ヒントビットPostgres Wikiの詳細 とこの関連質問をお読みください:

またはテーブルは極端に肥大化しています、その場合、インデックススキャンは意味があり、引用した前の回答の CLUSTER/pg_repack)に戻って参照します。 (または単にVACUUM FULL)VACUUM設定を調べてください。これらはmany inserts and updates every dayで重要です。

UPDATEパターンによっては、100未満のFILLFACTORも考慮します。ほとんどの場合、新しく追加された行のみを更新する場合は、低いFILLFACTERafterを設定してテーブルを圧縮します新しいページだけが更新のための小刻みな余地を保つように。

スキーマ

campaign_idは99%+ NULLであり、dt_updatedは0%NULLです。

列のシーケンスを少し調整して、行あたり8バイトを節約します(campaign_idがNULLの場合の99%の場合):

CREATE TABLE traffic (
    uuid_self       uuid not null REFERENCES ... ,
    uuid_partner    uuid not null REFERENCES ... ,
    id              serial PRIMARY KEY,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
);

詳細な説明と詳細へのリンク:

測定する:

3

大きなインデックスで大量のデータをクエリしているように見えるので、遅いです。特に問題はありません。

PostgreSQL 9.3または9.4を使用している場合は、これをソートのカバーインデックスにして、インデックスのみのスキャンが可能かどうかを確認することができます。

CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created, clicks, impressions)
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;

PostgreSQLには、真のカバーするインデックスや、単なる値であり、Bツリーの一部ではないインデックス用語のサポートがないため、これらの機能よりも速度が遅く、コストが高くなります。それでもプレーンインデックススキャンifバキュームが頻繁に実行され、可視性マップを最新の状態に保つことができます。


理想的には、PostgreSQLはインデックス内の補助データフィールドをサポートします MS-SQL Serverのようにこの構文はPostgreSQLでは機能しません) :

-- This will not work in PostgreSQL (at least 9.5)
-- it's an example of what I wish did work. Don't
-- comment to say it doesn't work.
--
CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created)
INCLUDING (clicks, impressions) -- auxillary data columns
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;
2
Craig Ringer