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):
⚠️ Не тащи четыре базы в каждое решение. Каждая лишняя база — это ещё один источник отказов и ещё одна система, которую надо сопровождать. Добавляй базу, только когда можешь назвать конкретную причину.
Индексы: оглавление книги
Ищешь в книге слово «транзакция». Без оглавления листаешь все 800 страниц: в базе это full scan. С алфавитным указателем открываешь нужную страницу сразу: это индекс.
Почти во всех SQL-базах индекс по умолчанию — B-tree. Это сбалансированное дерево, где значения отсортированы, а в каждом узле лежат сотни ключей. Поиск идёт от корня вниз и на каждом шаге отбрасывает почти все варианты:
На самом деле узел 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' |
❌ нет: это как искать в справочнике только по имени |
Как выбрать порядок колонок:
- Первыми ставь колонки с проверкой на равенство (
=), последними — с диапазоном (>,BETWEEN). - Подбирай порядок под самые частые запросы.
Пример: лента заказов пользователя, новые сверху.
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). Он почти ничего не отсекает, и база всё равно может пойти полным сканированием.
Запомни главное: сначала смотри, как система читает и пишет данные, потом выбирай базу, а индексы строй под конкретные запросы.