Eu tenho as seguintes tabelas (extraídas do banco de dados Sakila):
- film: film_id is pkey
- actor: actor_id is pkey
- film_actor: film_id e actor_id são as chaves do filme / ator
Estou selecionando um filme em particular. Para este filme, também quero que todos os atores participem desse filme. Eu tenho duas consultas para isso: uma com a LEFT JOIN
e outra com 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;
Ao comparar o plano de consulta, a primeira consulta executa muito pior (20x) que a 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 que é isso? Quero aprender a raciocinar sobre isso, para entender o que está acontecendo e prever como a consulta se comportará quando o tamanho dos dados aumentar e quais decisões o planejador tomará sob determinadas condições.
Penso: na primeira LEFT JOIN
consulta, parece que a subconsulta é executada para todos os filmes no banco de dados, sem levar em conta a filtragem na consulta externa de que estamos interessados apenas em um filme em particular. Por que o planejador não pode ter esse conhecimento na subconsulta?
Na LEFT JOIN LATERAL
consulta, estamos 'empurrando' mais ou menos essa filtragem para baixo. Portanto, o problema que tivemos na primeira consulta não está presente aqui, portanto, o melhor desempenho.
Acho que estou procurando principalmente regra de ouro, sabedoria geral, ... então essa mágica do planejador se torna uma segunda natureza - se isso faz sentido.
atualização (1)
Reescrever o LEFT JOIN
seguinte, também oferece melhor desempenho (um pouco melhor que o 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
Como podemos argumentar sobre isso?
atualização (2)
Continuei com alguns experimentos e acho uma regra interessante: aplicar a função agregada o mais alto / atrasado possível . A consulta na atualização (1) provavelmente tem um desempenho melhor porque estamos agregando na consulta externa, não mais na consulta interna.
O mesmo parece se aplicar se reescrevermos o LEFT JOIN LATERAL
seguinte:
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
Aqui, subimos array_agg()
. Como você pode ver, esse plano também é melhor que o original LEFT JOIN LATERAL
.
Dito isso, não tenho certeza se essa regra prática auto-inventada ( aplique a função agregada o mais alto / tarde possível ) é verdadeira em outros casos.
informação adicional
Violino: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb
Versão: PostgreSQL 10.4 no x86_64-pc-linux-musl, compilado pelo gcc (Alpine 6.4.0) 6.4.0, 64 bits
Ambiente: Docker: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila
. Observe que a imagem no hub do Docker está desatualizada, então fiz uma compilação localmente primeiro: build -t frantiseks/postgres-sakila
depois de clonar o repositório git.
Definições da tabela:
filme
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
ator
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
Dados: este é do banco de dados de amostra Sakila. Esta pergunta não é um caso da vida real, estou usando esse banco de dados principalmente como um banco de dados de exemplo de aprendizado. Fui apresentado ao SQL há alguns meses e estou tentando expandir meu conhecimento. Possui as seguintes distribuições:
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
Respostas:
Configuração de teste
Sua configuração original no violino deixa espaço para melhorias. Eu ficava pedindo sua configuração por um motivo.
Você tem esses índices em
film_actor
:O que já é bastante útil. Mas, para melhor apoiar a sua consulta particular, você teria um índice de várias colunas em
(film_id, actor_id)
, colunas nesta ordem. Uma solução prática: substituaidx_fk_film_id
por um índice(film_id, actor_id)
- ou crie a PK(film_id, actor_id)
para a finalidade deste teste, como faço abaixo. Vejo:Em um modo somente leitura (ou principalmente, ou geralmente quando o VACUUM pode acompanhar a atividade de gravação), também ajuda a ter um índice ativado
(title, film_id)
para permitir varreduras somente no índice. Meu caso de teste agora está altamente otimizado para desempenho de leitura.Digite incompatibilidade entre
film.film_id
(integer
) efilm_actor.film_id
(smallint
). Enquanto isso funciona , torna as consultas mais lentas e pode levar a várias complicações. Também torna as restrições de FK mais caras. Nunca faça isso se puder ser evitado. Se você não tem certeza, escolherinteger
maissmallint
. Emborasmallint
possa salvar 2 bytes por campo (geralmente consumido pelo preenchimento de alinhamento), há mais complicações do que cominteger
.Para otimizar o desempenho do teste em si, crie índices e restrições após a inserção em massa de muitas linhas. É substancialmente mais lento adicionar tuplas incrementalmente aos índices existentes do que criá-las do zero com todas as linhas presentes.
Não relacionado a este teste:
Sequências independentes e padrões de coluna em vez de colunas
serial
(ouIDENTITY
) muito mais simples e confiáveis . Não.timestamp without timestamp
normalmente não é confiável para uma coluna comolast_update
. Use emtimestamptz
vez disso. E observe que os padrões da coluna não cobrem a "última atualização", estritamente falando.O modificador de comprimento
character varying(255)
indica que o caso de teste não se destina ao Postgres, porque o comprimento ímpar é bastante inútil aqui. (Ou o autor não tem noção.)Considere o caso de teste auditado no violino:
db <> fiddle here - desenvolvendo seu fiddle, otimizado e com consultas adicionais.
Palavras-chave:
Uma configuração de teste com 1000 filmes e 200 atores tem validade limitada. As consultas mais eficientes levam <0,2 ms. O tempo de planejamento é superior ao tempo de execução. Um teste com 100 mil ou mais linhas seria mais revelador.
Por que recuperar apenas o primeiro nome dos autores? Depois de recuperar várias colunas, você já tem uma situação um pouco diferente.
ORDER BY title
não faz sentido ao filtrar um único título comWHERE title = 'ACADEMY DINOSAUR'
. TalvezORDER BY film_id
?E para o tempo de execução total, use
EXPLAIN (ANALYZE, TIMING OFF)
para reduzir o ruído (potencialmente enganoso) com sobrecarga de sub-temporização.Responda
É difícil formar uma regra simples, porque o desempenho total depende de muitos fatores. Diretrizes muito básicas:
A agregação de todas as linhas em sub-tabelas carrega menos sobrecarga, mas é paga apenas quando você realmente precisa de todas as linhas (ou uma parte muito grande).
Para selecionar poucas linhas (seu teste!), Diferentes técnicas de consulta produzem melhores resultados. É aí que
LATERAL
entra. Ele carrega mais sobrecarga, mas apenas lê as linhas necessárias das sub-tabelas. Uma grande vitória se apenas uma fração (muito) pequena for necessária.Para o seu caso de teste específico, eu também testaria um construtor ARRAY na
LATERAL
subconsulta :Embora apenas agregue uma única matriz na subconsulta lateral, um construtor ARRAY simples executa melhor que a função agregada
array_agg()
. Vejo:Ou com uma subconsulta pouco correlacionada para o caso simples:
Ou, basicamente, apenas 2x
LEFT JOIN
e depois agregue :Esses três parecem mais rápidos no meu violino atualizado (planejamento + tempo de execução).
Sua primeira tentativa (apenas ligeiramente modificada) geralmente é mais rápida para recuperar todos ou a maioria dos filmes , mas não para uma pequena seleção:
Testes com cardinalidades muito maiores serão mais reveladores. E não generalize os resultados de ânimo leve, há muitos fatores para o desempenho total.
fonte