Часть 7 из 16

Теперь расписание показывает событие, площадку и артиста в одной строке. Организатору нужны ещё итоги: сколько слотов у каждой сцены и сколько денег принесла продажа билетов. Добавим таблицу продаж и научимся группировать строки, считать количество и складывать суммы.

Здесь есть ловушка: если соединить билеты с несколькими выступлениями до подсчёта, одна продажа может попасть в сумму несколько раз. Разберём это на понятном примере и научимся проверять, что итог действительно верен.

Считаем слоты

SELECT e.event_id,e.name,COUNT(p.performance_id) AS slots
FROM events AS e
LEFT JOIN performances AS p ON p.event_id=e.event_id
GROUP BY e.event_id,e.name;

COUNT по ключу правой таблицы возвращает ноль для события без слотов; COUNT(*) посчитал бы само событие.

Создаём таблицу продаж

CREATE TABLE ticket_sales (
 sale_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
 event_id BIGINT UNSIGNED NOT NULL,
 ticket_code CHAR(12) NOT NULL UNIQUE,
 ticket_type VARCHAR(30) NOT NULL,
 price DECIMAL(8,2) NOT NULL,
 sold_at DATETIME NOT NULL,
 checked_in_at DATETIME NULL,
 PRIMARY KEY (sale_id),
 CHECK(price >= 0),
 FOREIGN KEY(event_id) REFERENCES events(event_id)
) ENGINE=InnoDB;

Заполните её несколькими вымышленными строками с разными кодами и существующими event_id.

Считаем продажи отдельно

SELECT event_id,COUNT(*) AS tickets_sold,
 SUM(price) AS revenue,AVG(price) AS average_price
FROM ticket_sales
GROUP BY event_id
HAVING COUNT(*) >= 1
ORDER BY revenue DESC;

WHERE фильтрует строки до группировки, HAVING — готовые группы. Если соединить продажи с тремя выступлениями события до SUM, сумма билетов может утроиться. Следом оформим промежуточные расчёты через CTE.

Что происходит во время группировки

Без GROUP BY запрос по ticket_sales возвращает каждую продажу. С группировкой по event_id сервер собирает продажи одного события в группу и вычисляет для неё итог. COUNT(*) считает строки, SUM(price) складывает цены, AVG(price) вычисляет среднюю.

Если билетов нет, группы нет вообще; чтобы показать и такие события, позже присоединим агрегат к events через LEFT JOIN.

WHERE и HAVING — на разных этапах

Представьте сначала отбор отдельных билетов по дате, а затем подсчёт. Условие на дату ставьте в WHERE — оно удалит строки до подсчёта. Если нужно оставить только события, где продано минимум 100 билетов, пишите HAVING COUNT(*) >= 100: это условие относится к готовой группе.

Проверяем, не раздулся ли итог

Если на событие продано два билета по 100 рублей и у него три слота, соединение продаж со слотами создаст шесть строк. SUM увидит каждый билет трижды и покажет 600 вместо 200. Сначала агрегируйте ticket_sales отдельно, а затем соединяйте получившуюся строку с событием.

Практика: вручную проверьте одну группу из двух билетов и сравните итог запроса с калькулятором.

Разбираем итог на маленьком наборе

Допустим, в таблице есть три строки для события 1: цены 1000, 1000 и 1500 рублей. Тогда COUNT(*) вернёт 3, SUM(price) — 3500, а AVG(price) — 1166.67 после округления отображения. Добавьте в тестовую таблицу несколько продаж и вручную пересчитайте одну группу.

Если итог отличается, сначала проверьте входящие строки, затем условие GROUP BY.

Обратите внимание: агрегатные функции игнорируют NULL для аргумента, кроме COUNT(*), который считает строки целиком. Наш столбец цены объявлен NOT NULL, поэтому сумма не теряет строку из-за пустой цены. В другой модели нужно заранее решить, что означает неизвестное значение.

Когда не нужно считать COUNT DISTINCT

Иногда пытаются исправить размноженный JOIN командой COUNT(DISTINCT sale_id). Это может сделать конкретный счётчик правильным, но не спасает сумму: повторившиеся цены всё равно сложатся несколько раз. Надёжнее построить правильный набор и агрегировать каждую сущность на своём уровне, а не маскировать неверную связь.

В следующем уроке мы разделим агрегат и внешний отчёт через CTE, чтобы каждому шагу было проще дать имя и проверить его отдельно.

Какой агрегат выбрать?

Функция Вопрос Пример
COUNT(*) Сколько строк? Количество проданных билетов
SUM(price) Какова сумма? Выручка события
AVG(price) Каково среднее? Средняя цена билета
MIN / MAX Какие крайние значения? Первое и последнее время слота

WHERE отбирает билеты до подсчёта, а HAVING отбирает уже посчитанные группы. Так проще проверить, на каком этапе должно действовать условие.

← Предыдущая статья: JOIN в MySQL: связываем артистов, события и площадки

Следующая статья: Подзапросы и CTE MySQL: строим отчёт по выручке →