it-swarm.com.de

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

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

  • film: film_id ist pkey
  • schauspieler: Schauspieler_ID ist pkey
  • film_Aktor: Film_ID und Schauspieler_ID sind Schlüssel zum Film/Schauspieler

Ich wähle einen bestimmten Film aus. Für diesen Film möchte ich auch, dass alle Schauspieler an diesem Film teilnehmen. Ich habe zwei Fragen dazu: eine mit einem LEFT JOIN Und eine mit einem 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 viel schlechter (20x) 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 die Datengröße zunimmt, und welche Entscheidungen der Planer unter bestimmten Bedingungen treffen wird.

Meine Gedanken: In der ersten LEFT JOIN - Abfrage sieht es so aus, als ob die Unterabfrage für alle Filme in der Datenbank ausgeführt wird, 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 Abfrage LEFT JOIN LATERAL Drücken wir diese Filterung mehr oder weniger nach unten. Das Problem, das wir in der ersten Abfrage hatten, ist hier also nicht vorhanden, daher die bessere Leistung.

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

Update (1)

Das Umschreiben des LEFT JOIN Wie folgt führt ebenfalls zu einer besseren Leistung (etwas besser als der 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 darüber argumentieren?

Update (2)

Ich habe einige Experimente fortgesetzt und denke, eine interessante Faustregel lautet: Wende die Aggregatfunktion so hoch/spät wie möglich an. Die Abfrage in Update (1) funktioniert wahrscheinlich besser, da wir in der äußeren Abfrage aggregieren, nicht mehr in der inneren Abfrage.

Dasselbe scheint zu gelten, wenn wir den obigen LEFT JOIN LATERAL 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 haben wir array_agg() nach oben verschoben. Wie Sie sehen können, ist dieser Plan auch besser als der ursprüngliche LEFT JOIN LATERAL.

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

zusätzliche Informationen

Geige: 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

Umgebung: 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. Daher habe ich zuerst lokal erstellt: build -t frantiseks/postgres-sakila, Nachdem ich das Git-Repository geklont habe.

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

Schauspieler

 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

Daten: Dies ist aus der Sakila-Beispieldatenbank. Diese Frage ist kein realer Fall. Ich verwende diese Datenbank hauptsächlich als Lernbeispieldatenbank. Ich wurde vor einigen Monaten in SQL eingeführt 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
13
Jelly Orns

Versuchsaufbau

Ihr ursprüngliches Setup in der Geige lässt Raum für Verbesserungen. Ich habe aus einem bestimmten Grund immer wieder 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 auf (film_id, actor_id), Spalten in dieser Reihenfolge. Eine praktische Lösung: Ersetzen Sie idx_fk_film_id Durch einen Index für (film_id, actor_id) - oder erstellen Sie die PK für (film_id, actor_id) Für den Zweck dieses Tests, wie unten beschrieben. Sehen:

    In einem schreibgeschützten Bereich (oder meistens oder allgemein, wenn VACUUM mit der Schreibaktivität Schritt halten kann) ist es auch hilfreich, einen Index für (title, film_id) zu haben, um nur Index-Scans zu ermöglichen. Mein Testfall ist jetzt stark für die Leseleistung optimiert.

  • Typfehlanpassung zwischen film.film_id (integer) und film_actor.film_id (smallint). Während das funktioniert verlangsamt es Abfragen und kann zu verschiedenen Komplikationen führen. Verteuert auch FK-Einschränkungen. Tun Sie dies niemals, wenn dies vermieden werden kann. Wenn Sie sich nicht sicher sind, wählen Sie integer über smallint. Während smallint can 2 Bytes pro Feld spart (häufig durch Ausrichtungsauffüllung verbraucht), gibt es mehr Komplikationen als mit integer.

  • Um die Leistung des Tests selbst zu optimieren, erstellen Sie Indizes und Einschränkungen after Masseneinfügung vieler Zeilen. Es ist wesentlich langsamer, Tupel schrittweise 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. Tu es nicht.

  • timestamp without timestamp Ist normalerweise für eine Spalte wie last_update Unzuverlässig. Verwenden Sie stattdessen timestamptz. Und beachten Sie, dass die Spaltenstandards nicht genau genommen das "letzte Update" abdecken.

  • Der Längenmodifikator in character varying(255) gibt an, dass der Testfall zunächst 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 hier - Aufbauend auf Ihrer Geige, optimiert und mit zusätzlichen Abfragen.

Verbunden:

Ein Testaufbau mit 1000 Filmen und 200 Schauspielern hat eine begrenzte Gültigkeit. Die effizientesten Abfragen dauern <0,2 ms. Planungszeit ist mehr als Ausführungszeit. Ein Test mit 100.000 oder mehr Zeilen wäre aufschlussreicher.

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

ORDER BY title Macht keinen Sinn, wenn Sie mit WHERE title = 'ACADEMY DINOSAUR' Nach einem einzelnen Titel filtern. Vielleicht ORDER BY film_id?

Und für Gesamtlaufzeit Verwenden Sie lieber EXPLAIN (ANALYZE, TIMING OFF) , um (möglicherweise irreführendes) Rauschen mit Sub-Timing-Overhead zu reduzieren.

Antworten

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

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

  • Für die Auswahl von wenigen Zeilen (Ihr Test!) Erzielen verschiedene Abfragetechniken bessere Ergebnisse. Hier kommt LATERAL ins Spiel. Es bringt mehr Overhead mit sich, liest aber nur die erforderlichen 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 Unterabfrage LATERAL 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 ein einfacher ARRAY-Konstruktor nur ein einzelnes Array in der lateralen Unterabfrage aggregiert, ist die Leistung besser als die Aggregatfunktion array_agg(). Sehen:

Oder mit einer niedrigen 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 im Grunde genommen einfach 2x LEFT JOIN Und 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 normalerweise am schnellsten, 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 werden aufschlussreicher sein. Verallgemeinern Sie die Ergebnisse nicht leichtfertig, es gibt viele Faktoren für die Gesamtleistung.

7