Перейти к основному содержимому

Базы данных

Экспресс-шпаргалка 20/20

  1. SQL выбирают для строгих связей и транзакций, NoSQL для гибкой схемы и отдельных сценариев масштабирования.
  2. INNER JOIN возвращает только совпадения, LEFT JOIN оставляет все строки левой таблицы.
  3. Индекс ускоряет поиск/сортировку, но требует памяти и обновляется на insert/update/delete.
  4. Запись замедляется, потому что БД должна модифицировать не только таблицу, но и все затронутые индексы.
  5. N+1 возникает при серии мелких запросов вместо батча; лечится joins/eager loading/batching.
  6. Транзакция дает атомарность, уровни изоляции контролируют аномалии конкурентного чтения/записи.
  7. ACID дает надежность, но в распределенных системах часто появляются компромиссы между latency и консистентностью.
  8. Для больших таблиц пагинацию строят по индексируемому ORDER BY и стабильному ключу.
  9. Offset прост, но деградирует на больших смещениях; cursor быстрее и стабильнее при большом объеме.
  10. План запроса (EXPLAIN/ANALYZE) показывает scan/join strategy, cost и узкие места.
  11. One-to-many обычно через FK, many-to-many через junction table с индексами по обоим ключам.
  12. Нормализация снижает дубли, денормализация ускоряет чтение ценой сложности обновлений.
  13. Optimistic locking через version/updated_at защищает от lost update без жестких блокировок.
  14. Zero-downtime миграции делают поэтапно: additive changes -> dual-read/write -> cleanup.
  15. Уникальные ограничения и idempotency key защищают от дублей при ретраях и сетевых повторах.
  16. Кэш без стратегии инвалидации быстро становится источником stale-данных и расхождений.
  17. Read-replica разгружает чтение, но нужно учитывать replication lag и eventual consistency.
  18. Мониторинг БД: p95 latency, slow queries, lock waits, CPU/IO saturation, pool usage.
  19. ORM хорош для скорости разработки, raw SQL нужен для сложных и perf-критичных запросов.
  20. 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;
ТребованиеSQLNoSQL
Заказ + платеж + остатоксильный кандидатриск ручной консистентности
Документ профиля с разными полямивозможносильный кандидат
Много 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.

Практика

  1. Составьте data-model decision matrix для трех доменов: ecommerce order, user profile, analytics events.
  2. Для каждого домена укажите: consistency, relationships, query shapes, write rate, schema evolution.
  3. Назовите один сценарий polyglot persistence: где SQL остается source of truth, а NoSQL/search используется как read model.

Типичные ошибки

  1. Говорят "NoSQL быстрее" без access pattern и consistency model.
  2. Выбирают SQL/NoSQL по привычке, не описав инварианты домена.
  3. Денормализуют данные и забывают, кто отвечает за синхронизацию.
  4. Игнорируют миграции, observability и backup/restore strategy.

Follow-up вопросы

  1. Какие инварианты база должна гарантировать сама?
  2. Когда денормализация оправдана?
  3. Что будет source of truth при SQL + search/read model?
  4. Как выбор БД повлияет на миграции и rollback?

Что повторить

  1. ACID, constraints, foreign keys, transactions.
  2. Document/key-value data modeling and denormalization.
  3. Access patterns, consistency, source of truth.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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, если бизнесу нужны пользователи без заказов.

Практика

  1. Предскажите результат INNER JOIN, LEFT JOIN, LEFT JOIN + WHERE right_table.field.
  2. Нарисуйте маленькие таблицы users и orders на 3 строки и выпишите итог руками.
  3. Объясните, почему COUNT(*) и COUNT(o.id) после LEFT JOIN дают разный смысл.

Типичные ошибки

  1. Ставят фильтр правой таблицы в WHERE и теряют строки без совпадений.
  2. Путают NULL справа с отсутствием строки слева.
  3. Используют SELECT * в join и получают конфликт/дубли колонок.
  4. Не проверяют cardinality: one-to-many join может размножить строки.

