다양한 타임 스탬프 (2 열)에서 쿼리 최적화


96

우분투 12.04에서 PostgreSQL 9.1을 사용합니다.

일정 시간 내에 레코드를 선택해야합니다. 테이블 time_limits에는 두 개의 timestamp필드와 하나의 integer속성이 있습니다. 내 실제 테이블에는이 쿼리와 관련이없는 추가 열이 있습니다.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

이 테이블은 대략 2M 레코드를 포함합니다.

다음과 같은 쿼리는 많은 시간이 걸렸습니다.

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

그래서 PK의 반대 인 또 다른 색인을 추가하려고했습니다.

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

성능이 향상되었다는 인상을 받았습니다. 테이블 중간에있는 레코드에 액세스하는 시간이 40 초에서 90 초 사이 인 것이 더 합리적입니다.

그러나 시간 범위 중간의 값은 여전히 ​​수십 초입니다. 그리고 테이블의 끝을 목표로 할 때 (시간순으로) 두 번 더.

explain analyze이 쿼리 계획을 처음으로 시도 했습니다.

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

depesz.com에서 결과를 확인하십시오.

검색을 최적화하려면 어떻게해야합니까? 당신은 모든 시간을 열 번이 타임 스탬프를 스캔하는 데 소요되는 볼 수 있습니다 id_phi로 설정됩니다 0. 타임 스탬프의 큰 스캔 (60K 행!)을 이해하지 못합니다. 기본 키로 색인을 생성하지 않았으며 idx_inversed추가 했습니까?

타임 스탬프 유형에서 다른 것으로 변경해야합니까?

GIST 및 GIN 지수에 대해 조금 읽었습니다. 사용자 정의 유형의 특정 조건에서 더 효율적일 수 있습니다. 사용 사례에 적합한 옵션입니까?


1
잘 45입니다. 왜 45ms인지 모르겠습니다. 45ms만큼 빠르면 불평조차하지 않을 것입니다 ... :-) Explain 분석의 출력에 버그 일 수 있습니다. 아니면 분석을 수행해야 할 때입니다. 던노 그러나 40/50 초가 내가 측정하는 것입니다.
Stephane Rolland

2
explain analyze출력에 보고 된 시간은 쿼리 가 서버에서 필요한 시간 입니다. 조회 45 초 정도 걸립니다 경우, 추가 시간이 62682 행의 각 행이 큰 경우 (예를 들어 긴 후 모든 쿼리를 실행하는 프로그램의 데이터베이스에서 데이터를 전송 소요되는 varchar또는 text열),이 전송 시간에 영향을 미칠 과감하게.
a_horse_with_no_name

@a_horse_with_no_name : rows=62682 rows은 플래너의 추정치 입니다. 쿼리는 0 개의 행을 반환합니다. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter

@ ErwinBrandstetter : 아, 맞아. 나는 그것을 간과했다. 그러나 여전히 Explain analysis의 출력이 실행 시간에 관한 거짓말을 본 적이 없습니다.
a_horse_with_no_name

답변:


162

Postgres 9.1 이상의 경우 :

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

대부분의 경우 인덱스의 정렬 순서는 거의 관련이 없습니다. Postgres는 실제적으로 뒤로 빠르게 스캔 할 수 있습니다. 그러나 여러 열에 대한 범위 쿼리의 경우 차이가 있습니다. 밀접하게 관련:

쿼리를 고려하십시오.

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

id_phi인덱스에서 첫 번째 열의 정렬 순서 는 관련이 없습니다. 평등 ( =) 이 확인 되었으므로 먼저 와야합니다. 니가 맞았 어. 이 관련 답변에서 더 많은 것 :

