Tại sao LEFT THAM GIA này thực hiện tồi tệ hơn nhiều so với LEFT THAM GIA LATITH?


13

Tôi có các bảng sau (lấy từ cơ sở dữ liệu Sakila):

  • phim: film_id là pkey
  • diễn viên: diễn viên_id là pkey
  • film_actor: film_id và Act_id là những người thích làm phim / diễn viên

Tôi đang chọn một bộ phim cụ thể. Đối với bộ phim này, tôi cũng muốn tất cả các diễn viên tham gia vào bộ phim đó. Tôi có hai truy vấn cho điều này: một với a LEFT JOINvà một với 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;

Khi so sánh kế hoạch truy vấn, truy vấn đầu tiên thực hiện kém hơn (20 lần) so với truy vấn thứ hai:

 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

Tại sao lại thế này? Tôi muốn tìm hiểu lý do về điều này, vì vậy tôi có thể hiểu những gì đang diễn ra và có thể dự đoán cách truy vấn sẽ hành xử khi kích thước dữ liệu tăng và quyết định nào mà người lập kế hoạch sẽ đưa ra trong những điều kiện nhất định.

Suy nghĩ của tôi: trong LEFT JOINtruy vấn đầu tiên , có vẻ như truy vấn con được thực thi cho tất cả các phim trong cơ sở dữ liệu, mà không tính đến việc lọc trong truy vấn bên ngoài mà chúng ta chỉ quan tâm đến một phim cụ thể. Tại sao người lập kế hoạch không thể có kiến ​​thức đó trong truy vấn con?

Trong LEFT JOIN LATERALtruy vấn, chúng tôi ít nhiều 'đẩy' việc lọc xuống dưới. Vì vậy, vấn đề chúng tôi gặp phải trong truy vấn đầu tiên không có ở đây, do đó hiệu suất tốt hơn.

Tôi đoán tôi chủ yếu tìm kiếm quy tắc của ngón tay cái, trí tuệ chung, ... vì vậy điều này phép thuật kế hoạch trở thành bản chất thứ hai - nếu điều đó có ý nghĩa.

cập nhật (1)

