Основы языка SQL

Сложные запросы с различными операторами

Излагаются принципы работы агрегатных функций SQL (COUNT, SUM, AVG, MIN, MAX) без группировки и в сочетании с подзапросами. Рассматриваются типовые сценарии: подсчёт строк и суммирование значений, вычисление средних и экстремальных величин, применение DISTINCT для уникальных значений. Особое внимание уделено коррелированным подзапросам — поиску минимальной цены внутри группы товаров, определению максимальной маржи и сравнению агрегированных показателей за разные периоды. В завершение обозначен переход к оператору GROUP BY.

Основные мысли

В результате изучения лекции слушатель будет способен:
1. Объяснить назначение и синтаксис агрегатных функций COUNT, SUM, AVG, MIN, MAX.
2. Применять агрегатные функции для получения сводных данных по всей таблице без оператора GROUP BY.
3. Анализировать различия в работе агрегатных функций с аргументом-столбцом и с ключевым словом DISTINCT.
4. Конструировать запросы с фильтрацией по диапазонам дат (BETWEEN) и соединениями нескольких таблиц.
5. Формулировать подзапросы, возвращающие агрегированные значения, для фильтрации строк внешнего запроса.
6. Разрабатывать коррелированные подзапросы, вычисляющие минимум или максимум в рамках группы, заданной альтернативным ключом.
7. Сравнивать суммарные показатели разных временных периодов, используя подзапросы в списке SELECT.
8. Интерпретировать результаты агрегации в контексте бизнес-аналитики (например, количество клиентов, сумма продаж, средняя цена товара).
Показывать лекцию целиком
Краткое изложение

Агрегатные функции без GROUP BY

Начнём с функции COUNT, которая подсчитывает количество строк. Она уже встречалась ранее, поэтому разберём сразу практический пример. Из таблицы покупателей DimCustomer получим количество клиентов с семейным положением MaritalStatus = 'M' (состоят в браке). Запрос возвращает 10 011 человек из 18 484. Если вместо столбца MaritalStatus указать первичный ключ CustomerKey, результат будет тем же — в каждой строке ровно одно значение статуса, поэтому число строк не изменится.

Перейдём к SUM. Отфильтровав женатых клиентов, посчитаем общее количество детей и общее число автомобилей во владении. Получаем 20 827 детей и 15 211 автомобилей. Добавим столбец со средним годовым доходом — функцию AVG (среднее арифметическое). Поскольку ни одного неагрегированного столбца не возвращается, группировка (GROUP BY) не нужна.

Далее — сумма продаж с округлением до двух знаков после запятой. Данные берутся из FactResellerSales (продажи в розничных точках). С помощью INNER JOIN соединяемся с таблицей магазинов DimReseller, а через неё — с географическим справочником DimGeography, чтобы отобрать только Германию. Период задаём условием BETWEEN по ключу даты OrderDateKey, который имеет формат ГГГГММДД (например, 20130101). Анализируем продажи за 2013 год.

Аналогично вычисляем сумму интернет-продаж в Соединённом Королевстве за 2013 год, используя таблицу FactInternetSales.

Функция AVG позволяет найти среднюю цену за единицу товара. Для подкатегории 3 (городские велосипеды, Touring Bikes) получаем среднюю цену продажи через интернет-магазин с точностью до двух знаков.
Далее строим запрос, который возвращает товары с ценой ниже средней по выбранным подкатегориям (1, 2 или 3). В подзапросе вычисляем среднюю цену по тем же подкатегориям. Результат сортируем по убыванию цены.

Функции MIN, MAX и DISTINCT

Рассмотрим MIN и MAX. Для подкатегории 2 (Mountain Bikes) вычисляем минимальную цену, максимальную цену и среднюю цену. Но товары могут различаться размером (значения 40, 42, 44 и т.д.) или цветом, однако цена иногда остаётся одинаковой. Чтобы средняя учитывала каждую уникальную цену ровно один раз, применяем DISTINCT внутри агрегатной функции: AVG(DISTINCT ListPrice). Без DISTINCT повторяющиеся цены повлияли бы на итог. Также подсчитываем количество товаров в подкатегории через COUNT(ProductKey).

Подзапросы с агрегатными функциями

Теперь объединяем агрегаты и подзапросы. Первый пример — поиск товаров с минимальной исторической ценой в своей группе. Берём таблицу DimProduct дважды: основная таблица P1 и её копия P2. Из P1 возвращаем ключ, английское название и цену. В условии WHERE указываем, что цена P1.ListPrice должна равняться минимальной цене из P2, где совпадают альтернативные ключи AlternateKey. Это коррелированный подзапрос: он выполняется для каждой строки P1 и находит минимум среди всех строк с тем же AlternateKey. Таким образом выбирается самая низкая цена, которая когда-либо была у каждого товара (цена может меняться со временем).

