SQL для QA: 10 задач, которые реально дают на собеседованиях

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет, Хабр! Это четвёртая статья из серии про QA собеседования. Уже разобрали тест-дизайн, API и Security и System Design. Теперь — SQL. На собеседованиях SQL задают чаще, чем многие ожидают. Не уровня DBA, но и не SELECT * FROM users. Обычно дают таблицу и просят написать запрос прямо на доске или в Google Docs. Если впадаете в ступор при слове JOIN — эта статья для вас.

Содержание

  1. Зачем QA вообще SQL?

  2. SELECT, WHERE и базовый синтаксис

  3. ORDER BY, LIMIT, DISTINCT

  4. JOIN — главный вопрос собеседования

  5. Агрегатные функции

  6. GROUP BY и HAVING

  7. Подзапросы

  8. Задачи с реальных собеседований

  9. UPDATE и DELETE — с осторожностью

  10. 5 ловушек, на которых валятся кандидаты

  11. Чек-лист перед собеседованием

  12. Как практиковаться


Исходные данные для всех примеров

Во всех примерах ниже используем две таблицы. Запомните их — они будут возвращаться снова и снова. Таблица users:

id

name

email

age

city

created_at

1

Test_user

Test_user@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

Таблица 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₽. Тест прошёл. А потом в production клиент жалуется, что ему списали 5000₽ дважды. Оказалось, в базе создались две записи в таблице payments — баг в бэкенде. UI показывал только последнюю. Если бы тестировщик заглянул в базу — поймал бы сразу. SQL нужен QA для трёх вещей:

#

Зачем

Пример

1

Верификация данных

UI показывает «Оплачен» — а в базе статус pending. Без SQL не узнаешь

2

Подготовка тестовых данных

100 пользователей через UI — день. Один INSERT — секунда

3

Поиск причины бага

«Не видит заказы» — UI, API или данные кривые? Один запрос — и ясно

2. SELECT — получаем данные

Начнём с того, что знают все. Но я видел людей, которые путаются даже тут, так что пробежимся быстро.

2.1 Получить все данные

SELECT FROM users; Звёздочка  — «дай все столбцы». Вернёт всю таблицу целиком (все 5 строк, все 6 столбцов).

2.2 Выбрать конкретные столбцы

SELECT name, email FROM users; Результат:

name

email

Atajan

atajan@mail.com

Maria

maria@mail.com

John

john@mail.com

Anna

anna@mail.com

Kemal

kemal@mail.com

На практике SELECT * используют для быстрой проверки. В тест-скриптах лучше указывать конкретные столбцы — быстрее и понятнее.

2.3 WHERE — фильтрация

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; Результат: только Maria (25 лет). Anna (22) не прошла по возрасту. -- Москва ИЛИ Стамбул SELECT FROM users WHERE city = 'Moscow' OR city = 'Istanbul'; Результат: Maria, John, Anna.

2.5 IN — вместо кучи OR

SELECT * FROM users WHERE city IN ('Moscow', 'Istanbul'); Делает то же самое, что city = 'Moscow' OR city = 'Istanbul', но короче и читабельнее. Особенно когда значений 5-10.

2.6 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.

2.7 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). _ = ровно один символ

2.8 IS NULL — проверка на пустоту

SELECT * FROM users WHERE city IS NULL; В нашей таблице у всех есть город, поэтому вернёт 0 строк. Но на реальных данных NULL встречается постоянно — незаполненные поля, удалённые связи. 

⚠️ Ловушка на собеседовании: 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; Кто-то обязательно напишет = NULL — и интервьюер это заметит. 

3. ORDER BY, LIMIT, DISTINCT

3.1 ORDER BY — сортировка

SELECT * FROM users ORDER BY age DESC; ASC — по возрастанию (по умолчанию), DESC — по убыванию. Результат: John (35), Atajan (30), Kemal (28), Maria (25), Anna (22).

3.2 LIMIT — ограничить количество

SELECT * FROM users ORDER BY age ASC LIMIT 3; Результат: три самых молодых — Anna (22), Maria (25), Kemal (28).

3.3 DISTINCT — уникальные значения

SELECT DISTINCT city FROM users; Результат: Ashgabat, Moscow, Istanbul (3 уникальных города, без дублей). SELECT COUNT(DISTINCT city) FROM users; Результат: 3. Полезно для быстрой проверки: «сколько уникальных значений в столбце?» 

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; Результат:

