Перейти к содержанию

Базы данных

«Возьмём 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.

CREATE INDEX idx_posts_fts ON posts USING GIN(to_tsvector('russian', body));

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.

📝 Подумай

  1. Сервис чата для команд. Какую БД выберешь для сообщений (Postgres / Mongo / Cassandra)? Почему?
  2. У тебя есть запрос WHERE user_id=? AND status='pending' ORDER BY created_at. Какой индекс создашь?
  3. В чём проблема OFFSET 1000000 LIMIT 20? Как заменить?
Ответ
  1. Если 1M пользователей и история уходит навсегда — Cassandra/ScyllaDB (отлично шкалируется по write, partition by chat_id). Если до сотни команд и нужны JOIN'ы / поиск / транзакции — Postgres с партиционированием по chat_id или created_at. Mongo — компромисс, но обычно неоправданный.
  2. (user_id, status, created_at DESC) — все три условия покрыты одним индексом. Можно частичный: (user_id, created_at DESC) WHERE status='pending' если pending — малая часть.
  3. БД сканирует и пропускает 1М строк. Решение — keyset pagination: WHERE created_at < $cursor ORDER BY created_at DESC LIMIT 20. Использует индекс, скан только LIMIT строк.

Дальше: load-balancing.md — как раздавать запросы между инстансами.