В лекциях курса "Разработка и защита баз данных в Microsoft SQL Server 2005 " мы научились проектировать и создавать базы данных, защищать их от несанкционированного использования и непредвиденных потерь данных, а также переносить данные в другие места. В данном курсе рассказывается о том, как манипулировать данными, хранящимися в базе данных; в этой лекции мы начинаем с вычисления итогов и другой сводной информации о данных в таблицах.
Прежде, чем приступить к чтению этой лекции, запустите SQL Server Management Studio, установите соединение с сервером SQL Server и выбрав базу данных Adventure Works, откройте окно New Query (Новый запрос).
Читая примеры, приведенные в этой лекции, поэкспериментируйте с собственными комбинациями и идеями. Агрегатные функции выполняют только операции чтения, поэтому вы не сможете повредить базу данных, если попробуете что-либо новое.
Иногда в приложениях нужно сообщить пользователю, сколько элементов имеется в базе данных.
Если вы ознакомитесь с группой сайтов электронной коммерции в интернете, то увидите, что многие из них на своей домашней странице информируют читателей о том, сколько товаров доступно для покупки на сайте. Этот вид информации может быть получен при помощи примерно такого алгоритма:

Тот же пример, написанный на языке Visual Basic, выглядит так:
Dim Counter As Integer = 0 While dataReader.Read Counter += 1 End While dataReader.Close()
Однако выполнение этой процедуры в таблице, содержащей много строк, займет очень много времени и потребует много ресурсов. Такое снижение производительности происходит из-за того, что для подсчета строк сценарий должен обратиться к каждой строке, по одному обращению на каждую строку..
Существует другой способ получить количество строк в том же объекте DataReader:
Dim Counter As Integer = dataReader.RecordsAffected()
Однако эта процедура при соединении с базой данных использует столько же сетевых ресурсов, что и предыдущая.
Как и в других задачах, имеющих отношение к базам данных, производительность системы будет более оптимальной, если доступ к данным будет выполнен внутри самой базы данных.
Язык Transact SQL (T-SQL) имеет особые функции для агрегации (обобщения информации о данных), эти функции являются скалярными (возвращают только одно значение) и обычно оперируют набором данных.
Если нужно подсчитать записи в таблице, можно использовать следующий сценарий:
SELECT COUNT(*) AS [Count] FROM Production.Product
*.Chapter05\Chapter 05.sln с компакт-диска.
Эти значения затраченного времени не будут одинаковыми при каждом выполнении операции. На результат влияют и другие факторы, например, создание связного пула или размер кэша сценариев. Однако у вас может возникнуть неплохая идея -подсчитать среднее время, необходимое для выполнения каждой операции.
Функцию COUNT можно использовать несколькими способами. Первый способ просто считает записи. В этом случае функция COUNT в качестве аргумента получает символ звездочки ( * ).
Но иногда бывает нужно подсчитать только те записи, которые имеют ненулевое значение в определенных полях. В этом случае можно использовать имя поля как аргумент.
Следующее предложение возвращает два различных результата для одной и той же таблицы:
SELECT COUNT(*) AS [Count],
COUNT(Class) AS [Classes with values]
FROM Production.Product
| Количество | Классы со значениями |
|---|---|
| 504 | 247 |
Возможно, вы хотели бы узнать, сколько кредитных карт и каких эмитентов используют ваши покупатели при оплате покупок. В этом случае нужно подсчитать количество различных брендов кредитных карт, а не общее количество записей, содержащих информацию о кредитных картах, в базе данных. Для этого с функцией COUNT придется использовать модификатор DISTINCT.
SELECT COUNT(DISTINCT CardType) AS [Different Credit Cards] FROM Sales.CreditCard
COUNT возвращает тип данных int, а функция COUNT_BIG выполняет те же агрегации, но тип данных возвращаемых ею значений будет bigint. Работая с таблицами, которые содержат миллионы записей, следует использовать функцию COUNT_BIG.Иногда приходится выполнять такие запросы к базе данных, которые отвечали бы на вопрос вроде: "Сколько у нас покупателей из Сиэтла?" Чтобы ответить на этот вопрос, нельзя использовать просто COUNT(*) или COUNT(DISTINCT имя_поля). Процесс подсчета требует использования фильтра.
По сути, синтаксис агрегации - это просто модификация инструкции SELECT. Учитывая это, можно фильтровать запрос при помощи предложения WHERE так же, как и в любой другой инструкции SELECT.
SELECT COUNT(*) AS [Customers In Seattle] FROM Person.Address WHERE (City = "Seattle")
Однако было бы лучше, если можно было бы предоставить информацию о пользователях для нескольких городов, а не только для одного города. Конечно, можно повторить запрос с параметром в предложении WHERE, чтобы пользователи могли указать город, который их интересует. Но что, если нужно ответить на вопрос: "Сколько у нас покупателей в каждом городе?"
Чтобы ответить на этот вопрос, придется изменить инструкцию SELECT, добавив в нее предложение GROUP BY. Посмотрите на этот сценарий и возвращенный им результат.
SELECT City, COUNT(*) AS [Count of Customers] FROM Person.Address GROUP BY City
| Город | Количество покупателей |
|---|---|
| Cheltenham | 55 |
| Kingsport | 1 |
| Baltimore | 1 |
| Reading | 31 |
В длинном списке трудно найти конкретный город, пока не будет выполнена сортировка. Для сортировки информации для пользователей выполните следующие действия:
ORDER BY. (Этот сценарий и все другие примеры на использование функции COUNT в этом разделе можно найти в файлах примеров в папке \SqlScripts под именем CountExamplesFromText.sql )SELECT City, COUNT(*) AS [Count of Customers] FROM Person.Address GROUP BY City ORDER BY City
COUNT после выполнения вычислений. Однако у T-SQL есть решение и для этого случая. Нажмите кнопку New Query (Новый запрос), затем введите и выполните следующий сценарий для фильтрации агрегата COUNT при помощи предложения HAVING.SELECT City, COUNT(*) AS [Count of Customers] FROM Person.Address GROUP BY City HAVING (COUNT(*) > 50) ORDER BY City
Различие между предложениями WHERE и HAVING заключается в типе информации, к которой применяется каждый фильтр. Предложение WHERE фильтрует оригинальный набор информации из таблицы. Предложение HAVING фильтрует информацию, полученную после выполнения агрегатных функций, при этом фильтры обычно основываются на результатах агрегации.
Если мы рассмотрим предполагаемый план выполнения для сценария, использующего предложение WHERE, и другого сценария, использующего предложение HAVING, разница между этими двумя сценариями станет очевидной: Ознакомьтесь с предполагаемыми планами выполнения для двух предыдущих запросов при помощи следующей процедуры. Эти два плана выполнения показаны на рисунках 1.1 и 1.2.
(рис 1.1) Предполагаемый план выполнения для предложения T-SQL, использующего агрегации и предложение WHERE
(рис 1.2) Предполагаемый план выполнения для предложения T-SQL, использующего агрегации и предложение HAVING
Чтобы получить детализированные и сводные суммы значений данных в таблице, можно использовать и другие агрегатные функции.
COUNT.Итоговые суммы необходимы для ответа на вопросы "Сколько денег мы выручили от продаж?" или "Сколько единиц данного товара мы продали?". Подсчитать записи вы могли бы и при помощи пользовательского приложения. Однако нужную информацию можно получить более эффективным способом, выполнив прямой запрос к базе данных.
Функция SUM делает именно то, что вы ожидаете: она возвращает сумму значений в столбце.
Эти значения имеют числовые типы данных.
Кроме того, функция возвратит ошибку, если обнаружит значение NULL при попытке вычислить итоговую сумму.
Давайте рассмотрим, какими способами можно использовать функцию SUM для получения различной полезной информации.
(Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле SumExamplesFromText.sql )
Adventure Works, введите и выполните следующий запрос:SELECT SUM(LineTotal) AS [Grand Total] FROM Sales.SalesOrderDetail
SELECT ProductID, SUM(LineTotal) AS [Product Total] FROM Sales.SalesOrderDetail GROUP BY ProductID
SELECT Production.Product.Name, SUM(Sales.SalesOrderDetail.LineTotal) AS [Product Total] FROM Sales.SalesOrderDetail INNER JOIN Production.Product ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID GROUP BY Production.Product.Name ORDER BY Production.Product.Name
SELECT C.Name AS Category, S.Name AS SubCategory,
P.Name AS Product, SUM(O.LineTotal) AS [Product Total]
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY P.Name, C.Name, S.Name ORDER BY Category, SubCategory, Product
Получаем следующие результаты.
| Category | Product | Sales | |
|---|---|---|---|
| Accessories | Bike |
Hitch |
237096.16 |
| Accessories | Bike Stands | All-Purpose Bike Stand | 39591.00 |
| Accessories | Bottles and Cages | Mountain Bottle Cage | 20229.75 |
| Accessories | Bottles and Cages | Road Bottle Cage | 15390.88 |
| Accessories | Bottles and Cages | Water Bottle - 30 oz. | 28654.16 |
| Accessories | Cleaners | Bike Wash - Dissolver | 18406.97 |
| Accessories | Fenders | Fender Set - Mountain | 46619.58 |
| Accessories | Helmets | Sport-100 Helmet, Black | 16.869.52 |
Вот теперь мы предоставляем пользователям очень полезный набор информации.
Если элементов слишком много, то анализ результатов может быть затруднительным. Кроме того, возможно, пользователю потребуется итоговая сумма по полям и Category.
Чтобы выполнить эту задачу, можно использовать функцию ROLLUP.
T-SQL предоставляет операторы для предложения GROUP BY, которые позволяют получить не только детализированную,
но и сводную информацию для каждого из полей, которые указаны в аргументе предложения GROUP BY.
(Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле RollupExamplesFromText.sql )
SELECT так, чтобы она использовала только информацию столбцов Category и SubCategory .
Это уменьшит количество возвращаемых строк и несколько упростит понимание данных.
Мы также добавим в предложение GROUP BY оператор WITH ROLLUP, чтобы вывести промежуточную сумму для столбцов Category и SubCategory .SELECT C.Name AS Category, S.Name AS SubCategory,
SUM(O.LineTotal) AS Sales
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P
ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY C.Name, S.Name WITH ROLLUP
ORDER BY Category, SubCategory
| Category | | Sales |
|---|---|---|
| NULL | NULL | $ 109846381.40 |
| Accessories | NULL | 1272072.88 |
| Accessories | Bike |
237096.16 |
| Accessories | Bike Stands | 39591.00 |
| Accessories | Bottles and Cages | 64274.79 |
| Accessories | Cleaners | 18406.97 |
| Accessories | Fenders | 46619.58 |
| Accessories | Helmets | 484048.53 |
| Accessories | Hydration Packs | 105826.42 |
| Accessories | Locks | 16240.22 |
| Accessories | Pumps | 13514.69 |
| Accessories | Tires and Tubes | 246454.53 |
| Bikes | NULL | 94651172.70 |
| Bikes | Mountain Bikes | 36445443.94 |
| … | … | … |
Из этого результирующего набора видно, что общая итоговая сумма продаж составляет 109,846,381.40 долларов, общая итоговая сумма продаж для категории Accessories составляет 1,272,072.88 долларов, общая итоговая сумма продаж для подкатегории Bike
GROUPING. Эта функция помогает обозначить различия между строками, содержащими итоговые значения, и строками, содержащие промежуточные значения. Добавьте функцию GROUPING в свой сценарий для полей Category и SubCategory , чтобы итоговые строки в результате были нагляднее; для этого выполните следующий сценарий:SELECT C.Name AS Category,
S.Name AS SubCategory,
SUM(O.LineTotal) AS Sales,
GROUPING(C.Name) AS IsCategoryGroup,
GROUPING(S.Name) AS IsSubCategoryGroup
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID GROUP BY C.Name, S.Name
WITH ROLLUP ORDER BY Category, SubCategory
SELECT C.Name AS Category,
S.Name AS SubCategory,
SUM(O.LineTotal) AS Sales,
GROUPING(C.Name) AS IsCategoryGroup,
GROUPING (S.Name) AS IsSubCategoryGroup
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY C.Name, S.Name WITH ROLLUP
ORDER BY IsCategoryGroup, Category, IsSubCategoryGroup, SubCategory
Результирующий набор для этого сценария показан в табл. 1.5. Обратите внимание на то, что некоторые строки были опущены для экономии места.
| Category | | Sales | IsCategoryGroup | IsSubCategoryGroup |
|---|---|---|---|---|
| Accessories | Bike |
$ 237096.16 | 0 | 0 |
| Accessories | Bike Stands | 39591.00 | 0 | 0 |
| Accessories | NULL | 1272072.88 | 0 | 1 |
| Bikes | Mountain Bikes | 36445443.94 | 0 | 0 |
| Bikes | Touring Bikes | 14296291.26 | 0 | 0 |
| Bikes | NULL | 94651172.70 | 0 | 1 |
| Clothing | 167558.62 | 0 | 0 | |
| Clothing | Caps | 51229.45 | 0 | 0 |
| Clothing | NULL | 2120542.52 | 0 | 1 |
| Components | Bottom |
51826.37 | 0 | 0 |
| Components | 680831.35 | 0 | 0 | |
| Components | NULL | 11802593.29 | 0 | 1 |
| … | … | … | … | |
| NULL | NULL | 109846381.40 | 1 | 1 |
NULL до более приемлемого для отчета вида при помощи добавления в запрос инструкции CASE:SELECT
CASE GROUPING(C.Name)
WHEN 1 THEN "Category Total"
ELSE C.Name END AS Category,
CASE GROUPING(S.Name)
WHEN 1 THEN "Subcategory Total"
ELSE S.Name END AS SubCategory,
SUM(O.LineTotal) AS Sales,
GROUPING(C.Name) AS IsCategoryGroup,
GROUPING(S.Name) AS IsSubCategoryGroup
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P
ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY C.Name, S.Name WITH ROLLUP
ORDER BY IsCategoryGroup, Category,
IsSubCategoryGroup, SubCategory
Можно выбрать, отображать ли значения GROUPING, включая или не включая их в предложение SELECT нашего сценария. Скрытые значения GROUPING могут использоваться в сценарии в других предложениях, например, в предложениях ORDER BY. Если вы получаете значения GROUPING в приложении, то их можно использовать для добавления форматирования итоговых строк в отчете или на экране.
Иногда необходимо показать в отчете возрастающие значения, чтобы продемонстрировать прогресс некоторых видов операций. Например, возможно, вам нужно сгенерировать отчет о ежедневном росте продаж. Результат должен выглядеть следующим образом:
| Date | Sales | Total Sales |
|---|---|---|
| 7/1/2001 | $ 665262.96 | $ 665262.96 |
| 7/2/2001 | 15394.33 | 680657.29 |
| 7/3/2001 | 16588.46 | 697245.75 |
| 7/4/2001 | 7907.98 | 705153.72 |
| 7/5/2001 | 16588.46 | 721742.18 |
| 7/6/2001 | 15815.95 | 737558.13 |
| 7/7/2001 | 8680.48 | 746238.61 |
| 7/8/2001 | 8680.48 | 754919.10 |
| 7/9/2001 | 23105.31 | 778024.40 |
| 7/10/2001 | 11664.97 | 789689.37 |
| 7/11/2001 | 15815.95 | 805505.32 |
| 7/12/2001 | 15618.95 | 821124.28 |
| 7/13/2001 | 7907.98 | 829032.25 |
| 7/14/2001 | 27677.92 | 856710.17 |
| 7/15/2001 | 12409.84 | 869120.02 |
| 7/16/2001 | 15815.95 | 884935.97 |
| … | … | … |
Как видите, значения в столбце Total Sales равно сумме итоговых сумм за все предыдущие даты плюс итоговая сумма текущей строки. Этот тип итоговой суммы называется промежуточным итогом, поскольку итоговая сумма рассчитывается до текущей суммы включительно.
Запрос для получения промежуточных итогов состоит из двух частей. Первая часть не представляет сложности: просто суммируем продажи, сгруппированные по дате:
SELECT OrderDate,
SUM(TotalDue) AS Sales
FROM Sales.SalesOrderHeader AS A
GROUP BY OrderDate
ORDER BY OrderDate
Нам нужно найти промежуточный итог для одной конкретной даты, для чего теоретически можно составить примерно такой запрос:
-Этот запрос не является синтаксически правильным запросом SQL SELECT SUM(TotalDue) AS Expr1 FROM Sales.SalesOrderHeader WHERE (OrderDate <= <Specific_Date>)
Имейте в виду, что этот запрос не является корректным сценарием T-SQL, он приводится только для объяснения того,
как можно было бы высчитать промежуточный итог на определенный момент времени.
Элемент <Specific_Date> не является корректным для T-SQL,
он просто представляет конкретную дату в нашем теоретическом запросе.
Чтобы претворить теорию в реальный T-SQL, мы можем воспользоваться полем OrderDate в качестве конкретной даты для каждой строки.
(Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле RunningTotalsExamplesFromText.sql )
SubQueryMethod.sql.SELECT OrderDate,
SUM(TotalDue) AS Sales,
(
SELECT SUM(TotalDue) FROM Sales.SalesOrderHeader
WHERE (OrderDate <= A.OrderDate)
)
AS [Actual Sales]
FROM Sales.SalesOrderHeader AS A
GROUP BY OrderDate
ORDER BY OrderDate
Давайте попробуем по-другому. Мы можем инкапсулировать промежуточный итог в определяемой пользователем функции.
CREATE FUNCTION SalesToDate
(
@ThisDate datetime
)
RETURNS money
AS
BEGIN
RETURN (SELECT SUM(TotalDue) AS Expr1
FROM Sales.SalesOrderHeader
WHERE (OrderDate <= @ThisDate))
END
UserDefFunctionMethod.sql.SELECT OrderDate,
SUM(TotalDue) AS Sales,
dbo.SalesToDate(A.OrderDate) AS [Actual sales]
FROM Sales.SalesOrderHeader AS A
GROUP BY OrderDate
ORDER BY OrderDate
Какой метод лучше? Для сравнения выполните следующую процедуру.
В следующей таблице показано подмножество статистических показателей двух запросов.
| Метод с использованием подзапроса | Попытка 1 | Среднее |
|---|---|---|
| Время выполнения клиента | 10375 | 10375.000 |
| Общее время выполнения | 10835 | 460.000 |
| Время ожидания при ответе сервера | 460 | 10835.000 |
| Метод с использованием пользовательской функции | Попытка 1 | Среднее |
| Время выполнения клиента | 35677 | 35677.000 |
| Общее время выполнения | 39502 | 39502.000 |
| Время ожидания при ответе сервера | 3825 | 3825.000 |
Как видите, время выполнения для запроса, использующего пользовательскую функцию, в три раза больше, чем время выполнения для запроса, использующего подзапрос.
Вычисление статистических значений
Подсчет количества и суммы - не единственные доступные возможности агрегации. В запросах можно использовать также основные и расширенные
Эти функции можно использовать тем же способом, что и функцию SUM. Для удовлетворения дополнительных требований можно добавить фильтры и группировки.
При фильтрации или группировке этих функций результирующее значение каждой функции будет отличаться в зависимости от ее определения. (Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле StatisticalExamplesFromText.sql )
Функция AVG возвращает среднее от всех значений, содержащихся в столбце, который указан в качестве аргумента. Функция AVG игнорирует значения NULL, поэтому если среди ваших данных присутствуют нулевые значения, это не изменит среднего значения, возвращаемого функцией.
Функция AVG в качестве аргумента принимает любые выражения, возвращающие численные значения. Аргументом может быть имя столбца или вычисление. Однако подзапросы или другие агрегатные функции в аргументе недопустимы.
Следующий пример - инструкция SELECT, которая возвращает среднюю цену продаж для каждого изделия:
SELECT Production.Product.Name AS Product,
AVG(Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
GROUP BY Production.Product.Name
ORDER BY Production.Product.Name
И еще одна инструкция SQL, возвращающая среднюю стоимость каждого изделия по месяцам и общую среднюю стоимость для каждого продукта.
SELECT
CASE GROUPING(Production. Product .Name)
WHEN 1 THEN "Global Average"
ELSE Production. Product .Name END AS Product,
CASE GROUPING( CONVERT(nvarchar(7),
Sales.SalesOrderHeader.OrderDate, 111))
WHEN 1 THEN "Average"
ELSE CONVERT(nvarchar(7), Sales.SalesOrderHeader.OrderDate, 111)
END AS Period,
AVG(Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
INNER JOIN Sales.SalesOrderHeader
ON Sales.SalesOrderDetail.SalesOrderID = Sales.SalesOrderHeader.SalesOrderID
GROUP BY Production.Product.Name,
CONVERT(nvarchar(7), Sales.SalesOrderHeader.OrderDate, 111)
WITH ROLLUP ORDER BY GROUPING(Production.Product.Name),
Production.Product.Name, Period
Эти две функции возвращают минимальное или максимальное значение выражения. И вновь, выражение, используемое в качестве аргумента для этих функций, может быть либо именем столбца, либо другим вычислением, кроме того, для форматирования результатов можно использовать операторы GROUP или ROLLUP.
Выполните следующий сценарий, чтобы увидеть функции MIN и MAX в действии.
SELECT Production.Product.Name,
MIN(Sales.SalesOrderDetail.UnitPrice) AS [Min Price],
MAX(Sales.SalesOrderDetail.UnitPrice) AS [Max Price],
AVG(Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
GROUP BY Production.Product.Name
ORDER BY Production.Product.Name
Иногда возникает необходимость проанализировать данные, хранящиеся в базе данных, более глубоко, чем позволяют функции, которые мы только что изучили. Например, демографический анализ и оценка численности популяции, которые преимущественно используются в приложениях для социальной или медицинской областей, требуют более глубокого
анализа собранных данных. Среди более сложных STDEV, STDEVP, VAR и VARP.
Функция VAR возвращает статистическое отклонение от численного значения определенного выражения.
Эта функция возвращает статистическое отклонения для совокупности значений в указанном выражении.
STDEV возвращает статистическое среднеквадратическое отклонение значения в указанном выражении, тогда как STDEVP возвращает статистическое среднеквадратическое отклонение совокупности значений в указанном выражении.
Рекомендуем ознакомиться с дополнительной информацией о значении этих
Все агрегатные функции принимают модификатор DISTINCT, который обусловливает выполнение вычисления агрегата только для каждого уникального вхождения значения.
SELECT Production.Product.Name,
MIN(DISTINCT Sales.SalesOrderDetail.UnitPrice) AS [Min Price],
MAX(DISTINCT Sales.SalesOrderDetail.UnitPrice) AS [Max Price],
AVG(DISTINCT Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
GROUP BY Production.Product.Name
ORDER BY Production.Product.Name
В следующей таблице можно сравнить результаты приведенного выше сценария с использованием и без использования ключевого слова DISTINCT. Обратите внимание на то, что значения MIN и MAX не изменяются, а значения AVG в большинстве случаев отличаются.
с DISTINCT |
без DISTINCT | |||||
|---|---|---|---|---|---|---|
Min | Max | AVG | Min | Max | AVG | |
| All-Purpose Bike Stand | $ 159.00 | $ 159.00 | $ 159.00 | $ 159.00 | $ 159.00 | $ 159.00 |
| AWC Logo Cap | 4.32 | 8.99 | 5.37 | 4.32 | 8.99 | 7.67 |
| Bike Wash -Dissolver | 3.98 | 7.95 | 5.14 | 3.98 | 7.95 | 6.94 |
| 14.50 | 15.00 | 14.75 | 14.50 | 15.00 | 14.99 | |
| Chain | 11.74 | 12.14 | 11.94 | 11.74 | 12.14 | 12.14 |
| Classic Vest, L | 38.10 | 63.50 | 50.80 | 38.10 | 63.50 | 62.74 |
| Classic Vest, M | 34.93 | 63.50 | 43.34 | 34.93 | 63.50 | 47.06 |
| … | … | … | … | … | … | ... |
SQL Server 2005 позволяет создавать новые агрегатные функции, написанные с использованием непосредственно языков CLR (общеязыковой среды выполнения). Например, представьте себе, что вам нужно рассчитать среднюю фактическую производительность по заказам. Это не особенно практический пример, но он покажет, как работать с типами данных Date, Time и TimeSpan, которыми трудно управлять, используя только T-SQL.




WAVG.vb, Visual Studio добавляет в структуру агрегата следующий суррогат кода.<Serializable()> _
<Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.Native)> _
Public Structure WAVG
Public Sub Init()
" Введите сюда свой код
End Sub
Public Sub Accumulate(ByVal value As SqlString)
" Введите сюда свой код
End Sub
Public Sub Merge(ByVal value As WAVG)
" Введите сюда свой код
End Sub
Public Function Terminate() As SqlString
" Введите сюда свой код
Return New SqlString("")
End Function
" Это заместитель элемента поля
Private var1 As Integer
End Structure
В этой структуре определены четыре процедуры: Init, , Merge и Terminate.
Чтобы обеспечить правильное выполнение функций, вам нужно будет добавить соответствующий код.
Прежде чем начать, необходимо определить, каким типом данных будет управлять этот агрегат.
В нашем примере, поскольку мы будем работать с датой и временем, укажем тип данных SqlDateTime.
Поскольку работать мы будем с затраченным временем, вероятно, для возвращаемого значения подойдет тип данных TimeSpan.
Однако этот тип данных в SQL Server нельзя использовать. Вместо него придется использовать строку с отображением промежутка времени.
Чтобы рассчитать средневзвешенное значение, нам нужно суммировать все промежутки времени для набора записей, а затем разделить эту итоговую сумму на количество записей в наборе.
WAVG.
Введите декларации, как показано ниже:Public Structure WAVG
Private Ticks As Long "Собирает интервалы между датами
Private Previous As SqlDateTime "Хранит предыдущие даты
"чтобы получить затраченное время
Private Count As Integer "Количество обработанных записей
Init до обработки первой записи.
В процедуре Init необходимо инициировать переменные при помощи следующего кода:Public Sub Init() Count = 0 Previous = Nothing Ticks = -1 "To detect the first record End Sub
Accumulate , которая подробно описана ниже.
Обратите внимание, что для данной процедуры не надо изменять аргумент с типа данных по умолчанию SqlString на SqlDateTime.Public Sub Accumulate(ByVal value As SqlString)
Dim span As New TimeSpan(0)
If Ticks > -1 Then
span = New TimeSpan(Ticks)
span = span.Add(value.Value.Subtract(Previous.Value))
Else
Ticks = 0
End If
Previous = value
Count += 1
Ticks = span.Ticks
End Sub
Когда исполнитель в первый раз вызывает эту процедуру, у нас нет предыдущей даты.
Следовательно, количество Ticks между проверяемой в настоящий момент датой и предыдущей будет равно нулю.
Код сохраняет актуальную дату в переменной Previous, чтобы выполнить вычисления при следующем вызове функции .
Кроме того, мы добавляем единицу в переменную Count, чтобы отслеживать количество уже проверенных записей.
Последующие вызовы процедуры вычисляют различные временные интервалы между датами.
Поскольку переменная Ticks больше не равна -1, процедура создает столбец TimeSpan и добавляет к нему разность между текущей и предыдущей датами.
Она также может сохранить дату в переменной Previous и прирост переменной Count, как и прежде.
Terminate. Введите код, как показано ниже.Public Function Terminate() As SqlString
If Ticks <= 0 Then
Return New TimeSpan(0).ToString
Else
Dim Resp As Long = CLng(Ticks / Count)
Dim RespDate As New System.TimeSpan(Math.Abs((Resp)))
Return RespDate.ToString
End If
End Function
Если не было вычислено ни одного интервала, то переменная Ticks будет содержать 0.
В этом случае было бы возвращено строковое представление новой переменной TimeSpan с количеством интервалов, равным 0.
Если интервалы были вычислены, то мы разделим сумму этих разностей на количество обработанных записей, а затем создадим новую переменную TimeSpan для хранения результата.
Затем мы возвращаем строковое представление этой переменной TimeSpan.
Merge.
В реальном приложении вам нужно будет соответствующим образом реализовать эту функцию.Test.sql, который Visual Studio автоматически добавила к проекту.
Этот файл можно найти в папке Test Scripts в обозревателе Solution Explorer.
В этом файле введите следующую инструкцию SELECT, которая использует функцию.SELECT CONVERT(nvarchar(7), OrderDate, 111) AS Period,
dbo.WAVG(OrderDate) as Span
FROM Sales.SalesOrderHeader
GROUP BY CONVERT(nvarchar(7), OrderDate, 111)
Ниже вы видите фрагмент результирующего набора.
| Дата | Интервал |
|---|---|
| 2001/07 | 03:54:46.9565217 |
| 2001/08 | 03:07:00.7792208 |
| 2001/09 | 03:22:43.1067961 |
| 2001/10 | 03:34:55.5223881 |
| 2001/11 | 02:41:14.1312741 |
| 2001/12 | 02:24:57.9865772 |
| 2002/01 | 03:09:28.4210526 |
| 2002/02 | 02:35:31.2000000 |
| 2002/03 | 02:44:15.5133080 |
| 2002/04 | 02:51:08.8524590 |
| 2002/05 | 02:24:28.8963211 |
| 2002/06 | 02:28:05.1063830 |
| 2002/07 | 02:12:55.3846154 |
| 2002/08 | 01:42:51.4285714 |
| 2002/09 | 02:15:08.7378641 |
| 2002/10 | 02:23:02.7814570 |
| 2002/11 | 02:08:05.8895706 |
| ... | ... |
Если внимательно посмотреть на процедуру , то можно заметить, что она создает переменную TimeSpan при каждом своем выполнении,
тогда как вычисленные значения хранит в переменной Ticks. Почему бы не использовать только переменную TimeSpan?
Проблема заключается в том, что агрегатные функции CLR нуждаются в сериали-зации между вызовами.
Способ, которым выполняется сериализация, определяется атрибутом в декларации функции. Например, посмотрим на следующую декларацию:
<Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.Native)> _ Public Structure WAVG
Аргумент Format устанавливает формат сериализации.
В формате Native можно сериализовать только типы значений, но не ссылочные типы, такие, как классы CLR (в том числе, класс System.String ) или ваши пользовательские классы.
В этом примере мы можем транслировать значение, которое нам нужно сохранить между вызовами, сохранив его представление в переменной Ticks с типом данных long.
Однако если нужно использовать ссылочные типы, можно изменить аргумент Format на Format.UserDefined.
Однако если такое изменение будет сделано, вам придется реализовать свой механизм сериализации.
Дополнительную информацию о механизмах сериализации можно найти в Электронной документации по SQL Server 2005 в теме "Вызов определяемых пользователем агрегатных функций CLR".
Полностью этот пример агрегатной функции CLR включен в файлы примеров этой лекции и размещен в папке WAVG.
Агрегаты - эффективные инструменты для организации и интерпретации данных. SQL Server предоставляет широкий диапазон агрегатов общего назначения, которые вы можете использовать, а вы можете применять их в различных модификациях, добавляя в агрегаты ключевые слова. Если подходящий агрегат еще не встроен в SQL Server, то несложно создать свою агрегатную функцию через Visual Studio, а затем вызвать свой пользовательский агрегат при помощи запроса SQL.
| Чтобы | Выполните следующие действия |
|---|---|
| Подсчитать записи в таблице | SELECT COUNT(*) FROM <Table_Name> |
| Подсчитать количество записей, в которых значения в ячейках не равны 0 | SELECT COUNT (<Field_Name>) FROM <Table_name> |
| Подсчитать количество записей, соответствующих определенному условию | SELECT COUNT (*) FROM <Table_Name> WHERE <condition> |
| Подсчитать записи с одинаковыми значениями в одном из полей | SELECT <Field_Name>, COUNT(*) FROM <Table_Name> GROUP BY <Field_Name> |
| Суммировать значения в столбце | SELECT SUM(<Field_name>) FROM <Table_Name> |
| Получить наименьшее значение в столбце | SELECT MIN(<Field_Name>) FROM <Table_Name> |
| Получить наибольшее значение в столбце | SELECT MAX(<Field_Name>) FROM <Table_Name> |
| Получить среднее для значений в столбце | SELECT AVG(<Field_Name>) FROM <Table_Name> |
| Получить промежуточные суммы и итоговые суммы для значений | SELECT <Field_Name>, <FUNCTION_NAME>(<Field_Name>) FROM <Table_Name> GROUP BY <Field_Name> WITH ROLLUP |
| Получить результаты только для тех значений, которые не повторяются | SELECT <FUNCTION_NAME> (DISTINCT <Field_Name>) FROM <Table_Name> |
| Определить свою агрегатную функцию | Создайте агрегатную функцию CLR в Visual Studio |
В лекциях курса "Разработка и защита баз данных в Microsoft SQL Server 2005 " мы научились проектировать и создавать базы данных, защищать их от несанкционированного использования и непредвиденных потерь данных, а также переносить данные в другие места. В данном курсе рассказывается о том, как манипулировать данными, хранящимися в базе данных; в этой лекции мы начинаем с вычисления итогов и другой сводной информации о данных в таблицах.
Прежде, чем приступить к чтению этой лекции, запустите SQL Server Management Studio, установите соединение с сервером SQL Server и выбрав базу данных Adventure Works, откройте окно New Query (Новый запрос).
Читая примеры, приведенные в этой лекции, поэкспериментируйте с собственными комбинациями и идеями. Агрегатные функции выполняют только операции чтения, поэтому вы не сможете повредить базу данных, если попробуете что-либо новое.
Иногда в приложениях нужно сообщить пользователю, сколько элементов имеется в базе данных.
Если вы ознакомитесь с группой сайтов электронной коммерции в интернете, то увидите, что многие из них на своей домашней странице информируют читателей о том, сколько товаров доступно для покупки на сайте. Этот вид информации может быть получен при помощи примерно такого алгоритма:

Тот же пример, написанный на языке Visual Basic, выглядит так:
Dim Counter As Integer = 0 While dataReader.Read Counter += 1 End While dataReader.Close()
Однако выполнение этой процедуры в таблице, содержащей много строк, займет очень много времени и потребует много ресурсов. Такое снижение производительности происходит из-за того, что для подсчета строк сценарий должен обратиться к каждой строке, по одному обращению на каждую строку..
Существует другой способ получить количество строк в том же объекте DataReader:
Dim Counter As Integer = dataReader.RecordsAffected()
Однако эта процедура при соединении с базой данных использует столько же сетевых ресурсов, что и предыдущая.
Как и в других задачах, имеющих отношение к базам данных, производительность системы будет более оптимальной, если доступ к данным будет выполнен внутри самой базы данных.
Язык Transact SQL (T-SQL) имеет особые функции для агрегации (обобщения информации о данных), эти функции являются скалярными (возвращают только одно значение) и обычно оперируют набором данных.
Если нужно подсчитать записи в таблице, можно использовать следующий сценарий:
SELECT COUNT(*) AS [Count] FROM Production.Product
*.Chapter05\Chapter 05.sln с компакт-диска.
Эти значения затраченного времени не будут одинаковыми при каждом выполнении операции. На результат влияют и другие факторы, например, создание связного пула или размер кэша сценариев. Однако у вас может возникнуть неплохая идея -подсчитать среднее время, необходимое для выполнения каждой операции.
Функцию COUNT можно использовать несколькими способами. Первый способ просто считает записи. В этом случае функция COUNT в качестве аргумента получает символ звездочки ( * ).
Но иногда бывает нужно подсчитать только те записи, которые имеют ненулевое значение в определенных полях. В этом случае можно использовать имя поля как аргумент.
Следующее предложение возвращает два различных результата для одной и той же таблицы:
SELECT COUNT(*) AS [Count],
COUNT(Class) AS [Classes with values]
FROM Production.Product
| Количество | Классы со значениями |
|---|---|
| 504 | 247 |
Возможно, вы хотели бы узнать, сколько кредитных карт и каких эмитентов используют ваши покупатели при оплате покупок. В этом случае нужно подсчитать количество различных брендов кредитных карт, а не общее количество записей, содержащих информацию о кредитных картах, в базе данных. Для этого с функцией COUNT придется использовать модификатор DISTINCT.
SELECT COUNT(DISTINCT CardType) AS [Different Credit Cards] FROM Sales.CreditCard
COUNT возвращает тип данных int, а функция COUNT_BIG выполняет те же агрегации, но тип данных возвращаемых ею значений будет bigint. Работая с таблицами, которые содержат миллионы записей, следует использовать функцию COUNT_BIG.Иногда приходится выполнять такие запросы к базе данных, которые отвечали бы на вопрос вроде: "Сколько у нас покупателей из Сиэтла?" Чтобы ответить на этот вопрос, нельзя использовать просто COUNT(*) или COUNT(DISTINCT имя_поля). Процесс подсчета требует использования фильтра.
По сути, синтаксис агрегации - это просто модификация инструкции SELECT. Учитывая это, можно фильтровать запрос при помощи предложения WHERE так же, как и в любой другой инструкции SELECT.
SELECT COUNT(*) AS [Customers In Seattle] FROM Person.Address WHERE (City = "Seattle")
Однако было бы лучше, если можно было бы предоставить информацию о пользователях для нескольких городов, а не только для одного города. Конечно, можно повторить запрос с параметром в предложении WHERE, чтобы пользователи могли указать город, который их интересует. Но что, если нужно ответить на вопрос: "Сколько у нас покупателей в каждом городе?"
Чтобы ответить на этот вопрос, придется изменить инструкцию SELECT, добавив в нее предложение GROUP BY. Посмотрите на этот сценарий и возвращенный им результат.
SELECT City, COUNT(*) AS [Count of Customers] FROM Person.Address GROUP BY City
| Город | Количество покупателей |
|---|---|
| Cheltenham | 55 |
| Kingsport | 1 |
| Baltimore | 1 |
| Reading | 31 |
В длинном списке трудно найти конкретный город, пока не будет выполнена сортировка. Для сортировки информации для пользователей выполните следующие действия:
ORDER BY. (Этот сценарий и все другие примеры на использование функции COUNT в этом разделе можно найти в файлах примеров в папке \SqlScripts под именем CountExamplesFromText.sql )SELECT City, COUNT(*) AS [Count of Customers] FROM Person.Address GROUP BY City ORDER BY City
COUNT после выполнения вычислений. Однако у T-SQL есть решение и для этого случая. Нажмите кнопку New Query (Новый запрос), затем введите и выполните следующий сценарий для фильтрации агрегата COUNT при помощи предложения HAVING.SELECT City, COUNT(*) AS [Count of Customers] FROM Person.Address GROUP BY City HAVING (COUNT(*) > 50) ORDER BY City
Различие между предложениями WHERE и HAVING заключается в типе информации, к которой применяется каждый фильтр. Предложение WHERE фильтрует оригинальный набор информации из таблицы. Предложение HAVING фильтрует информацию, полученную после выполнения агрегатных функций, при этом фильтры обычно основываются на результатах агрегации.
Если мы рассмотрим предполагаемый план выполнения для сценария, использующего предложение WHERE, и другого сценария, использующего предложение HAVING, разница между этими двумя сценариями станет очевидной: Ознакомьтесь с предполагаемыми планами выполнения для двух предыдущих запросов при помощи следующей процедуры. Эти два плана выполнения показаны на рисунках 1.1 и 1.2.
(рис 1.1) Предполагаемый план выполнения для предложения T-SQL, использующего агрегации и предложение WHERE
(рис 1.2) Предполагаемый план выполнения для предложения T-SQL, использующего агрегации и предложение HAVING
Чтобы получить детализированные и сводные суммы значений данных в таблице, можно использовать и другие агрегатные функции.
COUNT.Итоговые суммы необходимы для ответа на вопросы "Сколько денег мы выручили от продаж?" или "Сколько единиц данного товара мы продали?". Подсчитать записи вы могли бы и при помощи пользовательского приложения. Однако нужную информацию можно получить более эффективным способом, выполнив прямой запрос к базе данных.
Функция SUM делает именно то, что вы ожидаете: она возвращает сумму значений в столбце.
Эти значения имеют числовые типы данных.
Кроме того, функция возвратит ошибку, если обнаружит значение NULL при попытке вычислить итоговую сумму.
Давайте рассмотрим, какими способами можно использовать функцию SUM для получения различной полезной информации.
(Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле SumExamplesFromText.sql )
Adventure Works, введите и выполните следующий запрос:SELECT SUM(LineTotal) AS [Grand Total] FROM Sales.SalesOrderDetail
SELECT ProductID, SUM(LineTotal) AS [Product Total] FROM Sales.SalesOrderDetail GROUP BY ProductID
SELECT Production.Product.Name, SUM(Sales.SalesOrderDetail.LineTotal) AS [Product Total] FROM Sales.SalesOrderDetail INNER JOIN Production.Product ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID GROUP BY Production.Product.Name ORDER BY Production.Product.Name
SELECT C.Name AS Category, S.Name AS SubCategory,
P.Name AS Product, SUM(O.LineTotal) AS [Product Total]
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY P.Name, C.Name, S.Name ORDER BY Category, SubCategory, Product
Получаем следующие результаты.
| Category | Product | Sales | |
|---|---|---|---|
| Accessories | Bike |
Hitch |
237096.16 |
| Accessories | Bike Stands | All-Purpose Bike Stand | 39591.00 |
| Accessories | Bottles and Cages | Mountain Bottle Cage | 20229.75 |
| Accessories | Bottles and Cages | Road Bottle Cage | 15390.88 |
| Accessories | Bottles and Cages | Water Bottle - 30 oz. | 28654.16 |
| Accessories | Cleaners | Bike Wash - Dissolver | 18406.97 |
| Accessories | Fenders | Fender Set - Mountain | 46619.58 |
| Accessories | Helmets | Sport-100 Helmet, Black | 16.869.52 |
Вот теперь мы предоставляем пользователям очень полезный набор информации.
Если элементов слишком много, то анализ результатов может быть затруднительным. Кроме того, возможно, пользователю потребуется итоговая сумма по полям и Category.
Чтобы выполнить эту задачу, можно использовать функцию ROLLUP.
T-SQL предоставляет операторы для предложения GROUP BY, которые позволяют получить не только детализированную,
но и сводную информацию для каждого из полей, которые указаны в аргументе предложения GROUP BY.
(Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле RollupExamplesFromText.sql )
SELECT так, чтобы она использовала только информацию столбцов Category и SubCategory .
Это уменьшит количество возвращаемых строк и несколько упростит понимание данных.
Мы также добавим в предложение GROUP BY оператор WITH ROLLUP, чтобы вывести промежуточную сумму для столбцов Category и SubCategory .SELECT C.Name AS Category, S.Name AS SubCategory,
SUM(O.LineTotal) AS Sales
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P
ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY C.Name, S.Name WITH ROLLUP
ORDER BY Category, SubCategory
| Category | | Sales |
|---|---|---|
| NULL | NULL | $ 109846381.40 |
| Accessories | NULL | 1272072.88 |
| Accessories | Bike |
237096.16 |
| Accessories | Bike Stands | 39591.00 |
| Accessories | Bottles and Cages | 64274.79 |
| Accessories | Cleaners | 18406.97 |
| Accessories | Fenders | 46619.58 |
| Accessories | Helmets | 484048.53 |
| Accessories | Hydration Packs | 105826.42 |
| Accessories | Locks | 16240.22 |
| Accessories | Pumps | 13514.69 |
| Accessories | Tires and Tubes | 246454.53 |
| Bikes | NULL | 94651172.70 |
| Bikes | Mountain Bikes | 36445443.94 |
| … | … | … |
Из этого результирующего набора видно, что общая итоговая сумма продаж составляет 109,846,381.40 долларов, общая итоговая сумма продаж для категории Accessories составляет 1,272,072.88 долларов, общая итоговая сумма продаж для подкатегории Bike
GROUPING. Эта функция помогает обозначить различия между строками, содержащими итоговые значения, и строками, содержащие промежуточные значения. Добавьте функцию GROUPING в свой сценарий для полей Category и SubCategory , чтобы итоговые строки в результате были нагляднее; для этого выполните следующий сценарий:SELECT C.Name AS Category,
S.Name AS SubCategory,
SUM(O.LineTotal) AS Sales,
GROUPING(C.Name) AS IsCategoryGroup,
GROUPING(S.Name) AS IsSubCategoryGroup
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID GROUP BY C.Name, S.Name
WITH ROLLUP ORDER BY Category, SubCategory
SELECT C.Name AS Category,
S.Name AS SubCategory,
SUM(O.LineTotal) AS Sales,
GROUPING(C.Name) AS IsCategoryGroup,
GROUPING (S.Name) AS IsSubCategoryGroup
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY C.Name, S.Name WITH ROLLUP
ORDER BY IsCategoryGroup, Category, IsSubCategoryGroup, SubCategory
Результирующий набор для этого сценария показан в табл. 1.5. Обратите внимание на то, что некоторые строки были опущены для экономии места.
| Category | | Sales | IsCategoryGroup | IsSubCategoryGroup |
|---|---|---|---|---|
| Accessories | Bike |
$ 237096.16 | 0 | 0 |
| Accessories | Bike Stands | 39591.00 | 0 | 0 |
| Accessories | NULL | 1272072.88 | 0 | 1 |
| Bikes | Mountain Bikes | 36445443.94 | 0 | 0 |
| Bikes | Touring Bikes | 14296291.26 | 0 | 0 |
| Bikes | NULL | 94651172.70 | 0 | 1 |
| Clothing | 167558.62 | 0 | 0 | |
| Clothing | Caps | 51229.45 | 0 | 0 |
| Clothing | NULL | 2120542.52 | 0 | 1 |
| Components | Bottom |
51826.37 | 0 | 0 |
| Components | 680831.35 | 0 | 0 | |
| Components | NULL | 11802593.29 | 0 | 1 |
| … | … | … | … | |
| NULL | NULL | 109846381.40 | 1 | 1 |
NULL до более приемлемого для отчета вида при помощи добавления в запрос инструкции CASE:SELECT
CASE GROUPING(C.Name)
WHEN 1 THEN "Category Total"
ELSE C.Name END AS Category,
CASE GROUPING(S.Name)
WHEN 1 THEN "Subcategory Total"
ELSE S.Name END AS SubCategory,
SUM(O.LineTotal) AS Sales,
GROUPING(C.Name) AS IsCategoryGroup,
GROUPING(S.Name) AS IsSubCategoryGroup
FROM Sales.SalesOrderDetail AS O
INNER JOIN Production.Product AS P
ON O.ProductID = P.ProductID
INNER JOIN Production.ProductSubcategory AS S
ON P.ProductSubcategoryID = S.ProductSubcategoryID
INNER JOIN Production.ProductCategory AS C
ON S.ProductCategoryID = C.ProductCategoryID
GROUP BY C.Name, S.Name WITH ROLLUP
ORDER BY IsCategoryGroup, Category,
IsSubCategoryGroup, SubCategory
Можно выбрать, отображать ли значения GROUPING, включая или не включая их в предложение SELECT нашего сценария. Скрытые значения GROUPING могут использоваться в сценарии в других предложениях, например, в предложениях ORDER BY. Если вы получаете значения GROUPING в приложении, то их можно использовать для добавления форматирования итоговых строк в отчете или на экране.
Иногда необходимо показать в отчете возрастающие значения, чтобы продемонстрировать прогресс некоторых видов операций. Например, возможно, вам нужно сгенерировать отчет о ежедневном росте продаж. Результат должен выглядеть следующим образом:
| Date | Sales | Total Sales |
|---|---|---|
| 7/1/2001 | $ 665262.96 | $ 665262.96 |
| 7/2/2001 | 15394.33 | 680657.29 |
| 7/3/2001 | 16588.46 | 697245.75 |
| 7/4/2001 | 7907.98 | 705153.72 |
| 7/5/2001 | 16588.46 | 721742.18 |
| 7/6/2001 | 15815.95 | 737558.13 |
| 7/7/2001 | 8680.48 | 746238.61 |
| 7/8/2001 | 8680.48 | 754919.10 |
| 7/9/2001 | 23105.31 | 778024.40 |
| 7/10/2001 | 11664.97 | 789689.37 |
| 7/11/2001 | 15815.95 | 805505.32 |
| 7/12/2001 | 15618.95 | 821124.28 |
| 7/13/2001 | 7907.98 | 829032.25 |
| 7/14/2001 | 27677.92 | 856710.17 |
| 7/15/2001 | 12409.84 | 869120.02 |
| 7/16/2001 | 15815.95 | 884935.97 |
| … | … | … |
Как видите, значения в столбце Total Sales равно сумме итоговых сумм за все предыдущие даты плюс итоговая сумма текущей строки. Этот тип итоговой суммы называется промежуточным итогом, поскольку итоговая сумма рассчитывается до текущей суммы включительно.
Запрос для получения промежуточных итогов состоит из двух частей. Первая часть не представляет сложности: просто суммируем продажи, сгруппированные по дате:
SELECT OrderDate,
SUM(TotalDue) AS Sales
FROM Sales.SalesOrderHeader AS A
GROUP BY OrderDate
ORDER BY OrderDate
Нам нужно найти промежуточный итог для одной конкретной даты, для чего теоретически можно составить примерно такой запрос:
-Этот запрос не является синтаксически правильным запросом SQL SELECT SUM(TotalDue) AS Expr1 FROM Sales.SalesOrderHeader WHERE (OrderDate <= <Specific_Date>)
Имейте в виду, что этот запрос не является корректным сценарием T-SQL, он приводится только для объяснения того,
как можно было бы высчитать промежуточный итог на определенный момент времени.
Элемент <Specific_Date> не является корректным для T-SQL,
он просто представляет конкретную дату в нашем теоретическом запросе.
Чтобы претворить теорию в реальный T-SQL, мы можем воспользоваться полем OrderDate в качестве конкретной даты для каждой строки.
(Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле RunningTotalsExamplesFromText.sql )
SubQueryMethod.sql.SELECT OrderDate,
SUM(TotalDue) AS Sales,
(
SELECT SUM(TotalDue) FROM Sales.SalesOrderHeader
WHERE (OrderDate <= A.OrderDate)
)
AS [Actual Sales]
FROM Sales.SalesOrderHeader AS A
GROUP BY OrderDate
ORDER BY OrderDate
Давайте попробуем по-другому. Мы можем инкапсулировать промежуточный итог в определяемой пользователем функции.
CREATE FUNCTION SalesToDate
(
@ThisDate datetime
)
RETURNS money
AS
BEGIN
RETURN (SELECT SUM(TotalDue) AS Expr1
FROM Sales.SalesOrderHeader
WHERE (OrderDate <= @ThisDate))
END
UserDefFunctionMethod.sql.SELECT OrderDate,
SUM(TotalDue) AS Sales,
dbo.SalesToDate(A.OrderDate) AS [Actual sales]
FROM Sales.SalesOrderHeader AS A
GROUP BY OrderDate
ORDER BY OrderDate
Какой метод лучше? Для сравнения выполните следующую процедуру.
В следующей таблице показано подмножество статистических показателей двух запросов.
| Метод с использованием подзапроса | Попытка 1 | Среднее |
|---|---|---|
| Время выполнения клиента | 10375 | 10375.000 |
| Общее время выполнения | 10835 | 460.000 |
| Время ожидания при ответе сервера | 460 | 10835.000 |
| Метод с использованием пользовательской функции | Попытка 1 | Среднее |
| Время выполнения клиента | 35677 | 35677.000 |
| Общее время выполнения | 39502 | 39502.000 |
| Время ожидания при ответе сервера | 3825 | 3825.000 |
Как видите, время выполнения для запроса, использующего пользовательскую функцию, в три раза больше, чем время выполнения для запроса, использующего подзапрос.
Вычисление статистических значений
Подсчет количества и суммы - не единственные доступные возможности агрегации. В запросах можно использовать также основные и расширенные
Эти функции можно использовать тем же способом, что и функцию SUM. Для удовлетворения дополнительных требований можно добавить фильтры и группировки.
При фильтрации или группировке этих функций результирующее значение каждой функции будет отличаться в зависимости от ее определения. (Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле StatisticalExamplesFromText.sql )
Функция AVG возвращает среднее от всех значений, содержащихся в столбце, который указан в качестве аргумента. Функция AVG игнорирует значения NULL, поэтому если среди ваших данных присутствуют нулевые значения, это не изменит среднего значения, возвращаемого функцией.
Функция AVG в качестве аргумента принимает любые выражения, возвращающие численные значения. Аргументом может быть имя столбца или вычисление. Однако подзапросы или другие агрегатные функции в аргументе недопустимы.
Следующий пример - инструкция SELECT, которая возвращает среднюю цену продаж для каждого изделия:
SELECT Production.Product.Name AS Product,
AVG(Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
GROUP BY Production.Product.Name
ORDER BY Production.Product.Name
И еще одна инструкция SQL, возвращающая среднюю стоимость каждого изделия по месяцам и общую среднюю стоимость для каждого продукта.
SELECT
CASE GROUPING(Production. Product .Name)
WHEN 1 THEN "Global Average"
ELSE Production. Product .Name END AS Product,
CASE GROUPING( CONVERT(nvarchar(7),
Sales.SalesOrderHeader.OrderDate, 111))
WHEN 1 THEN "Average"
ELSE CONVERT(nvarchar(7), Sales.SalesOrderHeader.OrderDate, 111)
END AS Period,
AVG(Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
INNER JOIN Sales.SalesOrderHeader
ON Sales.SalesOrderDetail.SalesOrderID = Sales.SalesOrderHeader.SalesOrderID
GROUP BY Production.Product.Name,
CONVERT(nvarchar(7), Sales.SalesOrderHeader.OrderDate, 111)
WITH ROLLUP ORDER BY GROUPING(Production.Product.Name),
Production.Product.Name, Period
Эти две функции возвращают минимальное или максимальное значение выражения. И вновь, выражение, используемое в качестве аргумента для этих функций, может быть либо именем столбца, либо другим вычислением, кроме того, для форматирования результатов можно использовать операторы GROUP или ROLLUP.
Выполните следующий сценарий, чтобы увидеть функции MIN и MAX в действии.
SELECT Production.Product.Name,
MIN(Sales.SalesOrderDetail.UnitPrice) AS [Min Price],
MAX(Sales.SalesOrderDetail.UnitPrice) AS [Max Price],
AVG(Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
GROUP BY Production.Product.Name
ORDER BY Production.Product.Name
Иногда возникает необходимость проанализировать данные, хранящиеся в базе данных, более глубоко, чем позволяют функции, которые мы только что изучили. Например, демографический анализ и оценка численности популяции, которые преимущественно используются в приложениях для социальной или медицинской областей, требуют более глубокого
анализа собранных данных. Среди более сложных STDEV, STDEVP, VAR и VARP.
Функция VAR возвращает статистическое отклонение от численного значения определенного выражения.
Эта функция возвращает статистическое отклонения для совокупности значений в указанном выражении.
STDEV возвращает статистическое среднеквадратическое отклонение значения в указанном выражении, тогда как STDEVP возвращает статистическое среднеквадратическое отклонение совокупности значений в указанном выражении.
Рекомендуем ознакомиться с дополнительной информацией о значении этих
Все агрегатные функции принимают модификатор DISTINCT, который обусловливает выполнение вычисления агрегата только для каждого уникального вхождения значения.
SELECT Production.Product.Name,
MIN(DISTINCT Sales.SalesOrderDetail.UnitPrice) AS [Min Price],
MAX(DISTINCT Sales.SalesOrderDetail.UnitPrice) AS [Max Price],
AVG(DISTINCT Sales.SalesOrderDetail.UnitPrice) AS [Avg Price]
FROM Sales.SalesOrderDetail
INNER JOIN Production.Product
ON Sales.SalesOrderDetail.ProductID = Production.Product.ProductID
GROUP BY Production.Product.Name
ORDER BY Production.Product.Name
В следующей таблице можно сравнить результаты приведенного выше сценария с использованием и без использования ключевого слова DISTINCT. Обратите внимание на то, что значения MIN и MAX не изменяются, а значения AVG в большинстве случаев отличаются.
с DISTINCT |
без DISTINCT | |||||
|---|---|---|---|---|---|---|
Min | Max | AVG | Min | Max | AVG | |
| All-Purpose Bike Stand | $ 159.00 | $ 159.00 | $ 159.00 | $ 159.00 | $ 159.00 | $ 159.00 |
| AWC Logo Cap | 4.32 | 8.99 | 5.37 | 4.32 | 8.99 | 7.67 |
| Bike Wash -Dissolver | 3.98 | 7.95 | 5.14 | 3.98 | 7.95 | 6.94 |
| 14.50 | 15.00 | 14.75 | 14.50 | 15.00 | 14.99 | |
| Chain | 11.74 | 12.14 | 11.94 | 11.74 | 12.14 | 12.14 |
| Classic Vest, L | 38.10 | 63.50 | 50.80 | 38.10 | 63.50 | 62.74 |
| Classic Vest, M | 34.93 | 63.50 | 43.34 | 34.93 | 63.50 | 47.06 |
| … | … | … | … | … | … | ... |
SQL Server 2005 позволяет создавать новые агрегатные функции, написанные с использованием непосредственно языков CLR (общеязыковой среды выполнения). Например, представьте себе, что вам нужно рассчитать среднюю фактическую производительность по заказам. Это не особенно практический пример, но он покажет, как работать с типами данных Date, Time и TimeSpan, которыми трудно управлять, используя только T-SQL.




WAVG.vb, Visual Studio добавляет в структуру агрегата следующий суррогат кода.<Serializable()> _
<Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.Native)> _
Public Structure WAVG
Public Sub Init()
" Введите сюда свой код
End Sub
Public Sub Accumulate(ByVal value As SqlString)
" Введите сюда свой код
End Sub
Public Sub Merge(ByVal value As WAVG)
" Введите сюда свой код
End Sub
Public Function Terminate() As SqlString
" Введите сюда свой код
Return New SqlString("")
End Function
" Это заместитель элемента поля
Private var1 As Integer
End Structure
В этой структуре определены четыре процедуры: Init, , Merge и Terminate.
Чтобы обеспечить правильное выполнение функций, вам нужно будет добавить соответствующий код.
Прежде чем начать, необходимо определить, каким типом данных будет управлять этот агрегат.
В нашем примере, поскольку мы будем работать с датой и временем, укажем тип данных SqlDateTime.
Поскольку работать мы будем с затраченным временем, вероятно, для возвращаемого значения подойдет тип данных TimeSpan.
Однако этот тип данных в SQL Server нельзя использовать. Вместо него придется использовать строку с отображением промежутка времени.
Чтобы рассчитать средневзвешенное значение, нам нужно суммировать все промежутки времени для набора записей, а затем разделить эту итоговую сумму на количество записей в наборе.
WAVG.
Введите декларации, как показано ниже:Public Structure WAVG
Private Ticks As Long "Собирает интервалы между датами
Private Previous As SqlDateTime "Хранит предыдущие даты
"чтобы получить затраченное время
Private Count As Integer "Количество обработанных записей
Init до обработки первой записи.
В процедуре Init необходимо инициировать переменные при помощи следующего кода:Public Sub Init() Count = 0 Previous = Nothing Ticks = -1 "To detect the first record End Sub
Accumulate , которая подробно описана ниже.
Обратите внимание, что для данной процедуры не надо изменять аргумент с типа данных по умолчанию SqlString на SqlDateTime.Public Sub Accumulate(ByVal value As SqlString)
Dim span As New TimeSpan(0)
If Ticks > -1 Then
span = New TimeSpan(Ticks)
span = span.Add(value.Value.Subtract(Previous.Value))
Else
Ticks = 0
End If
Previous = value
Count += 1
Ticks = span.Ticks
End Sub
Когда исполнитель в первый раз вызывает эту процедуру, у нас нет предыдущей даты.
Следовательно, количество Ticks между проверяемой в настоящий момент датой и предыдущей будет равно нулю.
Код сохраняет актуальную дату в переменной Previous, чтобы выполнить вычисления при следующем вызове функции .
Кроме того, мы добавляем единицу в переменную Count, чтобы отслеживать количество уже проверенных записей.
Последующие вызовы процедуры вычисляют различные временные интервалы между датами.
Поскольку переменная Ticks больше не равна -1, процедура создает столбец TimeSpan и добавляет к нему разность между текущей и предыдущей датами.
Она также может сохранить дату в переменной Previous и прирост переменной Count, как и прежде.
Terminate. Введите код, как показано ниже.Public Function Terminate() As SqlString
If Ticks <= 0 Then
Return New TimeSpan(0).ToString
Else
Dim Resp As Long = CLng(Ticks / Count)
Dim RespDate As New System.TimeSpan(Math.Abs((Resp)))
Return RespDate.ToString
End If
End Function
Если не было вычислено ни одного интервала, то переменная Ticks будет содержать 0.
В этом случае было бы возвращено строковое представление новой переменной TimeSpan с количеством интервалов, равным 0.
Если интервалы были вычислены, то мы разделим сумму этих разностей на количество обработанных записей, а затем создадим новую переменную TimeSpan для хранения результата.
Затем мы возвращаем строковое представление этой переменной TimeSpan.
Merge.
В реальном приложении вам нужно будет соответствующим образом реализовать эту функцию.Test.sql, который Visual Studio автоматически добавила к проекту.
Этот файл можно найти в папке Test Scripts в обозревателе Solution Explorer.
В этом файле введите следующую инструкцию SELECT, которая использует функцию.SELECT CONVERT(nvarchar(7), OrderDate, 111) AS Period,
dbo.WAVG(OrderDate) as Span
FROM Sales.SalesOrderHeader
GROUP BY CONVERT(nvarchar(7), OrderDate, 111)
Ниже вы видите фрагмент результирующего набора.
| Дата | Интервал |
|---|---|
| 2001/07 | 03:54:46.9565217 |
| 2001/08 | 03:07:00.7792208 |
| 2001/09 | 03:22:43.1067961 |
| 2001/10 | 03:34:55.5223881 |
| 2001/11 | 02:41:14.1312741 |
| 2001/12 | 02:24:57.9865772 |
| 2002/01 | 03:09:28.4210526 |
| 2002/02 | 02:35:31.2000000 |
| 2002/03 | 02:44:15.5133080 |
| 2002/04 | 02:51:08.8524590 |
| 2002/05 | 02:24:28.8963211 |
| 2002/06 | 02:28:05.1063830 |
| 2002/07 | 02:12:55.3846154 |
| 2002/08 | 01:42:51.4285714 |
| 2002/09 | 02:15:08.7378641 |
| 2002/10 | 02:23:02.7814570 |
| 2002/11 | 02:08:05.8895706 |
| ... | ... |
Если внимательно посмотреть на процедуру , то можно заметить, что она создает переменную TimeSpan при каждом своем выполнении,
тогда как вычисленные значения хранит в переменной Ticks. Почему бы не использовать только переменную TimeSpan?
Проблема заключается в том, что агрегатные функции CLR нуждаются в сериали-зации между вызовами.
Способ, которым выполняется сериализация, определяется атрибутом в декларации функции. Например, посмотрим на следующую декларацию:
<Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.Native)> _ Public Structure WAVG
Аргумент Format устанавливает формат сериализации.
В формате Native можно сериализовать только типы значений, но не ссылочные типы, такие, как классы CLR (в том числе, класс System.String ) или ваши пользовательские классы.
В этом примере мы можем транслировать значение, которое нам нужно сохранить между вызовами, сохранив его представление в переменной Ticks с типом данных long.
Однако если нужно использовать ссылочные типы, можно изменить аргумент Format на Format.UserDefined.
Однако если такое изменение будет сделано, вам придется реализовать свой механизм сериализации.
Дополнительную информацию о механизмах сериализации можно найти в Электронной документации по SQL Server 2005 в теме "Вызов определяемых пользователем агрегатных функций CLR".
Полностью этот пример агрегатной функции CLR включен в файлы примеров этой лекции и размещен в папке WAVG.
Агрегаты - эффективные инструменты для организации и интерпретации данных. SQL Server предоставляет широкий диапазон агрегатов общего назначения, которые вы можете использовать, а вы можете применять их в различных модификациях, добавляя в агрегаты ключевые слова. Если подходящий агрегат еще не встроен в SQL Server, то несложно создать свою агрегатную функцию через Visual Studio, а затем вызвать свой пользовательский агрегат при помощи запроса SQL.
| Чтобы | Выполните следующие действия |
|---|---|
| Подсчитать записи в таблице | SELECT COUNT(*) FROM <Table_Name> |
| Подсчитать количество записей, в которых значения в ячейках не равны 0 | SELECT COUNT (<Field_Name>) FROM <Table_name> |
| Подсчитать количество записей, соответствующих определенному условию | SELECT COUNT (*) FROM <Table_Name> WHERE <condition> |
| Подсчитать записи с одинаковыми значениями в одном из полей | SELECT <Field_Name>, COUNT(*) FROM <Table_Name> GROUP BY <Field_Name> |
| Суммировать значения в столбце | SELECT SUM(<Field_name>) FROM <Table_Name> |
| Получить наименьшее значение в столбце | SELECT MIN(<Field_Name>) FROM <Table_Name> |
| Получить наибольшее значение в столбце | SELECT MAX(<Field_Name>) FROM <Table_Name> |
| Получить среднее для значений в столбце | SELECT AVG(<Field_Name>) FROM <Table_Name> |
| Получить промежуточные суммы и итоговые суммы для значений | SELECT <Field_Name>, <FUNCTION_NAME>(<Field_Name>) FROM <Table_Name> GROUP BY <Field_Name> WITH ROLLUP |
| Получить результаты только для тех значений, которые не повторяются | SELECT <FUNCTION_NAME> (DISTINCT <Field_Name>) FROM <Table_Name> |
| Определить свою агрегатную функцию | Создайте агрегатную функцию CLR в Visual Studio |
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.