이 LEFT JOIN이 LEFT JOIN LATERAL보다 성능이 나쁜 이유는 무엇입니까?


13

Sakila 데이터베이스에서 가져온 다음 테이블이 있습니다.

  • 영화 : film_id는 pkey입니다
  • 배우 : actor_id는 pkey입니다
  • film_actor : film_id 및 actor_id는 film / actor의 핵심 요소입니다.

특정 영화를 선택하고 있습니다. 이 영화에서는 모든 배우가 그 영화에 참여하기를 원합니다. 이에 대한 두 가지 쿼리가 있습니다. 하나는 a LEFT JOIN와 하나는 a LEFT JOIN LATERAL입니다.

select film.film_id, film.title, a.actors
from   film
left join
  (         
       select     film_actor.film_id, array_agg(first_name) as actors
       from       actor
       inner join film_actor using(actor_id)
       group by   film_actor.film_id
  ) as a
on       a.film_id = film.film_id
where    film.title = 'ACADEMY DINOSAUR'
order by film.title;

select film.film_id, film.title, a.actors
from   film
left join lateral
  (
       select     array_agg(first_name) as actors
       from       actor
       inner join film_actor using(actor_id)
       where      film_actor.film_id = film.film_id
  ) as a
on       true
where    film.title = 'ACADEMY DINOSAUR'
order by film.title;

쿼리 계획을 비교할 때 첫 번째 쿼리는 두 번째 쿼리보다 훨씬 나쁩니다 (20x).

 Merge Left Join  (cost=507.20..573.11 rows=1 width=51) (actual time=15.087..15.089 rows=1 loops=1)
   Merge Cond: (film.film_id = film_actor.film_id)
   ->  Sort  (cost=8.30..8.31 rows=1 width=19) (actual time=0.075..0.075 rows=1 loops=1)
     Sort Key: film.film_id
     Sort Method: quicksort  Memory: 25kB
     ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.044..0.058 rows=1 loops=1)
           Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
   ->  GroupAggregate  (cost=498.90..552.33 rows=997 width=34) (actual time=15.004..15.004 rows=1 loops=1)
     Group Key: film_actor.film_id
     ->  Sort  (cost=498.90..512.55 rows=5462 width=8) (actual time=14.934..14.937 rows=11 loops=1)
           Sort Key: film_actor.film_id
           Sort Method: quicksort  Memory: 449kB
           ->  Hash Join  (cost=6.50..159.84 rows=5462 width=8) (actual time=0.355..8.359 rows=5462 loops=1)
             Hash Cond: (film_actor.actor_id = actor.actor_id)
             ->  Seq Scan on film_actor  (cost=0.00..84.62 rows=5462 width=4) (actual time=0.035..2.205 rows=5462 loops=1)
             ->  Hash  (cost=4.00..4.00 rows=200 width=10) (actual time=0.303..0.303 rows=200 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on actor  (cost=0.00..4.00 rows=200 width=10) (actual time=0.027..0.143 rows=200 loops=1)
 Planning time: 1.495 ms
 Execution time: 15.426 ms

 Nested Loop Left Join  (cost=25.11..33.16 rows=1 width=51) (actual time=0.849..0.854 rows=1 loops=1)
   ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.045..0.048 rows=1 loops=1)
     Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
   ->  Aggregate  (cost=24.84..24.85 rows=1 width=32) (actual time=0.797..0.797 rows=1 loops=1)
     ->  Hash Join  (cost=10.82..24.82 rows=5 width=6) (actual time=0.672..0.764 rows=10 loops=1)
           Hash Cond: (film_actor.actor_id = actor.actor_id)
           ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=2) (actual time=0.072..0.150 rows=10 loops=1)
             Recheck Cond: (film_id = film.film_id)
             Heap Blocks: exact=10
             ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.041..0.041 rows=10 loops=1)
               Index Cond: (film_id = film.film_id)
           ->  Hash  (cost=4.00..4.00 rows=200 width=10) (actual time=0.561..0.561 rows=200 loops=1)
             Buckets: 1024  Batches: 1  Memory Usage: 17kB
             ->  Seq Scan on actor  (cost=0.00..4.00 rows=200 width=10) (actual time=0.039..0.275 rows=200 loops=1)
 Planning time: 1.722 ms
 Execution time: 1.087 ms

