Базы данных
Базы данных
«Базы данных — это не просто хранилища данных. Это отражение архитектурных решений, которые определяют, как ваша система справится с нагрузкой через пять лет.»
1 Введение
Базы данных — один из краеугольных камней любой современной системы. Почти каждое архитектурное решение в конечном счёте упирается в вопрос: как хранить данные, как их читать и как обеспечить их согласованность при масштабировании?
Выбор базы данных — это не технический каприз, а стратегическое решение с долгосрочными последствиями. Instagram начинал с PostgreSQL и продолжает использовать его в качестве основного хранилища для большинства данных. Uber мигрировал с PostgreSQL на MySQL, а затем разработал собственную систему репликации. Discord перешёл с Cassandra на ScyllaDB (переписанная Cassandra на C++) ради снижения задержек. Каждое из этих решений имело свою цену и обоснование.
В этой главе мы разберём:
- Реляционные СУБД: PostgreSQL и MySQL, их внутреннее устройство, индексы, транзакции, шардирование и репликацию.
- NoSQL базы данных: Key-Value, Document, Column-family, Graph — когда и почему их выбирают.
- NewSQL: попытка совместить горизонтальное масштабирование с ACID-гарантиями.
- Практическую методологию выбора базы данных под конкретную задачу.
2 Реляционные базы данных
2.1 Что такое реляционная СУБД
Реляционная система управления базами данных (РСУБД) организует данные в виде таблиц (отношений), связанных между собой ключами. Данные описываются строгой схемой, а язык SQL (Structured Query Language) позволяет делать сложные многотабличные запросы с JOIN-ами, агрегациями и фильтрацией.
Главное отличие реляционных БД от прочих — гарантии ACID:
- Atomicity (Атомарность): транзакция либо выполняется целиком, либо не выполняется вовсе. Если в середине транзакции произошёл сбой, все изменения откатываются.
- Consistency (Согласованность): транзакция переводит базу из одного корректного состояния в другое. Все ограничения целостности (foreign keys, constraints) соблюдаются.
- Isolation (Изолированность): параллельно выполняющиеся транзакции не видят незафиксированных изменений друг друга (в зависимости от уровня изоляции).
- Durability (Долговечность): после фиксации (COMMIT) транзакция сохранена на диске, даже если сразу после этого произошёл сбой.
2.2 PostgreSQL vs MySQL
Обе СУБД занимают лидирующие позиции в индустрии, но у каждой есть своя ниша.
| Характеристика | PostgreSQL | MySQL (InnoDB) |
|---|---|---|
| Тип | Object-Relational DBMS | Relational DBMS |
| Лицензия | PostgreSQL License (свободная) | GPL v2 / коммерческая |
| Поддержка JSON | Отличная (JSONB с индексацией) | Базовая (JSON тип) |
| Full-text search | Встроенный, мощный | Ограниченный |
| Расширяемость | Высокая (extensions: PostGIS, TimescaleDB) | Низкая |
| Репликация | Streaming, Logical | Binary log-based |
| Производительность чтения | Хорошая | Очень хорошая |
| MVCC | Собственная реализация | InnoDB MVCC |
| Популярность в стартапах | Очень высокая | Высокая |
PostgreSQL предпочитают, когда нужны сложные запросы, хранимые процедуры на множестве языков (PL/pgSQL, Python, JavaScript), работа с геоданными (PostGIS), временными рядами (TimescaleDB) или документами (JSONB).
MySQL традиционно доминирует в веб-проектах на LAMP-стеке и там, где приоритет — скорость чтения и простота настройки. Facebook, Twitter, YouTube исторически использовали MySQL.
2.3 ACID и транзакции
Рассмотрим классический пример банковского перевода:
BEGIN; UPDATE accounts SET balance = balance - 1000 WHERE id = 1; UPDATE accounts SET balance = balance + 1000 WHERE id = 2; -- Проверяем, что баланс не стал отрицательным -- Если нет — фиксируем COMMIT; -- Если что-то пошло не так: ROLLBACK;
Без атомарности возможна ситуация, когда деньги списались с первого счёта, но не зачислились на второй (например, из-за сбоя питания между двумя UPDATE). ACID-гарантии исключают такой сценарий.
2.4 Уровни изоляции транзакций
Изоляция — самое сложное из четырёх свойств ACID. Чем строже изоляция, тем ниже параллелизм (и производительность). SQL-стандарт определяет четыре уровня:
READ UNCOMMITTED
Транзакция может читать незафиксированные изменения другой транзакции. Возможна грязное чтение (dirty read): вы читаете данные, которые потом откатятся.
T1: UPDATE users SET name = 'Alice' WHERE id = 1; -- не зафиксировано
T2: SELECT name FROM users WHERE id = 1; -- видит 'Alice', хотя T1 ещё не закоммичена
T1: ROLLBACK; -- T2 прочитал данные, которых никогда не было
На практике почти не используется. Ни PostgreSQL, ни InnoDB его не реализуют в полном смысле.
READ COMMITTED (по умолчанию в PostgreSQL)
Транзакция видит только зафиксированные данные. Грязное чтение исключено. Но возможно неповторяемое чтение (non-repeatable read): если вы дважды читаете одну строку в рамках одной транзакции, между чтениями другая транзакция могла её изменить.
REPEATABLE READ (по умолчанию в MySQL InnoDB)
Гарантирует, что одни и те же строки дают одинаковый результат при повторном чтении. Но возможно фантомное чтение (phantom read): новые строки, добавленные другой транзакцией, могут появиться в повторном диапазонном запросе.
SERIALIZABLE
Полная изоляция. Транзакции выглядят так, как будто выполняются последовательно, одна за другой. Исключает все аномалии, но значительно снижает пропускную способность из-за блокировок.
Как выбрать уровень изоляции:
- Финансовые операции → SERIALIZABLE или REPEATABLE READ
- Аналитические запросы, где небольшое расхождение данных допустимо → READ COMMITTED
- Высоконагруженные OLTP-системы → READ COMMITTED с явными блокировками там, где нужно
2.5 MVCC — многоверсионное управление конкурентным доступом
PostgreSQL реализует изоляцию через MVCC (Multi-Version Concurrency Control). Вместо того чтобы блокировать строки при чтении, СУБД хранит несколько версий каждой строки. Каждая транзакция видит снапшот данных на момент своего начала.
Строка users(id=1):
версия 1: name='Bob', xmin=100, xmax=200 (создана транзакцией 100, удалена 200)
версия 2: name='Alice', xmin=200, xmax=NULL (создана транзакцией 200, актуальна)
Транзакция с id=150 видит версию 1 (name='Bob'), транзакция с id=250 — версию 2 (name='Alice').
Следствие: устаревшие версии строк накапливаются. Процесс VACUUM в PostgreSQL периодически их удаляет. Если VACUUM не успевает, база «раздувается» — это одна из частых проблем в production.
2.6 Индексы
Индекс — структура данных, ускоряющая поиск за счёт дополнительного пространства на диске и небольших накладных расходов на запись.
B-tree индексы
B-tree (сбалансированное дерево) — индекс по умолчанию в PostgreSQL и MySQL. Поддерживает операции =, <, >, BETWEEN, LIKE 'prefix%'.
[20 | 40]
/ | \
[10|15] [25|30] [45|50]
/ | \ / | \ / | \
... ...
Высота дерева логарифмически зависит от количества записей. Для таблицы с миллиардом строк высота B-tree составит около 30 уровней. Каждый уровень — одно чтение с диска (или из кэша).
Составные индексы покрывают несколько столбцов:
CREATE INDEX idx_user_created ON posts(user_id, created_at DESC); -- Эффективно использует индекс: SELECT * FROM posts WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20; -- Не использует индекс (пропущен первый столбец): SELECT * FROM posts WHERE created_at > '2024-01-01';
Правило левого префикса: составной индекс (A, B, C) эффективен для запросов по A, (A, B), (A, B, C), но не по B или C в отдельности.
Hash индексы
Хранят хэш значения ключа. Поддерживают только =. Быстрее B-tree для точного поиска, но бесполезны для диапазонных запросов.
CREATE INDEX idx_hash ON users USING HASH (email); -- Эффективно: WHERE email = 'user@example.com' -- Неэффективно: WHERE email LIKE '%@example.com'
GiST (Generalized Search Tree)
Обобщённое дерево поиска для нестандартных типов данных: геометрических объектов, текстовых документов, диапазонов.
-- Геопространственный запрос (PostGIS) CREATE INDEX idx_geo ON places USING GIST (location); SELECT * FROM places WHERE ST_DWithin(location, ST_MakePoint(37.6, 55.7), 1000);
GIN (Generalized Inverted Index)
Инвертированный индекс для составных значений: массивов, JSONB, полнотекстового поиска. Каждый элемент значения отображается на список строк, его содержащих.
-- Полнотекстовый поиск CREATE INDEX idx_fts ON articles USING GIN (to_tsvector('russian', body)); SELECT * FROM articles WHERE to_tsvector('russian', body) @@ to_tsquery('база & данных'); -- Поиск в JSONB CREATE INDEX idx_jsonb ON events USING GIN (metadata); SELECT * FROM events WHERE metadata @> '{"type": "click"}';
Partial индексы
Индексируют только часть строк, удовлетворяющих условию. Меньше размер, быстрее обновление.
-- Индекс только по активным пользователям CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
Covering индексы (Index-Only Scan)
Если запрос обращается только к столбцам, входящим в индекс, PostgreSQL может не обращаться к таблице вовсе.
CREATE INDEX idx_covering ON orders(user_id, status, created_at); -- Index-Only Scan: все нужные данные есть в индексе SELECT status, created_at FROM orders WHERE user_id = 42;
2.7 Репликация
Репликация — механизм копирования данных с одного сервера (primary/master) на один или несколько других (replica/slave). Даёт:
- Высокую доступность: при падении primary один из replica автоматически становится новым primary.
- Горизонтальное масштабирование чтения: читающие запросы распределяются между репликами.
- Резервное копирование: реплика — живой бэкап.
Streaming Replication (PostgreSQL)
PostgreSQL передаёт WAL (Write-Ahead Log) от primary к replica в реальном времени. Реплика применяет WAL и остаётся в синхронном или асинхронном режиме.
┌──────────┐ WAL stream ┌──────────┐
│ Primary │ ─────────────► │ Replica1 │ (sync — подтверждает получение)
│ (writes) │ ─────────────► │ Replica2 │ (async — подтверждает запись на диск)
└──────────┘ └──────────┘
▲
Writes
│
Application
Синхронная репликация: primary ждёт подтверждения от replica перед отправкой ответа клиенту. Гарантирует нулевую потерю данных при failover, но увеличивает задержку записи.
Асинхронная репликация: primary не ждёт. Задержка записи минимальна, но при сбое primary до синхронизации несколько последних транзакций могут быть потеряны.
Большинство production-систем используют полусинхронный режим: хотя бы одна реплика должна подтвердить получение, остальные — асинхронны.
Logical Replication
В отличие от физической репликации (побайтовое копирование), логическая передаёт изменения на уровне SQL-операций. Позволяет:
- Реплицировать отдельные таблицы, а не всю БД.
- Реплицировать между разными версиями PostgreSQL.
- Организовать мультимастер-сценарии через такие инструменты, как Bucardo или BDR.
2.8 Шардирование реляционных БД
Шардирование — горизонтальное партиционирование данных по нескольким независимым экземплярам БД. Каждый шард хранит подмножество данных.
Реляционные СУБД изначально не проектировались для шардирования. Поэтому оно добавляет существенную сложность:
- Нет JOIN между шардами — данные нужно агрегировать в приложении.
- Нет распределённых транзакций (без дополнительного координатора).
- Решардирование требует либо даунтайма, либо сложной миграции.
Стратегии шардирования:
Range-based шардирование — данные делятся по диапазону значений ключа:
Шард 1: user_id 1 — 1,000,000
Шард 2: user_id 1,000,001 — 2,000,000
Шард 3: user_id 2,000,001 — ...
Минус: горячие шарды (hot shards). Если большинство активных пользователей имеют свежие ID, один шард получает непропорциональную нагрузку.
Hash-based шардирование — шард определяется как hash(user_id) % N:
shard_id = hash(user_id) % NUM_SHARDS
Равномерное распределение, но добавление нового шарда требует перераспределения почти всех данных.
Consistent Hashing — решение проблемы перераспределения (подробно рассматривается в Главе 13).
Практика: Instagram хранит данные пользователей в PostgreSQL с шардированием на уровне приложения через PgBouncer и собственный маршрутизатор. Каждый шард — независимый кластер с primary и несколькими репликами.
3 NoSQL базы данных
3.1 Зачем NoSQL?
Термин «NoSQL» (Not Only SQL) появился как реакция на ограничения реляционных СУБД при масштабировании. Основные причины обратиться к NoSQL:
- Объём данных выходит за рамки одного сервера, а шардирование реляционной БД слишком сложно.
- Гибкая схема — данные не вписываются в строгую таблицу (например, атрибуты продуктов в маркетплейсе кардинально отличаются для телефона и книги).
- Специфические паттерны доступа — граф-запросы, полнотекстовый поиск, временны́е ряды — требуют специализированных структур данных.
- Экстремальная производительность — некоторые NoSQL системы жертвуют согласованностью ради субмиллисекундных задержек.
Важно понимать: NoSQL — не замена SQL, а инструмент для специфических задач. Часто оптимальная архитектура использует оба типа одновременно (polyglot persistence).
3.2 Key-Value хранилища
Key-Value (KV) — простейшая модель данных: ключ → значение. Операции: GET(key), SET(key, value), DELETE(key). Никаких запросов по содержимому значений.
Где применяют:
- Кэширование (сессии, результаты запросов)
- Хранение пользовательских предпочтений
- Счётчики и лидерборды
- Rate limiting
- Распределённые блокировки
Redis
Redis (Remote Dictionary Server) — in-memory KV-хранилище с опциональной персистентностью. Хранит данные в RAM, что обеспечивает задержки порядка микросекунд.
Типы данных Redis:
String → SET/GET — кэш, счётчики (INCR)
Hash → HSET/HGET — объекты (профиль пользователя)
List → LPUSH/RPUSH/LRANGE — очереди, ленты событий
Set → SADD/SMEMBERS — уникальные теги, множества
Sorted Set → ZADD/ZRANGE — лидерборды, очереди с приоритетом
Stream → XADD/XREAD — потоковые события (конкурент Kafka для малых объёмов)
Пример: лидерборд в реальном времени
import redis r = redis.Redis() # Добавляем очко игроку r.zadd('leaderboard', {'player:42': 1500}) r.zincrby('leaderboard', 50, 'player:42') # +50 очков # Топ-10 игроков top_10 = r.zrevrange('leaderboard', 0, 9, withscores=True) # [('player:99', 2000.0), ('player:42', 1550.0), ...] # Ранг конкретного игрока rank = r.zrevrank('leaderboard', 'player:42') # 1 (0-based)
Персистентность Redis:
- RDB (Redis Database): периодические снапшоты на диск. Быстрое восстановление, но возможна потеря данных за последний период.
- AOF (Append-Only File): лог каждой операции записи. Более надёжно, но медленнее и требует больше места.
- No persistence: только in-memory. Максимальная скорость, данные теряются при перезапуске.
Redis Cluster — горизонтальное масштабирование. Данные автоматически шардируются по 16384 хэш-слотам. Каждый узел отвечает за диапазон слотов.
Node 1: слоты 0-5460
Node 2: слоты 5461-10922
Node 3: слоты 10923-16383
Twitter использует Redis для кэширования лент пользователей (Timeline Cache). При генерации ленты пользователя результат помещается в Redis Sorted Set с временными метками твитов. Лента 800 млн активных пользователей — это огромный Redis-кластер.
Amazon DynamoDB
DynamoDB — полностью управляемое KV/документное хранилище от AWS. Масштабируется автоматически, предлагает предсказуемую производительность при любой нагрузке.
Модель данных: таблица с первичным ключом (partition key или partition key + sort key). Все данные о сущности хранятся вместе — нет JOIN, нет нормализации.
# Создание записи table.put_item(Item={ 'user_id': '42', # Partition Key 'post_id': '2024-01-15-01', # Sort Key 'content': 'Hello, World!', 'likes': 0 }) # Запрос всех постов пользователя (эффективно — один partition) response = table.query( KeyConditionExpression=Key('user_id').eq('42') )
DynamoDB обеспечивает < 10ms latency при практически любом масштабе. Amazon Prime Day, при котором проходят миллиарды транзакций в день, работает преимущественно на DynamoDB.
3.3 Документные базы данных
Документная БД хранит данные как полуструктурированные документы (обычно JSON или BSON). Документы группируются в коллекции. В отличие от KV, можно делать запросы по полям внутри документа.
Преимущества:
- Гибкая схема: каждый документ может иметь уникальный набор полей.
- Естественная модель для объектно-ориентированных приложений (нет impedance mismatch).
- Вложенные объекты хранятся вместе — один запрос вместо нескольких JOIN.
MongoDB
MongoDB — лидер среди документных БД. Хранит данные в BSON (Binary JSON).
// Вставка документа db.users.insertOne({ _id: ObjectId("..."), name: "Alice", email: "alice@example.com", address: { city: "Moscow", zip: "101000" }, tags: ["developer", "backend"], createdAt: new Date() }) // Запрос с фильтрацией вложенного объекта db.users.find({ "address.city": "Moscow", tags: "backend" }).sort({ createdAt: -1 }).limit(20)
Индексы в MongoDB аналогичны реляционным: B-tree, составные, текстовые, геопространственные.
Агрегационный конвейер — аналог SQL GROUP BY и сложных запросов:
db.orders.aggregate([ { $match: { status: "completed" } }, { $group: { _id: "$userId", totalAmount: { $sum: "$amount" }, orderCount: { $count: {} } }}, { $sort: { totalAmount: -1 } }, { $limit: 10 } ])
Репликация: MongoDB использует Replica Set — набор из нескольких узлов, один из которых является primary. При падении primary остальные голосуют за нового лидера (алгоритм Raft).
Шардирование: MongoDB поддерживает шардирование нативно через mongos (роутер) и config servers.
Когда выбирать MongoDB:
- Каталог продуктов с разнородными атрибутами
- CMS и системы управления контентом
- Профили пользователей с произвольными полями
- Прототипирование, когда схема часто меняется
Когда НЕ выбирать MongoDB:
- Финансовые данные с требованием строгих транзакций (до MongoDB 4.0 транзакции были ограничены; сейчас поддерживаются, но с накладными расходами)
- Данные с множеством сложных связей (граф-запросы плохо ложатся на документную модель)
CouchDB
CouchDB — документная БД с акцентом на офлайн-работу и eventual consistency. Использует HTTP/REST API и MVCC для разрешения конфликтов. Популярна для мобильных приложений с синхронизацией данных.
3.4 Column-family базы данных (Wide Column)
Column-family (широкие колонки) — данные организованы в строки с динамическими наборами колонок. Внешне похоже на таблицу, но каждая строка может иметь разное количество колонок.
Ключевое свойство: данные хранятся на диске не построчно (как в РСУБД), а поколоночно. Это делает их исключительно эффективными для аналитических запросов, которые читают несколько колонок миллионов строк.
Модель данных:
Row Key │ Column Family "user_info" │ Column Family "activity"
─────────┼──────────────────────────────┼──────────────────────────────────
user:42 │ name: Alice, email: a@a.com │ last_login: 2024-01-15
user:43 │ name: Bob │ last_login: 2024-01-10, posts: 42
Apache Cassandra
Cassandra — распределённая column-family БД, разработанная Facebook (сейчас — проект Apache). Проектировалась для записи с высокой частотой и горизонтального масштабирования.
Архитектура Cassandra:
Cassandra использует кольцевую топологию без выделенного master-узла (peer-to-peer). Данные распределяются по кольцу через consistent hashing. Каждый узел отвечает за диапазон токенов.
[Node A: 0-85]
/ \
[Node D: 256-341] [Node B: 86-170]
\ /
[Node C: 171-255]
Replication Factor = 3: каждая строка хранится на 3 узлах. При чтении/записи можно настроить кворум:
ONE— 1 узел отвечает. Минимальная задержка, слабая согласованность.QUORUM— большинство реплик (например, 2 из 3). Баланс.ALL— все реплики. Строгая согласованность, но потеря одного узла блокирует операцию.
CQL (Cassandra Query Language) похож на SQL, но с жёсткими ограничениями:
-- Таблица оптимизирована под конкретный паттерн доступа CREATE TABLE messages ( conversation_id UUID, timestamp TIMESTAMP, sender_id UUID, content TEXT, PRIMARY KEY (conversation_id, timestamp) ) WITH CLUSTERING ORDER BY (timestamp DESC); -- Эффективный запрос: по partition key + range по clustering key SELECT * FROM messages WHERE conversation_id = ? AND timestamp > ? LIMIT 50;
Важно: в Cassandra нельзя делать произвольные запросы. Схему нужно проектировать под конкретные запросы (query-first design).
Compaction: Cassandra использует LSM-tree (Log-Structured Merge-Tree) вместо B-tree. Записи идут в Memtable (оперативная память), затем сбрасываются в SSTable (на диск). Фоновый процесс компакции периодически объединяет SSTables.
Write path:
Client → CommitLog (WAL) → Memtable → [flush] → SSTable
↓
Compaction
Read path:
Client → Memtable → Bloom Filter → SSTable
Кто использует Cassandra:
- Netflix — хранение состояния пользователей, истории просмотров (~50 кластеров)
- Discord — хранение сообщений (~5 трлн сообщений) [перешли на ScyllaDB]
- Instagram — хранение лент активности
Apache HBase
HBase — column-family БД поверх Hadoop HDFS. Строго согласована (в отличие от Cassandra). Используется для Big Data аналитики и в связке с Hadoop/Spark.
Выбор между Cassandra и HBase:
| Cassandra | HBase | |
|---|---|---|
| Согласованность | Eventual (настраивается) | Строгая |
| Архитектура | Peer-to-peer | Master-slave (HMaster) |
| Запись | Очень быстрая | Быстрая |
| Зависимости | Независимая | Требует Hadoop/ZooKeeper |
| Применение | High-throughput write, IoT | Big Data, аналитика |
3.5 Граф-базы данных
Граф-БД оптимизированы для хранения и обхода графовых структур: сущностей (вершин) и связей (рёбер) между ними.
Где необходимы:
- Социальные графы (друзья друзей, рекомендации)
- Граф знаний (Knowledge Graph)
- Системы обнаружения мошенничества (связи между аккаунтами)
- Рекомендательные системы
- Маршрутизация в сетях
Ключевая особенность: в реляционных БД запрос «найди всех друзей друзей друзей» требует трёх JOIN и экспоненциально замедляется с глубиной. В граф-БД это нативная операция обхода за константное время на каждый переход.
Neo4j
Neo4j — наиболее распространённая граф-БД. Использует язык запросов Cypher.
// Создание вершин и рёбер CREATE (alice:Person {name: 'Alice', age: 30}) CREATE (bob:Person {name: 'Bob', age: 25}) CREATE (carol:Person {name: 'Carol', age: 35}) CREATE (alice)-[:FRIENDS_WITH {since: '2020-01-01'}]->(bob) CREATE (bob)-[:FRIENDS_WITH]->(carol) // Найти друзей друзей Alice (не являющихся её прямыми друзьями) MATCH (alice:Person {name: 'Alice'})-[:FRIENDS_WITH*2]->(fof:Person) WHERE NOT (alice)-[:FRIENDS_WITH]->(fof) AND fof <> alice RETURN DISTINCT fof.name // Кратчайший путь между двумя людьми MATCH path = shortestPath( (alice:Person {name: 'Alice'})-[:FRIENDS_WITH*]-(carol:Person {name: 'Carol'}) ) RETURN length(path)
LinkedIn изначально использовал граф-БД для хранения профессиональных связей. Google Knowledge Graph хранит сотни миллиардов фактов в граф-структуре.
Amazon Neptune
Neptune — полностью управляемый граф-сервис AWS. Поддерживает два языка запросов: Gremlin (для property graphs) и SPARQL (для RDF/семантических графов).
4 NewSQL
4.1 Проблема, которую решает NewSQL
Реляционные СУБД предоставляют ACID-гарантии, но плохо масштабируются горизонтально. NoSQL масштабируется горизонтально, но жертвует согласованностью или выразительностью запросов.
NewSQL — класс систем, стремящихся совместить:
- Горизонтальное масштабирование (как NoSQL)
- ACID-транзакции (как РСУБД)
- SQL-совместимый интерфейс
4.2 CockroachDB
CockroachDB — распределённая SQL-база данных с акцентом на глобальное горизонтальное масштабирование и выживаемость при региональных сбоях (отсюда название «таракан»).
Архитектура:
- Данные хранятся в виде упорядоченного KV-store поверх RocksDB.
- Транзакции используют алгоритм MVCC + распределённый 2-Phase Locking.
- Консенсус на уровне диапазонов данных — через алгоритм Raft.
- SQL-слой транслирует запросы в операции над KV.
-- Стандартный SQL работает с автоматическим шардированием BEGIN; UPDATE accounts SET balance = balance - 500 WHERE id = 'alice'; UPDATE accounts SET balance = balance + 500 WHERE id = 'bob'; COMMIT; -- Транзакция ACID, даже если alice и bob на разных шардах
Geo-partitioning — данные конкретных пользователей можно привязать к регионам (для соответствия GDPR):
ALTER TABLE user_data CONFIGURE ZONE USING constraints = '[+region=eu-west]' WHERE user_region = 'EU';
4.3 TiDB
TiDB — NewSQL система с открытым исходным кодом от PingCAP. Отделяет вычислительный слой (TiDB Server) от хранилища (TiKV) и аналитического слой (TiFlash).
┌────────────────────────────────┐
│ TiDB Server │ ← SQL-парсер, оптимизатор запросов
├────────────┬───────────────────┤
│ TiKV │ TiFlash │
│ (OLTP KV) │ (Columnar OLAP) │ ← раздельное хранение
└────────────┴───────────────────┘
Placement Driver (PD) ← метаданные, балансировка
TiDB совместима с MySQL-протоколом — миграция требует минимальных изменений кода.
4.4 Google Spanner
Spanner — глобально распределённая РСУБД Google с уникальной особенностью: внешне согласованные транзакции в глобальном масштабе. Это означает, что даже при записи в дата-центры на разных континентах транзакция сериализуется глобально корректно.
Ключевая инновация — TrueTime API: атомарные часы и GPS-приёмники в каждом дата-центре позволяют Spanner знать, что текущее время находится в диапазоне [t_early, t_late] с гарантированной точностью. Это позволяет присваивать транзакциям метки времени и гарантировать их глобальный порядок.
Spanner лежит в основе Google Cloud Spanner — коммерческого сервиса. Статья «Spanner: Google's Globally Distributed Database» (OSDI 2012) — обязательное чтение для любого архитектора.
5 Выбор базы данных под задачу
Это один из наиболее частых вопросов на System Design интервью. Ни одна база данных не является универсальным решением.
5.1 Дерево решений
Нужны ли транзакции с несколькими сущностями?
├─ Да → Нужна ли гибкая схема?
│ ├─ Нет → Реляционная БД (PostgreSQL, MySQL)
│ └─ Да → MongoDB с транзакциями или NewSQL (CockroachDB)
└─ Нет → Каков основной паттерн доступа?
├─ По ключу, субмиллисекундные задержки → Redis / DynamoDB
├─ Документы с гибкой схемой → MongoDB
├─ Высокая частота записи, временны́е ряды → Cassandra / InfluxDB
├─ Сложные связи между сущностями → Neo4j / Neptune
├─ Полнотекстовый поиск → Elasticsearch / OpenSearch
└─ Аналитика, OLAP → ClickHouse, BigQuery, Redshift
5.2 Сравнительная таблица
| База данных | Тип | Сильные стороны | Слабые стороны | Типичные применения |
|---|---|---|---|---|
| PostgreSQL | РСУБД | ACID, сложные запросы, расширяемость | Горизонтальное масштабирование | Финансы, SaaS, общее назначение |
| MySQL | РСУБД | Скорость чтения, простота | Меньше типов данных, чем Postgres | Веб-приложения, e-commerce |
| Redis | KV / In-memory | Субмиллисекундные задержки | Объём ограничен RAM | Кэш, сессии, лидерборды |
| DynamoDB | KV / Document | Бесконечное масштабирование, managed | Vendor lock-in, ограниченные запросы | Serverless, high-scale |
| MongoDB | Document | Гибкая схема, хорошие запросы | Память, транзакции дороже | CMS, каталоги, прототипы |
| Cassandra | Wide Column | Очень высокий write throughput | Ограниченные запросы, сложность | IoT, сообщения, логи |
| Neo4j | Graph | Эффективный обход графа | Не масштабируется как NoSQL | Соцсети, рекомендации |
| CockroachDB | NewSQL | ACID + горизонтальное масштабирование | Задержки при глобальных транзакциях | Финансы, глобальные приложения |
| Elasticsearch | Поисковый | Полнотекстовый поиск, аналитика | Не для первичного хранения | Поиск, логи (ELK) |
| ClickHouse | Columnar | Очень быстрая OLAP-аналитика | Не для OLTP | Аналитика, логи, метрики |
5.3 Polyglot Persistence
Реальные системы используют несколько баз данных одновременно — каждую для своей задачи. Это называется polyglot persistence.
Пример архитектуры интернет-магазина:
┌─────────────────────────────────────────────────────────┐
│ E-commerce платформа │
├──────────────┬──────────────┬───────────────┬───────────┤
│ PostgreSQL │ Redis │ Elasticsearch │ Cassandra │
│ │ │ │ │
│ Заказы │ Сессии │ Поиск по │ История │
│ Пользователи │ Корзина │ товарам │ просмотров│
│ Транзакции │ Кэш цен │ автодополнение│ Аналитика │
└──────────────┴──────────────┴───────────────┴───────────┘
Amazon использует более 20 различных хранилищ данных в своей инфраструктуре: Aurora, DynamoDB, ElastiCache, Elasticsearch, S3 и другие — каждое для своей задачи.
6 Оценка нагрузки (Back-of-the-envelope)
При проектировании важно понимать, справится ли выбранная БД с ожидаемой нагрузкой.
Базовые числа для расчётов:
| Метрика | Значение |
|---|---|
| PostgreSQL на одном сервере | ~10,000–50,000 QPS (SELECT) |
| MySQL на одном сервере | ~50,000–100,000 QPS (SELECT) |
| Redis | ~100,000–1,000,000 ops/sec |
| Cassandra (один узел) | ~10,000–50,000 writes/sec |
| HDD random read | ~100 IOPS |
| SSD random read | ~10,000–100,000 IOPS |
Пример расчёта для социальной сети:
DAU = 10M пользователей
Средний пользователь читает ленту 5 раз/день = 50M reads/day
50M / 86400 ≈ 578 reads/sec в среднем
Пиковая нагрузка ×3 = ~1700 reads/sec
PostgreSQL на одном сервере → выдержит с запасом
+ Read replicas для масштабирования
7 Отказоустойчивость и резервирование
Стратегии резервного копирования
Физический бэкап (pg_basebackup, xtrabackup): полная копия файлов БД. Быстрое восстановление, но большой размер.
Логический бэкап (pg_dump, mysqldump): дамп SQL-команд. Переносим между версиями, но медленнее восстанавливается.
WAL Archiving (Point-in-Time Recovery): непрерывный архив журнала изменений. Позволяет восстановить состояние БД на любой момент времени.
# PostgreSQL: восстановление на момент времени restore_command = 'aws s3 cp s3://my-wal-archive/%f %p' recovery_target_time = '2024-01-15 14:30:00'
Connection Pooling
Создание нового соединения с PostgreSQL стоит ~5–10ms. При высоком параллелизме тысячи одновременных соединений перегружают СУБД.
PgBouncer — легковесный пул соединений для PostgreSQL:
Приложение (1000 воркеров) → PgBouncer → PostgreSQL (100 соединений)
Три режима работы:
- Session pooling: соединение закреплено за клиентом на время сессии.
- Transaction pooling: соединение возвращается в пул после каждой транзакции (рекомендуется).
- Statement pooling: после каждого запроса (не поддерживает транзакции).
8 Мониторинг и метрики
Ключевые метрики PostgreSQL
-- Медленные запросы SELECT query, calls, mean_exec_time, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10; -- Использование индексов SELECT relname, idx_scan, seq_scan FROM pg_stat_user_tables ORDER BY seq_scan DESC; -- Размер таблиц SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC;
SLI/SLO для баз данных
| SLI | Типичный SLO | Инструмент |
|---|---|---|
| Query latency (p99) | < 100ms для OLTP | pg_stat_statements, Prometheus |
| Availability | 99.99% | Health check endpoint |
| Replication lag | < 1 секунды | pg_stat_replication |
| Connection pool usage | < 80% | PgBouncer stats |
| Disk space | < 80% | Node Exporter |
Alerting правила (Prometheus)
- alert: PostgreSQLSlowQueries expr: pg_stat_activity_max_tx_duration{state="active"} > 60 for: 5m annotations: summary: "Транзакция выполняется более 60 секунд" - alert: ReplicationLag expr: pg_replication_lag > 30 for: 2m annotations: summary: "Задержка репликации превышает 30 секунд"
9 Trade-offs и типичные ошибки
Ошибка 1: «Всегда PostgreSQL»
PostgreSQL — отличная БД для общего назначения, но неподходящий выбор для:
- Хранения сообщений в мессенджере (миллиарды сообщений → Cassandra)
- Кэширования сессий (Redis лучше)
- Полнотекстового поиска масштаба Google (Elasticsearch)
Ошибка 2: «NoSQL решает проблему масштабирования»
NoSQL упрощает горизонтальное масштабирование, но создаёт другие проблемы:
- Eventual consistency требует обработки конфликтов в приложении.
- Отсутствие транзакций усложняет логику.
- Ограниченные запросы вынуждают денормализовать данные.
Ошибка 3: Преждевременное шардирование
Шардирование добавляет огромную сложность. Большинство систем прекрасно работают на одном сервере с вертикальным масштабированием и read-репликами до нагрузки в сотни тысяч QPS.
Правило: шардировать только тогда, когда один сервер действительно перестаёт справляться.
Ошибка 4: Игнорирование индексов
-- Без индекса на user_id: Seq Scan по 100M строк — секунды -- С индексом: Index Scan — миллисекунды EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 42;
Всегда проверяйте EXPLAIN ANALYZE для медленных запросов.
Ошибка 5: N+1 запросы
# ПЛОХО: N+1 — для 100 постов = 101 запрос к БД posts = db.query("SELECT * FROM posts LIMIT 100") for post in posts: author = db.query(f"SELECT * FROM users WHERE id = {post.user_id}") # ХОРОШО: JOIN — 1 запрос posts = db.query(""" SELECT p.*, u.name as author_name FROM posts p JOIN users u ON u.id = p.user_id LIMIT 100 """)
10 Вопросы для самопроверки
-
Объясните разницу между READ COMMITTED и REPEATABLE READ. Приведите сценарий, где выбор уровня изоляции критически важен.
-
Почему VACUUM необходим в PostgreSQL? Что происходит, если его не запускать?
-
Вы проектируете систему для хранения 100 миллиардов твитов. Какую БД выберете и почему? Как будете шардировать?
-
В чём принципиальное отличие хранения данных в Cassandra от PostgreSQL с точки зрения дискового ввода-вывода?
-
Почему в Cassandra нельзя делать запросы по произвольному столбцу? Как это влияет на проектирование схемы?
-
Когда вы выберете MongoDB вместо PostgreSQL? Приведите три конкретных примера.
-
Объясните, как работает MVCC в PostgreSQL. Почему это позволяет читателям не блокировать писателей?
-
Что такое составной индекс и правило левого префикса? Создайте составной индекс для запроса
WHERE city = ? AND age > ? ORDER BY name. -
Чем отличается синхронная репликация от асинхронной? Какой режим использовать для финансовых систем?
-
Объясните концепцию polyglot persistence. Спроектируйте хранилище данных для клона Airbnb, используя несколько БД.
11 Дополнительные ресурсы
Обязательные к прочтению
- «Designing Data-Intensive Applications» — Martin Kleppmann. Глава 3 (Storage and Retrieval), Глава 5 (Replication).
- «PostgreSQL: Up and Running» — Regina Obe, Leo Hsu.
- Spanner: Google's Globally-Distributed Database — Corbett et al. (OSDI 2012). research.google
- Dynamo: Amazon's Highly Available Key-value Store — DeCandia et al. (SOSP 2007). Основополагающая статья по eventual consistency.
- Cassandra - A Decentralized Structured Storage System — Lakshman, Malik (Facebook). dl.acm.org
Инженерные блоги
- Instagram Engineering: Sharding & IDs at Instagram
- Uber Engineering: Why Uber Engineering Switched from Postgres to MySQL
- Discord Blog: How Discord Stores Billions of Messages
- Netflix Tech Blog: Introducing Hollow
- Cloudflare Blog: How we built Rate Limiting capable of scaling to millions of domains
Видеолекции
- CMU Database Group (Andy Pavlo) — бесплатный курс по внутреннему устройству СУБД: youtube.com/c/CMUDatabaseGroup
- «Cassandra Data Modeling» — DataStax Academy
Инструменты для практики
- pgbench — нагрузочное тестирование PostgreSQL.
- sysbench — универсальный бенчмарк для MySQL.
- NoSQLBench — нагрузочное тестирование для Cassandra, DynamoDB и других.
- Explain.depesz.com — визуализация EXPLAIN ANALYZE для PostgreSQL.
Следующая глава: Глава 6: Кэширование — стратегии Cache-Aside, Read-Through, Write-Through; Redis Cluster; проблема инвалидации кэша и thundering herd.