Follow-up вопросы

  1. Чем условие в ON отличается от условия в WHERE для LEFT JOIN?
  2. Почему one-to-many join размножает строки?
  3. Когда нужен COUNT(DISTINCT u.id)?
  4. Как проверить план join через EXPLAIN?

Что повторить

  1. INNER JOIN, LEFT JOIN, RIGHT/FULL JOIN.
  2. ON vs WHERE, NULL, row cardinality.
  3. Join plans and indexes on join keys.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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.idFK-side indexпомогает join/filter
LIKE '%term%'B-tree не подходитнужен другой подход

Кейс: endpoint "последние заказы пользователя" делает sort на миллионах строк. Индекс только на user_id находит строки, но сортировка остается дорогой. Хороший ответ: composite index по фильтру и order, проверить план и write cost.

Практика

  1. Для пяти queries составьте index read/write trade-off table: predicate, order, cardinality, proposed index, write cost.
  2. Объясните, почему индекс на низкоселективное поле может не использоваться.
  3. Сравните EXPLAIN до/после добавления composite index.

Типичные ошибки

  1. Добавляют индекс "на всякий случай" без запроса и плана.
  2. Не учитывают порядок колонок в composite index.
  3. Ожидают пользу от индекса на поле с плохой селективностью.
  4. Забывают, что индекс ускоряет чтение ценой storage/write overhead.

Follow-up вопросы

  1. Что такое selectivity и почему она важна?
  2. Когда composite index лучше двух одиночных?
  3. Почему ORDER BY влияет на выбор индекса?
  4. Что смотреть в EXPLAIN ANALYZE?

Что повторить

  1. B-tree indexes, composite indexes, selectivity.
  2. EXPLAIN ANALYZE, sequential scan, index scan.
  3. Read/write trade-off and storage overhead.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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/IOindex 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.

Практика

  1. Сделайте write-path cost checklist: insert rate, update fields, index count, index size, lock waits, p95 write latency.
  2. Для трех индексов решите: оставить, объединить в composite, удалить или перенести задачу в read model.
  3. Объясните, почему индекс ради редкого админского отчета может быть плохой сделкой.

Типичные ошибки

  1. Меряют только ускорение SELECT и игнорируют insert/update latency.
  2. Держат дублирующие индексы с почти одинаковым prefix.
  3. Индексируют каждую колонку фильтра без учета реальных query shapes.
  4. Не следят за размером индексов и maintenance overhead.

Follow-up вопросы

  1. Что такое write amplification?
  2. Какие индексы особенно дороги при update?
  3. Когда индекс лучше заменить read model?
  4. Как найти дублирующие или неиспользуемые индексы?

Что повторить

  1. Index maintenance on insert/update/delete.
  2. Unique/composite indexes and storage cost.
  3. Write latency, lock waits, read/write workload profile.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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стандартные relationsoverfetching
Batch WHERE id IN (...)много одинаковых lookupпорядок/дедупликация
DataLoaderGraphQL/resolverscache scope mistakes
Read modelчастый сложный экранstale/invalidation cost
Cacheповторяемые lookupstale data

Кейс: GraphQL resolver для списка заказов дергает пользователя и платеж отдельно на каждый order. При 50 заказах получается 101 запрос. Хороший ответ: включить query logging, посчитать запросы на request, батчить relations, добавить regression test на query count.

Практика

  1. Проведите N+1 query audit: endpoint, expected rows, actual query count, repeated SQL pattern, pool impact.
  2. Для каждого relation выберите join, include, batch или read model.
  3. Напишите критерий готовности: при 20 parent rows query count не растет линейно.

Типичные ошибки

  1. Смотрят только время одного SQL-запроса, а не количество запросов на request.
  2. Исправляют N+1 join-ом и получают огромный overfetch/duplicate rows.
  3. Не ограничивают scope cache в DataLoader и смешивают данные пользователей.
  4. Не добавляют guard на query count после фикса.

Follow-up вопросы

  1. Как обнаружить N+1 в логах?
  2. Чем join отличается от batch loading по trade-off?
  3. Почему include/eager loading может привести к overfetching?
  4. Как написать тест, который поймает возврат N+1?

Что повторить

  1. ORM eager loading, batching, DataLoader.
  2. Query count logging and connection pool pressure.
  3. Join cardinality and read model trade-offs.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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.

