Біг за рахунком?


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 update vs Hybrid Recursive CTE vs Cursor): ienablemuch.com/2012/05/… Я не включав загальну кількість, яка використовує підхід, заснований на чистому наборі, продуктивність нічого не має бути бажане: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Відповіді:


53

Ось кілька методів, з якими можна порівняти. Спочатку давайте створимо таблицю з деякими фіктивними даними. Я заповнюю це купою випадкових даних із sys.all_column. Ну, це наче випадково - я стежу за тим, щоб дати були суміжними (що насправді важливо лише для однієї з відповідей).

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
  • "рекурсивний 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 з рядком_ номер

Розрахунок рядкових номерів тут трохи дорогий. Знову ж це підтримує максимальний рівень рекурсії 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 з таблицею temp

Викрадати відповідь Мікаеля, як було запропоновано, включити це до тестів.

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 (змінивши верхню на 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

Будь-які ідеї, не застосовуючи тимчасову таблицю (мій процедур вже примушує значення через декілька темп-таблиць за необхідності, тому я намагаюся знайти спосіб уникнути використання іншої темп-таблиці)? Якщо ні, я буду використовувати цей метод. Я думаю, це спрацює

Це також можна зробити за допомогою самостійного приєднання або вкладеного підзапиту, але ці параметри не працюють майже так само. Також, ймовірно, ви все одно будете вдаряти 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 рядків, це зробить пінг-понг, прочитаний між таблицями в 5,050 разів. 200 рядів, це 20 100 разів. Тільки з 1000 рядків вона стрибає експоненціально до 500 500 читає sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Майкл Буен

Я побачив посилання на ваш блог після публікації цього, тепер я бачу, це дуже погана ідея, дякую!
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.