У меня есть следующие таблицы (взяты из базы данных Sakila):
- film: film_id это pkey
- actor: actor_id - это pkey
- film_actor: film_id и actor_id - это ключи к фильму / актеру
Я выбираю конкретный фильм. Для этого фильма я также хочу, чтобы все актеры участвовали в этом фильме. У меня есть два запроса для этого: один с а LEFT JOIN
и один с 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;
При сравнении плана запроса первый запрос работает намного хуже (в 20 раз), чем второй:
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
Почему это? Я хочу научиться рассуждать об этом, чтобы понять, что происходит, и предсказать, как будет вести себя запрос при увеличении размера данных и какие решения примет планировщик при определенных условиях.
Мои мысли: в первом LEFT JOIN
запросе похоже, что подзапрос выполняется для всех фильмов в базе данных, без учета фильтрации внешнего запроса, который нас интересует только для одного конкретного фильма. Почему планировщик не может получить эти знания в подзапросе?
В LEFT JOIN LATERAL
запросе мы более или менее «проталкиваем» эту фильтрацию вниз. Таким образом, проблема, с которой мы столкнулись в первом запросе, здесь не присутствует, следовательно, лучшая производительность.
Я думаю, что я в основном ищу правило большого пальца, общие мудрости ... так что эта магия планировщика становится второй натурой - если это имеет смысл.
обновление (1)
Переписав LEFT JOIN
следующее, вы получите лучшую производительность (чуть лучше, чем 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
Как мы можем рассуждать об этом?
обновление (2)
Я продолжил некоторые эксперименты и думаю, что интересное эмпирическое правило таково: применяйте агрегатную функцию настолько высоко / поздно, насколько это возможно . Запрос в update (1), вероятно, работает лучше, потому что мы агрегируем во внешнем запросе, а не во внутреннем запросе.
То же самое применимо, если мы переписали LEFT JOIN LATERAL
вышеизложенное следующим образом:
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
Здесь мы двинулись array_agg()
вверх. Как видите, этот план тоже лучше оригинала LEFT JOIN LATERAL
.
Тем не менее, я не уверен, является ли это эмпирическое правило ( примененное к статистической функции как можно выше / позднее ) верным в других случаях.
Дополнительная информация
Скрипка: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=4ec4f2fffd969d9e4b949bb2ca765ffb
Версия: PostgreSQL 10.4 на x86_64-pc-linux-musl, скомпилированная gcc (Alpine 6.4.0) 6.4.0, 64-битная
Окружающая среда: Docker: docker run -e POSTGRES_PASSWORD=sakila -p 5432:5432 -d frantiseks/postgres-sakila
. Обратите внимание, что образ в Docker-хабе устарел, поэтому сначала я сделал сборку локально: build -t frantiseks/postgres-sakila
после клонирования репозитория git.
Табличные определения:
фильм
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_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
Данные: это из базы данных Sakila. Этот вопрос не является реальным случаем, я использую эту базу данных в основном в качестве учебной базы данных. Я познакомился с SQL несколько месяцев назад и пытаюсь расширить свои знания. Имеет следующие дистрибутивы:
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
Ответы:
Испытательная установка
Ваша оригинальная установка в скрипке оставляет место для улучшения. Я продолжал спрашивать о вашей настройке по причине.
У вас есть эти индексы на
film_actor
:Что уже очень полезно. Но лучшей поддержка вашего конкретного запроса, вы бы индекс многоколоночного на
(film_id, actor_id)
, столбцы в указанном порядке. Практическое решение: заменитеidx_fk_film_id
индекс на(film_id, actor_id)
- или создайте PK(film_id, actor_id)
для целей этого теста, как я делаю ниже. Видеть:В режиме «только для чтения» (или, в основном, или обычно, когда VACUUM может справиться с операцией записи), также помогает включить индексирование,
(title, film_id)
чтобы разрешить сканирование только по индексу. Мой тестовый пример теперь сильно оптимизирован для производительности чтения.Несоответствие типов между
film.film_id
(integer
) иfilm_actor.film_id
(smallint
). Хотя это работает, это замедляет запросы и может привести к различным осложнениям. Также делает ограничения FK более дорогими. Никогда не делайте этого, если этого можно избежать. Если вы не уверены, выбратьinteger
болееsmallint
. Хотяsmallint
можно сэкономить 2 байта на поле (часто потребляемое выравниванием выравнивания), сложностей больше, чем сinteger
.Чтобы оптимизировать производительность самого теста, создайте индексы и ограничения после массовой вставки большого количества строк. Существенно медленнее постепенно добавлять кортежи к существующим индексам, чем создавать их с нуля при наличии всех строк.
Не связанные с этим тестом:
Отдельно стоящие последовательности плюс значения по умолчанию для столбцов вместо гораздо более простых и надежных
serial
(илиIDENTITY
) столбцов. Не.timestamp without timestamp
обычно ненадежен для столбца, какlast_update
. Используйтеtimestamptz
вместо этого. И обратите внимание, что по умолчанию столбец не охватывает «последнее обновление», строго говоря.Модификатор длины в
character varying(255)
указывает, что тестовый пример не предназначен для Postgres, потому что нечетная длина здесь довольно бессмысленна. (Или автор не имеет понятия.)Рассмотрим проверенный контрольный пример в скрипте:
db <> fiddle here - построение на вашей скрипке, оптимизированное и с добавленными запросами.
Связанный:
Тестовая установка с 1000 фильмами и 200 актерами имеет ограниченный срок действия. Наиболее эффективные запросы занимают <0,2 мс. Время планирования больше времени выполнения. Тест с 100k или более строк будет более показательным.
Зачем искать только имена авторов? Как только вы получите несколько столбцов, у вас уже будет немного другая ситуация.
ORDER BY title
не имеет смысла при фильтрации для одного заголовка сWHERE title = 'ACADEMY DINOSAUR'
. Может бытьORDER BY film_id
?А для общего времени выполнения лучше использовать
EXPLAIN (ANALYZE, TIMING OFF)
для уменьшения (потенциально вводящего в заблуждение) шум с дополнительными затратами времени.Ответ
Сложно составить простое правило, потому что общая производительность зависит от многих факторов. Очень основные рекомендации:
Агрегирование всех строк в подстолбьях требует меньше накладных расходов, но платит только тогда, когда вам действительно нужны все строки (или очень большая часть).
Для выбора нескольких строк (ваш тест!) Различные методы запроса дают лучшие результаты. Вот где
LATERAL
приходит. Он несет больше накладных расходов, но читает только необходимые строки из вложенных таблиц. Большой выигрыш, если нужна только (очень) маленькая фракция.Для вашего конкретного теста я также протестировал бы конструктор ARRAY в
LATERAL
подзапросе :При агрегировании только одного массива в боковом подзапросе простой конструктор ARRAY работает лучше, чем функция агрегирования
array_agg()
. Видеть:Или со слабо коррелированным подзапросом для простого случая:
Или, в принципе, просто 2х,
LEFT JOIN
а затем агрегировать :Эти три кажутся самыми быстрыми в моей обновленной скрипке (планирование + время выполнения).
Ваша первая попытка (только слегка измененная), как правило, быстрее всего получить все или большинство фильмов , но не для небольшого выбора:
Тесты с гораздо большим количеством элементов будут более показательными. И не обобщайте результаты слегка, есть много факторов для общей производительности.
источник