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

AleksandrMironov1993
03.03.2026 13:23Не знаю, что там с форматированием, но смысловая нагрузка и полезность хорошая. Даже ссылок на всякие курсы нет!
апд. Ах, чертова дюжина, все таки есть, не дочитал до конца прям.
Я когда открывал - был уверен, что будет.
От себя добавлю - знать базовый синтаксис, конечно, хорошо. Но в работе ещё очень важно уметь ориентироваться в базе, зависимостями между таблицами, ограничениями и тд и тп. Для джуна это наверно не сильно важно, но это очень полезный навык, тк при подготовке данных(если у вас будет все повязано на бд, ну нет апишки допустим) - львиную часть времени убьете на то, чтобы залить согласованные между собой данные
А так, ещё раз, автору респект

Akina
03.03.2026 13:23Любое сравнение с NULL даёт NULL (не TRUE и не FALSE).
Ну-ну... а, между прочим, СУБД у вас в тегах не указана. Так что берём, значит, MySQL (можно и MariaDB), спрашиваем
SELECT NULL <=> NULL AS compare_two_nulls;и получаем единичку, а вовсе даже не обещанный NULL.
Эту задачу дают постоянно — выучите наизусть.
WHERE NOT EXISTSкак минимум не хуже, чемLEFT JOIN WHERE IS NULL. А порой - намного эффективнее.Задача с собеседования: «Кто потратил больше 10 000₽?»
Незачёт. Вас спросили кто, но не спрашивали сколько.
На собеседовании лучше показать оба способа и объяснить разницу — это покажет глубину понимания.
На собеседовании надо не показывать оба способа (к слову, их куда как больше, чем два), а уточнить задачу, и только после снятия всех неоднозначностей начинать написание запроса.
То же относится и к Задача 4: «Топ-3 покупателя»
Найдите email, которые встречаются больше одного раза.
Решение вообще-то как минимум спорное. Если email может дублироваться, то не вижу, почему не может дублироваться и набор естественных идентифицирующих полей. Различие будет только в значении первичного синтетического ключа.
И это не баг, как вы пишете, а ошибка проектирования. Если email должен быть уникален, то должен существовать соответствующий constraint. А при его существовании обнаружение дублирования уже не просто не баг, а разрушение таблицы данных.
Найдите сумму второго по величине заказа.
Решение - ошибочное. Первые два заказа могут иметь одинаковую сумму.
Задача 3: «Пользователи без заказов за последний месяц»
Найдите пользователей, которые зарегистрированы больше месяца назад, но не сделали ни одного заказа.
Заголовок и условие - это две разные задачи. Зарегистрировался 3 месяца назад, сделал последнюю покупку 2 месяца назад - соответствует условию в названии и не соответствует условию в тексте.
Правило простое: всё, что не внутри агрегатной функции (COUNT, SUM...) — должно быть в GROUP BY.
В некоторых диалектах (и таких становится всё больше) группировка по первичному ключу делает необязательным указание в выражении группировки других полей той же таблицы, даже если они не агрегируются в списке вывода.
Но SQL выполняет в другом порядке:
Я понимаю, что полный порядок формального выполнения запроса вы не нашли - его просто пока вроде бы никто не озаботился опубликовать. Но можно было бы найти и скопипастить хотя бы наиболее полный из имеющихся.
Middle — то, что отличает от джуна
Всё, что ниже - это ещё джун. Middle - это как минимум табличные и оконные функции, хранимые объекты, латеральные запросы и рекурсивные CTE. А по-хорошему - уровни изоляции, блокировки, репликация, хотя бы на уровне понимания основ.

Moroshka
03.03.2026 13:23Middle - это как минимум табличные и оконные функции, хранимые объекты, латеральные запросы и рекурсивные CTE. А по-хорошему - уровни изоляции, блокировки, репликация, хотя бы на уровне понимания основ.
А поделитесь, пожалуйста, примерами ситуаций, когда тестировщику миддлу могут потребоваться эти знания?

Akina
03.03.2026 13:23Вот знаете... если человек освоил выборку и фильтрацию, но ни уха ни рыла в группировке - то практически ничего он и не освоил. Это вообще самые основные основы. Или вы всерьёз полагаете, что на проверке валидности данных в таблице БД работа тестировщика заканчивается? А такой фигнёй как "выбрать топ-3 из имеющихся данных" тестировщик имхо вообще не должен заниматься. Я даже представить не могу, как подобная задача коррелирует хоть с каким-то тестированием, только если это не тестирование кандидата на знание основ SQL.
Между тем, что перечислено в статье, и тем, что перечислил я, есть вполне зримая квалификационная граница. А между двумя списками, что в статье, имхо границы нет вообще. Разница между ними лишь в том, дочитан ли букварь по SQL до конца или заброшен на середине.

AleksandrMironov1993
03.03.2026 13:23вот мне тоже любопытно, это что за мидл который отлично знает все перечисленное
из личного опыта - СТЕшки использовал да, чтобы вложенность удобно расписать. все. оконки мб использовал, но редко. даже разрабы их за два года работы в сервисе, где все на бд оракловой повязано в хранимых процедурах использовали один раз за все время. и то нужно было для историчности. в других местах историчность совсем другими способами реализуют.
функциями опять же не пользовался
вы уверены, что это не про аналитика или про разраба? а то похоже
Akina
Форматирование - вообще никакое. Неужели трудно посмотреть, что публикуете?
Нечитабельно.
interstels Автор
Поправил