Ctrl K
Урок

SQL, NoSQL и индексы

Интуиция: картотека или склад коробок

Представь две библиотеки. В первой строгий каталог: у каждой книги карточка, полка, номер, и выдача записывается в журнал атомарно. Книга не может числиться выданной сразу двум людям. Это реляционная база (PostgreSQL, MySQL).

Во второй книги лежат в коробках, а на коробке написан ключ. Нужна коробка user:42, и ты сразу её берёшь, ничего не сверяя с журналом. Это быстро, и коробок можно расставить на сотне складов. Это NoSQL.

Ни одна библиотека не лучше другой. Они просто решают разные задачи. На собесе ценят не любимую технологию, а умение объяснить, почему выбрана именно эта.

Реляционные базы и ACID

Данные лежат в таблицах со строгой схемой. Связи между таблицами задаются внешними ключами, данные собираются через JOIN. Главный козырь реляционной базы — транзакции с гарантиями ACID:

  • A — Atomicity (атомарность). Транзакция выполняется целиком или не выполняется вовсе. Перевод денег не может списать сумму с одного счёта и не зачислить на другой.
  • C — Consistency (согласованность). После транзакции соблюдены все ограничения: UNIQUE, FOREIGN KEY, CHECK.
  • I — Isolation (изоляция). Параллельные транзакции не видят «полуготовые» данные друг друга.
  • D — Durability (долговечность). Если база ответила «закоммичено», данные переживут падение сервера.

💡 На собесе: «Для заказов и платежей беру PostgreSQL: нужны транзакции и целостность. Горизонтально масштабировать его сложнее, но на старте хватит одного мастера с репликами на чтение».

NoSQL: четыре семейства

NoSQL-базы обычно жертвуют частью гарантий (строгой схемой, JOIN, иногда мгновенной согласованностью) ради простого горизонтального масштабирования и скорости.

Тип Примеры Модель данных Когда брать Чем платишь
Реляционная PostgreSQL, MySQL Таблицы, связи Деньги, заказы, сложные связи и отчёты Шардирование сложное
Документная MongoDB JSON-документы Профили, каталоги с разной структурой Слабые JOIN, возможно дублирование
Ключ-значение Redis, DynamoDB ключ → значение Кэш, сессии, счётчики Ищет только по ключу
Колоночная (wide-column) Cassandra, HBase Строки с семействами колонок Огромный поток записей: логи, события, метрики Запросы проектируются заранее
Графовая Neo4j Узлы и рёбра Соцграф, рекомендации, «друзья друзей» Нишевая, хуже масштабируется

В реальных системах базы часто комбинируют (polyglot persistence):

HTTPSget/set по ключутранзакциипоток записейрекомендацииКлиентклиентAPI-сервиссервисRedis: сессии, кэшкэшPostgreSQL: заказы, платежиБДCassandra: события, кликиБДNeo4j: граф друзейБД
Каждая база решает свою задачу; ядро с деньгами — в реляционной

⚠️ Не тащи четыре базы в каждое решение. Каждая лишняя база — это ещё один источник отказов и ещё одна система, которую надо сопровождать. Добавляй базу, только когда можешь назвать конкретную причину.

Индексы: оглавление книги

Ищешь в книге слово «транзакция». Без оглавления листаешь все 800 страниц: в базе это full scan. С алфавитным указателем открываешь нужную страницу сразу: это индекс.

Почти во всех SQL-базах индекс по умолчанию — B-tree. Это сбалансированное дерево, где значения отсортированы, а в каждом узле лежат сотни ключей. Поиск идёт от корня вниз и на каждом шаге отбрасывает почти все варианты:

10203050708090
Упрощённо: ищем 70 — проходим 3 узла вместо перебора всех строк

На самом деле узел B-tree — это страница диска с сотнями ключей, поэтому дерево получается очень низким. Посчитаем:

Параметр Значение
Строк в таблице 100 000 000
Ключей в одном узле ~500
Глубина дерева log₅₀₀(10⁸) ≈ 3–4 уровня
Чтений страниц с индексом ~4
Чтений без индекса все ~1 000 000 страниц

Кроме поиска по точному значению, B-tree даёт диапазоны (BETWEEN, >) и сортировку (ORDER BY): данные в нём уже упорядочены.

Почему запись становится медленнее

Индекс — это отдельная структура, которую нужно держать в актуальном состоянии. Каждый INSERT, UPDATE или DELETE меняет не только таблицу, но и все её индексы. Иногда при этом приходится делить переполненные страницы дерева. Пять индексов на таблице означают примерно шесть записей вместо одной. К тому же индексы занимают место на диске и в памяти.

Правило: индексируй колонки из частых WHERE, JOIN и ORDER BY. Не индексируй «на всякий случай», особенно в таблицах, куда много пишут.

Составные индексы и порядок колонок

Составной индекс (user_id, created_at) устроен как телефонный справочник: строки отсортированы сначала по фамилии, а внутри одной фамилии — по имени. Отсюда правило левого префикса:

Запрос Индекс (user_id, created_at) работает?
WHERE user_id = 42 ✅ да
WHERE user_id = 42 AND created_at > '2024-01-01' ✅ да, идеально
WHERE user_id = 42 ORDER BY created_at DESC ✅ да, сортировка бесплатная
WHERE created_at > '2024-01-01' ❌ нет: это как искать в справочнике только по имени

Как выбрать порядок колонок:

  1. Первыми ставь колонки с проверкой на равенство (=), последними — с диапазоном (>, BETWEEN).
  2. Подбирай порядок под самые частые запросы.

Пример: лента заказов пользователя, новые сверху.

CREATE INDEX idx_orders_user_created
    ON orders (user_id, created_at DESC);

-- запрос использует индекс целиком: поиск + сортировка
SELECT id, total FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;

Как это звучит на собесе

«Основное хранилище — PostgreSQL: заказы требуют ACID. Главный запрос — история заказов пользователя, поэтому делаю составной индекс (user_id, created_at): поиск по равенству и готовая сортировка. Индекс замедлит вставки, но заказов пишем ~100 в секунду, а читаем в десятки раз чаще, так что размен выгодный. Поток кликов отправлю в Cassandra: там миллионы записей, а JOIN не нужны».

Это хороший ответ: в нём есть выбор, причина, нагрузка и цена решения.

Частые ошибки

  • «NoSQL быстрее, значит, берём MongoDB». Скорость зависит от модели данных и сценария доступа, а не от этикетки.
  • Забыть про транзакции там, где есть деньги или остатки на складе.
  • Индекс на каждую колонку. Запись замедляется, место на диске заканчивается.
  • Неверный порядок в составном индексе, например (created_at, user_id) для поиска по пользователю.
  • Индекс по колонке с двумя значениями (is_active). Он почти ничего не отсекает, и база всё равно может пойти полным сканированием.

Запомни главное: сначала смотри, как система читает и пишет данные, потом выбирай базу, а индексы строй под конкретные запросы.