1. Архитектура реляционных баз данных и особенности PostgreSQL
Архитектура реляционных баз данных и особенности PostgreSQL
Понимание того, как база данных работает «под капотом», отличает начинающего разработчика от профессионала. Когда ваше приложение на Python отправляет SQL-запрос через SQLAlchemy или Django ORM, база данных не просто читает файл на диске. Она запускает сложный конвейер процессов: от выделения памяти и планирования маршрута выполнения до управления конкурентным доступом и обеспечения отказоустойчивости.
В этой статье мы разберем архитектуру реляционных систем управления базами данных (СУБД) на примере PostgreSQL — одного из самых мощных и популярных решений в современном бэкенде. Мы изучим, как эта СУБД управляет процессами, памятью, хранением данных на диске и почему она способна выдерживать колоссальные нагрузки при правильной настройке.
Процессная модель: как СУБД обрабатывает подключения
Исторически сложились два основных подхода к архитектуре СУБД: многопоточная (используется в MySQL) и многопроцессная. PostgreSQL использует многопроцессную архитектуру (Process-per-Connection).
Когда вы запускаете сервер базы данных, первым стартует главный управляющий процесс — Postmaster. Его основная задача — слушать сетевой порт (по умолчанию 5432) и ожидать входящих подключений от клиентов (вашего бэкенда).
Как только поступает запрос на новое подключение, Postmaster выполняет системный вызов fork(), создавая свою точную копию — новый рабочий процесс (Backend Process). Именно этот выделенный процесс будет обрабатывать все SQL-запросы в рамках установленной сессии.
Такая архитектура имеет свои преимущества и недостатки, которые важно учитывать при проектировании инфраструктуры.
| Характеристика | Многопроцессная модель (PostgreSQL) | Многопоточная модель (MySQL) |
|---|---|---|
| Изоляция | Высокая. Сбой одного процесса не «уронит» всю базу. | Средняя. Ошибка в одном потоке может повлиять на весь процесс. |
| Потребление памяти | Высокое. Каждый процесс требует выделения отдельного блока памяти. | Низкое. Потоки делят общее адресное пространство. |
| Создание соединения | Медленное (системный вызов fork ресурсоемок). | Быстрое (создание потока дешевле). |
Из-за того, что создание нового процесса — дорогая операция, в мире Python-бэкенда (особенно при использовании асинхронных фреймворков вроде FastAPI, которые могут открывать сотни соединений за секунду) прямое подключение к базе данных часто становится узким местом.
> Для решения проблемы «дорогого» создания процессов в высоконагруженных системах всегда используются пулеры соединений, такие как PgBouncer или Odyssey. Они поддерживают пул постоянно открытых процессов на стороне СУБД и мультиплексируют через них тысячи легковесных клиентских подключений.
Архитектура памяти: где живут данные
Память в PostgreSQL строго разделена на две большие категории: общую (Shared Memory), доступную всем процессам, и локальную (Local Memory), выделяемую для каждого конкретного рабочего процесса.
Общая память (Shared Memory)
Эта область памяти выделяется при старте сервера и используется для обмена данными между всеми процессами. Ее главные компоненты:
Локальная память (Local Memory)
Локальная память выделяется динамически для каждого процесса, обслуживающего клиентское соединение.
Ключевой параметр здесь — work_mem. Это объем памяти, который процесс может использовать для выполнения операций сортировки (ORDER BY), группировки (GROUP BY) и создания хеш-таблиц при объединениях (HASH JOIN). Если данных больше, чем помещается в work_mem, база данных начинает использовать временные файлы на диске, что катастрофически снижает производительность.
Рассмотрим пример расчета потребления памяти. Допустим, параметр work_mem установлен в 10 МБ. У вас есть 100 активных соединений. Если каждый процесс выполняет сложный запрос с двумя независимыми сортировками, потребление локальной памяти составит:
Общая память = МБ (или около 2 ГБ).
Это означает, что бездумное увеличение work_mem на сервере с большим количеством соединений может привести к исчерпанию оперативной памяти (OOM — Out of Memory) и аварийному завершению работы базы данных операционной системой.
Физическое хранение: страницы и TOAST
На жестком диске база данных представляет собой набор файлов. Каждая таблица и каждый индекс хранятся в отдельных файлах. Но внутри этих файлов данные организованы не сплошным текстом, а разбиты на блоки фиксированного размера — страницы (Pages).
В PostgreSQL стандартный размер страницы составляет 8 КБ (8192 байта). Каждая страница имеет заголовок (Header), который занимает 24 байта. Таким образом, для самих данных остается:
Доступное место = байт.
Внутри страницы хранятся кортежи (Tuples) — так в терминологии баз данных называются строки таблицы. В конце страницы хранятся сами данные строк, а в начале (сразу после заголовка) — массив указателей (Item Pointers), которые сообщают, где именно внутри этой страницы начинается конкретная строка.
Но что произойдет, если вы попытаетесь сохранить в ячейку таблицы огромный JSON-документ или длинный текст, размер которого превышает 8 КБ? Строка физически не поместится в страницу.
Здесь на помощь приходит механизм TOAST (The Oversized-Attribute Storage Technique).
Если размер строки превышает примерно 2 КБ, база данных автоматически сжимает большие поля. Если после сжатия строка все еще слишком велика, PostgreSQL разбивает большое значение на фрагменты (chunks) и сохраняет их в специальной скрытой TOAST-таблице, а в основной таблице оставляет лишь легковесный указатель на эти данные.
Это гениальное архитектурное решение. Если вы делаете SELECT id, name FROM users, база данных читает только основную таблицу. Огромные текстовые поля (например, bio или settings), хранящиеся в TOAST, не загружаются в память и не замедляют запрос, пока вы явно не запросите их в SELECT.
Конкурентный доступ: магия MVCC
Представьте ситуацию: пользователь А читает таблицу счетов, чтобы сформировать финансовый отчет. В этот же момент пользователь Б переводит деньги и обновляет балансы в этой же таблице.
В старых СУБД для предотвращения конфликтов использовались жесткие блокировки: пока А читает, Б не может писать (и наоборот). Это приводило к огромным очередям и простоям. PostgreSQL решает эту проблему с помощью MVCC (Multi-Version Concurrency Control — управление конкурентным доступом с помощью многоверсионности).
Главное правило MVCC звучит так: «Читатели не блокируют писателей, а писатели не блокируют читателей».
Как это работает? В PostgreSQL операция UPDATE на самом деле не изменяет существующую строку на диске. Вместо этого она помечает старую версию строки как удаленную и вставляет новую версию строки с обновленными данными.
Каждая строка (кортеж) имеет два скрытых системных поля:
xmin: идентификатор транзакции (TXID), которая создала эту версию строки.xmax: идентификатор транзакции, которая удалила (или обновила) эту версию строки. Если строка актуальна, xmax равен нулю.Давайте посмотрим на это через SQL-запрос. Вы можете явно запросить эти скрытые столбцы:
Предположим, транзакция с номером 100 создала счет. Вы увидите xmin = 100, xmax = 0.
Затем транзакция 105 обновляет баланс. База данных создает новую строку. Теперь на диске физически лежат две версии одной и той же строки:
xmin = 100, xmax = 105 (баланс старый).xmin = 105, xmax = 0 (баланс новый).Если в этот момент долгий аналитический запрос (начавшийся при транзакции 102) читает таблицу, он сравнивает свой номер с xmin и xmax. Он видит, что новая строка создана в будущем (105 > 102), поэтому игнорирует ее. Он видит старую строку, которая была удалена в будущем (105 > 102), и читает именно ее. Таким образом, каждый запрос видит согласованный «снимок» базы данных на момент своего старта.
Проблема мертвых строк и VACUUM
Оборотная сторона MVCC заключается в том, что старые версии строк (мертвые кортежи или Dead Tuples) остаются на диске, занимая место. Если таблица часто обновляется, она начнет стремительно раздуваться в размерах (это явление называется Table Bloat).
Для очистки мусора в PostgreSQL существует фоновый процесс Autovacuum. Он периодически сканирует таблицы, находит строки, xmax которых меньше номеров всех текущих активных транзакций (то есть эти строки больше никому не нужны), и помечает их место как свободное для записи новых данных.
> Настройка Autovacuum — одна из важнейших задач при оптимизации базы данных. Отключение этого процесса (что иногда делают неопытные разработчики ради сиюминутного снижения нагрузки на диск) неизбежно приведет к деградации производительности и остановке базы данных из-за переполнения счетчика транзакций.
Журнал предзаписи: WAL и надежность
Любая реляционная база данных должна соответствовать требованиям ACID. Буква D означает Durability (Долговечность) — если база данных ответила клиенту, что транзакция успешно зафиксирована (COMMIT), данные не должны потеряться даже при внезапном отключении питания сервера.
Запись данных напрямую в файлы таблиц на диске — процесс медленный, так как данные разбросаны по разным файлам (случайный доступ, Random I/O). Чтобы обеспечить и скорость, и надежность, используется WAL (Write-Ahead Log — Журнал предзаписи).
Жизненный цикл изменения данных выглядит так:
UPDATE.COMMIT база данных сбрасывает (fsync) записи из буфера WAL в файл WAL на жестком диске. Это быстрая операция, так как запись идет строго последовательно в конец файла (Sequential I/O).Сами данные в файлах таблиц на диске в этот момент еще не обновлены! Измененные страницы висят в оперативной памяти (их называют «грязными страницами» — dirty pages).
Периодически запускается процесс Checkpoint (Контрольная точка). Он берет все «грязные страницы» из Shared Buffers и разом записывает их в файлы таблиц на диске. После успешного завершения Checkpoint старые файлы WAL можно удалить, так как данные уже безопасно лежат в основных файлах.
Если сервер внезапно обесточится, при следующем запуске PostgreSQL прочитает файлы WAL и повторно применит все изменения к таблицам, восстановив состояние базы данных на момент сбоя.
Конвейер выполнения запроса
Теперь, понимая архитектуру памяти и хранения, давайте посмотрим, какой путь проходит ваш SQL-запрос от получения до выдачи результата.
SELEC вместо SELECT, запрос будет отклонен на этом этапе.work_mem и возвращает результат клиенту.Понимание этого конвейера критически важно для оптимизации. Когда мы будем изучать команду EXPLAIN в следующих статьях, мы будем смотреть именно на результат работы Планировщика, чтобы понять, почему база данных решила выполнить запрос тем или иным способом.
Заключение
Архитектура PostgreSQL — это баланс между надежностью, соответствием стандартам ACID и высокой производительностью. Многопроцессная модель требует аккуратного управления соединениями со стороны бэкенда. Разделение памяти на общую и локальную диктует правила настройки серверов. Механизм MVCC позволяет тысячам пользователей работать одновременно без взаимных блокировок, а WAL гарантирует, что ни один байт данных не исчезнет при сбое оборудования.
Глубокое понимание этих внутренних механизмов позволит вам не просто писать SQL-запросы, но и осознанно проектировать схемы данных, настраивать индексы и оптимизировать узкие места в ваших Python-приложениях.
" }