Использование вложенных агрегатных функций. Язык SQL. Формирование запросов к базе данных. Использование агрегатных функций

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

  • 1) какова сумма доходов у всех жителей?
  • 2) каков наибольший и наименьший общий доход отдельного жителя?
  • 3) каков среднедушевой доход жителя Зеленограда?
  • 4) каков среднедушевой доход жителей каждой квартиры?
  • 5) сколько жителей в каждой квартире?

На языке SQL запросы такого типа можно создавать с помощью агрегатных функций и предложений GROUP BY и HAVING, используемых в операторе SELECT.

Использование агрегатных функций

Для подведения итогов по информации, содержащейся в БД, в SQL предусмотрены агрегатные функции. Агрегатная функция принимает в качестве аргумента какой-либо столбец данных целиком, а возвращает одно значение, которое определенным образом подытоживает этот столбец.

Например, агрегатная функция AVG() принимает в качестве аргумента столбец чисел и вычисляет их среднее значение.

Чтобы вычислить среднедушевой доход жителя Зеленограда, нужен такой запрос:

SELECT ‘СРЕДНЕДУШЕВОЙ ДОХОДА, AVG(SUMD) FROM PERSON

В SQL имеется шесть агрегатных функций, которые позволяют получать различные виды итоговой информации (рис. 3.16):

SUM() вычисляет сумму всех значений, содержащихся в столбце;

AVG() вычисляет среднее среди значений, содержащихся в столбце;

  • - MIN() находит наименьшее среди всех значений, содержащихся в столбце;
  • - МАХ() находит наибольшее среди всех значений, содержащихся в столбце;
  • - COUNT() подсчитывает количество значений, содержащихся в столбце;

COUNT(*) подсчитывает количество строк в таблице результатов запроса.

Аргументом агрегатной функции может быть простое имя столбца, как в предыдущем примере, или выражение, как в следующем запросе, задающем вычисление среднедушевого налога:

SELECT AVG(SUMD*0.13)

Рис. 3.16.

При выполнении этого запроса создается временный столбец, содержащий значения (SUMD*0.13) для каждой строки таблицы PERSON, а затем вычисляется среднее значение временного столбца.

Сумму доходов у всех жителей Зеленограда можно вычислить с помощью агрегатной функции SUM:

SELECT SUM(SUMD) FROM PERSON

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

SELECT SUM(MONEY)

FROM PROFIT, HAVE_D

WHERE PROFIT.ID=HAVE_D.ID

AND PROFIT.SOURCE^Стипендия’

Агрегатные функции MIN() и MAX() позволяют найти соответственно наименьшее и наибольшее значения в таблице. При этом столбец может содержать числовые или строковые значения либо значения даты или времени.

Например, можно определить:

(а) наименьший общий доход, полученный жителями, и наибольший налог, подлежащий уплате:

SELECT MIN(SUMD), MAX(SUMD*0.13)

(б) даты рождения самого старого и самого молодого жителя:

SELECT MIN(RDATE), MAX(RDATE)

(в) фамилии, имена и отчества самого первого и самого последнего жителей в списке, упорядоченном по алфавиту:

SELECT MIN(FIO), MAX(FIO)

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

При использовании функции MIN() и МАХ() со строковыми данными результат сравнения двух строк зависит от используемой таблицы кодировки символов.

Агрегатная функция COUNT() подсчитывает количество значений в столбце любого типа:

(а) сколько квартир в 1-м микрорайоне?

SELECT COUNT(ADR)

WHERE ADR LIKE *%, 1_

(б) сколько жителей имеют источники дохода?

SELECT C0UNT(DISTINCT NOM)

(в) сколько источников дохода используются жителями?

SELECT COUNT(DISTINCT ID)

Ключевое слово «DISTINCT» указывает, что подсчитываются неповторяющиеся значения в столбце.

Специальная агрегатная функция COUNT(*) подсчитывает строки в таблице результатов, а не значения данных:

(а) сколько квартир во 2-м микрорайоне?

WHERE ADR LIKE "%, 2_-%’

(б) сколько источников дохода у Иванова Ивана Ивановича?

FROM PERSON, HAVE_D

WHERE FIO = "Иванов Иван Иванович"

AND PERSON.NOM = HAVE_D.NOM

(в) сколько жителей проживает в квартире по определенному адресу?

SELECT COUNT(*) FROM PERSON WHERE ADR = "Зеленоград, 1001-45’

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

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

SELECT AVG(SUMD), SUM(SUMD), (100*AVG(MONEY/SUMD))

FROM PERSON, PROFIT, HAVE_D WHERE PERSON.NOM = HAVE_D.NOM AND HAVE_D.ID = PROFIT.ID

Без агрегатных функций запрос выглядел бы так:

SELECT SUMD, SUMD, M0NEY/SUMD FROM PERSON, PROFIT, HAVE_D WHERE PERSON.NOM = HAVE_D.NOM AND HAVE_D.ID = PROFIT.ID

и возвращал бы одну строку результатов для каждого жителя и конкретного источника дохода. Агрегатные функции используют столбцы таблицы результатов этого запроса для получения однострочной таблицы с итоговыми результатами.

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

SELECT МАХ(SUMD)-MIN(SUMD)

Однако агрегатная функция не может быть аргументом для другой агрегатной функции, т.е. запрещены вложенные агрегатные функции.

Кроме того, в списке возвращаемых столбцов нельзя одновременно использовать агрегатные функции и обычные имена столбцов, поскольку в этом нет смысла, например:

SELECT FIO, SUM(SUMD)

Здесь первый элемент списка указывает, чтобы СУБД создала таблицу, которая будет состоять из нескольких строк и содержать по одной строке для каждого жителя. Второй элемент списка просит СУБД получить одно результирующее значение, являющееся суммой значений столбца SUMD. Эти два указания противоречат друг другу, что приводит к ошибке.

Сказанное не относится к случаям обработки подзапросов и запросов с группировкой.

Важно! Если параметр функции имеет тип Строка и в нем указывается имя поля, которое содержит пробелы, то такое имя поля должно быть заключено в квадратные скобки.
Например: "[Количество Оборот]".

