Восемь задач по SQL на одной учебной базе данных магазина настольных игр, и каждое решение прогнано на двух СУБД: SQLite 3.50.4 (Python 3.14.3) и PostgreSQL 18.6. Задачи идут от SELECT с WHERE до GROUP BY с HAVING, JOIN, подзапросов и оконных функций.
В задачах 1-8 обе СУБД вернули одни и те же данные. Сначала напишите свой запрос, потом сверьтесь с решением.
- База данных для задач
- SELECT и WHERE
- Задача 1. Дорогая классика
- Задача 2. Клиенты без города
- GROUP BY и HAVING
- Задача 3. Клиенты с двумя оплаченными заказами и больше
- Задачи на JOIN
- Задача 4. Выручка по категориям
- Задача 5. Клиенты без единого заказа
- Подзапросы
- Задача 6. Товары дороже, чем в среднем
- Оконные функции
- Задача 7. Самый дорогой товар в каждой категории
- Задача 8. Выручка нарастающим итогом
- Где SQLite и PostgreSQL расходятся
- Где решать задачи по SQL дальше
- Частые вопросы
- На какой СУБД решать задачи по SQL новичку?
- Почему NOT IN вернул пустой результат?
База данных для задач
Три таблицы: клиенты, товары и заказы. Вера не указала город, Дина ничего не заказывала, а заказ 7 оформлен без регистрации, и customer_id у него пустой.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(100)
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(100),
price INTEGER
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
product_id INTEGER,
qty INTEGER,
order_date DATE,
status VARCHAR(20)
);
INSERT INTO customers VALUES
(1, 'Анна', 'Москва'), (2, 'Борис', 'Казань'), (3, 'Вера', NULL),
(4, 'Глеб', 'Москва'), (5, 'Дина', 'Пермь');
INSERT INTO products VALUES
(1, 'Шахматы', 'классика', 1500), (2, 'Нарды', 'классика', 2200),
(3, 'Домино', 'классика', 450), (4, 'Лото', 'семейные', 700),
(5, 'Мафия', 'для компании', 600), (6, 'Крокодил', 'для компании', 900);
INSERT INTO orders VALUES
(1, 1, 1, 1, '2026-09-01', 'paid'),
(2, 1, 3, 2, '2026-09-01', 'paid'),
(3, 2, 2, 1, '2026-09-02', 'paid'),
(4, 3, 4, 1, '2026-09-03', 'cancelled'),
(5, 4, 5, 3, '2026-09-03', 'paid'),
(6, 2, 1, 1, '2026-09-05', 'paid'),
(7, NULL, 3, 1, '2026-09-05', 'paid'),
(8, 4, 2, 1, '2026-09-06', 'cancelled'),
(9, 1, 5, 1, '2026-09-07', 'paid');
Для песочницы хватит модуля sqlite3 из стандартной библиотеки Python. Python незнаком? Начните с задач по Python для начинающих. Сохраните скрипт в shop.sql и запустите базу в памяти:
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript(open("shop.sql", encoding="utf-8").read())
print(con.execute("SELECT COUNT(*) FROM orders").fetchall())
[(9,)]
Почему данные разложены по трём таблицам, объясняет нормализация баз данных.
SELECT и WHERE
Задача 1. Дорогая классика
Выведите товары категории «классика» дороже 800 ₽, от самых дорогих.
SELECT name, price
FROM products
WHERE category = 'классика' AND price > 800
ORDER BY price DESC;
name | price
---------+-------
Нарды | 2200
Шахматы | 1500
Задача 2. Клиенты без города
Найдите клиентов, у которых не указан город.
SELECT name FROM customers WHERE city = NULL;
SELECT name FROM customers WHERE city IS NULL;
name
------
name
------
Вера
Первый запрос ничего не вернул: сравнение с NULL возвращает NULL, и WHERE отбрасывает строку.
GROUP BY и HAVING
Задача 3. Клиенты с двумя оплаченными заказами и больше
Выведите клиентов, у которых два оплаченных заказа или больше, и число таких заказов.
SELECT customer_id, COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING COUNT(*) >= 2
ORDER BY customer_id;
customer_id | paid_orders
-------------+-------------
1 | 3
2 | 2
WHERE отбирает записи до группировки, HAVING отбрасывает группы после неё: таков порядок выполнения запроса. Условие на агрегат в WHERE не работает:
SELECT customer_id FROM orders WHERE COUNT(*) >= 2 GROUP BY customer_id;
SQLite: sqlite3.OperationalError: misuse of aggregate: COUNT()
PostgreSQL: ERROR: aggregate functions are not allowed in WHERE

Задачи на JOIN
Задача 4. Выручка по категориям
Посчитайте выручку по оплаченным заказам в каждой категории.
SELECT p.category, SUM(o.qty * p.price) AS revenue
FROM orders AS o
JOIN products AS p ON p.id = o.product_id
WHERE o.status = 'paid'
GROUP BY p.category
ORDER BY revenue DESC;
category | revenue
--------------+---------
классика | 6550
для компании | 2400
Категории «семейные» нет: единственный заказ Лото отменён.
Задача 5. Клиенты без единого заказа
Найдите клиентов, которые не сделали ни одного заказа. Первым приходит в голову NOT IN:
SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
name
------
Пусто, хотя Дина ничего не заказывала: подзапрос вернул и NULL гостевого заказа. Если совпадения нет, а в списке есть NULL, NOT IN даёт NULL, а не истину, и строка отбрасывается. Рабочих вариантов три. Первый через LEFT JOIN (виды соединений разобраны в статье SQL JOIN):
SELECT c.name
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.id IS NULL;
name
------
Дина
Второй отсекает NULL в подзапросе, но фильтр легко забыть. Третий, NOT EXISTS, надёжен, как и LEFT JOIN:
SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders WHERE customer_id IS NOT NULL);
SELECT c.name FROM customers AS c
WHERE NOT EXISTS (SELECT 1 FROM orders AS o WHERE o.customer_id = c.id);
name
------
Дина
name
------
Дина
Подзапросы
Задача 6. Товары дороже, чем в среднем
Выведите товары дороже средней цены по магазину.
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC;
name | price
---------+-------
Нарды | 2200
Шахматы | 1500
Подзапрос возвращает одно число, среднюю цену 1058,33 ₽.
Оконные функции
Задача 7. Самый дорогой товар в каждой категории
Выведите самый дорогой товар каждой категории.
SELECT category, name, price
FROM (
SELECT category, name, price,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS pos
FROM products
) AS ranked
WHERE pos = 1
ORDER BY category;
category | name | price
--------------+----------+-------
для компании | Крокодил | 900
классика | Нарды | 2200
семейные | Лото | 700
Оконную функцию нельзя поставить в WHERE: в порядке выполнения запроса она идёт после WHERE, GROUP BY и HAVING. Номер считают во внутреннем запросе, фильтруют во внешнем. При ничьей (цену шахмат подняли до 2200 ₽) ROW_NUMBER() в PostgreSQL оставил Нарды, в SQLite Шахматы: порядок равных строк ORDER BY не задаёт. С ORDER BY price DESC, name обе оставили Нарды. RANK() вернул обе игры, он и нужен, если лидеров несколько.
Задача 8. Выручка нарастающим итогом
Посчитайте выручку оплаченных заказов по дням и нарастающим итогом: столбцы день, выручка дня, итог.
SELECT o.order_date,
SUM(o.qty * p.price) AS day_sum,
SUM(SUM(o.qty * p.price)) OVER (ORDER BY o.order_date) AS running_total
FROM orders AS o
JOIN products AS p ON p.id = o.product_id
WHERE o.status = 'paid'
GROUP BY o.order_date
ORDER BY o.order_date;
order_date | day_sum | running_total
------------+---------+---------------
2026-09-01 | 2400 | 2400
2026-09-02 | 2200 | 4600
2026-09-03 | 1800 | 6400
2026-09-05 | 1950 | 8350
2026-09-07 | 600 | 8950
Внутренний SUM считает выручку дня, внешний SUM ... OVER складывает дни по порядку.

Где SQLite и PostgreSQL расходятся
Задачу 7 соблазнительно решить без окна:
SELECT category, name, MAX(price) AS price
FROM products
GROUP BY category
ORDER BY category;
SQLite вернул строки задачи 7: при единственном MAX «голый» столбец name берётся из строки с максимумом. Это особое правило SQLite, при другом агрегате значение не определено. PostgreSQL ответил:
ERROR: column "products.name" must appear in the GROUP BY clause or be used in an aggregate function
Решения задач по SQL лучше проверять на PostgreSQL: он отвергает запросы, ответ которых в SQLite держится на особом правиле. В наших восьми задачах всё, что прошло там, сработало и в SQLite.

Где решать задачи по SQL дальше
После восьми задач переходите на интерактивные тренажёры: sql-academy.org с заданиями по сложности и задачами с собеседований или sql-ex.ru с упражнениями на SELECT, INSERT, UPDATE и DELETE.
Частые вопросы
На какой СУБД решать задачи по SQL новичку?
Начать можно с SQLite из Python. Готовый запрос проверьте на PostgreSQL: там падают некоторые запросы, которые SQLite выполняет.
Почему NOT IN вернул пустой результат?
Подзапрос содержит NULL. Надёжнее NOT EXISTS или LEFT JOIN с IS NULL: фильтр IS NOT NULL легко забыть (запросы в задаче 5).








