← Назад к вопросам
SQL: Пользователи с покупками выше среднего за 3 месяца
1.8 Middle🔥 201 комментариев
#SQL и базы данных#Аналитика и метрики
Условие
Даны таблицы:
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(100),
registration_date DATE
);
CREATE TABLE purchases (
purchase_id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
purchase_date DATE
);
Задание:
- Найдите всех пользователей, которые совершили покупки на сумму выше среднего за последние 3 месяца
- Средняя сумма считается по всем пользователям за этот период
- Выведите: имя пользователя, общую сумму его покупок, среднюю сумму по всем пользователям
- Отсортируйте результат по сумме покупок по убыванию
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ требований
Задача: Найти пользователей, чьи покупки за 3 месяца превышают общее среднее значение.
Ключевые моменты:
- Период: последние 3 месяца
- Средняя сумма: по ВСЕм пользователям (не индивидуальная)
- Фильтр: sum(amount по пользователю) > avg(amount по всем)
- Вывод: имя, сумма покупок, среднее по всем
Решение 1: С использованием оконной функции AVG (рекомендуется)
WITH last_3_months AS (
-- Фильтруем покупки за последние 3 месяца
SELECT
p.user_id,
p.amount,
p.purchase_date
FROM purchases p
WHERE p.purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
),
user_purchases AS (
-- Считаем сумму покупок по каждому пользователю
SELECT
u.user_id,
u.name,
SUM(p.amount) AS user_total_purchases,
-- Среднее по всем пользователям в одном окне
ROUND(AVG(SUM(p.amount)) OVER (), 2) AS avg_all_users
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name
)
SELECT
user_id,
name,
ROUND(user_total_purchases, 2) AS user_total_purchases,
avg_all_users
FROM user_purchases
WHERE user_total_purchases > avg_all_users
ORDER BY user_total_purchases DESC;
Объяснение пошагово:
- last_3_months CTE: Фильтруем все покупки за последние 3 месяца
DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'— дата 3 месяца назад- Эквивалент для других БД:
- MySQL: `DATE_SUB(CURDATE(), INTERVAL 3 MONTH)`
- SQL Server: `DATEADD(month, -3, CAST(GETDATE() AS DATE))`
- user_purchases CTE: Агрегация по пользователям
- LEFT JOIN — для включения пользователей без покупок (их сумма = NULL или 0)
SUM(p.amount)— сумма покупок каждого пользователяAVG(SUM(p.amount)) OVER ()— среднее по всем пользователям
- OVER () без partition — вычисляется по всему набору
-
WHERE user_total_purchases > avg_all_users — фильтруем выше среднего
-
ORDER BY user_total_purchases DESC — сортируем по убыванию
Решение 2: С подзапросом для среднего значения
WITH last_3_months AS (
SELECT
p.user_id,
p.amount
FROM purchases p
WHERE p.purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
),
user_totals AS (
SELECT
u.user_id,
u.name,
COALESCE(SUM(p.amount), 0) AS user_total_purchases
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name
),
average_calc AS (
-- Подзапрос для расчёта среднего по всем пользователям
SELECT
ROUND(AVG(user_total_purchases), 2) AS avg_all_users
FROM user_totals
)
SELECT
ut.user_id,
ut.name,
ut.user_total_purchases,
ac.avg_all_users
FROM user_totals ut
CROSS JOIN average_calc ac
WHERE ut.user_total_purchases > ac.avg_all_users
ORDER BY ut.user_total_purchases DESC;
Особенность: CROSS JOIN связывает таблицу результатов с одной строкой среднего.
Решение 3: Полностью в одном запросе (без CTE)
SELECT
u.user_id,
u.name,
ROUND(SUM(p.amount), 2) AS user_total_purchases,
ROUND(
AVG(SUM(p.amount)) OVER (
PARTITION BY NULL -- По всему набору
),
2
) AS avg_all_users
FROM users u
LEFT JOIN purchases p ON u.user_id = p.user_id
AND p.purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
GROUP BY u.user_id, u.name
HAVING SUM(p.amount) > AVG(SUM(p.amount)) OVER (PARTITION BY NULL)
ORDER BY user_total_purchases DESC;
Примечание: Некоторые БД не поддерживают оконные функции в HAVING. Используй Решение 1 в таких случаях.
Решение 4: Для разных БД
PostgreSQL:
WITH last_3_months AS (
SELECT user_id, amount
FROM purchases
WHERE purchase_date >= CURRENT_DATE - INTERVAL '3 months'
)
SELECT
u.user_id,
u.name,
ROUND(COALESCE(SUM(p.amount), 0)::NUMERIC, 2) AS user_total_purchases,
ROUND(AVG(COALESCE(SUM(p.amount), 0)) OVER (), 2) AS avg_all_users
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name
HAVING COALESCE(SUM(p.amount), 0) > AVG(COALESCE(SUM(p.amount), 0)) OVER ()
ORDER BY user_total_purchases DESC;
MySQL:
WITH last_3_months AS (
SELECT user_id, amount
FROM purchases
WHERE purchase_date >= DATE_SUB(CURDATE(), INTERVAL 3 MONTH)
)
SELECT
u.user_id,
u.name,
ROUND(COALESCE(SUM(p.amount), 0), 2) AS user_total_purchases,
ROUND(AVG(COALESCE(SUM(p.amount), 0)) OVER (), 2) AS avg_all_users
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name
HAVING COALESCE(SUM(p.amount), 0) > AVG(COALESCE(SUM(p.amount), 0)) OVER ()
ORDER BY user_total_purchases DESC;
SQL Server:
WITH last_3_months AS (
SELECT user_id, amount
FROM purchases
WHERE purchase_date >= DATEADD(MONTH, -3, CAST(GETDATE() AS DATE))
),
user_purchases AS (
SELECT
u.user_id,
u.name,
SUM(p.amount) AS user_total_purchases
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name
)
SELECT
user_id,
name,
ROUND(user_total_purchases, 2) AS user_total_purchases,
ROUND(AVG(user_total_purchases) OVER (), 2) AS avg_all_users
FROM user_purchases
WHERE user_total_purchases > (SELECT AVG(user_total_purchases) FROM user_purchases)
ORDER BY user_total_purchases DESC;
Решение 5: С дополнительной статистикой
WITH last_3_months AS (
SELECT user_id, amount, purchase_date
FROM purchases
WHERE purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
),
user_stats AS (
SELECT
u.user_id,
u.name,
u.registration_date,
COUNT(DISTINCT p.purchase_id) AS purchase_count,
SUM(p.amount) AS user_total_purchases,
AVG(p.amount) AS avg_purchase_amount,
MIN(p.amount) AS min_purchase,
MAX(p.amount) AS max_purchase,
ROUND(AVG(SUM(p.amount)) OVER (), 2) AS avg_all_users
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name, u.registration_date
)
SELECT
user_id,
name,
registration_date,
purchase_count,
ROUND(user_total_purchases, 2) AS user_total_purchases,
ROUND(avg_purchase_amount, 2) AS avg_purchase_amount,
ROUND(min_purchase, 2) AS min_purchase,
ROUND(max_purchase, 2) AS max_purchase,
avg_all_users,
ROUND(((user_total_purchases / avg_all_users - 1) * 100), 1) AS percent_above_avg
FROM user_stats
WHERE user_total_purchases > avg_all_users
ORDER BY user_total_purchases DESC;
Дополнительные колонки:
purchase_count— количество покупокavg_purchase_amount— средняя сумма покупкиpercent_above_avg— на сколько % выше среднего
Обработка NULL и граничные случаи
WITH last_3_months AS (
SELECT user_id, amount
FROM purchases
WHERE purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
AND amount > 0 -- Исключаем отрицательные суммы (возвраты)
),
user_purchases AS (
SELECT
u.user_id,
u.name,
-- Если нет покупок за 3 месяца, используем 0
COALESCE(SUM(p.amount), 0) AS user_total_purchases
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
-- Фильтруем новых пользователей (регистрация после периода)
WHERE u.registration_date <= (DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months')
GROUP BY u.user_id, u.name
)
SELECT
user_id,
name,
ROUND(user_total_purchases, 2) AS user_total_purchases,
ROUND(AVG(user_total_purchases) OVER (), 2) AS avg_all_users
FROM user_purchases
WHERE user_total_purchases > AVG(user_total_purchases) OVER ()
ORDER BY user_total_purchases DESC;
Performance Optimization
-- 1. Индексы для быстрого поиска
CREATE INDEX idx_purchases_user_date
ON purchases(user_id, purchase_date);
CREATE INDEX idx_users_id
ON users(user_id);
-- 2. Материализованное представление
CREATE MATERIALIZED VIEW user_purchases_3m AS
WITH last_3_months AS (
SELECT user_id, amount
FROM purchases
WHERE purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
)
SELECT
u.user_id,
u.name,
COALESCE(SUM(p.amount), 0) AS user_total_purchases
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
GROUP BY u.user_id, u.name;
-- 3. Запрос становится проще
SELECT
user_id,
name,
user_total_purchases,
ROUND(AVG(user_total_purchases) OVER (), 2) AS avg_all_users
FROM user_purchases_3m
WHERE user_total_purchases > AVG(user_total_purchases) OVER ()
ORDER BY user_total_purchases DESC;
Рекомендуемое решение для production
WITH last_3_months AS (
SELECT user_id, amount
FROM purchases
WHERE purchase_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months'
AND amount > 0
),
user_purchases AS (
SELECT
u.user_id,
u.name,
COALESCE(SUM(p.amount), 0) AS user_total_purchases,
ROUND(AVG(COALESCE(SUM(p.amount), 0)) OVER (), 2) AS avg_all_users
FROM users u
LEFT JOIN last_3_months p ON u.user_id = p.user_id
WHERE u.registration_date <= (DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '3 months')
GROUP BY u.user_id, u.name
)
SELECT
user_id,
name,
ROUND(user_total_purchases, 2) AS user_total_purchases,
avg_all_users
FROM user_purchases
WHERE user_total_purchases > avg_all_users
ORDER BY user_total_purchases DESC;
Итоговый чеклист
- OVER () без PARTITION BY — среднее по всему набору
- LEFT JOIN — чтобы не потерять пользователей без покупок
- COALESCE(..., 0) — обработка NULL для пользователей без покупок
- DATE_TRUNC(...) - INTERVAL — гибкий расчёт периодов
- HAVING с оконными функциями работает не везде (используй подзапрос)
- Фильтруй отрицательные суммы и новых пользователей
- На больших данных используй индексы и materialized views
- ROUND(..., 2) для финансовых данных