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있고 어떤 것도 아닌가?
Erwin Brandstetter

@Erwin 우리는 max ()와 (min)뿐만 아니라 예제에 표시되지 않은 네 번째 열에 관심이 있습니다.
uldall

답변:


6

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 개의 측면 결합을 사용하여 유사한 계획이 작성됩니다. 하나는 최소, 하나는 최대, 하나는 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 원래 질문에 쿼리에 대한 쿼리 계획을 추가했습니다. 쿼리는 많은 시간 동안 50ms 미만으로 실행됩니다.
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의 existssemi-join 에서 조회 테이블 대신 해당 뷰를 사용하십시오 .


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)에 조건이 없으므로 on on 인덱스가 도움이되지 않습니다 group_id.

당신의 가장 큰 문제는 처리되어야하는 데이터의 방대한 양이다 :

카운터에서 ts_index를 사용한 인덱스 스캔 (비용 = 0.56..467470.93 행 = 194892 너비 = 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가 제공 한 바이올린을 기반으로 함)

여기서 쿼리는 on on 인덱스를 선호 (ts, group_id)하지만 "클러스터"타임 스탬프를 사용한 테스트 설정 때문이라고 생각합니다. 당신이 선도 인덱스를 제거하면 ts( 더 그것에 대해 ), 플래너 행복에 인덱스를 사용할 (group_id, ts)뿐만 아니라 - 특히에 인덱스 만 스캔 .

이것이 효과가 있다면 다른 가능한 개선이 필요하지 않을 수도 있습니다 . 구체화 된 뷰에서 데이터를 사전 집계 하여 행 수를 대폭 줄입니다. 실제 카운트가 추가로 필요한 경우 특히 의미가 있습니다. 그런 다음 mv를 업데이트 할 때 많은 행을 한 번 처리해야합니다 . 일일 집계와 시간별 집계 (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하고 이에 맞게 쿼리를 조정하십시오 ...


1
10k 행 으로 SQL-Fiddle 에서 몇 가지 유사한 작업을 시도 했지만 모두 순차적 스캔을 보여주었습니다. 사용합니까 groups표는 차이가?
ypercubeᵀᴹ

@ ypercube : 그렇게 생각합니다. 또한 ANALYZE차이를 만듭니다. 그러나 테이블을 소개하자마자 인덱스 counter가 사용됩니다 . 포인트는 그 테이블이 없다면 가능한 group_id 세트를 빌드하기 위해 seqscan이 필요하다는 것입니다. 내 답변에 더 많이 추가했습니다. 그리고 바이올린 주셔서 감사합니다! ANALYZEgroups
Erwin Brandstetter

이상하다. Postgres의 옵티마이 저가 쿼리 group_id에도 인덱스를 사용하지 않는다고 말하고 SELECT DISTINCT group_id FROM t;있습니까?
ypercubeᵀᴹ

1
@ErwinBrandstetter 저도 그렇게 생각했습니다. 이 없으면 LIMIT 1비트 맵 인덱스 스캔을 선택할 수 있습니다. 비트 맵 인덱스 스캔은 조기 중지의 이점이없고 시간이 오래 걸립니다. 그러나 테이블이 새로 진공 청소기로 청소 된 경우 비트 맵 스캔보다 인덱스 만 스캔을 선호 할 수 있으므로 표시되는 동작은 테이블의 진공 상태에 따라 다릅니다.
jjanes

1
@uldall : 일일 집계는 행 수를 크게 줄입니다. 그 트릭을해야합니다. 그러나 EXISTS 쿼리에 시도해보십시오. 놀라 울 정도로 빠를 수도 있습니다. 최소 / 최대 동안 추가로 작동하지 않습니다. 그래도 여기에 줄을 올리는 것이 친절하다면 결과 성능에 관심이 있습니다.
Erwin Brandstetter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.