name

product

amount

Test_user

Laptop

50000

Test_user

Mouse

2000

Maria

Keyboard

3000

John

Monitor

25000

Anna и Kemal — не попали (нет заказов). Заказ с user_id = 99 — тоже (нет пользователя).

4.2 LEFT JOIN — все из левой + совпадения из правой

SELECT users.name, orders.product FROM users LEFT JOIN orders ON users.id = orders.user_id; Результат:

name

product

Test_user

Laptop

Test_user

Mouse

Maria

Keyboard

John

Monitor

Anna

NULL

Kemal

NULL

Anna и Kemal попали с NULL — они есть в users, но заказов нет.

4.3 RIGHT JOIN — все из правой + совпадения из левой

SELECT users.name, orders.product FROM users RIGHT JOIN orders ON users.id = orders.user_id; Результат:

name

product

Test_user

Laptop

Test_user

Mouse

Maria

Keyboard

John

Monitor

NULL

Headphones

Headphones попал с NULL — заказ есть, но user_id = 99 не существует.

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; Результат: Anna, Kemal. LEFT JOIN даёт NULL для тех, у кого нет заказов, а WHERE отфильтровывает только их. Эту задачу дают постоянно — выучите наизусть. 

5. Агрегатные функции

Функция

Что делает

Пример

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(*) считает все строки. А COUNT(column) — только те, где столбец не NULL. Подробнее — в ловушке #2. Пример: сколько заказов у каждого пользователя? 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

Test_user

2

Maria

1

John

1

Anna

0

Kemal

0

6. GROUP BY и HAVING

GROUP BY группирует строки, HAVING фильтрует группы. Главное отличие: 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₽?»

Этот запрос сложнее — тут и JOIN, и WHERE, и GROUP BY, и HAVING. Разберём по шагам: 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; Что происходит пошагово:

  1. FROM users INNER JOIN orders — соединяем таблицы (4 строки, без Anna и Kemal)

  2. WHERE status = 'paid' — убираем cancelled-заказ Maria. Осталось 3 строки

  3. GROUP BY users.name — группируем: Test_user (50000+2000), John (25000)

  4. HAVING SUM(amount) > 10000 — оба прошли (52000 и 25000 > 10000)

  5. SELECT — выводим имя и сумму

Результат:

name

total_spent

Test_user

52000

John

25000

Обратите внимание: Maria не попала в результат. Но не из-за HAVING — её единственный заказ был cancelled, и WHERE отсёк его ещё до группировки. После GROUP BY у Maria просто нет данных. 

7. Подзапросы

Подзапрос — это запрос внутри запроса. Иногда без них не обойтись.

7.1 Скалярный подзапрос

-- Пользователи, чей возраст выше среднего SELECT name, age FROM users WHERE age > (SELECT AVG(age) FROM users); Результат: Test_user (30), John (35). Средний возраст = 28.

7.2 Подзапрос с IN

-- Пользователи с хотя бы одним оплаченным заказом SELECT name FROM users WHERE id IN ( SELECT DISTINCT user_id FROM orders WHERE status = 'paid' ); Результат: Test_user, John.

7.3 Задача с собеседования: «Товар с макс. суммой»

Способ 1: подзапрос SELECT product, amount FROM orders WHERE amount = (SELECT MAX(amount) FROM orders); Внутренний запрос (SELECT MAX(amount) FROM orders) вернёт 50000. Внешний найдёт все заказы с такой суммой. Если два товара стоят 50000 — вернёт оба. Способ 2: ORDER BY + LIMIT SELECT product, amount FROM orders ORDER BY amount DESC LIMIT 1; Сортируем по убыванию, берём первый. Проще, но вернёт только одну строку, даже если максимум не уникален. Результат обоих: Laptop, 50000. На собеседовании лучше показать оба способа и объяснить разницу — это покажет глубину понимания. 

8. Задачи с реальных собеседований

Задача 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'; Разбор:

  • LEFT JOIN — берём всех пользователей, даже без заказов

  • WHERE o.id IS NULL — оставляем только тех, у кого заказов нет

  • u.created_at < NOW() - INTERVAL '1 month' — зарегистрированы больше месяца назад

Кстати: u и o — это алиасы (псевдонимы) таблиц. Вместо users.name пишем u.name. Короче и чище, особенно когда JOIN-ов несколько. Зачем это QA? Тестируем рассылку «Вы давно не заказывали» — убеждаемся, что она уходит именно этим пользователям.

