Warum ist LEFT JOIN so viel schlechter als LEFT JOIN LATERAL?

13

Ich habe die folgenden Tabellen (aus der Sakila-Datenbank):

  • film: film_id ist pkey
  • Schauspieler: actor_id ist pkey
  • film_actor: film_id und actor_id sind Schlüssel zu film / actor

Ich wähle einen bestimmten Film aus. Für diesen Film möchte ich auch, dass alle Schauspieler an diesem Film teilnehmen. Ich habe dazu zwei Fragen: eine mit a LEFT JOINund eine mit 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;

Beim Vergleich des Abfrageplans ist die erste Abfrage 20-mal schlechter als die zweite:

 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

Warum ist das? Ich möchte lernen, darüber nachzudenken, damit ich verstehen kann, was vor sich geht, und vorhersagen kann, wie sich die Abfrage verhält, wenn sich die Datenmenge erhöht und welche Entscheidungen der Planer unter bestimmten Bedingungen treffen wird.

Meine Gedanken: In der ersten LEFT JOINAbfrage sieht es so aus, als würde die Unterabfrage für alle Filme in der Datenbank ausgeführt, ohne die Filterung in der äußeren Abfrage zu berücksichtigen, dass wir nur an einem bestimmten Film interessiert sind. Warum kann der Planer dieses Wissen nicht in der Unterabfrage haben?

In der LEFT JOIN LATERALAbfrage "drücken" wir diese Filterung mehr oder weniger nach unten. Das Problem, das wir in der ersten Abfrage hatten, ist hier nicht vorhanden, daher die bessere Leistung.

Ich schätze, ich suche hauptsächlich nach Faustregeln, allgemeinen Weisheiten, ... also wird diese Planungsmagie zur zweiten Natur - wenn das Sinn macht.

Update (1)

