Общая сумма с подсчетом?

34

Как следует из названия, мне нужна помощь в получении промежуточного итога в T-SQL. Проблема в том, что сумма, которую мне нужно сделать, это сумма счета:

sum(count (distinct (customers))) 

Скажем, если бы я вел подсчет один, результат был бы:

Day | CountCustomers
----------------------
5/1  |      1
5/2  |      0
5/3  |      5

Мне нужен вывод с суммой:

Day | RunningTotalCustomers
----------------------
5/1  |      1
5/2  |      1
5/3  |      6

Я выполнил промежуточные итоги до использования coalesceметода, но никогда с подсчетом. Я не уверен, как это сделать сейчас, когда у меня есть счет.

Аарон Бертран
источник
2
Какую версию SQL Server, пожалуйста? Можете ли вы поделиться объемом данных - мы говорим о 1000 строках, миллионах, миллиардах? Это действительно только эти две колонки, или вы упростили схему для нас? Наконец, является Dayли ключ и являются ли значения смежными?
Аарон Бертран
Я сделал подробный блог о промежуточных итогах (Quirky обновление против Hybrid Recursive CTE против Cursor): ienablemuch.com/2012/05/… Я не включал промежуточные итоги, которые используют чисто основанный на множестве подход, производительность ничто не может быть желательно: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Майкл Буэн

Ответы:

53

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

CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);

CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);

INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
  COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
       s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;

SELECT 
  Earliest_Day   = MIN([Day]), 
  Latest_Day     = MAX([Day]), 
  Unique_Days    = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1, 
  Total_Rows     = COUNT(*)
FROM dbo.Hits;

Полученные результаты:

Earliest_Day         Latest_Day           Unique_Days  Total_Days
-------------------  -------------------  -----------  ----------
2012-05-01 00:00:00  2013-09-13 00:00:00  501          5000

Данные выглядят так (5000 строк), но в вашей системе они будут немного отличаться в зависимости от версии и номера сборки:

Day                  CustomerID
-------------------  ---
2012-05-01 00:00:00  95
2012-05-01 00:00:00  97
2012-05-01 00:00:00  97
2012-05-01 00:00:00  117
2012-05-01 00:00:00  100
...
2012-05-02 00:00:00  110
2012-05-02 00:00:00  110
2012-05-02 00:00:00  95
...

И результаты промежуточных итогов должны выглядеть следующим образом (501 строка):

Day                  c   rt
-------------------  --  --
2012-05-01 00:00:00  6   6
2012-05-02 00:00:00  5   11
2012-05-03 00:00:00  4   15
2012-05-04 00:00:00  7   22
2012-05-05 00:00:00  6   28
...

Итак, методы, которые я собираюсь сравнить:

  • «самообъединение» - пуристический подход на основе множеств
  • «рекурсивный CTE с датами» - это зависит от непрерывных дат (без пробелов)
  • "рекурсивный CTE с row_number" - похож на выше, но медленнее, полагаясь на ROW_NUMBER
  • "рекурсивный CTE с таблицей #temp" - украден из ответа Микаэля в соответствии с предложением
  • «странное обновление», которое, хотя и не поддерживается и не обещает определенного поведения, кажется довольно популярным
  • "курсор"
  • SQL Server 2012 с использованием новой функциональности окон

автообъединение

Именно так люди скажут вам сделать это, когда они предупреждают вас держаться подальше от курсоров, потому что «набор на основе всегда быстрее». В некоторых недавних экспериментах я обнаружил, что курсор опережает это решение.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
  FROM g INNER JOIN g AS g2
  ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];

рекурсивный cte с датами

Напоминание - это зависит от непрерывных дат (без пропусков), до 10000 уровней рекурсии и от того, что вы знаете дату начала интересующего диапазона (для установки привязки). Конечно, вы можете динамически устанавливать привязку, используя подзапрос, но я хотел, чтобы все было просто.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], c, rt = c
        FROM g
        WHERE [Day] = '20120501'
    UNION ALL
    SELECT g.[Day], g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

