Обчисліть загальний обсяг роботи в SQL Server


170

Уявіть таку таблицю (називається TestTable):

id     somedate    somevalue
--     --------    ---------
45     01/Jan/09   3
23     08/Jan/09   5
12     02/Feb/09   0
77     14/Feb/09   7
39     20/Feb/09   34
33     02/Mar/09   6

Я хотів би отримати запит, який повертає поточну загальну кількість у порядку дат, наприклад:

id     somedate    somevalue  runningtotal
--     --------    ---------  ------------
45     01/Jan/09   3          3
23     08/Jan/09   5          8
12     02/Feb/09   0          8
77     14/Feb/09   7          15  
39     20/Feb/09   34         49
33     02/Mar/09   6          55

Я знаю, що в SQL Server 2000/2005/2008 існують різні способи .

Мене особливо цікавить такий метод, який використовує трюк агрегування-встановлення оператора:

INSERT INTO @AnotherTbl(id, somedate, somevalue, runningtotal) 
   SELECT id, somedate, somevalue, null
   FROM TestTable
   ORDER BY somedate

DECLARE @RunningTotal int
SET @RunningTotal = 0

UPDATE @AnotherTbl
SET @RunningTotal = runningtotal = @RunningTotal + somevalue
FROM @AnotherTbl

... це дуже ефективно, але я чув, що навколо цього є проблеми, тому що ви не можете обов'язково гарантувати, що UPDATEоператор буде обробляти рядки в правильному порядку. Можливо, ми можемо отримати певні відповіді з цього питання.

Але, можливо, є й інші способи, які люди можуть запропонувати?

редагувати: Тепер із SqlFiddle з налаштуванням та прикладом 'трюк оновлення' вище


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Додайте замовлення до свого оновлення ... встановіть, і ви отримаєте гарантію.
Саймон Д

Але Order by не можна застосувати до оператора UPDATE ... чи можна?
codeulike

Також дивіться sqlperformance.com/2012/07/t-sql-queries/running-totals, особливо якщо ви використовуєте SQL Server 2012.
Аарон Бертран

Відповіді:


133

Оновлення , якщо ви використовуєте SQL Server 2012, див. Https://stackoverflow.com/a/10309947

Проблема полягає в тому, що реалізація пункту Over на SQL Server дещо обмежена .