Практика

  1. Составьте transaction anomaly table для сценариев: transfer money, reserve stock, update profile.
  2. Для каждого сценария укажите: isolation level, lock/retry strategy, что логировать при rollback.
  3. Объясните, почему retry должен повторять всю транзакцию, а не только последний query.

Типичные ошибки

  1. Думают, что транзакция автоматически решает все race conditions.
  2. Держат транзакцию открытой вокруг сетевых вызовов и UI-логики.
  3. Не обрабатывают serialization/deadlock retry.
  4. Путают rollback бизнес-ошибки и технический rollback после сбоя.

Follow-up вопросы

  1. Чем Read Committed отличается от Repeatable Read?
  2. Почему Serializable требует готовности к retry?
  3. Когда нужен SELECT FOR UPDATE?
  4. Почему нельзя делать внешний HTTP call внутри долгой транзакции?

Что повторить

  1. BEGIN, COMMIT, ROLLBACK, savepoints.
  2. Isolation anomalies and retry strategy.
  3. Row locks, optimistic locking, transaction duration.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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частичный updatesaga/compensation между сервисами
Consistencybroken invariantчасть правил в приложении опасна
Isolationrace conditionslocks/retries снижают throughput
Durabilityпотеря commitfsync/replication latency
Strong consistencyсвежие readsвыше latency/coupling
Eventual consistencyavailability/scaleнужны reconciliation и UX для pending

Кейс: checkout списывает деньги во внешнем платежном провайдере и обновляет order в своей БД. Одной ACID-транзакции на оба мира нет. Хороший ответ: idempotency key, outbox/queue, статус payment_pending, retry worker, reconciliation job и понятный UX для промежуточного состояния.

Практика

  1. Составьте ACID compromise matrix для payment, notification, inventory reservation.
  2. Для каждого шага укажите: можно ли сделать локальную транзакцию, нужен ли idempotency key, что делать при retry.
  3. Опишите, какие состояния увидит пользователь при eventual consistency.

Типичные ошибки

  1. Обещают ACID там, где участвуют несколько сервисов без distributed transaction.
  2. Не проектируют idempotency для повторов после timeout.
  3. Путают database consistency и business consistency.
  4. Не делают reconciliation для eventual states.

Follow-up вопросы

  1. Какой инвариант должна защищать БД, а какой приложение?
  2. Чем saga отличается от локальной транзакции?
  3. Где нужен idempotency key?
  4. Как объяснить пользователю промежуточный статус?

Что повторить

  1. ACID and transaction boundaries.
  2. Idempotency, outbox, saga, reconciliation.
  3. Strong vs eventual consistency trade-offs.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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.

Практика

  1. Составьте pagination stability checklist: unique order, index, mutation behavior, next/prev, deleted rows.
  2. Для таблицы posts(author_id, created_at, id) предложите индекс под ленту автора.
  3. Объясните, почему ORDER BY created_at без id может давать дубли/пропуски.

Типичные ошибки

  1. Используют LIMIT без стабильного ORDER BY.
  2. Делают глубокий OFFSET на больших таблицах.
  3. Сортируют по неиндексируемому выражению в hot endpoint.
  4. Не учитывают inserts/deletes между запросами страниц.

Follow-up вопросы

  1. Почему большой OFFSET дорогой?
  2. Какой order считается стабильным?
  3. Когда offset все еще нормален?
  4. Как индекс связан с WHERE и ORDER BY?

Что повторить

  1. LIMIT, OFFSET, stable ORDER BY.
  2. Composite index for filter + sort.
  3. Cursor/keyset pagination and mutation edge cases.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 минуты)

КритерийOffsetCursor
Номера страницудобнонеудобно
Глубокая страницамедленностабильно
Новые записи между запросамидубли/пропускилучше, если cursor корректный
Простота APIвысокаянужен opaque cursor
Индексжелателенкритичен
Sort keyлюбой стабильныйдолжен быть уникально-детерминированным

