Для текущего курса необходимо установить бесплатную версию Developer (не SQL Server Express, которая использовалась в предыдущих курсах).
После установки сервер стартует автоматически в режиме автозапуска (если целенаправленно не меняли эту опцию при установке).
В Management Studio, как правило, автоматически проставляется имя установленного локального сервера. Если имя не указано, то можно указать localhost.
Простейшая группировка: подсчёт клиентов по полу
Первый запрос демонстрирует базовую группировку. Требуется вернуть пол (
Gender) и количество строк. Функция
COUNT(*) считает все строки в группе. Поскольку в запросе есть неагрегированное поле
Gender, обязательно использовать
GROUP BY. Группировка выполняется по этому полю, для каждой группы вычисляется
COUNT(*). Результат — распределение клиентов по полу: 9131 женщина, 9301 мужчина.
sql
SELECT Gender, COUNT(*) AS CustomerCount
FROM DimCustomer
GROUP BY Gender;
Группировка с присоединением таблицы географической привязки
У каждого покупателя в таблице
DimCustomer обязательно заполнен географический идентификатор, то есть отсутствуют
NULL-значения. Нам требуется получить код страны (
CountryRegionCode) и количество клиентов в каждой стране. Присоединяем таблицу
DimGeography с помощью
левого соединения (LEFT JOIN). Группируем по
CountryRegionCode и считаем количество строк.
Поскольку NULL-значений в ключе связи нет,
LEFT JOIN и
внутреннее соединение (INNER JOIN) возвращают идентичные результаты. Запрос выдаёт распределение клиентов по странам: Австралия, Канада, Германия, Франция, Великобритания, США.
sql
SELECT g.CountryRegionCode, COUNT(*) AS CustomerCount
FROM DimCustomer c
LEFT JOIN DimGeography g ON c.GeographyKey = g.GeographyKey
GROUP BY g.CountryRegionCode;
Теперь модифицируем запрос: используем
INNER JOIN и фильтруем только США (условие
WHERE). Получаем разбивку по штатам: Вашингтон — 2263, Орегон — 1073, Калифорния — 4344. Чтобы проверить общее количество клиентов в США, превратим этот запрос в подзапрос и просуммируем
CustomerCount:
sql
SELECT SUM(CustomerCount) AS TotalUSCustomers
FROM (
SELECT g.StateProvinceName, COUNT(*) AS CustomerCount
FROM DimCustomer c
INNER JOIN DimGeography g ON c.GeographyKey = g.GeographyKey
WHERE g.CountryRegionCode = 'US'
GROUP BY g.StateProvinceName
) AS T1;
Результат — 7819, что совпадает с предыдущим итогом. ORDER BY в подзапросе избыточен и не нужен.
Фильтрация групп с помощью HAVING
Переходим к данным о продажах. Нужно для каждого продукта получить его ключ (
ProductKey), английское название (
EnglishProductName) и сумму продаж за 2013 год. Соединяем таблицу фактов продаж через интернет (
FactInternetSales) с таблицей продуктов (
DimProduct) по равенству ключей. Фильтруем по дате заказа (2013 год), группируем по полям продукта и вычисляем
сумму (SUM) продаж.
sql
SELECT p.ProductKey, p.EnglishProductName, SUM(s.SalesAmount) AS TotalSales
FROM FactInternetSales s
INNER JOIN DimProduct p ON s.ProductKey = p.ProductKey
WHERE YEAR(s.OrderDate) = 2013
GROUP BY p.ProductKey, p.EnglishProductName
ORDER BY p.ProductKey;
Чтобы оставить только товары с суммой продаж больше 100 000, добавляем условие после группировки —
HAVING. В отличие от
WHERE, который фильтрует строки до агрегации,
HAVING работает с результатами агрегатных функций.
sql
SELECT p.ProductKey, p.EnglishProductName, SUM(s.SalesAmount) AS TotalSales
FROM FactInternetSales s
INNER JOIN DimProduct p ON s.ProductKey = p.ProductKey
WHERE YEAR(s.OrderDate) = 2013
GROUP BY p.ProductKey, p.EnglishProductName
HAVING SUM(s.SalesAmount) > 100000;
Подсчёт уникальных продуктов по категориям и влияние типа JOIN
Требуется для каждой категории продукта вывести её название (
EnglishProductCategoryName) и количество уникальных продуктов. Используем
COUNT(DISTINCT ProductKey). Таблицу продуктов соединяем с подкатегориями, а затем с категориями. Применим
правое соединение (RIGHT JOIN) — это гарантирует, что в результат попадут все подкатегории и категории, даже если в них нет продуктов. Группируем по названию категории.
sql
SELECT pc.EnglishProductCategoryName,
COUNT(DISTINCT p.ProductKey) AS ProductCount
FROM DimProduct p
RIGHT JOIN DimProductSubcategory ps ON p.ProductSubcategoryKey = ps.ProductSubcategoryKey
RIGHT JOIN DimProductCategory pc ON ps.ProductCategoryKey = pc.ProductCategoryKey
GROUP BY pc.EnglishProductCategoryName
ORDER BY pc.EnglishProductCategoryName;
Результат: Accessories — 29, Bikes — 97, Clothing — 35, Components — 134. Сумма уникальных продуктов здесь — 295.
Если заменить
RIGHT JOIN на
LEFT JOIN, в выборку попадут все продукты, включая те, у которых нет подкатегории (а значит, и категории). Запрос вернёт 504 уникальных продукта. Разница (209 товаров) — продукты без указанной категории. Суммирование через подзапрос подтверждает, что
RIGHT JOIN даёт 295, а
LEFT JOIN — 504.
Разница между COUNT(*) и COUNT(DISTINCT)
Возьмём предыдущий запрос с
RIGHT JOIN и заменим
COUNT(DISTINCT p.ProductKey) на
COUNT(*). Результат значительно возрастёт. Причина: в таблице
DimProduct один и тот же продукт может встречаться несколько раз (при каждом изменении цены создаётся новая строка).
COUNT(*) считает все строки, тогда как
COUNT(DISTINCT) — только уникальные значения ключа. Таким образом, при наличии дубликатов
COUNT(*) даёт большее число.
Фильтрация категорий по количеству уникальных продуктов
Добавим к запросу с
COUNT(DISTINCT) условие
HAVING, чтобы оставить только категории, где число уникальных продуктов превышает 50.
sql
SELECT pc.EnglishProductCategoryName,
COUNT(DISTINCT p.ProductKey) AS ProductCount
FROM DimProduct p
RIGHT JOIN DimProductSubcategory ps ON p.ProductSubcategoryKey = ps.ProductSubcategoryKey
RIGHT JOIN DimProductCategory pc ON ps.ProductCategoryKey = pc.ProductCategoryKey
GROUP BY pc.EnglishProductCategoryName
HAVING COUNT(DISTINCT p.ProductKey) > 50;
Возвращаются категории Bikes (97) и Components (134).
Заключение: границы базового SQL и дальнейшие шаги
Владение операциями
INSERT,
DELETE,
UPDATE, а в рамках
SELECT — агрегатными функциями, группировкой, фильтрацией
HAVING, соединениями
INNER/
LEFT/
RIGHT, подзапросами, операциями над множествами (
UNION,
EXCEPT,
INTERSECT) формирует достаточный фундамент для построения отчётов и дальнейшего обучения. Более сложные механизмы, например
оконные функции (window functions) , выходят за рамки базового уровня и в данном курсе подробно не рассматриваются.
Для самостоятельного углубления в SQL можно воспользоваться русскоязычными интерактивными ресурсами, такими как sql.ru и sqltutorial.ru. Они предлагают упражнения, позволяющие на практике отточить написание запросов.
Далее в программе курса будут рассмотрены основы оптимизации и индексирования, после чего мы перейдём к построению отчётов в
Microsoft Power BI — компактной, интуитивно понятной и набирающей популярность платформе бизнес-аналитики. Базовые навыки SQL, полученные сейчас, станут надёжной опорой для формирования эффективных отчётов.
Краткие итоги
Освоение материала строится как последовательное усложнение приёмов группировки и агрегирования, что позволяет сформировать системное понимание аналитических возможностей SQL. Вначале демонстрируется фундаментальный принцип: любое поле, не охваченное агрегатной функцией, должно попасть в
GROUP BY. На примере подсчёта клиентов по полу закрепляется связка
GROUP BY и
COUNT(*). Далее вводится взаимодействие группировки с соединениями — сначала на задаче распределения клиентов по странам. Здесь же подчёркивается важный нюанс: при отсутствии незаполненных ключей
LEFT JOIN и
INNER JOIN неразличимы, что избавляет от неверных ожиданий при смене типа соединения.
Переход к продажам привносит новую агрегатную функцию
SUM и, главное, оператор
HAVING. Слушатель учится разделять фильтрацию до группировки (
WHERE) и после неё (
HAVING), что критически важно при отборе групп по агрегированным показателям, например, при выявлении товаров с объёмом продаж выше порога.
Кульминацией становится сопоставление
RIGHT JOIN и
LEFT JOIN в сочетании с
COUNT(DISTINCT). На практическом материале раскрывается диагностическая ценность разных соединений:
RIGHT JOIN позволяет увидеть полноту охвата категорий (исключая товары без категорий), в то время как
LEFT JOIN обнажает проблемы с качеством данных, показывая «потерянные» записи. Это напрямую учит выбирать тип соединения осмысленно, в зависимости от задачи — строить ли полный справочный отчёт или искать аномалии.
Параллельно разбирается различие между
COUNT(*) и
COUNT(DISTINCT). На примере продуктов с изменяющейся ценой становится очевидно, что
COUNT(*) чувствителен к дубликатам строк, тогда как
COUNT(DISTINCT) возвращает число уникальных сущностей. Это предостерегает от автоматического использования
COUNT(*) в аналитических выборках и формирует привычку проверять структуру исходных таблиц.
Финальный аккорд — фильтрация на уровне агрегатов уникальных значений через
HAVING COUNT(DISTINCT) > N. Такой шаблон востребован в реальной отчётности, когда требуется выделить категории-лидеры по ассортименту.
Таким образом, формируется не просто набор синтаксических конструкций, а связная методика: определить цель агрегации → выбрать тип соединения → решить, что считать (строки или уникальные ключи) → применить фильтрацию на нужном этапе → при необходимости упаковать в подзапрос для итоговых расчётов. Именно эта логика, подкреплённая практическими примерами, позволяет в дальнейшем уверенно строить запросы для выгрузок и дашбордов, закладывая основу для эффективного использования инструментов бизнес-аналитики, таких как Power BI.
1. Конструкция GROUP BY определяет группы строк, по которым вычисляются агрегатные функции.
2. Все неагрегированные столбцы в SELECT должны быть перечислены в GROUP BY.
3. COUNT(*) возвращает количество строк в группе; COUNT(DISTINCT столбец) — количество уникальных значений.
4. SUM, AVG, MIN, MAX работают на уровне группы после группировки.
5. HAVING фильтрует группы по результатам агрегатных функций и выполняется после GROUP BY.
6. WHERE отбирает строки до группировки и не может ссылаться на агрегаты.
7. При отсутствии NULL в ключах связи LEFT JOIN и INNER JOIN дают идентичный результат.
8. RIGHT JOIN берёт все строки из правой таблицы; если совпадений нет, атрибуты левой таблицы заполняются NULL.
9. Замена RIGHT JOIN на LEFT JOIN может выявить записи, не имеющие связанных данных (например, товары без категории).
10. COUNT(*) считает все строки, включая дубликаты, вызванные историческими изменениями атрибутов (например, цены).
11. Подзапросы позволяют выполнять дополнительную агрегацию над промежуточными результатами.
12. Базового владения GROUP BY, JOIN, HAVING, подзапросами достаточно для построения отчётов и перехода к инструментам визуализации.
1. В чём состоит обязательное требование к полям в SELECT при наличии GROUP BY?
2. Чем WHERE принципиально отличается от HAVING при работе с агрегированными данными?
3. Может ли HAVING использоваться без GROUP BY? Приведите пример.
4. Как COUNT(DISTINCT ProductKey) и COUNT(*) дадут разные результаты на таблице, где продукт встречается несколько раз из-за изменения цены?
5. В каком случае LEFT JOIN и INNER JOIN возвращают одинаковый набор строк?
6. Какой тип соединения выбрать, чтобы гарантированно получить все категории, даже если в них нет ни одного продукта?
7. Почему при использовании RIGHT JOIN с группировкой по категории итоговое количество уникальных продуктов оказалось меньше, чем общее число продуктов в базе?
8. Для чего в примере с подсчётом клиентов США потребовалось заключать запрос с GROUP BY в подзапрос и вычислять SUM(CustomerCount)?
9. Можно ли в ORDER BY подзапроса, который используется как источник данных во FROM, изменить порядок итогового результата внешнего запроса?
10. Какие агрегатные функции вы знаете, помимо COUNT и SUM, и для каких типов данных они применимы?
11. Как с помощью HAVING COUNT(DISTINCT column) > N отобрать категории-лидеры по ассортименту?
12. Какие ресурсы можно использовать для самостоятельной практики SQL после освоения базового уровня?