Oracle (і ANSI-SQL) дозволяють робити такі речі, як:

 SELECT somedate, somevalue,
  SUM(somevalue) OVER(ORDER BY somedate 
     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 
          AS RunningTotal
  FROM Table

SQL Server не дає чіткого рішення цієї проблеми. Моя кишка говорить мені, що це один з тих рідкісних випадків, коли курсор найшвидший, хоча мені доведеться зробити певний тест на великі результати.

Трюк оновлення зручний, але я вважаю його досить крихким. Здається, що якщо ви оновлюєте повну таблицю, то вона пройде в порядку первинного ключа. Тож якщо ви встановите дату в якості основного ключа у порядку зростання, ви будете в probablyбезпеці. Але ви покладаєтесь на недокументовану деталь реалізації SQL Server (також якщо запит виконується двома документами, мені цікаво, що буде, див. MAXDOP):

Повний робочий зразок:

drop table #t 
create table #t ( ord int primary key, total int, running_total int)

insert #t(ord,total)  values (2,20)
-- notice the malicious re-ordering 
insert #t(ord,total) values (1,10)
insert #t(ord,total)  values (3,10)
insert #t(ord,total)  values (4,1)

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t
order by ord 

ord         total       running_total
----------- ----------- -------------
1           10          10
2           20          30
3           10          40
4           1           41

Ви попросили орієнтир, це низький рівень.

Найшвидший БЕЗПЕЧНИЙ спосіб зробити це курсор, це на порядок швидше, ніж корельований підзапит на перехресне з'єднання.

Абсолютний найшвидший спосіб - трюк UPDATE. Моє єдине занепокоєння з цього приводу полягає в тому, що я не впевнений, що за будь-яких обставин оновлення буде проходити лінійно. У запиті немає нічого прямо сказаного.

Підсумок, для виробничого коду я б пішов курсором.

Дані тесту:

create table #t ( ord int primary key, total int, running_total int)

set nocount on 
declare @i int
set @i = 0 
begin tran
while @i < 10000
begin
   insert #t (ord, total) values (@i,  rand() * 100) 
    set @i = @i +1
end
commit

Тест 1:

SELECT ord,total, 
    (SELECT SUM(total) 
        FROM #t b 
        WHERE b.ord <= a.ord) AS b 
FROM #t a

-- CPU 11731, Reads 154934, Duration 11135 

Тест 2:

SELECT a.ord, a.total, SUM(b.total) AS RunningTotal 
FROM #t a CROSS JOIN #t b 
WHERE (b.ord <= a.ord) 
GROUP BY a.ord,a.total 
ORDER BY a.ord

-- CPU 16053, Reads 154935, Duration 4647

Тест 3:

DECLARE @TotalTable table(ord int primary key, total int, running_total int)

DECLARE forward_cursor CURSOR FAST_FORWARD 
FOR 
SELECT ord, total
FROM #t 
ORDER BY ord


OPEN forward_cursor 

DECLARE @running_total int, 
    @ord int, 
    @total int
SET @running_total = 0

FETCH NEXT FROM forward_cursor INTO @ord, @total 
WHILE (@@FETCH_STATUS = 0)
BEGIN
     SET @running_total = @running_total + @total
     INSERT @TotalTable VALUES(@ord, @total, @running_total)
     FETCH NEXT FROM forward_cursor INTO @ord, @total 
END

CLOSE forward_cursor
DEALLOCATE forward_cursor

SELECT * FROM @TotalTable

-- CPU 359, Reads 30392, Duration 496

Тест 4:

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t

-- CPU 0, Reads 58, Duration 139

1
Дякую. Отже, ваш зразок коду повинен продемонструвати, що він буде підсумовуватися в порядку первинного ключа, я припускаю. Було б цікаво знати, чи курсори все-таки ефективніші, ніж з'єднання для великих наборів даних.
codeulike

1
Я щойно тестував CTE @Martin, нічого не наближається до трюку оновлення - курсор здається нижчим на показаннях. Ось прослідковувальний слід i.stack.imgur.com/BbZq3.png
Сем Сафрон

3
@Martin Denali має гарне рішення для цього msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
Сем Сафрон

1
+1 за всю роботу, поставлену в цій відповіді - я люблю варіант UPDATE; чи може бути вбудований розділ у цей UPDATE-скрипт? Наприклад, якщо було додаткове поле "Колір автомобіля", чи може цей скрипт повернути загальні підсумки в кожному розділі "Колір автомобіля"?
whyqq

2
початкова відповідь (Oracle (і ANSI-SQL)) зараз працює на SQL сервері 2017. Дякую, дуже елегантно!
DaniDev


40

Хоча Сем Сафрон зробив велику роботу над цим, він все ще не надав рекурсивний загальний код вираження таблиці для цієї проблеми. А для нас, хто працює з SQL Server 2008 R2, а не Деналі, це все-таки найшвидший спосіб отримати загальний запуск, це приблизно в 10 разів швидше, ніж курсор на моєму робочому комп'ютері на 100000 рядків, і це також вбудований запит.
Отже, ось (я припускаю, що ordв таблиці є стовпець і це послідовне число без пропусків, для швидкої обробки також має бути унікальне обмеження на це число):

;with 
CTE_RunningTotal
as
(
    select T.ord, T.total, T.total as running_total
    from #t as T
    where T.ord = 0
    union all
    select T.ord, T.total, T.total + C.running_total as running_total
    from CTE_RunningTotal as C
        inner join #t as T on T.ord = C.ord + 1
)
select C.ord, C.total, C.running_total
from CTE_RunningTotal as C
option (maxrecursion 0)

-- CPU 140, Reads 110014, Duration 132

sql fiddle demo

Оновлення Мені також було цікаво про це оновлення зі змінним або химерним оновленням . Так зазвичай це нормально, але як ми можемо бути впевнені, що він працює щоразу? ну ось невеликий трюк (знайдено його тут - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - ви просто перевіряєте поточне та попереднє ordта використовуєте 1/0завдання, якщо вони відрізняються від того, що ви очікуєте:

declare @total int, @ord int

select @total = 0, @ord = -1

update #t set
    @total = @total + total,
    @ord = case when ord <> @ord + 1 then 1/0 else ord end,
    ------------------------
    running_total = @total

select * from #t

-- CPU 0, Reads 58, Duration 139

З того, що я бачив, якщо у вас на столі належним кластеризований індекс / первинний ключ (у нашому випадку це буде індексувати ord_id), оновлення триватиме лінійним шляхом весь час (ніколи не зустрічалося ділення на нуль). З цього приводу вирішувати, чи бажаєте ви використовувати його у виробничому коді, вирішувати :)

оновлення 2 Я пов'язую цю відповідь, тому що вона містить корисну інформацію про ненадійність вигадливого оновлення - nvarchar concatenation / index / nvarchar (max) незрозумілою поведінкою .


6
Ця відповідь заслуговує на більше визнання (а може, у неї є якийсь недолік, якого я не бачу?)
user1068352

має бути послідовний номер, щоб ви могли приєднатися до ord = ord + 1, а іноді потрібно трохи більше працювати. Але все одно, на SQL 2008 R2 я використовую це рішення
Роман Пекар

+1 На SQLServer2008R2 я також віддаю перевагу підходу з рекурсивним CTE. FYI, щоб знайти значення для таблиць, які дозволяють прогалини, я використовую співвіднесений підзапит. Він додає дві додаткові операції пошуку до запиту sqlfiddle.com/#!3/d41d8/18967
Олександр Федоренко

2
У випадку, коли у вас вже є порядковий запис ваших даних, і ви шукаєте стислого (не курсорного) рішення на основі SQL 2008 R2, це здається ідеальним.
Nick.McDermaid

1
Не кожен запущений загальний запит матиме порядкове поле, яке є суміжним. Іноді це поле для дати - це те, що у вас є, або записи видалено з середини сортування. Ось чому це не звикає частіше.
Рувим

28

Оператор APPLY у SQL 2005 та новіших версіях працює для цього:

select
    t.id ,
    t.somedate ,
    t.somevalue ,
    rt.runningTotal
from TestTable t
 cross apply (select sum(somevalue) as runningTotal
                from TestTable
                where somedate <= t.somedate
            ) as rt
order by t.somedate

5
Дуже добре працює для менших наборів даних. Недоліком є ​​те, що вам доведеться мати тотожні, де є пропозиції внутрішнього та зовнішнього запиту.
Сир

Оскільки деякі мої дати були абсолютно однаковими (аж до частки секунди), я повинен був додати: row_number () over (порядок за txndate) до внутрішньої та зовнішньої таблиці та кілька складних індексів, щоб змусити її працювати. Витончене / просте рішення. До речі, перевірений крос застосовується проти підзапиту ... це трохи швидше.
pghcpa

це дуже чисто і добре працює з невеликими наборами даних; швидше рекурсивного CTE
jtate

це також приємне рішення (для невеликих наборів даних), але ви також повинні мати на увазі, що це означає, що колонка сомедат буде унікальною
Роман Пекар,

11
SELECT TOP 25   amount, 
    (SELECT SUM(amount) 
    FROM time_detail b 
    WHERE b.time_detail_id <= a.time_detail_id) AS Total FROM time_detail a

Ви також можете скористатися функцією ROW_NUMBER () та тимчасовою таблицею, щоб створити довільний стовпець, який слід використовувати для порівняння у внутрішньому операторі SELECT.


1
Це дійсно неефективно ... але знову-таки немає справжнього чистого способу зробити це на сервері sql
Сем Сафрон

Абсолютно це неефективно - але це виконує роботу, і не виникає сумніву, чи виконано щось у правильному чи неправильному порядку.
Сем Акс

дякую, корисно мати альтернативні відповіді, а також корисно мати ефективну критику
codeulike

7

Використовуйте співвіднесений підзапит. Дуже просто, ось що:

SELECT 
somedate, 
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
GROUP BY somedate
ORDER BY somedate

Код може бути не зовсім правильним, але я впевнений, що ідея така.

GROUP BY - якщо дата з’являється більше одного разу, ви хочете побачити її лише один раз у наборі результатів.

Якщо ви не проти бачити повторювані дати або хочете побачити оригінальне значення та ідентифікатор, то вам потрібно:

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate

Дякую ... просто було чудово. Існував індекс, який потрібно додати для продуктивності, але це було досить просто (взявши одну з рекомендацій з радника з налаштування баз даних двигунів;), а потім він пройшов, як вистрілив.
Doug_Ivison


4

Якщо припустити, що вікна працює на SQL Server 2008, як це робиться в інших місцях (що я намагався), спробуйте:

select testtable.*, sum(somevalue) over(order by somedate)
from testtable
order by somedate;

MSDN каже, що вона доступна в SQL Server 2008 (а може бути, і в 2005 році?), Але я не маю примірника для того, щоб спробувати це.

EDIT: ну, мабуть, SQL Server не дозволяє специфікувати вікно ("OVER (...)") без вказівки "PARTITION BY" (поділ результату на групи, але не агрегуючий так, як це робить GROUP BY). Дратівливо - посилання на синтаксис MSDN говорить про те, що його необов'язково, але в даний момент у мене є лише екземпляри SqlServer 2000.

Запит, який я дав, працює як в Oracle 10.2.0.3.0, так і в PostgreSQL 8.4-beta. Тому скажіть МС наздогнати;)


