SQL Server'da Çalışan Toplamı Hesaplama


170

Aşağıdaki tabloyu (denilen TestTable) düşünün :

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

Gibi bir tarih sırası içinde çalışan bir toplam döndüren bir sorgu istiyorum:

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'de bunu yapmanın çeşitli yolları olduğunu biliyorum .

Özellikle toplama-küme-ifade hile kullanan yöntem bu tür ilgileniyorum:

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

... bu çok etkili ancak bununla ilgili sorunlar olduğunu duydum çünkü UPDATEifadenin satırları doğru sırada işleyeceğini garanti edemezsiniz . Belki bu konuda kesin cevaplar alabiliriz.

Ama belki insanların önerebileceği başka yollar da vardır?

edit: Şimdi kurulum ve yukarıdaki 'güncelleme hilesi' örneği ile bir SqlFiddle ile


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Güncellemenize bir sipariş ekleyin ... ayarladığınız zaman bir garanti alırsınız.
Simon D

Ancak Order by bir UPDATE deyimine uygulanamaz ... değil mi?
codeulike

Ayrıca özellikle SQL Server 2012 kullanıyorsanız sqlperformance.com/2012/07/t-sql-queries/running-totals adresine bakın .
Aaron Bertrand

Yanıtlar:


133

Güncelleme , SQL Server 2012 çalıştırıyorsanız bkz: https://stackoverflow.com/a/10309947

Sorun, Over yan tümcesinin SQL Server uygulaması biraz sınırlı olmasıdır .

Oracle (ve ANSI-SQL) aşağıdakileri yapmanızı sağlar:

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

SQL Server, bu soruna temiz bir çözüm sağlar. Bağırsak, bunun imlecin en hızlı olduğu nadir durumlardan biri olduğunu söylüyor, ancak büyük sonuçlarda bazı kıyaslamalar yapmak zorunda kalacağım.

Güncelleme hilesi kullanışlı ama oldukça kırılgan hissediyorum. Tam bir tabloyu güncelliyorsanız, birincil anahtar sırasına göre ilerleyecek gibi görünüyor. Tarihinizi artan birincil anahtar olarak ayarlarsanız probablygüvende olursunuz . Ancak belgelenmemiş bir SQL Server uygulama ayrıntısına güveniyorsunuz (ayrıca sorgu iki proc tarafından gerçekleştiriliyorsa ne olacağını merak ediyorum, bkz: MAXDOP):

Tam çalışma örnek:

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

Bu alçalma bir kriter istediniz.

Bunu yapmanın en güvenli yolu İmleç olurdu, çapraz birleşmenin ilişkili alt sorgusundan daha hızlı bir büyüklük sırasıdır.

Mutlak en hızlı yol GÜNCELLEME hile. Bununla ilgili tek endişem, her koşulda güncellemenin doğrusal bir şekilde ilerleyeceğinden emin olmadığım. Sorguda açıkça söylenen hiçbir şey yok.

Alt satırda, üretim kodu için imleç ile giderdim.

Test verisi:

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

Test 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 

Test 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

Test 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

Test 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
Teşekkürler. Kod örneğinizin, birincil anahtarın sırasına göre toplanacağını göstermek olduğunu düşünüyorum. İmleçlerin daha büyük veri kümeleri için birleştirmelerden daha verimli olup olmadığını bilmek ilginç olacaktır.
codeulike

1
CTE @ Martin'i test ettim, güncelleme hilesine yakın bir şey gelmiyor - imleç okumalarda daha düşük görünüyor. İşte bir profil izleyici i.stack.imgur.com/BbZq3.png
Sam Saffron


1
Bu cevaba yapılan tüm çalışmalar için +1 - GÜNCELLEME seçeneğini seviyorum; Bu UPDATE komut dosyasına bir bölüm oluşturulabilir? Örneğin, ek bir "Araba Rengi" alanı varsa, bu komut dosyası her "Araba Rengi" bölümü içinde toplamları döndürebilir mi?
whytheq

2
ilk (Oracle (ve ANSI-SQL)) cevabı şimdi SQL server 2017'de çalışıyor. Teşekkür ederim, çok zarif!
DaniDev

121

SQL Server 2012'de SUM () ile OVER () yan tümcesini kullanabilirsiniz.

select id,
       somedate,
       somevalue,
       sum(somevalue) over(order by somedate rows unbounded preceding) as runningtotal
from TestTable

SQL Keman


40

