linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть VI — Планировщик и выполнение

$ глава 24 · 50 минут

Жизнь запроса: от текста до результата

Ты пишешь SELECT, нажимаешь ввод, получаешь строки. Между этими двумя моментами запрос проходит конвейер из пяти стадий, и каждая оставляет свой артефакт: дерево разбора, дерево запроса, переписанный запрос, дерево плана, поток строк. Понимать этот конвейер нужно не из любопытства - почти все «почему медленно» живут на стадии планирования, и чтобы их чинить, надо знать, что туда приходит и что выходит.

В этой главе мы пройдём весь путь от текста SQL до исполнителя, посмотрим, во что превращается представление при раскрытии, и научимся читать дерево плана через EXPLAIN. Дальше вся часть VI - это подробный разбор четвёртой стадии, планирования.

24.1 Пять стадий

Запрос внутри сервера проходит строго по порядку:

  1. Разбор (parse). Текст SQL превращается в дерево разбора по грамматике. Здесь ловятся синтаксические ошибки. Сервер ещё не знает, существует ли таблица flights - он только убедился, что это синтаксически корректный SELECT.
  2. Семантический анализ (analyze). Имена связываются с объектами: flights ищется в каталоге, проверяются типы, раскрываются *. Получается дерево запроса (query tree) - уже осмысленное.
  3. Переписывание (rewrite). Система правил подставляет определения представлений, применяет RLS-политики, разворачивает некоторые конструкции. На входе и выходе - дерево запроса.
  4. Планирование (plan). Планировщик перебирает способы выполнить запрос и выбирает самый дешёвый. Результат - дерево плана: какие сканы, в каком порядке, какими соединениями.
  5. Выполнение (execute). Исполнитель идёт по дереву плана и выдаёт строки.

Ключ к пониманию: стадии 1-3 про что нужно получить, стадия 4 про как. Один и тот же запрос всегда даёт одно дерево запроса, но разные планы - в зависимости от данных, статистики и настроек.

24.2 Разбор и анализ: текст становится смыслом

Разбор работает только с грамматикой. SELECT * FROM flights WHERE x = 1 разберётся, даже если колонки x нет - синтаксис-то верный. Ошибка «column x does not exist» прилетает на следующей стадии, когда имена связываются с каталогом.

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

Результат - дерево запроса: структура «что выбрать, откуда, по какому условию, как сгруппировать и отсортировать», без единого слова о том, как это исполнять физически.

24.3 Переписывание: представления и RLS

Третья стадия - система правил. Главная её работа на практике - раскрытие представлений. Представление в PostgreSQL не хранит данные; это сохранённый запрос. Когда ты обращаешься к view, его определение подставляется в твой запрос как подзапрос.

sql
CREATE VIEW svo_flights AS
  SELECT * FROM flights WHERE departure = 'SVO';
-- Этот запрос:
SELECT flight_no FROM svo_flights WHERE arrival = 'LED';
-- после переписывания превращается примерно в:
SELECT flight_no FROM (SELECT * FROM flights WHERE departure = 'SVO') v
WHERE v.arrival = 'LED';

Дальше планировщик чаще всего «сплющит» подзапрос обратно в один уровень и применит оба условия к одному скану flights. Поэтому view сам по себе не делает запрос медленнее - он растворяется ещё до планирования. На этой же стадии применяются политики row-level security: к запросу автоматически добавляются условия, ограничивающие видимые строки. Подробнее - в query-rewrite.

24.4 Планирование: выбор из многих способов

Четвёртая стадия - сердце части VI. Дерево запроса говорит «нужны рейсы из SVO в LED». Способов получить их физически - много: прочитать всю таблицу и отфильтровать, пройти по индексу на departure, по индексу на arrival, пересечь два индекса. Если в запросе соединение, добавляется выбор алгоритма (nested loop, hash, merge) и порядка соединения.

Планировщик оценивает стоимость каждого варианта в условных единицах и выбирает самый дешёвый. Это не поиск «правильного» плана, а ставка на основе статистики о данных. Если статистика врёт, ставка проигрывает - и запрос тормозит, хотя формально всё «оптимально». Как считается стоимость - в cost-model, откуда берутся числа о данных - в planner-statistics.

24.5 Дерево плана и EXPLAIN

Результат планирования - дерево узлов, и EXPLAIN показывает его как есть, без выполнения:

sql
EXPLAIN SELECT t.passenger
FROM tickets t JOIN flights f ON f.flight_id = t.flight_id
WHERE f.departure = 'SVO';
Hash Join  (cost=2.12..18.40 rows=120 width=12)
  Hash Cond: (t.flight_id = f.flight_id)
  ->  Seq Scan on tickets t  (cost=0.00..14.00 rows=500 width=8)
  ->  Hash  (cost=1.62..1.62 rows=40 width=4)
        ->  Seq Scan on flights f  (cost=0.00..1.62 rows=40 width=4)
              Filter: (departure = 'SVO')

Дерево читается снизу вверх и изнутри наружу: листья - сканы таблиц, они отдают строки родителям, наверху - финальный узел, отдающий результат клиенту. Каждый узел показывает оценку стоимости (cost), числа строк (rows) и ширину (width). Это план, а не факт: чтобы увидеть реальные числа, нужен EXPLAIN ANALYZE, которому посвящена глава 30.

24.5.1 Копнуть глубже: простой и расширенный протокол

Есть два способа отправить запрос на сервер. Простой протокол - одна команда «вот текст, выполни» - проходит все пять стадий за раз. Так работает psql, когда ты вводишь запрос целиком.

