SQL для QA: что реально спрашивают на собеседованиях
SQL для QA: что реально спрашивают на собеседованиях
Минимум SQL, который должен знать каждый тестировщик — с реальными задачами и ловушками

Привет, Хабр! Это продолжение серии про QA собеседования. Уже разобрали тест-дизайн, API и Security и System Design. Теперь — SQL.
Честно: на собеседованиях SQL задают чаще, чем многие ожидают. Не уровня DBA, но и не SELECT * FROM users. Обычно дают таблицу и просят написать запрос прямо на доске или в Google Docs. Если впадаете в ступор при слове JOIN — эта статья для вас.
Зачем QA вообще SQL?
Потому что тестировать через UI — это смотреть на айсберг сверху. А баги живут под водой — в базе данных.
Вот реальная ситуация. Тестировщик создал заказ через UI, проверил — на экране всё красиво: статус «Оплачен», сумма 5000₽. Отлично, тест прошёл. А потом в production клиент жалуется, что ему списали 5000₽ дважды. Оказалось, в базе создались две записи в таблице payments — баг в бэкенде. UI показывал только последнюю. Если бы тестировщик после создания заказа заглянул в базу — поймал бы сразу.
SQL нужен QA для трёх вещей:
- Верификация данных. UI показывает одно — в базе может быть другое.
SELECTпосле каждого действия — привычка хорошего тестировщика. - Подготовка тестовых данных. Нужно 100 пользователей для нагрузочного теста? Руками через UI — день работы. Один
INSERT— секунда. - Поиск причины бага. «Пользователь не видит свои заказы» — это проблема в UI, в API, или данные в базе кривые? Один запрос — и понятно, куда копать.
SELECT — получаем данные
Начнём с того, что знают все. Но я видел людей, которые путаются даже тут, так что пробежимся быстро.
Допустим, у нас таблица users:
| id | name | age | city | created_at | |
|---|---|---|---|---|---|
| 1 | Atajan | atajan@mail.com | 30 | Ashgabat | 2025-01-15 |
| 2 | Maria | maria@mail.com | 25 | Moscow | 2025-03-20 |
| 3 | John | john@mail.com | 35 | Istanbul | 2025-06-10 |
| 4 | Anna | anna@mail.com | 22 | Moscow | 2025-09-01 |
| 5 | Kemal | kemal@mail.com | 28 | Ashgabat | 2026-01-05 |
Получить все данные
SELECT * FROM users;
Звёздочка * — «дай все столбцы». Вернёт всю таблицу целиком (все 5 строк, все 6 столбцов).
Выбрать конкретные столбцы
SELECT name, email FROM users;
| name | |
|---|---|
| Atajan | atajan@mail.com |
| Maria | maria@mail.com |
| John | john@mail.com |
| Anna | anna@mail.com |
| Kemal | kemal@mail.com |
На практике SELECT * используют для быстрой проверки. В тест-скриптах лучше указывать конкретные столбцы — быстрее и понятнее.
WHERE — фильтрация
WHERE — это фильтр. «Дай мне только те строки, где условие истинно.»
SELECT * FROM users WHERE city = 'Moscow';
| id | name | age | city |
|---|---|---|---|
| 2 | Maria | 25 | Moscow |
| 4 | Anna | 22 | Moscow |
AND, OR — комбинация условий
-- Москва И старше 23
SELECT * FROM users WHERE city = 'Moscow' AND age > 23;
Результат: только Maria (25 лет). Anna (22) не прошла по возрасту.
-- Москва ИЛИ Стамбул
SELECT * FROM users WHERE city = 'Moscow' OR city = 'Istanbul';
Результат: Maria, John, Anna.
IN — вместо кучи OR
SELECT * FROM users WHERE city IN ('Moscow', 'Istanbul');
Делает то же самое, что city = 'Moscow' OR city = 'Istanbul', но короче и читабельнее. Особенно когда значений 5-10.
BETWEEN — диапазон
SELECT * FROM users WHERE age BETWEEN 25 AND 35;
| name | age |
|---|---|
| Atajan | 30 |
| Maria | 25 |
| John | 35 |
| Kemal | 28 |
BETWEEN включает обе границы: 25 и 35 попадают в результат. Аналог: age >= 25 AND age <= 35.
LIKE — поиск по шаблону
SELECT * FROM users WHERE email LIKE '%@mail.com';
% — любое количество любых символов. %@mail.com — «что угодно, заканчивающееся на @mail.com». Вернёт всех пятерых.
Другие примеры:
LIKE 'A%'— начинается на A (Atajan, Anna)LIKE '%an%'— содержит «an» (Atajan, Anna)LIKE '_ohn'— 4 символа, заканчивается на «ohn» (John)._= ровно один символ
IS NULL — проверка на пустоту
SELECT * FROM users WHERE city IS NULL;
В нашей таблице у всех есть город, поэтому вернёт 0 строк. Но на реальных данных NULL встречается постоянно — незаполненные поля, удалённые связи.
Ловушка на собеседовании: WHERE city = NULL — не работает! NULL — это не значение, а отсутствие значения. Его нельзя сравнить через =. Любое сравнение с NULL даёт NULL (не TRUE и не FALSE).
-- Неправильно (вернёт 0 строк, даже если NULL есть)
SELECT * FROM users WHERE city = NULL;
-- Правильно
SELECT * FROM users WHERE city IS NULL;
SELECT * FROM users WHERE city IS NOT NULL;
ORDER BY, LIMIT, DISTINCT
ORDER BY — сортировка
SELECT * FROM users ORDER BY age DESC;
ASC — по возрастанию (по умолчанию), DESC — по убыванию. Результат: John (35), Atajan (30), Kemal (28), Maria (25), Anna (22).
LIMIT — ограничить количество
SELECT * FROM users ORDER BY age ASC LIMIT 3;
Результат: три самых молодых — Anna (22), Maria (25), Kemal (28).
DISTINCT — уникальные значения
SELECT DISTINCT city FROM users;
Результат: Ashgabat, Moscow, Istanbul (3 уникальных города, без дублей).
SELECT COUNT(DISTINCT city) FROM users;
Результат: 3. Полезно для быстрой проверки: «сколько уникальных значений в столбце?»
JOIN — главный вопрос собеседования
JOIN спрашивают в 80% случаев. Не потому что это сложно, а потому что кандидаты путают типы.
Добавим вторую таблицу orders:
| id | user_id | product | amount | status |
|---|---|---|---|---|
| 1 | 1 | Laptop | 50000 | paid |
| 2 | 1 | Mouse | 2000 | paid |
| 3 | 2 | Keyboard | 3000 | cancelled |
| 4 | 3 | Monitor | 25000 | paid |
| 5 | 99 | Headphones | 5000 | paid |
Обратите внимание: user_id = 99 не существует в таблице users, а пользователи Anna (4) и Kemal (5) не делали заказов.
INNER JOIN — только совпадения
SELECT users.name, orders.product, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
| name | product | amount |
|---|---|---|
| Atajan | Laptop | 50000 |
| Atajan | Mouse | 2000 |
| Maria | Keyboard | 3000 |
| John | Monitor | 25000 |
Anna и Kemal — не попали (нет заказов). Заказ с user_id = 99 — тоже не попал (нет такого пользователя).
LEFT JOIN — все из левой + совпадения из правой
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
| name | product |
|---|---|
| Atajan | Laptop |
| Atajan | Mouse |
| Maria | Keyboard |
| John | Monitor |
| Anna | NULL |
| Kemal | NULL |
Anna и Kemal попали с NULL — они есть в users, но заказов нет.
RIGHT JOIN — все из правой + совпадения из левой
SELECT users.name, orders.product
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;
| name | product |
|---|---|
| Atajan | Laptop |
| Atajan | Mouse |
| Maria | Keyboard |
| John | Monitor |
| NULL | Headphones |
Headphones попал с NULL — заказ есть, но user_id = 99 не существует.
FULL JOIN — всё из обеих таблиц
Комбинация LEFT и RIGHT: все пользователи + все заказы, даже если нет совпадений.
Шпаргалка
INNER JOIN = A ∩ B (только совпадения)
LEFT JOIN = A + (A ∩ B) (всё из левой)
RIGHT JOIN = B + (A ∩ B) (всё из правой)
FULL JOIN = A ∪ B (вообще всё)
Задача с собеседования: «Найди пользователей, которые не сделали ни одного заказа.»
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;
Результат: Anna, Kemal. LEFT JOIN даёт NULL для тех, у кого нет заказов, а WHERE отфильтровывает только их. Эту задачу дают постоянно — выучите наизусть.
Агрегатные функции
| Функция | Что делает | Пример |
|---|---|---|
| COUNT() | Считает строки | SELECT COUNT(*) FROM users; → 5 |
| SUM() | Сумма | SELECT SUM(amount) FROM orders; |
| AVG() | Среднее | SELECT AVG(age) FROM users; |
| MAX() | Максимум | SELECT MAX(amount) FROM orders; |
| MIN() | Минимум | SELECT MIN(age) FROM users; |
Ловушка: COUNT(*) считает все строки, включая NULL. COUNT(column) — только не-NULL значения. На собеседовании могут спросить разницу.
-- Сколько заказов у каждого пользователя
SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.name;
| name | order_count |
|---|---|
| Atajan | 2 |
| Maria | 1 |
| John | 1 |
| Anna | 0 |
| Kemal | 0 |
GROUP BY и HAVING
GROUP BY группирует строки, HAVING фильтрует группы. Главное отличие от WHERE: WHERE фильтрует строки до группировки, HAVING — после.
-- Города с более чем одним пользователем
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city
HAVING COUNT(*) > 1;
| city | user_count |
|---|---|
| Ashgabat | 2 |
| Moscow | 2 |
Задача с собеседования: «Найди пользователей, которые потратили больше 10 000₽.»
SELECT users.name, SUM(orders.amount) AS total_spent
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'paid'
GROUP BY users.name
HAVING SUM(orders.amount) > 10000;
| name | total_spent |
|---|---|
| Atajan | 52000 |
| John | 25000 |
Обратите внимание: Maria отфильтровалась, потому что её заказ cancelled, а WHERE отсёк его до группировки.
Подзапросы
Подзапрос — это запрос внутри запроса. Иногда без них не обойтись.
-- Пользователи, чей возраст выше среднего
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);
| name | age |
|---|---|
| Atajan | 30 |
| John | 35 |
Средний возраст = 28. Atajan (30) и John (35) — выше.
-- Пользователи, у которых есть хотя бы один оплаченный заказ
SELECT name
FROM users
WHERE id IN (
SELECT DISTINCT user_id
FROM orders
WHERE status = 'paid'
);
Результат: Atajan, John.
Задача с собеседования: «Найди товар с максимальной суммой заказа.»
-- Способ 1: подзапрос
SELECT product, amount
FROM orders
WHERE amount = (SELECT MAX(amount) FROM orders);
-- Способ 2: ORDER BY + LIMIT
SELECT product, amount
FROM orders
ORDER BY amount DESC
LIMIT 1;
Оба вернут: Laptop, 50000. Но если максимум не уникален — первый способ вернёт все записи, второй — только одну.
Задачи с реальных собеседований
Задача 1: «Дублирующиеся email»
Найдите email, которые встречаются больше одного раза.
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Зачем это QA? Проверяем, что система правильно блокирует регистрацию с дублем email. Если запрос возвращает строки — баг.
Задача 2: «Второй по величине заказ»
Найдите сумму второго по величине заказа.
SELECT DISTINCT amount
FROM orders
ORDER BY amount DESC
LIMIT 1 OFFSET 1;
Результат: 25000 (после Laptop за 50000). OFFSET 1 пропускает первую строку.
Задача 3: «Пользователи без заказов за последний месяц»
Найдите пользователей, которые зарегистрированы больше месяца назад, но не сделали ни одного заказа.
SELECT u.name, u.email, u.created_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL
AND u.created_at < NOW() - INTERVAL '1 month';
Зачем это QA? Например, тестируем email-рассылку «Вы давно не заказывали» — нужно убедиться, что она уходит именно этим пользователям.
Задача 4: «Топ-3 покупателя»
Найдите 3 покупателей с наибольшей суммой оплаченных заказов.
SELECT u.name, SUM(o.amount) AS total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY u.name
ORDER BY total DESC
LIMIT 3;
Задача 5: «Конверсия по городам»
Для каждого города посчитайте: сколько пользователей, сколько из них сделали хотя бы один заказ, и процент конверсии.
SELECT
u.city,
COUNT(DISTINCT u.id) AS total_users,
COUNT(DISTINCT o.user_id) AS buyers,
ROUND(
COUNT(DISTINCT o.user_id) * 100.0 / COUNT(DISTINCT u.id), 1
) AS conversion_pct
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.city;
| city | total_users | buyers | conversion_pct |
|---|---|---|---|
| Ashgabat | 2 | 1 | 50.0 |
| Moscow | 2 | 1 | 50.0 |
| Istanbul | 1 | 1 | 100.0 |
Это уже middle+ уровень. Если решите на собеседовании — впечатлите.
UPDATE и DELETE — с осторожностью
На собеседовании иногда просят написать UPDATE или DELETE. Главное правило: всегда WHERE. DELETE FROM users без WHERE удалит всех пользователей. На production это катастрофа.
-- Обновить город пользователя
UPDATE users SET city = 'Istanbul' WHERE id = 1;
-- Удалить отменённые заказы старше года
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
Совет с собеседования: перед DELETE/UPDATE сначала напишите SELECT с тем же WHERE — убедитесь, что затронете именно нужные строки.
-- Сначала проверяем
SELECT * FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
-- Убедились — удаляем
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
5 ловушек, на которых валятся кандидаты
1. NULL — не значение, а отсутствие значения.
-- Неправильно
SELECT * FROM users WHERE city = NULL;
-- Правильно
SELECT * FROM users WHERE city IS NULL;
NULL = NULL — это не TRUE. Это NULL. Любая операция с NULL даёт NULL.
2. COUNT(*) vs COUNT(column).
COUNT(*) — считает все строки. COUNT(city) — только те, где city не NULL.
3. GROUP BY — все неагрегированные столбцы.
-- Ошибка: name не в GROUP BY и не в агрегатной функции
SELECT name, city, COUNT(*)
FROM users
GROUP BY city;
-- Правильно
SELECT city, COUNT(*)
FROM users
GROUP BY city;
4. WHERE vs HAVING.
WHERE фильтрует строки до группировки. HAVING — после. Нельзя использовать агрегатные функции в WHERE.
-- Ошибка
SELECT city, COUNT(*) FROM users WHERE COUNT(*) > 1 GROUP BY city;
-- Правильно
SELECT city, COUNT(*) FROM users GROUP BY city HAVING COUNT(*) > 1;
5. Порядок выполнения SQL — не тот, что вы пишете.
Пишете: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
Выполняется: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
Поэтому в WHERE нельзя использовать алиасы из SELECT — они ещё не существуют.
Чек-лист: что знать перед собеседованием
- SELECT, WHERE, AND/OR, IN, BETWEEN, LIKE, IS NULL — базовый синтаксис
- ORDER BY, LIMIT, OFFSET — сортировка и пагинация
- DISTINCT — уникальные значения
- JOIN — INNER, LEFT, RIGHT, FULL (и когда какой)
- «Пользователи без заказов» — LEFT JOIN + WHERE IS NULL
- COUNT, SUM, AVG, MAX, MIN — агрегатные функции
- GROUP BY + HAVING — группировка и фильтрация групп
- Подзапросы — WHERE IN (SELECT …) и скалярные
- UPDATE и DELETE — всегда с WHERE, сначала SELECT
- NULL — IS NULL, не = NULL
- COUNT(*) vs COUNT(column)
- Порядок выполнения SQL
Этого хватит для 90% QA собеседований. Не нужно знать оконные функции, CTE или хранимые процедуры — это уже для DBA.
Как практиковаться
- SQLBolt — интерактивные уроки с нуля. Бесплатно. 15 минут в день — за неделю освоите базу.
- LeetCode (Database) — задачи уровня собеседований. Начните с Easy.
- HackerRank SQL — задачи с проверкой. Хорошая подборка для начинающих.
- Ваш рабочий проект. Если есть доступ к тестовой БД — пишите запросы к реальным данным. Это лучшая практика.
Курс по тестированию с практическими заданиями — бесплатно на annayev.com (English, Русский, Türkçe).
Ставьте плюс, если было полезно. Какие SQL-задачи вам давали на собеседованиях? Пишите в комментариях.