Содержание

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

#

Зачем

Пример

1

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

UI «Оплачен» — в БД pending. Без SQL не узнаешь

2

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

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

3

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

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

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

Начнём с базового, но без путаницы.

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

SELECT * FROM users;

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

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. Агрегатные функции

Функция

Что делает

Пример

COUNT()

Считает строки

SELECT COUNT(*) FROM users;

SUM()

Сумма

SELECT SUM(amount) FROM orders;

AVG()

Среднее

SELECT AVG(age) FROM users;

MAX()

Максимум

SELECT MAX(amount) FROM orders;

MIN()

Минимум

SELECT MIN(age) FROM users;

Сколько заказов у каждого пользователя:

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

Почему city = 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. Как практиковаться

Ресурс

Что там

Цена

SQLBolt

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

Бесплатно

LeetCode Database

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

Бесплатно

HackerRank SQL

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

Бесплатно

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

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


Курс по тестированию с практическими заданиями — бесплатно на annayev.com (English, Русский, Türkçe).

Ставьте плюс, если было полезно. Какие SQL-задачи вам давали на собеседованиях? Пишите в комментариях — возможно, добавлю разбор.

Комментарии (8)


  1. Akina
    03.03.2026 13:23

    Форматирование - вообще никакое. Неужели трудно посмотреть, что публикуете?

    Нечитабельно.


    1. interstels Автор
      03.03.2026 13:23

      Поправил


  1. AleksandrMironov1993
    03.03.2026 13:23

    Не знаю, что там с форматированием, но смысловая нагрузка и полезность хорошая. Даже ссылок на всякие курсы нет!

    апд. Ах, чертова дюжина, все таки есть, не дочитал до конца прям.

    Я когда открывал - был уверен, что будет.

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

    А так, ещё раз, автору респект


  1. 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. А по-хорошему - уровни изоляции, блокировки, репликация, хотя бы на уровне понимания основ.


    1. Moroshka
      03.03.2026 13:23

      Middle - это как минимум табличные и оконные функции, хранимые объекты, латеральные запросы и рекурсивные CTE. А по-хорошему - уровни изоляции, блокировки, репликация, хотя бы на уровне понимания основ.

      А поделитесь, пожалуйста, примерами ситуаций, когда тестировщику миддлу могут потребоваться эти знания?


      1. Akina
        03.03.2026 13:23

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

        Между тем, что перечислено в статье, и тем, что перечислил я, есть вполне зримая квалификационная граница. А между двумя списками, что в статье, имхо границы нет вообще. Разница между ними лишь в том, дочитан ли букварь по SQL до конца или заброшен на середине.


    1. AleksandrMironov1993
      03.03.2026 13:23

      вот мне тоже любопытно, это что за мидл который отлично знает все перечисленное
      из личного опыта - СТЕшки использовал да, чтобы вложенность удобно расписать. все. оконки мб использовал, но редко. даже разрабы их за два года работы в сервисе, где все на бд оракловой повязано в хранимых процедурах использовали один раз за все время. и то нужно было для историчности. в других местах историчность совсем другими способами реализуют.
      функциями опять же не пользовался
      вы уверены, что это не про аналитика или про разраба? а то похоже


  1. rozhnev
    03.03.2026 13:23

    Для тех кто осилил основы SQL и хочет подтянуть практику: sqltest.online