2. Что такое база данных и СУБД
База данных vs СУБД. Разделяем понятия
В повседневных разговорах бэкенд-разработчиков часто звучат фразы: «Мы храним профили в базе данных PostgreSQL» или «Сервис упал, потому что база не отвечает». Для беглого диалога у кулера это привычный сленг, но для инженера, проектирующего отказоустойчивые системы, терминологическая небрежность опасна. Она маскирует границу между пассивными данными и активным вычислительным ядром.
В классической теории вычислительных систем эти понятия строго разделены:
- База данных (БД, Database) — это организованная совокупность структурированных данных. На физическом уровне это набор файлов на диске (файловой системе ext4, XFS или ZFS), представляющих собой последовательности байтов, упакованных в блоки определенного формата.
- СУБД (Система Управления Базами Данных, DBMS — Database Management System) — это полномасштабный программный комплекс, функционирующий поверх этих файлов. СУБД берет на себя управление сетевыми соединениями, изоляцию конкурентных транзакций, кэширование страниц в ОЗУ, синтаксический анализ запросов и гарантию сохранности информации при аппаратных сбоях.
PostgreSQL, MySQL, Redis, ClickHouse, SQLite — всё это СУБД, а не базы данных. Ваша программа на Go устанавливает TCP-сокет или открывает дескриптор именно к процессу СУБД, а СУБД транслирует ваши высокоуровневые декларативные намерения в физические операции чтения и записи страниц данных.
Почему мы не используем просто файлы?
Каждый разработчик на заре карьеры задает себе соблазнительный вопрос: «Зачем нам разворачивать тяжелый демон PostgreSQL, настраивать пулы соединений и писать SQL, если можно просто сериализовать структуру в JSON или CSV и дописать в файл?»
Давайте посмотрим на хрестоматийный пример наивной записи данных на Go:
func saveUser(u User) error {
// Открываем файл в режиме добавления
file, err := os.OpenFile("users.csv", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
line := fmt.Sprintf("%d,%s,%d\n", u.ID, u.Name, u.Age)
_, err = file.WriteString(line) // syscall write
return err
}
Этот фрагмент безупречно отработает в однопоточном консольном скрипте. Но в боевом бэкенде под реальной нагрузкой он приведет к мгновенной катастрофе. Разберем анатомию проблем по пунктам:
- Конкурентность (Concurrency) и гонки данных:
Если 1000 горутин одновременно вызовутsaveUser, системный вызовwriteбез строгой синхронизации перемешает байты строк (interleaved writes). В файле появится мусорная строка вроде15,Ale42,Bob,28x\n. Если мы защитим запись с помощьюsync.Mutex, все горутины выстроятся в единую очередь, заблокировав веб-сервер. А если приложение развернуто в 10 подах Kubernetes? Мьютекс внутри рантайма одного процесса Go бессилен перед параллельным доступом из других контейнеров к общему сетевому диску (NFS/EBS). - Частичная запись и расщепление страниц (Partial / Torn Writes):
Что произойдет, если в момент выполненияwriteаварийно отключится питание сервера или ядро ОС запаникует (Kernel Panic)? На диск успеет записаться половина буфера — например,15,Ale. При следующем чтении парсер сломается на невалидной строке, и база будет необратимо повреждена. - Катастрофическая алгоритмическая сложность поиска ($O(N)$):
Чтобы найти пользователя поemailв CSV-файле объемом 50 ГБ, вы вынуждены последовательно прочитать с накопителя все 50 ГБ данных от первого до последнего байта (Full Scan), раскодировать каждую строчку и сравнить строки в памяти. Дисковый контроллер перегреется, кэш операционной системы вымоется, а пользователь будет ждать ответа десятки секунд вместо миллисекунды.
Архитектура СУБД под капотом
Чтобы гарантировать надежность и скорость в условиях тысяч параллельных соединений, современная реляционная СУБД проектируется как специализированная микро-ОС внутри основной операционной системы.
Когда ваш Go-код выполняет запрос:
db.QueryContext(ctx, "SELECT * FROM users WHERE id = 1")
запрос совершает путешествие через строго упорядоченные подсистемы:
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Client["Go App / database_sql"]:::network-->|"TCP сокет"| Transport["Сетевой уровень СУБД"]
Transport-->|"Текст SQL"| Parser["Парсер и Анализатор"]:::process
Parser-->|"AST дерево"| Optimizer["Оптимизатор запросов"]:::process
Optimizer-->|"План выполнения"| Exec["Исполнитель - Executor"]:::process
Exec-->|"Локи, Изоляция"| TM["Менеджер транзакций"]:::process
Exec-->|"Запрос страниц"| Buffer["Buffer Pool - Кэш в RAM"]:::entry
TM-->|"fsync"| WAL["WAL - Журнал упреждающей записи"]:::data
Buffer-->|"syscall pread, pwrite"| Storage["Storage Engine - Файлы на диске"]:::data
- Сетевой уровень (Transport): Принимает входящий TCP-поток, декодирует протокол СУБД (например, PostgreSQL Frontend/Backend Protocol), выполняет аутентификацию и связывает соединение с рабочим процессом или потоком.
- Парсер и Анализатор (Parser & Analyzer): Превращает сырую строку SQL в абстрактное синтаксическое дерево (AST), проверяет соответствие типов данных и наличие колонок в системном каталоге.
- Оптимизатор запросов (Query Optimizer / Cost-Based Optimizer): Главный мозг базы данных. На основе математической статистики распределения данных он перебирает сотни возможных путей выполнения запроса и выбирает план с минимальной ожидаемой стоимостью (CPU + Disk I/O).
- Исполнитель (Executor): Получает готовое дерево физического плана (Scan, Filter, Join, Aggregate) и шаг за шагом запрашивает кортежи данных у нижележащих слоев.
- Менеджер транзакций (Transaction Manager): Управляет блокировками, таблицей активных транзакций и версиями строк (MVCC), обеспечивая требования 1. ACID. Основы.
- Buffer Pool (Shared Buffers): Выделенный пул оперативной памяти (в production под него часто отдают от 25% до 75% всей RAM сервера), где кэшируются дисковые страницы. Если нужные данные уже в памяти — диск даже не просыпается.
- Журнал упреждающей записи (Write-Ahead Log / WAL): Прежде чем изменить страницу в памяти, СУБД обязана записать операцию в последовательный лог WAL и зафиксировать его на диске через системный вызов
fsync.
Mechanical Sympathy. Страничная организация
На физическом уровне СУБД практически никогда не оперирует отдельными строками (rows/tuples) при взаимодействии с дисковой подсистемой. Чтение отдельной 50-байтовой записи с диска привело бы к колоссальным накладным расходам контроллера.
Вместо этого все файлы таблиц и индексов разбиваются на равные монолитные блоки фиксированного размера — Страницы (Pages). В PostgreSQL размер страницы по умолчанию равен 8 КБ, в InnoDB (MySQL) — 16 КБ.
Это знание фундаментально для понимания производительности:
- Локальность данных: Если строки, которые запрашиваются вместе, лежат на одной странице (или на соседних), один системный вызов дискового чтения насыщает десятки запросов.
- Случайные чтения (Random IO): Если данные фрагментированы и разбросаны по разным концам терабайтного файла, СУБД будет вынуждена вычитывать по 8 КБ ради каждых 50 байт, создавая лавинообразную нагрузку на шину ввода-вывода.
Итог
- БД — это файлы на диске. СУБД — это интеллектуальный программный комплекс, обеспечивающий безопасный, параллельный и высокоэффективный доступ к этим файлам.
- Файловые системы ОС оперируют сырыми потоками байт. СУБД решает задачи высшего порядка: транзакционность, атомарность при аппаратных авариях и логарифмический поиск по индексам.
- Фундамент производительности реляционной СУБД опирается на блочную организацию страниц памяти (Buffer Pool) и оптимизацию дискового ввода-вывода (Sequential vs Random IO).
Теперь, когда мы определили, что представляет собой СУБД как программная архитектура и почему она незаменима в бэкенде, перейдем к фундаментальной классификации систем: для каких задач создаются строчные транзакционные хранилища, а для каких — колоночные аналитические движки? Разбираемся в следующей статье: 3. Типы баз данных. OLTP vs OLAP.