범위 유형에서 정확한 동등성으로 인해 잘못된 쿼리 계획을 처리하는 방법은 무엇입니까?


28

tstzrange변수 에서 정확한 동등성이 필요한 업데이트를 수행하고 있습니다. ~ 1M 행이 수정되고 쿼리는 ~ 13 분이 걸립니다. 의 결과는 여기 에서 EXPLAIN ANALYZE볼 수 있으며 실제 결과는 쿼리 플래너가 추정 한 결과와 매우 다릅니다. 문제는 인덱스 스캔시 단일 행이 리턴 될 것으로 예상한다는 것입니다.t_range

이것은 범위 유형에 대한 통계가 다른 유형의 통계와 다르게 저장된다는 사실과 관련이있는 것 같습니다. 상기 찾고 pg_stats열의보기 n_distinct-1 및 기타 분야 (예를 들어 most_common_vals, most_common_freqs) 비어 있습니다.

그러나 t_range어딘가에 통계가 저장되어 있어야합니다 . t_range에서 '동일'을 사용하는 정확한 동등성 대신 매우 유사한 업데이트를 수행하는 데 약 4 분이 소요되며, 실질적으로 다른 쿼리 계획을 사용합니다 ( 여기 참조 ). 임시 테이블의 모든 행과 히스토리 테이블의 상당 부분이 사용되므로 두 번째 쿼리 계획이 의미가 있습니다. 더 중요한 것은 쿼리 플래너가에 대한 필터의 대략적인 행 수를 예측한다는 것입니다 t_range.

분포 t_range는 조금 이례적입니다. 이 테이블을 사용하여 다른 테이블의 기록 상태를 저장하고 다른 테이블의 변경 사항이 큰 덤프에서 한 번에 발생하므로의 고유 값이 많지 않습니다 t_range. 다음은 각각의 고유 한 값에 해당하는 개수입니다 t_range.

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

t_range위의 구별에 대한 카운트 가 완료되었으므로 카디널리티는 ~ 3M입니다 (이 중 ~ 1M은 두 업데이트 쿼리의 영향을받습니다).

쿼리 1이 쿼리 2보다 훨씬 성능이 떨어지는 이유는 무엇입니까? 제 경우에는 쿼리 2가 좋은 대안이지만 정확한 범위 평등이 실제로 필요한 경우 Postgres가 더 똑똑한 쿼리 계획을 사용하도록하려면 어떻게해야합니까?

인덱스가있는 테이블 정의 (관련없는 열 삭제) :

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

쿼리 1 :

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

쿼리 2 :

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1은 999753 개의 행을 업데이트하고 Q2는 999753 + 36791 = 1036544를 업데이트합니다 (즉, 임시 테이블은 시간 범위 조건과 일치하는 모든 행이 업데이트되도록합니다).

@ypercube의 의견 에 대한 응답 으로이 쿼리를 시도했습니다 .

쿼리 3 :

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

쿼리 계획과 결과 ( 여기 참조 )는 이전 두 사례 사이에서 중간 정도였습니다 (~ 6 분).

2016/02/05 편집

1.5 년 후에 더 이상 데이터에 액세스 할 수 없으므로 동일한 구조 (인덱스 없음)와 유사한 카디널리티로 테스트 테이블을 만들었습니다. jjanes의 답변 은 원인이 업데이트에 사용되는 임시 테이블의 순서 일 수 있다고 제안했습니다. track_io_timing(Amazon RDS 사용)에 액세스 할 수 없기 때문에 가설을 직접 테스트 할 수 없습니다 .

  1. 전반적인 결과는 훨씬 빨랐습니다 (여러 요인에 의해). 나는 이것이 Erwin의 대답 과 일치하는 인덱스 제거 때문이라고 추측합니다 .

  2. 이 테스트 사례에서는 쿼리 1과 쿼리 2가 모두 병합 조인을 사용했기 때문에 기본적으로 같은 시간이 걸렸습니다. 즉, Postgres가 해시 조인을 선택하게 한 원인을 트리거 할 수 없었기 때문에 Postgres가 왜 성능이 좋지 않은 해시 조인을 선택했는지 명확하지 않습니다.


1
동등 조건 (a = b)을 두 개의 "포함"조건으로 변환하면 (a @> b AND b @> a)어떻게됩니까? 계획이 변경됩니까?
ypercubeᵀᴹ

@ ypercube : 계획은 여전히 ​​최적화되지는 않지만 실질적으로 변경됩니다-내 편집 # 2를 참조하십시오.
abeboparebop

1
또 다른 아이디어는 (lower(t_range),upper(t_range))평등을 확인한 후 일반 btree 인덱스를 추가하는 것 입니다.
ypercubeᵀᴹ

답변:


9

실행 계획에서 가장 큰 시간 차이는 최상위 노드 인 UPDATE 자체에 있습니다. 이는 업데이트하는 동안 대부분의 시간이 IO로 전환됨을 나타냅니다. 다음을 사용 track_io_timing하여 쿼리를 켜고 실행하여이를 확인할 수 있습니다.EXPLAIN (ANALYZE, BUFFERS)

다른 계획은 다른 순서로 업데이트 될 행을 제시하고 있습니다. 하나는 trip_id순서이며, 다른 하나는 임시 테이블에 물리적으로 존재하는 순서입니다.

업데이트중인 테이블의 물리적 순서가 trip_id 열과 상관 관계가있는 것으로 보이며이 순서로 행을 업데이트하면 미리 읽기 / 순차적 읽기로 효율적인 IO 패턴이 발생합니다. 임시 테이블의 물리적 순서는 많은 무작위 읽기로 이어지는 것 같습니다.

order by trip_id임시 테이블을 만든 명령문에를 추가 하면 문제를 해결할 수 있습니다.

