Базы данных
Экспресс-шпаргалка 20/20
- SQL выбирают для строгих связей и транзакций, NoSQL для гибкой схемы и отдельных сценариев масштабирования.
INNER JOINвозвращает только совпадения,LEFT JOINоставляет все строки левой таблицы.- Индекс ускоряет поиск/сортировку, но требует памяти и обновляется на
insert/update/delete. - Запись замедляется, потому что БД должна модифицировать не только таблицу, но и все затронутые индексы.
- N+1 возникает при серии мелких запросов вместо батча; лечится joins/eager loading/batching.
- Транзакция дает атомарность, уровни изоляции контролируют аномалии конкурентного чтения/записи.
- ACID дает надежность, но в распределенных системах часто появляются компромиссы между latency и консистентностью.
- Для больших таблиц пагинацию строят по индексируемому
ORDER BYи стабильному ключу. - Offset прост, но деградирует на больших смещениях; cursor быстрее и стабильнее при большом объеме.
- План запроса (
EXPLAIN/ANALYZE) показывает scan/join strategy, cost и узкие места. - One-to-many обычно через FK, many-to-many через junction table с индексами по обоим ключам.
- Нормализация снижает дубли, денормализация ускоряет чтение ценой сложности обновлений.
- Optimistic locking через
version/updated_atзащищает от lost update без жестких блокировок. - Zero-downtime миграции делают поэтапно: additive changes -> dual-read/write -> cleanup.
- Уникальные ограничения и idempotency key защищают от дублей при ретраях и сетевых повторах.
- Кэш без стратегии инвалидации быстро становится источником stale-данных и расхождений.
- Read-replica разгружает чтение, но нужно учитывать replication lag и eventual consistency.
- Мониторинг БД: p95 latency, slow queries, lock waits, CPU/IO saturation, pool usage.
- ORM хорош для скорости разработки, raw SQL нужен для сложных и perf-критичных запросов.
- JSON в SQL удобен для гибкости схемы, но усложняет индексацию, валидацию и оптимизацию запросов.
SQL practice bridge
Используйте эту таблицу как быстрый маршрут после чтения карточек: один навык - один набор вопросов - одна исполняемая задача в SQL-песочнице.
| Навык | Ответить | Практика | Готовность |
|---|---|---|---|
Фильтрация, NULL, уникальность и стабильная выдача | q-2, q-8, q-9 | Активные клиенты, Сотрудники без ментора, Три последних заказа | Вы объясняете WHERE, IS NULL, ORDER BY и почему порядок должен быть детерминированным. |
| Join cardinality и потеря строк | q-2, q-5, q-11 | Клиенты без потери нулевых заказов, Оплаченные заказы без дублей, Сотрудники и руководители | Вы заранее предсказываете, где LEFT JOIN станет INNER JOIN, где появятся дубли и когда нужен COUNT(DISTINCT ...). |
Aggregation, GROUP BY, HAVING и расчетные поля | q-3, q-10, q-19 | Города с выручкой выше порога, Средний оплаченный чек по городам, Метки приоритета тикетов | Вы группируете только нужные строки, фильтруете агрегаты через HAVING и не считаете отмененные/дублированные записи. |
| Cursor/keyset и оконные функции | q-8, q-9, q-19 | Следующая страница заказов, Второй заказ пользователя, Накопительная выручка | Вы задаете стабильный cursor, добавляете tie-breaker и объясняете ROW_NUMBER/window order без подсказки. |
1. Когда выбрать SQL, а когда NoSQL?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
SQL выбирают, когда важны связи, транзакции, ограничения, сложные запросы и консистентность. NoSQL выбирают, когда модель доступа проще, схема часто меняется, данные естественно живут документом/ключом или нужен специализированный storage pattern. Главный критерий - не мода, а access patterns, consistency requirements и стоимость изменений.
Что сказать на интервью (30-60 секунд)
Я бы начал с вопросов к домену: есть ли транзакции между сущностями, нужны ли joins, какие инварианты должна гарантировать база, какой expected read/write pattern и как данные будут меняться. Для заказов, платежей и связей many-to-many чаще беру SQL. Для event log, key-value session store, document profile или high-write telemetry может подойти NoSQL. Важно не противопоставлять "масштабируется/не масштабируется", а назвать конкретный trade-off.
Мини-пример
-- SQL-source of truth для заказа: связи и транзакция важнее гибкой схемы.
BEGIN;
INSERT INTO orders(user_id, status) VALUES ($1, 'created');
UPDATE stock SET reserved = reserved + 1 WHERE product_id = $2;
COMMIT;
| Требование | SQL | NoSQL |
|---|---|---|
| Заказ + платеж + остаток | сильный кандидат | риск ручной консистентности |
| Документ профиля с разными полями | возможно | сильный кандидат |
| Много ad-hoc аналитических запросов | сильный кандидат | зависит от движка |
| Key-value cache/session | возможно, но тяжеловато | сильный кандидат |
Углубление (2-3 минуты)
SQL дает декларативные запросы, constraints, foreign keys, transactions и удобную работу со связями. Цена - более строгая схема, миграции и необходимость думать об индексах/планах запросов. NoSQL часто дает гибкую модель данных, удобную денормализацию и специализированные access patterns, но часть гарантий и связей приходится проектировать явно в приложении.
Кейс: маркетплейс хранит orders, payments, refunds и stock movements. Если выбрать document store только потому, что "NoSQL масштабируется", можно получить ручную синхронизацию статусов и сложные компенсации. Хороший ответ: для финансовой части SQL/transactions, для search/read model можно отдельный specialized store.
Практика
- Составьте data-model decision matrix для трех доменов: ecommerce order, user profile, analytics events.
- Для каждого домена укажите: consistency, relationships, query shapes, write rate, schema evolution.
- Назовите один сценарий polyglot persistence: где SQL остается source of truth, а NoSQL/search используется как read model.
Типичные ошибки
- Говорят "NoSQL быстрее" без access pattern и consistency model.
- Выбирают SQL/NoSQL по привычке, не описав инварианты домена.
- Денормализуют данные и забывают, кто отвечает за синхронизацию.
- Игнорируют миграции, observability и backup/restore strategy.
Follow-up вопросы
- Какие инварианты база должна гарантировать сама?
- Когда денормализация оправдана?
- Что будет source of truth при SQL + search/read model?
- Как выбор БД повлияет на миграции и rollback?
Что повторить
- ACID, constraints, foreign keys, transactions.
- Document/key-value data modeling and denormalization.
- Access patterns, consistency, source of truth.
Связанные модули и карта
2. Как объяснить разницу между INNER JOIN и LEFT JOIN?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
INNER JOIN возвращает только строки, где найдено совпадение по условию join. LEFT JOIN сначала делает совпадения, а потом сохраняет все строки левой таблицы; если справа нет пары, правые колонки будут NULL. Главный edge-case - условия в ON и WHERE могут менять результат outer join.
Что сказать на интервью (30-60 секунд)
Я объясняю join через вопрос "какие строки должны выжить". INNER JOIN отвечает: покажи только пользователей с заказами. LEFT JOIN отвечает: покажи всех пользователей, а заказ - если есть. На интервью важно не только дать определение, но и предсказать результат на маленькой таблице, особенно когда фильтр по правой таблице попал в WHERE и случайно превратил left join почти в inner join.
Мини-пример
SELECT u.id, o.id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
Если у пользователя нет orders, строка пользователя останется, а o.id будет NULL.
Углубление (2-3 минуты)
| Query | Результат |
|---|---|
users INNER JOIN orders | только users с orders |
users LEFT JOIN orders | все users, orders если есть |
LEFT JOIN ... WHERE orders.status = 'paid' | users без orders исчезнут |
LEFT JOIN ... ON ... AND orders.status = 'paid' | users сохранятся, paid order если есть |
Кейс: отчет "все пользователи и дата последнего заказа" внезапно перестал показывать пользователей без заказов. Причина - фильтр по orders.status добавили в WHERE. Хороший ответ: перенести условие в ON или явно обработать NULL, если бизнесу нужны пользователи без заказов.
Практика
- Предскажите результат
INNER JOIN,LEFT JOIN,LEFT JOIN + WHERE right_table.field. - Нарисуйте маленькие таблицы
usersиordersна 3 строки и выпишите итог руками. - Объясните, почему
COUNT(*)иCOUNT(o.id)послеLEFT JOINдают разный смысл.
Типичные ошибки
- Ставят фильтр правой таблицы в
WHEREи теряют строки без совпадений. - Путают
NULLсправа с отсутствием строки слева. - Используют
SELECT *в join и получают конфликт/дубли колонок. - Не проверяют cardinality: one-to-many join может размножить строки.
Follow-up вопросы
- Чем условие в
ONотличается от условия вWHEREдляLEFT JOIN? - Почему one-to-many join размножает строки?
- Когда нужен
COUNT(DISTINCT u.id)? - Как проверить план join через
EXPLAIN?
Что повторить
INNER JOIN,LEFT JOIN,RIGHT/FULL JOIN.ONvsWHERE,NULL, row cardinality.- Join plans and indexes on join keys.
Связанные модули и карта
3. Что такое индекс и как он влияет на запросы?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Индекс - это дополнительная структура данных, которая помогает быстрее находить строки, фильтровать, сортировать или соединять таблицы. Он ускоряет подходящие read queries, но занимает место, требует обновления при записи и полезен только если соответствует реальному predicate/order/join pattern.
Что сказать на интервью (30-60 секунд)
Я бы сказал, что индекс похож на отсортированный справочник: вместо полного scan таблицы база может быстро перейти к нужному диапазону. Но индекс не "ускоряет таблицу вообще". Нужно смотреть запрос: WHERE user_id = ? ORDER BY created_at DESC LIMIT 20 может выиграть от composite index (user_id, created_at), а одиночный индекс на created_at может не помочь. Проверка - EXPLAIN ANALYZE, cardinality/selectivity и production p95.
Мини-пример
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
EXPLAIN ANALYZE
SELECT id, total
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 20;
Углубление (2-3 минуты)
| Query pattern | Индекс | Комментарий |
|---|---|---|
WHERE user_id = ? | (user_id) | простой lookup |
WHERE user_id = ? ORDER BY created_at | (user_id, created_at) | фильтр + сортировка |
WHERE status = 'active' при 95% active | может не помочь | низкая селективность |
Join orders.user_id = users.id | FK-side index | помогает join/filter |
LIKE '%term%' | B-tree не подходит | нужен другой подход |
Кейс: endpoint "последние заказы пользователя" делает sort на миллионах строк. Индекс только на user_id находит строки, но сортировка остается дорогой. Хороший ответ: composite index по фильтру и order, проверить план и write cost.
Практика
- Для пяти queries составьте index read/write trade-off table: predicate, order, cardinality, proposed index, write cost.
- Объясните, почему индекс на низкоселективное поле может не использоваться.
- Сравните
EXPLAINдо/после добавления composite index.
Типичные ошибки
- Добавляют индекс "на всякий случай" без запроса и плана.
- Не учитывают порядок колонок в composite index.
- Ожидают пользу от индекса на поле с плохой селективностью.
- Забывают, что индекс ускоряет чтение ценой storage/write overhead.
Follow-up вопросы
- Что такое selectivity и почему она важна?
- Когда composite index лучше двух одиночных?
- Почему
ORDER BYвлияет на выбор индекса? - Что смотреть в
EXPLAIN ANALYZE?
Что повторить
- B-tree indexes, composite indexes, selectivity.
EXPLAIN ANALYZE, sequential scan, index scan.- Read/write trade-off and storage overhead.
Связанные модули и карта
4. Почему индекс может замедлить запись?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Индекс замедляет запись, потому что при INSERT, UPDATE и DELETE база меняет не только строки таблицы, но и все затронутые индексы. Чем больше индексов и чем чаще обновляются индексируемые колонки, тем выше write amplification, storage cost, lock/IO pressure и цена maintenance.
Что сказать на интервью (30-60 секунд)
Я бы объяснил это как read/write trade-off. Индекс ускоряет конкретные чтения, но каждая запись должна поддерживать индекс в консистентном состоянии. Если таблица high-write, а мы добавили 8 индексов ради редких отчетов, запись может стать медленнее, увеличится storage и maintenance. Поэтому индекс оценивают по частоте чтения, критичности запроса, write rate и альтернативам вроде read replica/materialized view.
Мини-пример
-- Полезно для чтения, но каждый insert/update order обновит индекс.
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC);
Углубление (2-3 минуты)
| Фактор | Как бьет по записи | Что проверить |
|---|---|---|
| Много индексов | каждый надо обновить | write latency before/after |
| Индексируемая колонка часто меняется | update дороже | update frequency |
| Большой composite index | больше storage/IO | index size |
| Unique index | проверка уникальности | conflict rate |
| Foreign key side без нужного индекса | delete/update parent дороже | FK operations |
| Редкий отчетный query | индекс может не окупаться | query frequency |
Кейс: на таблицу events с тысячами inserts в секунду добавили несколько индексов для админского фильтра. Read стал быстрее, но ingestion latency вырос. Хороший ответ: измерить write path, оставить только критичные индексы, отчет вынести в read model или replica.
Практика
- Сделайте write-path cost checklist: insert rate, update fields, index count, index size, lock waits, p95 write latency.
- Для трех индексов решите: оставить, объединить в composite, удалить или перенести задачу в read model.
- Объясните, почему индекс ради редкого админского отчета может быть плохой сделкой.
Типичные ошибки
- Меряют только ускорение SELECT и игнорируют insert/update latency.
- Держат дублирующие индексы с почти одинаковым prefix.
- Индексируют каждую колонку фильтра без учета реальных query shapes.
- Не следят за размером индексов и maintenance overhead.
Follow-up вопросы
- Что такое write amplification?
- Какие индексы особенно дороги при update?
- Когда индекс лучше заменить read model?
- Как найти дублирующие или неиспользуемые индексы?
Что повторить
- Index maintenance on insert/update/delete.
- Unique/composite indexes and storage cost.
- Write latency, lock waits, read/write workload profile.
Связанные модули и карта
5. Что такое N+1 проблема и как ее избежать?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
N+1 возникает, когда приложение сначала делает один запрос за списком, а потом по одному запросу на каждый элемент списка. Вместо 1-2 запросов получается 1 + N, растет latency и нагрузка на connection pool. Лечат через join, eager loading, batching, dataloader pattern или изменение read model.
Что сказать на интервью (30-60 секунд)
Я бы описал N+1 через endpoint: получить 20 posts и автора каждого post. Плохой вариант: SELECT posts и потом 20 раз SELECT user WHERE id = ?. На dev базе это незаметно, а в production дает десятки roundtrips, забивает pool и ломает p95. Хороший вариант зависит от ORM и формы ответа: join/eager include, batch WHERE id IN (...), dataloader или заранее подготовленная read model.
Мини-пример
const posts = await db.post.findMany();
for (const post of posts) {
post.author = await db.user.findUnique({ where: { id: post.authorId } });
}
Лучше: одним запросом с join/include или батчем по authorId.
Углубление (2-3 минуты)
| Решение | Когда подходит | Риск |
|---|---|---|
| SQL join | нужны связанные данные сразу | размножение строк |
| ORM eager loading/include | стандартные relations | overfetching |
Batch WHERE id IN (...) | много одинаковых lookup | порядок/дедупликация |
| DataLoader | GraphQL/resolvers | cache scope mistakes |
| Read model | частый сложный экран | stale/invalidation cost |
| Cache | повторяемые lookup | stale data |
Кейс: GraphQL resolver для списка заказов дергает пользователя и платеж отдельно на каждый order. При 50 заказах получается 101 запрос. Хороший ответ: включить query logging, посчитать запросы на request, батчить relations, добавить regression test на query count.
Практика
- Проведите N+1 query audit: endpoint, expected rows, actual query count, repeated SQL pattern, pool impact.
- Для каждого relation выберите join, include, batch или read model.
- Напишите критерий готовности: при 20 parent rows query count не растет линейно.
Типичные ошибки
- Смотрят только время одного SQL-запроса, а не количество запросов на request.
- Исправляют N+1 join-ом и получают огромный overfetch/duplicate rows.
- Не ограничивают scope cache в DataLoader и смешивают данные пользователей.
- Не добавляют guard на query count после фикса.
Follow-up вопросы
- Как обнаружить N+1 в логах?
- Чем join отличается от batch loading по trade-off?
- Почему include/eager loading может привести к overfetching?
- Как написать тест, который поймает возврат N+1?
Что повторить
- ORM eager loading, batching, DataLoader.
- Query count logging and connection pool pressure.
- Join cardinality and read model trade-offs.
Связанные модули и карта
6. Как работает транзакция и зачем уровни изоляции?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Транзакция объединяет несколько операций в блок "все или ничего": либо изменения фиксируются через COMMIT, либо откатываются через ROLLBACK. Уровни изоляции нужны, чтобы управлять тем, какие эффекты параллельных транзакций видны друг другу: dirty read, non-repeatable read, phantom read и serialization anomalies.
Что сказать на интервью (30-60 секунд)
Я бы объяснил на переводе денег: списание, начисление и запись ledger должны либо пройти вместе, либо не пройти вообще. Но атомарность не решает все проблемы конкурентности. Если две транзакции одновременно читают и пишут связанные данные, нужен правильный isolation level, locks или retry strategy. На интервью важно назвать не только BEGIN/COMMIT, а конкретную аномалию и цену более строгой изоляции.
Мини-пример
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'alice';
UPDATE accounts SET balance = balance + 100 WHERE id = 'bob';
INSERT INTO ledger(from_id, to_id, amount) VALUES ('alice', 'bob', 100);
COMMIT;
Углубление (2-3 минуты)
| Аномалия | Что происходит | Как объяснить на интервью |
|---|---|---|
| Dirty read | читаем uncommitted данные | PostgreSQL не допускает dirty reads |
| Non-repeatable read | повторное чтение той же строки изменилось | возможно на Read Committed |
| Phantom read | повторный range query увидел новые строки | важно для отчетов/лимитов |
| Serialization anomaly | итог не соответствует serial order | нужен Serializable или retry |
| Lost update | два writer перетирают друг друга | locks/version/check condition |
Кейс: два администратора одновременно уменьшают остаток товара. Если оба прочитали stock = 1 и потом записали результат, можно продать больше, чем есть. Хороший ответ: атомарный UPDATE ... WHERE stock > 0, SELECT FOR UPDATE, optimistic version или serializable transaction с retry.
Практика
- Составьте transaction anomaly table для сценариев: transfer money, reserve stock, update profile.
- Для каждого сценария укажите: isolation level, lock/retry strategy, что логировать при rollback.
- Объясните, почему retry должен повторять всю транзакцию, а не только последний query.
Типичные ошибки
- Думают, что транзакция автоматически решает все race conditions.
- Держат транзакцию открытой вокруг сетевых вызовов и UI-логики.
- Не обрабатывают serialization/deadlock retry.
- Путают rollback бизнес-ошибки и технический rollback после сбоя.
Follow-up вопросы
- Чем
Read Committedотличается отRepeatable Read? - Почему
Serializableтребует готовности к retry? - Когда нужен
SELECT FOR UPDATE? - Почему нельзя делать внешний HTTP call внутри долгой транзакции?
Что повторить
BEGIN,COMMIT,ROLLBACK, savepoints.- Isolation anomalies and retry strategy.
- Row locks, optimistic locking, transaction duration.
Связанные модули и карта
7. Что такое ACID и где нужны компромиссы?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
ACID описывает надежность транзакционных систем: atomicity, consistency, isolation, durability. Компромиссы появляются, когда нужно распределять данные, снижать latency, повышать availability или масштабировать запись: часть гарантий переносится из одной локальной транзакции в protocols, queues, idempotency, sagas или eventual consistency.
Что сказать на интервью (30-60 секунд)
Я бы не расшифровывал ACID как школьную аббревиатуру, а связал каждую букву с риском. Atomicity - не списать без начисления. Consistency - не нарушить constraints и бизнес-инварианты. Isolation - не получить неверный результат из-за параллельных операций. Durability - не потерять подтвержденную запись после сбоя. Компромиссы начинаются, когда операция выходит за пределы одной БД или одного shard.
Мини-пример
BEGIN;
INSERT INTO payments(id, order_id, amount) VALUES ($1, $2, 1000);
UPDATE orders SET status = 'paid' WHERE id = $2 AND status = 'pending';
COMMIT;
Углубление (2-3 минуты)
| Свойство | Что защищает | Компромисс |
|---|---|---|
| Atomicity | частичный update | saga/compensation между сервисами |
| Consistency | broken invariant | часть правил в приложении опасна |
| Isolation | race conditions | locks/retries снижают throughput |
| Durability | потеря commit | fsync/replication latency |
| Strong consistency | свежие reads | выше latency/coupling |
| Eventual consistency | availability/scale | нужны reconciliation и UX для pending |
Кейс: checkout списывает деньги во внешнем платежном провайдере и обновляет order в своей БД. Одной ACID-транзакции на оба мира нет. Хороший ответ: idempotency key, outbox/queue, статус payment_pending, retry worker, reconciliation job и понятный UX для промежуточного состояния.
Практика
- Составьте ACID compromise matrix для payment, notification, inventory reservation.
- Для каждого шага укажите: можно ли сделать локальную транзакцию, нужен ли idempotency key, что делать при retry.
- Опишите, какие состояния увидит пользователь при eventual consistency.
Типичные ошибки
- Обещают ACID там, где участвуют несколько сервисов без distributed transaction.
- Не проектируют idempotency для повторов после timeout.
- Путают database consistency и business consistency.
- Не делают reconciliation для eventual states.
Follow-up вопросы
- Какой инвариант должна защищать БД, а какой приложение?
- Чем saga отличается от локальной транзакции?
- Где нужен idempotency key?
- Как объяснить пользователю промежуточный статус?
Что повторить
- ACID and transaction boundaries.
- Idempotency, outbox, saga, reconciliation.
- Strong vs eventual consistency trade-offs.
Связанные модули и карта
8. Как проектировать пагинацию для больших таблиц?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Пагинацию больших таблиц проектируют от стабильного ORDER BY, подходящего индекса и ожидаемой навигации. Для маленьких списков LIMIT/OFFSET прост и удобен, но на больших offset база все равно должна пройти пропускаемые строки. Для бесконечных лент и больших таблиц чаще нужен cursor/keyset pagination.
Что сказать на интервью (30-60 секунд)
Я начинаю с UX: нужен переход на страницу 137 или только "загрузить еще"? Потом фиксирую порядок: ORDER BY created_at DESC, id DESC, чтобы не было случайных перестановок при одинаковом времени. Затем подбираю индекс под фильтр и сортировку. На больших таблицах опасны большой OFFSET, нестабильный order и изменение данных между запросами.
Мини-пример
SELECT id, created_at, title
FROM posts
WHERE author_id = $1
ORDER BY created_at DESC, id DESC
LIMIT 20;
Углубление (2-3 минуты)
| Решение | Плюс | Риск |
|---|---|---|
| Offset pagination | просто, page numbers | большой offset дорогой |
| Cursor/keyset | стабильно и быстро для next page | сложнее random access |
ORDER BY created_at | понятно пользователю | ties дают нестабильность |
ORDER BY created_at, id | стабильный порядок | нужен composite index |
| Snapshot export | консистентный отчет | дороже и сложнее |
| Search-after/read model | хорошо для search | отдельная freshness strategy |
Кейс: админка показывает OFFSET 500000 LIMIT 50, и запрос медленно сканирует/сортирует огромный набор. Хороший ответ: определить UX, перейти на cursor для next/prev, добавить composite index и проверить EXPLAIN ANALYZE.
Практика
- Составьте pagination stability checklist: unique order, index, mutation behavior, next/prev, deleted rows.
- Для таблицы
posts(author_id, created_at, id)предложите индекс под ленту автора. - Объясните, почему
ORDER BY created_atбезidможет давать дубли/пропуски.
Типичные ошибки
- Используют
LIMITбез стабильногоORDER BY. - Делают глубокий
OFFSETна больших таблицах. - Сортируют по неиндексируемому выражению в hot endpoint.
- Не учитывают inserts/deletes между запросами страниц.
Follow-up вопросы
- Почему большой
OFFSETдорогой? - Какой order считается стабильным?
- Когда offset все еще нормален?
- Как индекс связан с
WHEREиORDER BY?
Что повторить
LIMIT,OFFSET, stableORDER BY.- Composite index for filter + sort.
- Cursor/keyset pagination and mutation edge cases.
Связанные модули и карта
9. Offset pagination vs cursor pagination?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Offset pagination выбирает страницу через LIMIT/OFFSET: просто, но глубокие страницы дорогие и нестабильны при изменениях данных. Cursor pagination передает позицию последней строки и читает "следующие после нее": лучше для больших динамичных списков, но требует стабильного сортировочного ключа и аккуратного cursor contract.
Что сказать на интервью (30-60 секунд)
Я бы сказал: offset подходит для простых таблиц и небольших административных списков, где важны номера страниц. Cursor подходит для feed/search results/infinite scroll, где важны стабильность и latency. Cursor должен включать все поля сортировки, например created_at и id, иначе одинаковые timestamps дадут пропуски или дубли.
Мини-пример
SELECT id, created_at, title
FROM posts
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Углубление (2-3 минуты)
| Критерий | Offset | Cursor |
|---|---|---|
| Номера страниц | удобно | неудобно |
| Глубокая страница | медленно | стабильно |
| Новые записи между запросами | дубли/пропуски | лучше, если cursor корректный |
| Простота API | высокая | нужен opaque cursor |
| Индекс | желателен | критичен |
| Sort key | любой стабильный | должен быть уникально-детерминированным |
Кейс: пользователь листает ленту, в это время появляются новые посты. При offset вторая страница может повторить часть первой или пропустить элементы. Хороший ответ: keyset cursor по (created_at, id), opaque cursor в API и regression тест с insert между page requests.
Практика
- Спроектируйте cursor для
ORDER BY created_at DESC, id DESC: payload, encoding, comparison condition. - Опишите тест: между первой и второй страницей добавили новый row.
- Назовите сценарий, где offset лучше cursor из-за UX page numbers.
Типичные ошибки
- Используют cursor только по
id, хотя сортировка идет поcreated_at. - Отдают cursor как доверенный user input без validation/encoding.
- Не добавляют tie-breaker к sort key.
- Обещают random page access там, где cursor API его не поддерживает.
Follow-up вопросы
- Что должно входить в cursor?
- Почему нужен tie-breaker вроде
id? - Как сделать backward pagination?
- Как cursor pagination влияет на API contract?
Что повторить
- Offset vs keyset/cursor pagination.
- Stable ordering and composite indexes.
- Opaque cursors, next/prev contract, mutation tests.
Связанные модули и карта
10. Как читать план выполнения запроса?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
План выполнения показывает, как база собирается выполнить запрос: scan type, join algorithm, sort/hash nodes, estimated rows/cost. EXPLAIN ANALYZE дополнительно запускает запрос и показывает actual time/rows, что помогает найти расхождения оценок, лишние scans, expensive sort, wrong join order или missing index.
Что сказать на интервью (30-60 секунд)
Я читаю план сверху вниз как дерево, но анализирую узкие места по nodes: где много rows, где estimate сильно отличается от actual, где Seq Scan на большой таблице, где sort ушел на disk, где nested loop умножает строки. Важно помнить: EXPLAIN ANALYZE реально выполняет запрос, поэтому для write-запросов его запускают внутри BEGIN ... ROLLBACK на безопасной среде.
Мини-пример
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 20;
Углубление (2-3 минуты)
| Что смотреть | Почему важно | Красный флаг |
|---|---|---|
Seq Scan vs Index Scan | путь доступа к данным | большая таблица без нужного filter |
| estimated rows vs actual rows | качество статистики | ошибка в десятки раз |
Sort | цена сортировки | disk sort/temp files |
Nested Loop | может быть норм или N+1-like | много loops на большие inputs |
Hash Join/Merge Join | join strategy | memory spill |
Buffers | IO/cache behavior | много reads вместо hits |
Execution Time | фактическая цена | не совпадает с ожиданием |
Кейс: endpoint тормозит, но индекс вроде есть. План показывает Bitmap Heap Scan, затем Sort, потому что индекс не покрывает ORDER BY. Хороший ответ: composite index под WHERE + ORDER BY, обновить statistics, проверить plan до/после и p95 в staging/production metrics.
Практика
- Составьте EXPLAIN plan reading checklist: scan, filter, rows estimate, loops, sort, join, buffers, execution time.
- Для одного query объясните, какой индекс вы бы добавили и что должно измениться в плане.
- Покажите, почему
EXPLAIN ANALYZEдляUPDATEнужно запускать внутри rollback-safe сценария.
Типичные ошибки
- Смотрят только total cost и игнорируют actual rows/loops.
- Видят
Seq Scanи автоматически считают это багом даже на маленькой таблице. - Запускают
EXPLAIN ANALYZEна write-запросе без rollback. - Не обновляют statistics и лечат неправильный plan случайными индексами.
Follow-up вопросы
- Чем
EXPLAINотличается отEXPLAIN ANALYZE? - Почему estimated rows могут отличаться от actual rows?
- Когда
Seq Scanнормален? - Что означает большое количество
loops?
Что повторить
- PostgreSQL
EXPLAIN,EXPLAIN ANALYZE,BUFFERS. - Scan types, join algorithms, sort/hash nodes.
- Statistics, row estimates, query-budget regression.
Связанные модули и карта
11. Как проектировать связи one-to-many и many-to-many?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
One-to-many обычно хранится как foreign key на стороне "many". Many-to-many требует отдельную relation/join-таблицу с двумя FK, уникальностью пары и, если нужно, собственными полями связи: роль, дата назначения, порядок, статус.
Что сказать на интервью (30-60 секунд)
Я сначала называю кардинальность: один пользователь имеет много заказов - FK orders.user_id; пост имеет много тегов и тег есть у многих постов - нужна post_tags. Потом фиксирую ownership: что удаляется каскадом, что запрещено удалять, где нужна история. В many-to-many почти всегда проверяю уникальность пары, индексы под оба направления чтения и наличие metadata в join-таблице.
Мини-пример
CREATE TABLE post_tags (
post_id bigint REFERENCES posts(id) ON DELETE CASCADE,
tag_id bigint REFERENCES tags(id) ON DELETE RESTRICT,
assigned_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (post_id, tag_id)
);
Углубление (2-3 минуты)
| Связь | Где хранить | Что проверить |
|---|---|---|
| One-to-many | FK на стороне "many" | nullable или обязательная связь |
| Many-to-many без metadata | join table с двумя FK | composite PK/unique pair |
| Many-to-many с metadata | полноценная entity table | audit fields, status, ordering |
| Self-reference | FK на ту же таблицу | циклы, root nodes, cascade risk |
| Delete behavior | CASCADE, RESTRICT, SET NULL | соответствует ownership |
Кейс: у пользователя есть роли в проектах. Если сделать массив roleIds внутри пользователя, сложно гарантировать целостность и искать участников проекта. Сильнее: project_members(user_id, project_id, role, joined_at) с уникальностью (project_id, user_id), FK, индексом для списка участников и отдельным индексом для проектов пользователя.
Практика
- Нарисуйте relationship cardinality map для
users,projects,project_members,roles. - Для каждой связи укажите owner, FK location, delete action и нужную уникальность.
- Проверьте два запроса: "все участники проекта" и "все проекты пользователя"; назовите индексы под оба направления.
Типичные ошибки
- Хранят many-to-many как JSON/array и теряют FK, uniqueness и удобные joins.
- Не задают уникальность пары в join-таблице и получают дубли связей.
- Ставят
ON DELETE CASCADEтам, где сущности независимы. - Не индексируют обратное направление чтения relation table.
Follow-up вопросы
- Когда join-таблица должна стать отдельной domain entity?
- Чем
RESTRICTотличается отCASCADEс точки зрения ownership? - Как запретить повторное добавление одного тега к одному посту?
- Какой индекс нужен для чтения relation table в обратную сторону?
Что повторить
- Foreign keys, primary keys, composite unique constraints.
ON DELETE CASCADE,RESTRICT,SET NULL.- Explicit vs implicit many-to-many relations.
Связанные модули и карта
12. Как выбирать между денормализацией и нормализацией?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Нормализация снижает дубли, денормализация ускоряет чтение в обмен на сложность записи.
Что сказать на интервью (30-60 секунд)
Нормализация - стартовая позиция для OLTP: один факт хранится в одном месте, меньше update/insert/delete anomalies, проще защищать инварианты constraints. Денормализация нужна осознанно: когда конкретный read path слишком дорогой, а команда готова платить write-cost, синхронизацией, backfill и проверками consistency. На интервью важно назвать не "нормализация медленная", а какой запрос, какая частота чтения и как вы докажете корректность дубля.
Мини-пример
-- Денормализованное поле ускоряет список заказов,
-- но требует синхронизации при изменении профиля.
ALTER TABLE orders ADD COLUMN customer_email text;
Углубление (2-3 минуты)
| Решение | Когда выбирать | Главный риск |
|---|---|---|
| Нормализация | частые updates, строгая целостность | больше joins/read complexity |
| Денормализация column copy | hot list/detail read | stale duplicate after update |
| Materialized view/read model | тяжелые агрегаты/отчеты | refresh lag и ownership |
| Cache вместо денормализации | дорогой read без schema change | invalidation и stampede |
| Event-driven projection | отдельный read model | eventual consistency |
Кейс: список заказов показывает email клиента. Join с customers нормален, пока latency и нагрузка приемлемы. Если endpoint стал hot path, можно добавить orders.customer_email, но только с планом синхронизации: backfill, dual-write/update hook, consistency check и понятное поведение при изменении email.
Практика
- Составьте normalization/denormalization decision table для
orders,customers,order_items. - Для каждого дубля укажите source of truth, кто обновляет копию, как найти stale rows.
- Опишите rollback: как вернуться к normalized read, если денормализация дала рассинхрон.
Типичные ошибки
- Денормализуют "на всякий случай" без конкретного read bottleneck.
- Не назначают source of truth и получают конфликтующие значения.
- Забывают backfill и consistency check для существующих данных.
- Путают cache, read model и permanent denormalized column.
Follow-up вопросы
- Какие anomalies убирает нормализация?
- Как вы докажете, что денормализация нужна?
- Кто владеет обновлением денормализованного поля?
- Что увидит пользователь, если read model отстает?
Что повторить
- Normal forms, update/insert/delete anomalies.
- Materialized views, read models, cache invalidation.
- Backfill, dual-write, consistency verification.
Связанные модули и карта
13. Что такое optimistic locking и где его применять?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Optimistic locking защищает от lost updates через версионирование записи.
Что сказать на интервью (30-60 секунд)
Optimistic locking подходит, когда конфликты редкие, а держать pessimistic lock дорого или неудобно. Клиент читает запись вместе с version, а update проходит только если версия не изменилась. Если UPDATE ... WHERE id = ? AND version = ? затронул 0 строк, значит кто-то уже изменил запись: нужно вернуть conflict, перечитать данные или попросить пользователя смержить изменения.
Мини-пример
UPDATE documents
SET title = $1, body = $2, version = version + 1
WHERE id = $3 AND version = $4;
Углубление (2-3 минуты)
| Сценарий | Подходит ли optimistic locking | Почему |
|---|---|---|
| Редактирование профиля | да | конфликт редкий, можно показать merge |
| Списание остатков товара | осторожно | нужен atomic condition или lock |
| Финансовый ledger | редко как основной механизм | лучше append-only + транзакции |
| CMS-документ | да | пользователь может решить conflict |
| Hot counter | нет | конфликты частые, будут постоянные retries |
Кейс: два менеджера открыли карточку клиента, первый изменил телефон, второй через минуту сохранил старую версию с новым комментарием. Без version check второй update перетрет телефон. Хороший ответ: хранить version, проверять affected rows, возвращать 409 Conflict или domain conflict state и показывать разницу пользователю.
Практика
- Сделайте optimistic-locking conflict drill: T1 и T2 читают
version = 7, T1 сохраняет, T2 пытается сохранить. - Запишите ожидаемый SQL, affected rows и ответ API/сервиса на конфликт.
- Назовите, где можно auto-retry, а где нужен ручной merge пользователя.
Типичные ошибки
- Читают
version, но не включают его вWHEREupdate-запроса. - Автоматически retry-ят пользовательские изменения и теряют смысл conflict.
- Используют timestamp с низкой точностью как единственную защиту.
- Не проверяют affected rows после update.
Follow-up вопросы
- Чем optimistic locking отличается от
SELECT FOR UPDATE? - Почему conflict - это не обязательно ошибка сервера?
- Когда retry безопасен, а когда опасен?
- Как optimistic locking связан с lost update?
Что повторить
- Lost update, version columns, affected rows.
- Pessimistic locks vs optimistic locking.
- Conflict handling in API and UI flows.
Связанные модули и карта
14. Как организовать миграции схемы без простоев?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Zero-downtime миграции требуют обратной совместимости и поэтапного релиза.
Что сказать на интервью (30-60 секунд)
Я бы описал expand/contract: сначала добавить совместимую схему, потом научить код писать/читать обе версии, выполнить backfill, проверить consistency, переключить чтение, и только потом удалить старое. Опасность не только в SQL, а в rolling deploy: старая и новая версии приложения какое-то время работают одновременно, поэтому нельзя сразу переименовать/удалить колонку или сделать блокирующую операцию на большой таблице.
Мини-пример
ALTER TABLE users ADD COLUMN full_name text;
-- deploy code that writes old + new fields
-- backfill in batches
-- deploy code that reads full_name
-- later: drop old columns after compatibility window
Углубление (2-3 минуты)
| Шаг | Что делаем | Что проверяем |
|---|---|---|
| Expand | добавляем nullable column/table/index | старая версия кода не ломается |
| Dual write/read fallback | код поддерживает обе схемы | rolling deploy safe |
| Backfill | переносим данные batches | lock time, lag, retry, progress |
| Validate | сравниваем old/new данные | нет stale/missing rows |
| Switch read | читаем новую схему | feature flag/rollback path |
| Contract | удаляем старое позже | нет старых consumers |
Кейс: нужно заменить first_name/last_name на full_name. Нельзя просто удалить старые колонки в одном релизе: часть приложений еще может читать старую схему. Хороший ответ: добавить новую колонку, dual-write, batch backfill, метрика расхождений, переключение чтения и отложенное удаление.
Практика
- Составьте expand/contract migration checklist для переименования колонки на большой таблице.
- Отметьте, какие шаги rollback-safe, а какие destructive.
- Назовите проверки: row count, null count, mismatch sample, lock duration, failed batches.
Типичные ошибки
- Делают destructive change в том же релизе, где меняют код.
- Не учитывают старые процессы во время rolling deploy.
- Запускают большой backfill одной транзакцией.
- Добавляют constraint/index на большую таблицу без плана lock/concurrent/validation.
Follow-up вопросы
- Что такое expand/contract migration?
- Почему drop/rename колонки опасны при rolling deploy?
- Как добавлять constraint на большую таблицу безопаснее?
- Какие метрики покажут, что backfill идет безопасно?
Что повторить
- Expand/contract, dual write, read fallback.
- Batched backfill and rollback plan.
CREATE INDEX CONCURRENTLY,NOT VALID,VALIDATE CONSTRAINT.
Связанные модули и карта
15. Как проектировать уникальные ограничения и idempotency?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Уникальные ограничения и idempotency-ключи защищают от дублей при повторах запросов.
Что сказать на интервью (30-60 секунд)
Я разделяю два слоя. Unique constraint защищает состояние БД от дублей даже при гонках: например, один активный invite на email в project. Idempotency key защищает повтор команды после timeout/retry: тот же запрос должен вернуть тот же результат или безопасно показать, что операция уже выполнена. Надежное решение обычно хранит key, request fingerprint, status/result и использует уникальность key в транзакции.
Мини-пример
CREATE UNIQUE INDEX uniq_payment_idempotency_key
ON payment_requests (idempotency_key);
INSERT INTO payment_requests(idempotency_key, request_hash, status)
VALUES ($1, $2, 'processing')
ON CONFLICT (idempotency_key) DO NOTHING;
Углубление (2-3 минуты)
| Механизм | Что защищает | Edge-case |
|---|---|---|
| Unique constraint | invariant в таблице | NULL может вести себя не как ожидаете |
| Partial unique index | uniqueness для subset rows | soft delete / active status |
| Idempotency key | повтор команды | тот же key с другим payload |
| Request hash | misuse key detection | нельзя молча вернуть старый результат |
| Stored result/status | стабильный retry response | что делать с processing/failed |
Кейс: клиент создает платеж, получает network timeout и повторяет запрос. Без idempotency можно создать два платежа. Хороший ответ: клиент присылает key, сервер в транзакции создает запись с unique key, сохраняет fingerprint запроса и итог операции. Повтор с тем же key и тем же payload получает сохраненный результат; повтор с другим payload - conflict/misuse error.
Практика
- Составьте idempotency/unique-constraint retry matrix: first request, same retry, retry with changed payload, concurrent retry, expired key.
- Для
invites(project_id, email, status)предложите unique rule для одного active invite. - Объясните, что возвращать, если idempotency key уже есть, но операция еще
processing.
Типичные ошибки
- Проверяют "нет ли дубля" SELECT-ом перед INSERT без unique constraint.
- Не сравнивают payload при повторном idempotency key.
- Хранят idempotency key без TTL/retention policy.
- Не продумывают concurrent retry, когда первая операция еще не завершилась.
Follow-up вопросы
- Почему SELECT-before-INSERT не защищает от гонки?
- Чем idempotency отличается от retry?
- Как обработать тот же key с другим request body?
- Когда нужен partial unique index?
Что повторить
- Unique constraints, partial unique indexes,
NULLS NOT DISTINCT. INSERT ... ON CONFLICT, request fingerprint, stored response.- Retry semantics, TTL, concurrent idempotent requests.
Связанные модули и карта
16. Как кешировать данные и не ломать консистентность?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Кэш ускоряет чтение и разгружает БД, но вводит риск stale данных. Надежный ответ всегда включает pattern (cache-aside, write-through, write-behind), TTL, invalidation, stampede protection и правило, какие данные нельзя отдавать устаревшими.
Что сказать на интервью (30-60 секунд)
Я бы начал с класса данных. Каталог товаров может жить с коротким TTL и invalidate-on-write, а баланс счета или права доступа нельзя спокойно отдавать stale. В cache-aside приложение сначала читает кэш, при miss идет в БД и заполняет кэш. После write нужно либо удалить ключ, либо обновить кэш, плюс защититься от stampede, когда много запросов одновременно прогревают один тяжелый ключ.
Мини-пример
read product: cache GET product:42 -> miss -> SELECT -> SET product:42 ttl=60s
update product: UPDATE products -> DELETE product:42 -> next read warms cache
Углубление (2-3 минуты)
| Pattern | Когда подходит | Риск |
|---|---|---|
| Cache-aside | read-heavy данные | miss latency, stale до TTL |
| Write-through | нужен свежий кэш после write | write latency выше |
| Delete-on-write | простой invalidate | race: старое значение может вернуться |
| Short TTL | допустима небольшая stale window | лишняя нагрузка на БД |
| Negative cache | частые miss по несуществующим данным | долго скрывает только что созданное |
| Lock/singleflight | дорогой warmup | сложнее failure handling |
Кейс: карточка товара обновилась, но пользователи еще минуту видят старую цену. Хороший ответ: определить допустимую stale window, invalidation на write, versioned key или delete-after-commit, метрики hit rate/miss rate/stale incidents и fallback на primary DB для критичных операций.
Практика
- Составьте cache invalidation timeline: read miss, cache fill, update DB, invalidate, concurrent read, retry.
- Для
product,user_permissions,feed_pageвыберите pattern, TTL и допустимую stale window. - Опишите, как поймать cache stampede в метриках.
Типичные ошибки
- Кэшируют права доступа или деньги без строгого правила свежести.
- Делают update DB и cache в разном порядке без учета race.
- Не ставят TTL и не имеют ручного invalidate/debug path.
- Не защищают hot key от stampede после истечения TTL.
Follow-up вопросы
- Чем cache-aside отличается от write-through?
- Что такое stale window?
- Как избежать cache stampede?
- Какие данные вы не стали бы кэшировать?
Что повторить
- Cache-aside, write-through, TTL, invalidation.
- Stampede protection, negative cache, versioned keys.
- Consistency requirements for permissions, payments, catalogs.
Связанные модули и карта
17. Когда нужен read-replica и как с ним работать?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Read replica разгружает primary от read-only запросов и помогает HA/аналитическим чтениям, но данные на replica отстают от primary. После записи пользователь может не увидеть свой update, если следующий read ушел на replica.
Что сказать на интервью (30-60 секунд)
Я бы сказал, что replica - не бесплатное масштабирование всего. На нее можно отправлять списки, отчеты и read-only экраны, где допустима eventual consistency. Но read-after-write, checkout, права доступа и любые решения "можно/нельзя" часто должны идти на primary или использовать sticky read после write. Также нужно мониторить lag и уметь отключать replica routing при деградации.
Мини-пример
POST /profile -> write primary -> mark session read_primary_until=now+5s
GET /profile -> primary while sticky window is active
GET /catalog -> replica if lag is below threshold
Углубление (2-3 минуты)
| Read type | Куда маршрутизировать | Почему |
|---|---|---|
| Read-after-own-write | primary или sticky primary | replica может отставать |
| Каталог/лента | replica при acceptable lag | stale допустим |
| Permission check | primary | stale права опасны |
| Отчет/analytics | replica/warehouse | разгрузка primary |
| Long query | replica cautiously | может конфликтовать с WAL replay |
| Health/failover check | отдельный route | не смешивать с бизнес-read |
Кейс: пользователь изменил email и сразу видит старый email в профиле, потому что GET ушел на replica. Хороший ответ: sticky reads после write, explicit consistency level для endpoint, lag threshold, fallback на primary и метрики replica lag/canceled standby queries.
Практика
- Составьте replica lag routing matrix для profile, catalog, permissions, admin report.
- Для каждого read укажите: stale допустим или нет, max lag, fallback.
- Опишите тест: write на primary, read сразу после write, replica lag simulated.
Типичные ошибки
- Отправляют все SELECT на replica без классификации freshness.
- Не имеют sticky read после write.
- Не мониторят lag и canceled standby queries.
- Запускают долгие отчеты на HA-replica и мешают recovery.
Follow-up вопросы
- Почему read replica eventually consistent?
- Что такое read-after-write consistency?
- Когда нужно fallback на primary?
- Какие запросы опасно отправлять на replica?
Что повторить
- Hot standby, replication lag, read-only restrictions.
- Sticky reads, consistency levels, lag thresholds.
- Primary vs replica routing and failover behavior.
Связанные модули и карта
18. Как мониторить базу и выявлять деградации?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
Мониторинг БД должен показывать не только "медленные запросы", а симптомы деградации: latency percentiles, query volume, lock waits, connection pool saturation, replication lag, cache hit ratio, temp files, dead tuples/vacuum pressure и top queries по total time.
Что сказать на интервью (30-60 секунд)
Я бы строил мониторинг от пользовательского симптома к DB-сигналу. Если вырос p95 API, смотрю pool wait, количество активных запросов, waits/locks, top SQL по pg_stat_statements, replication lag и изменения в планах. На интервью важно не говорить "добавлю индекс", пока не понятно, это CPU, IO, locks, bad plan, connection pool или внешний retry storm.
Мини-пример
SELECT pid, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL;
Углубление (2-3 минуты)
| Сигнал | Что проверяет | Возможная причина |
|---|---|---|
| p95/p99 query latency | пользовательская деградация | bad plan, IO, lock waits |
| Pool wait / max connections | saturation приложения | connection leak, burst traffic |
pg_stat_activity waits | текущие блокировки/IO | lock contention, WAL sync |
pg_stat_statements total time | самые дорогие SQL | hot query или N+1 |
| Replication lag | свежесть replica | heavy write/WAL replay delay |
| Temp files/sorts | memory/work_mem pressure | сортировка/агрегация на disk |
| Dead tuples/vacuum | bloat pressure | неуспевающий vacuum |
Кейс: после релиза API стал медленнее, но CPU БД нормальный. План расследования: сравнить top SQL до/после, посмотреть pool wait, waits по locks, EXPLAIN ANALYZE для изменившегося запроса и rollback/feature flag, если деградация связана с новым query shape.
Практика
- Составьте DB degradation signal map: symptom, metric, likely cause, first action.
- Для "p95 API вырос в 3 раза" назовите первые 5 DB-проверок.
- Опишите alert, который отличает slow query от connection pool saturation.
Типичные ошибки
- Смотрят только average latency и пропускают p95/p99.
- Не различают query execution time и pool wait time.
- Не хранят baseline top queries до релиза.
- Лечат любую деградацию индексом без проверки waits/locks.
Follow-up вопросы
- Чем slow query отличается от pool saturation?
- Что покажет
pg_stat_activity? - Зачем нужен
pg_stat_statements? - Какие метрики нужны для read replica?
Что повторить
pg_stat_activity, wait events, locks.pg_stat_statements, top queries, baselines.- Pool metrics, replication lag, vacuum/bloat basics.
Связанные модули и карта
19. Какие риски у ORM и когда писать raw SQL?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
ORM ускоряет CRUD, миграции и типизацию доступа к данным, но может скрывать N+1, overfetching, неудачные joins, транзакционные границы и ограничения SQL-диалекта. Raw SQL нужен, когда запрос сложнее выразить корректно/эффективно через ORM или когда важен точный контроль плана.
Что сказать на интервью (30-60 секунд)
Я не противопоставляю ORM и SQL. ORM нормален для типовых операций, но я обязательно смотрю, какой SQL он генерирует. Raw SQL выбираю для CTE/window functions, bulk updates, report queries, специфичных индексов, locking hints или когда ORM генерирует плохой план. При raw SQL обязательны parameterized queries, review, тесты и защита от SQL injection.
Мини-пример
const rows = await prisma.$queryRaw`
SELECT user_id, count(*)::int AS orders_count
FROM orders
WHERE created_at >= ${from}
GROUP BY user_id
`;
Углубление (2-3 минуты)
| Ситуация | ORM | Raw SQL |
|---|---|---|
| Simple CRUD | подходит | избыточно |
| Relation include | удобно | проверить N+1/overfetch |
| Bulk update | может быть ограничен | часто точнее |
| Window/CTE/report | может быть сложно | подходит |
| Dynamic filters | удобно | опасно без параметров |
| Locking/transaction nuance | зависит от ORM | точный контроль |
Кейс: endpoint "топ клиентов за месяц" через ORM грузит заказы и считает в приложении. На малых данных работает, на проде ломает memory/latency. Хороший ответ: агрегировать в SQL, проверить EXPLAIN ANALYZE, оставить ORM для mapping, а raw query оформить параметризованно и покрыть тестом.
Практика
- Составьте ORM/raw SQL decision drill для CRUD, report, bulk update, lock-sensitive workflow.
- Для одного ORM include запишите ожидаемый SQL и риск N+1/overfetch.
- Перепишите динамический фильтр так, чтобы user input не попадал в строковую конкатенацию.
Типичные ошибки
- Не смотрят generated SQL и считают ORM "магией".
- Пишут raw SQL через string concatenation с user input.
- Тянут большие наборы в память вместо aggregation в БД.
- Не тестируют transaction/locking behavior, потому что ORM скрыл детали.
Follow-up вопросы
- Когда raw SQL оправдан?
- Как безопасно параметризовать raw query?
- Как ORM может создать N+1?
- Что вы проверите в плане после замены ORM-запроса?
Что повторить
- Generated SQL, query logging,
EXPLAIN ANALYZE. - Parameterized raw queries and SQL injection risks.
- ORM relations, batching, transactions, bulk operations.
Связанные модули и карта
20. Как объяснить trade-offs хранения JSON в SQL БД?
Теги: database, sql
Сложность: Middle/Senior
Короткий ответ
JSON/JSONB в SQL полезен для гибких атрибутов, внешних payloads и редких полей, но плохо заменяет нормальную модель для данных с частыми фильтрами, joins, constraints и updates. В PostgreSQL обычно выбирают jsonb для обработки и индексации, но помнят про GIN-index trade-offs и row-level lock при обновлении большого документа.
Что сказать на интервью (30-60 секунд)
Я бы сказал: JSON в SQL хорош, когда схема частично неизвестна или меняется быстрее, чем core model. Но если поле участвует в WHERE, сортировке, FK, unique constraint или бизнес-инварианте, его часто стоит вынести в колонку/таблицу. Для JSONB нужно заранее знать access patterns: общий GIN индекс гибкий, expression index меньше и точнее, а большие документы увеличивают write cost.
Мини-пример
CREATE TABLE events (
id bigserial PRIMARY KEY,
type text NOT NULL,
payload jsonb NOT NULL
);
CREATE INDEX events_payload_user_idx
ON events ((payload ->> 'userId'));
Углубление (2-3 минуты)
| Данные | Лучше JSONB | Лучше колонка/таблица |
|---|---|---|
| Rare optional metadata | да | нет |
| External webhook payload | да, raw snapshot | нормализовать важные поля |
| Частый filter/sort | только с индексом | чаще колонка |
| FK/unique constraint | нет | да |
| Частые partial updates | осторожно | чаще нормализовать |
| Audit/event log | часто да | core fields отдельно |
Кейс: marketplace хранит характеристики товаров. Для редких атрибутов вроде screenRefreshRate JSONB нормален. Но brand, price, categoryId, availability должны быть колонками, потому что по ним фильтруют, сортируют, строят индексы и проверяют инварианты.
Практика
- Составьте JSON-in-SQL modeling checklist для
products.attributes. - Разделите поля на core columns, indexed JSON expressions, raw payload only.
- Для трех запросов укажите, нужен ли GIN, expression index или нормализация.
Типичные ошибки
- Кладут весь domain object в JSON и теряют constraints.
- Фильтруют по JSON-полю без подходящего индекса.
- Не различают
jsonиjsonb. - Обновляют большой JSON-документ ради маленького поля в hot write path.
Follow-up вопросы
- Чем
jsonотличается отjsonb? - Когда JSONB лучше нормализованной таблицы?
- Что нельзя надежно выразить внутри JSON без внешних проверок?
- Как выбрать GIN vs expression index?
Что повторить
- PostgreSQL
jsonvsjsonb, containment, existence. - GIN indexes,
jsonb_ops,jsonb_path_ops, expression indexes. - Constraints, row-level locks, normalization boundary.
Куда дальше
- Вернитесь в модуль: Базы данных.
- Сверьтесь с картой темы: Базы данных.
- Закрепите SQL-навык в SQL-песочнице: начните с
Активные клиенты, затем переходите к join, aggregation и cursor задачам из bridge выше. - Продолжайте по маршруту: Senior трек.