Кейс: пользователь листает ленту, в это время появляются новые посты. При offset вторая страница может повторить часть первой или пропустить элементы. Хороший ответ: keyset cursor по (created_at, id), opaque cursor в API и regression тест с insert между page requests.

Практика

  1. Спроектируйте cursor для ORDER BY created_at DESC, id DESC: payload, encoding, comparison condition.
  2. Опишите тест: между первой и второй страницей добавили новый row.
  3. Назовите сценарий, где offset лучше cursor из-за UX page numbers.

Типичные ошибки

  1. Используют cursor только по id, хотя сортировка идет по created_at.
  2. Отдают cursor как доверенный user input без validation/encoding.
  3. Не добавляют tie-breaker к sort key.
  4. Обещают random page access там, где cursor API его не поддерживает.

Follow-up вопросы

  1. Что должно входить в cursor?
  2. Почему нужен tie-breaker вроде id?
  3. Как сделать backward pagination?
  4. Как cursor pagination влияет на API contract?

Что повторить

  1. Offset vs keyset/cursor pagination.
  2. Stable ordering and composite indexes.
  3. Opaque cursors, next/prev contract, mutation tests.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 Joinjoin strategymemory spill
BuffersIO/cache behaviorмного reads вместо hits
Execution Timeфактическая ценане совпадает с ожиданием

Кейс: endpoint тормозит, но индекс вроде есть. План показывает Bitmap Heap Scan, затем Sort, потому что индекс не покрывает ORDER BY. Хороший ответ: composite index под WHERE + ORDER BY, обновить statistics, проверить plan до/после и p95 в staging/production metrics.

Практика

  1. Составьте EXPLAIN plan reading checklist: scan, filter, rows estimate, loops, sort, join, buffers, execution time.
  2. Для одного query объясните, какой индекс вы бы добавили и что должно измениться в плане.
  3. Покажите, почему EXPLAIN ANALYZE для UPDATE нужно запускать внутри rollback-safe сценария.

Типичные ошибки

  1. Смотрят только total cost и игнорируют actual rows/loops.
  2. Видят Seq Scan и автоматически считают это багом даже на маленькой таблице.
  3. Запускают EXPLAIN ANALYZE на write-запросе без rollback.
  4. Не обновляют statistics и лечат неправильный plan случайными индексами.

Follow-up вопросы

  1. Чем EXPLAIN отличается от EXPLAIN ANALYZE?
  2. Почему estimated rows могут отличаться от actual rows?
  3. Когда Seq Scan нормален?
  4. Что означает большое количество loops?

Что повторить

  1. PostgreSQL EXPLAIN, EXPLAIN ANALYZE, BUFFERS.
  2. Scan types, join algorithms, sort/hash nodes.
  3. Statistics, row estimates, query-budget regression.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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-manyFK на стороне "many"nullable или обязательная связь
Many-to-many без metadatajoin table с двумя FKcomposite PK/unique pair
Many-to-many с metadataполноценная entity tableaudit fields, status, ordering
Self-referenceFK на ту же таблицуциклы, root nodes, cascade risk
Delete behaviorCASCADE, RESTRICT, SET NULLсоответствует ownership

Кейс: у пользователя есть роли в проектах. Если сделать массив roleIds внутри пользователя, сложно гарантировать целостность и искать участников проекта. Сильнее: project_members(user_id, project_id, role, joined_at) с уникальностью (project_id, user_id), FK, индексом для списка участников и отдельным индексом для проектов пользователя.

Практика

  1. Нарисуйте relationship cardinality map для users, projects, project_members, roles.
  2. Для каждой связи укажите owner, FK location, delete action и нужную уникальность.
  3. Проверьте два запроса: "все участники проекта" и "все проекты пользователя"; назовите индексы под оба направления.

Типичные ошибки

  1. Хранят many-to-many как JSON/array и теряют FK, uniqueness и удобные joins.
  2. Не задают уникальность пары в join-таблице и получают дубли связей.
  3. Ставят ON DELETE CASCADE там, где сущности независимы.
  4. Не индексируют обратное направление чтения relation table.

Follow-up вопросы

  1. Когда join-таблица должна стать отдельной domain entity?
  2. Чем RESTRICT отличается от CASCADE с точки зрения ownership?
  3. Как запретить повторное добавление одного тега к одному посту?
  4. Какой индекс нужен для чтения relation table в обратную сторону?

