Содержание

  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;

Результат:

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-задачи вам давали на собеседованиях? Пишите в комментариях — возможно, добавлю разбор.