Mam następujące tabele (zaczerpnięte z bazy danych Sakila):
- film: film_id jest kluczem
- aktor: actor_id to pkey
- film_actor: film_id i actor_id to klucze do filmu / aktora
Wybieram konkretny film. W tym filmie chcę też, aby wszyscy aktorzy uczestniczyli w tym filmie. Mam na to dwa pytania: jedno z LEFT JOIN
a drugie z 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;
Podczas porównywania planu zapytań pierwsze zapytanie działa znacznie gorzej (20x) niż drugie:
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
Dlaczego to? Chcę nauczyć się rozumować na ten temat, aby móc zrozumieć, co się dzieje, i przewidzieć, jak zachowa się zapytanie, gdy wzrośnie rozmiar danych i jakie decyzje planista podejmie w określonych warunkach.
Moje przemyślenia: w pierwszym LEFT JOIN
zapytaniu wygląda na to, że podzapytanie jest wykonywane dla wszystkich filmów w bazie danych, bez uwzględnienia filtrowania w zewnętrznym zapytaniu, że jesteśmy zainteresowani tylko jednym konkretnym filmem. Dlaczego planista nie może mieć tej wiedzy w podzapytaniu?
W LEFT JOIN LATERAL
zapytaniu mniej więcej „popychamy” to filtrowanie w dół. Problem, który mieliśmy w pierwszym zapytaniu, nie jest tutaj obecny, stąd lepsza wydajność.
Chyba przede wszystkim szukam zasady kciuka, ogólnej mądrości ... więc magia planisty staje się drugą naturą - jeśli to ma sens.
aktualizacja (1)
Przepisanie w LEFT JOIN
następujący sposób również daje lepszą wydajność (nieco lepszą niż 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
Jak możemy o tym myśleć?
aktualizacja (2)
Kontynuowałem niektóre eksperymenty i myślę, że interesująca zasada jest następująca: zastosuj funkcję agregującą tak wysoko / późno, jak to możliwe . Zapytanie w aktualizacji (1) prawdopodobnie działa lepiej, ponieważ agregujemy w zapytaniu zewnętrznym, a nie wewnętrznym.
To samo wydaje się obowiązywać, jeśli przepisujemy LEFT JOIN LATERAL
powyższe w następujący sposób:
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
Tutaj ruszyliśmy w array_agg()
górę. Jak widać, ten plan jest również lepszy niż oryginał LEFT JOIN LATERAL
.
To powiedziawszy, nie jestem pewien, czy ta wymyślona przez ciebie zasada ( zastosuj funkcję agregującą tak wysoko / późno, jak to możliwe ) jest prawdziwa w innych przypadkach.
Dodatkowe informacje
Fiddle: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb
Wersja: PostgreSQL 10.4 na x86_64-pc-linux-musl, skompilowany przez gcc (Alpine 6.4.0) 6.4.0, 64-bit
Środowisko: Docker: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila
. Pamiętaj, że obraz w Docker Hub jest nieaktualny, więc najpierw wykonałem kompilację lokalnie: build -t frantiseks/postgres-sakila
po klonowaniu repozytorium git.
Definicje tabel:
film
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
aktor
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
aktor filmowy
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
Dane: pochodzi z przykładowej bazy danych Sakila. To pytanie nie jest prawdziwym przypadkiem, używam tej bazy danych głównie jako przykładowej bazy danych do nauki. Kilka miesięcy temu zapoznałem się z SQL i staram się poszerzać swoją wiedzę. Ma następujące dystrybucje:
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
Odpowiedzi:
Konfiguracja testowa
Twoja oryginalna konfiguracja w skrzypcach pozostawia miejsce na ulepszenia. Ciągle pytałem o twoją konfigurację z jakiegoś powodu.
Masz te indeksy na
film_actor
:Co już jest dość pomocne. Ale do najlepszej obsługi określonej kwerendy, można mieć indeks wielokolumnowej on
(film_id, actor_id)
, kolumny w tej kolejności. Praktyczne rozwiązanie: zamieńidx_fk_film_id
z indeksem na(film_id, actor_id)
- lub utwórz PK na(film_id, actor_id)
potrzeby tego testu, tak jak poniżej. Widzieć:W trybie tylko do odczytu (lub przeważnie lub ogólnie, gdy VACUUM może nadążyć za działaniami związanymi z zapisem), pomocne jest także włączenie indeksu,
(title, film_id)
aby umożliwić skanowanie tylko indeksu. Mój przypadek testowy jest teraz wysoce zoptymalizowany pod kątem wydajności odczytu.Wpisz niezgodność między
film.film_id
(integer
) afilm_actor.film_id
(smallint
). Chociaż to działa , spowalnia zapytania i może prowadzić do różnych komplikacji. Zwiększa także ograniczenia FK. Nigdy tego nie rób, jeśli można tego uniknąć. Jeżeli nie jesteś pewien, pickinteger
nadsmallint
. Chociażsmallint
można zapisać 2 bajty na pole (często zużywane przez wypełnienie wyrównania), jest więcej komplikacji niż zinteger
.Aby zoptymalizować wydajność samego testu, utwórz indeksy i ograniczenia po masowym wstawieniu wielu wierszy. Znacznie wolniej jest dodawać krotki przyrostowo do istniejących indeksów, niż tworzyć je od zera ze wszystkimi obecnymi wierszami.
Niepowiązane z tym testem:
Wolnostojące sekwencje plus ustawienia domyślne kolumn zamiast znacznie prostszych i bardziej niezawodnych
serial
(lubIDENTITY
) kolumn. Nie róbtimestamp without timestamp
jest zwykle zawodny w przypadku kolumny podobnej dolast_update
. Użyjtimestamptz
zamiast tego. I zwróć uwagę, że ustawienia domyślne kolumny nie obejmują „ostatniej aktualizacji”, ściśle mówiąc.Modyfikator długości
character varying(255)
wskazuje, że przypadek testowy nie jest przeznaczony dla Postgres, ponieważ nieparzysta długość jest tutaj raczej bezcelowa. (Lub autor nie ma pojęcia.)Rozważ zbadany przypadek testowy w skrzypcach:
db <> skrzypce tutaj - budowanie na skrzypcach, zoptymalizowane i z dodanymi zapytaniami.
Związane z:
Konfiguracja testowa z 1000 filmów i 200 aktorów ma ograniczoną ważność. Najbardziej wydajne zapytania trwają <0,2 ms. Czas planowania to więcej niż czas realizacji. Test z 100k lub więcej rzędami byłby bardziej odkrywczy.
Po co pobierać tylko imiona autorów? Po pobraniu wielu kolumn masz już nieco inną sytuację.
ORDER BY title
nie ma sensu podczas filtrowania pojedynczego tytułu za pomocąWHERE title = 'ACADEMY DINOSAUR'
. MożeORDER BY film_id
?A dla całkowitego czasu działania raczej używaj
EXPLAIN (ANALYZE, TIMING OFF)
do zmniejszania (potencjalnie wprowadzającego w błąd) hałasu przy obciążeniu sub-timingiem.Odpowiedź
Trudno jest sformułować prostą zasadę, ponieważ całkowita wydajność zależy od wielu czynników. Bardzo podstawowe wytyczne:
Agregacja wszystkich wierszy w podtabelach niesie narzut, ale płaci tylko wtedy, gdy faktycznie potrzebujesz wszystkich wierszy (lub bardzo dużej części).
Aby wybrać kilka wierszy (twój test!), Różne techniki zapytań dają lepsze wyniki. Tam właśnie
LATERAL
pojawia się. To niesie narzut, ale czyta tylko wymagane wiersze z pod-tabel. Duża wygrana, jeśli potrzebna jest tylko (bardzo) niewielka część.W przypadku konkretnego przypadku testowego przetestowałbym również konstruktor ARRAY w
LATERAL
podzapytaniu :Podczas gdy agreguje tylko jedną tablicę w podzapytaniu bocznym, prosty konstruktor ARRAY działa lepiej niż funkcja agregująca
array_agg()
. Widzieć:Lub z mało skorelowanym podzapytaniem dla prostego przypadku:
Lub, w zasadzie, tylko 2x,
LEFT JOIN
a następnie agreguj :Te trzy wydają się najszybsze w moim zaktualizowanym skrzypcach (planowanie + czas wykonania).
Twoja pierwsza próba (tylko nieznacznie zmodyfikowana) jest zazwyczaj najszybsza w celu odzyskania wszystkich lub większości filmów , ale nie w przypadku niewielkiego wyboru:
Testy ze znacznie większymi licznościami będą bardziej odkrywcze. I nie uogólniaj lekko wyników, istnieje wiele czynników wpływających na całkowitą wydajność.
źródło