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


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.