WHERE条件とGROUP BYを使用したSQLクエリのインデックス


15

WHERE条件付きのSQLクエリに使用するインデックスと、GROUP BY現在非常に遅いインデックスを決定しようとしています。

私のクエリ:

SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id

テーブルには現在32.000.000行があります。時間枠を増やすと、クエリの実行時間が非常に長くなります。

問題のテーブルは次のようになります。

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id bigint NOT NULL
);

現在、次のインデックスがありますが、パフォーマンスはまだ遅いです。

CREATE INDEX ts_index
  ON counter
  USING btree
  (ts);

CREATE INDEX group_id_index
  ON counter
  USING btree
  (group_id);

CREATE INDEX comp_1_index
  ON counter
  USING btree
  (ts, group_id);

CREATE INDEX comp_2_index
  ON counter
  USING btree
  (group_id, ts);

クエリでEXPLAINを実行すると、次の結果が得られます。

"QUERY PLAN"
"HashAggregate  (cost=467958.16..467958.17 rows=1 width=4)"
"  ->  Index Scan using ts_index on counter  (cost=0.56..467470.93 rows=194892 width=4)"
"        Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"

サンプルデータを含むSQL Fiddle:http : //sqlfiddle.com/#!15 / 7492b/1

質問

より良いインデックスを追加することでこのクエリのパフォーマンスを改善できますか、それとも処理能力を向上させる必要がありますか?

編集1

PostgreSQLバージョン9.3.2が使用されます。

編集2

私は@Erwinの提案を試しましたEXISTS

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

しかし、残念ながら、これによってパフォーマンスが向上することはありませんでした。クエリプラン:

"QUERY PLAN"
"Nested Loop Semi Join  (cost=1607.18..371680.60 rows=113 width=4)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Bitmap Heap Scan on counter c  (cost=1607.18..158895.53 rows=60641 width=4)"
"        Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        ->  Bitmap Index Scan on comp_2_index  (cost=0.00..1592.02 rows=60641 width=0)"
"              Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

編集3

ypercubeからのLATERALクエリのクエリプラン:

"QUERY PLAN"
"Nested Loop  (cost=8.98..1200.42 rows=133 width=20)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Result  (cost=8.98..8.99 rows=1 width=0)"
"        One-Time Filter: ($1 IS NOT NULL)"
"        InitPlan 1 (returns $1)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan using comp_2_index on counter c  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        InitPlan 2 (returns $2)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan Backward using comp_2_index on counter c_1  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

group_idテーブルにはいくつの異なる値がありますか?
ypercubeᵀᴹ

133の異なるgroup_idがあります。

タイムスタンプの範囲は2011〜2014です。秒とミリ秒の両方が使用されています。

group_id数だけに興味がありますか?
アーウィンブランドステッター14年

@Erwinこの例には示されていない4番目の列のmax()および(min)にも関心があります。
uldall 14年

回答:


6

また、groupsテーブルとLATERALjoin という構造を使用する別のアイデア(SQL Serverファンの場合、これはとほぼ同じですOUTER APPLY)。サブクエリで集計を計算できるという利点があります。

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

SQL-Fiddleでのテストは、クエリがインデックスに対してインデックススキャンを実行することを示しています(group_id, ts)

同様の計画は、2つのラテラル結合を使用して作成されます。1つは最小、もう1つは最大、および2つのインライン相関サブクエリも使用します。またcounter、最小日付と最大日付以外の行全体を表示する必要がある場合にも使用できます。

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;

@ypercubeクエリのクエリプランを元の質問に追加しました。クエリは、長い時間間隔でも50ミリ秒未満で実行されます。
uldall

5

選択リストに集計がないため、これは選択リストにgroup byを入れるのとほとんど同じdistinctですよね?

必要な場合は、PostgreSQL wikiで説明されているように、これを書き換えて再帰クエリを使用することにより、comp_2_indexで高速なインデックスルックアップを取得できる場合があります