Das Umschreiben LEFT JOINwie folgt führt auch zu einer besseren Leistung (etwas besser als die 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

Wie können wir das beurteilen?

Update (2)

Ich fuhr mit einigen Experimenten fort und ich denke, eine interessante Faustregel ist: Wende die Aggregatfunktion so hoch / spät wie möglich an . Die Abfrage in update (1) hat wahrscheinlich eine bessere Leistung, da wir in der äußeren Abfrage aggregieren und nicht mehr in der inneren Abfrage.

Dasselbe scheint zuzutreffen, wenn wir das LEFT JOIN LATERALObige wie folgt umschreiben :

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

Hier zogen wir nach array_agg()oben. Wie Sie sehen, ist dieser Plan auch besser als das Original LEFT JOIN LATERAL.

Ich bin mir jedoch nicht sicher, ob diese selbst erfundene Faustregel ( die Aggregatfunktion so hoch / spät wie möglich anwenden ) auch in anderen Fällen zutrifft.

zusätzliche Information

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

Version: PostgreSQL 10.4 auf x86_64-pc-linux-musl, kompiliert von gcc (Alpine 6.4.0) 6.4.0, 64-bit

Umwelt: Docker: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila. Bitte beachten Sie, dass das Image auf dem Docker-Hub veraltet ist. Deshalb habe ich zuerst eine lokale Erstellung durchgeführt: build -t frantiseks/postgres-sakilanach dem Klonen des Git-Repositorys.

Tabellendefinitionen:

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

Darsteller

 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

Filmschauspieler

 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

Daten: Dies ist aus der Sakila-Beispieldatenbank. Diese Frage ist kein realer Fall, ich verwende diese Datenbank hauptsächlich als Lernbeispieldatenbank. Ich bin vor einigen Monaten mit SQL vertraut geworden und versuche, mein Wissen zu erweitern. Es hat die folgenden Verteilungen:

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
Jelly Orns
quelle
1
Eine weitere Sache: Alle wichtigen Informationen sollten in die Frage eingehen (einschließlich Ihres Geigenlinks). Niemand wird später alle Kommentare durchlesen wollen (oder sie werden sowieso von einem bestimmten, sehr fähigen Moderator gelöscht).
Erwin Brandstetter
Geige wird zur Frage hinzugefügt!
Jelly Orns

Antworten:

7

Versuchsaufbau

Ihr ursprüngliches Setup in der Geige lässt Raum für Verbesserungen. Ich habe aus einem bestimmten Grund nach Ihrem Setup gefragt.

  • Sie haben diese Indizes für film_actor:

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

    Welches ist schon ziemlich hilfreich. Um Ihre spezielle Abfrage bestmöglich zu unterstützen, hätten Sie einen mehrspaltigen Index für (film_id, actor_id)Spalten in dieser Reihenfolge. Eine praktische Lösung: Ersetzen Sie sie idx_fk_film_iddurch einen Index auf (film_id, actor_id)- oder erstellen Sie die PK auf (film_id, actor_id)für diesen Test, wie unten beschrieben. Sehen:

    In einem schreibgeschützten Modus (oder meistens oder allgemein, wenn VACUUM mit Schreibaktivitäten Schritt halten kann) ist es auch hilfreich, einen Index zu haben (title, film_id), um Index-Only-Scans zuzulassen. Mein Testfall ist jetzt stark auf Leseleistung optimiert.

  • Typenkonflikt zwischen film.film_id( integer) und film_actor.film_id( smallint). Während das funktioniert es macht Abfragen langsamer und zu verschiedenen Komplikationen führen kann. Macht auch FK-Einschränkungen teurer. Tun Sie dies niemals, wenn dies vermieden werden kann. Wenn Sie nicht sicher sind, holen Sie integervorbei smallint. Zwar smallint können 2 Bytes pro Feld eingespart werden (die häufig durch Ausrichtungsauffüllung verbraucht werden), dies ist jedoch komplizierter als bei integer.

  • Um die Leistung des Tests selbst zu optimieren, erstellen Sie Indizes und Einschränkungen, nachdem Sie mehrere Zeilen gleichzeitig eingefügt haben. Es ist wesentlich langsamer, Tupel inkrementell zu vorhandenen Indizes hinzuzufügen, als sie mit allen vorhandenen Zeilen von Grund auf neu zu erstellen.

Unabhängig von diesem Test:

  • Freistehende Sequenzen plus Spaltenstandards anstelle von viel einfacheren und zuverlässigeren serial(oder IDENTITY) Spalten. Nicht.

  • timestamp without timestampist in der Regel für eine Spalte wie unzuverlässig last_update. Verwenden Sie timestamptzstattdessen. Beachten Sie, dass die Standardeinstellungen der Spalten streng genommen nicht das "letzte Update" abdecken.

  • Der Längenmodifikator in character varying(255)gibt an, dass der Testfall nicht für Postgres vorgesehen ist, da die ungerade Länge hier ziemlich sinnlos ist. (Oder der Autor ist ahnungslos.)

Betrachten Sie den geprüften Testfall in der Geige:

db <> fiddle here - baut auf Ihrer Geige auf, optimiert und mit zusätzlichen Abfragen.

Verbunden:

Ein Testaufbau mit 1000 Filmen und 200 Schauspielern ist nur begrenzt gültig. Die effizientesten Abfragen dauern <0,2 ms. Planungszeit ist mehr als Ausführungszeit. Ein Test mit 100k oder mehr Zeilen wäre aufschlussreicher.

Warum nur Vornamen von Autoren abrufen ? Sobald Sie mehrere Spalten abrufen, haben Sie bereits eine etwas andere Situation.

ORDER BY titlemacht beim filtern nach einem titel mit keinen sinn WHERE title = 'ACADEMY DINOSAUR'. Vielleicht ORDER BY film_id?

Und für die gesamte Laufzeit eher verwenden EXPLAIN (ANALYZE, TIMING OFF), um (möglicherweise irreführende) Rauschen mit Sub-Timing-Overhead zu reduzieren.

Antworten

Es ist schwierig, eine einfache Faustregel zu bilden, da die Gesamtleistung von vielen Faktoren abhängt. Sehr grundlegende Richtlinien:

  • Das Aggregieren aller Zeilen in Untertabellen verursacht weniger Aufwand, zahlt sich jedoch nur dann aus, wenn Sie tatsächlich alle Zeilen (oder einen sehr großen Teil) benötigen.

  • Für die Auswahl weniger Zeilen (Ihr Test!) Ergeben verschiedene Abfragetechniken bessere Ergebnisse. Das ist, wo LATERALkommt in. Es trägt mehr Aufwand, liest aber nur erforderliche Zeilen aus Untertabellen. Ein großer Gewinn, wenn nur ein (sehr) kleiner Bruchteil benötigt wird.

Für Ihren speziellen Testfall würde ich auch einen ARRAY-Konstruktor in der LATERALUnterabfrage testen :

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 

Während nur ein einzelnes Array in der lateralen Unterabfrage aggregiert wird, ist ein einfacher ARRAY-Konstruktor leistungsfähiger als die Aggregatfunktion array_agg(). Sehen:

Oder mit einer schwach korrelierten Unterabfrage für den einfachen Fall:

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

Oder ganz einfach nur 2x LEFT JOINund dann aggregieren :

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;

Diese drei scheinen in meiner aktualisierten Geige (Planung + Ausführungszeit) am schnellsten zu sein.

Ihr erster Versuch (nur geringfügig geändert) ist in der Regel der schnellste, um alle oder die meisten Filme abzurufen , jedoch nicht für eine kleine Auswahl:

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!

Tests mit viel größeren Kardinalitäten sind aufschlussreicher. Und verallgemeinern Sie die Ergebnisse nicht leichtfertig, es gibt viele Faktoren für die Gesamtleistung.

Erwin Brandstetter
quelle