Если результат необходим в отсортированном виде, оптимизатор может предпочесть соединение слиянием. Особенно, если данные от дочерних узлов можно получить уже отсортированными с помощью индексного сканирования:
=> EXPLAIN (COSTS OFF) SELECT * FROM tickets t JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no ORDER BY t.ticket_no;
QUERY PLAN ----------------------------------------------------------------- Merge Join Merge Cond: (t.ticket_no = tf.ticket_no) -> Index Scan using tickets_pkey on tickets t -> Index Scan using ticket_flights_pkey on ticket_flights tf (4 rows)
Посмотрим на стоимость:
=> EXPLAIN SELECT * FROM tickets t JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no ORDER BY t.ticket_no;
QUERY PLAN --------------------------------------------------------------------------------------------------------------- Merge Join (cost=38.10..787871.36 rows=8391906 width=150) Merge Cond: (t.ticket_no = tf.ticket_no) -> Index Scan using tickets_pkey on tickets t (cost=0.43..138478.98 rows=2949570 width=104) -> Index Scan using ticket_flights_pkey on ticket_flights tf (cost=0.56..537184.08 rows=8391906 width=32) (4 rows)
Первая компонента включает:
Вторая компонента стоимости складывается из:
Общий вывод: стоимость соединения слиянием пропорциональна N + M (где N и M - число соединяемых строк), если не требуется отдельная сортировка. Сортировка набора из K строк добавляет к оценке как минимум K * log(K).
В отличие от соединения хешированием, слияние без сортировки хорошо подходит для случая, когда надо быстро получить первые строки.
=> EXPLAIN SELECT * FROM tickets t JOIN ticket_flights tf ON tf.ticket_no = t.ticket_no ORDER BY t.ticket_no LIMIT 1000;
QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Limit (cost=38.10..131.98 rows=1000 width=150) -> Merge Join (cost=38.10..787871.36 rows=8391906 width=150) Merge Cond: (t.ticket_no = tf.ticket_no) -> Index Scan using tickets_pkey on tickets t (cost=0.43..138478.98 rows=2949570 width=104) -> Index Scan using ticket_flights_pkey on ticket_flights tf (cost=0.56..537184.08 rows=8391906 width=32) (5 rows)
Обратите внимание и на то, как уменьшилась общая стоимость.
Если нужного индекса не окажется, придется выполнять сортировку в узле Sort. Тогда первая компонента стоимости будет не меньше стоимости сортировки, и первые строки запрос не сможет выдавать без задержки.
Вот пример такого плана (здесь явная сортировка выбрана из-за небольшого размера таблицы):
=> EXPLAIN SELECT * FROM aircrafts a JOIN seats s ON a.aircraft_code = s.aircraft_code ORDER BY a.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------------------- Merge Join (cost=1.51..420.71 rows=1339 width=83) Merge Cond: (s.aircraft_code = ml.aircraft_code) -> Index Scan using seats_pkey on seats s (cost=0.28..64.60 rows=1339 width=15) -> Sort (cost=1.23..1.26 rows=9 width=52) Sort Key: ml.aircraft_code -> Seq Scan on aircrafts_data ml (cost=0.00..1.09 rows=9 width=52) (6 rows)
Как мы видели, для устранения дубликатов может использоваться хеширование. Другой способ - сортировка значений:
=> EXPLAIN SELECT DISTINCT book_date FROM bookings ORDER BY book_date;
QUERY PLAN ------------------------------------------------------------------------------ Unique (cost=314054.67..324610.22 rows=441843 width=8) -> Sort (cost=314054.67..319332.45 rows=2111110 width=8) Sort Key: book_date -> Seq Scan on bookings (cost=0.00..34558.10 rows=2111110 width=8) (4 rows)
Сортировка и устранение дубликатов происходит в узле Unique. Для группировки будет использоваться узел с другим названием - GroupAggregate.
Такой способ особенно выгоден, если требуется получить отсортированный результат (как в данном случае).
Еще один случай, когда сортировка оказывается выгодней - ограниченная оперативная память: алгоритм внешней сортировки работает эффективней, чем хеш-соединение с использованием нескольких пакетов.
Включим сообщения о временных файлах и посмотрим, как выполняется сортировка.
=> SET log_temp_files = 0;
SET
=> EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE) SELECT DISTINCT book_date FROM bookings;
QUERY PLAN ---------------------------------------------------------------- Unique (actual rows=540474 loops=1) -> Sort (actual rows=2111110 loops=1) Sort Key: book_date Sort Method: external merge Disk: 37248kB -> Seq Scan on bookings (actual rows=2111110 loops=1) Planning time: 0.056 ms Execution time: 2397.735 ms (7 rows)
Строки не помещаются в доступную память и используется внешняя сортировка (Sort Method: external merge).
Также в журнале сообщений мы видим запись о временном файле:
postgres$ tail -n 4 /var/log/postgresql/postgresql-10-main.log
FROM bookings b JOIN tickets t ON b.book_ref = t.book_ref; 2019-02-13 13:05:45.184 MSK [7523] postgres@demo LOG: temporary file: path "base/pgsql_tmp/pgsql_tmp7523.0", size 38141952 2019-02-13 13:05:45.184 MSK [7523] postgres@demo STATEMENT: EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE) SELECT DISTINCT book_date FROM bookings;
Запись появляется, когда файл освобождается, поэтому его размер известен.
А теперь увеличим work_mem:
=> SET work_mem = '32MB';
SET
=> EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE) SELECT DISTINCT book_date FROM bookings;
QUERY PLAN ---------------------------------------------------------- HashAggregate (actual rows=540474 loops=1) Group Key: book_date -> Seq Scan on bookings (actual rows=2111110 loops=1) Planning time: 0.068 ms Execution time: 1190.318 ms (5 rows)
Поскольку сортировка не требуется и памяти достаточно, планировщик переключился на использование хеширования.
Но если добавить предложение ORDER BY, планировщик возвращается к сортировке:
=> EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE) SELECT DISTINCT book_date FROM bookings ORDER BY book_date;
QUERY PLAN ---------------------------------------------------------------- Sort (actual rows=540474 loops=1) Sort Key: book_date Sort Method: quicksort Memory: 30475kB -> HashAggregate (actual rows=540474 loops=1) Group Key: book_date -> Seq Scan on bookings (actual rows=2111110 loops=1) Planning time: 0.064 ms Execution time: 1620.304 ms (8 rows)
Теперь все строки поместились в память (Sort Method: quicksort).
Пример запроса с двумя соединениями слиянием (номера билетов и места в салоне):
=> EXPLAIN (COSTS OFF) SELECT t.ticket_no, bp.flight_id, bp.seat_no FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no JOIN boarding_passes bp ON bp.ticket_no = tf.ticket_no AND bp.flight_id = tf.flight_id ORDER BY t.ticket_no;
QUERY PLAN -------------------------------------------------------------------------------- Merge Join Merge Cond: ((t.ticket_no = tf.ticket_no) AND (bp.flight_id = tf.flight_id)) -> Merge Join Merge Cond: (bp.ticket_no = t.ticket_no) -> Index Scan using boarding_passes_pkey on boarding_passes bp -> Index Only Scan using tickets_pkey on tickets t -> Index Only Scan using ticket_flights_pkey on ticket_flights tf (7 rows)
Здесь соединяются билеты (tickets) и посадочные талоны (boarding_passes), и с этим, уже отсортированным по номерам билетов, набором строк соединяются перелеты (ticket_flights).
Конец демонстрации.