Второй подзапрос — максимальная маржа (разница между ценой продажи ListPrice и дилерской ценой DealerPrice). Из P1 выбираем ключ, название, цену продажи, дилерскую цену и вычисляем PriceDifference = ListPrice – DealerPrice. В WHERE задаём условие: разница должна быть равна максимальной разнице в истории (снова коррелированный подзапрос по тому же продукту). Результат — строка с максимальной маржой для каждого продукта.

Последний запрос демонстрирует сравнение сумм продаж за два года с помощью подзапросов в списке SELECT. Первое поле — сумма продаж из FactInternetSales за 2013 год (это подзапрос, возвращающий скаляр). Второе поле — сумма продаж за 2014 год (ещё один подзапрос). Третье поле — разница между ними. Важно: в подзапросах используется функция SUM, а не MAX, как могло показаться из описания. Логика построения запроса остаётся прежней: мы получаем константные значения для разных периодов и выполняем арифметическую операцию над ними.

Переход к GROUP BY

До сих пор мы применяли агрегатные функции ко всей таблице или фильтрованной выборке, получая одну строку. Когда же требуется вывести неагрегированные столбцы вместе с агрегатами, необходимо группировать данные с помощью GROUP BY. Этому посвящён следующий раздел курса.

Краткие итоги

Последовательное рассмотрение агрегатных функций выстраивает логику перехода от элементарных сводок к сложным аналитическим конструкциям. Сначала осваивается подсчёт количества строк и суммирование значений без группировки, что позволяет получить единый итог по отфильтрованному набору данных. Такие операции служат фундаментом для расчёта ключевых бизнес-показателей: числа клиентов в определённом сегменте, суммарной выручки, общего количества товаров.

Следующий уровень — вычисление средних, минимальных и максимальных величин. Здесь возникает важный нюанс: использование ключевого слова DISTINCT внутри агрегатной функции даёт возможность игнорировать дубликаты значений и получать статистику именно по уникальным величинам, что принципиально, например, при анализе ценовых справочников с повторяющимися ценами.

По-настоящему гибким инструментарий становится при включении подзапросов. Скалярный подзапрос в условии WHERE позволяет динамически сравнивать каждую строку с агрегированным показателем — скажем, отбирать товары дешевле средней цены по категории. Коррелированный подзапрос, опирающийся на связь через альтернативный ключ, решает задачу выбора экстремальных значений внутри групп: минимальной исторической цены товара или максимальной маржи. Такой подход имитирует функциональность оконных функций и группировки, оставаясь при этом в рамках классического SQL.

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

Практическая ценность освоенных техник очевидна: они незаменимы при построении оперативных сводок, подготовке данных для дашбордов и расчёте KPI. Понимание того, когда достаточно простой агрегации, а когда требуются подзапросы, напрямую влияет на производительность и читаемость кода. Наконец, фиксация момента, при котором без GROUP BY уже не обойтись, чётко разделяет два режима работы с агрегатами и готовит к изучению более сложных групповых операций.
Агрегатные функции без GROUP BY

Агрегатные функции COUNT, SUM, AVG, MIN, MAX обрабатывают множество строк и возвращают одно значение. Если в запросе нет столбцов, не входящих в агрегатные функции, группировка GROUP BY не требуется — результатом всегда будет одна строка.

COUNT подсчитывает количество строк. Пример: SELECT COUNT(MaritalStatus) FROM DimCustomer WHERE MaritalStatus = 'M' вернёт число клиентов в браке (10 011). Можно использовать COUNT(CustomerKey) — результат идентичен, поскольку в каждой строке есть значение первичного ключа.

SUM суммирует значения столбца. Запрос с фильтром по женатым клиентам вычисляет общее число детей SUM(TotalChildren) и автомобилей SUM(NumberCarsOwned). Добавление AVG(YearlyIncome) даёт средний годовой доход этих клиентов.

Для расчёта суммы продаж подключаются соединения. Из FactResellerSales через INNER JOIN с DimReseller и DimGeography выбираются данные по Германии. Фильтр по дате реализуется через BETWEEN с ключом OrderDateKey (формат ГГГГММДД). Аналогично вычисляется сумма интернет-продаж в Соединённом Королевстве за 2013 год.

AVG находит среднюю цену за единицу товара в определённой подкатегории (например, Touring Bikes, подкатегория 3).