Sam Saffron bu konuda çok çalıştı, ancak yine de bu sorun için yinelemeli ortak tablo ifade kodu vermedi . Denali ile değil, SQL Server 2008 R2 ile çalışan bizim için, hala toplam koşmanın en hızlı yolu, 100000 satır için iş bilgisayarımdaki imleçten yaklaşık 10 kat daha hızlı ve aynı zamanda satır içi sorgu.
Yani, işte ( ordtabloda bir sütun olduğunu ve boşluklar olmadan sıralı sayı olduğunu varsayalım, hızlı işleme için de bu sayı üzerinde benzersiz bir kısıtlama olmalı):

;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

update Ayrıca bu güncellemeyi değişken veya ilginç güncellemeyle merak ettim . Yani genellikle iyi çalışıyor, ama her seferinde çalıştığından nasıl emin olabiliriz? işte küçük bir numara (burada buldum - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - sadece güncel ve öncekiord1/0 ne olduğundan farklıysa ödev kullanın bekliyorsun:

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

Tablonuzda uygun kümelenmiş dizin / birincil anahtar varsa gördüğümden (bizim durumumuzda dizin tarafından ord_id ) güncelleme her zaman doğrusal bir şekilde devam eder (hiçbir zaman sıfıra bölünmez). Bununla birlikte, üretim kodunda kullanmak isteyip istemediğinize karar vermek size kalmış :)

Güncelleme 2 Bu cevabı bağlıyorum, çünkü ilginç güncellemenin güvenilmezliği hakkında bazı yararlı bilgiler içeriyor - nvarchar birleştirme / index / nvarchar (max) açıklanamayan davranış .


6
Bu cevap daha fazla tanınmayı hak ediyor (veya belki de görmediğim bir kusuru var mı?)
user1068352

ord = ord + 1'e katılabilmeniz için sıralı bir sayı olması gerekir ve bazen biraz daha çalışması gerekir. Her neyse, SQL 2008 R2'de bu çözümü kullanıyorum
Roman Pekar

+1 SQLServer2008R2'de özyinelemeli CTE ile yaklaşımı da tercih ederim. FYI, boşluklara izin veren tabloların değerini bulmak için ilişkili bir alt sorgu kullanıyorum. Bu sorguya sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko

2
Verileriniz için zaten bir sıraya sahip olduğunuz ve SQL 2008 R2'de özlü (imleçsiz) ayarlanmış bir çözüm arıyorsanız, bu mükemmel görünüyor.
Nick.McDermaid

1
Çalışan toplam sorguların her biri bitişik olan bir sıralı alana sahip olmaz. Bazen datetime alanı sahip olduğunuz alanlardır veya kayıtlar sıralamanın ortasından silinmiştir. Bu yüzden daha sık kullanılmıyor olabilir.
Reuben

28

SQL 2005 ve sonraki sürümlerdeki APPY operatörü bunun için çalışır:

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
Daha küçük veri kümeleri için çok iyi çalışır. Bir dezavantajı, iç ve dış sorgudaki özdeş cümleciklerin aynı olması gerekir.
Sire

Bazı tarihlerim tam olarak aynı olduğundan (bir saniyenin kesirine kadar) eklemek zorundaydım: iç ve dış tabloya row_number () üzerinden (txndate tarafından sipariş) ve çalışmasını sağlamak için birkaç bileşik endeks. Kaygan / basit bir çözüm. BTW, alt sorguya karşı çapraz test uygulandı ... biraz daha hızlı.
pghcpa

bu çok temiz ve küçük veri kümeleriyle iyi çalışır; özyinelemeli
CTE'den

bu da güzel bir çözümdür (küçük veri kümeleri için), ancak bazı tarih sütunlarının benzersiz olduğunu ima ettiğini de bilmeniz gerekir
Roman Pekar

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

Ayrıca, iç SELECT ifadesinde karşılaştırmada kullanılacak rasgele bir sütun oluşturmak için ROW_NUMBER () işlevini ve geçici tabloyu kullanabilirsiniz.


1
Bu gerçekten verimsiz ... ama sonra tekrar bunu sql sunucusunda yapmanın gerçek temiz bir yolu yok
Sam Saffron

Kesinlikle verimsiz - ama işi yapıyor ve bir şeyin doğru veya yanlış sırayla yürütülüp yürütülmediğine dair bir soru yok.
Sam Ax

teşekkürler, alternatif cevaplar için yararlı ve aynı zamanda verimli bir eleştiri için yararlı
codeulike

7

İlişkili bir alt sorgu kullanın. Çok basit, işte başlıyoruz:

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

Kod tam olarak doğru olmayabilir, ama eminim fikir.

GROUP BY, bir tarihin birden çok kez görünmesi durumunda, sonuç kümesinde yalnızca bir kez görmek istersiniz.

Yinelenen tarihleri ​​görmekten sakınmıyorsanız veya orijinal değeri ve kimliği görmek istiyorsanız, istediğiniz şey aşağıdadır:

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

