11.4.23

Агрегат WITH ROLLUP


Автор оригинала: 
Craig Freedman: Aggregation WITH ROLLUP

В этой статье мы обсудим, как устроен агрегат WITH ROLLUP. Использование предложения WITH ROLLUP позволяет выполнить несколько «уровней» агрегации в одном операторе. Например, предположим, что у нас есть некие данные о продажах (это те же данные, которые я использовал в серии статей об операторе PIVOT).

CREATE TABLE Sales (EmpId INT, Yr INT, Sales MONEY)
INSERT Sales VALUES(1, 2005, 12000)
INSERT Sales VALUES(1, 2006, 18000)
INSERT Sales VALUES(1, 2007, 25000)
INSERT Sales VALUES(2, 2005, 15000)
INSERT Sales VALUES(2, 2006, 6000)
INSERT Sales VALUES(3, 2006, 20000)
INSERT Sales VALUES(3, 2007, 24000)

Мы можем написать простой запрос c агрегатом для вычисления общего объема продаж по годам:

SELECT Yr, SUM(Sales) AS Sales
FROM Sales
GROUP BY Yr

Как и ожидалось, этот запрос возвращает три строки — по одной для каждого года:

Yr          Sales
----------- ---------------------
2005        27000.00
2006        44000.00
2007        49000.00

План запроса представляет собой простой Stream Aggregate:

  |--Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [Expr1010]=(0) THEN NULL ELSE [Expr1011] END))
       |--Stream Aggregate(GROUP BY:([Sales].[Yr]) DEFINE:([Expr1010]=COUNT_BIG([Sales].[Sales]), [Expr1011]=SUM([Sales].[Sales])))
            |--Sort(ORDER BY:([Sales].[Yr] ASC))
                 |--Table Scan(OBJECT:([Sales]))

Теперь предположим, что мы хотим вычислить не только продажи по годам, но и общие продажи. Мы могли бы написать запрос с UNION ALL:

SELECT Yr, SUM(Sales) AS Sales
FROM Sales
GROUP BY Yr
UNION ALL
SELECT NULL, SUM(Sales) AS Sales
FROM Sales

Этот запрос вернёт правильный результат:

Yr          Sales
----------- ---------------------
2005        27000.00
2006        44000.00
2007        49000.00
NULL        120000.00

Однако план запроса содержит два просмотра и два агрегирования (одно для вычисления продаж по годам, а второе для вычисления общих продаж):

  |--Concatenation
       |--Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [Expr1023]=(0) THEN NULL ELSE [Expr1024] END))
       |    |--Stream Aggregate(GROUP BY:([Sales].[Yr]) DEFINE:([Expr1023]=COUNT_BIG([Sales].[Sales]), [Expr1024]=SUM([Sales].[Sales])))
       |         |--Sort(ORDER BY:([Sales].[Yr] ASC))
       |              |--Table Scan(OBJECT:([Sales]))
       |--Compute Scalar(DEFINE:([Expr1010]=NULL))
            |--Compute Scalar(DEFINE:([Expr1009]=CASE WHEN [Expr1025]=(0) THEN NULL ELSE [Expr1026] END))
                 |--Stream Aggregate(DEFINE:([Expr1025]=COUNT_BIG([Sales].[Sales]), [Expr1026]=SUM([Sales].[Sales])))
                      |--Table Scan(OBJECT:([Sales]))

Мы можем улучшить ситуацию, добавив в исходный запрос предложение WITH ROLLUP:

SELECT Yr, SUM(Sales) AS Sales
FROM Sales
GROUP BY Yr WITH ROLLUP

