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
このクエリのパフォーマンスは、より良いインデックスを追加することで改善できますか、それとも処理能力を向上させる必要がありますか?
PostgreSQLバージョン9.3.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))"
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))"
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 ;
選択リストに集計がないため、group by
はdistinct
を選択リストに入れるのとほとんど同じですよね?
それが必要な場合は、再記述クエリを使用するようにこれを書き直すと、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
セミジョインのルックアップテーブルの代わりに使用します。
133 different group_id's
しかないため、group_idにinteger
(またはsmallint
)を使用できます。ただし、8バイトにパディングすると、テーブルの残りの部分や複数列のインデックスが消費されるため、あまり購入されません。ただし、プレーンなinteger
の処理は少し高速になります。 int
とint2
の詳細。
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_id
sしかありません。したがって、時間枠内のgorup_id
ごとの最初のヒットでクエリを満足させることができます。したがって、 EXISTS
semi-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
に必要なインデックスを作成し、それを使用するようにクエリを調整します...