Что повторить

  1. Foreign keys, primary keys, composite unique constraints.
  2. ON DELETE CASCADE, RESTRICT, SET NULL.
  3. Explicit vs implicit many-to-many relations.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 copyhot list/detail readstale duplicate after update
Materialized view/read modelтяжелые агрегаты/отчетыrefresh lag и ownership
Cache вместо денормализациидорогой read без schema changeinvalidation и stampede
Event-driven projectionотдельный read modeleventual consistency

Кейс: список заказов показывает email клиента. Join с customers нормален, пока latency и нагрузка приемлемы. Если endpoint стал hot path, можно добавить orders.customer_email, но только с планом синхронизации: backfill, dual-write/update hook, consistency check и понятное поведение при изменении email.

Практика

  1. Составьте normalization/denormalization decision table для orders, customers, order_items.
  2. Для каждого дубля укажите source of truth, кто обновляет копию, как найти stale rows.
  3. Опишите rollback: как вернуться к normalized read, если денормализация дала рассинхрон.

Типичные ошибки

  1. Денормализуют "на всякий случай" без конкретного read bottleneck.
  2. Не назначают source of truth и получают конфликтующие значения.
  3. Забывают backfill и consistency check для существующих данных.
  4. Путают cache, read model и permanent denormalized column.

Follow-up вопросы

  1. Какие anomalies убирает нормализация?
  2. Как вы докажете, что денормализация нужна?
  3. Кто владеет обновлением денормализованного поля?
  4. Что увидит пользователь, если read model отстает?

Что повторить

  1. Normal forms, update/insert/delete anomalies.
  2. Materialized views, read models, cache invalidation.
  3. Backfill, dual-write, consistency verification.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 и показывать разницу пользователю.

Практика

  1. Сделайте optimistic-locking conflict drill: T1 и T2 читают version = 7, T1 сохраняет, T2 пытается сохранить.
  2. Запишите ожидаемый SQL, affected rows и ответ API/сервиса на конфликт.
  3. Назовите, где можно auto-retry, а где нужен ручной merge пользователя.

Типичные ошибки

  1. Читают version, но не включают его в WHERE update-запроса.
  2. Автоматически retry-ят пользовательские изменения и теряют смысл conflict.
  3. Используют timestamp с низкой точностью как единственную защиту.
  4. Не проверяют affected rows после update.

Follow-up вопросы

  1. Чем optimistic locking отличается от SELECT FOR UPDATE?
  2. Почему conflict - это не обязательно ошибка сервера?
  3. Когда retry безопасен, а когда опасен?
  4. Как optimistic locking связан с lost update?

Что повторить

  1. Lost update, version columns, affected rows.
  2. Pessimistic locks vs optimistic locking.
  3. Conflict handling in API and UI flows.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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переносим данные batcheslock 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, метрика расхождений, переключение чтения и отложенное удаление.

Практика

  1. Составьте expand/contract migration checklist для переименования колонки на большой таблице.
  2. Отметьте, какие шаги rollback-safe, а какие destructive.
  3. Назовите проверки: row count, null count, mismatch sample, lock duration, failed batches.

Типичные ошибки

  1. Делают destructive change в том же релизе, где меняют код.
  2. Не учитывают старые процессы во время rolling deploy.
  3. Запускают большой backfill одной транзакцией.
  4. Добавляют constraint/index на большую таблицу без плана lock/concurrent/validation.

Follow-up вопросы

  1. Что такое expand/contract migration?
  2. Почему drop/rename колонки опасны при rolling deploy?
  3. Как добавлять constraint на большую таблицу безопаснее?
  4. Какие метрики покажут, что backfill идет безопасно?

Что повторить

  1. Expand/contract, dual write, read fallback.
  2. Batched backfill and rollback plan.
  3. CREATE INDEX CONCURRENTLY, NOT VALID, VALIDATE CONSTRAINT.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 constraintinvariant в таблицеNULL может вести себя не как ожидаете