個別のgroup_idを効率的に返すビューを作成します。

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

そして、Erwinのexists準結合のルックアップテーブルの代わりにそのビューを使用します。


4

のみがあるため、group_idに(または)を133 different group_id's使用できます。ただし、8バイトにパディングすると、テーブル内の残りの部分と可能なマルチカラムインデックスが消費されるため、あまり買いません。ただし、プレーンの処理は少し速くなるはずです。詳細integersmallintintegerintint2

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@Leo:タイムスタンプは、最新のインストールでは8バイト整数として保存され、完全に高速に処理できます。詳細。

@ypercube:クエリ(group_id, ts)には条件がないため、インデックスは役に立たないgroup_id

あなたの主な問題は、処理しなければならない大量のデータです。

カウンターでts_indexを使用したインデックススキャン(cost = 0.56..467470.93 rows = 194892 width = 4)

あなたはaの存在にのみ興味がgroup_idあり、実際のカウントには興味がないことがわかります。また、133種類しかありませんgroup_id。したがって、クエリはgorup_id、時間枠内の最初のヒットごとに満たすことができます。したがって、EXISTS準結合を使用した代替クエリのこの提案:

グループのルックアップテーブルを想定:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

あなたのインデックスcomp_2_indexには、(group_id, ts)今の楽器になります。

SQL Fiddle(コメントで@ypercubeが提供するフィドル上に構築)

ここでは、クエリはのインデックスを優先していますが(ts, group_id)、これは「クラスター化された」タイムスタンプを使用したテストのセットアップが原因だと思います。インデックスを先頭に付けてtsそれについて)インデックスを削除すると、プランナーはインデックス(group_id, ts)を同様に、特に インデックスのみのスキャンで喜んで使用します。

それが機能する場合、この他の可能な改善を必要としない可能性があります。マテリアライズドビューでデータを事前集計して、行数を大幅に削減します。さらに、実際のカウントも必要な場合、これは特に意味があります。その場合、mvを更新するときに一度に多くの行を処理するコストがかかります。毎日および毎時の集計(2つの個別のテーブル)を組み合わせて、クエリをそれに適合させることもできます。

クエリの時間枠は任意ですか?それとも、主に1分/時間/日でいっぱいですか?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

必要なインデックスを作成し、counter_mvクエリを調整して使用します...


1
SQL-Fiddleで同様のことをいくつか試しましたが、1万行ありましたが、すべてがいくつかの順次スキャンを示しました。groupsテーブルを使用すると違いが生じますか?
ypercubeᵀᴹ

@ypercube:そう思う。また、ANALYZE違いが生じます。しかし、テーブルを紹介するとすぐに、インデックスはcounter使用されなくANALYZEても使用されgroupsます。ポイントは、そのテーブルがなければ、可能なgroup_idのセットを作成するためにとにかくseqscanが必要です。答えにさらに追加しました。そして、あなたのフィドルに感謝します!
アーウィンブランドステッター14年

それは変です。Postgresのオプティマイザーはクエリgroup_idに対してもインデックスを使用しないと言っていSELECT DISTINCT group_id FROM t;ますか?
ypercubeᵀᴹ

1
@ErwinBrandstetterそれも私が考えたことであり、そうでないことを知って非常に驚いた。がなければ、LIMIT 1ビットマップインデックススキャンを選択できますが、これは早期停止の恩恵を受けず、はるかに時間がかかります。(ただし、テーブルが新たにバキューム処理された場合、ビットマップスキャンよりもインデックスオンリースキャンを好む可能性があるため、表示される動作はテーブルのバキュームステータスによって異なります)。
jjanes

1
@uldall:日次集計により、行数が大幅に削減されます。これでうまくいくはずです。ただし、EXISTSクエリを試してみてください。驚くほど速いかもしれません。さらに最小/最大では機能しません。ただし、ここに一線を引くほど親切であれば、結果のパフォーマンスに興味があります。
アーウィンブランドステッター14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.