1. Сумма (Total) - рассчитывает сумму значений выражений, переданных ей в качестве аргумента для всех детальных записей. В качестве параметра можно передавать Массив. В этом случае функция будет применена к содержимому массива.

Пример :
Сумма(Продажи.СуммаОборот)

2. Количество (Count) - рассчитывает количество значений отличных от значения NULL. В качестве параметра можно передавать Массив. В этом случае функция будет применена к содержимому массива.

Синтаксис :
Количество([Различные] Параметр)

В указания получения различных значений следует перед параметром метода Количество указать Различные (Distinct).

Пример :
Количество(Продажи.Контрагент)
Количество(Различные Продажи.Контрагент)

3. Максимум (Maximum) - получает максимальное значение. В качестве параметра можно передавать Массив. В этом случае функция будет применена к содержимому массива.

Пример :
Максимум(Остатки.Количество)

4. Минимум (Minimum) - получает минимальное значение. В качестве параметра можно передавать Массив. В этом случае функция будет применена к содержимому массива.

Пример :
Минимум(Остатки.Количество)

5. Среднее (Average) - получает среднее значение для значений, отличных от NULL. В качестве параметра можно передавать Массив. В этом случае функция будет применена к содержимому массива.

Пример :
Среднее(Остатки.Количество)

6. Массив (Array) - формирует массив, содержащий для каждой детальной записи значение параметра.

Синтаксис :
Массив([Различные] Выражение)

В качестве параметра можно использовать таблицу значений. При этом результатом работы функции будет массив, содержащий значения первой колонки таблицы значений, переданной в качестве параметра. Если выражение содержит функцию Массив, то считается, что данное выражение является агрегатным. Если указано ключевое слово Различные, то получаемый массив не будет содержать дублирующихся значений.

Пример :
Массив(Контрагент)

7. ТаблицаЗначений (ValueTable) - формирует таблицу значений, содержащую столько колонок, сколько параметров у функции. Детальные записи получаются из наборов данных, которые нужны для получения всех полей, участвующих в выражениях параметров функции.

Синтаксис :
ТаблицаЗначений([Различные] Выражение1 [КАК ИмяКолонки1][, Выражение2 [КАК ИмяКолонки2],...])

Если параметрами функции выступают поля–остатки, то в результирующую таблицу значений попадут значения для записей по уникальным комбинациям измерений из других периодов. При этом значения получаются только для полей-остатков, измерений, счетов, полей периодов и их реквизитов. Значениями остальных полей в записях из других периодов считаются равными NULL. Если выражение содержит функцию ТаблицаЗначений, то считается, что данное выражение является агрегатным. Если указано ключевое слово Различные, то в получаемой таблице значений не будет строк, содержащих одинаковые данные. После каждого параметра может располагаться необязательное ключевое слово КАК и имя, которое будет назначено колонке таблицы значений.

Пример :
ТаблицаЗначений(Различные Номенклатура, ХарактеристикаНоменклатуры КАК Характеристика)

8. Свернуть (GroupBy) - предназначена для удаления дубликатов из массива.

Синтаксис :
Свернуть(Выражение, НомераКолонок)

Параметры :

  • Выражение - выражение типа Массив или ТаблицаЗначений, значения элементов которого нужно свернуть;
  • НомераКолонок - (если выражение имеет тип ТаблицаЗначений) тип Строка. Номера или имена (через запятую) колонок таблицы значений, среди которых нужно искать дубликаты. По умолчанию – все колонки.
Пример :
Свернуть(ТаблицаЗначений(НомерТелефона, Адрес) ,"НомерТелефона");

9. ПолучитьЧасть (GetPart) - получает таблицу значений, содержащую определенные колонки из исходной таблицы значений.

Синтаксис :
ПолучитьЧасть(Выражение, НомераКолонок)

Параметры :

  • Выражение - тип ТаблицаЗначений. Таблица значений, из которой нужно получить колонки;
  • НомераКолонок - тип Строка. Номера или имена (через запятую) колонок таблицы значений, которые нужно получить.
Возвращаемое значение: ТаблицаЗначений, в которой имеются только колонки, которые указаные в параметре.

Пример :
ПолучитьЧасть(Свернуть(ТаблицаЗначений(НомерТелефона, Адрес) ,"НомерТелефона"),"НомерТелефона");

10. Упорядочить (Order) - предназначена для упорядочивания элементов массива и таблицы значений.

Синтаксис :
Упорядочить(Выражение, НомераКолонок)

Параметры :

  • Выражение - Массив или ТаблицаЗначений, из которой нужно получить колонки;
  • НомераКолонок - (если выражение имеет тип ТаблицаЗначений) номера или имена (через запятую) колонок таблицы значений, по которым нужно упорядочить. Может содержать направление упорядочивания и необходимость автоупорядочивания: Убыв/Возр + Автоупорядочивание.
Возвращаемое значение: Массив или ТаблицаЗначений, с упорядоченными элементами.

Пример :
Упорядочить(ТаблицаЗначений(НомерТелефона, Адрес, ДатаЗвонка),"ДатаЗвонка Убыв");

11. СоединитьСтроки (JoinStrings) - предназначена для объединения строк в одну строку.

Синтаксис :
СоединитьСтроки (Значение, РазделительЭлементов, РазделителиКолонок)

Параметры :

  • Значение - выражения, которые нужно объединить в одну строку. Если является Массивом, то в строку будут объединяться элементы массива. Если является ТаблицаЗначений, то в строку будут объединяться все колонки и строки таблицы;
  • РазделительЭлементов - строка, содержащая текст, который нужно использовать в качестве разделителя между элементами массива и строками таблицы значений. По умолчанию – символ перевода строк;
  • РазделителиКолонок - строка, содержащая текст, который нужно использовать в качестве разделителя между колонками таблицы значений. По умолчанию "; ".
Пример :
СоединитьСтроки(ТаблицаЗначений(НомерТелефона, Адрес));