Чтобы отобрать товары с ценой ниже средней по нескольким подкатегориям, применяется подзапрос в WHERE: WHERE ListPrice < (SELECT AVG(ListPrice) FROM DimProduct WHERE ProductSubcategoryKey IN (1,2,3)). Результат сортируется по убыванию цены.

MIN, MAX и DISTINCT

MIN и MAX возвращают минимальное и максимальное значения. DISTINCT внутри агрегатной функции учитывает только уникальные значения. Например, AVG(DISTINCT ListPrice) для горных велосипедов (подкатегория 2) игнорирует повторяющиеся цены у разных размеров или цветов. Без DISTINCT повторения завысили бы или занизили среднее. Также вычисляется COUNT(ProductKey) — количество товаров в подкатегории.

Подзапросы с агрегатами

Самый мощный приём — коррелированные подзапросы. Они связываются с внешним запросом через общий столбец, например AlternateKey (альтернативный ключ товара). Запрос для поиска минимальной исторической цены продукта:
sql
SELECT ProductKey, EnglishProductName, ListPrice
FROM DimProduct P1
WHERE ListPrice = (
SELECT MIN(ListPrice)
FROM DimProduct P2
WHERE P2.AlternateKey = P1.AlternateKey
)

Подзапрос выполняется для каждой строки P1, находит минимальную цену среди всех записей с тем же AlternateKey, и выбирается строка с этой ценой. Так возвращается самая низкая цена каждого товара за всю историю.

Аналогично находится максимальная маржа: разность ListPrice – DealerPrice должна быть равна максимальной разности для данного продукта. Здесь также используется коррелированный подзапрос.

Подзапросы могут размещаться в списке SELECT и возвращать скалярные значения. В примере три столбца: сумма интернет-продаж за 2013 год (подзапрос), сумма за 2014 год (второй подзапрос) и их разница. Фактически это две независимые агрегации, упакованные в одну строку.

Переход к GROUP BY

Когда требуется вместе с агрегированными показателями вывести и обычные столбцы (например, категорию и сумму продаж по ней), необходимо использовать GROUP BY. Все неагрегированные столбцы должны быть перечислены в этом операторе. Рассмотренные же техники составляют базу для дальнейшего освоения группировки.

Выводы

1. Функция COUNT возвращает количество строк; аргумент-столбец не влияет на результат, если в нём нет NULL.
2. SUM и AVG вычисляют сумму и среднее значение по столбцу, игнорируя NULL.
3. Без GROUP BY агрегатные функции схлопывают всю выборку в одну строку.
4. DISTINCT внутри агрегата позволяет оперировать только уникальными значениями.
5. Функции MIN и MAX находят наименьшее и наибольшее значения в наборе.
6. Фильтрация по датам через BETWEEN и ключ OrderDateKey удобна для выделения временных интервалов.
7. Соединения таблиц (INNER JOIN) дают доступ к связанным данным при агрегации.
8. Подзапрос в WHERE, возвращающий агрегат, динамически задаёт порог для фильтрации строк.
9. Коррелированный подзапрос позволяет найти минимум/максимум в группе, определённой альтернативным ключом.
10. Подзапросы в SELECT превращают отдельные агрегированные значения в столбцы одной строки.
11. Скалярные подзапросы для разных периодов дают возможность вычислять разницу между итогами.
12. Добавление в результат неагрегированных столбцов требует обязательного применения GROUP BY.

Вопросы для самопроверки

1. Как с помощью COUNT узнать количество покупателей, состоящих в браке, из таблицы DimCustomer?
2. Почему результат COUNT(MaritalStatus) и COUNT(CustomerKey) одинаков при отсутствии NULL?
3. Каким образом фильтр по датам с BETWEEN и ключом OrderDateKey ограничивает продажи 2013 годом?
4. Для чего нужен DISTINCT внутри AVG при расчёте средней цены, если в таблице есть повторяющиеся значения?
5. Что вернёт запрос с агрегатными функциями, если в SELECT отсутствуют неагрегированные столбцы?
6. Как построить запрос, отбирающий товары, цена которых ниже средней по их подкатегориям?
7. Объясните, как работает коррелированный подзапрос для поиска минимальной цены продукта среди всех записей с тем же AlternateKey.
8. В чём различие между подзапросом в условии WHERE и подзапросом в списке SELECT?
9. Как вычислить максимальную разницу между ListPrice и DealerPrice для каждого товара с помощью подзапроса?
10. Почему в последнем примере использована функция SUM, а не MAX, хотя речь идёт о сравнении продаж за разные годы?
11. Как получить одной строкой суммы продаж за два разных года и разность между ними?
12. В какой ситуации к агрегатному запросу необходимо добавить конструкцию GROUP BY?
Вернуться к учебному плану