Содержание
Исходные данные для всех примеров
Во всех примерах ниже используем две таблицы. Запомните их — они будут возвращаться снова и снова.
Таблица 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%'— начинается на ALIKE '%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-задачи вам давали на собеседованиях? Пишите в комментариях — возможно, добавлю разбор.