12. ГрупповаяОбработка (GroupProcessing) - возвращает объект ДанныеГрупповойОбработкиКомпоновкиДанных. В объект в свойство Данные помещается в виде таблицы значений значения группировок для каждого выражения, указанного в параметре функции Выражения. В случае использования иерархической группировки каждый уровень иерархии обрабатывается отдельно. Значения для иерархических записей также помещаются в данные. В свойство ТекущийЭлемент объекта помещается строка таблицы значений, для которой в настоящий момент вычисляется функция.

Синтаксис :
ГрупповаяОбработка(Выражения, ВыраженияИерархии, ИмяГруппировки)

Параметры :

  • Выражения . Выражения, которые нужно вычислить. Строка, в которой через запятую перечислены выражения, которые нужно вычислить. После каждого выражение возможно наличие необязательного ключевого слова КАК и имени колонки результирующей таблицы значений. Каждое выражение образует колонку таблицы значений свойства Данные объекта ДанныеГрупповойОбработкиКомпоновкиДанных.
  • ВыраженияИерархии . Выражения, которые нужно вычислить для иерархических записей. Аналогично параметру Выражения с тем отличием, что параметр ВыраженияИерархии используется для иерархических записей. Если параметр не указан, то для вычисления значений для иерархических записей используется выражения, указанные в параметре Выражение.
  • ИмяГруппировки . Имя группировки, в которой нужно вычислять группировку обработки. Строка. Если не указано, то вычисление происходит в текущей группировке. Если вычисление идет в таблице и параметр содержит пустую строку, или не указан, то значение вычисляется для группировки – строки. Компоновщик макета при генерации макета компоновки данных заменяет данное имя на имя группировки в результирующем макете. Если группировка не доступна, то функция будет заменена на значение NULL.
13. Каждый (Every) - если хоть одна запись имеет значение Ложь, то результат Ложь, иначе Истина.

Синтаксис :
Каждый(Выражение)

Параметр :

  • Выражение - тип Булево.
Пример :
Каждый()

14. Любой (Any) - если хоть одна запись имеет значение Истина, то результат Истина, иначе Ложь

Синтаксис :
Любой(Выражение)

Параметр :

  • Выражение - тип Булево.
Пример :
Любой()

15. СтандартноеОтклонениеГенеральнойСовокупности (Stddev_Pop) - вычисляет стандартное отклонение совокупности. Вычисляется по формуле: SQRT(ДисперсияГенеральнойСовокупности(X)).

Синтаксис :
СтандартноеОтклонениеГенеральнойСовокупности(Выражение)

Параметр :

  • Выражение - тип Число.

Пример :

X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ СтандартноеОтклонениеГенеральнойСовокупности(Y) ИЗ Таблица
Результат: 805.694444

16. СтандартноеОтклонениеВыборки (Stddev_Samp) - вычисляет совокупное типовое стандартное отклонение. Вычисляется по формуле: SQRT(ДисперсияВыборки(X)).

Синтаксис :
СтандартноеОтклонениеВыборки(Выражение)

Параметр :

  • Выражение - тип Число.
Тип возвращаемого значения Число.

Пример :

X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ СтандартноеОтклонениеВыборки(Y) ИЗ Таблица
Результат: 28.3847573

17. ДисперсияВыборки (Var_Samp) - вычисляет типовое различие ряда чисел без учета значений NULL в этом наборе. Вычисляется по формуле: (Сумма(X^2) - Сумма(X)^2 / Количество(X)) / (Количество(X) - 1). Если Количество(X) = 1, то возвращается значение NULL.

Синтаксис :
ДисперсияВыборки(Выражение)

Параметр :

  • Выражение - тип Число.
Пример :
ВЫБРАТЬ ДисперсияГенеральнойСовокупности(Y) ИЗ Таблица
Результат: 716.17284

19. КовариацияГенеральнойСовокупности (Covar_Pop) - вычисляет ковариацию ряда числовых пар. Вычисляется по формуле: (Сумма(Y * X) - Сумма(X) * Сумма(Y) / n) / n, где n число пар (Y, X) в которых ни Y ни X не являются NULL.

