느슨한 인덱스 스캔 또는 PostgreSQL의 스킵 스캔 🐘🚀

에서 저는 그의 Select the Most Recent Record (of Many Items) With PostgreSQL 에서 제안한 솔루션과 이들이 YugabyteDB에 어떻게 매핑되는지 살펴보았습니다. Ryan은 DISTINCT ON(truck_id)에 대해 인덱스 건너뛰기 스캔을 투명하게 구현하는 Timescale과 함께 작업합니다. 그는 Loose indexscan으로 문서화된 재귀 CTE가 있는 PostgreSQL 대안을 언급하지만 이 접근 방식의 가장 큰 단점은 여러 데이터 열을 반환하는 것이 훨씬 더 어렵다는 것입니다. 이것은 하나의 열만 반환하는 DISTINCT를 보여줍니다.

다음은 올바른 색인이 있을 때 가장 최근 레코드(많은 항목)에 대한 가장 빠른 답변을 위해 추가 열로 작업하는 내 버전입니다.

이전 게시물(및 Ryan Booz 기사)과 동일한 테이블을 만들고 있습니다.

drop table truck_readings;

create table truck_readings (
 truck_id bigint 
 ,ts timestamptz
 ,milage int
 ,primary key(truck_id, ts)
);


여기서는 PostgreSQL과 호환되는 오픈 소스 분산 SQL 데이터베이스인 YugabyteDB를 사용하고 있습니다. 구문은 동일하지만 행은 기본적으로 첫 번째 열의 해시에 의해 배포됩니다.

몇 개의 행을 삽입합니다. 2,000대의 트럭 각각에 대해 10,000개의 판독값입니다.

with trucks as (select generate_series(1,2000) truck_id)
insert into truck_readings(truck_id, ts, milage)
select truck_id, now() - 
 interval '1 second' * generate_series(1,10000)
 , 42 from trucks;
analyze truck_readings;


이 시점에서 각 트럭에 대한 마지막 판독값을 얻기 위해 이전 게시물과 동일한 솔루션이 있습니다. 트럭 목록이 도움이 되며 트리거로 마지막 값을 유지하는 것이 하나의 효율적인 솔루션입니다.

이 게시물에서는 Index Skip Scan과 유사한 기술을 사용하지만 WITH RECURSIVE 절에 의해 SQL에서 구동됩니다. 분산되지 않은 PostgreSQL이나 YugabyteDB에서는 범위별로 샤딩할 때 다른 인덱스가 필요하지 않습니다. 그러나 해시 샤딩이거나 단순히 생성된 기본 키가 있는 경우에는 보조 인덱스가 필요합니다. 여기있어:

create index truck_last_reading on truck_readings 
( truck_id asc, ts asc) include (milage);


이 인덱스는 먼저 truck_id 로 정렬된 다음 타임스탬프인 ts 로 정렬됩니다. 여기에는 필요한 모든 열이 포함됩니다. 여기에서 각 트럭의 마지막 판독값에 대한 마일리지를 원합니다.

내 목표는 인덱스 끝에서 시작하여 마지막 트럭에 대한 마지막 판독값을 갖는 것입니다.

yugabyte=#

  select
   last_truck_last_reading.truck_id,
   last_truck_last_reading.milage
   from truck_readings last_truck_last_reading
  order by
   last_truck_last_reading.truck_id desc,
   last_truck_last_reading.ts desc
  limit 1;

 truck_id | milage
----------+--------
     2000 |     42
(1 row)


그런 다음 다음 항목으로 건너뜁니다.

yugabyte=#

  select truck_id,milage from truck_readings
  where truck_id < 2000
  order by truck_id desc, ts desc limit 1;

 truck_id | milage
----------+--------
     1999 |     42


조인과 결합하여 이전 항목에서 truck_id를 가져올 수 있습니다.

with truck_last_reading as (
  select
   last_truck_last_reading.truck_id,
   last_truck_last_reading.milage
   from truck_readings last_truck_last_reading
  order by
   last_truck_last_reading.truck_id desc,
   last_truck_last_reading.ts desc
  limit 1
)
 select
  next_truck_last_reading.truck_id,
  next_truck_last_reading.milage
 from truck_last_reading, lateral
 (
  select truck_id,milage from truck_readings
  where truck_id < truck_last_reading.truck_id
  order by truck_id desc, ts desc limit 1
 )
 as next_truck_last_reading;


재귀 ... 측면으로



마지막으로 UNION ALL을 사용하여 두 행을 모두 표시할 수 있습니다. 그리고 이것을 재귀적으로 수행하여 다음truck_id을 찾고 결과에 연결합니다.