Этот запрос проще, и он использует более эффективный план, у которого только один просмотр:

  |--Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [Expr1005]=(0) THEN NULL ELSE [Expr1006] END))
       |--Stream Aggregate(GROUP BY:([Sales].[Yr]) DEFINE:([Expr1005]=SUM([Expr1007]), [Expr1006]=SUM([Expr1008])))
            |--Stream Aggregate(GROUP BY:([Sales].[Yr]) DEFINE:([Expr1007]=COUNT_BIG([Sales].[Sales]), [Expr1008]=SUM([Sales].[Sales])))
                 |--Sort(ORDER BY:([Sales].[Yr] ASC))
                      |--Table Scan(OBJECT:([Sales]))

В этом плане запроса нижний Stream Aggregate такой же, как потоковый агрегат у исходного запроса без ROLLUP. Это обычное агрегирование, которое может быть реализовано с помощью Stream Aggregate (как в примере) или Hash Aggregate (попробуйте добавить предложение OPTION (HASH GROUP) в приведенный выше запрос). Всё это прекрасно распараллеливается.

Верхний Stream Aggregate — это специальный агрегат, который вычисляет ROLLUP (к сожалению, в SQL Server 2005 было невозможно определить из плана запроса, что этот агрегат реализует ROLLUP, однако проблема была исправлена в графическом и XML-планах в SQL Server 2008). Агрегат ROLLUP всегда реализуется с использованием Stream Aggregate и не может распараллеливаться. В этом простом примере потоковый агрегат ROLLUP возвращает каждую полученную на входе предагрегированную строку, и вычисляет промежуточный итог по столбцу Sales. После обработки последней строки агрегат добавляет одну дополнительную строку с общей суммой. Поскольку в SQL отсутствует концепция значения ALL, для столбца Yr в последней строке установлено значение NULL. Если для Yr значение NULL является допустимым, мы можем идентифицировать строку ROLLUP с помощью конструкции GROUPING(Yr) и подставить вместо него ALL.

SELECT
      CASE WHEN GROUPING(Yr) = 0
            THEN CAST (Yr AS CHAR(5))
            ELSE 'ALL'
      END AS Yr,
      SUM(Sales) AS Sales
FROM Sales
GROUP BY Yr WITH ROLLUP
Yr    Sales
----- ---------------------
2005  27000.00
2006  44000.00
2007  49000.00
ALL   120000.00

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

SELECT EmpId, Yr, SUM(Sales) AS Sales
FROM Sales
GROUP BY EmpId, Yr WITH ROLLUP
EmpId       Yr          Sales
----------- ----------- ---------------------
1           2005        12000.00
1           2006        18000.00
1           2007        25000.00
1           NULL        55000.00
2           2005        15000.00
2           2006        6000.00
2           NULL        21000.00
3           2006        20000.00
3           2007        24000.00
3           NULL        44000.00
NULL        NULL        120000.00

В этом запросе стоит отметить несколько моментов. Во-первых, поскольку комбинация значений в столбцах EmpId и Yr уникальна, без предложения WITH ROLLUP запрос просто вернул бы исходные данные. Однако с предложением WITH ROLLUP запрос дает желаемый результат. Во-вторых, порядок столбцов в предложении GROUP BY соответствует предложению WITH ROLLUP. Чтобы понять, почему так, просто попробуйте в запросе поменять местами столбцы EmpId и Yr. После этого, вместо вычисления продаж по сотруднику сначала будут продажи по годам.

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

EmpId

Yr

SUM(Sales)

BY EmpId, Yr

SUM(Sales)

BY EmpId

SUM(Sales)

1

2005

12000.00

12000.00

12000.00

1

2006

18000.00

30000.00

30000.00

1

2007

25000.00

55000.00

55000.00

1

NULL

 

55000.00

55000.00

2

2005

15000.00

15000.00

70000.00

2

2006

6000.00

21000.00

76000.00

2

NULL

 

21000.00

76000.00

3

2006

20000.00

20000.00

96000.00

3

2007

24000.00

44000.00

120000.00

3

NULL

 

44000.00

120000.00

NULL

NULL

 

 

120000.00

Комментариев нет:

Отправить комментарий