2
Використання OVER з SUM у цьому випадку не допоможе отримати загальну кількість. Стаття OVER не приймає ORDER BY при використанні з SUM. Ви повинні використовувати PARTITION BY, який не працює для підсумків.
Сем Акс

дякую, насправді корисно почути, чому ця робота працює. araqnid, можливо, ви могли б відредагувати свою відповідь, щоб пояснити, чому це не варіант
codeulike


Це насправді працює для мене, тому що мені потрібно розділити - так що, хоча це не найпопулярніша відповідь, це найпростіше рішення моєї проблеми з RT в SQL.
Вільям МБ

У мене немає MSSQL 2008, але я думаю, що ви, ймовірно, могли б розділити (select null) і зламати навколо проблеми з розділенням. Або зробіть підвідбір з 1 partitionmeі розділ за цим. Також розділ by, мабуть, потрібен у реальних ситуаціях, коли роблять звіти.
Нуреттін

4

Якщо ви використовуєте сервер Sql 2008 R2 вище. Тоді це було б найкоротшим способом;

Select id
    ,somedate
    ,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable 

LAG використовується для отримання попереднього значення рядка Ви можете зробити Google для отримання додаткової інформації.

[1]:


1
Я вважаю, що LAG існує лише на SQL сервері 2012 і вище (не 2008)
AaA

