PostgreSQL에서 DISTINCT ON 속도를 높이는 방법은 무엇입니까?


13

station_logsPostgreSQL 9.6 데이터베이스에 테이블 이 있습니다.

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

에 대해 level_sensor에 대한 마지막 값 을 얻으려고합니다 . 약 400 개의 고유 한 값과 하루에 약 20k 개의 행이 있습니다.submitted_atstation_idstation_idstation_id

색인을 작성하기 전에 :

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 고유 (비용 = 4347852.14..4450301.72 행 = 89 너비 = 20) (실제 시간 = 22202.080..27619.167 행 = 98 루프 = 1)
   -> 정렬 (비용 = 4347852.14..4399076.93 행 = 20489916 너비 = 20) (실제 시간 = 22202.077..26540.827 행 = 20489812 루프 = 1)
         정렬 키 : station_id, submission_at DESC
         정렬 방법 : 외부 병합 디스크 : 681040kB
         -> station_logs의 Seq 스캔 (비용 = 0.00..598895.16 행 = 20489916 너비 = 20) (실제 시간 = 0.023..3443.587 행 = 20489812 루프 = $
 계획 시간 : 0.072ms
 실행 시간 : 27690.644ms

색인 작성 :

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

동일한 쿼리에 대해 인덱스를 생성 한 후 :

 고유 (비용 = 0.56..2156367.51 행 = 89 폭 = 20) (실제 시간 = 0.184..16263.413 행 = 98 루프 = 1)
   -> station_logs에서 station_id__submitted_at를 사용한 인덱스 스캔 (비용 = 0.56..2105142.98 행 = 20489812 너비 = 20) (실제 시간 = 0.181..1 $
 계획 시간 : 0.206 ms
 실행 시간 : 16263.490ms

이 쿼리를 더 빠르게 만드는 방법이 있습니까? 예를 들어 1 초와 마찬가지로 16 초는 여전히 너무 큽니다.


2
몇 개의 고유 한 스테이션 ID가 있습니까? 즉, 쿼리가 몇 개의 행을 반환합니까? 그리고 어떤 Postgres 버전입니까?
ypercubeᵀᴹ

Postgre 9.6, 약 400 개의 고유 station_id 및 station_id 당 하루 약 20k 레코드
Kokizzu 2016 년

이 쿼리는 "각 station_id에 대해 submit_at에 기반한 마지막 level_sensor 값"을 리턴 합니다 . DISTINCT ON은 필요하지 않은 경우를 제외하고 무작위로 선택합니다.
philipxy 2016 년

답변:


18

400 개의 스테이션에 대해서만이 쿼리는 매우 빠릅니다.

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle here
(이 쿼리, Abelisto의 대안 및 원본에 대한 계획 비교)

EXPLAIN ANALYZEOP에서 제공 한 결과 :

 중첩 루프 (비용 = 0.56..356.65 행 = 102 폭 = 20) (실제 시간 = 0.034..0.979 행 = 98 루프 = 1)
   -> 스테이션의 Seq 스캔 (비용 = 0.00..3.02 행 = 102 너비 = 4) (실제 시간 = 0.009..0.016 행 = 102 루프 = 1)
   -> 한계 (비용 = 0.56..3.45 행 = 1 너비 = 16) (실제 시간 = 0.009..0.009 행 = 1 루프 = 102)
         -> station_logs에서 station_id__submitted_at를 사용한 인덱스 스캔 (비용 = 0.56..664062.38 행 = 230223 너비 = 16) (실제 시간 = 0.009 $
               인덱스 조건 : (station_id = s.id)
 계획 시간 : 0.542ms
 실행 시간 : 1.013ms-   !!

필요한 유일한 색인 은 다음과 같습니다 station_id__submitted_at.. UNIQUE제약은 uniq_sid_sat또한 기본적으로 작업을 수행합니다. 둘 다 유지하면 디스크 공간과 쓰기 성능이 낭비되는 것처럼 보입니다.

정의되지 않았기 때문에 쿼리에 추가 NULLS LAST했습니다 . 이상적으로 적용 가능한 경우 column에 제약 조건을 추가 하고 추가 인덱스를 삭제 하고 쿼리에서 제거하십시오 .ORDER BYsubmitted_atNOT NULLNOT NULLsubmitted_atNULLS LAST

경우 submitted_at일 수있다 NULL,이 만들어 UNIQUE현재 지수 모두 교체 지수 고유 제한 조건 :

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

치다:

이것은 관련 (일반적으로 PK) 당 하나의 행 이있는 별도의 테이블station 을 가정하고 station_id있습니다. 없는 경우 만드십시오. 다시 말하지만, 매우 빠른이 rCTE 기술과 :

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

나는 바이올린에서도 사용합니다. 비슷한 쿼리를 사용하여 station테이블을 만들지 않고 직접 작업을 해결할 수 있습니다.

자세한 지침, 설명 및 대안 :

인덱스 최적화

귀하의 쿼리는 지금 매우 빠릅니다. 여전히 읽기 성능을 최적화해야하는 경우에만 ...

joanolo commented 와 같이 인덱스 전용 스캔level_sensor 을 허용하기 위해 인덱스에 마지막 열로 추가 하는 것이 좋습니다 . 단점 : 인덱스가 더 커져 인덱스를 사용하는 모든 쿼리에 약간의 비용이 추가됩니다. 장점 : 실제로 인덱스 만 스캔하면, 쿼리는 힙 페이지를 전혀 방문하지 않아도되므로 두 배 정도 빠릅니다. 그러나 이것은 지금 매우 빠른 쿼리에 대한 실질적인 이득이 될 수 있습니다.

그러나 귀하의 경우에는 효과가 없을 것으로 예상됩니다. 당신은 언급했다 :

... 하루에 약 20k 행 station_id.

일반적으로 이는 쓰기로드가 끊어지지 않음을 나타냅니다 ( station_id5 초마다 1 개 ). 그리고 당신은 최신 행에 관심이 있습니다. 인덱스 전용 스캔은 모든 트랜잭션에 표시되는 힙 페이지 (가시성 맵의 비트가 설정 됨)에 대해서만 작동합니다. VACUUM쓰기로드를 유지하기 위해 테이블에 대해 매우 적극적인 설정 을 실행해야하며 여전히 대부분의 시간에 작동하지 않습니다. 내 가정이 정확하고 인덱스 전용 스캔이 없으면level_sensor 인덱스에 추가 하지 마십시오 .

OTOH, 내 가정이 보유하고 테이블이 증가하면 매우 큰 하는 BRIN 인덱스 힘의 도움을. 관련 :

또는 훨씬 더 전문적이고 효율적입니다. 관련없는 행을 잘라내는 최신 추가 기능에 대한 부분 인덱스 :

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

당신이하는 타임 스탬프 선택 알고 젊은 행이 존재해야합니다. 다음 WHERE과 같이 모든 쿼리에 일치 조건 을 추가해야합니다 .

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

색인과 쿼리를 수시로 적용해야합니다.
자세한 내용이 포함 된 관련 답변 :


LATERAL을 사용하면 중첩 루프 (종종)를 원할 때마다 여러 상황에서 성능이 향상됩니다.
폴 드레이퍼

6

고전적인 방법으로 시도하십시오.

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

ThreadStarter의 분석 설명

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.