Чтобы научиться SQL, недостаточно прочитать синтаксис: нужно самому сформулировать запрос, запустить его и сравнить строки с ожидаемым результатом. Ниже одна учебная база и 10 задач с решениями: от SELECT и WHERE до JOIN, GROUP BY и HAVING. Все запросы совместимы с PostgreSQL и почти без изменений работают в SQLite.
Работайте в два прохода. Сначала читайте только условие и пишите свой запрос. Затем открывайте решение, сравнивайте не текст SQL, а полученные строки. Один результат можно получить разными корректными запросами.
SELECT, WHERE, AND, ORDER BY и LIMIT отвечают на точные вопросы.
COUNT, AVG и GROUP BY превращают много заказов в сводку.
JOIN связывает заказ с пользователем по user_id и id.
Подготовка учебной базы users и orders
У нас интернет-магазин с двумя таблицами. В users лежат пользователи, в orders их заказы. Поле orders.user_id ссылается на users.id. Выполните скрипт один раз в пустой базе.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR(50) NOT NULL,
city VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total NUMERIC(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATE NOT NULL
);
INSERT INTO users (id, name, city) VALUES
(1, 'Анна', 'Москва'),
(2, 'Борис', 'Казань'),
(3, 'Вера', 'Москва'),
(4, 'Глеб', 'Тула'),
(5, 'Даша', 'Самара');
INSERT INTO orders (id, user_id, total, status, created_at) VALUES
(101, 1, 1200, 'paid', '2026-08-10'),
(102, 1, 800, 'new', '2026-08-12'),
(103, 2, 2500, 'paid', '2026-08-13'),
(104, 3, 700, 'cancelled', '2026-08-14'),
(105, 3, 1800, 'paid', '2026-08-15'),
(106, 5, 400, 'paid', '2026-08-16'),
(107, 2, 900, 'paid', '2026-08-17'),
(108, 3, 600, 'paid', '2026-08-18');| Таблица | Одна строка означает | Ключ |
|---|---|---|
users | Одного зарегистрированного человека | id |
orders | Один оформленный заказ | id |
| Связь | Пользователь может иметь 0, 1 или много заказов | orders.user_id = users.id |
SELECT COUNT(*) FROM users; и SELECT COUNT(*) FROM orders; возвращают 5 и 8. Если числа другие, очистите учебные таблицы и выполните setup заново.Задачи 1-4: SELECT, WHERE, ORDER BY и LIMIT
- Нужны только столбцы
nameиcity. - Количество ожидаемых строк: 5.
- Не используйте
SELECT *: выбирайте данные, которые просит условие.
SELECT name, city
FROM users;- Добавьте фильтр по полю
city. - Строковые значения заключаются в одинарные кавычки.
- Ожидаемые имена: Анна и Вера.
SELECT id, name
FROM users
WHERE city = 'Москва';- Нужны одновременно два условия: статус
paidи сумма больше 1000. - Отсортируйте результат от большей суммы к меньшей.
- Ожидаемый порядок id: 103, 105, 101.
SELECT id, user_id, total
FROM orders
WHERE status = 'paid' AND total > 1000
ORDER BY total DESC;- Статус заказа не важен.
- Сначала сортировка по убыванию, затем ограничение числа строк.
- Ожидаемые суммы: 2500, 1800 и 1200.
SELECT id, total, status
FROM orders
ORDER BY total DESC
LIMIT 3;SELECT выбирает столбцы, WHERE отбрасывает строки, ORDER BY задаёт порядок, LIMIT оставляет нужное число строк. Порядок этих частей в запросе фиксирован.

Текст запроса читается сверху вниз, но для рассуждения полезно сначала представить исходные строки и фильтр.
Задачи 5-6: агрегаты и GROUP BY
Агрегатная функция сворачивает несколько строк в одно значение. Без GROUP BY весь результат считается одной группой. С GROUP BY status база создаёт отдельную группу для каждого статуса.
- В результате одна строка соответствует одному status.
- Дайте вычисленному столбцу имя
orders_countчерез AS. - Ожидается: paid = 6, new = 1, cancelled = 1.
SELECT status, COUNT(*) AS orders_count
FROM orders
GROUP BY status
ORDER BY orders_count DESC;- Сначала оставьте только paid-строки через WHERE.
- Затем примените
AVG(total). - Ожидаемое значение без округления: примерно 1233.33.
SELECT AVG(total) AS average_paid_total
FROM orders
WHERE status = 'paid';В шестой задаче отменённый и новый заказы не участвуют в среднем: WHERE убирает их до того, как AVG начинает вычисление.