Postgres는 id_phi = 0바로 다음 단계로 넘어갈 수 있으며 일치하는 인덱스의 다음 두 열을 고려할 수 있습니다. 역 정렬 순서 ( <=, >=) 의 범위 조건으로 쿼리됩니다 . 내 색인에서 정규 행이 우선합니다. B- 트리 인덱스 1을 사용하는 가장 빠른 방법이어야합니다 .

  • 당신이 원하는 start_date_time <= something: 인덱스는 가장 빠른 타임 스탬프를 먼저가집니다.

    • 조건이 충족되면 열 3도 확인하십시오 . 첫 번째 행이 규정에 실패 할 때까지 반복 하십시오 (초고속).
  • 당신이 원하는 end_date_time >= something: 인덱스는 최신 타임 스탬프를 먼저가집니다.
    • 자격이되는 경우 첫 번째 행이 완전하지 않을 때까지 행을 페치하십시오.
      2 열의 다음 값으로 계속하십시오.

포스트 그레스 전방 스캔 할 하나 또는 역방향. 인덱스가있는 방식으로 처음 두 열과 일치하는 모든 행 을 읽은 다음 세 번째 열을 필터링 해야합니다. 색인ORDER BY 설명서를 반드시 읽으십시오 . 귀하의 질문에 아주 잘 맞습니다.

처음 두 열에서 몇 개의 행이 일치합니까? 테이블의 시간 범위의 시작에 가까운
소수 start_date_time. 그러나 테이블의 연대순 끝에 거의 모든 행이 있습니다 id_phi = 0! 따라서 시작 시간이 지날수록 성능이 저하됩니다.

플래너 견적

플래너는 rows=62682예제 쿼리를 추정 합니다. 이 중 자격이없는 사람 ( rows=0)입니다. 테이블에 대한 통계 목표를 늘리면 더 나은 추정치를 얻을 수 있습니다. 2.000.000 행의 경우 ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... 지불 할 수도 있습니다. 또는 더 높습니다. 이 관련 답변에서 더 많은 것 :

나는 당신이 id_phi(고유 한 소수의 균등하게 분포 된)를 필요로하지 않지만 타임 스탬프 (고유 한 값이 많고 고르지 않은 분포)를 필요로한다고 생각합니다 .
또한 개선 된 인덱스와 관련이 있다고 생각하지 않습니다.

CLUSTER / pg_repack

더 빨리 원한다면 테이블의 실제 행 순서를 능률화 할 수 있습니다. 짧은 시간 동안 (예를 들어 시간 외) 독점적으로 테이블을 잠글 수있는 경우 인덱스에 따라 테이블을 다시 작성하고 행을 주문하십시오.

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

동시 액세스의 경우 독점 잠금없이 동일한 작업을 수행 할 수있는 pg_repack을 고려 하십시오 .

어느 쪽이든, 결과는 테이블에서 더 적은 블록을 읽을 필요가 있으며 모든 것이 사전 분류됩니다. 물리적 정렬 순서를 조각난 테이블에 대한 쓰기로 시간이 지남에 따라 악화되는 일회성 효과입니다.

Postgres 9.2+의 GiST 지수

1 pg 9.2+에는 범위 열에 대한 GiST 인덱스가 있습니다.

  • 이 내장되어 있습니다에 대한 범위 유형 timestamptimestamp with time zone: tsrange,tstzrange . btree 인덱스는 일반적으로과 integer같은 추가 열에 더 빠릅니다 id_phi. 더 작고 저렴하게 유지할 수 있습니다. 그러나 결합 된 인덱스를 사용하면 쿼리가 전체적으로 더 빨라질 수 있습니다.

  • 테이블 정의를 변경하거나 표현식 색인을 사용하십시오 .

  • 다중 컬럼 GiST 인덱스의 경우을 btree_gist포함하는 연산자 클래스를 제공하는 추가 모듈 (데이터베이스 당 한 번)이 설치되어 있어야합니다 integer.

트라이 펙타! 여러 열로 기능 GIST 지수 :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

사용하여 연산자 "범위를 포함"@> 지금 쿼리 :

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Postgres 9.3 이상의 SP-GiST 지수

SP-요점 - 인덱스는 쿼리 이런 종류의 더 빨리 될 수 를 제외하고 ,이 설명서를 인용 :

현재 B- 트리, GiST, GIN 및 BRIN 인덱스 유형 만 다중 열 인덱스를 지원합니다.

Postgres 12에서는 여전히 그렇습니다.
spgist인덱스를 (tsrange(...))두 번째 btree인덱스 와 결합해야합니다 (id_phi). 추가 된 오버 헤드로 인해 이것이 경쟁 할 수 있는지 확실하지 않습니다. 열에
대한 벤치 마크가있는 관련 답변 tsrange:


