Оптимизация запроса

Отключим параллельное выполнение.

=> SET max_parallel_workers_per_gather = 0;
SET
=> EXPLAIN (ANALYZE, TIMING OFF) SELECT t.*
FROM  tickets t,
      ticket_flights tf,
      flights f
WHERE tf.ticket_no = t.ticket_no
  AND f.flight_id = tf.flight_id
  AND tf.fare_conditions = 'Business'
  AND f.actual_departure > f.scheduled_departure + interval '5 hour';
                                                           QUERY PLAN                                                            
---------------------------------------------------------------------------------------------------------------------------------
 Merge Join  (cost=210524.16..361374.43 rows=285603 width=104) (actual rows=2 loops=1)
   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) (actual rows=1336684 loops=1)
   ->  Materialize  (cost=210523.63..211951.65 rows=285603 width=14) (actual rows=2 loops=1)
         ->  Sort  (cost=210523.63..211237.64 rows=285603 width=14) (actual rows=2 loops=1)
               Sort Key: tf.ticket_no
               Sort Method: quicksort  Memory: 17kB
               ->  Hash Join  (cost=6534.28..179760.29 rows=285603 width=14) (actual rows=2 loops=1)
                     Hash Cond: (tf.flight_id = f.flight_id)
                     ->  Seq Scan on ticket_flights tf  (cost=0.00..170976.83 rows=856814 width=18) (actual rows=859656 loops=1)
                           Filter: ((fare_conditions)::text = 'Business'::text)
                           Rows Removed by Filter: 7532196
                     ->  Hash  (cost=5639.00..5639.00 rows=71622 width=4) (actual rows=1 loops=1)
                           Buckets: 131072  Batches: 1  Memory Usage: 513kB
                           ->  Seq Scan on flights f  (cost=0.00..5639.00 rows=71622 width=4) (actual rows=1 loops=1)
                                 Filter: (actual_departure > (scheduled_departure + '05:00:00'::interval))
                                 Rows Removed by Filter: 214866
 Planning time: 18.513 ms
 Execution time: 4485.808 ms
(19 rows)

Здесь мы имеем дело с запросом, характерным для OLTP - небольшое число строк, для получения которых надо выбрать только небольшую часть данных. Поэтому общее направление оптимизации - переход от полного сканирования и соединения хешированием к индексам и вложенным циклам.

Заметим, что рейс, задержанный более чем на 5 часов, всего один, а планировщик сканирует всю таблицу. Можно построить функциональный индекс на разности двух столбцов и немного переписать условие:

=> CREATE INDEX ON flights ((actual_departure - scheduled_departure));
CREATE INDEX
=> ANALYZE flights;
ANALYZE
=> EXPLAIN (ANALYZE, TIMING OFF) SELECT t.*
FROM  tickets t,
      ticket_flights tf,
      flights f
WHERE tf.ticket_no = t.ticket_no
  AND f.flight_id = tf.flight_id
  AND tf.fare_conditions = 'Business'
  AND f.actual_departure - f.scheduled_departure > interval '5 hour';
                                                       QUERY PLAN                                                        
-------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=31.97..173277.94 rows=28 width=104) (actual rows=2 loops=1)
   ->  Hash Join  (cost=31.54..173257.54 rows=28 width=14) (actual rows=2 loops=1)
         Hash Cond: (tf.flight_id = f.flight_id)
         ->  Seq Scan on ticket_flights tf  (cost=0.00..170976.83 rows=856814 width=18) (actual rows=859656 loops=1)
               Filter: ((fare_conditions)::text = 'Business'::text)
               Rows Removed by Filter: 7532196
         ->  Hash  (cost=31.45..31.45 rows=7 width=4) (actual rows=1 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 5kB
               ->  Bitmap Heap Scan on flights f  (cost=4.47..31.45 rows=7 width=4) (actual rows=1 loops=1)
                     Recheck Cond: ((actual_departure - scheduled_departure) > '05:00:00'::interval)
                     Heap Blocks: exact=1
                     ->  Bitmap Index Scan on flights_expr_idx  (cost=0.00..4.47 rows=7 width=0) (actual rows=1 loops=1)
                           Index Cond: ((actual_departure - scheduled_departure) > '05:00:00'::interval)
   ->  Index Scan using tickets_pkey on tickets t  (cost=0.43..0.73 rows=1 width=104) (actual rows=1 loops=2)
         Index Cond: (ticket_no = tf.ticket_no)
 Planning time: 6.338 ms
 Execution time: 2097.192 ms
(17 rows)

Теперь займемся таблицей ticket_flights, которая тоже сканируется полностью, хотя из нее читается незначительная часть строк.

Помог бы индекс по классам обслуживания fare_conditions, но лучше создать индекс по столбцу flight_id, что позволит эффективно выполнять соединение вложенным циклом с flights:

=> CREATE INDEX ON ticket_flights(flight_id);
CREATE INDEX
=> EXPLAIN (ANALYZE, TIMING OFF) SELECT t.*
FROM  tickets t,
      ticket_flights tf,
      flights f
WHERE tf.ticket_no = t.ticket_no
  AND f.flight_id = tf.flight_id
  AND tf.fare_conditions = 'Business'
  AND f.actual_departure - f.scheduled_departure > interval '5 hour';
                                                                  QUERY PLAN                                                                  
----------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=5.34..2801.29 rows=28 width=104) (actual rows=2 loops=1)
   ->  Nested Loop  (cost=4.91..2780.90 rows=28 width=14) (actual rows=2 loops=1)
         ->  Bitmap Heap Scan on flights f  (cost=4.47..31.45 rows=7 width=4) (actual rows=1 loops=1)
               Recheck Cond: ((actual_departure - scheduled_departure) > '05:00:00'::interval)
               Heap Blocks: exact=1
               ->  Bitmap Index Scan on flights_expr_idx  (cost=0.00..4.47 rows=7 width=0) (actual rows=1 loops=1)
                     Index Cond: ((actual_departure - scheduled_departure) > '05:00:00'::interval)
         ->  Index Scan using ticket_flights_flight_id_idx on ticket_flights tf  (cost=0.43..392.68 rows=10 width=18) (actual rows=2 loops=1)
               Index Cond: (flight_id = f.flight_id)
               Filter: ((fare_conditions)::text = 'Business'::text)
               Rows Removed by Filter: 10
   ->  Index Scan using tickets_pkey on tickets t  (cost=0.43..0.73 rows=1 width=104) (actual rows=1 loops=2)
         Index Cond: (ticket_no = tf.ticket_no)
 Planning time: 7.072 ms
 Execution time: 6.965 ms
(15 rows)

Время выполнения уменьшилось до миллисекунд.