web-dev-qa-db-ja.com

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

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

私のクエリ:

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

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

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))"
15
uldall

groupsテーブルと LATERAL 結合という構造を使用する別のアイデア(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 ;
6
ypercubeᵀᴹ

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

それが必要な場合は、再記述クエリを使用するようにこれを書き直すと、comp_2_indexで高速なインデックスルックアップを取得できる場合があります PostgreSQL wikiで

個別の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セミジョインのルックアップテーブルの代わりに使用します。

5
jjanes

133 different group_id'sしかないため、group_idにinteger(またはsmallint)を使用できます。ただし、8バイトにパディングすると、テーブルの残りの部分や複数列のインデックスが消費されるため、あまり購入されません。ただし、プレーンなintegerの処理は少し高速になります。 intint2 の詳細。

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.93rows = 194892width = 4)

group_idの存在にのみ関心があり、実際の数には関心がないようです。また、133のgroup_idsしかありません。したがって、時間枠内のgorup_idごとの最初のヒットでクエリを満足させることができます。したがって、 EXISTSsemi-join を使用した代替クエリのこの提案:

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

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を更新するときにmany行を1回処理するコストがあります。毎日と毎時の集計(2つの別々のテーブル)を組み合わせて、それにクエリを適合させることもできます。

クエリの時間枠は任意ですか?または主に完全な分/時間/日ですか?

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に必要なインデックスを作成し、それを使用するようにクエリを調整します...

4