Группировка и агрегатные функции
Курс MS SQL с нуля · Урок 5 из 12 · Часть 2. Соединения и агрегация
Агрегация превращает тысячи строк в управляемый отчёт. Урок даёт GROUP BY, пять базовых агрегатов, отличие HAVING от WHERE и логический порядок выполнения оператора SELECT — опору всей дальнейшей диагностики.
Перед уроком база обязана быть чистой: если проходили уроки изменения данных, восстановите TechStore скриптами урока 2.
Содержание
1. Что вы освоите
К концу урока вы:
- называете шесть шагов логического выполнения SELECT по порядку;
- пишете GROUP BY с корректными столбцами в SELECT;
- отсеиваете группы в HAVING и отличаете его от WHERE;
- предсказываете поведение COUNT и AVG при NULL в столбце;
- читаете Msg 8120 и устраняете его двумя способами осознанно.
2. Логический порядок SELECT
Сервер читает оператор не так, как он написан. Логический порядок:
| Шаг | Предложение | Что делает |
|---|---|---|
| 1 | FROM | откуда данные, включая соединения |
| 2 | WHERE | отсев строк до группировки |
| 3 | GROUP BY | сшивание оставшихся строк в группы |
| 4 | HAVING | отсев готовых групп |
| 5 | SELECT | вычисление столбцов результата |
| 6 | ORDER BY | сортировка результата |
3. Пять агрегатов и их подвохи
Пять базовых агрегатов и их подвохи:
Чистый код
SELECT COUNT(*) AS rows_cnt,
COUNT(Segment) AS filled_cnt,
COUNT(DISTINCT o.CustomerID) AS clients_cnt,
SUM(o.TotalAmount) AS revenue,
AVG(o.TotalAmount) AS avg_check
FROM dbo.Orders AS o
GROUP BY YEAR(o.OrderDate);
- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с двух дефисов и стрелки вправо, содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
SELECT COUNT(*) AS rows_cnt, -- → строк в группе: звёздочка считает и NULL-значения COUNT(Segment) AS filled_cnt, -- → не-NULL значений столбца: пустые пропускаются COUNT(DISTINCT o.CustomerID) AS clients_cnt, -- → сколько разных клиентов попало в группу SUM(o.TotalAmount) AS revenue, AVG(o.TotalAmount) AS avg_check -- → оборот и средний чек; NULL в сумму и среднее не входит FROM dbo.Orders AS o GROUP BY YEAR(o.OrderDate); -- → одна строка результата на каждый год даты заказаSQL
- COUNT(*) считает строки, а COUNT(столбец) — заполненные значения: разница этих чисел на столбце с NULL — готовая диагностика;
- AVG игнорирует NULL: среднее за период с пропусками «искажается» само собой; в отчётах пропуски заменяют нулём до агрегата, если это соответствует смыслу;
- деление целых отсекает дробную часть (5 пополам даст 2): привычка — приводить делимое к DECIMAL множителем 100.0 или CAST;
- GROUP BY считает все NULL одной группой: «без значения» — полноправная строка результата.
HAVING против WHERE
Оба отсекают, но на разных шагах: WHERE — строки до группировки, HAVING — группы после. Дешевле отсекать рано: фильтр, который можно выразить строками, пишется в WHERE:
Чистый код
SELECT c.City, COUNT(*) AS orders_cnt, AVG(o.TotalAmount) AS avg_check
FROM dbo.Orders AS o
JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID
WHERE NOT (o.Status = N'Cancelled')
GROUP BY c.City
HAVING COUNT(*) BETWEEN 100 AND 999999999;
- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с двух дефисов и стрелки вправо, содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
SELECT c.City, COUNT(*) AS orders_cnt, AVG(o.TotalAmount) AS avg_check -- → отчёт: город, число заказов, средний чек FROM dbo.Orders AS o JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID WHERE NOT (o.Status = N'Cancelled') -- → строки отсекаются до группировки: рано и дёшево GROUP BY c.City HAVING COUNT(*) BETWEEN 100 AND 999999999; -- → условие на агрегат допустимо только здесь, уже на готовых группахSQL
4. Задачи и эталонные решения
Шесть задач на контексте TechStore.
- число заказов в каждом статусе, статусы по убыванию числа; сверка: Cancelled примерно пять процентов, сумма по статусам — ровно 25000;
- выручка и число заказов по месяцам за весь период, свежие сначала; ключ группы — год и месяц вместе;
- средний чек и число заказов по городам, только города от ста заказов — отсев именно в HAVING;
- десятка клиентов по сумме неотменённых заказов с именем через соединение;
- категории без единственного активного товара — LEFT JOIN и HAVING с счётчиком, равным нулю;
- COUNT(*) против COUNT(Email) по клиентам и AVG «как есть» против AVG с заменой NULL нулём на учебном ряде — с письменным объяснением разниц.
Эталоны урока
открывать после самостоятельной попытки
Чистый код
SELECT o.Status, COUNT(*) AS orders_cnt
FROM dbo.Orders AS o
GROUP BY o.Status
ORDER BY orders_cnt DESC;
GO
SELECT YEAR(o.OrderDate) AS order_year, MONTH(o.OrderDate) AS order_month,
COUNT(*) AS orders_cnt, SUM(o.TotalAmount) AS revenue
FROM dbo.Orders AS o
GROUP BY YEAR(o.OrderDate), MONTH(o.OrderDate)
ORDER BY order_year DESC, order_month DESC;
GO
SELECT TOP (10) c.CustomerID, c.FullName, SUM(o.TotalAmount) AS total_spent
FROM dbo.Orders AS o
JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID
WHERE NOT (o.Status = N'Cancelled')
GROUP BY c.CustomerID, c.FullName
ORDER BY total_spent DESC;
GO
SELECT cat.CategoryID, cat.Name
FROM dbo.Categories AS cat
LEFT JOIN dbo.Products AS p
ON p.CategoryID = cat.CategoryID AND p.IsActive = 1
GROUP BY cat.CategoryID, cat.Name
HAVING COUNT(p.ProductID) = 0;
GO
- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с двух дефисов и стрелки вправо, содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
SELECT o.Status, COUNT(*) AS orders_cnt -- → задача 5.1: распределение заказов по статусам FROM dbo.Orders AS o GROUP BY o.Status ORDER BY orders_cnt DESC; -- → сортировка по алиасу вычисленной колонки GO SELECT YEAR(o.OrderDate) AS order_year, MONTH(o.OrderDate) AS order_month, COUNT(*) AS orders_cnt, SUM(o.TotalAmount) AS revenue -- → задача 5.2: динамика по годам и месяцам, обе части даты в ключе FROM dbo.Orders AS o GROUP BY YEAR(o.OrderDate), MONTH(o.OrderDate) ORDER BY order_year DESC, order_month DESC; -- → новые периоды вверху GO SELECT TOP (10) c.CustomerID, c.FullName, SUM(o.TotalAmount) AS total_spent -- → задача 5.4: десятка клиентов по сумме покупок FROM dbo.Orders AS o JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID WHERE NOT (o.Status = N'Cancelled') -- → отменённые заказы в траты не входят: отсев до группировки GROUP BY c.CustomerID, c.FullName -- → имя в ключе группы: иначе Msg 8120 ORDER BY total_spent DESC; GO SELECT cat.CategoryID, cat.Name -- → задача 5.5: пустые категории без активных товаров FROM dbo.Categories AS cat LEFT JOIN dbo.Products AS p ON p.CategoryID = cat.CategoryID AND p.IsActive = 1 -- → фильтр активности в ON: перевести его в WHERE — левость погибнет GROUP BY cat.CategoryID, cat.Name HAVING COUNT(p.ProductID) = 0; -- → после LEFT пустота правой стороны даёт ноль строк GOрешения задач 5.1, 5.2, 5.4, 5.5
5. Типичные ошибки урока
- Msg 8120 «столбец ни в агрегате, ни в GROUP BY» — лечится одним из двух: убрать столбец или дописать его в ключ группировки; выбор — по смыслу отчёта.
- WHERE на агрегате — условие по COUNT или SUM принадлежит HAVING: WHERE выполняется раньше групп.
- Отсев после вместо до — фильтр строки, написанный в HAVING, заставляет группировать лишнее; строки отсеивают в WHERE.
- Целочисленная доля — 100 пополам целочисленно это 50, но COUNT пополам — ноль; приведение типа до деления.
- Сортировка по старому имени — сортируется результат, поэтому ORDER BY пишется по алиасу вычисленной колонки.
- Забытая NULL-группа — группа с NULL в ключе существует; в отчётах её подписывают явно через COALESCE (урок 9), а не теряют.
6. Чек-лист практики и источники
Чек-лист урока 5
отметки делаются по факту
Источники для углубления:
Проверено: 23.10.2026
↑ К оглавлению