왜 이런거야? 나는 이것에 대해 추론하고 싶어서 무슨 일이 일어나고 있는지 이해하고 데이터 크기가 커질 때 쿼리가 어떻게 작동하는지, 그리고 어떤 조건에서 플래너가 어떤 결정을 내릴지를 예측할 수 있습니다.

내 생각 : 첫 번째 LEFT JOIN쿼리에서는 하나의 특정 필름에만 관심이있는 외부 쿼리의 필터링을 고려하지 않고 데이터베이스의 모든 필름에 대해 하위 쿼리가 실행되는 것처럼 보입니다. 플래너가 서브 쿼리에서 해당 지식을 가질 수없는 이유는 무엇입니까?

에서 LEFT JOIN LATERAL쿼리, 우리는 더 많거나 적은 '밀어'필터링 아래로한다는 것이다. 따라서 첫 번째 쿼리에서 발생한 문제는 여기에 없으므로 성능이 향상됩니다.

나는 주로 엄지 손가락의 규칙, 일반적인 지혜를 찾고 있다고 생각합니다 ... 그래서이 플래너 마술 은 제 2의 자연이됩니다.

업데이트 (1)

LEFT JOIN다음과 같이 다시 작성하면 성능이 향상됩니다 ( LEFT JOIN LATERAL.

select film.film_id, film.title, array_agg(a.first_name) as actors
from   film
left join
  (         
       select     film_actor.film_id, actor.first_name
       from       actor
       inner join film_actor using(actor_id)
  ) as a
on       a.film_id = film.film_id
where    film.title = 'ACADEMY DINOSAUR'
group by film.film_id
order by film.title;

 GroupAggregate  (cost=29.44..29.49 rows=1 width=51) (actual time=0.470..0.471 rows=1 loops=1)
   Group Key: film.film_id
   ->  Sort  (cost=29.44..29.45 rows=5 width=25) (actual time=0.428..0.430 rows=10 loops=1)
     Sort Key: film.film_id
     Sort Method: quicksort  Memory: 25kB
     ->  Nested Loop Left Join  (cost=4.74..29.38 rows=5 width=25) (actual time=0.149..0.386 rows=10 loops=1)
           ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.056..0.057 rows=1 loops=1)
             Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
           ->  Nested Loop  (cost=4.47..19.09 rows=200 width=8) (actual time=0.087..0.316 rows=10 loops=1)
             ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=4) (actual time=0.052..0.089 rows=10 loops=1)
               Recheck Cond: (film_id = film.film_id)
               Heap Blocks: exact=10
               ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.035..0.035 rows=10 loops=1)
                 Index Cond: (film_id = film.film_id)
             ->  Index Scan using actor_pkey on actor  (cost=0.14..0.17 rows=1 width=10) (actual time=0.011..0.011 rows=1 loops=10)
               Index Cond: (actor_id = film_actor.actor_id)
 Planning time: 1.833 ms
 Execution time: 0.706 ms

우리는 이것을 어떻게 추론 할 수 있습니까?

업데이트 (2)

나는 몇 가지 실험을 계속했고 흥미로운 경험 법칙은 집계 함수를 가능한 한 높거나 늦게 적용하는 것 입니다. 업데이트 (1)의 쿼리는 아마도 더 이상 내부 쿼리가 아닌 외부 쿼리에서 집계하기 때문에 성능이 더 좋습니다.

LEFT JOIN LATERAL위 의 내용을 다음과 같이 다시 작성하면 동일하게 적용됩니다 .

select film.film_id, film.title, array_agg(a.first_name) as actors
from   film
left join lateral
  (
       select     actor.first_name
       from       actor
       inner join film_actor using(actor_id)
       where      film_actor.film_id = film.film_id
  ) as a
on       true
where    film.title = 'ACADEMY DINOSAUR'
group by film.film_id
order by film.title;

 GroupAggregate  (cost=29.44..29.49 rows=1 width=51) (actual time=0.088..0.088 rows=1 loops=1)
   Group Key: film.film_id
   ->  Sort  (cost=29.44..29.45 rows=5 width=25) (actual time=0.076..0.077 rows=10 loops=1)
     Sort Key: film.film_id
     Sort Method: quicksort  Memory: 25kB
     ->  Nested Loop Left Join  (cost=4.74..29.38 rows=5 width=25) (actual time=0.031..0.066 rows=10 loops=1)
           ->  Index Scan using idx_title on film  (cost=0.28..8.29 rows=1 width=19) (actual time=0.010..0.010 rows=1 loops=1)
             Index Cond: ((title)::text = 'ACADEMY DINOSAUR'::text)
           ->  Nested Loop  (cost=4.47..19.09 rows=200 width=8) (actual time=0.019..0.052 rows=10 loops=1)
             ->  Bitmap Heap Scan on film_actor  (cost=4.32..18.26 rows=5 width=4) (actual time=0.013..0.024 rows=10 loops=1)
               Recheck Cond: (film_id = film.film_id)
               Heap Blocks: exact=10
               ->  Bitmap Index Scan on idx_fk_film_id  (cost=0.00..4.32 rows=5 width=0) (actual time=0.007..0.007 rows=10 loops=1)
                 Index Cond: (film_id = film.film_id)
             ->  Index Scan using actor_pkey on actor  (cost=0.14..0.17 rows=1 width=10) (actual time=0.002..0.002 rows=1 loops=10)
               Index Cond: (actor_id = film_actor.actor_id)
 Planning time: 0.440 ms
 Execution time: 0.136 ms

여기, 우리는 array_agg()위로 움직였다 . 보시다시피이 계획은 원본보다 좋습니다 LEFT JOIN LATERAL.

즉,이 자체 발명 된 경험 법칙 ( 가능하면 집계 기능을 가능한 한 높거나 늦게 적용 )이 다른 경우에 해당 되는지 확실하지 않습니다 .

추가 정보

바이올린 : https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb

버전 : xcc_64-pc-linux-musl의 PostgreSQL 10.4, gcc (Alpine 6.4.0) 6.4.0, 64 비트로 컴파일 됨

환경 : 도커 : docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila. Docker 허브의 이미지는 구식이므로 build -t frantiseks/postgres-sakilagit 저장소를 복제 한 후 로컬에서 먼저 빌드를 수행했습니다 .

테이블 정의 :

필름

 film_id              | integer                     | not null default nextval('film_film_id_seq'::regclass)
 title                | character varying(255)      | not null

 Indexes:
    "film_pkey" PRIMARY KEY, btree (film_id)
    "idx_title" btree (title)

 Referenced by:
    TABLE "film_actor" CONSTRAINT "film_actor_film_id_fkey" FOREIGN KEY (film_id) REFERENCES film(film_id) ON UPDATE CASCADE ON DELETE RESTRICT

배우

 actor_id    | integer                     | not null default nextval('actor_actor_id_seq'::regclass)
 first_name  | character varying(45)       | not null

 Indexes:
    "actor_pkey" PRIMARY KEY, btree (actor_id)

 Referenced by:
    TABLE "film_actor" CONSTRAINT "film_actor_actor_id_fkey" FOREIGN KEY (actor_id) REFERENCES actor(actor_id) ON UPDATE CASCADE ON DELETE RESTRICT

film_actor

 actor_id    | smallint                    | not null
 film_id     | smallint                    | not null

 Indexes:
    "film_actor_pkey" PRIMARY KEY, btree (actor_id, film_id)
    "idx_fk_film_id" btree (film_id)
 Foreign-key constraints:
    "film_actor_actor_id_fkey" FOREIGN KEY (actor_id) REFERENCES actor(actor_id) ON UPDATE CASCADE ON DELETE RESTRICT
    "film_actor_film_id_fkey" FOREIGN KEY (film_id) REFERENCES film(film_id) ON UPDATE CASCADE ON DELETE RESTRICT

