Архитектура СУБД: Жизненный цикл запроса
Вместо того чтобы просто перечислять компоненты базы данных, давай проследим путь конкретного запроса. Представь, что твой бэкенд отправляет в базу простую строку: . Что происходит дальше?SELECT * FROM orders WHERE status = 'paid'
Шаг 1: Транспортный слой
СУБД — это сервер. Твое приложение — клиент. Прежде чем база прочитает запрос, нужно установить соединение.
- Endpoint Listener: Главный процесс (например,
postmasterв Postgres) принимает TCP-соединение и выделяет под него отдельный процесс или поток. - Аутентификация и TLS: База проверяет пароль и устанавливает зашифрованный туннель.
Первое «узкое горлышко» БД часто находится именно здесь. TLS-handshake — тяжелая операция. Поэтому в продакшене всегда используют Connection Pool (например, PgBouncer), который держит заранее открытые соединения с базой, чтобы не тратить время на handshake для каждого запроса.
Шаг 2: Процессор запросов
База получила текст запроса. Теперь ей нужно понять, что ты от нее хочешь.
- Парсер: Превращает текст в AST-дерево (Abstract Syntax Tree). Здесь проверяется синтаксис. Если ты опечатался и написал
, база отбросит запрос именно тут, не тратя ресурсы.SELCT - Проверка прав: Проверяет, существуют ли вообще таблица
и колонкаorders, и есть ли у твоего пользователя права на их чтение.status - Переписчик: Это «синтаксический сахар» базы. Если ты делаешь запрос к
, Rewriter незаметно подставит вместо него реальный SQL-код этого представления. Также он разворачиваетVIEWв обычные подзапросы.WITH (CTE)
Шаг 3: Оптимизатор
Это самая сложная часть реляционной базы. SQL — декларативный язык. Ты говоришь «что» хочешь получить, а база сама решает, «как» это сделать.
- Сбор статистики: Оптимизатор смотрит во внутренние таблицы (в Postgres это
pg_statistic), чтобы понять: таблица большая или маленькая? Сколько примерно строк имеют?status = 'paid' - Генерация альтернатив: Он строит десятки вариантов. Использовать индекс или прочитать таблицу целиком (Seq Scan)? Какой Join использовать (Hash Join или Nested Loop)?
- Оценка стоимости каждого варианта: Каждому плану присваивается «стоимость» (например, по I/O диска и тактов CPU). Оптимизатор выбирает самый «дешевый» путь.
- План выполнения (Execution Plan) — это финальный результат работы оптимизатора. Именно его ты видишь, когда пишешь команду
.EXPLAIN
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».
Движок исполнения получает сырые данные, при необходимости фильтрует их, форматирует результат в бинарный протокол и через транспортный слой отправляет обратно твоему приложению.
Финальная версия
В следующем блоке мы погрузимся как раз в движок хранения