Синтаксис :
КовариацияГенеральнойСовокупности(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ КовариацияГенеральнойСовокупности(Y, X) ИЗ Таблица
Результат: 59.4444444

20. КовариацияВыборки (Covar_Samp) - вычисляет типовое различие ряда чисел без учета значений NULL в этом наборе. Вычисляется по формуле: (Сумма(Y * X) - Сумма(Y) * Сумма(X) / n) / (n-1), где n число пар (Y, X) в которых ни Y ни X не являются NULL.

Синтаксис :
КовариацияВыборки(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ КовариацияВыборки(Y, X) ИЗ Таблица
Результат: 66.875

21. Корреляция (Corr) - вычисляет коэффициент корреляции ряда числовых пар. Вычисляется по формуле: КовариацияГенеральнойСовокупности(Y, X) / (СтандартноеОтклонениеГенеральнойСовокупности(Y) * СтандартноеОтклонениеГенеральнойСовокупности(X)). Не учитываются пары, в которых Y или X равны NULL.

Синтаксис :
Корреляция(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ Корреляция(X, Y) ИЗ Таблица
Результат: 0.860296149

22. РегрессияНаклон (Regr_Slope) - вычисляет наклон линии. Вычисляется по формуле: КовариацияГенеральнойСовокупности(Y, X) / ДисперсияГенеральнойСовокупности(X). Вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияНаклон(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ РегрессияНаклон(Y, X) ИЗ Таблица
Результат: 8.91666667

23. РегрессияОтрезок (Regr_Intercept) - вычисляет Y-точку пересечения линии регресса. Вычисляется по формуле: Среднее(Y) - РегрессияНаклон(Y, X) * Среднее(X). Вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияОтрезок(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
ВЫБРАТЬ РегрессияКоличество(Y, X) ИЗ Таблица
Результат: 9

25. РегрессияR2 (Regr_R2) - вычисляет коэффициент детерминации. Вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияR2(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Возвращаемое значение:
  • Null - если ДисперсияГенеральнойСовокупности(X) = 0;
  • 1 - если ДисперсияГенеральнойСовокупности(Y)=0 И ДисперсияГенеральнойСовокупности(X)<>0;
  • POW(Корреляция(Y,X),2) - если ДисперсияГенеральнойСовокупности(Y)>0 И ДисперсияГенеральнойСовокупности(X)<>0.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ РегрессияR2(Y, X) ИЗ Таблица
Результат: 0.740109464

26. РегрессияСреднееX (Regr_AvgX) - вычисляет среднее число X после исключения X и Y пар, где или X или Y являются пустыми. Среднее(X) вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияСреднееX(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ РегрессияСреднееX(Y, X) ИЗ Таблица
Результат: 5

27. РегрессияСреднееY (Regr_AvgY) - вычисляет среднее число Y после исключения X и Y пар, где или X или Y являются пустыми. Среднее(Y) вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияСреднееY(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ РегрессияСреднееY(Y, X) ИЗ Таблица
Результат: 24.2222222

28. РегрессияSXX (Regr_SXX) - вычисляется по формуле: РегрессияКоличество(Y, X) * ДисперсияГенеральнойСовокупности(X). Вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияSXX(Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Возвращает сумму квадратов независимых выражений, используемых в линейной модели регресса. Функция может использоваться, чтобы оценить статистическую обоснованность модели регресса.

Пример :
ВЫБРАТЬ РегрессияSYY(Y, X) ИЗ Таблица
Результат: 6445.55556

30. РегрессияSXY (Regr_SXY) - вычисляется по формуле: РегрессияКоличество(Y, X) * КовариацияГенеральнойСовокупности(Y, X). Вычисляется без учета пар, содержащих NULL.

Синтаксис :
РегрессияSXY (Y, X)

Параметры :

  • Y - тип Число;
  • X - тип Число.
Пример :
X 1 2 3 4 5 6 7 8 9
Y 7 1 2 5 7 34 32 43 87
ВЫБРАТЬ РегрессияSXY(Y, X) ИЗ Таблица
Результат: 535

31. МестоВПорядке (Rank)

Синтаксис :
МестоВПорядке(Порядок, ПорядокИеррахии, ИмяГруппировки)

Параметры :

  • Порядок – тип Строка. Содержит выражения, в последовательности которых нужно расположить групповые записи, разделенные через запятую. Направление упорядочивания управляется при помощи слов Возр, Убыв. После поля также можно указать строку Автоупорядочивание, что обозначает, что при упорядочивании ссылок нужно использовать поля упорядочивания, определенные для объекта, на который ссылка. Если последовательность не указана, то значение рассчитывается в последовательности группировки;
  • ПорядокИеррахии – тип Строка. Содержит выражения упорядочивания для иерархических записей;
  • ИмяГруппировки – тип Строка. Имя группировки, в которой нужно вычислять группировку обработки. Если не указано, то вычисление происходит в текущей группировке. Если вычисление идет в таблице и параметр содержит пустую строку, или не указан, то значение вычисляется для группировки – строки. Компоновщик макета при генерации макета компоновки данных заменяет данное имя на имя группировки в результирующем макете. Если группировка не доступна, то функция будет заменена на значение NULL.
Если в последовательности имеются две или более записей с одинаковыми значениями полей упорядочивания, то для всех записей функция возвращает одинаковые значения.

Пример :
МестоВПорядке("[Количество Оборот]")

32. КлассификацияABC (ClassificationABC)

Синтаксис :
КлассификацияABC(Значение, КоличествоГрупп, ПроцентыДляГрупп, ИмяГруппировки)

Параметры :

  • Значение – тип Строка. по которому нужно рассчитывать классификацию. Строка, в которой указано выражение;
  • КоличествоГрупп - тип Число. Задает количество групп, на который нужно разбить;
  • ПроцентыДляГрупп - тип Строка. Столько, на сколько групп нужно разбить минус 1. Через запятую. Если не задано, то автоматически;
  • ИмяГруппировки - тип Строка. Имя группировки, в которой нужно вычислять группировку обработки. Если не указано, то вычисление происходит в текущей группировке. Если вычисление идет в таблице и параметр содержит пустую строку, или не указан, то значение вычисляется для группировки – строки. Компоновщик макета при генерации макета компоновки данных заменяет данное имя на имя группировки в результирующем макете. Если группировка не доступна, то функция будет заменена на значение NULL.
Результатом работы функции будет номер класса, начиная с 1, который соответствует классу A.

Пример :
КлассификацияABC("Сумма(ВаловаяПрибыль)", 3, "60, 90")

В SQL добавлены дополнительные функции, которые позволяют вычислять обобщенные групповые значения. Для применения агрегатных функций предполагается предварительная операция группировки. При группировке все множество кортежей отношения разбивается на группы, в которых собираются кортежи, имеющие одинаковые значения атрибутов, которые заданы в списке группировки.

Например, сгруппируем отношение R1 по значению столбца Дисциплина. Мы получим 4 группы, для которых можем вычислить некоторые групповые значения, например количество кортежей в группе, максимальное или минимальное значение столбца Оценка.

Это делается с помощью агрегатных функций. Агрегатные функции вычисляют одиночное значение для всей группы таблицы. Список этих функций представлен в табл. 7.

Агрегатные функции используются подобно именам полей в операторе SELECT, но с одним исключением: они берут имя поля как аргумент. С функциями SUM и AVG могут использоваться только числовые поля. С функциями COUNT, MAX и MIN могут использоваться как числовые, так и символьные поля. При использовании с символьными полями MAX и MIN будут транслировать их в эквивалент ASCII кода и обрабатывать в алфавитном порядке. Некоторые СУБД позволяют использовать вложенные агрегаты, но это является отклонением от стандарта ANSI со всеми вытекающими отсюда последствиями.

Например, можно вычислить количество студентов, сдававших экзамены по каждой дисциплине. Для этого надо выполнить запрос с группировкой по полю "Дисциплина" и вывести в качестве результата название дисциплины и количество строк в группе по данной дисциплине. Применение символа * в качестве аргумента функции COUNT означает подсчет всех строк в группе.

SELECT R1.Дисциплина, COUNT(*)FROM R1GROUP BY R1.Дисциплина

Результат:

Пример. Получить список дисциплин, по которым сдали экзамен не менее 5 человек:

SELECT R1.ДисциплинаFROM R1GROUP BY R1.ДисциплинаHAVING COUNT(*) >= 5 Результат: Здесь инструкция HAVING выбирает группы, удовлетворяющие заданному условию.

Вложенные запросы

С помощью SQL можно вкладывать запросы внутрь друг друга. Обычно внутренний запрос генерирует значение, которое проверяется в предикате внешнего запроса (в предложении WHERE или HAVING), определяющего, верно оно или нет. Совместно с подзапросом можно использовать предикат EXISTS, который возвращает истину, если вывод подзапроса не пуст.

Отношение D (Детали)

Отношение PD (Поставки)

1. Получить список поставщиков, статус которых меньше максимального статуса в таблице поставщиков (сравнение с подзапросом):

WHERE P.STATYS <

(SELECT MAX(P.STATUS)

Замечание . Т.к. поле P.STATUS сравнивается с результатом подзапроса, то подзапрос должен быть сформулирован так, чтобы возвращать таблицу, состоящую ровно из одной строки и одной колонки .

Замечание

  1. Выполнить один раз вложенный подзапрос и получить максимальное значение статуса.
  2. Просканировать таблицу поставщиков P, каждый раз сравнивая значение статуса поставщика с результатом подзапроса, и отобрать только те строки, в которых статус меньше максимального.

2. Использование предиката IN

(SELECT DISTINCT PD.PNUM

WHERE PD.DNUM = 2);

Замечание . В данном случае вложенный подзапрос может возвращать таблицу, содержащую несколько строк.

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

  1. Выполнить один раз вложенный подзапрос и получить список номеров поставщиков, поставляющих деталь номер 2.
  2. Просканировать таблицу поставщиков P, каждый раз проверяя, содержится ли номер поставщика в результате подзапроса.

3. Использование предиката EXISTS . Получить список поставщиков, поставляющих деталь номер 2:

PD.PNUM = P.PNUM AND

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

  1. Просканировать таблицу поставщиков P, каждый раз выполняя подзапрос с новым значением номера поставщика, взятым из таблицы P.
  2. В результат запроса включить только те строки из таблицы поставщиков, для которых вложенный подзапрос вернул непустое множество строк.

Замечание . В отличие от двух предыдущих примеров, вложенный подзапрос содержит параметр (внешнюю ссылку), передаваемый из основного запроса - номер поставщика P.PNUM. Такие подзапросы называются коррелируемыми (correlated ). Внешняя ссылка может принимать различные значения для каждой строки-кандидата, оцениваемого с помощью подзапроса, поэтому подзапрос должен выполняться заново для каждой строки, отбираемой в основном запросе. Такие подзапросы характерны для предиката EXISTS, но могут быть использованы и в других подзапросах.

Замечание . Может показаться, что запросы, содержащие коррелируемые подзапросы будут выполняться медленнее, чем запросы с некоррелируемыми подзапросами. На самом деле это не так, т.к. то, как пользователь, сформулировал запрос, не определяет , как этот запрос будет выполняться. Язык SQL является непроцедурным, а декларативным. Это значит, что пользователь, формулирующий запрос, просто описывает, каким должен быть результат запроса , а как этот результат будет получен - за это отвечает сама СУБД.

4. Использование предиката NOT EXISTS . Получить список поставщиков, не поставляющих деталь номер 2:

WHERE NOT EXISTS

PD.PNUM = P.PNUM AND

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

5. Получить имена поставщиков, поставляющих все детали:

SELECT DISTINCT PNAME

WHERE NOT EXISTS

WHERE NOT EXISTS

PD.DNUM = D.DNUM AND

PD.PNUM = P.PNUM));

Замечание . Данный запрос содержит два вложенных подзапроса и реализует реляционную операцию деления отношений .

Самый внутренний подзапрос параметризован двумя параметрами (D.DNUM, P.PNUM) и имеет следующий смысл: отобрать все строки, содержащие данные о поставках поставщика с номером PNUM детали с номером DNUM. Отрицание NOT EXISTS говорит о том, что данный поставщик не поставляет данную деталь. Внешний к нему подзапрос, сам являющийся вложенным и параметризованным параметром P.PNUM, имеет смысл: отобрать список деталей, которые не поставляются поставщиком PNUM. Отрицание NOT EXISTS говорит о том, что для поставщика с номером PNUM не должно быть деталей, которые не поставлялись бы этим поставщиком. Это в точности означает, что во внешнем запросе отбираются только поставщики, поставляющие все детали.

Внешние соединения

Часто необходимо объединять таблицы таким образом, чтобы в результат попали все строки из первой таблицы, а вместо тех строк второй таблицы, для которых не выполнено условие соединения, в результат попадали бы неопределенные значения. Или наоборот, включаются все строки из правой (второй) таблицы, а отсутствующие части строк из первой таблицы дополняются неопределенными значениями. Такие объединения были названы внешними.

В общем случае синтаксис части FROM в стандарте SQL2 выглядит следующим образом:

FROM <список исходных таблиц>< выражение естественного соединения >< выражение соединения >< выражение перекрестного соединения >< выражение запроса на объединение ><список исходных таблиц>::= <имя_таблицы_1> [ имя синонима таблицы_1] [ …] [,<имя_таблицы_n>[ <имя синонима таблицы_n> ] ]<выражение естественного соединения>:: =<имя_таблицы_1> NATURAL { INNER | FULL LEFT | RIGHT } JOIN <имя_таблицы_2><выражение перекрестного соединения>:: = <имя_таблицы_1> CROSS JOIN <имя_таблицы_2><выражение запроса на объединение>::=<имя_таблицы_1> UNION JOIN <имя_таблицы_2><выражение соединения>::= <имя_таблицы_1> { INNERFULL | LEFT | RIGHT } JOIN {ON условие | } <имя_таблицы_2>

В этих определениях INNER - означает внутреннее (естественное) соединение, LEFT - левое соединение, то есть в результат входят все строки таблицы 1, а части результирующих кортежей, для которых не было соответствующих значений в таблице 2, дополняются значениями NULL (неопределено). Ключевое слово RIGHT означает правое внешнее соединение, и в отличие от левого соединения в этом случае в результирующее отношение включаются все строки таблицы 2, а недостающие части из таблицы 1 дополняются неопределенными значениями. Ключевое слово FULL определяет полное внешнее соединение: и левое и правое. При полном внешнем соединении выполняются и правое и левое внешние соединения и в результирующее отношение включаются все строки из таблицы 1, дополненные неопределенными значениями, и все строки из таблицы 2, также дополненные неопределенными значениями.

Ключевое слово OUTER означает внешнее, но если заданы ключевые слова FULL, LEFT, RIGHT, то соединение всегда считается внешним.

Рассмотрим примеры выполнения внешних соединений. Снова вернемся к БД "Сессия". Создадим отношение, в котором будут стоять все оценки, полученные всеми студентами по всем экзаменам, которые они должны были сдавать. Если студент не сдавал данного экзамена, то вместо оценки у него будет стоять неопределенное значение. Для этого выполним последовательно естественное внутреннее соединение таблиц R2 и R3 по атрибуту Группа, а полученное отношение соединим левым внешним естественным соединением с таблицей R1, используя столбцы ФИО и Дисциплина. При этом в стандарте разрешено использовать скобочную структуру, так как результат соединения может быть одним из аргументов в части FROM оператора SELECT.

SELECT R1.ФИО, R1.Дисциплина, R1.ОценкаFROM (R2 NATURAL INNER JOIN R3) LEFT JOIN R1 USING (ФИО, Дисциплина)

Результат:

ФИО Дисциплина Оценка
Петров Ф. И. Базы данных
Сидоров К. А. Базы данных
Миронов А. В. Базы данных
Степанова К. Е. Базы данных
Крылова Т. С Базы данных
Владимиров В. А. Базы данных
Петров Ф. И. Теория информации Null
Сидоров К. А. Теория информации
Миронов А. В. Теория информации Null
Степанова К. Е. Теория информации
Крылова Т. С Теория информации

Пример 21 . Получить общее количество поставщиков (ключевое слово COUNT ):

SELECT COUNT(*) AS N

В результате получим таблицу с одним столбцом и одной строкой, содержащей количество строк из таблицы P:

Использование агрегатных функций с группировками

Пример 23 . Для каждой детали получить суммарное поставляемое количество (ключевое слово GROUP BY …):

SUM(PD.VOLUME) AS SM

GROUP BY PD.DNUM;

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

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

SUM(PD.VOLUME) AS SM

GROUP BY PD.DNUM;

Причина ошибки в том, что в список отбираемых полей включено поле PNUM, которое не входит в раздел GROUP BY. И действительно, в каждую полученную группу строк может входить несколько строк с различными значениями поля PNUM. Из каждой группы строк будет сформировано по одной итоговой строке. При этом нет однозначного ответа на вопрос, какое значение выбрать для поля PNUM в итоговой строке.

Замечание . Некоторые диалекты SQL не считают это за ошибку. Запрос будет выполнен, но предсказать, какие значения будут внесены в поле PNUM в результатирующей таблице, невозможно.

Пример 24 . Получить номера деталей, суммарное поставляемое количество которых превосходит 400 (ключевое слово HAVING …):

Замечание . Условие, что суммарное поставляемое количество должно быть больше 400 не может быть сформулировано в разделе WHERE, т.к. в этом разделе нельзя использовать агрегатные функции. Условия, использующие агрегатные функции должны быть размещены в специальном разделе HAVING:

SUM(PD.VOLUME) AS SM

GROUP BY PD.DNUM

HAVING SUM(PD.VOLUME) > 400;

В результате получим следующую таблицу:

Замечание . В одном запросе могут встретиться как условия отбора строк в разделе WHERE, так и условия отбора групп в разделе HAVING. Условия отбора групп нельзя перенести из раздела HAVING в раздел WHERE. Аналогично и условия отбора строк нельзя перенести из раздела WHERE в раздел HAVING, за исключением условий, включающих поля из списка группировки GROUP BY.

Использование подзапросов

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

Пример 25 . Получить список поставщиков, статус которых меньше максимального статуса в таблице поставщиков (сравнение с подзапросом):

WHERE P.STATYS <

(SELECT MAX(P.STATUS)

Замечание . Т.к. поле P.STATUS сравнивается с результатом подзапроса, то подзапрос должен быть сформулирован так, чтобы возвращать таблицу, состоящую ровно из одной строки и одной колонки .

Замечание

    Выполнить один раз вложенный подзапрос и получить максимальное значение статуса.

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

Пример 26 . Использование предиката IN

(SELECT DISTINCT PD.PNUM

WHERE PD.DNUM = 2);

Замечание . В данном случае вложенный подзапрос может возвращать таблицу, содержащую несколько строк.

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

    Выполнить один раз вложенный подзапрос и получить список номеров поставщиков, поставляющих деталь номер 2.

    Просканировать таблицу поставщиков P, каждый раз проверяя, содержится ли номер поставщика в результате подзапроса.

Пример 27 . Использование предиката EXIST . Получить список поставщиков, поставляющих деталь номер 2:

PD.PNUM = P.PNUM AND

Замечание . Результат выполнения запроса будет эквивалентен результату следующей последовательности действий:

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

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

Замечание . В отличие от двух предыдущих примеров, вложенный подзапрос содержит параметр (внешнюю ссылку), передаваемый из основного запроса - номер поставщика P.PNUM. Такие подзапросы называются коррелируемыми (correlated ). Внешняя ссылка может принимать различные значения для каждой строки-кандидата, оцениваемого с помощью подзапроса, поэтому подзапрос должен выполняться заново для каждой строки, отбираемой в основном запросе. Такие подзапросы характерны для предиката EXIST, но могут быть использованы и в других подзапросах.

Замечание . Может показаться, что запросы, содержащие коррелируемые подзапросы будут выполняться медленнее, чем запросы с некоррелируемыми подзапросами. На самом деле это не так, т.к. то, как пользователь, сформулировал запрос, не определяет , как этот запрос будет выполняться. Язык SQL является непроцедурным, а декларативным. Это значит, что пользователь, формулирующий запрос, просто описывает, каким должен быть результат запроса , а как этот результат будет получен - за это отвечает сама СУБД.

Пример 28 . Использование предиката NOT EXIST . Получить список поставщиков, не поставляющих деталь номер 2:

PD.PNUM = P.PNUM AND

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

Пример 29 . Получить имена поставщиков, поставляющих все детали:

SELECT DISTINCT PNAME

PD.DNUM = D.DNUM AND

PD.PNUM = P.PNUM));

Замечание . Данный запрос содержит два вложенных подзапроса и реализует реляционную операцию деления отношений .

Самый внутренний подзапрос параметризован двумя параметрами (D.DNUM, P.PNUM) и имеет следующий смысл: отобрать все строки, содержащие данные о поставках поставщика с номером PNUM детали с номером DNUM. Отрицание NOT EXIST говорит о том, что данный поставщик не поставляет данную деталь. Внешний к нему подзапрос, сам являющийся вложенным и параметризованным параметром P.PNUM, имеет смысл: отобрать список деталей, которые не поставляются поставщиком PNUM. Отрицание NOT EXIST говорит о том, что для поставщика с номером PNUM не должно быть деталей, которые не поставлялись бы этим поставщиком. Это в точности означает, что во внешнем запросе отбираются только поставщики, поставляющие все детали.

Как вы помните, различные элементы определения окна (секционирование, упорядочение и кадрирование) по сути являются различными вариантами фильтрации. Существуют другие потребности в фильтрации, которые эти определения не в состоянии удовлетворить. Некоторые из этих потребностей удается удовлетворить с помощью предложения FILTER , которое не было реализовано в SQL Server 2012. Есть также попытки решить эту проблему за счет внесения предложений по расширению стандарта, которые, я надеюсь, так или иначе появятся в стандарте и SQL Server.

Начну с предложения FILTER. В стандарте определено, что в функциях агрегирования это предложение позволяет фильтровать набор строк, к которому применяется агрегирование, с использованием предиката, формат этого предложения выглядит так:

В качестве примера приведу запрос, вычисляющий разницу между текущим количеством и среднемесячным количеством для сотрудника до текущей даты (не месяца текущей строки):

SQL Server 2012 пока не поддерживает предложение FILTER. Честно говоря, я не знаю СУБД, которая поддерживала его. Если вам нужна такая возможность, существует довольно простое альтернативное решение - использовать в качестве входных данных для функции агрегирования выражение CASE :

<функция агрегирования>(CASE WHEN <условие поиска> THEN <входное выражение> END)

Вот полный запрос, который решает ту же задачу:

SELECT empid, ordermonth, qty, qty - AVG(CASE WHEN ordermonth <= DATEADD(month, -3, CURRENT_TIMESTAMP) THEN qty END) OVER(PARTITION BY empid) AS diff FROM Sales.EmpOrders;

Чего все еще не хватает в стандарте (начиная с версии SQL 2008) и SQL Server 2012, так это возможности ссылаться на элементы текущей строки для целей фильтрации. Это можно было бы применять в предложении FILTER, в альтернативном решении с использованием выражения CASE, а также в других случаях, в которых нужна фильтрация.

Для демонстрации этой потребности представьте на секундочку, что на элемент текущей строки можно ссылаться с помощью префикса $current_row. А теперь представим себе, что нужно написать запрос представления Sales.OrderValues, который бы вычислял для каждого заказа разницу между значением текущего заказа и средним значением для определенного сотрудника для всех клиентов кроме того клиента, которому принадлежит этот заказ. Эта задача решается следующим запросом с предложением FILTER:

В качестве альтернативы можно воспользоваться выражением CASE:

Не работает в T-SQL SELECT orderid, orderdate, empid, custid, val, val - AVG(CASE WHEN custid <> $current_row.custid THEN val END) OVER(PARTITION BY empid) AS diff FROM Sales.OrderValues;

Ещё раз напомню, что это всего лишь мои выдумки для иллюстрации того, чего не хватает в стандарте языка, поэтому, как говорится, «не пытайтесь повторить это у себя дома».

Предложение по улучшению

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

Паттерны в последовательностях строк определяются с применением семантики, похожей на регулярные выражения. Этот механизм может применяться для определения табличного выражения, а также для фильтрации строк в определении окна. Он также может использоваться в технологиях потоковой передачи данных, например с StreamInsight в SQL Server, а также в запросах, которые работают с неперемещаемыми данными. Вот ссылка на предоставленный для всеобщего доступа документ: http://www.softwareworkshop.com/h2/SQL-RPR-review-paper.pdf . Прежде чем читать этот документ, я предлагаю освободить голову от лишних мыслей и хорошенько взбодриться кофе. Это непросто чтение, но идея исключительно интересна и я надеюсь, что она пробьет себе путь в стандарт SQL и будет использоваться не только для данных в движении, но и для неактивных данных.

Ключевое слово DISTINCT в функциях агрегирования

SQL Server 2012 не поддерживает параметр DISTINCT в оконных функциях агрегирования. Представьте, что вам нужно запрашивать представление Sales.OrderValues и получить для каждого заказа число конкретных клиентов, с которыми работал текущий сотрудник с начала и до текущей даты. Вам нужно выполнить такой запрос:

Не работает в T-SQL SELECT empid, orderdate, orderid, val, COUNT(DISTINCT custid) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM Sales.OrderValues;

Но поскольку этот запрос не поддерживается, нужно искать обходное решение. Один из вариантов - прибегнуть к помощи функции ROW_NUMBER. Я расскажу о ней подробнее чуть попозже, а пока достаточно будет сказать, что она возвращает уникальное целое значение для каждой строки секции, начиная с единицы и с шагом 1, в соответствии с определением упорядочения в окне. С помощью функции ROW_NUMBER можно назначить строкам номера, секционированные по empid и custid и упорядоченные по orderdate. Это означает, что строки с номером 1 относятся к первому случаю работы сотрудника с данным клиентом при упорядочении заказов по датам. Используя выражение CASE можно вернуть значение custid, только если номер строки равен 1, а в противном случае вернуть NULL. Вот запрос, реализующий описанную логику, с результатом его работы:

SELECT empid, orderdate, orderid, custid, val, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.OrderValues;

Заметьте, что для каждого сотрудника возвращается только первое значение custid при условии упорядочения по дате, а для последующих значений возвращаются NULL. Следующий шаг заключается в определении обобщенного табличного значения (CTE) на основе предыдущего запроса, а затем применении агрегирования текущего числа строк к результату выражения CASE:

WITH C AS (SELECT empid, orderdate, orderid, custid, val, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.OrderValues) SELECT empid, orderdate, orderid, val, COUNT(distinct_custid) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM C;

Вложенные агрегаты

На данный момент вы знаете, что есть групповые и оконные агрегаты. Как уже говорилось, функции при этом используются одинаковые, но контекст разный. Групповые агрегаты работают на основе групп строк, определенных предложением GROUP BY и возвращают одно значение на группу. Оконные агрегаты действуют на основе окон строк и возвращают одно значение для каждой строки в базовом запросе. Вспомните рассказ о логической обработке запросов из статьи Запросы . Напомню порядок, в котором в соответствии с концепцией должны обрабатываться различные предложения запросов:

Групповые агрегаты используются, когда запрос является групповым, и они разрешены в фазах, которые обрабатываются после определения групп, а именно, начиная с фазы 4 и далее. Помните, что в результате запроса каждая группа представлена только одной строкой. Оконные агрегаты разрешены, начиная с фазы 5 и последующих, потому что они работают на основе строк базового запроса - после фазы HAVING.

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

Это совершенно легальный, но на первый взгляд странный подход - применять оконный агрегат к окну, содержащему строки с атрибутами, полученными с применением групповых агрегатов. Я сказал «странный», потому что на первый взгляд выражение SUM(SUM(val)) в запросе выглядит неуместным. Но оно имеет право на существование. Посмотрите на запрос, который решает поставленную задачу:

Групповой агрегат SUM(val) вычисляет общую сумму цены всех заказов для каждого сотрудника. Это означает, что в результате базового запроса есть строка для каждого сотрудника с этой общей суммой. После этого оконный агрегат вычисляет сумму сумм для отдельных сотрудников, иначе говоря вычисляет итоговую сумму, и делит групповой агрегат на оконный, чтобы вычислить в процентах долю каждого сотрудника и итоговой цифре.

Будет проще увидеть логику вложенных агрегатов, если анализ запроса разбить да два этапа. На первом вычисляется групповой агрегат:

SELECT empid, SUM(val) AS emptotal FROM Sales.OrderValues GROUP BY empid;

Этот результат можно считать начальной точкой для дальнейшего оконной агрегации. Таким образом, можно применить агрегат SUM к выражению, представленному псевдонимом emptotal. К сожалению нельзя применить его непосредственно к псевдониму по причинам, изложенным ранее (помните принцип «все сразу»?). Но его можно применить к базовому выражению так: SUM(SUM(val)) OVER(...) и можно считать, что это SUM(emptotal) OVER(...). Таким образом получаем следующее:

SELECT empid, SUM(val) AS emptotal, SUM(val) / SUM(SUM(val)) OVER() * 100. AS pct FROM Sales.OrderValues GROUP BY empid;

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

WITH C AS (SELECT empid, SUM(val) AS emptotal FROM Sales.OrderValues GROUP BY empid) SELECT empid, emptotal, emptotal / SUM(emptotal) OVER() * 100. AS pct FROM C;

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

WITH C AS (SELECT empid, orderdate, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.Orders) SELECT empid, orderdate, COUNT(distinct_custid) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM C GROUP BY empid, orderdate;

Но при выполнении запроса вы получаете следующую ошибку:

Column "C.distinct_custid" is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

Внешняя функция COUNT является не групповыми, а оконным агрегатом. Таким образом она может работать только на основе элементов, которые действительны, только если определяются самостоятельно, то есть не как входные данные для оконного агрегата. Теперь на секундочку забудьте об оконном агрегате и скажите, корректен ли следующий запрос (для краткости определение CTE опущено)?

SELECT empid, orderdate, distinct_custid FROM C GROUP BY empid, orderdate;

Ясно, что ответ отрицательный. Атрибут distinct_custid в списке SELECT неверен, потому что не содержится ни в агрегирующей функции, ни в предложении GROUP BY, и примерно об этом говорится в сообщении об ошибке. Что вам нужно сделать, так это применить оконный агрегат SUM с кадром, реализующим принцип нарастающего итога, к групповому агрегату COUNT, который считает конкретные вхождения:

WITH C AS (SELECT empid, orderdate, CASE WHEN ROW_NUMBER() OVER(PARTITION BY empid, custid ORDER BY orderdate) = 1 THEN custid END AS distinct_custid FROM Sales.Orders) SELECT empid, orderdate, SUM(COUNT(distinct_custid)) OVER(PARTITION BY empid ORDER BY orderdate) AS numcusts FROM C GROUP BY empid, orderdate;

Ясно, что это не единственный способ получения нужного результата, но моей задачей было проиллюстрировать принцип вложения групповых агрегатов в оконные. Как вы помните, в соответствии с порядком логической обработки запросов оконные функции обрабатываются на этапе SELECT или ORDER BY, то есть после GROUP BY. По этой причине групповые агрегаты видны в качестве входных выражений оконных агрегатов. Также вспомните, что если код становится сложным для понимания, всегда можно задействовать табличные выражения, чтобы избежать прямого сложения функций и повысить читабельность кода.