IN, JOIN и EXISTS. Есть ли разница?!
Периодически встречаю такое поверье - логически эквивалентные запросы на одних и тех же данных будут физически по-разному выполняться движком СУБД. В зависимости от того, как записано условие - через in, join или exists.
Вот буквально в этом месяце на техническом собеседовании в Тинькофф собеседующий мне это утверждал.
У меня на ноуте установлен PostgreSQL, проверим для этой СУБД.
--у меня установлен PostgreSQL 12.6 select version() --PostgreSQL 12.6, compiled by Visual C++ build 1914, 64-bit
Будут использоваться таблицы из демонстрационной базы https://postgrespro.ru/education/demodb
Задача 1 - вернуть информацию из таблицы ticket_flights для всех пассажиров ALEKSANDRA ARKHIPOVA. Всего в таблице bookings.tickets 124 таких записи.
1.1. Используем join и смотрим план запроса
explain (analyze, costs off, timing off) select tf.* from bookings.ticket_flights tf join bookings.tickets t on t.ticket_no = tf.ticket_no where t.passenger_name = 'ALEKSANDRA ARKHIPOVA' /*Gather (actual rows=343 loops=1) Workers Planned: 2 Workers Launched: 2 -> Nested Loop (actual rows=114 loops=3) -> Parallel Seq Scan on tickets t (actual rows=41 loops=3) Filter: (passenger_name = 'ALEKSANDRA ARKHIPOVA'::text) Rows Removed by Filter: 983244 -> Index Scan using ticket_flights_pkey on ticket_flights tf (actual rows=3 loops=124) Index Cond: (ticket_no = t.ticket_no) Planning Time: 10.497 ms Execution Time: 390.187 ms*/
explain (analyze, costs off, timing off) select tf.* from bookings.ticket_flights tf where tf.ticket_no in (select t.ticket_no from bookings.tickets t where t.passenger_name = 'ALEKSANDRA ARKHIPOVA') /*Gather (actual rows=343 loops=1) Workers Planned: 2 Workers Launched: 2 -> Nested Loop (actual rows=114 loops=3) -> Parallel Seq Scan on tickets t (actual rows=41 loops=3) Filter: (passenger_name = 'ALEKSANDRA ARKHIPOVA'::text) Rows Removed by Filter: 983244 -> Index Scan using ticket_flights_pkey on ticket_flights tf (actual rows=3 loops=124) Index Cond: (ticket_no = t.ticket_no) Planning Time: 0.327 ms Execution Time: 361.855 ms*/
explain (analyze, costs off, timing off) select tf.* from bookings.ticket_flights tf where exists (select 1 from bookings.tickets t where t.passenger_name = 'ALEKSANDRA ARKHIPOVA' and t.ticket_no = tf.ticket_no) /*Gather (actual rows=343 loops=1) Workers Planned: 2 Workers Launched: 2 -> Nested Loop (actual rows=114 loops=3) -> Parallel Seq Scan on tickets t (actual rows=41 loops=3) Filter: (passenger_name = 'ALEKSANDRA ARKHIPOVA'::text) Rows Removed by Filter: 983244 -> Index Scan using ticket_flights_pkey on ticket_flights tf (actual rows=3 loops=124) Index Cond: (ticket_no = t.ticket_no) Planning Time: 0.337 ms Execution Time: 335.240 ms*/
Видно, что во всех 3 случаях был применён один и тот же план, оптимизатор выбрал реализацию соединения через nested loop.
Задача 2 - вернуть информацию из таблицы ticket_flights для всех пассажиров c именами, начинающимися на заглавную A. Всего в таблице bookings.tickets 563814 таких записи (т.е. много).
explain (analyze, costs off, timing off) select tf.* from bookings.ticket_flights tf join bookings.tickets t on t.ticket_no = tf.ticket_no where t.passenger_name like 'A%' /*Hash Join (actual rows=1604919 loops=1) Hash Cond: (tf.ticket_no = t.ticket_no) -> Seq Scan on ticket_flights tf (actual rows=8391852 loops=1) -> Hash (actual rows=563814 loops=1) Buckets: 131072 Batches: 16 Memory Usage: 2606kB -> Seq Scan on tickets t (actual rows=563814 loops=1) Filter: (passenger_name ~~ 'A%'::text) Rows Removed by Filter: 2386043 Planning Time: 11.102 ms Execution Time: 10048.527 ms*/
explain (analyze, costs off, timing off) select tf.* from bookings.ticket_flights tf where tf.ticket_no in (select t.ticket_no from bookings.tickets t where t.passenger_name like 'A%') /*Hash Join (actual rows=1604919 loops=1) Hash Cond: (tf.ticket_no = t.ticket_no) -> Seq Scan on ticket_flights tf (actual rows=8391852 loops=1) -> Hash (actual rows=563814 loops=1) Buckets: 131072 Batches: 16 Memory Usage: 2606kB -> Seq Scan on tickets t (actual rows=563814 loops=1) Filter: (passenger_name ~~ 'A%'::text) Rows Removed by Filter: 2386043 Planning Time: 0.312 ms Execution Time: 8913.533 ms*/
explain (analyze, costs off, timing off) select tf.* from bookings.ticket_flights tf where exists (select 1 from bookings.tickets t where t.passenger_name like 'A%' and t.ticket_no = tf.ticket_no) /*Hash Join (actual rows=1604919 loops=1) Hash Cond: (tf.ticket_no = t.ticket_no) -> Seq Scan on ticket_flights tf (actual rows=8391852 loops=1) -> Hash (actual rows=563814 loops=1) Buckets: 131072 Batches: 16 Memory Usage: 2606kB -> Seq Scan on tickets t (actual rows=563814 loops=1) Filter: (passenger_name ~~ 'A%'::text) Rows Removed by Filter: 2386043 Planning Time: 0.341 ms Execution Time: 8580.005 ms*/
Также во всех 3 случаях был применён один и тот же план, оптимизатор выбрал реализацию соединения hash join.
Итого, для одинаковых логически запросов были выполнены одинаковые планы, вне зависимости от того, как было оформлено условие на датасет - через in, join или exists. Для маленького датасета была выбрана реализация nested loop, для большого - hash join.
Возможно, для каких-то старых версий каких-то СУБД поверье из преамбулы является реальностью. Помню, сам на ms sql 2008 in на join переписывал, и вроде как помогало.