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

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

    => EXPLAIN SELECT * FROM flights;

                               QUERY PLAN
    ----------------------------------------------------------------
     Seq Scan on flights  (cost=0.00..4564.67 rows=214867 width=63)
    (1 row)

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

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

- cost - оценка стоимости;
- rows - оценка числа строк, возвращаемых операцией;
- width - оценка размера одной записи в байтах.

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

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

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

Оптимизатор 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 - агрегирует полученные частичные агрегаты. Поскольку для этого надо сложить всего три числа, оценка минимальна.

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