PostgreSQL은 UPDATE 작업을 계획 할 때 IO 순서의 영향을 고려하지 않습니다. (SELECT 작업과 달리 작업을 고려합니다). PostgreSQL이 더 영리한 경우 하나의 계획이보다 효율적인 순서를 생성하거나 업데이트와 하위 노드 사이에 명시 적 정렬 노드를 삽입하여 업데이트가 ctid 순서로 행을 제공하도록합니다.

PostgreSQL이 범위에서 동등 조인의 선택성을 추정하는 열악한 작업을 수행하는 것이 맞습니다. 그러나 이것은 근본적인 문제와 실질적으로 관련이 있습니다. 업데이트의 선택 부분에 대한보다 효율적인 쿼리는 우연히 더 나은 순서로 업데이트 행에 행을 공급하기 위해 발생할 수 있지만 대부분의 경우 운이 좋지 않습니다.


불행히도 나는 수정할 수 없으며 track_io_timing(1 년 반이기 때문에) 더 이상 원본 데이터에 액세스 할 수 없습니다. 그러나 동일한 스키마와 비슷한 크기 (수백만 행)의 테이블을 만들고 임시 업데이트 테이블이 원래 테이블과 같은 정렬과 다른 두 가지 업데이트를 실행하여 이론을 테스트했습니다. 거의 무작위로. 불행히도 두 업데이트는 거의 같은 시간이 걸리므로 업데이트 테이블의 순서가이 쿼리에 영향을 미치지 않습니다.
abeboparebop

7

등식 술어의 선택성이 tstzrange컬럼 의 GiST 인덱스에 의해 과도하게 과대 평가되는 이유를 정확히 모르겠습니다 . 그것은 그 자체로 흥미로 남아 있지만, 귀하의 특정 경우와 관련이없는 것 같습니다.

UPDATE모든 기존 3M 행의 3 분의 1 (!)을 수정 하므로 인덱스가 전혀 도움이되지 않습니다 . 반대로, 테이블 외에 인덱스를 점진적으로 업데이트하면에 상당한 비용이 추가 UPDATE됩니다.

간단한 쿼리 1 유지하십시오 . 간단하고 급진적 인 해결책 은 앞에 인덱스삭제하는UPDATE입니다. 다른 목적으로 필요한 경우, 다음에 다시 만드십시오 UPDATE. 이것은 큰 기간 동안 인덱스를 유지하는 것보다 여전히 빠릅니다 UPDATE.

UPDATE모든 행의 3 분의 1 에 대해서는 아마도 다른 모든 인덱스를 삭제하고 UPDATE. 유일한 단점 : 테이블에 대한 추가 권한과 독점 잠금이 필요합니다 (을 사용하는 경우 잠시만 CREATE INDEX CONCURRENTLY).

GiST 인덱스 대신 btree를 사용하는 @ypercube의 아이디어 는 원칙적으로 좋을 것 같습니다. 그러나 하지 (인덱스로 시작하는 좋은 없음), 모든 행의 3 분의 1에 하지 에 불과 (lower(t_range),upper(t_range))하기 때문에, tstzrange이산 범위 형식이 아닙니다있다.

대부분의 이산 범위 유형은 표준 형식을 가지므로 "평등"개념이 더 단순 해집니다. 표준 형식의 값의 상한 및 하한이이를 정의합니다. 설명서 :

불연속 범위 유형 에는 요소 유형에 대해 원하는 단계 크기를 인식 하는 정규화 기능이 있어야합니다. 정규화 함수는 범위 유형의 동등한 값을 동일한 표현, 특히 일관되게 포함 또는 배타적 범위로 변환합니다. 정규화 함수를 지정하지 않으면 실제로 동일한 형식의 값을 나타낼 수 있지만 형식이 다른 범위는 항상 동일하지 않은 것으로 취급됩니다.

내장 범위 유형 int4range, int8rangedaterange모두는 하한을 포함하고 상한을 제외하는 표준 형식을 사용합니다. 즉 [). 그러나 사용자 정의 범위 유형은 다른 규칙을 사용할 수 있습니다.

tstzrange상한 및 하한의 포괄 성이 동등성을 고려할 필요가있는 의 경우에는 그렇지 않다 . 가능한 btree 인덱스가 켜져 있어야합니다.

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

그리고 쿼리는 WHERE절 에서 동일한 식을 사용해야합니다 .

하나는 인덱스에 값 전체 캐스트를 유혹 할 수있다 text: (cast(t_range AS text))-하지만이 표현은하지 IMMUTABLE의 텍스트 표현하기 때문에 timestamptz값이 전류에 따라 timezone설정. IMMUTABLE표준 양식을 생성 하는 랩퍼 함수 에 추가 단계를 넣고 그에 대한 기능 색인을 작성해야합니다 ...

추가 조치 / 대안 아이디어

경우 shape_dist_traveled이미 같은 값을 가질 수 있습니다 tt.shape_dist_traveled업데이트 된 행의 몇 이상 (당신이 당신의 부작용에 의존하지 않는 UPDATE같은 트리거 ...), 당신은 빈 업데이트를 제외하여 빠르게 쿼리를 만들 수 있습니다 :

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

물론 성능 최적화에 대한 모든 일반적인 조언이 적용됩니다. Postgres Wiki는 좋은 출발점입니다.

VACUUM FULL일부 죽은 튜플 (또는에 의해 확보 된 공간 FILLFACTOR)이 UPDATE성능에 유리하기 때문에 독이 될 수 있습니다 .

업데이트 된 행이 많고 동시 액세스 또는 기타 종속성이 없어 여유가 있으면 업데이트하는 대신 완전히 새로운 테이블을 작성하는 것이 더 빠를 수 있습니다. 이 관련 답변의 지침 :

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