Последовательное сканирование

Для того, чтобы посмотреть план запроса, используется команда EXPLAIN.

=> EXPLAIN SELECT * FROM flights;
                           QUERY PLAN                           
----------------------------------------------------------------
 Seq Scan on flights  (cost=0.00..4564.67 rows=214867 width=63)
(1 row)

Выбран метод доступа Seq Scan - последовательное чтение.

В скобках приведены важные значения:

Стоимость указывается в некоторых условных единицах.

Приводятся два числа. Первое показывает оценку ресурсов на предварительную подготовку к операции. Для последовательного сканирования это ноль - чтобы начать возвращать данные, никакой подготовки не требуется.

Второе число показывает общую оценку ресурсов для получения всех данных. Как оно получается?


Оптимизатор PostgreSQL учитывает две компоненты: дисковый ввод-вывод и ресурсы процессора. Компонента ввода-вывода рассчитывается как произведение числа страниц в таблице на условную стоимость чтения одной страницы:

=> SELECT relpages, current_setting('seq_page_cost'),
  relpages * current_setting('seq_page_cost')::real AS total
FROM pg_class WHERE relname='flights';
 relpages | current_setting | total 
----------+-----------------+-------
     2416 | 1               |  2416
(1 row)


Вторая компонента - ресурсы процессора - складывается из стоимости обработки каждой строки:

=> SELECT reltuples, current_setting('cpu_tuple_cost'),
  reltuples * current_setting('cpu_tuple_cost')::real AS total
FROM pg_class WHERE relname='flights';
 reltuples | current_setting |  total  
-----------+-----------------+---------
    214867 | 0.01            | 2148.67
(1 row)


Агрегация

Рассмотрим более сложный запрос с агрегатной функцией (на небольшой таблице):

=> EXPLAIN SELECT count(*) FROM seats;
                          QUERY PLAN                           
---------------------------------------------------------------
 Aggregate  (cost=24.74..24.75 rows=1 width=8)
   ->  Seq Scan on seats  (cost=0.00..21.39 rows=1339 width=0)
(2 rows)

План состоит из двух узлов. Верхний - Aggregate, в котором происходит вычисление count, - получает данные от нижнего - Seq Scan.

Обратите внимание на стоимость узла Aggregate: нижняя цифра практически равна верхней. Это означает, что узел не может выдать результат, пока не обработает все данные (что вполне логично).


Разница между оценкой для Aggregate и верхней оценкой для Seq Scan - стоимость работы собственно узла Aggregate. Она вычисляется исходя из оценки ресурсов на выполнение условной операции:

=> SELECT reltuples, current_setting('cpu_operator_cost'),
  reltuples * current_setting('cpu_operator_cost')::real AS total
FROM pg_class WHERE relname='seats';
 reltuples | current_setting | total  
-----------+-----------------+--------
      1339 | 0.0025          | 3.3475
(1 row)


Параллельное последовательное сканирование

Планировщик не будет рассматривать параллельные планы для таблиц размером меньше, чем

=> SELECT current_setting('min_parallel_table_scan_size');
 current_setting 
-----------------
 8MB
(1 row)

Поэтому обратимся к большой таблице:

=> SELECT pg_size_pretty(pg_table_size('bookings')) size,
  (SELECT count(*) FROM bookings) cnt;
  size  |   cnt   
--------+---------
 105 MB | 2111110
(1 row)


=> EXPLAIN SELECT count(*) FROM bookings;
                                         QUERY PLAN                                         
--------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=25442.58..25442.59 rows=1 width=8)
   ->  Gather  (cost=25442.36..25442.57 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=24442.36..24442.37 rows=1 width=8)
               ->  Parallel Seq Scan on bookings  (cost=0.00..22243.29 rows=879629 width=0)
(5 rows)

Все, что находится ниже узла Gather - параллельная часть плана. Он выполняется в каждом из рабочих процессов (которых запланировано 2) и, возможно, в ведущем процессе.

Узел Gather и все узлы выше выполняются только в ведущем процессе. Это последовательная часть плана.


Начнем разбираться снизу вверх. Узел Parallel Seq Scan представляет сканирование таблицы в параллельном режиме.

В поле rows показана оценка числа строк, которые обработает один рабочий процесс. Всего их запланировано 2, и еще часть работы выполнит ведущий, поэтому общее число строк делится на 2.4 (доля ведущего процесса уменьшается с ростом числа рабочих процессов).

=> SELECT round(reltuples / 2.4) FROM pg_class WHERE relname = 'bookings';
 round  
--------
 879629
(1 row)


Для оценки узла Parallel Seq Scan компонента ввода-вывода берется полностью (таблицу все равно придется прочитать страница за страницей), а ресурсы процессора делятся между процессами (на 2.4 в данном случае).

=> SELECT round(
  relpages * current_setting('seq_page_cost')::real +
  reltuples * current_setting('cpu_tuple_cost')::real / 2.4
) FROM pg_class WHERE relname = 'bookings';
 round 
-------
 22243
(1 row)


=> EXPLAIN SELECT count(*) FROM bookings;
                                         QUERY PLAN                                         
--------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=25442.58..25442.59 rows=1 width=8)
   ->  Gather  (cost=25442.36..25442.57 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=24442.36..24442.37 rows=1 width=8)
               ->  Parallel Seq Scan on bookings  (cost=0.00..22243.29 rows=879629 width=0)
(5 rows)

Следующий узел - Partial Aggregate - выполняет агрегацию данных, полученных рабочим процессом, то есть в данном случае подсчитывает количество строк. Оценка выполняется уже известным образом (и добавляется к оценке сканирования таблицы):

=> SELECT round(reltuples / 2.4 * current_setting('cpu_operator_cost')::real)
FROM pg_class WHERE relname='bookings';
 round 
-------
  2199
(1 row)


Следующий узел - Gather - выполняется на ведущем процессе. Он отвечает за запуск рабочих процессов и получение от них данных.

Запуск процессов и пересылка каждой строки данных оцениваются как

=> SELECT current_setting('parallel_setup_cost') parallel_setup_cost,
current_setting('parallel_tuple_cost') parallel_tuple_cost;
 parallel_setup_cost | parallel_tuple_cost 
---------------------+---------------------
 1000                | 0.1
(1 row)

В данном случае пересылается всего одна строка и основная стоимость приходится на запуск.


=> EXPLAIN SELECT count(*) FROM bookings;
                                         QUERY PLAN                                         
--------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=25442.58..25442.59 rows=1 width=8)
   ->  Gather  (cost=25442.36..25442.57 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=24442.36..24442.37 rows=1 width=8)
               ->  Parallel Seq Scan on bookings  (cost=0.00..22243.29 rows=879629 width=0)
(5 rows)

Последний узел - Finalize Aggregate - агрегирует полученные частичные агрегаты. Поскольку для этого надо сложить всего три числа, оценка минимальна.


Конец демонстрации.