Оптимизация работы серверов баз данных Microsoft SQL Server 2005

Вычисление агрегатов

Разбить на страницы
Показывать лекцию целиком

В лекциях курса "Разработка и защита баз данных в 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()

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

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

Использование функций T-SQL для вычисления количества записей

Язык Transact SQL (T-SQL) имеет особые функции для агрегации (обобщения информации о данных), эти функции являются скалярными (возвращают только одно значение) и обычно оперируют набором данных.

Используем функцию COUNT

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

SELECT COUNT(*) AS [Count] FROM Production.Product
Совет. Да. Здесь (и только здесь) можно использовать *.

Запускаем пример приложения, написанного на Visual Basic и использующего функцию Count

  • Откройте файл Chapter05\Chapter 05.sln с компакт-диска.
  • Откроется окно Microsoft Visual Studio. В меню Build (Построение) выберите Build (Построить) Chapter 05.
  • Из меню Debug (Отладка) выберите команду Start Debugging (Начать отладку). Это действие запускает приложение, вызывающее агрегатные функции. Важно отметить, что в этот момент для выполнения дальнейших действий база данных AdventureWorks должна быть присоединена локально.
  • В меню Database (База данных) выберите команду Connect (Соединиться).
  • В меню Demonstrations (Демонстрация) выберите команду Counting Records. Эти действия запускают приложение Counting Records. Теперь можно нажать три кнопки Run (Запуск) для выполнения трех различных агрегатных функций и сравнить их рабочие циклы.
  • Эти значения затраченного времени не будут одинаковыми при каждом выполнении операции. На результат влияют и другие факторы, например, создание связного пула или размер кэша сценариев. Однако у вас может возникнуть неплохая идея -подсчитать среднее время, необходимое для выполнения каждой операции.

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

    Функцию 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

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

    Создаем итоговую сводку с сортировкой данных

  • В меню Start (Пуск) выберите All Programs,. Microsoft SQL Server 2005, SQL Server Management Studio (Все программы, Microsoft SQL Server 2005, Среда SQL Server Management Studio). Откройте окно New Query (Новый запрос), нажав кнопку New Query (Новый запрос) на панели инструментов. Введите и выполните следующий сценарий для сортировки результатов при помощи предложения ORDER BY. (Этот сценарий и все другие примеры на использование функции COUNT в этом разделе можно найти в файлах примеров в папке \SqlScripts под именем CountExamplesFromText.sql )
    SELECT City, COUNT(*) AS [Count of Customers] 
      FROM Person.Address GROUP BY City ORDER BY City
  • Возможно, вам понадобится еще раз отфильтровать данные для ответа на вопрос: "Сколько покупателей у нас в каждом городе, если брать в расчет только такие города, в которых проживает более 50 покупателей?". Это особый случай, потому что нужно отфильтровать результаты агрегации 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

    Просматриваем предполагаемый план выполнения

  • Выделите сценарий, план выполнения которого нужно просмотреть.
  • Щелкните на нем правой кнопкой и выберите из контекстного меню команду Display Estimated Execution Plan (Показать предполагаемый план выполнения).
  • Примечание. Предполагаемый план выполнения в графической форме демонстрирует, как будет обрабатываться запрос и в какой степени каждый этап влияет на стоимость запроса. Ознакомьтесь в электронной документации по SQL Server 2005 с темой "Как вывести на экран предполагаемый план выполнения", чтобы узнать значение всех элементов графического представления.

    Вычисление итоговых и промежуточных сумм

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

    Совет. Для любой другой агрегатной функции можно использовать все модификаторы, которые мы научились использовать для функции COUNT.

    Вычисление итоговых сумм

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

    Применяем функцию SUM

    Функция SUM делает именно то, что вы ожидаете: она возвращает сумму значений в столбце. Эти значения имеют числовые типы данных. Кроме того, функция возвратит ошибку, если обнаружит значение NULL при попытке вычислить итоговую сумму. Давайте рассмотрим, какими способами можно использовать функцию SUM для получения различной полезной информации. (Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле SumExamplesFromText.sql )

    Генерируем итоговые суммы

  • Чтобы подсчитать итоговую сумму значений в столбце LineTotal таблицы SalesOrderDetail в базе данных Adventure Works, введите и выполните следующий запрос:
    SELECT SUM(LineTotal) AS [Grand Total] FROM Sales.SalesOrderDetail
  • Сделаем результат, возвращаемый этим сценарием, более полезным: пусть он отображает итоговое количество продаж по продуктам; для этого выполним следующий сценарий:
    SELECT ProductID, SUM(LineTotal) AS [Product Total] 
      FROM Sales.SalesOrderDetail
      GROUP BY ProductID
  • Уже лучше, но название столбца 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 SubCategory Product Sales
    Accessories Bike Racks Hitch Rack - 4-Bike 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

    Вот теперь мы предоставляем пользователям очень полезный набор информации.

    Если элементов слишком много, то анализ результатов может быть затруднительным. Кроме того, возможно, пользователю потребуется итоговая сумма по полям SubCategory и Category. Чтобы выполнить эту задачу, можно использовать функцию ROLLUP.

  • Используем функцию 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 SubcategorySales
    NULL NULL $ 109846381.40
    Accessories NULL 1272072.88
    Accessories Bike Racks 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 Racks -237,096.16 долларов и т. д. Однако вывод общих итоговых сумм таким образом - не лучший способ представления информации.

  • Чтобы получить более удобный структурированный результирующий набор, можно воспользоваться особой агрегатной функцией, 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 SubCategorySalesIsCategoryGroupIsSubCategoryGroup
    Accessories Bike Racks $ 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 Bib-Shorts 167558.62 0 0
    Clothing Caps 51229.45 0 0
    Clothing NULL 2120542.52 0 1
    Components Bottom Brackets 51826.37 0 0
    Components Wheels 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
  • Посмотрим предполагаемый план выполнения этого запроса в SQL Server Management Studio. Мы видим два запроса, выполняющихся параллельно до получения результирующих наборов.
  • Давайте попробуем по-другому. Мы можем инкапсулировать промежуточный итог в определяемой пользователем функции.

    Вычисляем промежуточный итог с помощью пользовательской функции

  • Сначала создайте определяемую пользователем функцию, выполнив следующий сценарий.
    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
  • Какой метод лучше? Для сравнения выполните следующую процедуру.

    Сравниваем производительность двух запросов

  • В SQL Server Management Studio откройте меню Query (Запрос).
  • Выберите команду Include Client Statistics (Включить статистику клиента).
  • Выполните два набора запросов, которые вы сохранили во второй раз. Теперь вы увидите, что наряду с результатом доступна вкладка Client Statistics (Статистика клиента). Используйте предоставленную информацию для того, чтобы узнать стоимость двух различных видов запросов.
  • В следующей таблице показано подмножество статистических показателей двух запросов.

    Примечание. В таблице показана только часть доступной статистики.
    Пример статистики клиента
    Метод с использованием подзапросаПопытка 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 возвращает среднее от всех значений, содержащихся в столбце, который указан в качестве аргумента. Функция 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

    Использование функций MIN и MAX

    Эти две функции возвращают минимальное или максимальное значение выражения. И вновь, выражение, используемое в качестве аргумента для этих функций, может быть либо именем столбца, либо другим вычислением, кроме того, для форматирования результатов можно использовать операторы 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

    Функция VAR возвращает статистическое отклонение от численного значения определенного выражения.

    Использование функции VARP

    Эта функция возвращает статистическое отклонения для совокупности значений в указанном выражении.

    Использование функций STDEV и STDEVP

    STDEV возвращает статистическое среднеквадратическое отклонение значения в указанном выражении, тогда как STDEVP возвращает статистическое среднеквадратическое отклонение совокупности значений в указанном выражении.

    Рекомендуем ознакомиться с дополнительной информацией о значении этих статистических функций в литературе по статистике.

    Использование ключевого слова DISTINCT

    Все агрегатные функции принимают модификатор 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 без 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
    Cable Lock 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
    ...

    Проектирование собственных пользовательских агрегатов с использованием CLR

    Совет. Чтобы выполнять упражнения в этом разделе, в SQL Server должна быть включена интеграция CLR. Узнать об этой настройке можно в Электронной документации по SQL Server 2005 в теме "Настройка конфигурации контактной зоны для функций".

    SQL Server 2005 позволяет создавать новые агрегатные функции, написанные с использованием непосредственно языков CLR (общеязыковой среды выполнения). Например, представьте себе, что вам нужно рассчитать среднюю фактическую производительность по заказам. Это не особенно практический пример, но он покажет, как работать с типами данных Date, Time и TimeSpan, которыми трудно управлять, используя только T-SQL.

    Создаем суррогат агрегатной функции

  • Из меню Start (Пуск) откройте All Programs, Microsoft Visual Studio 2005, Microsoft Visual Studio 2005 (Все программы, Microsoft Visual Studio 2005, Microsoft Visual Studio 2005).
  • В меню File (Файл) выберите команду New (Создать), затем Project (Проект). Откроется окно New Project (Новый проект), показанное ниже.
  • В панели Project Types (Типы проектов) разверните узел Visual Basic и выберите тип проекта Database (База данных). В панели Templates (Шаблоны) выделите SQL Server Project (Проект SQL Server). Укажите имя и путь к файлу для этого проекта, а затем нажмите кнопку ОК, чтобы создать его.
  • Откроется диалоговое окно Add Database Reference (Добавление ссылки на базу данных), показанное ниже. Выберите ссылку на базу данных, которую вы хотите использовать, и нажмите кнопку ОК, чтобы установить соединение. Если у вас не настроены ссылки на базы данных, то программа предложит создать новую ссылку. При выборе или создании ссылки, убедитесь, что указали в качестве базы данных, с которой нужно установить соединение, базу данных
  • Если программа выведет приглашение включить отладку SQL/CLR, как показано на рисунке, нажмите кнопку Yes (Да), чтобы включить отладку.
  • В меню View (Вид) выберите команду Solution Explorer (Обозреватель решений). В панели Solution Explorer (Обозреватель решений) щелкните правой кнопкой мыши на своем проекте и выберите команду Add (Добавить), а затем Aggregate (Агрегат), как показано на рисунке.
  • В диалоговом окне Add New Item (Добавление нового элемента) укажите имя для новой агрегатной функции WAVG.vb (сокращение от weighted average -взвешенное среднее), как показано на рисунке ниже, а затем нажмите кнопку Add (Добавить).
  • Когда вы добавляете элемент 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, Accumulate, 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, чтобы выполнить вычисления при следующем вызове функции Accumulate. Кроме того, мы добавляем единицу в переменную Count, чтобы отслеживать количество уже проверенных записей.

    Последующие вызовы процедуры Accumulate вычисляют различные временные интервалы между датами. Поскольку переменная 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.

    Примечание. SQL Server может также разделить работу на меньшие фрагменты, результаты которых нужно будет объединить, вызвав метод 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)
  • Затем выберите команды Build (Построить) <ProjectName> из меню Build (Построение), а затем Start Debugging (Начать отладку) из меню Debug (Отладка), чтобы выполнить сценарий.

    Ниже вы видите фрагмент результирующего набора.

    Результаты
    ДатаИнтервал
    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
    ... ...

    Если внимательно посмотреть на процедуру Accumulate, то можно заметить, что она создает переменную 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.

    Краткий справочник по 1 лекции

    Чтобы Выполните следующие действия
    Подсчитать записи в таблице 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()

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

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

    Использование функций T-SQL для вычисления количества записей

    Язык Transact SQL (T-SQL) имеет особые функции для агрегации (обобщения информации о данных), эти функции являются скалярными (возвращают только одно значение) и обычно оперируют набором данных.

    Используем функцию COUNT

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

    SELECT COUNT(*) AS [Count] FROM Production.Product
    Совет. Да. Здесь (и только здесь) можно использовать *.

    Запускаем пример приложения, написанного на Visual Basic и использующего функцию Count

  • Откройте файл Chapter05\Chapter 05.sln с компакт-диска.
  • Откроется окно Microsoft Visual Studio. В меню Build (Построение) выберите Build (Построить) Chapter 05.
  • Из меню Debug (Отладка) выберите команду Start Debugging (Начать отладку). Это действие запускает приложение, вызывающее агрегатные функции. Важно отметить, что в этот момент для выполнения дальнейших действий база данных AdventureWorks должна быть присоединена локально.
  • В меню Database (База данных) выберите команду Connect (Соединиться).
  • В меню Demonstrations (Демонстрация) выберите команду Counting Records. Эти действия запускают приложение Counting Records. Теперь можно нажать три кнопки Run (Запуск) для выполнения трех различных агрегатных функций и сравнить их рабочие циклы.
  • Эти значения затраченного времени не будут одинаковыми при каждом выполнении операции. На результат влияют и другие факторы, например, создание связного пула или размер кэша сценариев. Однако у вас может возникнуть неплохая идея -подсчитать среднее время, необходимое для выполнения каждой операции.

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

    Функцию 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

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

    Создаем итоговую сводку с сортировкой данных

  • В меню Start (Пуск) выберите All Programs,. Microsoft SQL Server 2005, SQL Server Management Studio (Все программы, Microsoft SQL Server 2005, Среда SQL Server Management Studio). Откройте окно New Query (Новый запрос), нажав кнопку New Query (Новый запрос) на панели инструментов. Введите и выполните следующий сценарий для сортировки результатов при помощи предложения ORDER BY. (Этот сценарий и все другие примеры на использование функции COUNT в этом разделе можно найти в файлах примеров в папке \SqlScripts под именем CountExamplesFromText.sql )
    SELECT City, COUNT(*) AS [Count of Customers] 
      FROM Person.Address GROUP BY City ORDER BY City
  • Возможно, вам понадобится еще раз отфильтровать данные для ответа на вопрос: "Сколько покупателей у нас в каждом городе, если брать в расчет только такие города, в которых проживает более 50 покупателей?". Это особый случай, потому что нужно отфильтровать результаты агрегации 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

    Просматриваем предполагаемый план выполнения

  • Выделите сценарий, план выполнения которого нужно просмотреть.
  • Щелкните на нем правой кнопкой и выберите из контекстного меню команду Display Estimated Execution Plan (Показать предполагаемый план выполнения).
  • Примечание. Предполагаемый план выполнения в графической форме демонстрирует, как будет обрабатываться запрос и в какой степени каждый этап влияет на стоимость запроса. Ознакомьтесь в электронной документации по SQL Server 2005 с темой "Как вывести на экран предполагаемый план выполнения", чтобы узнать значение всех элементов графического представления.

    Вычисление итоговых и промежуточных сумм

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

    Совет. Для любой другой агрегатной функции можно использовать все модификаторы, которые мы научились использовать для функции COUNT.

    Вычисление итоговых сумм

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

    Применяем функцию SUM

    Функция SUM делает именно то, что вы ожидаете: она возвращает сумму значений в столбце. Эти значения имеют числовые типы данных. Кроме того, функция возвратит ошибку, если обнаружит значение NULL при попытке вычислить итоговую сумму. Давайте рассмотрим, какими способами можно использовать функцию SUM для получения различной полезной информации. (Сценарии из этого раздела можно найти в примерах в папке \SqlScripts в файле SumExamplesFromText.sql )

    Генерируем итоговые суммы

  • Чтобы подсчитать итоговую сумму значений в столбце LineTotal таблицы SalesOrderDetail в базе данных Adventure Works, введите и выполните следующий запрос:
    SELECT SUM(LineTotal) AS [Grand Total] FROM Sales.SalesOrderDetail
  • Сделаем результат, возвращаемый этим сценарием, более полезным: пусть он отображает итоговое количество продаж по продуктам; для этого выполним следующий сценарий:
    SELECT ProductID, SUM(LineTotal) AS [Product Total] 
      FROM Sales.SalesOrderDetail
      GROUP BY ProductID
  • Уже лучше, но название столбца 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 SubCategory Product Sales
    Accessories Bike Racks Hitch Rack - 4-Bike 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

    Вот теперь мы предоставляем пользователям очень полезный набор информации.

    Если элементов слишком много, то анализ результатов может быть затруднительным. Кроме того, возможно, пользователю потребуется итоговая сумма по полям SubCategory и Category. Чтобы выполнить эту задачу, можно использовать функцию ROLLUP.

  • Используем функцию 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 SubcategorySales
    NULL NULL $ 109846381.40
    Accessories NULL 1272072.88
    Accessories Bike Racks 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 Racks -237,096.16 долларов и т. д. Однако вывод общих итоговых сумм таким образом - не лучший способ представления информации.

  • Чтобы получить более удобный структурированный результирующий набор, можно воспользоваться особой агрегатной функцией, 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 SubCategorySalesIsCategoryGroupIsSubCategoryGroup
    Accessories Bike Racks $ 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 Bib-Shorts 167558.62 0 0
    Clothing Caps 51229.45 0 0
    Clothing NULL 2120542.52 0 1
    Components Bottom Brackets 51826.37 0 0
    Components Wheels 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
  • Посмотрим предполагаемый план выполнения этого запроса в SQL Server Management Studio. Мы видим два запроса, выполняющихся параллельно до получения результирующих наборов.
  • Давайте попробуем по-другому. Мы можем инкапсулировать промежуточный итог в определяемой пользователем функции.

    Вычисляем промежуточный итог с помощью пользовательской функции

  • Сначала создайте определяемую пользователем функцию, выполнив следующий сценарий.
    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
  • Какой метод лучше? Для сравнения выполните следующую процедуру.

    Сравниваем производительность двух запросов

  • В SQL Server Management Studio откройте меню Query (Запрос).
  • Выберите команду Include Client Statistics (Включить статистику клиента).
  • Выполните два набора запросов, которые вы сохранили во второй раз. Теперь вы увидите, что наряду с результатом доступна вкладка Client Statistics (Статистика клиента). Используйте предоставленную информацию для того, чтобы узнать стоимость двух различных видов запросов.
  • В следующей таблице показано подмножество статистических показателей двух запросов.

    Примечание. В таблице показана только часть доступной статистики.
    Пример статистики клиента
    Метод с использованием подзапросаПопытка 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 возвращает среднее от всех значений, содержащихся в столбце, который указан в качестве аргумента. Функция 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

    Использование функций MIN и MAX

    Эти две функции возвращают минимальное или максимальное значение выражения. И вновь, выражение, используемое в качестве аргумента для этих функций, может быть либо именем столбца, либо другим вычислением, кроме того, для форматирования результатов можно использовать операторы 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

    Функция VAR возвращает статистическое отклонение от численного значения определенного выражения.

    Использование функции VARP

    Эта функция возвращает статистическое отклонения для совокупности значений в указанном выражении.

    Использование функций STDEV и STDEVP

    STDEV возвращает статистическое среднеквадратическое отклонение значения в указанном выражении, тогда как STDEVP возвращает статистическое среднеквадратическое отклонение совокупности значений в указанном выражении.

    Рекомендуем ознакомиться с дополнительной информацией о значении этих статистических функций в литературе по статистике.

    Использование ключевого слова DISTINCT

    Все агрегатные функции принимают модификатор 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 без 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
    Cable Lock 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
    ...

    Проектирование собственных пользовательских агрегатов с использованием CLR

    Совет. Чтобы выполнять упражнения в этом разделе, в SQL Server должна быть включена интеграция CLR. Узнать об этой настройке можно в Электронной документации по SQL Server 2005 в теме "Настройка конфигурации контактной зоны для функций".

    SQL Server 2005 позволяет создавать новые агрегатные функции, написанные с использованием непосредственно языков CLR (общеязыковой среды выполнения). Например, представьте себе, что вам нужно рассчитать среднюю фактическую производительность по заказам. Это не особенно практический пример, но он покажет, как работать с типами данных Date, Time и TimeSpan, которыми трудно управлять, используя только T-SQL.

    Создаем суррогат агрегатной функции

  • Из меню Start (Пуск) откройте All Programs, Microsoft Visual Studio 2005, Microsoft Visual Studio 2005 (Все программы, Microsoft Visual Studio 2005, Microsoft Visual Studio 2005).
  • В меню File (Файл) выберите команду New (Создать), затем Project (Проект). Откроется окно New Project (Новый проект), показанное ниже.
  • В панели Project Types (Типы проектов) разверните узел Visual Basic и выберите тип проекта Database (База данных). В панели Templates (Шаблоны) выделите SQL Server Project (Проект SQL Server). Укажите имя и путь к файлу для этого проекта, а затем нажмите кнопку ОК, чтобы создать его.
  • Откроется диалоговое окно Add Database Reference (Добавление ссылки на базу данных), показанное ниже. Выберите ссылку на базу данных, которую вы хотите использовать, и нажмите кнопку ОК, чтобы установить соединение. Если у вас не настроены ссылки на базы данных, то программа предложит создать новую ссылку. При выборе или создании ссылки, убедитесь, что указали в качестве базы данных, с которой нужно установить соединение, базу данных
  • Если программа выведет приглашение включить отладку SQL/CLR, как показано на рисунке, нажмите кнопку Yes (Да), чтобы включить отладку.
  • В меню View (Вид) выберите команду Solution Explorer (Обозреватель решений). В панели Solution Explorer (Обозреватель решений) щелкните правой кнопкой мыши на своем проекте и выберите команду Add (Добавить), а затем Aggregate (Агрегат), как показано на рисунке.
  • В диалоговом окне Add New Item (Добавление нового элемента) укажите имя для новой агрегатной функции WAVG.vb (сокращение от weighted average -взвешенное среднее), как показано на рисунке ниже, а затем нажмите кнопку Add (Добавить).
  • Когда вы добавляете элемент 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, Accumulate, 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, чтобы выполнить вычисления при следующем вызове функции Accumulate. Кроме того, мы добавляем единицу в переменную Count, чтобы отслеживать количество уже проверенных записей.

    Последующие вызовы процедуры Accumulate вычисляют различные временные интервалы между датами. Поскольку переменная 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.

    Примечание. SQL Server может также разделить работу на меньшие фрагменты, результаты которых нужно будет объединить, вызвав метод 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)
  • Затем выберите команды Build (Построить) <ProjectName> из меню Build (Построение), а затем Start Debugging (Начать отладку) из меню Debug (Отладка), чтобы выполнить сценарий.

    Ниже вы видите фрагмент результирующего набора.

    Результаты
    ДатаИнтервал
    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
    ... ...

    Если внимательно посмотреть на процедуру Accumulate, то можно заметить, что она создает переменную 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.

    Краткий справочник по 1 лекции

    Чтобы Выполните следующие действия
    Подсчитать записи в таблице 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
    Вернуться к учебному плану