Задача 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; Разбор:

  • INNER JOIN — соединяем пользователей с заказами

  • WHERE status = 'paid' — только оплаченные

  • GROUP BY u.name — группируем по имени

  • SUM(o.amount) — считаем сумму заказов каждого

  • ORDER BY total DESC LIMIT 3 — сортируем по убыванию, берём топ-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; Разбор по частям:

  • LEFT JOIN — берём всех пользователей (а не только тех, у кого есть заказы)

  • COUNT(DISTINCT u.id) — сколько уникальных пользователей в городе

  • COUNT(DISTINCT o.user_id) — сколько из них сделали заказ. DISTINCT нужен, чтобы не считать одного покупателя дважды (если у него 2 заказа)

  • ROUND(... * 100.0 / ..., 1) — процент конверсии, округлённый до 1 знака. 100.0 (а не 100) нужен для дробного деления, иначе SQL округлит до целого

Результат:

city

total_users

buyers

conversion_pct

Ashgabat

2

1

50.0

Moscow

2

1

50.0

Istanbul

1

1

100.0

Если решите на собеседовании — впечатлите. Тут и LEFT JOIN, и GROUP BY, и два разных COUNT DISTINCT, и ROUND — всё в одном запросе. 

9. UPDATE и DELETE — с осторожностью

Главное правило: всегда WHERE. DELETE FROM users без WHERE удалит всех пользователей. На production это катастрофа.

9.1 UPDATE — обновить данные

UPDATE users SET city = 'Istanbul' WHERE id = 1; Обновит город только у пользователя с id = 1. Без WHERE — поменяет город у всех.

9.2 DELETE — удалить строки

DELETE FROM orders WHERE status = 'cancelled' AND created_at < NOW() - INTERVAL '1 year'; Удалит отменённые заказы старше года. Без WHERE — удалит все заказы.

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 — это не значение

Представьте анкету. Поле «Город» оставили пустым. Это не значит, что город — пустая строка "". Это значит, что данных нет вообще. Вот что такое NULL. -- ❌ Так не работает (вернёт 0 строк, даже если NULL есть!) SELECT FROM users WHERE city = NULL; -- ✅ Правильно SELECT FROM users WHERE city IS NULL; Почему? Потому что NULL = NULL — это не TRUE. Это тоже NULL. В SQL любая операция с NULL даёт NULL:

Выражение

Результат

Почему

NULL = NULL

NULL (не TRUE!)

Неизвестное нельзя сравнить с неизвестным

NULL != 'Moscow'

NULL (не TRUE!)

Неизвестное нельзя сравнить ни с чем

NULL + 5

NULL

Неизвестное + 5 = неизвестное

NULL IS NULL

TRUE

IS NULL — специальная проверка

Ловушка #2: COUNT(*) vs COUNT(column)

Допустим, у 3 из 5 пользователей заполнено поле phone, а у двоих — NULL. SELECT COUNT(*) FROM users; -- → 5 (все строки) SELECT COUNT(phone) FROM users; -- → 3 (только не-NULL) COUNT(*) считает строки. COUNT(phone) считает значения. Разница — те самые NULL. На собеседовании любят спрашивать: «А что вернёт COUNT, если в столбце есть NULL?»

Ловушка #3: GROUP BY — забыли столбец

-- ❌ Ошибка: name не в GROUP BY и не в агрегатной функции SELECT name, city, COUNT(*) FROM users GROUP BY city; SQL не знает, какое name показать — в Москве и Maria, и Anna. Ошибка. -- ✅ Вариант 1: убрать лишний столбец SELECT city, COUNT(*) FROM users GROUP BY city; -- ✅ Вариант 2: добавить в GROUP BY SELECT name, city, COUNT(*) FROM users GROUP BY city, name; Правило простое: всё, что не внутри агрегатной функции (COUNT, SUM...) — должно быть в GROUP BY.

Ловушка #4: WHERE vs HAVING