with recursive truck_last_reading as (
 (
  select
   last_truck_last_reading.truck_id,
   last_truck_last_reading.milage
   from truck_readings last_truck_last_reading
  order by
   last_truck_last_reading.truck_id desc,
   last_truck_last_reading.ts desc
  limit 1
 )
union all
 select
  next_truck_last_reading.truck_id,
  next_truck_last_reading.milage
 from truck_last_reading, lateral
 (
  select truck_id,milage from truck_readings
  where truck_id < truck_last_reading.truck_id
  order by truck_id desc, ts desc limit 1
 )
 as next_truck_last_reading
) select * from truck_last_reading
order by truck_id,milage;


이것은 필요한 결과를 제공하는 최종 쿼리입니다(각각 ts에 대해 truck_id 순서로 마지막 읽기 및 milage 반환). 실행 계획이 최적입니다.
,

                                                                               QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort (actual time=1078.569..1078.661 rows=2000 loops=1)
   Output: truck_last_reading.truck_id, truck_last_reading.milage
   Sort Key: truck_last_reading.truck_id, truck_last_reading.milage
   Sort Method: quicksort  Memory: 142kB
   CTE truck_last_reading
     ->  Recursive Union (actual time=0.872..1075.442 rows=2000 loops=1)
           ->  Subquery Scan on "*SELECT* 1" (actual time=0.871..0.872 rows=1 loops=1)
                 Output: "*SELECT* 1".truck_id, "*SELECT* 1".milage
                 ->  Limit (actual time=0.871..0.871 rows=1 loops=1)
                       Output: last_truck_last_reading.truck_id, last_truck_last_reading.milage, last_truck_last_reading.ts
                       ->  Index Only Scan Backward using truck_last_reading on public.truck_readings last_truck_last_reading (actual time=0.870..0.870 rows=1 loops=1)
                             Output: last_truck_last_reading.truck_id, last_truck_last_reading.milage, last_truck_last_reading.ts
                             Heap Fetches: 0
           ->  Nested Loop (actual time=0.525..0.526 rows=1 loops=2000)
                 Output: truck_readings.truck_id, truck_readings.milage
                 ->  WorkTable Scan on truck_last_reading truck_last_reading_1 (actual time=0.000..0.000 rows=1 loops=2000)
                       Output: truck_last_reading_1.truck_id, truck_last_reading_1.milage
                 ->  Limit (actual time=0.524..0.525 rows=1 loops=2000)
                       Output: truck_readings.truck_id, truck_readings.milage, truck_readings.ts
                       ->  Index Only Scan Backward using truck_last_reading on public.truck_readings (actual time=0.513..0.513 rows=1 loops=2000)
                             Output: truck_readings.truck_id, truck_readings.milage, truck_readings.ts
                             Index Cond: (truck_readings.truck_id < truck_last_reading_1.truck_id)
                             Heap Fetches: 0
   ->  CTE Scan on truck_last_reading (actual time=0.874..1077.252 rows=2000 loops=1)
         Output: truck_last_reading.truck_id, truck_last_reading.milage
 Planning Time: 0.165 ms
 Execution Time: 1078.821 ms
 Peak Memory Usage: 337 kB
(28 rows)


각 반복에 대해 하나의 행만 읽히므로 작업 영역 메모리가 필요하지 않습니다. 이것은 최상의 사용을 위해 색인을 사용합니다. 각 Index Only Scan에 대해 하나의 truck_id, 먼저 truck_id의 목록을 가져올 필요가 없습니다. 인덱스에서 읽을 때마다 올바른 위치를 직접 찾고 한 행만 읽습니다. YugabyteDB는 스토리지 태블릿 서버에 LIMIT 1을 푸시합니다.

이론적으로 이 액세스는 truck_id에 대한 해시 샤딩과 값이 아닌 해시에 대한 정렬을 통해 가능해야 합니다. 목표는 다음 truck_id에 도달하는 것입니다. 그런 다음 order by yb_hash_code(truck_id) desc, truck_id desc, ts desc는 구현이 없는 동일한 비트를 사용할 수 있습니다. 필요한 경우 git 문제를 여십시오. 그러나 primary key (truck_id desc, ts desc)로 테이블을 선언하고 사전 분할하거나 테이블이 커지면 자동 분할하도록 할 수도 있습니다. 2.15.0에는 issue이 있기 때문에 버전 2.15.1에서 이 예제를 실행했습니다.

좋은 웹페이지 즐겨찾기