База для тестировщика это второй источник правды после API. На собесе на QA почти всегда есть пара SQL-вопросов, а иногда и задачи: «найди дубли», «найди заказы без оплаты». Здесь разберём запросы, которые реально пишут в работе.
Зачем лезть в базу
- Проверить результат. API вернуло 201, а в базе не тот статус или сумма.
- Найти дефекты в данных. Дубли, сироты, расхождения сумм.
- Подготовить данные. Заказ, который «висит в оплате третий день», через интерфейс не создашь.
- Разобрать баг. По времени и id видно, на каком шаге данные испортились.
Проверка после запроса
SELECT email, name, status, created_at
FROM users
WHERE id = 57;
Конкретная запись по ключу и конкретные поля. SELECT * по всей таблице ничего не проверяет, а count(*) соврёт при параллельных тестах.
NULL
NULL значит «неизвестно», и сравнение с ним через = даёт unknown, а не true. WHERE такие строки отбрасывает.
WHERE deleted_at IS NULL -- правильно
WHERE deleted_at = NULL -- всегда 0 строк, тест молча проходит
Ещё две ловушки: x NOT IN (подзапрос), где в подзапросе есть NULL, всегда пуст. SUM по пустому набору даёт NULL, поэтому сравнение total <> SUM(...) для заказа без позиций тоже вернёт unknown. Лечится COALESCE(SUM(...), 0).
Типовые проверки
Дубли. Условие на агрегат пишется в HAVING, не в WHERE.
SELECT lower(email) AS email, count(*) AS cnt
FROM users
GROUP BY lower(email)
HAVING count(*) > 1;
Сироты (ссылка в пустоту). LEFT JOIN сохраняет все строки слева, у ненайденных справа колонки равны NULL.
SELECT o.id
FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE o.user_id IS NOT NULL AND u.id IS NULL;
С INNER JOIN этот запрос всегда пуст: он выбрасывает именно те строки, которые мы ищем. Аналог через NOT EXISTS безопасен к NULL.
Отсутствие связанного. Оплаченный заказ без успешного платежа:
SELECT o.id
FROM orders o
WHERE o.status IN ('paid', 'shipped')
AND NOT EXISTS (
SELECT 1 FROM payments p
WHERE p.order_id = o.id AND p.status = 'success'
);
Если писать через LEFT JOIN, условие на статус платежа ставят в ON. В WHERE оно отфильтрует заказы без платежей, и дефект не найдётся.
Сходятся ли суммы. Итог заказа против позиций, оплаты против итога. Для частичных сумм удобен SUM(...) FILTER (WHERE ...).
Логика статусов и дат. updated_at < created_at, удалённый, но активный пользователь, отгруженный заказ без оплаты.
Последнее по группе. Статус последнего платежа по каждому заказу: ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC) и фильтр rn = 1, или в PostgreSQL DISTINCT ON.
Транзакции и тестовые данные
ACID: атомарность, согласованность, изоляция, надёжность. Для тестов важно:
- Незафиксированные изменения видит только своё соединение. Если тест вставил строку и не сделал
COMMIT, API её не увидит. В PostgreSQL грязного чтения нет вообще. BEGIN ... ROLLBACKбесследно убирает изменения, но работает, только если тест и проверка в одном соединении.- Для API-тестов данные лучше создавать через API или фикстуры с уникальными метками и удалять по ним. Удаление «всего за последний час» снесёт чужие данные на общем стенде.
- Полная изоляция: своя база в контейнере на прогон (Testcontainers) с миграциями и сидами.
Ограничения и миграции
Проверка «email уже занят» в коде не спасает от двух параллельных регистраций. Защищает UNIQUE в базе, ссылки держит FOREIGN KEY, правила CHECK и NOT NULL. Отсутствие ограничений на критичных полях стоит поднимать как риск.
Миграцию проверяют не на пустой базе, а на данных, похожих на прод: что будет с существующими NULL, сколько займёт, не заблокирует ли таблицу, как откатить.
Безопасность и этика
На общих стендах пишите только в свои данные. В проде, если доступ вообще есть, только чтение. Копию прода для тестов берут обезличенной: персональные данные в тестовой среде нарушают 152-ФЗ.
Что спросят на собесе
- Зачем тестировщику SQL? Приведите пример запроса из работы.
- Чем
LEFT JOINотличается отINNER JOIN? - Как найти дубли? Чем
WHEREотличается отHAVING? - Почему
= NULLне работает и чем опасенNOT IN? - Как вы готовите и убираете тестовые данные?