Жизненный цикл запроса

Архитектура СУБД: Жизненный цикл запроса

Вместо того чтобы просто перечислять компоненты базы данных, давай проследим путь конкретного запроса. Представь, что твой бэкенд отправляет в базу простую строку: SELECT * FROM orders WHERE status = 'paid'. Что происходит дальше?

Шаг 1: Транспортный слой

СУБД — это сервер. Твое приложение — клиент. Прежде чем база прочитает запрос, нужно установить соединение.

  • Endpoint Listener: Главный процесс (например, postmaster в Postgres) принимает TCP-соединение и выделяет под него отдельный процесс или поток.
  • Аутентификация и TLS: База проверяет пароль и устанавливает зашифрованный туннель.

Первое «узкое горлышко» БД часто находится именно здесь. TLS-handshake — тяжелая операция. Поэтому в продакшене всегда используют Connection Pool (например, PgBouncer), который держит заранее открытые соединения с базой, чтобы не тратить время на handshake для каждого запроса.

Шаг 2: Процессор запросов

База получила текст запроса. Теперь ей нужно понять, что ты от нее хочешь.

  1. Парсер: Превращает текст в AST-дерево (Abstract Syntax Tree). Здесь проверяется синтаксис. Если ты опечатался и написал SELCT, база отбросит запрос именно тут, не тратя ресурсы.
  2. Проверка прав: Проверяет, существуют ли вообще таблица orders и колонка status, и есть ли у твоего пользователя права на их чтение.
  3. Переписчик: Это «синтаксический сахар» базы. Если ты делаешь запрос к VIEW, Rewriter незаметно подставит вместо него реальный SQL-код этого представления. Также он разворачивает WITH (CTE) в обычные подзапросы.
Шаг 3: Оптимизатор

Это самая сложная часть реляционной базы. SQL — декларативный язык. Ты говоришь «что» хочешь получить, а база сама решает, «как» это сделать.

  • Сбор статистики: Оптимизатор смотрит во внутренние таблицы (в Postgres это pg_statistic), чтобы понять: таблица большая или маленькая? Сколько примерно строк имеют status = 'paid'?
  • Генерация альтернатив: Он строит десятки вариантов. Использовать индекс или прочитать таблицу целиком (Seq Scan)? Какой Join использовать (Hash Join или Nested Loop)?
  • Оценка стоимости каждого варианта: Каждому плану присваивается «стоимость» (например, по I/O диска и тактов CPU). Оптимизатор выбирает самый «дешевый» путь.
  • План выполнения (Execution Plan) — это финальный результат работы оптимизатора. Именно его ты видишь, когда пишешь команду EXPLAIN.
SQL
EXPLAIN SELECT * FROM orders WHERE status = 'paid';

-- Пример простого плана:
-- Index Scan using orders_status_idx on orders  (cost=0.15..8.20 rows=10 width=45)
--   Index Cond: (status = 'paid'::text)
Шаг 4: Исполнительный движок

Движок исполнения берет готовый план выполнения и начинает по нему идти. Сам он данные не хранит. Он выступает неким прорабом: обращается к Движку хранения и просит: «Дай мне страницу памяти номер X».

Движок исполнения получает сырые данные, при необходимости фильтрует их, форматирует результат в бинарный протокол и через транспортный слой отправляет обратно твоему приложению.

Финальная версия

В следующем блоке мы погрузимся как раз в движок хранения