데이터 : Sakila 샘플 데이터베이스에서 가져온 것입니다. 이 질문은 실제 사례가 아니며,이 데이터베이스를 주로 학습 샘플 데이터베이스로 사용하고 있습니다. 몇 달 전에 SQL을 소개 받았으며 지식을 넓히려 고합니다. 다음과 같은 배포판이 있습니다.

select count(*) from film: 1000
select count(*) from actor: 200
select avg(a) from (select film_id, count(actor_id) a from film_actor group by film_id) a: 5.47

1
한 가지 더 : 모든 중요한 정보는 질문에 포함되어야합니다 (피들 링크 포함). 아무도 나중에 모든 의견을 읽고 싶지 않을 것입니다 (또는 의견이 매우 유능한 중재자에 의해 삭제됩니다).
Erwin Brandstetter

바이올린이 질문에 추가됩니다!
Jelly Orns

답변:


7

테스트 설정

바이올린 의 원래 설정 개선의 여지를 남겨 둡니다. 이유 때문에 설정을 계속 요청했습니다.

  • 이 인덱스는 다음과 film_actor같습니다.

    "film_actor_pkey" PRIMARY KEY, btree (actor_id, film_id)  
    "idx_fk_film_id" btree (film_id)

    이미 도움이되었습니다. 그러나 특정 쿼리를 가장 잘 지원하려면 , 열에 대해 여러(film_id, actor_id)인덱스 를 이 순서대로 갖습니다 . 실용적인 해결책 : 아래에서 idx_fk_film_id와 같이 인덱스로 (film_id, actor_id)바꾸거 (film_id, actor_id)나이 테스트의 목적으로 PK를 작성하십시오 . 보다:

    읽기 전용 (또는 대부분 VACUUM이 쓰기 활동을 유지할 수있는 경우)에서 (title, film_id)인덱스 전용 스캔을 허용하도록 인덱스를 설정 하는 데 도움이 됩니다. 내 테스트 사례는 이제 읽기 성능에 최적화되어 있습니다.

  • film.film_id( integer)와 film_actor.film_id( smallint) 사이의 유형이 일치하지 않습니다 . 그것이 작동 하는 동안 쿼리 속도가 느려지고 다양한 합병증이 발생할 수 있습니다. 또한 FK 제약 조건이 더 비쌉니다. 피할 수 있으면 절대로하지 마십시오. 당신이 확실하지 않은 경우, 선택 integer이상 smallint. 필드 당 2 바이트 smallint 절약 할 수 있지만 (종종 정렬 패딩에 의해 소비 됨)보다 더 복잡한 문제가 있습니다 integer.

  • 테스트 자체의 성능을 최적화하려면 많은 행을 대량 삽입 한 후 인덱스와 제한 조건 작성하십시오 . 모든 행이있는 상태에서 처음부터 새로 만드는 것보다 기존 인덱스에 점차적으로 튜플을 추가하는 것이 속도가 느립니다.

이 테스트와 관련이 없습니다 :

  • 시퀀스 플러스 대신 간단 훨씬 더 신뢰할 수의 열 기본적으로 무료 - 서 serial(또는 IDENTITY) 열을. 하지마

  • timestamp without timestamp일반적으로과 같은 열에는 신뢰할 수 없습니다 last_update. timestamptz대신 사용하십시오 . 그리고 열 기본값은 "마지막 업데이트"에 적용 되지 않으며 엄밀히 말하면됩니다.

  • 길이 수정 자 in character varying(255)은 테스트 케이스가 Postgres에서 시작하도록 의도되지 않았 음을 나타냅니다. 여기서 홀수 길이는 무의미합니다. (또는 저자는 단서가 없습니다.)

바이올린에서 감사 된 테스트 사례를 고려하십시오.

DB <> 바이올린 여기 - 최적화하여 바이올린,과 추가 질의가있는 건물.