Partial unique indexuniqueness для subset rowssoft delete / active status
Idempotency keyповтор командытот же key с другим payload
Request hashmisuse key detectionнельзя молча вернуть старый результат
Stored result/statusстабильный retry responseчто делать с processing/failed

Кейс: клиент создает платеж, получает network timeout и повторяет запрос. Без idempotency можно создать два платежа. Хороший ответ: клиент присылает key, сервер в транзакции создает запись с unique key, сохраняет fingerprint запроса и итог операции. Повтор с тем же key и тем же payload получает сохраненный результат; повтор с другим payload - conflict/misuse error.

Практика

  1. Составьте idempotency/unique-constraint retry matrix: first request, same retry, retry with changed payload, concurrent retry, expired key.
  2. Для invites(project_id, email, status) предложите unique rule для одного active invite.
  3. Объясните, что возвращать, если idempotency key уже есть, но операция еще processing.

Типичные ошибки

  1. Проверяют "нет ли дубля" SELECT-ом перед INSERT без unique constraint.
  2. Не сравнивают payload при повторном idempotency key.
  3. Хранят idempotency key без TTL/retention policy.
  4. Не продумывают concurrent retry, когда первая операция еще не завершилась.

Follow-up вопросы

  1. Почему SELECT-before-INSERT не защищает от гонки?
  2. Чем idempotency отличается от retry?
  3. Как обработать тот же key с другим request body?
  4. Когда нужен partial unique index?

Что повторить

  1. Unique constraints, partial unique indexes, NULLS NOT DISTINCT.
  2. INSERT ... ON CONFLICT, request fingerprint, stored response.
  3. Retry semantics, TTL, concurrent idempotent requests.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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-asideread-heavy данныеmiss latency, stale до TTL
Write-throughнужен свежий кэш после writewrite latency выше
Delete-on-writeпростой invalidaterace: старое значение может вернуться
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 для критичных операций.

Практика

  1. Составьте cache invalidation timeline: read miss, cache fill, update DB, invalidate, concurrent read, retry.
  2. Для product, user_permissions, feed_page выберите pattern, TTL и допустимую stale window.
  3. Опишите, как поймать cache stampede в метриках.

Типичные ошибки

  1. Кэшируют права доступа или деньги без строгого правила свежести.
  2. Делают update DB и cache в разном порядке без учета race.
  3. Не ставят TTL и не имеют ручного invalidate/debug path.
  4. Не защищают hot key от stampede после истечения TTL.

Follow-up вопросы

  1. Чем cache-aside отличается от write-through?
  2. Что такое stale window?
  3. Как избежать cache stampede?
  4. Какие данные вы не стали бы кэшировать?

Что повторить

  1. Cache-aside, write-through, TTL, invalidation.
  2. Stampede protection, negative cache, versioned keys.
  3. Consistency requirements for permissions, payments, catalogs.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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-writeprimary или sticky primaryreplica может отставать
Каталог/лентаreplica при acceptable lagstale допустим
Permission checkprimarystale права опасны
Отчет/analyticsreplica/warehouseразгрузка primary
Long queryreplica 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.

Практика

  1. Составьте replica lag routing matrix для profile, catalog, permissions, admin report.
  2. Для каждого read укажите: stale допустим или нет, max lag, fallback.
  3. Опишите тест: write на primary, read сразу после write, replica lag simulated.

Типичные ошибки

  1. Отправляют все SELECT на replica без классификации freshness.
  2. Не имеют sticky read после write.
  3. Не мониторят lag и canceled standby queries.
  4. Запускают долгие отчеты на HA-replica и мешают recovery.

Follow-up вопросы

  1. Почему read replica eventually consistent?
  2. Что такое read-after-write consistency?
  3. Когда нужно fallback на primary?
  4. Какие запросы опасно отправлять на replica?

Что повторить

  1. Hot standby, replication lag, read-only restrictions.
  2. Sticky reads, consistency levels, lag thresholds.
  3. Primary vs replica routing and failover behavior.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 connectionssaturation приложенияconnection leak, burst traffic