GROUP BY не сортирует результат: если порядок важен, добавляйте ORDER BY отдельно.
Синтаксис можно закрепить в курсе SQL: короткие упражнения помогают увидеть разницу между фильтром строк и групп без копирования готового ответа.
Задачи 7-9: INNER JOIN и LEFT JOIN
В orders хранится только user_id, но в отчёте нужен name. JOIN соединяет строку заказа со строкой пользователя по условию o.user_id = u.id. Алиасы o и u сокращают запрос и показывают источник каждого столбца.
- Используйте INNER JOIN: нужны только существующие заказы с найденным пользователем.
- Выведите id заказа, name, total и status.
- Количество ожидаемых строк: 8.
SELECT o.id, u.name, o.total, o.status
FROM orders AS o
JOIN users AS u ON u.id = o.user_id
ORDER BY o.id;- Соедините users и orders, оставьте paid-заказы.
- Сгруппируйте строки по id и name пользователя.
- Ожидаемые суммы: Борис 3400, Вера 2400, Анна 1200, Даша 400.
SELECT u.id, u.name, SUM(o.total) AS paid_revenue
FROM users AS u
JOIN orders AS o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.id, u.name
ORDER BY paid_revenue DESC;- Начните с users и примените LEFT JOIN, чтобы Глеб не исчез.
- Считайте
o.id, а не звёздочку. - Ожидается пять строк; у Глеба orders_count = 0.
SELECT u.name, COUNT(o.id) AS orders_count
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY u.id;
LEFT JOIN сохраняет левую строку даже без пары, поэтому подходит для поиска нулевых значений.
Задача 10: HAVING и проверка решений
- WHERE должен оставить только paid-заказы до группировки.
- HAVING должен отфильтровать уже посчитанный
SUM(o.total). - Ожидаемые строки: Борис 3400 и Вера 2400.
SELECT u.name, SUM(o.total) AS paid_revenue
FROM users AS u
JOIN orders AS o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.id, u.name
HAVING SUM(o.total) > 2000
ORDER BY paid_revenue DESC;WHERE отвечает «какие исходные заказы участвуют», HAVING отвечает «какие готовые группы оставить». Агрегат SUM нельзя перенести в обычный WHERE этого же уровня запроса.
В SELECT есть обычный столбец, который не входит в GROUP BY и не обёрнут агрегатной функцией. Добавьте ключ и имя группы.
Использован INNER JOIN. Начните запрос с users и примените LEFT JOIN, если нужны пользователи без заказов.
После LEFT JOIN функция COUNT(*) считает сохранённую строку пользователя. Используйте COUNT(o.id): NULL-значения она не считает.
SQL не обещает порядок без ORDER BY. Добавьте явную сортировку, если проверка зависит от расположения строк.
В каком порядке писать части SELECT?
Для этих задач: SELECT, FROM, JOIN и ON, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Не каждая часть обязательна, но их взаимный порядок фиксирован.
Чем WHERE отличается от HAVING?
WHERE фильтрует исходные строки до группировки. HAVING фильтрует группы после GROUP BY и может проверять COUNT, SUM или AVG.
Когда нужен LEFT JOIN?
Когда нужно сохранить все строки левой таблицы, даже если справа нет совпадения. Например, показать пользователей с нулём заказов.
Почему COUNT(*) и COUNT(column) дают разные числа?
COUNT(*) считает строки результата. COUNT(column) считает только строки, где значение указанного столбца не NULL.
Как лучше решать задачи по SQL?
Сначала сформулируйте ожидаемые строки словами, затем напишите запрос, запустите его и проверьте на граничном случае: нуле, NULL, равных значениях или отсутствии связи.
Повторите базовые команды по материалу SQL: SELECT, INSERT, UPDATE и DELETE, а для локальных экспериментов используйте SQLite для начинающих. Затем добавьте третью таблицу products и попробуйте связать её с orders.
SQL нужен не только backend-разработчикам: в маршруте QA-инженера он используется для проверки данных и поиска причин ошибки. В Кодике перепишите каждое решение без подсказки и сохраните запросы как маленькое портфолио практики.