관련 :

1000 개의 영화와 200 명의 배우로 구성된 테스트 설정에는 유효성이 제한되어 있습니다. 가장 효율적인 쿼리는 0.2ms 미만입니다. 계획 시간은 실행 시간 이상입니다. 행이 100k 이상인 테스트가 더 드러납니다.

왜 저자의 이름 검색 합니까? 여러 열을 검색하면 이미 약간 다른 상황이 있습니다.

ORDER BY title로 단일 제목을 필터링하는 동안 의미가 없습니다 WHERE title = 'ACADEMY DINOSAUR'. 아마도 ORDER BY film_id?

그리고 전체 런타임의 경우 EXPLAIN (ANALYZE, TIMING OFF)서브 타이밍 오버 헤드로 노이즈를 줄일 수 있습니다.

대답

총 성능은 많은 요소에 의존하기 때문에 간단한 경험 법칙을 만들기는 어렵습니다. 매우 기본적인 지침 :

  • 모두 집계서브 테이블에서 행을 오버 헤드가 줄어들지 만 실제로 모든 행 (또는 매우 큰 부분)이 필요할 때만 지불합니다.

  • 선택을위한 몇 가지 (테스트를!) 행을, 다른 쿼리의 기술은 더 나은 결과를 얻을 수 있습니다. 여기에서 LATERAL더 많은 오버 헤드가 발생하지만 서브 테이블에서 필요한 행만 읽습니다. 아주 작은 부분 만 필요한 경우 큰 승리입니다.

특정 테스트 사례의 경우 하위 쿼리 에서 ARRAY 생성자를LATERAL 테스트합니다 .

SELECT f.film_id, f.title, a.actors
FROM   film
LEFT   JOIN LATERAL (
   SELECT ARRAY (
      SELECT a.first_name
      FROM   film_actor fa
      JOIN   actor a USING (actor_id)
      WHERE  fa.film_id = f.film_id
      ) AS actors
   ) a ON true
WHERE  f.title = 'ACADEMY DINOSAUR';
-- ORDER  BY f.title; -- redundant while we filter for a single title 

측면 하위 쿼리에서 단일 배열 만 집계하는 동안 간단한 ARRAY 생성자는 집계 함수보다 성능이 뛰어납니다 array_agg(). 보다:

또는 간단한 경우에 상관 관계 가 낮은 하위 쿼리 를 사용합니다.

SELECT f.film_id, f.title
     , ARRAY (SELECT a.first_name
              FROM   film_actor fa
              JOIN   actor a USING (actor_id)
              WHERE  fa.film_id = f.film_id) AS actors
FROM   film f
WHERE  f.title = 'ACADEMY DINOSAUR';

또는 매우 기본적으로 2x LEFT JOIN다음에 집계하십시오 .

SELECT f.film_id, f.title, array_agg(a.first_name) AS actors
FROM   film f
LEFT   JOIN film_actor fa USING (film_id)
LEFT   JOIN actor a USING (actor_id)
WHERE  f.title = 'ACADEMY DINOSAUR'
GROUP  BY f.film_id;

이 세 가지는 업데이트 된 바이올린 (계획 + 실행 시간)에서 가장 빠릅니다.

첫 번째 시도 (약간 수정 된 것)는 일반적으로 모든 영화 또는 대부분의 영화 를 검색하는 것이 가장 빠르지 만 작은 선택은 아닙니다.

SELECT f.film_id, f.title, a.actors
FROM   film f
LEFT   JOIN (         
   SELECT fa.film_id, array_agg(first_name) AS actors
   FROM   actor
   JOIN   film_actor fa USING (actor_id)
   GROUP  by fa.film_id
   ) a USING (film_id)
WHERE  f.title = 'ACADEMY DINOSAUR';  -- not good for a single (or few) films!

카디널리티가 훨씬 큰 테스트가 더 많이 드러날 것입니다. 그리고 결과를 가볍게 일반화하지 마십시오. 총 성능에는 여러 가지 요소가 있습니다.

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