Чому ця ліва приєднана працює набагато гірше, ніж ЛІВНЕ ПРИЄДНАЙТЕСЬ ЛАТЕРАЛ?


13

У мене є такі таблиці (взяті з бази даних Sakila):

  • фільм: film_id є pkey
  • актор: glum_id - це кек
  • film_actor: film_id та act_id - фійки для фільму / актора

Я підбираю конкретний фільм. Для цього фільму я також хочу всіх акторів, які беруть участь у цьому фільмі. У мене є два запити для цього: один з a LEFT JOINі один з 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;

Порівнюючи план запитів, перший запит виконує набагато гірше (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)

Я продовжував деякі експерименти, і думаю, що цікавим правилом є: застосувати функцію агрегату якомога вище / пізніше . Запит в оновлення (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.

З цього приводу я не впевнений, чи справжнє це власне винахідне правило ( застосувати функцію сукупності якомога вище / пізніше ) вірно в інших випадках.

Додаткова інформація

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

кіноактор

 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

1
І ще одне: вся важлива інформація повинна містити питання (включаючи ваш скрипковий посилання). Ніхто не захоче прочитати всі коментарі пізніше (або їх все одно видалить якийсь дуже здатний модератор).
Ервін Брандстеттер

Fiddle додано до питання!
Jelly Orns

Відповіді:


7

Налаштування тесту

Ваша оригінальна настройка у скрипці залишає місце для вдосконалення. Я постійно просив вашої настройки з причини.

  • Ці показники у вас є film_actor:

    "film_actor_pkey" PRIMARY KEY, btree (actor_id, film_id)  
    "idx_fk_film_id" btree (film_id)

    Що вже досить корисно. Але найкращою підтримка вашого конкретного запиту, ви б індекс многоколоночной на (film_id, actor_id), стовпці в зазначеному порядку. Практичне рішення: замініть idx_fk_film_idіндекс на (film_id, actor_id)- або створіть ПК (film_id, actor_id)для цілі цього тесту, як я це робив нижче. Побачити:

    У режимі лише для читання (або в основному, або загалом, коли VACUUM може бути в курсі активності запису), це також допомагає мати індекс, (title, film_id)щоб дозволити сканування лише індексу. Мій тестовий випадок зараз оптимізований для продуктивності читання.

  • Введіть невідповідність між film.film_id( integer) та film_actor.film_id( smallint). Хоча це працює, запити роблять повільніше і можуть призвести до різних ускладнень. Також робить обмеження ФК дорожчими. Ніколи цього не робіть, якщо цього можна уникнути. Якщо ви не впевнені, вибрати integerбільш smallint. Хоча ви smallint можете зберегти 2 байти на поле (часто споживається накладання на вирівнювання), є більше ускладнень, ніж з integer.

  • Щоб оптимізувати продуктивність самого тесту, створіть індекси та обмеження після масового вставки багато рядків. Набагато повільніше додавати кортежі поступово до існуючих індексів, ніж створювати їх з нуля з усіма присутніми рядками.

Не пов'язані з цим тестом:

  • Вільно стоячі послідовності плюс параметри за замовчуванням замість набагато простіших і надійніших serial(або IDENTITY) стовпців. Не варто.

  • timestamp without timestampзазвичай ненадійний для стовпця типу last_update. Використовуйте timestamptzзамість цього. І зауважте, що параметри за замовчуванням стовпців не покривають "останнього оновлення", строго кажучи.

  • Модифікатор довжини character varying(255)вказує на те, що тестовий випадок не призначений для початку Postgres, оскільки непарна довжина тут досить безглузда. (Або автор не зрозумілий.)

Розглянемо перевірений тестовий випадок у скрипці:

db <> fiddle here - спираючись на вашу скрипку, оптимізовану та із доданими запитами.

Пов'язані:

Тестова установка на 1000 фільмів та 200 акторів має обмежене значення. Найбільш ефективні запити займають <0,2 мс. Час планування - це більше, ніж час виконання. Тест із 100 і більше рядків був би більш показовим.

Навіщо отримувати лише імена авторів? Після отримання кількох стовпців у вас вже є дещо інша ситуація.

ORDER BY titleне має сенсу під час фільтрування одного заголовка за допомогою WHERE title = 'ACADEMY DINOSAUR'. Можливо ORDER BY film_id?

А для загального часу виконання, скоріше, використовуйте EXPLAIN (ANALYZE, TIMING OFF)для зменшення (потенційно оманливого) шуму з накладними накладними режимами.

Відповідь

Важко сформувати просте правило, оскільки загальна продуктивність залежить від багатьох факторів. Дуже основні рекомендації:

  • Агрегація всіх рядків у підтаблицях має менші накладні витрати, але платить лише тоді, коли вам потрібні всі рядки (або дуже велика частина).

  • Для вибору кількох рядків (ваш тест!) Різні методи запитів дають кращі результати. Ось де це LATERALвходить. Він містить більше накладних витрат, але читає лише необхідні рядки з підтаблиць. Великий виграш, якщо потрібна лише (дуже) мала частка.

Для вашого конкретного тестового випадку я також би протестував конструктор ARRAY у LATERALпідзапиті :

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 

Простий конструктор ARRAY виконує лише агрегацію одного масиву в бічному підзапиті array_agg(). Побачити:

Або з низько корельованим підзапитом для простого випадку:

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';

Або, в основному, просто 2x LEFT JOINі потім агрегувати :

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;

Ці три здаються найшвидшими в моєму оновленому скрипці (планування + час виконання).

Ваша перша спроба (лише злегка модифікована), як правило, найшвидша для отримання всіх або більшості фільмів , але не для невеликого вибору:

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!

Випробування зі значно більшими кардинальністю будуть більш показовими. І не узагальнюйте результати легко, є багато факторів для загальної продуктивності.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.