Tengo las siguientes tablas (tomadas de la base de datos Sakila):
- película: film_id es pkey
- actor: actor_id es pkey
- film_actor: film_id y actor_id son fkeys para película / actor
Estoy seleccionando una película en particular. Para esta película, también quiero que todos los actores participen en esa película. Tengo dos consultas para esto: una con a LEFT JOIN
y otra con 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;
Al comparar el plan de consulta, la primera consulta funciona mucho peor (20x) que la segunda:
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
¿Por qué es esto? Quiero aprender a razonar sobre esto, para poder entender lo que está sucediendo y predecir cómo se comportará la consulta cuando aumente el tamaño de los datos y qué decisiones tomará el planificador bajo ciertas condiciones.
Mis pensamientos: en la primera LEFT JOIN
consulta, parece que la subconsulta se ejecuta para todas las películas en la base de datos, sin tener en cuenta el filtrado en la consulta externa de que solo estamos interesados en una película en particular. ¿Por qué el planificador no puede tener ese conocimiento en la subconsulta?
En la LEFT JOIN LATERAL
consulta, estamos más o menos "empujando" ese filtrado hacia abajo. Entonces, el problema que tuvimos en la primera consulta no está presente aquí, de ahí el mejor rendimiento.
Supongo que estoy buscando principalmente la regla general, las sabidurías generales, ... así que esta magia planificadora se convierte en una segunda naturaleza, si eso tiene sentido.
actualización (1)
Reescribir lo LEFT JOIN
siguiente también ofrece un mejor rendimiento (un poco mejor que el 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
¿Cómo podemos razonar sobre esto?
actualización (2)
Continué con algunos experimentos y creo que una regla práctica interesante es: aplicar la función de agregado lo más alto / tardío posible . La consulta en la actualización (1) probablemente funciona mejor porque estamos agregando en la consulta externa, ya no en la consulta interna.
Lo mismo parece aplicarse si reescribimos lo LEFT JOIN LATERAL
anterior de la siguiente manera:
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
Aquí, nos movimos array_agg()
hacia arriba. Como puede ver, este plan también es mejor que el original LEFT JOIN LATERAL
.
Dicho esto, no estoy seguro de si esta regla empírica auto-inventada ( aplicar la función de agregado lo más alto / tardío posible ) es cierta en otros casos.
Información Adicional
Fiddle: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb
Versión: PostgreSQL 10.4 en x86_64-pc-linux-musl, compilado por gcc (Alpine 6.4.0) 6.4.0, 64 bits
Medio Ambiente: acoplable: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila
. Tenga en cuenta que la imagen en Docker Hub está desactualizada, así que primero hice una compilación local: build -t frantiseks/postgres-sakila
después de clonar el repositorio git.
Definiciones de tabla:
película
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
actor
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
Actor de películas
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
Datos: esto es de la base de datos de muestra Sakila. Esta pregunta no es un caso de la vida real, estoy usando esta base de datos principalmente como una base de datos de muestra de aprendizaje. Me presentaron a SQL hace unos meses y estoy tratando de ampliar mi conocimiento. Tiene las siguientes distribuciones:
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
Respuestas:
Configuración de prueba
Su configuración original en el violín deja margen de mejora. Seguí preguntando por su configuración por una razón.
Tienes estos índices en
film_actor
:Lo cual ya es bastante útil. Sin embargo, para mejor soporte a su consulta en particular, tendría un índice de múltiples de
(film_id, actor_id)
columnas en este orden. Una solución práctica: reemplazaridx_fk_film_id
con un índice activado(film_id, actor_id)
, o crear el PK activado(film_id, actor_id)
para el propósito de esta prueba, como lo hago a continuación. Ver:En un modo de solo lectura (o mayormente, o generalmente cuando VACUUM puede mantenerse al día con la actividad de escritura), también ayuda tener un índice activado
(title, film_id)
para permitir escaneos de índice solamente. Mi caso de prueba ahora está altamente optimizado para el rendimiento de lectura.Escriba falta de coincidencia entre
film.film_id
(integer
) yfilm_actor.film_id
(smallint
). Si bien eso funciona , hace que las consultas sean más lentas y puede provocar varias complicaciones. También hace que las restricciones de FK sean más caras. Nunca haga esto si se puede evitar. Si no está seguro, elijainteger
mássmallint
. Si biensmallint
puede guardar 2 bytes por campo (a menudo consumido por el relleno de alineación), hay más complicaciones que coninteger
.Para optimizar el rendimiento de la prueba en sí, cree índices y restricciones después de la inserción masiva de muchas filas. Es sustancialmente más lento agregar tuplas de forma incremental a los índices existentes que crearlas desde cero con todas las filas presentes.
No relacionado con esta prueba:
Secuencias independientes más valores predeterminados de columna en lugar de columnas mucho más simples y más confiables
serial
(oIDENTITY
). No lo hagastimestamp without timestamp
normalmente no es confiable para una columna comolast_update
. Usar en sutimestamptz
lugar. Y tenga en cuenta que los valores predeterminados de la columna no cubren la "última actualización", estrictamente hablando.El modificador de longitud en
character varying(255)
indica que el caso de prueba no está destinado a Postgres para comenzar porque la longitud impar es bastante inútil aquí. (O el autor no tiene idea).Considere el caso de prueba auditado en el violín:
db <> fiddle here : construyendo sobre su violín, optimizado y con consultas adicionales.
Relacionado:
Una configuración de prueba con 1000 películas y 200 actores tiene una validez limitada. Las consultas más eficientes toman <0.2 ms. El tiempo de planificación es más que el tiempo de ejecución. Una prueba con 100k o más filas sería más reveladora.
¿Por qué recuperar solo los nombres de los autores? Una vez que recupera varias columnas, ya tiene una situación ligeramente diferente.
ORDER BY title
no tiene sentido mientras se filtra por un solo título conWHERE title = 'ACADEMY DINOSAUR'
. Tal vezORDER BY film_id
?Y para el tiempo de ejecución total, más bien use
EXPLAIN (ANALYZE, TIMING OFF)
para reducir el ruido (potencialmente engañoso) con sobrecarga de subtiempo.Responder
Es difícil formar una regla general simple, porque el rendimiento total depende de muchos factores. Pautas muy básicas:
Agregar todas las filas en las subtablas conlleva menos gastos generales, pero solo paga cuando realmente necesita todas las filas (o una parte muy grande).
Para seleccionar algunas filas (¡su prueba!), Diferentes técnicas de consulta producen mejores resultados. Ahí es donde
LATERAL
entra. Lleva más sobrecarga pero solo lee las filas requeridas de las subtablas. Una gran victoria si solo se necesita una fracción (muy) pequeña.Para su caso de prueba particular, también probaría un constructor ARRAY en la
LATERAL
subconsulta :Si bien solo agrega una matriz en la subconsulta lateral, un simple constructor ARRAY funciona mejor que la función de agregado
array_agg()
. Ver:O con una subconsulta poco correlacionada para el caso simple:
O, básicamente, solo 2x
LEFT JOIN
y luego agregar :Estos tres parecen más rápidos en mi violín actualizado (planificación + tiempo de ejecución).
Su primer intento (solo ligeramente modificado) suele ser el más rápido para recuperar todas o la mayoría de las películas , pero no para una pequeña selección:
Las pruebas con cardinalidades mucho más grandes serán más reveladoras. Y no generalice los resultados a la ligera, hay muchos factores para el rendimiento total.
fuente