Teşekkürler ... basit harikaydı. Performans için eklenecek bir dizin vardı, ancak bu yeterince basitti (Veritabanı Motoru Ayarlama Danışmanı'nın önerilerinden birini alarak;) ve sonra bir çekim gibi koştu.
Doug_Ivison


4

Pencerenin SQL Server 2008'de başka bir yerde olduğu gibi çalıştığını varsayarsak (denedim), bunu deneyin:

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

MSDN , SQL Server 2008'de (ve belki de 2005'te) kullanılabileceğini söylüyor, ancak denemek için bir örneğim yok.

DÜZENLEME: iyi, görünüşe göre SQL Server "PARTITION BY" belirtmeden (sonucu gruplar halinde bölmek ama tamamen GROUP BY yaptığı gibi toplama değil) bir pencere belirtimi ("OVER (...)") izin vermez. Can sıkıcı - MSDN sözdizimi başvurusu onun isteğe bağlı olduğunu önerir, ancak şu anda sadece SqlServer 2000 örnekleri var.

Verdiğim sorgu hem Oracle 10.2.0.3.0 hem de PostgreSQL 8.4-beta sürümlerinde çalışıyor. MS'e yetişmesini söyle;)


2
TOPLA ile TOPLA işlevinin kullanılması bu durumda çalışan bir toplam vermek için çalışmaz. TOPLA yan tümcesi, TOPLA ile kullanıldığında ORDER BY kabul etmez. Koşu toplamları için çalışmayacak PARTITION BY kullanmanız gerekir.
Sam Axe

teşekkürler, bunun neden işe yaramayacağını duymak gerçekten yararlı. araqnid belki cevabını neden bunun bir seçenek olmadığını açıklamak için düzenleyebilirsiniz
codeulike


Bu aslında benim için çalışıyor, çünkü bölümlemem gerekiyor - bu en popüler cevap olmasa da, SQL'deki RT sorunum için en kolay çözüm.
William MB

Benimle MSSQL 2008 yok, ama muhtemelen (null select) ile bölümleme ve bölümleme sorunu etrafında kesmek düşünüyorum. Ya da bununla bir alt seçim yapın 1 partitionmeve bölümleyin. Ayrıca, raporlar hazırlanırken muhtemelen gerçek yaşam durumlarında bölümlere ihtiyaç duyulur.
nurettin

4

Yukarıdaki Sql server 2008 R2 kullanıyorsanız. O zaman, bunu yapmanın en kısa yolu olurdu;

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

LAG , önceki satır değerini almak için kullanılır. Daha fazla bilgi için google yapabilirsiniz.

[1]:


1
İnanıyorum LAG sadece SQL server 2012 ve (değil 2008) Yukarıdaki var
Aaa

1
LAG () kullanmak SUM(somevalue) OVER(...) , benim için çok daha temiz görünüyor gibi
gelişmiyor

2

Aşağıdaki basit INNER JOIN işlemi kullanılarak bir toplamın elde edilebileceğine inanıyorum.

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

Evet, bu Sam Saffron'un cevabındaki 'Test 3' ile eşdeğer olduğunu düşünüyorum.
codeulike

2

Aşağıdakiler gerekli sonuçları verecektir.

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 üzerinde kümelenmiş bir dizin olması, performansı büyük ölçüde artıracaktır.


@Dave Bence bu soru bunu yapmanın etkili bir yolunu bulmaya çalışıyor, çapraz birleştirme büyük setler için gerçekten yavaş olacak
Sam Saffron

teşekkürler, alternatif cevaplar için yararlı ve aynı zamanda verimli bir eleştiri için yararlı
codeulike


2

En iyi yol, bir pencere işlevi kullanmak olacak olsa da, basit bir ilişkili alt sorgu kullanılarak da yapılabilir .

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

Muhtemelen burada ne yaptığınız hakkında bilgi vermeli ve bu özel yöntemin avantajlarını / dezavantajlarını not etmelisiniz.
TT.

0

Çalışan toplamı hesaplamanın 2 basit yolu:

Yaklaşım 1 : DBMS'niz Analitik İşlevleri destekliyorsa bu şekilde yazılabilir

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

Yaklaşım 2 : Veritabanı sürümünüz / DBMS'nizin kendisi Analitik İşlevleri desteklemiyorsa OUTER UYGULAMASINI kullanabilirsiniz.

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;

Not: - Farklı bölümler için koşu toplamını ayrı olarak hesaplamanız gerekiyorsa, burada belirtildiği gibi yapılabilir: Satırlardaki Koşu toplamlarını hesaplama ve kimliğe göre gruplama

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.