Viết lại LEFT JOINnhư sau cũng cho hiệu suất tốt hơn (tốt hơn một chút so với 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

Làm thế nào chúng ta có thể lý do về điều này?

cập nhật (2)

Tôi tiếp tục với một số thí nghiệm và tôi nghĩ một quy tắc thú vị là: áp dụng hàm tổng hợp càng cao / muộn càng tốt . Truy vấn trong bản cập nhật (1) có thể hoạt động tốt hơn vì chúng tôi đang tổng hợp trong truy vấn bên ngoài, không còn trong truy vấn bên trong.

Điều tương tự cũng áp dụng nếu chúng ta viết lại LEFT JOIN LATERALnhư trên:

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

Ở đây, chúng tôi di chuyển array_agg()lên trên. Như bạn thấy, kế hoạch này cũng tốt hơn so với ban đầu LEFT JOIN LATERAL.

Điều đó nói rằng, tôi không chắc liệu quy tắc tự phát minh này ( áp dụng hàm tổng hợp càng cao / muộn càng tốt ) có đúng trong các trường hợp khác hay không.

thông tin thêm

Fiddle: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb

Phiên bản: PostgreSQL 10.4 trên x86_64-pc-linux-musl, được biên dịch bởi gcc (Alpine 6.4.0) 6.4.0, 64-bit

Môi trường: Docker : docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila. Xin lưu ý rằng hình ảnh trên Docker hub đã lỗi thời, vì vậy tôi đã xây dựng cục bộ trước: build -t frantiseks/postgres-sakilasau khi nhân bản kho lưu trữ git.

Bảng định nghĩa:

phim ảnh

 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

diễn viên

 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

Diễn viên điện ảnh

 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

Dữ liệu: đây là từ cơ sở dữ liệu mẫu Sakila. Câu hỏi này không phải là một trường hợp thực tế, tôi đang sử dụng cơ sở dữ liệu này chủ yếu như một cơ sở dữ liệu mẫu học tập. Tôi đã được giới thiệu về SQL vài tháng trước và tôi đang cố gắng mở rộng kiến ​​thức của mình. Nó có các bản phân phối sau:

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
Một điều nữa: tất cả các thông tin quan trọng nên đi vào câu hỏi (bao gồm cả liên kết fiddle của bạn). Không ai muốn đọc qua tất cả các bình luận sau này (hoặc chúng sẽ bị xóa bởi một người điều hành rất có thể).
Erwin Brandstetter

Fiddle được thêm vào câu hỏi!
Jelly Orns

Câu trả lời:


7

Kiểm tra thiết lập

Thiết lập ban đầu của bạn trong fiddle rời khỏi phòng để cải thiện. Tôi tiếp tục yêu cầu thiết lập của bạn cho một lý do.

  • Bạn có các chỉ mục này trên film_actor:

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

    Đó là khá hữu ích rồi. Nhưng để hỗ trợ tốt nhất cho truy vấn cụ thể của bạn, bạn sẽ có một chỉ mục nhiều màu trên (film_id, actor_id)các cột theo thứ tự này. Một giải pháp thực tế: thay thế idx_fk_film_idbằng một chỉ mục trên (film_id, actor_id)- hoặc tạo PK trên (film_id, actor_id)cho mục đích thử nghiệm này, như tôi làm dưới đây. Xem:

    Trong chế độ chỉ đọc (hoặc chủ yếu, hoặc nói chung khi VACUUM có thể theo kịp hoạt động ghi), nó cũng giúp có một chỉ mục trên (title, film_id)để cho phép quét chỉ mục. Trường hợp thử nghiệm của tôi bây giờ được tối ưu hóa cao cho hiệu suất đọc.

  • Nhập không khớp giữa film.film_id( integer) vàfilm_actor.film_id ( smallint). Trong khi nó hoạt động, nó làm cho các truy vấn chậm hơn và có thể dẫn đến các biến chứng khác nhau. Cũng làm cho các ràng buộc FK đắt hơn. Không bao giờ làm điều này nếu nó có thể tránh được. Nếu bạn không chắc chắn, hãy chọn integerqua smallint. Mặc dù smallint có thể tiết kiệm 2 byte cho mỗi trường (thường được sử dụng bởi phần đệm căn chỉnh) nhưng có nhiều sự phức tạp hơn so với integer.

  • Để tối ưu hóa hiệu suất của chính bài kiểm tra, hãy tạo các chỉ mục và các ràng buộc sau khi chèn nhiều hàng. Về cơ bản, việc thêm các bộ dữ liệu vào các chỉ mục hiện có sẽ chậm hơn so với việc tạo chúng từ đầu với tất cả các hàng có mặt.

Không liên quan đến thử nghiệm này:

  • Trình tự đứng cộng với mặc định cột thay vì các cột serial(hoặc IDENTITY) đơn giản hơn và đáng tin cậy hơn nhiều . Đừng.

  • timestamp without timestamp thường không đáng tin cậy cho một cột như last_update . Sử dụng timestamptzthay thế. Và lưu ý rằng mặc định của cột không bao gồm "bản cập nhật cuối cùng", nói đúng ra.

  • Công cụ sửa đổi độ dài trong character varying(255)chỉ ra rằng trường hợp thử nghiệm không dành cho Postgres bắt đầu vì độ dài lẻ khá vô nghĩa ở đây. (Hoặc tác giả không biết gì.)

Xem xét trường hợp kiểm tra được kiểm toán trong fiddle:

db <> ở đây - xây dựng trên fiddle của bạn, được tối ưu hóa và với các truy vấn được thêm vào.

Liên quan:

Một thiết lập thử nghiệm với 1000 phim và 200 diễn viên có hiệu lực hạn chế. Các truy vấn hiệu quả nhất mất <0,2 ms. Thời gian lập kế hoạch nhiều hơn thời gian thực hiện. Một bài kiểm tra với 100k hoặc nhiều hàng sẽ tiết lộ hơn.

Tại sao chỉ lấy tên của tác giả? Khi bạn truy xuất nhiều cột, bạn đã có một tình huống hơi khác.

ORDER BY titlekhông có ý nghĩa trong khi lọc cho một tiêu đề duy nhất với WHERE title = 'ACADEMY DINOSAUR'. Có lẽ ORDER BY film_idnào?

Và đối với tổng thời gian chạy thay vì sử dụng EXPLAIN (ANALYZE, TIMING OFF)để giảm tiếng ồn (có khả năng gây hiểu lầm) với chi phí phụ thời gian phụ.

Câu trả lời

Thật khó để hình thành một quy tắc đơn giản, bởi vì tổng hiệu suất phụ thuộc vào nhiều yếu tố. Hướng dẫn rất cơ bản:

  • Tổng hợp tất cả các hàng trong bảng phụ mang ít chi phí hơn nhưng chỉ trả tiền khi bạn thực sự cần tất cả các hàng (hoặc một phần rất lớn).

  • Để chọn một vài hàng (thử nghiệm của bạn!), Các kỹ thuật truy vấn khác nhau mang lại kết quả tốt hơn. Đó là nơi LATERALđến. Nó mang nhiều chi phí hơn nhưng chỉ đọc các hàng bắt buộc từ các bảng phụ. Một chiến thắng lớn nếu chỉ cần một phần (rất) nhỏ.

Đối với trường hợp thử nghiệm cụ thể của bạn, tôi cũng sẽ kiểm tra hàm tạo ARRAY trong LATERALtruy vấn con :

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 

Trong khi chỉ tổng hợp một mảng duy nhất trong truy vấn con bên, một hàm tạo ARRAY đơn giản thực hiện tốt hơn hàm tổng hợp array_agg(). Xem:

Hoặc với một truy vấn con tương quan thấp cho trường hợp đơn giản:

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';

Hoặc, rất cơ bản, chỉ cần 2 lần LEFT JOINvà sau đó tổng hợp :

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;

Cả ba dường như nhanh nhất trong fiddle cập nhật của tôi (lập kế hoạch + thời gian thực hiện).

Nỗ lực đầu tiên của bạn (chỉ được sửa đổi một chút) thường nhanh nhất để truy xuất tất cả hoặc hầu hết các bộ phim , nhưng không phải là một lựa chọn nhỏ:

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!

Các xét nghiệm với số lượng lớn hơn nhiều sẽ được tiết lộ nhiều hơn. Và đừng khái quát hóa kết quả một cách nhẹ nhàng, có nhiều yếu tố cho tổng hiệu suất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.