J'ai les tableaux suivants (extraits de la base de données Sakila):
- film: film_id is pkey
- acteur: acteur_id est pkey
- film_actor: film_id et acteur_id sont les clés du film / acteur
Je sélectionne un film particulier. Pour ce film, je veux aussi que tous les acteurs participent à ce film. J'ai deux requêtes pour cela: une avec un LEFT JOIN
et une avec un 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;
Lors de la comparaison du plan de requête, la première requête est bien pire (20x) que la seconde:
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
Pourquoi est-ce? Je veux apprendre à raisonner à ce sujet, afin que je puisse comprendre ce qui se passe et prédire comment la requête se comportera lorsque la taille des données augmentera et quelles décisions le planificateur prendra dans certaines conditions.
Mes pensées: dans la première LEFT JOIN
requête, il semble que la sous-requête soit exécutée pour tous les films de la base de données, sans tenir compte du filtrage dans la requête externe que nous ne sommes intéressés que par un film particulier. Pourquoi le planificateur n'est-il pas en mesure d'avoir ces connaissances dans la sous-requête?
Dans la LEFT JOIN LATERAL
requête, nous poussons plus ou moins ce filtrage vers le bas. Le problème rencontré lors de la première requête n'est donc pas présent ici, d'où les meilleures performances.
Je suppose que je recherche principalement des règles de base, des idées reçues, ... donc cette magie de planificateur devient une seconde nature - si cela a du sens.
mise à jour (1)
La réécriture de ce LEFT JOIN
qui suit donne également de meilleures performances (légèrement meilleures que les 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
Comment pouvons-nous raisonner à ce sujet?
mise à jour (2)
J'ai continué avec quelques expériences et je pense qu'une règle empirique intéressante est: appliquer la fonction d'agrégation aussi haut / tard que possible . La requête dans la mise à jour (1) fonctionne probablement mieux parce que nous agrégons dans la requête externe, et non plus dans la requête interne.
La même chose semble s'appliquer si nous réécrivons ce qui LEFT JOIN LATERAL
précède comme suit:
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
Ici, nous avons array_agg()
progressé. Comme vous pouvez le voir, ce plan est également meilleur que l'original LEFT JOIN LATERAL
.
Cela dit, je ne sais pas si cette règle empirique inventée ( appliquer la fonction d'agrégation aussi haut / tard que possible ) est vraie dans d'autres cas.
Information additionnelle
Violon: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb
Version: PostgreSQL 10.4 sur x86_64-pc-linux-musl, compilé par gcc (Alpine 6.4.0) 6.4.0, 64 bits
Environnement: Docker: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila
. Veuillez noter que l'image sur le hub Docker est obsolète, j'ai donc fait une construction localement d'abord: build -t frantiseks/postgres-sakila
après le clonage du référentiel git.
Définitions des tableaux:
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
acteur
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
acteur de film
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
Données: elles proviennent de la base de données d'exemples Sakila. Cette question n'est pas un cas réel, j'utilise cette base de données principalement comme exemple de base de données d'apprentissage. J'ai été initié à SQL il y a quelques mois et j'essaie d'élargir mes connaissances. Il a les distributions suivantes:
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
Réponses:
Configuration de test
Votre configuration d'origine dans le violon laisse place à amélioration. J'ai continué à demander votre configuration pour une raison.
Vous avez ces index sur
film_actor
:Ce qui est déjà assez utile. Mais pour mieux prendre en charge votre requête particulière, vous auriez un index multicolonne sur les
(film_id, actor_id)
colonnes dans cet ordre. Une solution pratique: remplacezidx_fk_film_id
par un index sur(film_id, actor_id)
- ou créez le PK sur(film_id, actor_id)
aux fins de ce test, comme je le fais ci-dessous. Voir:Dans une lecture seule (ou la plupart du temps, ou généralement lorsque VACUUM peut suivre l'activité d'écriture), il est également utile d'avoir un index
(title, film_id)
pour autoriser les analyses d'index uniquement. Mon scénario de test est désormais hautement optimisé pour les performances de lecture.Tapez incompatibilité entre
film.film_id
(integer
) etfilm_actor.film_id
(smallint
). Bien que cela fonctionne, cela ralentit les requêtes et peut entraîner diverses complications. Rend également les contraintes FK plus chères. Ne faites jamais cela si cela peut être évité. Si vous n'êtes pas sûr, choisissezinteger
plussmallint
. Bien qu'ilsmallint
puisse économiser 2 octets par champ (souvent consommé par le remplissage d'alignement), il y a plus de complications qu'avecinteger
.Pour optimiser les performances du test lui-même, créez des index et des contraintes après avoir inséré en vrac de nombreuses lignes. Il est beaucoup plus lent d'ajouter des tuples de manière incrémentielle aux index existants que de les créer à partir de zéro avec toutes les lignes présentes.
Sans rapport avec ce test:
Séquences autonomes plus les valeurs par défaut des colonnes au lieu de colonnes beaucoup plus simples et plus fiables
serial
(ouIDENTITY
). Non.timestamp without timestamp
est généralement peu fiable pour une colonne commelast_update
. Utiliseztimestamptz
plutôt. Et notez que les valeurs par défaut des colonnes ne couvrent pas la "dernière mise à jour" à proprement parler.Le modificateur de longueur dans
character varying(255)
indique que le cas de test n'est pas destiné à Postgres pour commencer car la longueur impaire est assez inutile ici. (Ou l'auteur n'a aucune idée.)Considérez le cas de test audité dans le violon:
db <> fiddle here - s'appuyant sur votre violon, optimisé et avec des requêtes supplémentaires.
En relation:
Une configuration de test avec 1000 films et 200 acteurs a une validité limitée. Les requêtes les plus efficaces prennent <0,2 ms. Le temps de planification est plus que le temps d'exécution. Un test avec 100 000 lignes ou plus serait plus révélateur.
Pourquoi ne récupérer que les prénoms des auteurs? Une fois que vous avez récupéré plusieurs colonnes, vous avez déjà une situation légèrement différente.
ORDER BY title
n'a aucun sens lors du filtrage d'un seul titre avecWHERE title = 'ACADEMY DINOSAUR'
. PeutORDER BY film_id
- être ?Et pour une durée d'exécution totale, utilisez plutôt
EXPLAIN (ANALYZE, TIMING OFF)
pour réduire (potentiellement trompeur) le bruit avec des frais généraux de sous-synchronisation.Répondre
Il est difficile de former une règle empirique simple, car les performances globales dépendent de nombreux facteurs. Lignes directrices très basiques:
L'agrégation de toutes les lignes dans les sous-tables entraîne moins de frais généraux mais ne paie que lorsque vous avez réellement besoin de toutes les lignes (ou d'une très grande partie).
Pour sélectionner quelques lignes (votre test!), Différentes techniques de requête donnent de meilleurs résultats. C'est là
LATERAL
qu'intervient. Il comporte plus de surcharge mais ne lit que les lignes requises des sous-tables. Une grosse victoire si seulement une (très) petite fraction est nécessaire.Pour votre cas de test particulier, je testerais également un constructeur ARRAY dans la
LATERAL
sous - requête :Tout en agrégeant un seul tableau dans la sous-requête latérale, un simple constructeur ARRAY fonctionne mieux que la fonction d'agrégation
array_agg()
. Voir:Ou avec une sous-requête faiblement corrélée pour le cas simple:
Ou, fondamentalement, juste 2x
LEFT JOIN
et ensuite agréger :Ces trois semblent les plus rapides dans mon violon mis à jour (planification + temps d'exécution).
Votre première tentative (seulement légèrement modifiée) est généralement la plus rapide pour récupérer tous ou la plupart des films , mais pas pour une petite sélection:
Des tests avec des cardinalités beaucoup plus importantes seront plus révélateurs. Et ne généralisez pas les résultats à la légère, il existe de nombreux facteurs pour la performance totale.
la source