База дан: устройство, принципы работы и логика хранения данных
База данных становится архитектурной проблемой не тогда, когда в ней появляется первая таблица, а когда приложение начинает одновременно записывать, читать и менять связанные данные. На прототипе обычно хватает нескольких таблиц, пары индексов и ORM.

Под нагрузкой выясняется, что важны уже детали: как СУБД раскладывает записи по страницам, что именно индекс ускоряет, какие изменения считаются атомарными и что произойдёт при сбое посреди операции.
Под запросом «база дан» обычно имеют в виду не конкретный продукт, а систему управления базами данных, или СУБД. Это программный слой между приложением и хранилищем: он принимает запросы, проверяет их, находит нужные записи, меняет данные и отвечает за заданные гарантии. Чтобы понять, как работает база дан простыми словами, полезно проследить путь запроса от приложения до диска. Ниже разберём этот путь на примерах PostgreSQL и MongoDB.
Архитектура и роль систем управления данными в инфраструктуре
У СУБД нет одной универсальной внутренней схемы. У PostgreSQL и MongoDB разные реализации и разные границы компонентов. Но путь запроса в общих чертах похож: клиент подключается к серверу, сервер разбирает запрос, выбирает способ его выполнить, обращается к данным и возвращает результат.
В этой цепочке можно выделить несколько функций:
- Сетевой слой принимает соединения и обменивается данными по протоколу, который понимает клиент. Подключение может идти по TCP или через локальный Unix-сокет. MongoDB, например, использует собственный wire protocol.
- Разбор и проверка запроса определяют, что именно просит клиент, доступны ли ему нужные объекты и допустимы ли указанные операции. В PostgreSQL SQL-запрос проходит разбор и проверку имён таблиц, столбцов и прав доступа.
- Планировщик выбирает способ выполнения запроса. Он оценивает доступные индексы, порядок соединения таблиц и стоимость разных вариантов. Одинаковый SQL-запрос может исполняться по-разному в зависимости от данных, статистики и настроек.
- Исполнитель выполняет выбранный план: читает строки или документы, применяет фильтры, соединяет результаты и передаёт их клиенту.
- Подсистема хранения работает с физическими данными, кэшем и журналами. Именно здесь запросы к логической таблице или коллекции превращаются в чтение и запись на носитель.
- Средства восстановления возвращают систему в согласованное состояние после сбоя. Для этого СУБД использует журналы и контрольные точки, но их назначение и устройство зависят от продукта.
Полезно различать журнал изменений и сами файлы данных. В PostgreSQL WAL, то есть журнал предзаписи, фиксирует изменения до того, как соответствующие страницы данных будут записаны на диск. При восстановлении СУБД может повторить операции из журнала. Контрольная точка отмечает момент, к которому изменения уже перенесены в файлы данных, и помогает ограничить объём работы при восстановлении.
В MongoDB роль oplog другая. Это журнал операций, который replica set использует для передачи изменений между участниками и синхронизации реплик. Oplog связан с репликацией; его некорректно называть журналом физического хранения документов. Файлы данных и механизмы журналирования WiredTiger относятся к отдельному уровню хранения. Внешне эти механизмы могут казаться похожими, потому что оба связаны с последовательностью изменений, но решают разные задачи.
СУБД управляет не только тем, где лежат данные. Она определяет, как запрос к ним превращается в чтение, запись и восстановление после сбоя.
Для приложения эта внутренняя механика важна через вполне практические последствия. Если планировщик выбирает полный просмотр таблицы там, где приложение рассчитывало на индекс, запрос замедляется. Если запись сначала попадает в журнал, а затем в файл данных, это влияет на восстановление и долговечность. Если данные реплицируются, задержка между узлами может иметь значение для чтения и подтверждения изменений.
Слово «кластер» тоже стоит употреблять аккуратно. В PostgreSQL так называют один экземпляр сервера с общими каталогами данных, который может содержать несколько баз данных. Это не обязательно несколько серверов. В MongoDB кластером часто называют группу серверов или развертывание, например replica set или шардированную систему. Контекст меняет значение термина, поэтому в документации и обсуждении архитектуры лучше уточнять, о каком именно уровне идёт речь.
Реляционная модель: организация таблиц, столбцов и кластеров
Реляционная модель представляет данные в виде отношений, которые на практике чаще всего выглядят как таблицы. Таблица состоит из строк и столбцов. Для столбцов задаются имена и типы, а ограничения могут требовать уникальности, запрещать пустое значение или связывать запись с другой таблицей.
Тип столбца задаёт правила хранения и допустимых операций. Даты сравниваются как даты, числа можно складывать, а текстовые значения обрабатываются с учётом правил сортировки. Типы помогают СУБД обнаруживать ошибки и выбирать подходящие способы выполнения запросов. Если в поле, предназначенное для даты, приложение начинает передавать строки в разных форматах, проблема возникает не только при чтении: усложняется проверка, сортировка и работа индексов.
Структура хранения информации в СУБД не сводится к видимой таблице. В PostgreSQL строки таблицы хранятся на страницах, а системный каталог содержит описание объектов: таблиц, типов, столбцов и индексов. Системные представления дают к этим сведениям удобный интерфейс; сами каталоги, например pg_class и pg_attribute, тоже доступны для анализа. Поэтому таблица в SQL — логическая структура, а не обещание, что на диске она лежит одним простым файлом в том же виде.
У таблицы может быть схема, а внутри неё находятся связанные объекты. Например, схема помогает разделить имена таблиц и других объектов в одной базе. В PostgreSQL кластер обычно обслуживает несколько баз данных, а пользователь подключается к одной из них и работает с её схемами. Резервная копия отдельной базы и копия всего кластера решают разные задачи: в последнем случае сохраняются также общие настройки и объекты уровня кластера.
Организация таблиц и связей особенно важна там, где сущности существуют независимо друг от друга. Заказ, клиент и позиции заказа обычно удобно хранить раздельно: так меньше повторяющихся данных, а изменения клиента не требуют обновлять каждую его покупку. Внешний ключ может запретить создание заказа для несуществующего клиента. Такой контроль целостности переносит часть правил из прикладного кода в СУБД.
Есть и цена нормализации. Чтобы собрать страницу заказа с именем клиента и списком позиций, запросу может понадобиться соединить несколько таблиц. Это нормальная операция реляционной модели, но её производительность зависит от объёма данных, индексов и плана выполнения. Удобство структуры нельзя оценивать отдельно от запросов, которые приложение делает постоянно.
Несколько свойств таблиц, которые легко упустить:
- SQL не обещает порядок строк без
ORDER BY. Результат может выглядеть упорядоченным в одном запуске, но план выполнения, индекс или изменение данных способны поменять последовательность. - Столбец и его тип задают контракт. Изменение типа или ограничений требует миграции, а её стоимость зависит от операции, размера таблицы и версии СУБД.
- Индекс не становится бесплатным после создания. Система обновляет его при изменении соответствующих данных, поэтому лишние индексы увеличивают объём хранения и могут замедлять запись.
- Ограничения целостности полезны только тогда, когда соответствуют правилам предметной области. Если внешние ключи или уникальность обойти в приложении невозможно, часть ошибок отсечётся до того, как попадёт в данные.
Реляционная модель особенно удобна, когда структура сущностей известна, между ними есть устойчивые связи, а запросы объединяют данные по нескольким условиям. Это не означает, что схему нельзя менять. Её меняют миграциями, проверяя совместимость старой и новой версии приложения. Чем больше сервисов и процессов зависят от схемы, тем важнее планировать такие изменения отдельно от обычной правки кода.
Документные базы данных: структура BSON и роль поля _id в MongoDB
В MongoDB основная логическая единица хранения называется документом. Документы собраны в коллекции. Их формат основан на BSON, бинарном представлении, похожем на JSON, но с дополнительными типами, включая даты, бинарные данные и числовые типы с различной точностью и диапазоном.
Документы одной коллекции могут иметь разные наборы полей. Это удобно, когда записи по природе неоднородны или модель развивается быстро. Но отсутствие обязательной фиксированной схемы не отменяет необходимости договориться о структуре. Если разные версии приложения записывают одно и то же значение под разными именами или в несовместимых форматах, чтение становится сложнее. Проверку формы документа можно организовать на уровне приложения, а в MongoDB также поддерживаются правила валидации коллекций.
Каждый документ в стандартной коллекции должен иметь уникальное поле _id. Если клиент не передал его сам, драйвер обычно создаёт значение ObjectId до отправки документа серверу. Это позволяет сформировать идентификатор без отдельного запроса к базе. ObjectId занимает 12 байт: в современной структуре первые 4 байта содержат время создания в секундах, следующие 5 байт представляют случайное значение, а последние 3 байта содержат счётчик. Старое описание, в котором внутри ObjectId фигурируют идентификатор процесса и отдельный машинный компонент, не соответствует актуальной структуре.
ObjectId удобен как идентификатор, но не обязан быть ключом для каждой предметной задачи. Если приложению нужен внешний идентификатор, номер договора или ключ, который приходит из другой системы, его можно хранить отдельно, задав уникальное ограничение при необходимости. Сам _id обеспечивает идентификацию документа, но его наличие не отвечает на вопрос, как приложению моделировать связи и правила изменения данных.
| Параметр | Реляционная таблица | Документная коллекция |
|---|---|---|
| Структура записи | Столбцы с заданными типами и ограничениями | Документы с полями; набор полей может различаться |
| Идентификация | Часто первичный ключ, заданный схемой | Обязательное поле _id, часто с ObjectId |
| Связи | Внешние ключи и соединения таблиц | Вложенные документы, ссылки и подходящие операции агрегации |
| Изменение модели | Миграции схемы | Изменения формата документов и правил валидации |
| Транзакции | Поддержка транзакций и ограничений целостности | Атомарность отдельного документа; транзакции доступны и для нескольких документов |
Вложенность помогает хранить рядом данные, которые обычно читаются и изменяются вместе. Например, настройки профиля могут быть частью документа пользователя. Но если массив внутри документа без ограничений растёт со временем, документ становится неудобным для обновления и чтения. А если одни и те же сведения дублируются в тысячах документов, изменение исходного значения превращается в задачу синхронизации.
Ссылки между документами дают другой компромисс: связь явная, но для получения полного результата может понадобиться несколько запросов или операция агрегации с соединением коллекций. MongoDB поддерживает такие операции; говорить, что в документной модели соединений вообще нет, неправильно. Вопрос в том, как часто они нужны, каковы размеры выборок и насколько удобно поддерживать выбранную схему.
Документная модель не отменяет проектирование схемы. Она позволяет выбирать, где фиксировать её правила: в структуре документов, в валидации коллекции или в коде приложения.
Реляционную и документную модели поэтому не стоит ранжировать по общей шкале «строгая против гибкой». Для одной задачи гибкость документа сокращает количество сложных соединений. Для другой связи и ограничения реляционной модели лучше защищают данные от ошибок. Критерий выбора находится в реальных операциях приложения: какие данные меняются вместе, какие поля фильтруются и какие инварианты нельзя нарушать.
Механика поиска: как работают индексы и почему важен порядок полей
Без индекса СУБД может просмотреть много записей, пока не найдёт подходящие. Индекс хранит дополнительную структуру, по которой можно быстрее перейти к нужным значениям. В PostgreSQL доступно несколько типов индексов, включая B-дерево и специализированные варианты для отдельных задач. В MongoDB тип индекса выбирают по сценарию: обычный индекс, текстовый, геопространственный, хэшированный или TTL-индекс. Нельзя сводить все индексы обоих продуктов к одному устройству или обещать, что любой индекс ускорит любой запрос.
Индекс ускоряет чтение только тогда, когда планировщик может использовать его для конкретного запроса и это выгоднее альтернативы. Если запрос выбирает большую часть таблицы, последовательное чтение иногда оказывается дешевле, чем переходы по индексу и последующее чтение множества строк. Поэтому наличие индекса в схеме ещё не означает, что каждый запрос пойдёт по нему.
В составном индексе важен порядок полей. Индекс по (a, b, c) обычно полезен для запросов, начинающихся с a, а также с префиксов (a, b) и (a, b, c). По одному b он, как правило, не заменяет индекс, начинающийся с b. Это связано с тем, как значения упорядочены внутри структуры индекса: сначала группируются по первому полю, затем внутри его значений учитываются следующие.
Порядок нельзя выбирать только по правилу «самое селективное поле ставим первым». Имеют значение форма запроса, равенства и диапазоны, сортировка, группировка, распределение значений и возможности конкретной СУБД. Для одного набора запросов индекс с полями в одном порядке полезен, для другого потребуется отдельная структура. Проверять результат нужно по плану выполнения и фактической нагрузке, а не по одному предположению о селективности.
Селективность описывает, насколько условие сокращает выборку. Поле is_active со значениями true и false само по себе не говорит, что каждая выборка делится пополам. В реальных данных активных записей может быть гораздо больше или меньше неактивных. Даже если распределение близко к равномерному, полезность индекса зависит от размера таблицы, сочетания фильтров и стоимости чтения. Такой индекс может оказаться полезным в составной структуре или для отдельного набора данных, но по одному лишь числу возможных значений вывод делать нельзя.
Индекс требует места и обслуживания. При вставке или изменении записи СУБД должна обновить соответствующие индексные структуры. Чем больше индексов у таблицы или коллекции, тем больше работы может потребовать запись. Особенно это заметно, когда приложение часто меняет поля, по которым построены индексы.
В MongoDB построение индексов на больших объёмах данных тоже требует планирования. Старый совет включать background: true, чтобы строить индекс в фоне без влияния на запись, устарел: этот параметр удалён в современных версиях MongoDB. Современная процедура построения индекса допускает параллельную работу с операциями чтения и записи, но это не означает, что она бесплатна или никак не влияет на нагрузку. Построение расходует ресурсы, а на завершающих этапах требует координации с операциями на коллекции. Перед добавлением индекса в загруженную систему стоит учитывать размер данных, текущую нагрузку и требования к доступности.
Практичный порядок работы с индексами выглядит так:
1. Сначала определить запросы, которые действительно выполняются часто или критичны по времени ответа. Теоретически возможные запросы не всегда заслуживают отдельного индекса.
2. Записать фильтры, сортировки и условия соединения для каждого важного запроса. Два похожих запроса могут требовать разных индексов.
3. Проверить план выполнения и статистику на реалистичных данных. Важен не только выбранный индекс, но и объём данных, который система всё равно читает после поиска.
4. Сравнить эффект с ценой обновления и хранения индекса. Если ускорение чтения не оправдывает дополнительную нагрузку на запись, структура может быть лишней.
5. Проверить дублирование. Индекс по (a, b) способен обслуживать часть запросов по a, но не делает автоматически ненужными все другие индексы: могут различаться сортировка, уникальность, условия или тип индекса.
Индексирование проще всего объяснять как ускоренный путь к записи, но это не отдельная копия таблицы в удобном формате. Это ещё одна структура, которую нужно поддерживать в актуальном состоянии. Ошибочное ожидание обычно звучит так: добавим индекс, и чтение станет быстрее без последствий. В действительности проектирование индексов всегда затрагивает и чтение, и запись.
Транзакции и гарантии целостности при изменении записей
Транзакция объединяет операции в одну логическую единицу. Если выполнение завершается ошибкой, изменения можно откатить; если операция успешно подтверждена, система применяет её согласно своим гарантиям долговечности и изоляции. Транзакция особенно важна, когда одно действие приложения меняет несколько связанных записей.
В реляционной СУБД отдельный запрос тоже выполняется атомарно, а несколько запросов можно объединить явной транзакцией. ACID описывает четыре группы свойств: атомарность, согласованность, изоляцию и долговечность. Это не обещание одинакового поведения при любых настройках: конкретные гарантии зависят от уровня изоляции, ограничений схемы, параметров фиксации и конфигурации репликации.
Уровни READ COMMITTED, REPEATABLE READ и SERIALIZABLE задают разные правила видимости изменений и защиты от аномалий. Более строгая изоляция может ограничить некоторые эффекты конкурентных операций, но при этом требует от приложения корректно обрабатывать блокировки или повтор транзакций. Выбор уровня имеет смысл рассматривать вместе с бизнес-правилом: например, можно ли двум запросам одновременно списать остаток, если они видели одно и то же исходное значение.
В MongoDB запись одного документа атомарна. Если обновление затрагивает несколько документов, для атомарности всей группы можно использовать транзакцию. Транзакции поддерживаются в replica set и шардированных кластерах, но требуют соответствующей конфигурации и имеют дополнительные издержки. Называть их просто «дорогими» или утверждать, что MongoDB от них отказывается, было бы неверно: стоимость и допустимость зависят от объёма работы, длительности транзакции и архитектуры развертывания.
Транзакция не заменяет продуманную модель данных. Если связанные сведения читаются и меняются вместе, их иногда разумно хранить в одном документе, чтобы использовать атомарность документа. Если объединение в один документ приводит к бесконтрольному росту или сложным обновлениям, транзакция может сохранить согласованность нескольких документов. Выбор зависит от того, какие операции приложение выполняет чаще и какие правила обязано соблюдать.
Есть и другой уровень гарантий: подтверждение записи клиенту и её сохранность при отказе узла не всегда одно и то же. В распределённой системе настройки подтверждения, репликации и записи на диск влияют на то, что именно приложение считает завершённой операцией. Эти параметры нельзя подменять общим словом «транзакция»: транзакция описывает границы и свойства группы изменений, а репликация — копирование изменений между участниками системы.
Атомарная запись защищает от частичного изменения одной операции. Для согласованного изменения нескольких сущностей нужны ограничения модели и, когда требуется, транзакция.
Конкурентный доступ тоже нужно учитывать заранее. Две транзакции могут пытаться менять одну и ту же строку или документ. СУБД разрешает такие ситуации с помощью блокировок, версий данных и других механизмов, но приложение должно уметь обрабатывать конфликтные исходы. При повторе операции важно понимать, безопасно ли выполнить её ещё раз: повторное списание и повторная установка того же статуса имеют разные последствия.
Выбор модели начинается с операций
Реляционные и документные базы решают разные задачи, но граница между ними не проходит по формуле «связи против скорости» или «строгая схема против свободы». Современные СУБД предлагают несколько способов организовать данные и запросы; конкретная производительность зависит от схемы, индексов, размера данных, оборудования и характера нагрузки.
Для выбора полезно описать не абстрактный объём данных, а операции приложения:
- какие сущности создаются и изменяются вместе;
- какие поля участвуют в фильтрах и сортировках;
- нужно ли связывать данные из разных наборов;
- какие правила должны соблюдаться всегда, даже если ошибся код приложения;
- как система должна вести себя при сбое, задержке реплики или повторной отправке запроса.
Если связи устойчивы, а запросы регулярно собирают информацию из нескольких сущностей, реляционная модель даёт выразительные соединения и ограничения целостности. Если данные естественно образуют вложенные документы и читаются вместе, документная модель может упростить путь от записи к ответу. Это не гарантирует автоматического выигрыша по скорости: неудачный документный формат тоже приводит к тяжёлым запросам, а правильно спроектированная реляционная база справляется с разными нагрузками.
Особенно дорого обходится выбор, сделанный по названию технологии вместо требований к данным. Миграция между моделями затрагивает формат записи, запросы, индексы и правила согласованности. Иногда достаточно изменить отдельный участок архитектуры; иногда приходится переписывать логику, которая годами опиралась на прежние допущения. Поэтому разумная отправная точка проста: сначала описать данные и операции над ними, затем выбрать модель, а уже после этого оценивать конкретную СУБД и её настройки.