78
SO와 DBA에 대한 귀하의 각 답변은 실제로 부가 가치 / 경험이 높고 대부분이 가장 완전 하다는 것을 적어도 한 번만 말해야합니다 . 한 번만 말하면 : 존중!.
Stephane Rolland

1
머시 비엔! :) 그래서 더 빠른 결과를 얻었습니까?
Erwin Brandstetter

나는 엄청나게 어색한 쿼리에서 생성 된 대량 대량 복사를 끝내야하므로 프로세스를 정말 느리게 만들었습니다. 질문을하기 전에 몇 시간 동안 바뀌 었습니다. 그러나 나는 계산하고 내일 아침까지 돌리고 끝내고 새 테이블을 내일 채울 준비를하기로 결정했습니다. 작업 중에 동시에 색인을 만들려고했지만 너무 많은 액세스 (생각)로 인해 색인 생성이 잠겨 있어야합니다. 솔루션과 함께이 테스트 시간을 다시 반복하겠습니다. 또한 debian / ubuntu에서 9.2 ;-)로 업그레이드하는 방법을 살펴 보았습니다.
Stephane Rolland

2
@StephaneRolland : Explain 분석 출력에 45 밀리 초가 표시되는 반면 쿼리가 40 초 이상 걸리는 이유는 여전히 흥미로울 것입니다.
a_horse_with_no_name

1
@John : Postgres는 인덱스를 앞뒤로 이동할 수 있지만 동일한 스캔에서 방향을 변경할 수는 없습니다. 이상적으로는 노드 당 모든 규정 된 행을 먼저 (또는 마지막으로) 갖지만 최상의 결과를 얻으려면 모든 열에 대해 동일한 정렬 (일치하는 쿼리 술어)이어야합니다.
Erwin Brandstetter

5

그러나 Erwin의 답변은 이미 포괄적입니다.

타임 스탬프의 범위 유형은 Post Davis의 Temporal 확장 기능이있는 PostgreSQL 9.1에서 사용할 수 있습니다. https://github.com/jeff-davis/PostgreSQL-Temporal

참고 : 기능이 제한되어 있습니다 (Timestamptz를 사용하며 '[)'스타일 만 겹쳐서 사용할 수 있음). 또한 PostgreSQL 9.2로 업그레이드해야 할 다른 좋은 이유가 많이 있습니다.


3

여러 열 인덱스를 다른 순서로 만들 수 있습니다.

primary key(id_phi, start_date_time,end_date_time);

여러 열 인덱스의 인덱스 순서와 관련하여 비슷한 질문을 한 번 게시했습니다 . 핵심은 검색 공간을 줄이기 위해 가장 제한적인 조건을 먼저 사용하는 것입니다.

편집 : 내 실수. 이제이 인덱스가 이미 정의되어 있습니다.


이미 두 인덱스가 있습니다. 기본 키를 제외하고 다른 키는 있지만 제안한 인덱스는 이미 존재하며 설명을 볼 때 사용되는 인덱스입니다.Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland

1

나는 빠르게 증가했다 (1 초에서 70ms로)

많은 측정과 많은 수준 ( l열) (30 초, 1m, 1h 등)의 집계가있는 테이블이 있습니다. $s시작 및 $e끝의 두 범위 범위 열이 있습니다.

시작과 종료를위한 두 개의 다중 열 인덱스를 만들었습니다.

선택 쿼리를 조정했습니다. 시작 범위가 지정된 범위에있는 범위를 선택하십시오. 또한 끝 범위가 지정된 범위에있는 범위를 선택하십시오.

Explain은 인덱스를 효율적으로 사용하는 두 개의 행 스트림을 보여줍니다.

인덱스 :

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

검색어를 선택하십시오.

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

설명:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

비결은 계획 노드에 원하는 행만 포함한다는 것입니다. 이전에는 계획 노드 all points from some point in time to the very end에서을 선택했기 때문에 수천 개의 행이 있었고 다음 노드는 불필요한 행을 제거했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.