Расширенный протокол разбивает это на шаги: Parse (разобрать и запомнить под именем), Bind (подставить параметры $1, $2), Execute (выполнить). Так работают подготовленные выражения и драйверы приложений. Выигрыш в том, что стадии разбора и анализа можно сделать один раз, а выполнять много - с разными параметрами.

Это открывает дверь к кешу планов: подготовленное выражение может запомнить не только разобранный запрос, но и план. Когда это выгодно, а когда нет (generic против custom plan) - тема главы 29.

24.6 Зачем знать конвейер

Разделение «что» и «как» - это и есть причина, по которой PostgreSQL вообще можно оптимизировать. Ты пишешь декларативный SQL (что хочешь), а планировщик каждый раз решает, как это сделать, исходя из текущих данных. Поменялся объём таблицы или статистика - план для того же текста сменится сам.

Практический вывод: когда запрос «вдруг стал медленным», текст SQL обычно ни при чём - сменился план. Поэтому диагностика начинается не с переписывания запроса, а с EXPLAIN: посмотреть, какой план выбран сейчас, и сравнить с тем, что планировщик считал, когда было быстро. Этим и займёмся в оставшихся главах части.

Уроки в sandbox

lab-24.1. Пройти запрос по стадиям и раскрыть представление

Посмотрим, как один и тот же запрос выглядит до и после раскрытия view, и прочитаем дерево плана. Перед EXPLAIN предскажи, сколько узлов Seq Scan будет в плане.

  1. Создай представление: CREATE VIEW svo AS SELECT * FROM flights WHERE departure = 'SVO';.

  2. Сделай EXPLAIN SELECT flight_no FROM svo WHERE arrival = 'LED';. Найди в плане оба условия (departure и arrival) - они применены к одному скану flights, view растворился.

  3. Сравни с прямым запросом EXPLAIN SELECT flight_no FROM flights WHERE departure = 'SVO' AND arrival = 'LED'; - план должен совпасть.

  4. Сделай EXPLAIN для соединения tickets и flights. Прочитай дерево снизу вверх: какие узлы листья, какой наверху.

  5. Введи заведомо неверную колонку (SELECT nope FROM flights) - ошибка прилетит на стадии анализа, не разбора. Объясни разницу.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Запрос проходит пять стадий: разбор, семантический анализ, переписывание, планирование, выполнение. Стадии 1-3 про «что», стадия 4 про «как».
  • Разбор проверяет только грамматику; имена таблиц и колонок связываются с каталогом на стадии анализа - там же рождается дерево запроса.
  • Переписывание раскрывает представления (view - это сохранённый запрос, не данные) и применяет RLS-политики; планировщик потом сплющивает подзапросы.
  • Планирование выбирает самый дешёвый способ по оценке стоимости - это ставка на основе статистики, а не поиск единственно верного плана.
  • EXPLAIN показывает дерево плана без выполнения: читается снизу вверх, листья - сканы, наверху - финальный узел. cost/rows/width - оценки.
  • Простой протокол проходит все стадии за раз; расширенный (Parse/Bind/Execute) разделяет их и открывает дорогу кешу планов.
  • Когда запрос «вдруг стал медленным», обычно сменился план, а не текст. Диагностика начинается с EXPLAIN.

Контрольные вопросы

  1. Почему ошибка «колонка не существует» появляется не сразу при разборе?

    Показать ответ

    Разбор работает только по грамматике: он проверяет, что текст - синтаксически корректный SQL, и строит дерево разбора. На этом этапе сервер ещё не заглядывал в каталог и не знает, какие таблицы и колонки есть. Связывание имён с реальными объектами происходит на следующей стадии - семантическом анализе. Там же проверяются типы и права. Поэтому SELECT nope FROM flights разберётся успешно, а упадёт на анализе.

  2. Делает ли представление (VIEW) запрос медленнее?

    Показать ответ

    Само по себе - нет. View не хранит данные, это сохранённый запрос. На стадии переписывания его определение подставляется в твой запрос как подзапрос, а планировщик в большинстве случаев сплющивает этот подзапрос обратно и применяет все условия к одному скану. Итоговый план обращения через view и эквивалентного прямого запроса обычно совпадает. Медленным запрос делает не факт использования view, а его логика и выбранный план.

  3. Почему один и тот же текст SQL может выполняться по разным планам в разное время?

    Показать ответ

    Потому что план - это результат стадии планирования, а она зависит от данных и статистики, а не только от текста. Дерево запроса («что») для одного текста всегда одно, но способ его выполнить («как») планировщик выбирает каждый раз заново по оценке стоимости. Вырос объём таблицы, обновилась статистика, изменились настройки - и для того же запроса выбирается другой план. Поэтому «вдруг стало медленно» почти всегда значит «сменился план».

  4. Что показывает EXPLAIN без ANALYZE и чем cost отличается от реального времени?

    Показать ответ

    EXPLAIN без ANALYZE показывает выбранный план и оценки планировщика, но запрос не выполняет. cost - это условные единицы, в которых планировщик сравнивает варианты между собой; они не равны миллисекундам и не измеряют реальное выполнение. Чтобы получить фактические время и число строк, нужен EXPLAIN ANALYZE - он действительно выполняет запрос и показывает actual time/rows рядом с оценками.

  5. В чём смысл расширенного протокола (Parse/Bind/Execute)?

    Показать ответ

    Он разделяет разбор, подстановку параметров и выполнение на отдельные шаги. Запрос можно разобрать и проанализировать один раз (Parse), а потом много раз выполнять с разными значениями параметров (Bind + Execute), не повторяя ранние стадии. Так работают подготовленные выражения и драйверы. Дополнительно это позволяет кешировать не только разобранный запрос, но и план - отсюда тема generic против custom plan.

← Предыдущая23-lightweight-predicate-locksСледующая →25-cost-model
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки