Задачи по SQL с решениями: от SELECT до оконных функций

Ноутбук с размытым кодом и схема из трёх таблиц Изучение

Восемь задач по SQL на одной учебной базе данных магазина настольных игр, и каждое решение прогнано на двух СУБД: SQLite 3.50.4 (Python 3.14.3) и PostgreSQL 18.6. Задачи идут от SELECT с WHERE до GROUP BY с HAVING, JOIN, подзапросов и оконных функций.

В задачах 1-8 обе СУБД вернули одни и те же данные. Сначала напишите свой запрос, потом сверьтесь с решением.

База данных для задач

Три таблицы: клиенты, товары и заказы. Вера не указала город, Дина ничего не заказывала, а заказ 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,)]

Почему данные разложены по трём таблицам, объясняет нормализация баз данных.

Читайте также:  Простые программы на Python: 8 программ с кодом, выводом и разбором

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

Читайте также:  Простые программы на Python: 8 программ с кодом, выводом и разбором

Оконные функции

Задача 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.

Читайте также:  Задачи по Python для начинающих: 10 задач с решениями и разбором

Два монитора: размытая таблица и текст с красной строкой

Где решать задачи по 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).

Оцените статью
bestprogrammer.ru
Добавить комментарий