1
Використання LAG () не покращується, SUM(somevalue) OVER(...) що мені здається набагато чистішим
Used_By_Already

2

Я вважаю, що загальний обсяг можна досягти, використовуючи просту операцію INNER JOIN нижче.

SELECT
     ROW_NUMBER() OVER (ORDER BY SomeDate) AS OrderID
    ,rt.*
INTO
    #tmp
FROM
    (
        SELECT 45 AS ID, CAST('01-01-2009' AS DATETIME) AS SomeDate, 3 AS SomeValue
        UNION ALL
        SELECT 23, CAST('01-08-2009' AS DATETIME), 5
        UNION ALL
        SELECT 12, CAST('02-02-2009' AS DATETIME), 0
        UNION ALL
        SELECT 77, CAST('02-14-2009' AS DATETIME), 7
        UNION ALL
        SELECT 39, CAST('02-20-2009' AS DATETIME), 34
        UNION ALL
        SELECT 33, CAST('03-02-2009' AS DATETIME), 6
    ) rt

SELECT
     t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
    ,SUM(t2.SomeValue) AS RunningTotal
FROM
    #tmp t1
    JOIN #tmp t2
        ON t2.OrderID <= t1.OrderID
GROUP BY
     t1.OrderID
    ,t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
ORDER BY
    t1.OrderID