рекурсивный cte с row_number

Расчет Row_number здесь немного дороже. Опять же, это поддерживает максимальный уровень рекурсии 10000, но вам не нужно назначать привязку.

;WITH g AS 
(
  SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY), 
    c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], rn, c, rt = c
        FROM g
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

рекурсивный cte с временной таблицей

Кража из ответа Микаэля, как предлагалось, чтобы включить это в тесты.

CREATE TABLE #Hits
(
  rn INT PRIMARY KEY,
  c INT,
  [Day] SMALLDATETIME
);

INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
       COUNT(DISTINCT CustomerID),
       [Day]
FROM dbo.Hits
GROUP BY [Day];

WITH x AS
(
    SELECT [Day], rn, c, rt = c
        FROM #Hits as c
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN #Hits as g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

DROP TABLE #Hits;

необычное обновление

Опять же, я включил это только для полноты; Лично я бы не стал полагаться на это решение, поскольку, как я уже упоминал в другом ответе, этот метод совсем не гарантированно работает и может полностью сломаться в будущей версии SQL Server. (Я делаю все возможное, чтобы заставить SQL Server подчиняться желаемому порядку, используя подсказку для выбора индекса.)

CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);

INSERT #x([Day], c) 
    SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt1 INT;
SET @rt1 = 0;

UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);

SELECT [Day], c, rt FROM #x ORDER BY [Day];

DROP TABLE #x;

курсор

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

CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);

INSERT #x2([Day], c) 
    SELECT [Day], COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT [Day], c FROM #x2 ORDER BY [Day];

OPEN c;

FETCH NEXT FROM c INTO @d, @c;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt2 = @rt2 + @c;
  UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
  FETCH NEXT FROM c INTO @d, @c;
END

SELECT [Day], c, rt FROM #x2 ORDER BY [Day];

DROP TABLE #x2;

SQL Server 2012

Если вы работаете с самой последней версией SQL Server, усовершенствования функциональности управления окнами позволяют нам легко рассчитать промежуточные итоги без экспоненциальной стоимости самостоятельного объединения (SUM рассчитывается за один проход), сложности CTE (включая требование смежных строк для более эффективной работы CTE), неподдерживаемое причудливое обновление и запрещенный курсор. Просто будьте осторожны с разницей между использованием RANGEи ROWS, или вообще не указанием - ROWSизбегайте только спулинга на диске, который в противном случае значительно снизит производительность.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], c, 
  rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];

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

Я взял каждый подход и упаковал его, используя следующее:

SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();

Вот результаты общей длительности в миллисекундах (помните, что это также включает команды DBCC каждый раз):

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1296 ms   1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1655 ms   1516 ms
recursive cte with row_number   19747 ms  19630 ms
recursive cte with #temp table   1624 ms   1329 ms
quirky update                     880 ms   1030 ms -- non-SQL 2012 winner
cursor                           1962 ms   1850 ms
SQL Server 2012                   847 ms    917 ms -- winner if SQL 2012 available

И я сделал это снова без команд DBCC:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1272 ms   1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1247 ms   1593 ms
recursive cte with row_number   18646 ms  18803 ms
recursive cte with #temp table   1340 ms   1564 ms
quirky update                    1024 ms   1116 ms -- non-SQL 2012 winner
cursor                           1969 ms   1835 ms
SQL Server 2012                   600 ms    569 ms -- winner if SQL 2012 available

Удаление DBCC и циклов, просто измерение одной необработанной итерации:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                         313 ms    242 ms
recursive cte with dates          217 ms    217 ms
recursive cte with row_number    2114 ms   1976 ms
recursive cte with #temp table     83 ms    116 ms -- "supported" non-SQL 2012 winner
quirky update                      86 ms     85 ms -- non-SQL 2012 winner
cursor                           1060 ms    983 ms
SQL Server 2012                    68 ms     40 ms -- winner if SQL 2012 available

Наконец, я умножил количество строк в исходной таблице на 10 (изменив top на 50000 и добавив другую таблицу в качестве перекрестного соединения). Результаты этой одной итерации без команд DBCC (просто в интересах времени):

