SQL для QA: 10 задач, которые реально дают на собеседованиях
Содержание
Исходные данные для всех примеров
Во всех примерах ниже используем две таблицы. Запомните их — они будут возвращаться снова и снова.
Таблица users:
|
id |
name |
|
age |
city |
created_at |
|---|---|---|---|---|---|
|
1 |
Test_user |
30 |
Ashgabat |
2025-01-15 |
|
|
2 |
Maria |
25 |
Moscow |
2025-03-20 |
|
|
3 |
John |
35 |
Istanbul |
2025-06-10 |
|
|
4 |
Anna |
22 |
Moscow |
2025-09-01 |
|
|
5 |
Kemal |
28 |
Ashgabat |
2026-01-05 |
Таблица 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) не делали заказов. Это важно для понимания JOIN-ов.
1. Зачем QA вообще SQL?
Потому что тестировать через UI — это смотреть на айсберг сверху. А баги живут под водой — в базе данных.
Реальный пример: тестировщик создал заказ через UI, статус «Оплачен», сумма 5000₽. Всё ок. В проде клиент жалуется, что списали дважды: в таблице payments две записи — баг в бэкенде. UI показал только одну. Если бы QA заглянул в базу — поймал бы сразу.
|
# |
Зачем |
Пример |
|---|---|---|
|
1 |
Верификация данных |
UI «Оплачен» — в БД |
|
2 |
Подготовка тестовых данных |
100 пользователей через UI — день. Один |
|
3 |
Поиск причины бага |
«Не видит заказы» — UI, API или данные кривые? Один запрос — и ясно |
2. SELECT — получаем данные
Начнём с базового, но без путаницы.
2.1 Получить все данные
SELECT * FROM users;
2.2 Выбрать конкретные столбцы
SELECT name, email FROM users;
Результат:
|
name |
|
|---|---|
|
Atajan |
|
|
Maria |
|
|
John |
|
|
Anna |
|
|
Kemal |
2.3 WHERE — фильтрация
SELECT * FROM users WHERE city = 'Moscow';
Результат:
|
id |
name |
age |
city |
|---|---|---|---|
|
2 |
Maria |
25 |
Moscow |
|
4 |
Anna |
22 |
Moscow |
2.4 AND, OR — комбинация условий
-- Москва И старше 23
SELECT * FROM users WHERE city = 'Moscow' AND age > 23;
-- Москва ИЛИ Стамбул
SELECT * FROM users WHERE city = 'Moscow' OR city = 'Istanbul';
2.5 IN — вместо кучи OR
SELECT * FROM users WHERE city IN ('Moscow', 'Istanbul');
2.6 BETWEEN — диапазон
SELECT * FROM users WHERE age BETWEEN 25 AND 35;
2.7 LIKE — поиск по шаблону
SELECT * FROM users WHERE email LIKE '%@mail.com';
-
LIKE 'A%'— начинается на A -
LIKE '%an%'— содержит «an» -
LIKE '_ohn'— 4 символа, заканчивается на «ohn»
2.8 IS NULL — проверка на пустоту
SELECT * FROM users WHERE city IS NULL;
Ловушка: WHERE city = NULL не работает. Используйте IS NULL / IS NOT NULL.
3. ORDER BY, LIMIT, DISTINCT
SELECT * FROM users ORDER BY age DESC;
SELECT * FROM users ORDER BY age ASC LIMIT 3;
SELECT DISTINCT city FROM users;
SELECT COUNT(DISTINCT city) FROM users;
4. JOIN — главный вопрос собеседования
JOIN спрашивают в 80% случаев, потому что часто путают типы.
4.1 INNER JOIN — только совпадения
SELECT users.name, orders.product, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
4.2 LEFT JOIN — все из левой + совпадения из правой
SELECT users.name, orders.product
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
4.3 RIGHT JOIN — все из правой + совпадения из левой
SELECT users.name, orders.product
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;
4.4 FULL JOIN — всё из обеих таблиц
Комбинация LEFT и RIGHT: все пользователи + все заказы, даже если нет совпадений.
4.5 Шпаргалка по JOIN
|
Тип |
Формула |
Простым языком |
|---|---|---|
|
INNER JOIN |
A ∩ B |
Только совпадения |
|
LEFT JOIN |
A + (A ∩ B) |
Всё из левой таблицы |
|
RIGHT JOIN |
B + (A ∩ B) |
Всё из правой таблицы |
|
FULL JOIN |
A ∪ B |
Вообще всё |
4.6 Задача: «Пользователи без заказов»
SELECT users.name
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.id IS NULL;
5. Агрегатные функции
|
Функция |
Что делает |
Пример |
|---|---|---|
|
|
Считает строки |
|
|
|
Сумма |
|
|
|
Среднее |
|
|
|
Максимум |
|
|
|
Минимум |
|
Сколько заказов у каждого пользователя:
SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.name;
6. GROUP BY и HAVING
GROUP BY группирует строки, HAVING фильтрует группы. WHERE — до группировки, HAVING — после.
-- Города с более чем одним пользователем
SELECT city, COUNT(*) AS user_count
FROM users
GROUP BY city
HAVING COUNT(*) > 1;
Задача: «Кто потратил больше 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;
7. Подзапросы
7.1 Скалярный подзапрос
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);
7.2 Подзапрос с IN
SELECT name
FROM users
WHERE id IN (
SELECT DISTINCT user_id
FROM orders
WHERE status = 'paid'
);
7.3 «Товар с максимальной суммой»
-- Способ 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;
8. Задачи с реальных собеседований
Задача 1: «Дублирующиеся email»
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Задача 2: «Второй по величине заказ»
SELECT DISTINCT amount
FROM orders
ORDER BY amount DESC
LIMIT 1 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';
Задача 4: «Топ-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: «Конверсия по городам» (middle+)
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;
9. UPDATE и DELETE — с осторожностью
Главное правило: всегда WHERE. Без него обновите/удалите всё.
9.1 UPDATE — обновить данные
UPDATE users SET city = 'Istanbul' WHERE id = 1;
9.2 DELETE — удалить строки
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
9.3 Золотое правило: SELECT перед DELETE/UPDATE
-- Шаг 1: посмотреть, что затронет условие
SELECT * FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
-- Шаг 2: выполнить удаление тем же WHERE
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year';
10. 5 ловушек, на которых валятся кандидаты
Ловушка #1: NULL — это не значение
-- Неправильно
SELECT * FROM users WHERE city = NULL;
-- Правильно
SELECT * FROM users WHERE city IS NULL;
Любая операция с NULL даёт NULL. Сравнивать нужно через IS NULL.
Ловушка #2: COUNT(*) vs COUNT(column)
SELECT COUNT(*) FROM users; -- 5
SELECT COUNT(phone) FROM users; -- 3 если 2 NULL
Ловушка #3: GROUP BY — забыли столбец
-- Ошибка: name не в GROUP BY и не в агрегате
SELECT name, city, COUNT(*)
FROM users
GROUP BY city;
-- Исправления:
SELECT city, COUNT(*) FROM users GROUP BY city;
-- или
SELECT name, city, COUNT(*) FROM users GROUP BY city, name;
Ловушка #4: WHERE vs 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
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Поэтому алиас из SELECT недоступен в WHERE.
-- Ошибка
SELECT city, COUNT(*) AS cnt
FROM users
WHERE cnt > 1
GROUP BY city;
-- Правильно
SELECT city, COUNT(*) AS cnt
FROM users
GROUP BY city
HAVING COUNT(*) > 1;
11. Чек-лист перед собеседованием
Junior — минимум, без которого не возьмут
|
Тема |
Проверь себя |
|---|---|
|
SELECT, WHERE |
Запрос с AND, OR, IN, BETWEEN, LIKE |
|
ORDER BY, LIMIT |
Топ-5 самых дорогих заказов |
|
DISTINCT |
Сколько уникальных городов? |
|
NULL |
Почему |
|
INNER JOIN |
Соедини users и orders, объясни результат |
|
LEFT JOIN |
Найди пользователей без заказов |
Middle — то, что отличает от джуна
|
Тема |
Проверь себя |
|---|---|
|
Агрегатные функции |
COUNT, SUM, AVG, MAX, MIN |
|
GROUP BY + HAVING |
Города с >1 пользователем; сумма > 10 000₽ |
|
Подзапросы |
Пользователи старше среднего возраста |
|
WHERE vs HAVING |
Когда что использовать? |
|
COUNT(*) vs COUNT(col) |
Что если в столбце NULL? |
|
UPDATE, DELETE |
Почему сначала SELECT, потом DELETE? |
|
Порядок выполнения |
Почему алиас из SELECT нельзя в WHERE? |
Этого хватит для 90% QA собеседований. Оконные функции, CTE, хранимые процедуры — это уже DBA-территория.
12. Как практиковаться
|
Ресурс |
Что там |
Цена |
|---|---|---|
|
Интерактивные уроки с нуля. 15 мин/день — за неделю база |
Бесплатно |
|
|
Задачи уровня собеседований. Начните с Easy |
Бесплатно |
|
|
Задачи с проверкой. Хорошая подборка для начинающих |
Бесплатно |
|
|
Ваш рабочий проект |
Запросы к реальной тестовой БД — лучшая практика |
— |
Курс по тестированию с практическими заданиями — бесплатно на annayev.com (English, Русский, Türkçe).
Ставьте плюс, если было полезно. Какие SQL-задачи вам давали на собеседованиях? Пишите в комментариях — возможно, добавлю разбор.
Автор: interstels