Это как фильтр на входе в клуб vs фильтр внутри. WHERE работает до группировки (отсеивает строки), HAVING — после (отсеивает группы). -- ❌ Ошибка: агрегатная функция в WHERE SELECT city, COUNT(*) FROM users WHERE COUNT(*) > 1 GROUP BY city; Почему не работает? Потому что в момент WHERE строки ещё не сгруппированы — COUNT не существует. -- ✅ Правильно: агрегат — в HAVING SELECT city, COUNT(*) FROM users GROUP BY city HAVING COUNT(*) > 1; Наглядно: Все строки (5 штук) │ ▼ WHERE ← фильтрует СТРОКИ (age > 18, status = 'paid') │ ▼ GROUP BY ← группирует (по city, по name...) │ ▼ HAVING ← фильтрует ГРУППЫ (COUNT(*) > 1, SUM(amount) > 10000) │ ▼ Результат

Ловушка #5: SQL выполняется не в том порядке, в котором пишется

Вы пишете запрос так: SELECT city, COUNT(*) AS cnt -- 1. пишете первым FROM users -- 2. пишете вторым WHERE age > 18 -- 3. пишете третьим GROUP BY city -- 4. HAVING COUNT(*) > 1 -- 5. ORDER BY cnt DESC; -- 6. пишете последним Но SQL выполняет в другом порядке:

Порядок выполнения

Что делает

Порядок записи

1. FROM

Берёт таблицу

2-я строка

2. WHERE

Фильтрует строки

3-я строка

3. GROUP BY

Группирует

4-я строка

4. HAVING

Фильтрует группы

5-я строка

5. SELECT

Выбирает столбцы

1-я строка (!)

6. ORDER BY

Сортирует

6-я строка

Практическое следствие: в WHERE нельзя использовать алиасы из SELECT — потому что SELECT ещё не выполнился. -- ❌ Ошибка: алиас cnt ещё не существует на этапе WHERE SELECT city, COUNT(*) AS cnt FROM users WHERE cnt > 1 GROUP BY city; -- ✅ Правильно: используем HAVING (выполняется после GROUP BY) 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

Объясни, почему WHERE city = NULL не работает

INNER JOIN

Соедини users и orders, объясни что получится

LEFT JOIN

Найди пользователей без заказов

Middle — то, что отличает от джуна

Тема

Проверь себя

Агрегатные функции

COUNT, SUM, AVG, MAX, MIN — для чего каждая

GROUP BY + HAVING

Города с >1 пользователем. Покупатели с суммой >10000₽

Подзапросы

Пользователи старше среднего возраста

WHERE vs HAVING

Объясни разницу и когда что использовать

COUNT(*) vs COUNT(col)

Что будет если в столбце есть NULL?

UPDATE, DELETE

Почему сначала SELECT, потом DELETE?

Порядок выполнения

Почему алиас из SELECT нельзя в WHERE?

Этого хватит для 90% QA собеседований. Оконные функции, CTE, хранимые процедуры — это уже DBA-территория. 

12. Как практиковаться

Ресурс

Что там

Цена

SQLBolt

Интерактивные уроки с нуля. 15 мин/день — за неделю база

Бесплатно

LeetCode Database

Задачи уровня собеседований. Начните с Easy

Бесплатно

HackerRank SQL

Задачи с проверкой. Хорошая подборка для начинающих

Бесплатно

Ваш рабочий проект

Запросы к реальной тестовой БД — лучшая практика

—


Курс по тестированию с практическими заданиями — бесплатно на annayev.com (English, Русский, Türkçe). Ставьте плюс, если было полезно. Какие SQL-задачи вам давали на собеседованиях? Пишите в комментариях — может, добавлю разбор.

Источник: https://habr.com/ru/articles/1005806/


Интересные статьи

Интересные статьи

DISCLAIMER: Данная статья не является «статьёй конкурента» (поскольку у моей нынешней IT-компании нет продуктов, конкурирующих с изделиями Bitrix) или какой-то «заказной» (поскольку вряд ли кто-то мне...
Если в команде каждый делает всё, а пользовательские исследования – это «когда-нибудь потом», статья для вас. Я делюсь опытом, как мы наладили процесс discovery в маленькой команде без ресерчера и бю...
Мы поддерживаем кастомизацию наших продуктов и всячески помогаем в этом разработчикам: у нас есть курсы, гайды, маркетплейс решений и т.д. При этом мы выступаем за безопасность и правильный подход, за...
Получить трафик для интернет-магазина сегодня не проблема. Есть много каналов его привлечения: органическая выдача, контекстная реклама, контент-маркетинг, RTB-сети и т. д. Вопрос в том, как вы распор...
В прошлом году, нашей команде повезло принять участие в разработке программного обеспечения для социально значимого проекта – системы ввода архивных данных военных комиссариатов Хабаровского края...