method                           run 1      run 2
-----------------------------    --------   --------
self-join                         2401 ms    2520 ms
recursive cte with dates           442 ms     473 ms
recursive cte with row_number   144548 ms  147716 ms
recursive cte with #temp table     245 ms     236 ms -- "supported" non-SQL 2012 winner
quirky update                      150 ms     148 ms -- non-SQL 2012 winner
cursor                            1453 ms    1395 ms
SQL Server 2012                    131 ms     133 ms -- winner

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


демонстрация

Я добавил sqlfiddle . Полученные результаты:

введите описание изображения здесь


вывод

В моих тестах выбор был бы:

  1. Метод SQL Server 2012, если у меня есть SQL Server 2012 доступен.
  2. Если SQL Server 2012 недоступен, а даты у меня смежные, я бы выбрал рекурсивный метод cte с датами.
  3. Если ни 1., ни 2. не применимы, я бы согласился на самообъединение из-за причудливого обновления, хотя производительность была близка, просто потому, что поведение задокументировано и гарантировано. Я меньше беспокоюсь о будущей совместимости, потому что, надеюсь, если причудливое обновление прекратится, это произойдет после того, как я уже преобразовал весь свой код в 1. :-)

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


ОБНОВИТЬ

Я написал больше об этом здесь:

Лучшие подходы к подведению итогов - обновлено для SQL Server 2012

Аарон Бертран
источник
1

Это, по-видимому, оптимальное решение

DECLARE @dailyCustomers TABLE (day smalldatetime, CountCustomers int, RunningTotal int)

DECLARE @RunningTotal int

SET @RunningTotal = 0

INSERT INTO @dailyCustomers 
SELECT day, CountCustomers, null
FROM Sales
ORDER BY day

UPDATE @dailyCustomers
SET @RunningTotal = RunningTotal = @RunningTotal + CountCustomers
FROM @dailyCustomers

SELECT * FROM @dailyCustomers
Код Волшебника
источник
Любые идеи без реализации временной таблицы (мой proc уже форсирует значения через несколько временных таблиц по необходимости, поэтому я пытаюсь найти способ избежать использования другой временной таблицы)? Если нет, я буду использовать этот метод. Я думаю, что это будет работать
Это также можно сделать с помощью самостоятельного соединения или вложенного подзапроса, но эти параметры работают не так хорошо. Также, вероятно, вы все равно будете использовать tempdb с этими альтернативами с некоторыми буферизацией или рабочими столами.
3
Просто имейте в виду, что этот метод «необычного обновления» не гарантированно работает - этот синтаксис не поддерживается и его поведение не определено, и он может сломаться в будущей версии, оперативном исправлении или пакете обновления. Так что, хотя да, это быстрее, чем некоторые поддерживаемые альтернативы, это может привести к будущей стоимости совместимости.
Аарон Бертран
6
У этого подхода есть много предостережений, которые Джефф Моден где-то написал. dayНапример, у вас должен быть кластерный индекс .
Мартин Смит
2
@MartinSmith Это ОЧЕНЬ БОЛЬШАЯ статья на sqlservercentral.com (перейдите на страницу автора и найдите его статьи об обновлениях причуд).
Fabricio Araujo
-2

Просто другой способ, дорогой, но независимый от версии. Он не использует временные таблицы или переменные.

select T.dday, T.CustomersByDay + 
    (select count(A.customer) from NewCustomersByDate A 
      where A.dday < T.dday) as TotalCustomerTillNow 
from (select dday, count(customer) as CustomersByDay 
        from NewCustomersByDate group by dday) T 

источник
2
Это не хорошо, это очень медленно. Даже если у вас всего 100 строк, он будет читать пинг-понг между таблицами в 5050 раз. 200 строк, это 20,100 раз. Только с 1000 строк он увеличивается экспоненциально до 500 500
Майкл Буэн,
После публикации я увидел ссылку на ваш блог, теперь я вижу, что это очень плохая идея, спасибо!