Задача: вычислить среднюю стоимость билетов на перелеты, выполняемые различными типами самолетов. При этом исключить самые дешевые и самые дорогие билеты.
Сразу отключим параллельное выполнение. В виртуальной машине с одним ядром оно не поможет, а параллельные планы сложнее читать.
=> SET max_parallel_workers_per_gather = 0;
SET
Начинаем со следующего запроса, который решает задачу, но работает медленно:
=> \timing on
Timing is on.
=> SELECT a.aircraft_code, ( SELECT round(avg(tf.amount)) FROM flights f JOIN ticket_flights tf ON tf.flight_id = f.flight_id WHERE f.aircraft_code = a.aircraft_code AND tf.amount > (SELECT min(amount) FROM ticket_flights) AND tf.amount < (SELECT max(amount) FROM ticket_flights) ) FROM aircrafts a GROUP BY a.aircraft_code;
aircraft_code | round ---------------+------- 319 | 51546 320 | 321 | 15666 733 | 16545 763 | 34262 773 | 23783 CN1 | 6586 CR2 | 13228 SU9 | 14265 (9 rows) Time: 36559,895 ms (00:36,560)
=> \timing off
Timing is off.
Посмотрим схему плана выполнения:
=> EXPLAIN (COSTS OFF) SELECT a.aircraft_code, ( SELECT round(avg(tf.amount)) FROM flights f JOIN ticket_flights tf ON tf.flight_id = f.flight_id WHERE f.aircraft_code = a.aircraft_code AND tf.amount > (SELECT min(amount) FROM ticket_flights) AND tf.amount < (SELECT max(amount) FROM ticket_flights) ) FROM aircrafts a GROUP BY a.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------ Group Group Key: ml.aircraft_code -> Index Only Scan using aircrafts_pkey on aircrafts_data ml SubPlan 3 -> Aggregate InitPlan 1 (returns $0) -> Aggregate -> Seq Scan on ticket_flights InitPlan 2 (returns $1) -> Aggregate -> Seq Scan on ticket_flights ticket_flights_1 -> Hash Join Hash Cond: (tf.flight_id = f.flight_id) -> Seq Scan on ticket_flights tf Filter: ((amount > $0) AND (amount < $1)) -> Hash -> Seq Scan on flights f Filter: (aircraft_code = ml.aircraft_code) (18 rows)
Можно заметить, что для вычисления минимума и максимума выполняется полное сканирование таблицы ticket_flights. Создание индекса должно ускорить эту операцию:
=> CREATE INDEX ON ticket_flights(amount);
CREATE INDEX
=> EXPLAIN (ANALYZE, TIMING OFF) SELECT a.aircraft_code, ( SELECT round(avg(tf.amount)) FROM flights f JOIN ticket_flights tf ON tf.flight_id = f.flight_id WHERE f.aircraft_code = a.aircraft_code AND tf.amount > (SELECT min(amount) FROM ticket_flights) AND tf.amount < (SELECT max(amount) FROM ticket_flights) ) FROM aircrafts a GROUP BY a.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Group (cost=0.14..613333.61 rows=9 width=48) (actual rows=9 loops=1) Group Key: ml.aircraft_code -> Index Only Scan using aircrafts_pkey on aircrafts_data ml (cost=0.14..12.27 rows=9 width=16) (actual rows=9 loops=1) Heap Fetches: 9 SubPlan 5 -> Aggregate (cost=68146.80..68146.81 rows=1 width=32) (actual rows=1 loops=9) InitPlan 2 (returns $1) -> Result (cost=0.49..0.50 rows=1 width=32) (actual rows=1 loops=1) InitPlan 1 (returns $0) -> Limit (cost=0.43..0.49 rows=1 width=6) (actual rows=1 loops=1) -> Index Only Scan using ticket_flights_amount_idx on ticket_flights (cost=0.43..488182.63 rows=8391852 width=6) (actual rows=1 loops=1) Index Cond: (amount IS NOT NULL) Heap Fetches: 1 InitPlan 4 (returns $3) -> Result (cost=0.49..0.50 rows=1 width=32) (actual rows=1 loops=1) InitPlan 3 (returns $2) -> Limit (cost=0.43..0.49 rows=1 width=6) (actual rows=1 loops=1) -> Index Only Scan Backward using ticket_flights_amount_idx on ticket_flights ticket_flights_1 (cost=0.43..488182.63 rows=8391852 width=6) (actual rows=1 loops=1) Index Cond: (amount IS NOT NULL) Heap Fetches: 1 -> Hash Join (cost=6332.08..68132.68 rows=5245 width=6) (actual rows=925529 loops=9) Hash Cond: (tf.flight_id = f.flight_id) -> Bitmap Heap Scan on ticket_flights tf (cost=894.51..62584.97 rows=41959 width=10) (actual rows=8329761 loops=8) Recheck Cond: ((amount > $1) AND (amount < $3)) Heap Blocks: exact=528624 -> Bitmap Index Scan on ticket_flights_amount_idx (cost=0.00..884.02 rows=41959 width=0) (actual rows=8329761 loops=8) Index Cond: ((amount > $1) AND (amount < $3)) -> Hash (cost=5101.84..5101.84 rows=26858 width=4) (actual rows=23874 loops=9) Buckets: 65536 (originally 32768) Batches: 1 (originally 1) Memory Usage: 1551kB -> Seq Scan on flights f (cost=0.00..5101.84 rows=26858 width=4) (actual rows=23874 loops=9) Filter: (aircraft_code = ml.aircraft_code) Rows Removed by Filter: 190993 Planning time: 1.396 ms Execution time: 67516.750 ms (34 rows)
Все только ухудшилось. Почему?
Теперь все три обращения к таблице ticket_flights выполняются с помощью индекса. Но в случае tf планировщик сильно ошибается с оценкой кардинальности: вместо ожидаемых 40 тыс строк читаются практически все строки из 8 млн. Очевидно, что использовать для этого индекс неэффективно.
Запретить индексный доступ можно разными средствами. Например, переписать условие так, чтобы индекс не мог применяться (добавить 0 к amount слева от оператора сравнения).
Но лучше исправить оценку планировщика. Сейчас он не знает значений, которые будут выбраны подзапросами, потому оценивает "из общих соображений", но их можно вычислить заранее и подставить в основной запрос.
Чтобы получить согласованные данные, используем уровень изоляции Repeatable Read:
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT max(amount) AS a_max, min(amount) AS a_min FROM ticket_flights \gset
=> EXPLAIN (ANALYZE, TIMING OFF) SELECT a.aircraft_code, ( SELECT round(avg(tf.amount)) FROM flights f JOIN ticket_flights tf ON tf.flight_id = f.flight_id WHERE f.aircraft_code = a.aircraft_code AND tf.amount > :a_min AND tf.amount < :a_max ) FROM aircrafts a GROUP BY a.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------- Group (cost=0.14..1996648.19 rows=9 width=48) (actual rows=9 loops=1) Group Key: ml.aircraft_code -> Index Only Scan using aircrafts_pkey on aircrafts_data ml (cost=0.14..12.27 rows=9 width=16) (actual rows=9 loops=1) Heap Fetches: 9 SubPlan 1 -> Aggregate (cost=221848.42..221848.43 rows=1 width=32) (actual rows=1 loops=9) -> Hash Join (cost=5437.56..219246.89 rows=1040610 width=6) (actual rows=925529 loops=9) Hash Cond: (tf.flight_id = f.flight_id) -> Seq Scan on ticket_flights tf (cost=0.00..191955.78 rows=8324997 width=10) (actual rows=8329761 loops=8) Filter: ((amount > 3000.00) AND (amount < 203300.00)) Rows Removed by Filter: 62091 -> Hash (cost=5101.84..5101.84 rows=26858 width=4) (actual rows=23874 loops=9) Buckets: 65536 (originally 32768) Batches: 1 (originally 1) Memory Usage: 1551kB -> Seq Scan on flights f (cost=0.00..5101.84 rows=26858 width=4) (actual rows=23874 loops=9) Filter: (aircraft_code = ml.aircraft_code) Rows Removed by Filter: 190993 Planning time: 1.758 ms Execution time: 27912.127 ms (18 rows)
Теперь оценки кардинальности исправились и планировщик сам предпочел полное сканирование таблицы tf.
Однако запрос все еще выполняется не самым эффективным образом. Мы читаем одни и те же таблицы несколько раз в цикле из-за того, что сумма вычисляется в коррелированном подзапросе. Планировщик не может раскрыть его самостоятельно, но мы можем сделать это вручную. Обратите внимание на необходимость левых соединений.
=> EXPLAIN (ANALYZE, TIMING OFF) SELECT a.aircraft_code, round(avg(tf.amount)) FROM aircrafts a LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code LEFT JOIN ticket_flights tf ON tf.flight_id = f.flight_id AND tf.amount > :a_min AND tf.amount < :a_max GROUP BY a.aircraft_code;
QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------- HashAggregate (cost=369679.77..369679.90 rows=9 width=48) (actual rows=9 loops=1) Group Key: ml.aircraft_code -> Hash Right Join (cost=8091.71..328054.78 rows=8324997 width=22) (actual rows=8394051 loops=1) Hash Cond: (f.aircraft_code = ml.aircraft_code) -> Hash Right Join (cost=8090.51..295909.84 rows=8324997 width=10) (actual rows=8394050 loops=1) Hash Cond: (tf.flight_id = f.flight_id) -> Seq Scan on ticket_flights tf (cost=0.00..191955.78 rows=8324997 width=10) (actual rows=8329761 loops=1) Filter: ((amount > 3000.00) AND (amount < 203300.00)) Rows Removed by Filter: 62091 -> Hash (cost=4564.67..4564.67 rows=214867 width=8) (actual rows=214867 loops=1) Buckets: 131072 Batches: 2 Memory Usage: 3459kB -> Seq Scan on flights f (cost=0.00..4564.67 rows=214867 width=8) (actual rows=214867 loops=1) -> Hash (cost=1.09..1.09 rows=9 width=16) (actual rows=9 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 5kB -> Seq Scan on aircrafts_data ml (cost=0.00..1.09 rows=9 width=16) (actual rows=9 loops=1) Planning time: 33.632 ms Execution time: 10323.508 ms (17 rows)
Вот теперь запрос выполняется эффективно, необходимые данные читаются только один раз.
Можно еще немного улучшить результат, увеличив значение параметра work_mem, чтобы хеш-соединение таблиц flights и ticket_flights выполнялось за один проход без временных файлов.
=> SET work_mem = '8MB';
SET
=> EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, BUFFERS) SELECT a.aircraft_code, round(avg(tf.amount)) FROM aircrafts a LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code LEFT JOIN ticket_flights tf ON tf.flight_id = f.flight_id AND tf.amount > :a_min AND tf.amount < :a_max GROUP BY a.aircraft_code;
QUERY PLAN ------------------------------------------------------------------------------- HashAggregate (actual rows=9 loops=1) Group Key: ml.aircraft_code Buffers: shared hit=16284 read=52211 -> Hash Right Join (actual rows=8394051 loops=1) Hash Cond: (f.aircraft_code = ml.aircraft_code) Buffers: shared hit=16284 read=52211 -> Hash Right Join (actual rows=8394050 loops=1) Hash Cond: (tf.flight_id = f.flight_id) Buffers: shared hit=16283 read=52211 -> Seq Scan on ticket_flights tf (actual rows=8329761 loops=1) Filter: ((amount > 3000.00) AND (amount < 203300.00)) Rows Removed by Filter: 62091 Buffers: shared hit=13867 read=52211 -> Hash (actual rows=214867 loops=1) Buckets: 262144 Batches: 1 Memory Usage: 6900kB Buffers: shared hit=2416 -> Seq Scan on flights f (actual rows=214867 loops=1) Buffers: shared hit=2416 -> Hash (actual rows=9 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 5kB Buffers: shared hit=1 -> Seq Scan on aircrafts_data ml (actual rows=9 loops=1) Buffers: shared hit=1 Planning time: 0.368 ms Execution time: 9585.179 ms (25 rows)
На этом можно остановиться, поскольку запрос не делает лишних чтений:
=> SELECT sum(relpages) FROM pg_class WHERE relname IN ('aircrafts','flights','ticket_flights');
sum ------- 68494 (1 row)
Дальнейших путей для оптимизации запроса не видно (хотя и остаются пути для оптимизации настроек сервера, что является темой курса DBA2).
=> END;
COMMIT
Конец демонстрации.