DROP TABLE #tmp

Так, я вважаю, що це відповідь "Тесту 3" у відповіді Сема Сафрона.
codeulike

2

Нижче наведено необхідні результати.

SELECT a.SomeDate,
       a.SomeValue,
       SUM(b.SomeValue) AS RunningTotal
FROM TestTable a
CROSS JOIN TestTable b
WHERE (b.SomeDate <= a.SomeDate) 
GROUP BY a.SomeDate,a.SomeValue
ORDER BY a.SomeDate,a.SomeValue

Наявність кластерного індексу на SomeDate значно покращить продуктивність.


@Dave Я думаю, що це питання намагається знайти ефективний спосіб зробити це, перехресне з’єднання буде дуже повільним для великих наборів
Сем Сафрон

дякую, корисно мати альтернативні відповіді, а також корисно мати ефективну критику
codeulike

2

Використання приєднання Іншим варіантом є використання приєднання. Тепер запит може виглядати так:

    SELECT a.id, a.value, SUM(b.Value)FROM   RunTotalTestData a,
    RunTotalTestData b
    WHERE b.id <= a.id
    GROUP BY a.id, a.value 
    ORDER BY a.id;

Докладніше ви можете відвідати це посилання http://askme.indianyouth.info/details/calculating-simple-running-totals-in-sql-server-12


2

Хоча найкращим способом зробити це буде використання віконної функції, це також можна зробити за допомогою простого співвіднесеного підзапиту .

Select id, someday, somevalue, (select sum(somevalue) 
                                from testtable as t2
                                where t2.id = t1.id
                                and t2.someday <= t1.someday) as runningtotal
from testtable as t1
order by id,someday;

0
BEGIN TRAN
CREATE TABLE #Table (_Id INT IDENTITY(1,1) ,id INT ,    somedate VARCHAR(100) , somevalue INT)


INSERT INTO #Table ( id  ,    somedate  , somevalue  )
SELECT 45 , '01/Jan/09', 3 UNION ALL
SELECT 23 , '08/Jan/09', 5 UNION ALL
SELECT 12 , '02/Feb/09', 0 UNION ALL
SELECT 77 , '14/Feb/09', 7 UNION ALL
SELECT 39 , '20/Feb/09', 34 UNION ALL
SELECT 33 , '02/Mar/09', 6 

;WITH CTE ( _Id, id  ,  _somedate  , _somevalue ,_totvalue ) AS
(

 SELECT _Id , id  ,    somedate  , somevalue ,somevalue
 FROM #Table WHERE _id = 1
 UNION ALL
 SELECT #Table._Id , #Table.id  , somedate  , somevalue , somevalue + _totvalue
 FROM #Table,CTE 
 WHERE #Table._id > 1 AND CTE._Id = ( #Table._id-1 )
)

SELECT * FROM CTE

ROLLBACK TRAN

Напевно, ви повинні дати деяку інформацію щодо того, що ви тут робите, і відзначити будь-які переваги / недоліки цього конкретного методу.
ТТ.

0

Ось два простих способи розрахунку загальної кількості запущених даних:

Підхід 1 : Це можна записати так, якщо ваші СУБД підтримують аналітичні функції

SELECT     id
           ,somedate
           ,somevalue
           ,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM       TestTable

Підхід 2 : Ви можете скористатися OUTER APPLY, якщо версія бази даних / сама СУБД не підтримує аналітичні функції

SELECT     T.id
           ,T.somedate
           ,T.somevalue
           ,runningtotal = OA.runningtotal
FROM       TestTable T
           OUTER APPLY (
                           SELECT   runningtotal = SUM(TI.somevalue)
                           FROM     TestTable TI
                           WHERE    TI.somedate <= S.somedate
                       ) OA;

Примітка: - Якщо вам доведеться окремо обчислити загальну кількість запуску для різних розділів, це можна зробити так, як розміщено тут: Обчислення загальної кількості за рядками та групування за ідентифікатором

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