Базы данных¶
«Возьмём Postgres» — правильный дефолт. «Возьмём MongoDB, потому что NoSQL быстрее» — красная карточка. Здесь разбираемся, какую БД выбрать, как её масштабировать, и почему индексы — твой главный инструмент.
SQL vs NoSQL: когда что¶
🧩 Простыми словами. SQL — это «жёстко, нормализовано, транзакции». NoSQL — это «гибко, денормализовано, шардируется». Большинство сервисов начинаются с Postgres и так и остаются на нём.
SQL (RDBMS): Postgres, MySQL¶
✅ ACID-транзакции, JOIN'ы, сложные запросы, индексы любых видов, decades stable. ❌ Шардирование требует усилий (Citus, Vitess, application-level).
Когда выбирать: дефолт. Если данные структурированные и ты не уверен — Postgres. Постфактум подключаешь read replicas, partitioning, PgBouncer.
Key-Value: Redis, DynamoDB, Memcached¶
✅ Очень быстро (микросекунды), линейно шардируется. ❌ Только GET/SET по ключу, нет JOIN'ов, нет сложных query.
Когда: кэш, сессии, rate-limiter, leaderboard (Redis sorted set).
Document: MongoDB, Couchbase¶
✅ Гибкая схема (JSON документы), удобно для catalog/CMS, шардирование встроено. ❌ Транзакции с оговорками, cross-collection JOIN — головная боль.
Когда: catalog, content management, события с разной структурой. Не для денег.
Wide-column: Cassandra, ScyllaDB, BigTable, HBase¶
✅ Безумный write throughput (миллионы ops/s), линейная масштабируемость, multi-DC репликация из коробки. ❌ Query patterns надо знать заранее: индексы только то, что заложил при дизайне. Eventual consistency.
Когда: time-series, IoT, message history, лог событий. Discord на ScyllaDB держит миллиарды сообщений.
Graph: Neo4j, JanusGraph, Dgraph¶
✅ Запросы по графам (друзья друзей, рекомендации) на 1–2 порядка быстрее, чем JOIN в SQL. ❌ Узкая ниша. Шардировать сложно.
Когда: соцграф, knowledge graph, anti-fraud (граф транзакций).
graph TD
Q{Какие данные?} --> Struct{Жёсткая схема +<br/>транзакции?}
Struct -->|Да| SQL[SQL: Postgres]
Struct -->|Гибкая| Doc[Document: Mongo]
Q --> KV{Только ключ → значение?}
KV --> Redis[KV: Redis/Dynamo]
Q --> Time{Time-series<br/>events?}
Time --> Wide[Wide-column: Cassandra]
Q --> Graph{Связи между сущностями?}
Graph --> GraphDB[Graph: Neo4j]
🛠 В Go. Postgres — pgx/pgxpool. Redis — go-redis. Mongo — официальный
mongo-go-driver. Cassandra — gocql. ScyllaDB — gocqlx.
Репликация¶
🧩 Простыми словами. Репликация — это копии БД для (а) отказоустойчивости (один упал — есть второй), (б) масштабирования чтений (читаешь с реплики, пишешь в primary).
Master-slave (primary-replica)¶
graph LR
W[Writes] --> P[(Primary)]
P -.WAL.-> R1[(Replica 1)]
P -.WAL.-> R2[(Replica 2)]
Read1[Reads] --> R1
Read2[Reads] --> R2
✅ Просто, проверено десятилетиями. Failover автоматизируется через Patroni/repmgr. ❌ Запись только в primary — write throughput не масштабируется. Replica lag.
Master-master (multi-master)¶
graph LR
P1[(Master A)] <-->|sync| P2[(Master B)]
Writes1 --> P1
Writes2 --> P2
✅ Можно писать в любой узел. Используется для multi-region. ❌ Конфликты: одна и та же запись в обеих → как мержить? Vector clocks, last-write-wins, CRDT — всё непросто.
Postgres не делает master-master из коробки (только через extensions типа BDR). Cassandra/Dynamo — да, потому что AP-системы.
Sync vs async¶
- Synchronous. Primary ждёт ack от реплик, потом отвечает клиенту. Strong consistency, но latency = max latency реплики.
- Asynchronous. Primary отвечает сразу, реплики догоняют. Быстро, но replica может отставать (replica lag) → читатель видит stale.
- Semi-sync. Ждёт ack от хотя бы одной реплики (Postgres
synchronous_commit=on+synchronous_standby_names). Компромисс.
📊 Cheat-sheet.
| Сценарий | Sync | Async | Semi-sync |
|---|---|---|---|
| Финансы / деньги | ✅ | ❌ | возможно |
| Лента / контент | ❌ (медленно) | ✅ | ✅ |
| Cross-region DR | semi-sync | ✅ | ✅ |
Индексы¶
🧩 Простыми словами. Индекс = указатель в БД на «где лежат строки с этим значением». Без индекса БД делает полное сканирование таблицы. На таблице 1 млн строк — индекс ускоряет в 100–10000 раз.
B-tree¶
Дефолтный. Подходит для:
- equality:
WHERE id = 42 - range:
WHERE created_at > '2025-01-01' - prefix:
WHERE name LIKE 'And%' - ORDER BY (по тем же столбцам).
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
⚙️ Композитные индексы. Порядок колонок важен. (user_id, created_at)
работает для запросов с WHERE user_id=? [AND created_at...]. НЕ работает для
WHERE created_at=? без user_id.
Hash¶
Только equality. Быстрее B-tree на точный поиск, но не даёт range. В Postgres есть, но обычно не нужен — B-tree + equality почти не уступает.
Inverted (full-text)¶
Маппит «слово → список документов». Используется для поиска по тексту.
Postgres: tsvector + GIN-индекс. Production search: Elasticsearch / OpenSearch
/ Meilisearch.
GiST / GIN / BRIN (Postgres special)¶
- GIN — для массивов, JSON, full-text.
- GiST — для геоданных (PostGIS), R-tree-like.
- BRIN — block range, для огромных таблиц с временным порядком (логи).
Covering index¶
Индекс, который полностью покрывает запрос — не нужно читать саму таблицу.
CREATE INDEX idx_users_covering ON users(email) INCLUDE (id, name);
SELECT id, name FROM users WHERE email='x@y.z'; -- только индекс
Query patterns¶
🧩 Простыми словами. Прежде чем рисовать схему, перечисли запросы, которые будут на эту таблицу. Индексы и схема строятся под них.
-- Запросы на posts:
1. SELECT * FROM posts WHERE user_id=? ORDER BY created_at DESC LIMIT 20 -- профиль
2. SELECT * FROM posts WHERE id=? -- одиночный
3. SELECT count(*) FROM posts WHERE created_at > NOW()-'1 day' -- статистика
→ индексы:
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);
-- запрос 2 — primary key
-- запрос 3 — BRIN или partition by created_at
N+1 problem¶
Классика: цикл по users, на каждом — отдельный SELECT по posts.
// плохо
for _, u := range users {
posts := repo.PostsBy(ctx, u.ID) // N запросов
...
}
// хорошо: один запрос с IN
ids := collectIDs(users)
postsByUser := repo.PostsByUserIDs(ctx, ids)
В ORM решается через Preload / Include. В чистом SQL — WHERE user_id = ANY($1).
🛠 Применение в Go-проекте¶
import "github.com/jackc/pgx/v5/pgxpool"
cfg, _ := pgxpool.ParseConfig(dsn)
cfg.MaxConns = 25
cfg.MinConns = 5
cfg.MaxConnLifetime = time.Hour
cfg.MaxConnIdleTime = 30 * time.Minute
pool, _ := pgxpool.NewWithConfig(ctx, cfg)
Связка под прод: pgxpool (драйвер) + sqlc или goqu (query gen) + golang-migrate
(миграции) + PgBouncer (transaction pooling в проде).
🔥 Пример: выбор БД для нового сервиса заказов¶
Требования:
- 1k orders/sec write
- транзакции (oversell нельзя)
- запросы: по user_id, по статусу, по дате
- хранить 5 лет
Выбор:
- Postgres (CP, ACID).
- Партиционирование по created_at (год).
- Индексы:
PRIMARY KEY (id),
(user_id, created_at DESC),
(status) — частичный, WHERE status IN ('pending','processing').
- Архив старше 1 года — в холодное хранилище (Parquet on S3).
- Read replicas для аналитики/дашборда.
Mongo? — нет, нужны транзакции и строгие схемы. Cassandra? — нет, обновления статуса по PK редкие, JOIN'ы с users нужны.
❌ Типичные ошибки¶
- «NoSQL — потому что современно». Без обоснования query patterns.
- Индекс на каждый столбец «на всякий случай» → INSERT тормозят, диск раздут.
- Нет индекса на FK — JOIN сканирует.
- Используют OFFSET для пагинации больших таблиц → keyset pagination не знают.
- Не мониторят
pg_stat_statements/ slow query log. - Один primary без replica → авария = downtime.
🤖 Что спрашивает AI-ментор¶
- Когда выбирать SQL, а когда NoSQL? Дай 3 ясных критерия.
- В чём разница между B-tree и hash индексом? Когда какой?
- Что такое replica lag и как с ним жить?
- Что такое N+1 запросы и как ловить?
- Композитный индекс
(a, b)— для каких запросов работает, для каких нет?
📊 Уровни глубины¶
L1. Знаешь, что есть SQL и NoSQL. Используешь Postgres + один индекс на FK.
L2. Различаешь типы NoSQL (KV, document, wide-column, graph). Делал read replicas. Понимаешь композитные индексы и EXPLAIN ANALYZE.
L3. Спорил между Postgres + Citus и Cassandra с цифрами по latency / cost / консистентности. Видел production replication lag, слабые индексы, прогон ANALYZE, изоляции (см. sprint-2/sql.md). Использовал partitioning, covering indexes, JIT, prepared statements.
📝 Подумай¶
- Сервис чата для команд. Какую БД выберешь для сообщений (Postgres / Mongo / Cassandra)? Почему?
- У тебя есть запрос
WHERE user_id=? AND status='pending' ORDER BY created_at. Какой индекс создашь? - В чём проблема
OFFSET 1000000 LIMIT 20? Как заменить?
Ответ
- Если 1M пользователей и история уходит навсегда — Cassandra/ScyllaDB (отлично шкалируется по write, partition by chat_id). Если до сотни команд и нужны JOIN'ы / поиск / транзакции — Postgres с партиционированием по chat_id или created_at. Mongo — компромисс, но обычно неоправданный.
(user_id, status, created_at DESC)— все три условия покрыты одним индексом. Можно частичный:(user_id, created_at DESC) WHERE status='pending'если pending — малая часть.- БД сканирует и пропускает 1М строк. Решение — keyset pagination:
WHERE created_at < $cursor ORDER BY created_at DESC LIMIT 20. Использует индекс, скан только LIMIT строк.
Дальше: load-balancing.md — как раздавать запросы между инстансами.