pg_stat_activity waitsтекущие блокировки/IOlock contention, WAL sync
pg_stat_statements total timeсамые дорогие SQLhot query или N+1
Replication lagсвежесть replicaheavy write/WAL replay delay
Temp files/sortsmemory/work_mem pressureсортировка/агрегация на disk
Dead tuples/vacuumbloat pressureнеуспевающий vacuum

Кейс: после релиза API стал медленнее, но CPU БД нормальный. План расследования: сравнить top SQL до/после, посмотреть pool wait, waits по locks, EXPLAIN ANALYZE для изменившегося запроса и rollback/feature flag, если деградация связана с новым query shape.

Практика

  1. Составьте DB degradation signal map: symptom, metric, likely cause, first action.
  2. Для "p95 API вырос в 3 раза" назовите первые 5 DB-проверок.
  3. Опишите alert, который отличает slow query от connection pool saturation.

Типичные ошибки

  1. Смотрят только average latency и пропускают p95/p99.
  2. Не различают query execution time и pool wait time.
  3. Не хранят baseline top queries до релиза.
  4. Лечат любую деградацию индексом без проверки waits/locks.

Follow-up вопросы

  1. Чем slow query отличается от pool saturation?
  2. Что покажет pg_stat_activity?
  3. Зачем нужен pg_stat_statements?
  4. Какие метрики нужны для read replica?

Что повторить

  1. pg_stat_activity, wait events, locks.
  2. pg_stat_statements, top queries, baselines.
  3. Pool metrics, replication lag, vacuum/bloat basics.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 минуты)

СитуацияORMRaw 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 оформить параметризованно и покрыть тестом.

Практика

  1. Составьте ORM/raw SQL decision drill для CRUD, report, bulk update, lock-sensitive workflow.
  2. Для одного ORM include запишите ожидаемый SQL и риск N+1/overfetch.
  3. Перепишите динамический фильтр так, чтобы user input не попадал в строковую конкатенацию.

Типичные ошибки

  1. Не смотрят generated SQL и считают ORM "магией".
  2. Пишут raw SQL через string concatenation с user input.
  3. Тянут большие наборы в память вместо aggregation в БД.
  4. Не тестируют transaction/locking behavior, потому что ORM скрыл детали.

Follow-up вопросы

  1. Когда raw SQL оправдан?
  2. Как безопасно параметризовать raw query?
  3. Как ORM может создать N+1?
  4. Что вы проверите в плане после замены ORM-запроса?

Что повторить

  1. Generated SQL, query logging, EXPLAIN ANALYZE.
  2. Parameterized raw queries and SQL injection risks.
  3. ORM relations, batching, transactions, bulk operations.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных

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 должны быть колонками, потому что по ним фильтруют, сортируют, строят индексы и проверяют инварианты.

Практика

  1. Составьте JSON-in-SQL modeling checklist для products.attributes.
  2. Разделите поля на core columns, indexed JSON expressions, raw payload only.
  3. Для трех запросов укажите, нужен ли GIN, expression index или нормализация.

Типичные ошибки

  1. Кладут весь domain object в JSON и теряют constraints.
  2. Фильтруют по JSON-полю без подходящего индекса.
  3. Не различают json и jsonb.
  4. Обновляют большой JSON-документ ради маленького поля в hot write path.

Follow-up вопросы

  1. Чем json отличается от jsonb?
  2. Когда JSONB лучше нормализованной таблицы?
  3. Что нельзя надежно выразить внутри JSON без внешних проверок?
  4. Как выбрать GIN vs expression index?

Что повторить

  1. PostgreSQL json vs jsonb, containment, existence.
  2. GIN indexes, jsonb_ops, jsonb_path_ops, expression indexes.
  3. Constraints, row-level locks, normalization boundary.

Куда дальше

  1. Вернитесь в модуль: Базы данных.
  2. Сверьтесь с картой темы: Базы данных.
  3. Закрепите SQL-навык в SQL-песочнице: начните с Активные клиенты, затем переходите к join, aggregation и cursor задачам из bridge выше.
  4. Продолжайте по маршруту: Senior трек.

Связанные модули и карта

  1. Обучение: Базы данных
  2. Обучение: Node.js
  3. Карта подготовки: Базы данных