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.