Какие SQL-проверки QA использует в первую очередь?
Junior
Основные SQL-проверки: SELECT для сверки данных из API/UI с тем, что реально сохранилось в БД; COUNT(*) для проверки количества записей; сравнение NULL-полей и граничных значений.
Дополнительно использую JOIN для проверки связей между таблицами, GROUP BY для агрегатов, и прямые UPDATE/DELETE для подготовки тестовых данных.
Из практики: первым делом иду в базу после любого POST/PUT-запроса — это быстрее всего показывает, что реально сохранилось и нет ли расхождения с ответом API.
Частые ошибки
Тесты на боевой БД.
Нет фиксации схем/окружения.
Дополнительные вопросы
Как валидируете миграции?
Чем опасны SELECT *?
Что такое СУБД?
Junior
СУБД — программное обеспечение, которое управляет данными: создание структур, хранение, выборка, обновление, удаление, контроль доступа и обеспечение целостности.
Основные функции: выполнение SQL-запросов, транзакционность (ACID), контроль целостности через ограничения, управление параллельным доступом, резервное копирование.
Из практики: для QA важно понимать СУБД не только как хранилище, но и как слой валидации — ограничения на уровне схемы ловят дефекты, которые прошли через приложение.
Частые ошибки
Путают СУБД и саму базу данных.
Имеют в виду под СУБД только реляционные системы, забывая о NoSQL и других типах.
Дополнительные вопросы
Примеры популярных СУБД и их типы (реляционные, NoSQL)?
Какие задачи решает СУБД помимо хранения данных?
В чём разница между SQL и NoSQL?
Junior
SQL-БД — реляционные, хранят данные в таблицах с фиксированной схемой, поддерживают ACID-транзакции, используют SQL. Примеры: PostgreSQL, MySQL, Oracle.
NoSQL — нереляционные хранилища без жёсткой схемы, масштабируются горизонтально, но часто жертвуют частью ACID. Типы: документные (MongoDB), ключ-значение (Redis), колоночные (Cassandra), графовые.
Из практики: выбор между SQL и NoSQL зависит от структуры данных и требований к консистентности. В тестировании важно знать, какой тип БД используется, — это определяет, как и где проверять данные.
Частые ошибки
Сводят разницу к «SQL – это язык, NoSQL – нет SQL», хотя речь про типы хранилищ.
Считают, что NoSQL всегда лучше для больших данных, не учитывая сложности согласованности и моделирования данных.
Дополнительные вопросы
Примеры NoSQL баз (MongoDB, Redis, Neo4j) и их модели данных?
Когда реляционная БД предпочтительнее NoSQL, несмотря на масштабируемость?
Что такое CRUD в SQL?
Junior
CRUD — четыре базовые операции с данными: Create, Read, Update, Delete.
В SQL им соответствуют операторы INSERT (создать), SELECT (прочитать), UPDATE (обновить) и DELETE (удалить).
Для тестирования важно понимать, какая операция выполняется на каждом шаге сценария и как проверить результат запросом к базе.
Из практики: такие проверки быстро показывают, где данные теряются или записываются не туда.
Частые ошибки
Ограничивают SQL только этими операциями, забывая про другие возможности (DDL, DCL и т.д.).
Путают SELECT с DML операцией изменения – SELECT только чтение, не изменяет данные.
Дополнительные вопросы
Что такое DML и как связан с CRUD?
Как обеспечивается атомарность CRUD-операций (роль транзакций)?
Что такое DDL?
Junior
DDL — это команды для структуры базы: создать таблицу, изменить схему, удалить объект.
Типичные команды: CREATE, ALTER, DROP, TRUNCATE.
Из практики: перед релизом всегда проверяю DDL-изменения на копии боевых данных, чтобы не словить неожиданные блокировки.
Частые ошибки
Смешивают DDL и DML: например, считают CREATE TABLE частью манипулирования данными (на самом деле это определение структуры).
Не учитывают, что DDL-операции зачастую auto-commit (сразу фиксируются вне транзакции).
Дополнительные вопросы
Что такое DML и DCL в сравнении с DDL?
Может ли DDL команда быть откатана транзакцией в разных СУБД?
Что такое DML?
Junior
DML — это команды для работы с данными внутри таблиц: добавить, обновить, удалить и выбрать записи.
Обычно сюда относят INSERT, UPDATE, DELETE и SELECT.
Из практики: при проверке DML-сценариев я всегда сверяю итоговое состояние данных в базе, а не только ответ интерфейса.
Частые ошибки
Забывают про SELECT, говоря о DML только как о модификации: хотя SELECT не изменяет, он тоже оперирует данными (DQL – подкатегория DML).
Не соблюдают транзакционность DML: например, выполняют UPDATE/DELETE без WHERE, затрагивая лишние записи.
Дополнительные вопросы
В чём разница между DELETE и TRUNCATE (DDL или DML)?
Что такое транзакция и как DML с ней взаимодействует?
Что такое агрегирующие функции, группировка, сортировка, джоины?
Junior
Агрегирующие функции вычисляют сводное значение по набору строк. Часто применяются с группировкой (GROUP BY) для получения агрегатов по категориям.
Сортировка осуществляется через ORDER BY. Джоины (JOIN) позволяют объединять строки из нескольких таблиц по связанным полям.
Из практики: ошибки чаще всего возникают в неверной группировке и условиях JOIN, поэтому их проверяю в первую очередь.
Частые ошибки
Забывают про условие HAVING при агрегатах.
Путают виды JOIN.
Дополнительные вопросы
Что делает LEFT JOIN в отличие от INNER JOIN?
Как использовать индекс при JOIN для ускорения запроса?
В чём разница между UNION и JOIN?
Junior
UNION объединяет результаты двух запросов вертикально — складывает строки из двух выборок друг за другом. Столбцы должны совпадать по количеству и типам. UNION ALL сохраняет дубликаты, UNION — удаляет.
JOIN соединяет таблицы горизонтально, объединяя столбцы на основе связанных ключей: результат — одна широкая таблица с полями из обеих.
Из практики: UNION использую чтобы объединить продажи двух регионов в один отчёт (одинаковая структура данных); JOIN — чтобы добавить к заказу имя клиента из другой таблицы (связь через ключ).
Частые ошибки
Путают понятия: пытаются заменить JOIN на UNION или наоборот, не понимая разницы в структуре результата.
Забывают про требование соответствия столбцов при UNION (ошибка при разном количестве/типах столбцов).
Дополнительные вопросы
В каких случаях удобнее использовать UNION, а в каких JOIN?
Чем отличается UNION от подзапроса с SELECT.. FROM X UNION SELECT..?
Как работать с с тестовыми данными в базах данных?
Middle
Для стабильной работы с тестовыми данными в БД применяю несколько подходов: subsetting (выгружаю репрезентативную часть продовых данных), синтетическую генерацию через faker, и управление состоянием через транзакции с откатом.
Перед тестом создаю нужное состояние через SQL-скрипты или API, после теста откатываю изменения — это сохраняет изоляцию между тестами.
Из практики: самая частая проблема — тесты делят общие данные и мешают друг другу. Решается уникальными ключами и teardown после каждого теста.
Частые ошибки
Shared state между тестами.
Медленные операции с данными.
Неполная очистка после тестов.
Дополнительные вопросы
Как обеспечиваете консистентность данных?
Стратегии оптимизации DB операций?
Как проверять миграции базы данных перед релизом?
Middle
Проверяю, что миграция применяется на чистой базе и на базе с реальными данными без потери информации. Важно проверить оба сценария — они дают разные ошибки.
Смотрю время выполнения, блокировки таблиц и обратимость изменений — есть ли возможность rollback если что-то пойдёт не так.
Из практики: проверяю миграцию на копии продовых данных — миграция, которая работает на пустой базе, может висеть часами на миллионах строк из-за блокировок. Это критично для zero-downtime деплоя.
Частые ошибки
Проверяют миграцию только на пустой базе.
Дополнительные вопросы
Какие проверки обязательны для отката миграции?
Как тестировать транзакции в критичных операциях?
Middle
Проверяю, что при ошибке внутри транзакции все изменения откатываются полностью — нет частично записанных данных.
Отдельно тестирую конкурентные операции с одними данными: две параллельные транзакции должны работать согласно ожидаемому уровню изоляции без неожиданных блокировок или phantom read.
Из практики: имитирую сбой в середине операции — отключаю БД или прерываю запрос. Частичные записи после сбоя — типичный дефект, который сложно поймать иначе. Проверяю через прямые SQL-запросы к таблицам.
Частые ошибки
Проверяют только одиночный успешный сценарий.
Дополнительные вопросы
Как проверишь атомарность перевода средств?
Как организовать тестовые данные для стабильных проверок?
Middle
Разделяю фиксированные наборы данных (справочники, роли) и данные, создаваемые в ходе теста. Динамические данные делаю воспроизводимыми: понятные фабрики, очистка после теста, уникальные ключи (UUID).
Статические тестовые данные версионирую в git рядом с тестами — изменение ожидаемого ответа сразу видно в diff.
Из практики: общие тестовые данные без изоляции — главная причина флаки при параллельном запуске. Каждый тест создаёт свои данные через API с уникальным UUID в ключевых полях.
Частые ошибки
Используют общие данные без очистки между прогонами.
Дополнительные вопросы
Как избежишь конфликтов данных при параллельном запуске?
Что проверять в ограничениях целостности базы данных?
Junior
Проверяю первичные и внешние ключи, уникальные индексы, NOT NULL ограничения, CHECK-ограничения на диапазоны и enum-значения.
Смотрю, что некорректные записи не попадают в таблицы даже при прямой записи в БД в обход приложения — ограничения должны работать на уровне БД, а не только в коде.
Из практики: ограничения на уровне БД — последний рубеж обороны против грязных данных. Проверяю их через прямые INSERT/UPDATE с нарушением ограничений — каждое нарушение должно давать ошибку БД.
Частые ошибки
Считают, что проверки интерфейса полностью заменяют ограничения базы.
Дополнительные вопросы
Какие ограничения особенно важны для таблицы заказов?
Как проверять корректность отчётных данных?
Middle
Сверяю расчёты отчёта с первичными данными напрямую через SQL-запросы к источнику — не доверяю только визуальному сравнению.
Проверяю граничные значения в формулах, фильтры по периоду, часовой пояс, округление чисел и поведение при отсутствующих данных (NULL vs 0).
Из практики: расхождения в отчётах чаще всего связаны с неверным часовым поясом или разной трактовкой NULL в агрегирующих функциях — эти случаи проверяю первыми.
Частые ошибки
Проверяют только внешний вид отчёта.
Дополнительные вопросы
Какие проверки сделаешь для отчёта по выручке?
Как проверять производительность запросов базы данных?
Middle
Смотрю план выполнения запроса через EXPLAIN ANALYZE — проверяю, что используется Index Scan там, где ожидается, нет Seq Scan по большим таблицам и нет избыточных Sort-операций.
Проверяю время ответа на типичных и больших объёмах данных и слежу, что изменения в запросе не ухудшают его на реалистичных данных.
Из практики: EXPLAIN ANALYZE показывает реальный план исполнения. Seq Scan по таблице с миллионами строк, где должен быть Index Scan — первый сигнал пропущенного индекса.
Частые ошибки
Оценивают скорость только на маленьком наборе данных.
Дополнительные вопросы
Какие признаки в плане выполнения запроса считаешь тревожными?
Как тестировать восстановление данных из резервной копии?
Middle
Проверяю сам процесс восстановления: восстановление проходит без ошибок, занимает приемлемое время согласно RTO, данные целостны после восстановления.
После восстановления сверяю количество записей, ключевые связи, последние транзакции — система должна быть работоспособна, а не просто запускаться.
Из практики: резервное копирование без проверки восстановления — это иллюзия безопасности. Регулярно тестирую полный цикл: backup → restore → проверка работоспособности.
Частые ошибки
Ограничиваются проверкой факта создания копии.
Дополнительные вопросы
Какие метрики восстановления считаешь ключевыми?
Какие виды JOIN есть в SQL и чем они отличаются?
Junior
INNER JOIN возвращает только строки, у которых есть совпадение в обеих таблицах. LEFT JOIN — все строки левой таблицы плюс совпадения справа (или NULL, если справа нет). RIGHT JOIN — симметрично, все справа. FULL JOIN — объединение LEFT и RIGHT: все строки с обеих сторон, NULL там, где нет пары.
CROSS JOIN — декартово произведение, каждая строка слева с каждой справа. Часто нужен для генерации комбинаций.
Из практики: 80% задач решается INNER и LEFT. RIGHT почти не встречается (тот же LEFT с переставленными таблицами). FULL — для сверок и поиска расхождений между двумя списками.
Частые ошибки
Считать, что LEFT JOIN не выкидывает строки — выкинет, если потом в WHERE отфильтровать по столбцу из правой таблицы без IS NULL.
Забыть условие в ON и получить декартово произведение.
Дополнительные вопросы
Как найти строки из A, которых нет в B?
Чем USING отличается от ON?
В чём разница между WHERE и HAVING?
Junior
WHERE фильтрует строки до группировки — работает с отдельными строками. HAVING фильтрует результаты после GROUP BY — работает с агрегатами.
Например, WHERE status = 'paid' уберёт неоплаченные заказы. HAVING SUM(amount) > 1000 оставит только тех клиентов, у кого сумма заказов больше 1000.
Из практики: если условию не нужен агрегат — пишите его в WHERE, оптимизатор быстрее обработает. HAVING — только для условий по агрегатам.
Частые ошибки
Писать HAVING там, где можно WHERE — теряется производительность.
Ссылаться на алиас столбца в WHERE — в большинстве СУБД нельзя (он ещё не вычислен).
Дополнительные вопросы
Можно ли использовать оба в одном запросе?
Можно ли в HAVING писать условие без агрегата?
Как работает GROUP BY и когда его применять?
Junior
GROUP BY объединяет строки с одинаковыми значениями в указанных столбцах в одну группу. К каждой группе можно применить агрегатные функции: COUNT, SUM, AVG, MIN, MAX.
В SELECT можно класть только либо столбцы из GROUP BY, либо агрегаты. Иначе СУБД не знает, какое из значений показать.
Из практики: применяют для отчётов «сколько заказов на пользователя», «сумма по дням», «среднее по категории». GROUP BY несколькими столбцами создаёт группы по их комбинации.
Частые ошибки
Положить в SELECT столбец, которого нет в GROUP BY и не агрегированного — ошибка (в Postgres) или непредсказуемое значение (в MySQL в режиме ONLY_FULL_GROUP_BY=OFF).
Считать, что GROUP BY сортирует — порядок не гарантирован, нужен ORDER BY.
Дополнительные вопросы
Чем отличается GROUP BY от DISTINCT?
Что такое GROUP BY ROLLUP?
Что такое индекс и как он ускоряет запрос?
Junior
Индекс — это отдельная структура данных (чаще всего B-tree), которая хранит значения столбца и ссылки на строки таблицы. По нему СУБД находит нужные строки за логарифмическое время вместо полного сканирования.
Самые частые сценарии, где помогает: WHERE col = X, JOIN по столбцу, ORDER BY, поиск диапазона, проверка UNIQUE.
Из практики: индекс ускоряет чтение, но замедляет вставку и обновление, потому что его нужно поддерживать. На большие таблицы индексы строят осторожно и только под конкретные запросы.
Частые ошибки
Индекс на каждое поле «на всякий случай» — деградация INSERT/UPDATE.
Запрос с WHERE по функции от столбца (LOWER(name) = ...) не использует обычный индекс — нужен функциональный.
Дополнительные вопросы
Что такое составной индекс?
Почему индекс на boolean-столбец обычно бесполезен?
Что показывает EXPLAIN и как им пользоваться?
Middle
EXPLAIN показывает план запроса: какие операции и в каком порядке СУБД собирается выполнить, какие индексы использовать, как соединять таблицы.
EXPLAIN ANALYZE реально выполняет запрос и показывает фактическое время каждой операции — это основной инструмент диагностики медленных запросов. В Postgres и в MySQL (с 8.0.18) это EXPLAIN ANALYZE; при этом MySQL всегда выводит древовидный TREE-формат, а FORMAT=JSON доступен только для обычного (оценочного) EXPLAIN, но не для EXPLAIN ANALYZE.
Из практики: ищут Seq Scan (полное сканирование) на больших таблицах, неиспользованные индексы, неожиданные Nested Loop вместо Hash Join, а также сильное расхождение estimated и actual rows — это указывает на устаревшую статистику.
Частые ошибки
EXPLAIN без ANALYZE показывает только план, без реального времени.
Запускать EXPLAIN ANALYZE на UPDATE/DELETE — он реально изменит данные. Оборачивайте в транзакцию с ROLLBACK.
Дополнительные вопросы
Что значит Bitmap Heap Scan в плане Postgres?
Как обновить статистику таблицы?
Что такое транзакция и какие у неё свойства (ACID)?
Middle
Транзакция — это группа операций, которые выполняются как единое целое: либо все, либо ни одной. В коде окружают командами BEGIN и COMMIT (или ROLLBACK для отмены).
ACID: Atomicity (атомарность — всё или ничего), Consistency (согласованность — переход из одного валидного состояния в другое), Isolation (изоляция — параллельные транзакции не мешают друг другу), Durability (долговечность — после COMMIT данные не теряются даже при сбое).
Из практики: ACID важен для финансовых операций и любых ситуаций, где нельзя оставить данные в полусохранённом состоянии (списали со счёта, не зачислили на другой).
Частые ошибки
Считать, что любая операция всегда атомарна — она не атомарна без транзакции при нескольких изменениях.
Делать в транзакции долгие сетевые вызовы — блокировки и таймауты.
Дополнительные вопросы
Что такое уровни изоляции?
Чем отличается ROLLBACK от SAVEPOINT?
Какие уровни изоляции транзакций есть в SQL?
Middle
По стандарту четыре: READ UNCOMMITTED (можно читать незакоммиченные данные — dirty read), READ COMMITTED (только подтверждённые), REPEATABLE READ (повторное чтение даст те же строки), SERIALIZABLE (как если бы транзакции выполнялись по очереди).
Феномены: dirty read, non-repeatable read (повторное чтение даст другое значение), phantom read (повторный запрос вернёт новые строки). С повышением уровня феноменов меньше, но и параллелизм ниже.
Из практики: в Postgres по умолчанию READ COMMITTED, в MySQL InnoDB — REPEATABLE READ. SERIALIZABLE используют только когда без него нельзя обеспечить корректность.
Частые ошибки
Полагаться на REPEATABLE READ как на полную защиту от phantom — в стандарте phantom возможен, в Postgres практически нет.
Использовать SERIALIZABLE без понимания — растёт частота serialization failure, нужно ретраить.
Дополнительные вопросы
Что такое serialization failure и как с ним работать?
Чем MVCC отличается от блокировочного подхода?
Чем подзапрос отличается от JOIN, и когда что выбирать?
Middle
JOIN соединяет таблицы на одном уровне, возвращая комбинированные строки. Подзапрос — это вложенный SELECT, который возвращает значение, набор значений или таблицу, используемые в основном запросе.
В большинстве случаев подзапрос можно переписать через JOIN. Оптимизатор современных СУБД часто сам переписывает один в другой. Читаемость зависит от ситуации: для «отфильтровать пользователей, у которых есть оплаченный заказ» — EXISTS читается лучше, чем JOIN + DISTINCT.
Из практики: коррелированные подзапросы (зависят от внешней строки) на больших данных могут быть медленными — JOIN надёжнее. Скалярный подзапрос в SELECT — удобно, но осторожно с производительностью.
Частые ошибки
Коррелированный подзапрос в SELECT для каждой строки внешней таблицы — N+1 на уровне SQL.
Использовать IN (SELECT ...) c большим набором — может работать медленнее, чем JOIN или EXISTS.
Дополнительные вопросы
Когда EXISTS быстрее IN?
Что такое скалярный подзапрос?
Что такое оконные функции (window functions) и для чего они нужны?
Middle
Оконная функция вычисляет значение по «окну» строк, связанных с текущей, не сворачивая их в одну. Например, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) даст номер записи в группе.
Самые частые: ROW_NUMBER (последовательный номер), RANK и DENSE_RANK (ранги с одинаковыми/без пропусков), LAG/LEAD (значение из предыдущей/следующей строки), SUM/AVG OVER (накопительные итоги).
Из практики: оконные функции заменяют сложные джойны и self-join. Типовая задача — взять последнюю запись по группе, найти разницу с предыдущим значением, посчитать накопительную сумму.
Частые ошибки
Путать PARTITION BY с GROUP BY — оконная функция не схлопывает строки.
Забывать ORDER BY внутри OVER — для RANK и накопительных итогов он критичен.
Дополнительные вопросы
Чем RANK отличается от DENSE_RANK?
Как получить последнюю запись для каждого пользователя?
Что такое CTE (WITH) и когда его применять?
Middle
CTE (Common Table Expression) — это именованный подзапрос, который объявляется в начале запроса через WITH name AS (SELECT ...) и используется дальше как таблица.
Помогает разбить сложный запрос на читаемые шаги. Бывают рекурсивные CTE — они позволяют обходить иерархии (дерево категорий, граф) и генерировать последовательности.
Из практики: для отчётов с 4-5 уровнями вложенности CTE радикально улучшает читаемость. В Postgres до 12 версии CTE были оптимизационным барьером (материализовались), сейчас инлайнятся, если без RECURSIVE.
Частые ошибки
Использовать CTE везде — для простых запросов это лишний шум.
Полагаться, что CTE материализуется (быстрая «таблица») в свежем Postgres — это уже не так по умолчанию.
Дополнительные вопросы
Как с помощью рекурсивного CTE построить дерево?
Чем CTE отличается от подзапроса в FROM?
Чем UNION отличается от UNION ALL?
Junior
UNION объединяет результаты двух запросов и убирает дубликаты строк (вызывает сортировку или хеширование для дедупликации). UNION ALL объединяет без дедупликации.
UNION ALL быстрее, потому что не делает лишнюю работу. UNION используют, только когда дубликаты действительно нужно убрать.
Из практики: если вы точно знаете, что результаты не пересекаются — всегда UNION ALL. Это типичный анти-паттерн начинающих — везде ставить UNION «на всякий случай».
Частые ошибки
Использовать UNION по привычке и терять время на дедупликации.
Объединять запросы с разным числом или типом столбцов — будет ошибка.
Дополнительные вопросы
Можно ли в UNION использовать разный порядок столбцов?
Чем INTERSECT отличается от UNION?
Что такое EXISTS и когда он лучше IN?
Middle
EXISTS проверяет, существует ли хотя бы одна строка во вложенном подзапросе для текущей строки внешней таблицы. Возвращает true/false и прекращает поиск на первой найденной.
IN сравнивает значение со списком из подзапроса. Если список большой — может быть медленнее. На NULL значения IN ведёт себя неожиданно: NOT IN с NULL в списке вернёт пусто.
Из практики: для проверки «есть ли связанная запись» — EXISTS предпочтительнее. Для NOT IN с подзапросом — почти всегда NOT EXISTS безопаснее из-за NULL-проблем.
Частые ошибки
Использовать NOT IN с подзапросом, который может вернуть NULL — внешняя строка пропадёт.
Считать, что IN всегда быстрее EXISTS — зависит от данных и оптимизатора.
Дополнительные вопросы
Что такое антиджойн?
Как переписать NOT IN на NOT EXISTS?
Что такое нормализация и зачем нужны 1НФ, 2НФ, 3НФ?
Middle
Нормализация — это процесс разбиения данных на таблицы так, чтобы избежать дублирования и аномалий обновления.
1НФ: значения в ячейках атомарны (никаких списков в одном поле). 2НФ: 1НФ плюс все неключевые столбцы зависят от всего первичного ключа, а не от его части (актуально для составного ключа). 3НФ: 2НФ плюс нет транзитивных зависимостей (неключевые столбцы зависят только от ключа, а не друг от друга).
Из практики: 3НФ — обычный уровень для нормальных бизнес-данных. Иногда сознательно денормализуют для скорости чтения (отчёты, аналитика).
Частые ошибки
Хранить в поле через запятую и потом мучиться с SPLIT.
Денормализовать без причины — потом сложно синхронизировать данные.
Дополнительные вопросы
Когда денормализация оправдана?
Что такое BCNF?
Что такое внешний ключ и какие у него действия ON DELETE?
Junior
Внешний ключ (FOREIGN KEY) — это ссылка одного столбца (или нескольких) на первичный ключ другой таблицы. Он гарантирует, что в зависимой таблице не появится значение, которого нет в родительской.
Действия при удалении/обновлении родителя: CASCADE (удалить/обновить и потомков), SET NULL (выставить NULL у потомков), SET DEFAULT (значение по умолчанию), RESTRICT/NO ACTION (запретить операцию, если есть потомки).
Из практики: CASCADE удобен для тесно связанных данных (комментарии при удалении поста). Для важных бизнес-сущностей чаще выбирают RESTRICT — заставить разработчика подумать.
Частые ошибки
CASCADE на родительской таблице с миллионами потомков — длинная блокировка.
Забыть индекс на столбце внешнего ключа — DELETE родителя проверяет потомков полным сканом.
Дополнительные вопросы
Почему нужен индекс на FOREIGN KEY столбец?
Что такое отложенная проверка constraint?
Чем VIEW отличается от MATERIALIZED VIEW?
Middle
VIEW — это сохранённый запрос. При обращении к view СУБД каждый раз выполняет внутренний SELECT. Данные всегда свежие, но работа повторяется.
MATERIALIZED VIEW — это сохранённый результат запроса. Данные физически лежат на диске и обновляются по команде REFRESH (или по расписанию). Чтение быстрое, но данные не свежие.
Из практики: VIEW — для удобной абстракции и инкапсуляции логики. MATERIALIZED VIEW — для тяжёлых аналитических запросов, где допустима задержка обновления.
Частые ошибки
Считать VIEW кешем — там нет кеша, каждый раз SELECT.
Забывать REFRESH MATERIALIZED VIEW и видеть устаревшие данные.
Дополнительные вопросы
Можно ли обновить таблицу через VIEW?
Что такое REFRESH MATERIALIZED VIEW CONCURRENTLY?
Что такое UPSERT и как его сделать в PostgreSQL?
Middle
UPSERT — операция «вставить или обновить»: если строки нет — INSERT, если есть (по ключу) — UPDATE.
В PostgreSQL это INSERT ... ON CONFLICT (column) DO UPDATE SET col = EXCLUDED.col. Псевдотаблица EXCLUDED содержит значения, которые пытались вставить.
Из практики: незаменим для синхронизации с внешними источниками и для idempotent-операций, чтобы при повторе скрипта не было дубликатов и не нужно было заранее проверять существование.
Частые ошибки
Использовать ON CONFLICT без unique constraint — нечего отслеживать.
Думать, что ON CONFLICT DO NOTHING всегда быстрее — UPDATE может всё-таки запускать триггеры.
Дополнительные вопросы
Чем INSERT ... ON CONFLICT отличается от MERGE?
Можно ли в одной транзакции делать UPSERT нескольких строк?
Почему в продакшен-коде не пишут SELECT *?
Junior
SELECT * возвращает все столбцы, что увеличивает сетевой трафик и память, особенно если в таблице есть тяжёлые поля (TEXT, BLOB).
Зависимость от *: при добавлении столбца в таблицу клиент начинает получать его без изменения кода — это может сломать сериализацию или утечь чувствительные поля.
Из практики: всегда явно перечисляйте нужные столбцы. Это документирует контракт и защищает от случайных регрессий, когда модель меняется.
Частые ошибки
Использовать SELECT * в коде, который потом маппится в строгую модель — поломки при изменении схемы.
Делать SELECT * в JOIN — могут попасть одинаковые имена столбцов, путаница.
Дополнительные вопросы
Когда SELECT * допустим?
Чем рискован SELECT * с JOIN в коде?
Чем COALESCE отличается от NULLIF и как их использовать?
Middle
COALESCE возвращает первый не-NULL аргумент из списка. COALESCE(value, 'default') — частая замена NULL на значение по умолчанию.
NULLIF принимает два аргумента и возвращает NULL, если они равны, иначе первый. Часто используют, чтобы превратить пустую строку или нулевое значение в NULL: NULLIF(value, '').
Из практики: COALESCE и NULLIF — пара инструментов для аккуратной работы с NULL. Особенно полезны в импорте данных, где «пусто» и «отсутствует» нужно различать.
Частые ошибки
Использовать COALESCE с разными типами аргументов — будет приведение или ошибка.
Полагаться, что COALESCE короче, чем CASE — для нескольких ветвей CASE яснее.
Дополнительные вопросы
Чем IFNULL отличается от COALESCE?
Как заменить NULL в SUM на 0?
Чем DISTINCT отличается от GROUP BY для дедупликации?
Middle
Для простой дедупликации по списку столбцов SELECT DISTINCT и SELECT ... GROUP BY дают одинаковый результат и часто одинаковый план запроса.
Разница появляется, если нужно агрегатить — DISTINCT не позволяет считать агрегаты в том же запросе, GROUP BY позволяет. Также DISTINCT ON (Postgres) — отдельный синтаксис для «возьми по одной строке на каждое значение».
Из практики: для просто «уникальные значения столбца» — DISTINCT читается явно. Для «уникальные пары + что-то ещё» — GROUP BY. Для «возьми лучшую строку на ключ» — DISTINCT ON или window function.
Частые ошибки
Использовать DISTINCT, чтобы починить плохой JOIN, который удвоил строки — лечить нужно причину.
Считать DISTINCT * быстрым — он часто дорогой, особенно по широким строкам.
Дополнительные вопросы
Что такое DISTINCT ON и в каких СУБД он есть?
Как взять последнюю запись на пользователя?
Что такое составной индекс и в каком порядке указывать столбцы?
Middle
Составной индекс — это индекс по двум и более столбцам. Хранит ключи в порядке: сначала по первому столбцу, потом по второму внутри одинаковых значений первого и т.д.
Правило выбора порядка: первым ставят столбец, по которому чаще фильтруют через равенство, потом — по которому диапазон или сортировка. Иначе индекс не сможет работать в полную силу.
Из практики: индекс по (user_id, created_at) ускоряет запрос «последние заказы пользователя». Тот же индекс не поможет, если в запросе только created_at без user_id.
Частые ошибки
Делать индекс по (created_at, user_id) для типичных запросов с фильтром по user_id — индекс не пригодится.
Создавать второй односложный индекс там, где составной уже накрывает запрос.
Дополнительные вопросы
Что такое covering index?
Можно ли использовать левую часть составного индекса отдельно?
Что такое covering index и зачем он?
Middle
Covering index — это индекс, который содержит все столбцы, нужные запросу, и СУБД может ответить, не обращаясь к таблице. Это даёт максимальное ускорение чтения.
В Postgres делают через INCLUDE: CREATE INDEX ix ON orders(user_id) INCLUDE (amount, status). В MySQL — добавляя столбцы в сам индекс.
Из практики: applies для частых запросов «выбери эти поля по этому фильтру». Особенно ценно, когда таблица большая, а нужны только 2-3 поля.
Частые ошибки
Включать в индекс много колонок — он раздувается и замедляет запись.
Полагаться, что INCLUDE-столбцы участвуют в поиске — нет, они только для чтения.
Дополнительные вопросы
Чем INCLUDE отличается от добавления столбца в ключ индекса?
Когда не стоит делать covering index?
Чем B-tree индекс отличается от hash индекса?
Middle
B-tree — сбалансированное дерево, поддерживает поиск по равенству, диапазон, сортировку, LIKE 'foo%'. Это индекс по умолчанию в большинстве СУБД.
Hash индекс хранит хеши ключей в bucket-ах. Очень быстр для поиска по равенству, но не поддерживает диапазон и сортировку. В Postgres есть отдельный тип, но используется редко.
Из практики: 95% задач решает B-tree. Hash индекс выбирают для частых поисков по точному ключу в очень больших таблицах, где B-tree излишен.
Частые ошибки
Создавать hash индекс на UUID-ключи в надежде на ускорение — обычно overkill.
Полагаться, что hash индекс отсортирован — нет, для ORDER BY он не сработает.
Дополнительные вопросы
Что такое GiST и GIN индексы?
Для каких типов данных нужен GIN?
Что такое EXPLAIN ANALYZE и как его читать в Postgres?
Middle
EXPLAIN ANALYZE реально выполняет запрос и возвращает план с фактическими временами и количеством строк. План — это дерево узлов: Scan, Join, Aggregate, Sort.
Ключевые поля: cost (оценка), actual time, rows (estimated/actual), buffers (если EXPLAIN (ANALYZE, BUFFERS)). Сильное расхождение estimated/actual rows — повод обновить статистику ANALYZE table.
Из практики: читать снизу вверх. Самый «толстый» узел — главный кандидат на оптимизацию. Видишь Seq Scan по большой таблице с фильтром — нужен индекс.
Частые ошибки
Гнаться за уменьшением cost — это оценка, ориентируйтесь на actual time.
Запускать EXPLAIN ANALYZE на UPDATE/DELETE — он реально изменит данные.
Дополнительные вопросы
Что такое Bitmap Index Scan?
Как использовать EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)?
Что такое N+1 запрос и как с ним бороться?
Middle
N+1 — это анти-паттерн: один запрос вытащил список (1 запрос), а дальше для каждого элемента отдельно запрашиваются связанные данные (N запросов). Итого N+1 поход в БД вместо одного-двух.
Признак: на отрисовку страницы из 50 элементов выполняется 51 запрос. Видно в логах БД, в трассах APM или в инструментах ORM.
Из практики: решают eager loading в ORM (preload/include/with), JOIN-ом или подзапросом, либо отдельным batch-запросом с IN. Это типовой источник медленных страниц.
Частые ошибки
Не подключать eager loading и удивляться, что страница «иногда тормозит».
Что такое блокировки в Postgres (row lock, table lock, advisory)?
Middle
Row-level locks накладываются UPDATE/DELETE/SELECT FOR UPDATE на конкретные строки. Не блокируют чтение, но блокируют другие модификации тех же строк.
Table-level locks — на всю таблицу. Большинство ставится автоматически (Access Share при чтении, Row Exclusive при UPDATE). Иногда явно: LOCK TABLE для миграций.
Advisory locks — пользовательские мутексы на уровне БД, не привязанные к строкам. pg_advisory_lock(key). Удобно для координации задач между процессами. Из практики: понимание блокировок критично, чтобы избежать тупиков (deadlocks) — обычно встречается, когда две транзакции захватывают ресурсы в разном порядке.
Частые ошибки
Делать долгие транзакции с UPDATE — блокировки держатся весь срок транзакции.
Использовать SELECT FOR UPDATE без LIMIT — заблокирует огромный диапазон.
Дополнительные вопросы
Что такое SELECT FOR UPDATE SKIP LOCKED?
Как поймать deadlock?
Что такое MVCC и как он работает в PostgreSQL?
Middle
MVCC (Multi-Version Concurrency Control) — это подход, при котором каждое изменение создаёт новую версию строки, а старые остаются видимыми для уже идущих транзакций. Читателям не нужны блокировки от писателей.
Каждая строка имеет xmin (транзакция, создавшая её) и xmax (транзакция, удалившая/обновившая). Транзакция видит только те версии, которые валидны на её snapshot.
Из практики: побочный эффект — нужна периодическая чистка устаревших версий (VACUUM, autovacuum). Без неё таблица «раздувается».
Частые ошибки
Игнорировать autovacuum в нагруженной БД — таблица раздуется, индексы устареют.
Считать, что MVCC бесплатен — он создаёт постоянный поток мусора, требующий уборки.
Дополнительные вопросы
Что такое VACUUM FULL и в чём опасность?
Как посмотреть bloat таблицы?
Что такое partitioning и когда он нужен?
Middle
Партиционирование — это разделение большой таблицы на несколько физических подтаблиц по ключу (диапазон дат, список, хеш). Снаружи это всё ещё одна логическая таблица.
Преимущества: запросы по части ключа сканируют только нужные партиции, обслуживание (DROP, REINDEX) выполняется по частям, индексы внутри партиции меньше и быстрее.
Из практики: нужно для таблиц от десятков миллионов строк, особенно с time-series данными. Самая частая стратегия — по диапазону created_at (месяц/неделя).
Частые ошибки
Сделать партиционирование, но не использовать ключ партиции в WHERE — будут сканироваться все.
Использовать партиционирование на 100К строк — оверкилл, добавит сложности без выгоды.
Дополнительные вопросы
Что такое partition pruning?
Когда удобно hash partitioning вместо range?
Что такое sharding и чем отличается от партиционирования?
Middle
Партиционирование — это разделение в рамках одной БД. Sharding — это распределение данных по разным БД/серверам. Каждый shard — независимая инстанция.
Sharding решает проблему, когда одна машина не справляется с объёмом или нагрузкой. Минусы: сложно делать кросс-shard JOIN, транзакции между шардами требуют отдельных протоколов (двухфазный коммит или согласованность eventual).
Из практики: до многих десятков ТБ данных и сотен тысяч TPS обычно хватает оптимизации и партиционирования. Sharding выбирают, когда другие способы не работают.
Частые ошибки
Выбирать ключ shard, который ведёт к перекосу (например, user_id, где 1% пользователей создают 80% трафика).
Считать, что после sharding'а можно жить без изменений в приложении.
Дополнительные вопросы
Что такое consistent hashing?
Чем sharding отличается от репликации?
Что такое репликация и какие виды бывают?
Middle
Репликация — это копирование изменений с одной БД (primary) на другие (replicas). Используется для отказоустойчивости, разгрузки чтения, географической близости.
Виды: synchronous (primary ждёт подтверждения от реплики — гарантия консистентности, но медленнее) vs asynchronous (не ждёт — быстро, но при сбое часть данных может потеряться). Также: logical vs physical (на уровне записи WAL или на уровне SQL/команд).
Из практики: в Postgres есть streaming replication (physical async по умолчанию). Чтения на реплику разгружают primary, но имейте в виду replication lag — реплика может отставать.
Частые ошибки
Считать данные на реплике актуальными после write на primary — между ними lag.
Использовать только async-репликацию для финансовых данных без backup.
Дополнительные вопросы
Что такое replication lag и как его мониторить?
Чем logical replication лучше physical?
Что такое триггеры и когда их применять?
Middle
Триггер — это код (функция), который автоматически выполняется при определённом событии в таблице: BEFORE/AFTER INSERT/UPDATE/DELETE.
Применения: автоматическое обновление updated_at, аудит изменений (запись в audit-таблицу), валидация бизнес-правил, поддержание денормализованных счётчиков.
Из практики: триггеры удобны там, где правило должно выполняться независимо от приложения (множество клиентов, легаси-доступ). Но они «магические» — поведение БД меняется неочевидно для разработчика.
Частые ошибки
Делать тяжёлую бизнес-логику в триггере — все транзакции замедляются.
Использовать триггеры, когда хватает обычного кода — потерянная читаемость.
Дополнительные вопросы
Чем BEFORE триггер отличается от AFTER?
Что такое триггер на представление (INSTEAD OF)?
Что такое json и jsonb в PostgreSQL?
Middle
json хранит JSON в исходном тексте — быстро записывает, медленно ищет внутри. jsonb хранит в бинарном виде после парсинга — занимает место плотнее и поддерживает индексы (GIN).
Для запросов внутри JSON-структуры используют операторы ->, ->>, @>, ? и функции jsonb_path_query.
Из практики: для рабочих данных всегда jsonb. json оставляйте для редких случаев, когда важно сохранить точную форму (порядок ключей, пробелы).
Частые ошибки
Хранить структурированные данные в jsonb там, где можно нормальную таблицу — теряете типизацию и индексирование.
Делать поиск без GIN-индекса по большой таблице — sequential scan.
Дополнительные вопросы
Что делает GIN индекс на jsonb?
Как обновить одно поле в jsonb?
Чем хранимые процедуры отличаются от функций в SQL?
Middle
Функция возвращает значение (или набор) и обычно используется внутри SELECT. У неё ограниченный набор побочных эффектов (в Postgres до 11 — без транзакционного управления).
Процедура (CALL) может управлять транзакциями (COMMIT, ROLLBACK внутри), не возвращает напрямую значение в SELECT, скорее предназначена для action-сценариев — миграции, batch-обновления.
Из практики: процедуры удобны для регламентных задач, выполняемых cron-ом или вручную. Бизнес-логику лучше держать в приложении, а не в процедурах — иначе версионирование и тестирование становится больно.
Частые ошибки
Втащить всю бизнес-логику в процедуры — обновлять и тестировать тяжело.
Полагаться, что функция может коммитить — в большинстве СУБД нет.
Дополнительные вопросы
Чем PL/pgSQL отличается от других расширений?
Когда лучше использовать процедуру?
Что такое skip locked и зачем его используют?
Middle
SELECT ... FOR UPDATE SKIP LOCKED — это вариант блокировки, при котором строки, уже захваченные другой транзакцией, не ждут, а пропускаются.
Главное применение — реализация очередей задач в БД. Несколько воркеров одновременно берут «следующий пакет», не блокируя друг друга и не получая одну и ту же задачу.
Из практики: для лёгких очередей до сотен задач в секунду PostgreSQL с SKIP LOCKED спокойно справляется, и нет нужды поднимать Redis/RabbitMQ.
Частые ошибки
Забыть SKIP LOCKED и устроить очередь, где воркеры стоят в ожидании друг друга.
Использовать FOR UPDATE NOWAIT там, где ждать нормально, и получить лишние ошибки.
Дополнительные вопросы
Чем NOWAIT отличается от SKIP LOCKED?
Какой LIMIT поставить на очереди в SKIP LOCKED?
Что такое CHECK constraint и когда его использовать?
Middle
CHECK — это ограничение на значения столбца или строки: CHECK (age >= 0), CHECK (price > 0 AND discount BETWEEN 0 AND 100). Если условие false при вставке/обновлении — операция отвергается.
Применяется для бизнес-правил, которые точно должны выполняться (возраст не отрицательный, статус из списка значений и т.п.).
Из практики: лучше иметь правило на уровне БД, чем полагаться на приложение — изменения BD могут идти из разных источников.
Частые ошибки
Писать тяжёлые подзапросы в CHECK — это влияет на каждую вставку.
Использовать CHECK для проверки, которая зависит от другой строки — лучше триггер.
Дополнительные вопросы
Чем CHECK отличается от FOREIGN KEY?
Можно ли проверять CHECK на нескольких столбцах?
Что такое savepoint и для каких задач он нужен?
Middle
SAVEPOINT — это «точка сохранения» внутри транзакции. После SAVEPOINT можно сделать ROLLBACK TO SAVEPOINT и откатить часть операций, не откатывая всю транзакцию.
Удобно при импорте: внутри одной большой транзакции для каждой строки делают savepoint и при ошибке откатывают только её, продолжая остальные.
Из практики: ORM часто использует savepoint автоматически для вложенных транзакций. Слишком много savepoint увеличивает оверхед.
Частые ошибки
Создавать savepoint на каждую строку в большой выборке — оверхед.
Полагаться, что savepoint обходит constraint в момент commit — нет.
Дополнительные вопросы
Чем savepoint отличается от вложенной транзакции?
Можно ли иметь несколько savepoint одновременно?
Что такое VACUUM и autovacuum в PostgreSQL?
Middle
VACUUM очищает таблицу от устаревших версий строк, оставленных MVCC. Освобождает место внутри страниц для повторного использования, обновляет visibility map и статистику.
Autovacuum — фоновая служба, запускающая VACUUM (и ANALYZE) автоматически по достижении порогов изменений. Настраивается параметрами autovacuum_vacuum_scale_factor и т.д.
Из практики: без adequately настроенного autovacuum таблица «раздувается» — растут размер на диске и время сканирования. На больших таблицах часто настраивают пер-таблично более агрессивный autovacuum.
Частые ошибки
Запускать VACUUM FULL на проде днём — берёт ACCESS EXCLUSIVE и блокирует всё.
Игнорировать предупреждения о росте dead tuples.
Дополнительные вопросы
Чем VACUUM отличается от VACUUM FULL?
Что такое pg_repack?
Что такое полнотекстовый поиск и чем он отличается от LIKE?
Middle
Полнотекстовый поиск умеет искать не подстроки, а смысл: учитывает морфологию (читал → читать), стоп-слова, ранжирование по релевантности. В Postgres есть tsvector/tsquery и GIN-индекс.
LIKE 'foo%' работает по символьному совпадению и без индекса делает sequential scan. Подходит для простых случаев («начинается с»), но не для естественного поиска по контенту.
Из практики: для поиска по статьям, описаниям товаров — полнотекстовый. Для строгих фильтров (email LIKE '@example.com') — LIKE с trigram-индексом или функциональным.
Частые ошибки
Использовать LIKE '%foo%' на большой таблице без индекса — медленно.
Считать полнотекстовый поиск заменой Elasticsearch — Postgres хорош до десятков миллионов записей.
Дополнительные вопросы
Что такое pg_trgm?
Когда выбрать Elasticsearch вместо Postgres FTS?
Что такое pessimistic и optimistic locking?
Middle
Pessimistic — это когда транзакция заранее блокирует строку (SELECT FOR UPDATE) и другие ждут. Простая и понятная модель, но снижает параллелизм.
Optimistic — транзакции работают без блокировок, а при COMMIT проверяется, что данные не изменились (через version-поле или временную метку). Если изменились — ошибка, попробуй ещё раз.
Из практики: pessimistic подходит для редких изменений с высокой ценой конфликта (платежи). Optimistic — для частых независимых правок (профиль, корзина), где конфликт редок.
Частые ошибки
Использовать pessimistic для высоконагруженной таблицы — узкое место.
Реализовать optimistic без version-поля — не получится корректно сравнить.
Дополнительные вопросы
Как реализовать optimistic locking в JPA/Hibernate?
Что такое retry policy при optimistic?
Какие типичные ошибки при работе с временными типами (TIMESTAMP / TIMESTAMPTZ)?
Middle
TIMESTAMP хранит дату-время без часового пояса — буквально как написано. TIMESTAMPTZ хранит UTC и приводит к timezone клиента при чтении.
Главная ошибка — хранить локальное время без пояса и потом не понимать, что значит «12:00». Стандартная рекомендация: TIMESTAMPTZ + хранить в UTC. На клиенте всё конвертируется в нужный пояс.
Из практики: при работе с несколькими часовыми поясами TIMESTAMP без TZ — источник постоянных багов (двойное добавление UTC offset, поломанные интервалы вокруг DST).
Частые ошибки
Использовать TIMESTAMP для бизнес-данных и потом мучиться с локализацией.
Сохранять с одним TZ, читать с другим без учёта.
Дополнительные вопросы
Как Postgres обрабатывает летнее время?
Чем DATE отличается от TIMESTAMP?
Чем INNER JOIN отличается от CROSS JOIN на больших таблицах?
Middle
CROSS JOIN даёт декартово произведение — каждая строка слева с каждой справа. На таблицах в миллион строк это триллион строк результата.
INNER JOIN использует условие ON и оптимизатор выбирает hash/merge/nested loop — на индексах работает быстро.
Из практики: CROSS JOIN иногда нужен для генерации комбинаций (даты × категории), но всегда вместе с разумным фильтром в WHERE.
Частые ошибки
Случайно сделать CROSS JOIN, забыв ON — миллионы лишних строк.
Не оценивать ожидаемый размер результата.
Дополнительные вопросы
Когда CROSS JOIN полезен?
Что такое cardinality estimation?
Что такое FULL OUTER JOIN и для каких задач?
Middle
FULL OUTER JOIN возвращает все строки из обеих таблиц. Совпадения соединяются, несовпадающие строки получают NULL с противоположной стороны.
Применяют для сверок: найти, какие записи есть только в A, какие только в B, какие в обеих.
Из практики: типовой пример — сверка данных между двумя источниками для миграции, ETL, аудита.
Частые ошибки
Считать, что FULL OUTER ускоряет — он медленнее INNER, часто требует sort/hash обеих таблиц.
Не использовать в MySQL — там его нет, нужно UNION.
Дополнительные вопросы
Как имитировать FULL OUTER в MySQL?
Что такое antijoin?
Что такое self-join и где он нужен?
Middle
Self-join — это соединение таблицы с самой собой через алиасы. Применяют для иерархических данных (сотрудник → начальник), для сравнения строк в одной таблице (текущая со следующей).
Например, SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id.
Из практики: для иерархий лучше рекурсивный CTE — он обходит несколько уровней. Self-join хорош на один-два уровня.
Частые ошибки
Забыть алиасы — будет ошибка двусмысленности.
Делать self-join на огромной таблице без индексов.
Дополнительные вопросы
Чем self-join отличается от рекурсивного CTE?
Когда лучше window function?
Что такое HAVING с COUNT и типовой пример?
Junior
HAVING фильтрует группы после GROUP BY по агрегатам. Например, SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) > 10 — пользователи с более 10 заказами.
В отличие от WHERE, HAVING работает с агрегатами, которые видны только после группировки.
Из практики: незаменим для отчётов «у кого больше N» или «у кого сумма выше X». Если можно отфильтровать строкой — WHERE.
Частые ошибки
Использовать алиас агрегата в HAVING — в большинстве СУБД нельзя, нужно повторить выражение.
Писать неагрегатное условие в HAVING — медленнее и не семантично.
Дополнительные вопросы
Можно ли в HAVING ссылаться на WHERE-фильтр?
Чем HAVING отличается от WHERE?
Что такое LIMIT и OFFSET и почему они медленны на больших offset?
Middle
LIMIT N OFFSET M возвращает следующие N строк после пропуска M. Удобно для пагинации.
Проблема: для больших OFFSET СУБД всё равно читает M+N строк и отбрасывает M. На странице 1000 это медленно.
Из практики: для больших данных используют keyset-пагинацию: WHERE id > last_id LIMIT N. Это O(log n) против O(n) для OFFSET.
Частые ошибки
Делать пагинацию на 100к страниц через OFFSET — последние страницы тормозят.
Не учитывать, что между запросами данные могут измениться, и OFFSET «съезжает».
Дополнительные вопросы
Что такое keyset pagination?
Как пагинировать с сортировкой по нескольким полям?
Что такое GROUPING SETS, ROLLUP, CUBE?
Middle
Расширения GROUP BY для агрегации по нескольким наборам колонок одним запросом. ROLLUP даёт промежуточные итоги по иерархии (страна, город, итого). CUBE — все возможные комбинации. GROUPING SETS — явный список.
Намного быстрее, чем UNION нескольких GROUP BY.
Из практики: для отчётов с подытогами — классический инструмент. Поддерживается во всех серьёзных СУБД (Postgres, Oracle, SQL Server).
Частые ошибки
Использовать CUBE на много колонок — экспоненциальный рост результата.
Забывать функцию GROUPING() для различения NULL-результата и NULL из subtotal.
Дополнительные вопросы
Что такое функция GROUPING()?
Чем ROLLUP отличается от CUBE?
Что такое CASE WHEN и где его использовать?
Junior
CASE WHEN condition THEN value [WHEN ...] ELSE other END — выражение if/else в SQL. Можно использовать в SELECT, WHERE, ORDER BY, GROUP BY.
Простая форма: CASE column WHEN val1 THEN ... WHEN val2 THEN ... END. Поисковая — с произвольными условиями.
Из практики: типовой инструмент для условной агрегации (SUM(CASE WHEN status='paid' THEN amount END)), категоризации, сортировки по приоритету.
Частые ошибки
Забыть ELSE и получить NULL.
Сложить несовместимые типы в одном CASE — будет ошибка.
Дополнительные вопросы
Что такое условная агрегация?
Чем COALESCE отличается от CASE?
Что такое subquery in FROM (производная таблица)?
Middle
SELECT ... FROM (SELECT ... FROM t WHERE ...) AS sub — это вложенный SELECT как источник данных. Псевдоним обязателен.
Полезен для пред-агрегации перед JOIN, для отбора с window function, для упрощения сложных запросов.
Из практики: часто переписывается в CTE для лучшей читаемости. Большинство современных СУБД оптимизирует оба варианта одинаково.
Частые ошибки
Без алиаса — синтаксическая ошибка.
Класть туда что попало без понимания, что СУБД материализует.
Дополнительные вопросы
Чем производная таблица отличается от CTE?
Что такое lateral join?
Что такое LATERAL JOIN?
Middle
LATERAL — это разрешение подзапросу в FROM ссылаться на колонки предыдущих таблиц того же FROM. Похоже на CROSS APPLY в SQL Server.
Применяется для «для каждой строки слева выполни этот подзапрос»: top-N на группу, динамические агрегаты.
Из практики: даёт компактный способ выразить запросы, которые иначе требуют window function или коррелированных подзапросов с побочными эффектами.
Частые ошибки
Применять LATERAL без необходимости — добавляет сложность.
Считать LATERAL быстрее обычного подзапроса — зависит от данных.
Дополнительные вопросы
Чем LATERAL отличается от CROSS JOIN?
Когда LATERAL быстрее, чем window function?
Что такое constraint NOT NULL и зачем он?
Junior
NOT NULL запрещает значение NULL в столбце. При попытке вставки/обновления с NULL — ошибка.
Это самый базовый способ обеспечить наличие данных. Часто комбинируют с DEFAULT.
Из практики: ставьте NOT NULL по умолчанию там, где можно. Это убирает целый класс багов «забыли заполнить поле».
Частые ошибки
Создавать таблицу без NOT NULL и получить мусорные NULL в проде.
Добавлять NOT NULL на большую таблицу без DEFAULT — долгая блокировка.
Дополнительные вопросы
Что такое DEFAULT?
Как добавить NOT NULL без даунтайма?
Чем CHAR отличается от VARCHAR?
Junior
CHAR(n) — фиксированная длина n, доконечно дополняется пробелами. VARCHAR(n) — переменная до n, занимает только реальный объём + 1-2 байта на длину.
В большинстве современных СУБД VARCHAR удобнее и не медленнее. CHAR оправдан для строго фиксированной длины (код страны).
Из практики: для текста почти всегда VARCHAR. В Postgres VARCHAR без n и TEXT по производительности одинаковы.
Частые ошибки
Использовать CHAR для текстовых полей — лишние пробелы и потенциальные баги сравнения.
В MySQL — CHAR в utf8mb4 это 4×n байт.
Дополнительные вопросы
Чем VARCHAR отличается от TEXT?
Когда выбирать TEXT?
Что такое функция COUNT(*) vs COUNT(column)?
Junior
COUNT(*) считает все строки. COUNT(column) — только строки, где column не NULL.
Из практики: помнить, что COUNT(column) пропускает NULL — типовая ошибка. Если столбец NOT NULL — оба варианта дадут одно число.
Частые ошибки
Считать, что COUNT(column) = COUNT(*) — не равно, если есть NULL.
Использовать COUNT(*) с GROUP BY и удивляться, что NULL-группы тоже считаются.
Дополнительные вопросы
Что вернёт COUNT(*) на пустой таблице?
Чем COUNT(*) быстрее COUNT(column)?
Что такое DEFAULT для столбца?
Junior
DEFAULT задаёт значение, которое подставится, если при INSERT столбец не указан или передано DEFAULT.
Может быть константой, функцией (CURRENT_TIMESTAMP, gen_random_uuid()), последовательностью (NEXT VALUE FOR).
Из практики: created_at часто делают NOT NULL DEFAULT CURRENT_TIMESTAMP. Это избавляет от необходимости подставлять значение в коде каждый раз.
Частые ошибки
DEFAULT работает только на INSERT — для UPDATE значения по умолчанию пишут в коде.
Менять DEFAULT на огромной таблице — долгая блокировка в старых версиях Postgres.
Дополнительные вопросы
Чем DEFAULT отличается от GENERATED?
Может ли DEFAULT ссылаться на другие столбцы?
Что такое SEQUENCE и в чём она лучше AUTO_INCREMENT?
Middle
SEQUENCE — отдельный объект, генерирующий уникальные числа. nextval() возвращает следующее значение. В Postgres использовали SEQUENCE до появления GENERATED ALWAYS AS IDENTITY (стандарт SQL).
В отличие от MySQL AUTO_INCREMENT, SEQUENCE — отдельный объект, можно использовать в нескольких таблицах, явно управлять.
Из практики: для нового кода в Postgres лучше IDENTITY-колонки. Но SEQUENCE остаётся, когда нужны более тонкие настройки (cache, increment by, cycle).
Частые ошибки
Откатить транзакцию с nextval — значение всё равно «съедено».
Полагаться на отсутствие пропусков в id.
Дополнительные вопросы
Что такое GENERATED ALWAYS AS IDENTITY?
Чем UUID лучше int для id?
Какие подводные камни у UUID как первичного ключа?
Middle
Случайные UUID (v4) — большие (16 байт vs 8 у BIGINT), не упорядочены — индекс на них растёт не последовательно, что ухудшает кеш и фрагментацию.
Упорядоченные варианты (UUID v7, ULID) сохраняют преимущество глобальной уникальности и не убивают индекс.
Из практики: для распределённых систем и публичных id — UUID. Для внутренних таблиц с тяжёлыми JOIN'ами — int/bigint. Можно использовать оба: bigint id для join и uuid public_id для внешнего мира.
Частые ошибки
Использовать UUID v4 как pk в нагруженной таблице без понимания последствий.
Сериализовать UUID в текст — лишний расход.
Дополнительные вопросы
Что такое UUID v7?
Когда лучше ULID?
Что такое pgcrypto и для чего его используют?
Middle
Расширение Postgres с функциями шифрования: crypt() для хеширования паролей, gen_random_uuid(), digest(text, 'sha256'), encrypt/decrypt.
Удобно для хранения чувствительных данных в зашифрованном виде на уровне БД.
Из практики: для паролей лучше использовать bcrypt/argon2 на уровне приложения. pgcrypto — для случаев, когда шифрование на стороне БД оправдано.
Частые ошибки
Хранить пароли в открытом виде в БД.
Использовать digest для паролей вместо crypt с salt.
Дополнительные вопросы
Чем crypt отличается от digest?
Что такое column-level encryption?
Что такое pg_stat_statements?
Middle
Расширение Postgres, собирающее статистику по выполненным запросам: количество, общее время, мин/макс/среднее, IO.
Запросы группируются по шаблону (с заменой параметров). Видно top самых медленных и самых частых запросов.
Из практики: первая утилита для диагностики медленной БД. Включают на проде и регулярно анализируют.
Частые ошибки
Не включать в проде и потом не знать, что тормозит.
Полагать, что pg_stat_statements заменяет APM.
Дополнительные вопросы
Что такое pg_stat_activity?
Чем auto_explain дополняет?
Что такое write-ahead log (WAL)?
Middle
WAL — журнал изменений Postgres. Каждое изменение сначала пишется в WAL, потом применяется к таблицам. Это даёт durability (D из ACID) и основу для репликации.
На WAL построены: репликация (поток изменений на реплику), point-in-time recovery (PITR), логическое декодирование.
Из практики: размер WAL и checkpoint частота — важные параметры тюнинга. Слишком частый checkpoint — лишний IO.
Частые ошибки
Не настраивать checkpoint и max_wal_size.
Полагать, что WAL — это просто лог.
Дополнительные вопросы
Что такое checkpoint?
Чем logical decoding полезен?
Что такое connection pool в БД и зачем?
Middle
Каждое соединение с БД — ресурс (память, процесс). Открытие — дорого (TCP, TLS, аутентификация). Pool переиспользует.
PgBouncer / pgcat — внешние pooler для Postgres. Также есть pool в драйверах (HikariCP для Java).
Из практики: чтобы много клиентов работали с малым числом серверных соединений, используют PgBouncer в transaction mode.
Частые ошибки
Открывать соединение на каждый запрос.
Ставить max_connections слишком высоко без понимания.
Дополнительные вопросы
Что такое transaction pooling mode?
Чем session mode отличается?
Что такое NULL semantics в SQL?
Middle
NULL — это «неизвестно», а не «пусто». Все операции с NULL дают NULL: NULL = NULL → NULL (не TRUE), NULL + 1 → NULL.
Для проверки — IS NULL / IS NOT NULL. Тернарная логика (TRUE/FALSE/NULL) усложняет условия.
Из практики: типовая ошибка — фильтр WHERE col != 'x' и удивление, что не вернулись строки с NULL. Нужно WHERE col != 'x' OR col IS NULL.
Частые ошибки
Сравнивать NULL через =.
Не учитывать NULL в HAVING, JOIN, ORDER BY.
Дополнительные вопросы
Что такое three-valued logic?
Чем NULLS FIRST/LAST полезен?
Что такое EXPLAIN buffers в Postgres?
Middle
EXPLAIN (ANALYZE, BUFFERS) показывает, сколько страниц в буферном кеше прочитано (hit) и сколько с диска (read).
Высокий read — запрос упирается в IO, кеш не помогает. Высокий hit — данные в памяти, ускорение CPU-bound оптимизациями.
Из практики: BUFFERS помогает понять, стоит ли увеличивать shared_buffers, какие запросы холодные.
Частые ошибки
Игнорировать BUFFERS и смотреть только на cost.
Запускать EXPLAIN без ANALYZE.
Дополнительные вопросы
Что такое shared_buffers?
Как прогреть кеш?
Что такое isolation level repeatable read в Postgres?
Middle
В стандарте SQL repeatable read предотвращает non-repeatable read и dirty read, но допускает phantom read. В Postgres repeatable read = snapshot isolation, и phantom тоже предотвращён (почти серилазуемая).
Использует MVCC: транзакция видит snapshot БД на момент старта.
Из практики: для большинства задач default READ COMMITTED достаточен. REPEATABLE READ — когда внутри транзакции нужно несколько раз читать одни данные и видеть одно и то же.
Частые ошибки
Полагаться на стандартное поведение REPEATABLE READ в Postgres — оно сильнее.
Использовать без понимания возможных конфликтов.
Дополнительные вопросы
Чем SERIALIZABLE Postgres отличается от REPEATABLE READ?
Что такое serialization failure?
Что такое heap-only tuple (HOT)?
Middle
В Postgres MVCC создаёт новую версию строки при UPDATE. Обычно нужно обновить и индексы (новая строка — новое местоположение).
HOT — оптимизация: если обновляется только не-индексированный столбец, и в той же странице есть место, индексы остаются прежними.
Из практики: HOT-update сильно ускоряет UPDATE на колонках вне индексов. Это причина, почему лишние индексы замедляют запись.
Частые ошибки
Создавать индексы на каждое поле — теряются HOT-updates.
Не следить за fillfactor.
Дополнительные вопросы
Что такое fillfactor?
Когда HOT не работает?
Что такое JSONB операторы в Postgres?
Middle
-> возвращает поле как JSON. ->> возвращает как text. @> проверяет включение (contains). ? проверяет существование ключа. #>> идёт по пути.
Пример: SELECT data->>'name' FROM users WHERE data @> '{"active": true}'.
Из практики: для эффективного поиска по JSONB ставят GIN-индекс. Это даёт быстрый @>, ?, ?&, ?|.
Частые ошибки
Использовать -> вместо ->> и потом удивляться, что возвращается JSON, а не строка.
Не ставить GIN-индекс на JSONB-поле с активным поиском.
Дополнительные вопросы
Что такое jsonb_path_query?
Чем jsonpath отличается от jsonb_path?
Что такое generated column и зачем?
Middle
GENERATED ALWAYS AS (expression) — столбец, значение которого вычисляется автоматически из других. STORED (хранится физически) или VIRTUAL (вычисляется при чтении, не во всех СУБД).
Удобно для производных значений, индексов на сложных выражениях, нормализации.
Из практики: классический пример — full_name AS (first_name || ' ' || last_name). Можно индексировать generated column для поиска.
Частые ошибки
Использовать generated column без понимания performance влияния.
Полагать, что VIRTUAL = бесплатно.
Дополнительные вопросы
Чем STORED отличается от VIRTUAL?
Как индексировать выражение?
Почему реляционные БД называют реляционными?
Junior
Название идёт от математического термина «отношение» (relation). В реляционной модели Кодда отношение — это таблица: множество кортежей (строк) с одинаковым набором атрибутов (колонок).
Связи между сущностями выражаются не указателями, а значениями ключей в других таблицах — поэтому модель «реляционная»: всё описывается через отношения и операции над ними (выборка, проекция, соединение).
Из практики: понимание реляционной модели помогает писать корректные SQL — становится ясно, почему JOIN, GROUP BY и подзапросы устроены именно так.
Частые ошибки
Думают, что «реляционная» — от «relations between tables» (связи). На деле — от «relation = таблица».
Считают, что NoSQL «не имеет связей» — связи есть, просто другие.
Дополнительные вопросы
Какие операции реляционной алгебры знаешь?
Что такое нормальные формы и зачем они нужны?
Что такое нормализация БД? Назови первые три нормальные формы.
Middle
Нормализация — приведение схемы к виду, минимизирующему избыточность и аномалии. Шаги: 1NF — атомарные значения, без повторяющихся групп. 2NF — 1NF + все неключевые атрибуты полностью зависят от первичного ключа (а не от его части). 3NF — 2NF + неключевые атрибуты не зависят транзитивно через другие неключевые.
Дальше — BCNF, 4NF, 5NF — для редких краевых случаев. На практике обычно достигают 3NF/BCNF.
Из практики: нормализация — это не «всегда хорошо». В аналитических хранилищах часто специально денормализуют для скорости чтения.
Частые ошибки
Путают 2NF и 3NF.
Хранят CSV в одной колонке и нарушают 1NF.
Дополнительные вопросы
Что такое BCNF и чем она отличается от 3NF?
Когда оправдана денормализация?
Что такое денормализация и когда её применять?
Middle
Денормализация — намеренное введение избыточности данных (дублирование, материализованные агрегаты, плоские таблицы) ради ускорения чтения, упрощения запросов или повышения доступности.
Применяю в OLAP-хранилищах (звезда/снежинка), в reporting-таблицах, в read-replica для тяжёлых запросов. В OLTP — точечно: счётчики, кэшированные агрегаты с фоновой синхронизацией.
Из практики: денормализация всегда обмен. Получаем скорость чтения, платим консистентностью и сложностью записи. Поэтому только при подтверждённой бизнес-потребности.
Частые ошибки
Денормализуют «на всякий случай» и получают рассинхрон данных.
Не документируют, какие поля денормализованы — путаница для разработчиков.
1:1 — одна строка слева соответствует не более чем одной справа (пользователь и его профиль). 1:N — одной строке слева соответствует много справа (клиент и его заказы). N:M — обе стороны имеют много (студенты и курсы).
1:1 редко самостоятельна — обычно следствие разделения таблицы по причинам производительности или безопасности. N:M реализуется через промежуточную таблицу-связку.
Из практики: при описании предметной области сначала рисую сущности и связи на бумаге. Потом превращаю в физическую схему — это снимает половину будущих изменений.
Частые ошибки
Путают «нет связи» и «опциональная связь» (необязательная).
Делают 1:1 там, где достаточно одной таблицы.
Дополнительные вопросы
Чем кардинальность отличается от мощности связи?
Как обозначают опциональность в нотации Крофт-Чена?
Как реализуется связь «многие ко многим»?
Junior
Через промежуточную таблицу-связку (junction table). Например, для students и courses создаётся student_courses с парой внешних ключей (student_id, course_id) — первичным ключом может быть их комбинация.
В связке часто появляются дополнительные атрибуты связи: дата записи, оценка, роль участника. Это превращает её в полноценную сущность.
Из практики: как только в связке появляются атрибуты — переименуйте её в осмысленную сущность (Enrollment, Membership). Дальше будет проще обсуждать.
Частые ошибки
Делают N:M «логически», без таблицы-связки — данные размазываются.
Не вешают индексы на оба FK в связке — JOIN становится тяжёлым.
Дополнительные вопросы
Что такое составной первичный ключ?
Когда выгодно ввести суррогатный ID в таблице-связке?
Что такое первичный ключ? Какими свойствами обладает?
Junior
Первичный ключ (PK) — атрибут или набор атрибутов, однозначно идентифицирующий каждую строку таблицы. Свойства: уникальность, NOT NULL, неизменяемость (по согласию команды).
Бывает естественный (значение из домена — ИНН, email) и суррогатный (искусственный UUID/serial). На практике чаще используют суррогатный — он стабильнее и не зависит от изменений бизнеса.
Из практики: естественный ключ кажется «честным», но почти всегда подводит — ИНН меняется, email перевыпускается. Суррогатный безопаснее по умолчанию.
Частые ошибки
Делают PK на основании email/телефона — потом обнаруживают, что они меняются.
Меняют значения PK для «причёсывания» — ломают внешние ключи.
Дополнительные вопросы
Что такое candidate key и alternate key?
Когда composite PK оправдан?
Что такое внешний ключ?
Junior
Внешний ключ (FK) — атрибут или набор атрибутов в одной таблице, ссылающийся на PK другой (или той же) таблицы. Гарантирует ссылочную целостность: нельзя вставить orders.client_id, которого нет в clients.id.
Поведение при удалении/обновлении родителя задаётся ON DELETE / ON UPDATE: CASCADE (удалить детей), SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
Из практики: отказ от FK на уровне БД «ради производительности» — частая ошибка. Сэкономленные миллисекунды оборачиваются битыми данными, которые потом чинят руками.
Частые ошибки
Не индексируют FK — JOIN и удаление становятся медленными.
Используют ON DELETE CASCADE без понимания цепочки — теряют данные.
Дополнительные вопросы
Что такое deferred constraint?
Когда FK хранят как «логический», без db-constraint?
Что такое индекс? Какие типы знаешь? Когда индекс вредит?
Middle
Индекс — структура данных (чаще B-tree), ускоряющая поиск и сортировку по выбранным колонкам ценой накладных расходов на запись. Типы: B-tree (универсальный), Hash (равенство), GIN/GiST (полнотекст, JSON, гео), BRIN (большие таблицы с порядком), частичный, функциональный, составной.
Индекс вредит, когда: таблица маленькая (полный скан быстрее), много операций записи (каждый индекс — лишняя запись), низкая селективность (мало уникальных значений), запросы не покрываются индексом, дублирующиеся индексы.
Из практики: «больше индексов — быстрее» — миф. Хороший индексный план — это 3–5 продуманных индексов под реальные запросы, а не 20 «на всякий случай».
Частые ошибки
Индексируют каждую колонку и убивают INSERT/UPDATE.
Создают индекс по `boolean` или `gender` — индекс бесполезен.
Дополнительные вопросы
Что такое covering index?
Как ты выбираешь порядок колонок в составном индексе?
Что такое транзакция?
Junior
Транзакция — логически целостная последовательность операций, которая выполняется как единое целое: либо вся целиком (commit), либо никак (rollback).
Гарантии транзакции описывают свойства ACID. Типичный пример: списать со счёта A и зачислить на B — оба действия должны произойти вместе.
Из практики: не любая «логическая операция» — это БД-транзакция. Распределённые операции через очереди и микросервисы требуют отдельных подходов (Saga, outbox), а не одной транзакции на всё.
Частые ошибки
Делают транзакцию на час и блокируют половину системы.
Полагают, что HTTP-запрос автоматически = транзакция.
Дополнительные вопросы
Что такое savepoint?
Какие альтернативы транзакциям в распределённых системах?
Что такое ACID? Расшифруй каждую букву.
Junior
Atomicity — атомарность: всё или ничего. Consistency — согласованность: после commit БД остаётся в валидном состоянии (соблюдены constraints). Isolation — изолированность: параллельные транзакции не видят промежуточных состояний друг друга. Durability — устойчивость: после commit изменения переживают сбой.
ACID обычно полностью реализуют реляционные БД (PostgreSQL, Oracle, MS SQL). В NoSQL и распределённых системах часто жертвуют Consistency ради Availability/Performance.
Из практики: чаще всего «теряется» Isolation — её уровень в коде явно не указывают, и в продакшене ловят странные баги.
Частые ошибки
Путают Consistency из ACID и Consistency из CAP — это разные понятия.
Считают, что NoSQL «вообще без транзакций» — у современных есть, но с ограничениями.
Дополнительные вопросы
Чем Consistency в ACID отличается от Consistency в CAP?
Как реализуется Durability на уровне WAL?
Какие уровни изоляции транзакций знаешь?
Middle
Стандарт SQL: Read Uncommitted (видит чужие незакоммиченные изменения — dirty read), Read Committed (видит только закоммиченные), Repeatable Read (повторное чтение даёт тот же результат), Serializable (полная изоляция, как при последовательном выполнении).
Чем выше уровень — тем меньше аномалий, но дороже по блокировкам и параллелизму. Реальное поведение зависит от СУБД: например, PostgreSQL Read Committed — это снимок (snapshot), а не блокировки.
Из практики: для большинства приложений Read Committed достаточно. Serializable использую точечно — там, где деньги/учёт.
Частые ошибки
Считают, что уровень изоляции по умолчанию у всех СУБД одинаков — это не так (Postgres: RC, MySQL InnoDB: RR).
Полагаются на Repeatable Read, но не учитывают phantom-чтения в одних реализациях и их отсутствие в других.
Дополнительные вопросы
Что такое snapshot isolation?
Чем serializable отличается от serializable snapshot isolation (SSI)?
Что такое грязное, неповторяемое и фантомное чтение?
Middle
Dirty read — транзакция читает изменения другой ещё незакоммиченной транзакции. Non-repeatable read — повторное чтение той же строки даёт другое значение, потому что другая транзакция её обновила и закоммитила. Phantom read — повторный запрос с тем же условием возвращает другой набор строк (другая транзакция вставила/удалила).
Защита: Read Committed запрещает dirty read; Repeatable Read добавляет защиту от non-repeatable; Serializable защищает и от phantom.
Из практики: phantom-чтения чаще всего вылавливают в отчётных запросах — суммы расходятся между двумя select'ами в одной транзакции.
Частые ошибки
Путают non-repeatable и phantom.
Думают, что Repeatable Read везде защищает от phantom — в Postgres защищает, в стандарте — нет.
Дополнительные вопросы
Какие ещё аномалии бывают (lost update, write skew)?
Как выбрать уровень изоляции под бизнес-сценарий?
Что такое CAP-теорема?
Middle
Теорема Брюера: распределённая система не может одновременно гарантировать все три свойства — Consistency (все узлы видят одинаковые данные), Availability (каждый запрос получает ответ), Partition tolerance (система работает при разрывах сети).
При сетевом разрыве (P случается всегда в распределёнке) приходится выбирать между C и A: CP-системы предпочитают согласованность (отказывают в ответе), AP — доступность (могут вернуть устаревшие данные).
Из практики: CAP — упрощение. Реально выбор не «или/или», а тонкая настройка уровней согласованности (quorum-чтение/запись).
Частые ошибки
Путают C в CAP (linearizability) с C в ACID (constraints).
Считают, что «выбрал AP — потерял согласованность навсегда» — есть eventual consistency.
Дополнительные вопросы
Что такое PACELC-теорема?
Какие БД относят к CP, какие к AP?
Что такое партиционирование? По каким принципам реализуется?
Middle
Партиционирование — разбиение большой таблицы на части (партиции) для ускорения запросов и упрощения обслуживания. Партиции хранятся отдельно, но логически — одна таблица.
Принципы: по диапазону (range — по дате), по списку значений (list — по региону), по хэшу (hash — равномерное распределение), композитные. В Postgres есть декларативное партиционирование, в MySQL — встроенное.
Из практики: больше всего пользы — на временных рядах (логи, события). DROP старой партиции занимает секунды, DELETE миллионов строк — часы.
Частые ошибки
Партиционируют по тому, что НЕ используется в WHERE — выигрыша нет.
Создают слишком много партиций — планировщик начинает тормозить.
Дополнительные вопросы
Чем партиционирование отличается от шардирования?
Как поддерживать индексы на партиционированных таблицах?
Что такое репликация? Какие виды?
Middle
Репликация — поддержание копий данных на нескольких узлах для отказоустойчивости и масштабирования чтения. Виды: master-slave (записи в один, чтения с многих), master-master (записи в любой), синхронная/асинхронная, потоковая/логическая.
Trade-off: синхронная даёт сильную согласованность, но снижает скорость записи и доступность. Асинхронная — быстрая, но реплики отстают (replication lag).
Из практики: read-replica для аналитических запросов экономит ресурс master'а. Но нужно помнить про lag — отчёт за «сегодня» может не увидеть данные последних секунд.
Частые ошибки
Используют асинхронную репликацию для критичных «прочитал-сразу-увидел» сценариев.
Не мониторят replication lag — обнаруживают проблему в инциденте.
Дополнительные вопросы
Что такое split-brain в master-master?
Чем логическая репликация отличается от физической?
Что такое шардирование? Чем отличается от репликации?
Middle
Шардирование — горизонтальное разбиение данных на независимые узлы (шарды) по ключу шардирования. Каждый шард содержит часть данных, не дублирующую другие.
Отличие от репликации: репликация делает копии данных (для отказоустойчивости/чтения), шардирование делит данные (для масштаба записи и объёма). Часто используются вместе: шарды реплицируются.
Из практики: выбор ключа шардирования — самое сложное. Плохой ключ ведёт к hotspots, дисбалансу, тяжёлым кросс-шард-запросам.
Частые ошибки
Шардируют слишком рано — добавляют сложность без необходимости.
Шардируют по auto-increment ID — все новые записи попадают на один шард.
Иногда выделяют отдельно DQL (Data Query) для SELECT. Это условное деление — в разных учебниках границы немного разные.
Из практики: понимание групп помогает в коде и в код-ревью. DDL внутри миграции — отдельно; DML — в приложении; DCL — у админов.
Частые ошибки
Путают TRUNCATE (DDL) и DELETE (DML) — у них разная семантика по транзакциям и триггерам.
Кладут DDL внутрь транзакции с DML — в некоторых СУБД auto-commit меняет поведение.
Дополнительные вопросы
К какой группе относится MERGE/UPSERT?
Чем INFORMATION_SCHEMA удобна?
Что такое JOIN? Какие виды JOIN существуют?
Junior
JOIN — операция соединения строк двух таблиц по условию. INNER JOIN — только совпадающие пары. LEFT JOIN — все строки слева, совпадения справа (несовпавшие — NULL). RIGHT JOIN — зеркально. FULL OUTER JOIN — все строки с обеих сторон. CROSS JOIN — декартово произведение.
Дополнительно: SELF JOIN (таблица сама с собой), NATURAL JOIN (по одноимённым колонкам — лучше избегать), USING (короткая запись ON).
Из практики: LEFT JOIN с проверкой `WHERE right.id IS NULL` — типовой способ найти «строки в одной таблице, которых нет в другой».
Частые ошибки
Используют CROSS JOIN неосознанно — получают взрыв строк.
Думают, что FULL JOIN есть везде — в MySQL до 8.x его не было.
Дополнительные вопросы
Чем JOIN отличается от UNION?
Когда оправдан hash join vs nested loop?
Чем отличается WHERE от HAVING?
Junior
WHERE фильтрует строки ДО группировки, HAVING — группы ПОСЛЕ агрегации. В WHERE нельзя использовать агрегатные функции (SUM, COUNT), в HAVING — можно.
Пример: `SELECT region, SUM(amount) FROM orders WHERE status='paid' GROUP BY region HAVING SUM(amount) > 1000`. WHERE отсекает строки с другим статусом, HAVING — регионы с маленькой суммой.
Из практики: ставить условие в HAVING вместо WHERE — типовой косяк по производительности. Если можно в WHERE — кладите туда: меньше строк дойдёт до агрегации.
Частые ошибки
Пишут `HAVING status='paid'` вместо WHERE — теряют производительность.
Пытаются использовать алиас агрегата в WHERE — не получится.
Дополнительные вопросы
Можно ли в HAVING ссылаться на колонку, не указанную в GROUP BY?
Что такое QUALIFY (в некоторых СУБД)?
Чем отличается TRUNCATE от DELETE и DROP?
Junior
DELETE — DML, удаляет строки по условию (можно с WHERE), пишет в журнал, срабатывают триггеры, обратимо в транзакции. TRUNCATE — DDL, удаляет все строки одной операцией без построчного журнала, обычно сбрасывает счётчики, может быть необратим. DROP — DDL, удаляет саму таблицу со схемой и данными.
По скорости: TRUNCATE > DELETE для больших таблиц. По безопасности: DELETE контролируем условием, DROP/TRUNCATE — массовые операции.
Из практики: в проде команды TRUNCATE и DROP — это всегда чек-лист и резервная копия. DELETE без WHERE опасен не меньше.
Частые ошибки
Запускают DELETE без WHERE.
Не учитывают, что TRUNCATE в некоторых СУБД не вызывает триггеры — нарушается аудит.
Дополнительные вопросы
Можно ли откатить TRUNCATE?
Чем DELETE отличается от soft-delete?
Чем отличается UNION от UNION ALL?
Junior
UNION объединяет результаты двух SELECT и убирает дубликаты строк (внутренне делает SORT/HASH). UNION ALL объединяет без проверки на дубли — поэтому быстрее.
Используйте UNION ALL, если знаете, что дубликатов не будет или они не мешают. UNION — только когда явно нужна дедупликация.
Из практики: UNION без необходимости — частая причина медленных отчётов. На больших объёмах разница может быть в разы.
Частые ошибки
Ставят UNION «на всякий случай» и теряют скорость.
Забывают, что у обоих SELECT должно совпадать число колонок и совместимые типы.
Дополнительные вопросы
Чем INTERSECT и EXCEPT отличаются от UNION?
Что произойдёт, если типы колонок не совпадут?
Как выбрать строки из таблицы 1, которых нет в таблице 2?
Middle
Четыре основных способа: LEFT JOIN + IS NULL (`SELECT a.* FROM a LEFT JOIN b ON a.id=b.a_id WHERE b.a_id IS NULL`), NOT EXISTS (`SELECT a.* FROM a WHERE NOT EXISTS (SELECT 1 FROM b WHERE b.a_id=a.id)`), NOT IN (опасен при NULL), EXCEPT/MINUS (в зависимости от СУБД).
Рекомендуется NOT EXISTS — он корректно обрабатывает NULL и обычно выбирает хороший план. LEFT JOIN + IS NULL — классика, тоже работает.
Из практики: NOT IN с подзапросом, в котором может быть NULL, — типовой источник «странных» результатов (возвращает пустоту).
Частые ошибки
Используют NOT IN с подзапросом, где есть NULL — получают пустой результат.
Забывают индекс на колонке соединения — запрос становится тяжёлым.
Дополнительные вопросы
Какой план обычно выбирает планировщик для NOT EXISTS vs LEFT JOIN?
Чем EXCEPT отличается от LEFT JOIN + IS NULL?
Что такое подзапрос (subquery) и CTE (WITH)?
Middle
Подзапрос — SELECT внутри другого SELECT/INSERT/UPDATE/DELETE. Бывает скалярный (одно значение), in-list, EXISTS, в FROM (derived table), коррелированный (зависит от внешнего запроса).
CTE (Common Table Expression, WITH) — именованный временный результат, на который можно ссылаться по имени. Улучшает читаемость многошаговых запросов и поддерживает рекурсию (WITH RECURSIVE).
Из практики: длинные вложенные подзапросы превращаю в CTE — резко падает порог входа для коллеги, читающего запрос завтра.
Частые ошибки
Считают, что CTE всегда быстрее подзапроса — это зависит от СУБД и плана.
Используют рекурсивный CTE без условия выхода.
Дополнительные вопросы
Чем коррелированный подзапрос отличается от обычного?
Как реализовать иерархию через рекурсивный CTE?
Что такое оконные функции? Приведи пример.
Middle
Оконные функции выполняют агрегацию по «окну» строк, не схлопывая их в одну. Синтаксис: `func() OVER (PARTITION BY ... ORDER BY ...)`. Примеры: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM/AVG OVER (..).
Пример — топ-3 заказа по клиенту: `SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY client_id ORDER BY amount DESC) AS rn FROM orders) WHERE rn <= 3`.
Из практики: оконные функции упрощают аналитические запросы, которые на «обычном SQL» требуют тяжёлых self-join или коррелированных подзапросов.
Частые ошибки
Путают RANK (с пропусками) и DENSE_RANK (без пропусков).
Не указывают ORDER BY в окне там, где порядок важен.
Дополнительные вопросы
Чем PARTITION BY отличается от GROUP BY?
Что такое frame clause (ROWS/RANGE BETWEEN)?
Как найти дубликаты в таблице?
Junior
Классика: `SELECT col1, col2, COUNT(*) FROM t GROUP BY col1, col2 HAVING COUNT(*) > 1`. Покажет, какие комбинации полей повторяются.
Для просмотра самих дублирующихся строк удобнее ROW_NUMBER: `SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY id) AS rn FROM t) WHERE rn > 1`.
Из практики: перед DELETE дублей обязательно нужен SELECT с теми же условиями — иначе можно случайно снести лишнее.
Частые ошибки
Сносят дубли DELETE без предварительного SELECT с точным условием.
Учитывают только часть колонок и считают «дублями» то, что ими не является.
Дополнительные вопросы
Как удалить дубли, сохранив самую старую запись?
Как предотвратить появление дублей (UNIQUE constraint)?