Още първата публикация в блога на този сайт, още през юли 2012 г., говори за най-добрите подходи за текущи суми. Оттогава многократно ме питаха как бих подходил към проблема, ако текущите суми са по-сложни – по-конкретно, ако трябва да изчисля текущите суми за множество обекти – да речем, поръчките на всеки клиент.
В оригиналния пример е използван фиктивен случай на град, издаващ глоби за превишена скорост; общият брой просто събираше и поддържаше текущо броене на броя на глобите за превишена скорост на ден (независимо на кого е издаден билетът или за колко е бил). По-сложен (но практичен) пример може да бъде обобщаването на текущата обща стойност на глобите за превишена скорост, групирани по шофьорска книжка, на ден. Нека си представим следната таблица:
CREATE TABLE dbo.SpeedingTickets( IncidentID INT IDENTITY(1,1) PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL); СЪЗДАЙТЕ УНИКАЛЕН ИНДЕКС x ON dbo.SpeedingTickets(LicenseNumber, IncidentDate) INCLUDE(TicketAmount);
Можете да попитате, DECIMAL(7,2)
, наистина ли? Колко бързо вървят тези хора? Е, в Канада, например, не е толкова трудно да получите глоба от 10 000 долара за превишена скорост.
Сега нека попълним таблицата с някои примерни данни. Тук няма да навлизам във всички подробности, но това би трябвало да доведе до около 6000 реда, представящи множество шофьори и множество количества билети за период от един месец:
;WITH TicketAmounts(ID,Value) AS ( -- 10 произволни количества билети ИЗБЕРЕТЕ i,p ОТ ( СТОЙНОСТИ(1,32.75),(2,75), (3,109),(4,175),(5,295), (6,68.50),(7,125),(8,145),(9,199),(10,250) ) AS v(i,p)),LicenseNumbers(LicenseNumber,[newid]) AS ( -- 1000 произволни номера на лицензи ИЗБЕРЕТЕ ТОП ( 1000) 7000000 + число, n =NEWID() ОТ [master].dbo.spt_values КЪДЕ число МЕЖДУ 1 И 999999 ORDER BY n),JanuaryDates([day]) AS ( -- всеки ден през януари 2014 г. SELECT TOP (31) DATEADD(DAY, number, '20140101') FROM [master].dbo.spt_values WHERE [type] =N'P' ORDER BY number),Bickets(LicenseNumber,[day],s) AS( -- съвпадение *some* лицензи до дни, когато са получили билети SELECT DISTINCT l.LicenseNumber, d.[ден], s =RTRIM(l.LicenseNumber) ОТ LicenseNumbers КАТО l CROSS JOIN Януари Дати КАТО d WHERE CHECKSUM(NEWID()) % 100 =l.LicenseNumber % 111 И (RTRIM(l.LicenseNumber) LIKE '%' + RIGHT(CONVERT(CHAR(8), d.[day], 112),1) + '%') ИЛИ (RTRIM(l.LicenseNumber+1) LIKE ' %' + НАДЯСНО( CONVERT(CHAR(8), d.[day], 112),1) + '%'))INSERT dbo.SpeedingTickets(LicenseNumber,IncidentDate,TicketAmount)ИЗБЕРЕТЕ t.LicenseNumber, t.[ден], ta.Стойност ОТ ОТ. Билети КАТО t INNER JOIN Суми на билети КАТО ta ON ta.ID =CONVERT(INT,RIGHT(t.s,1))-CONVERT(INT,LEFT(RIGHT(t.s,2),1)) ORDER BY t.[ден], t. .Номер на лиценз;
Това може да изглежда малко прекалено ангажирано, но едно от най-големите предизвикателства, които често срещам, когато съставя тези публикации в блога, е конструирането на подходящо количество реалистични „случайни“ / произволни данни. Ако имате по-добър метод за произволна популация от данни, в никакъв случай не използвайте мрънканията ми като пример – те са периферни до точката на тази публикация.
Подходи
Има различни начини за решаване на този проблем в T-SQL. Ето седем подхода, заедно със свързаните с тях планове. Пропуснах техники като курсори (защото те несъмнено ще бъдат по-бавни) и базирани на дата рекурсивни CTE (защото зависят от последователни дни).
Подзаявка №1
ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets AS s WHERE s.LicenseNumber =o.LicenseNumber И s.IncidentDate) d.IncidentDateF .SpeedingTickets КАТО ПОРЪЧКА ПО LicenseNumber, IncidentDate;
План за подзаявка №1
Подзаявка №2
ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets WHERE LicenseNumber =t.LicenseNumber И IncidentDate <=t.IncidentDateS )FROM dbo.SpeedingTickets WHERE LicenseNumber =t.LicenseNumber И IncidentDate <=t.IncidentDateS )FROM dbo.SpeedingTickets.>
План за подзаявка №2Самоприсъединяване
ИЗБЕРЕТЕ t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets КАТО t1INNER JOIN dbo.SpeedingTickets AS tL2 ON denticense2. t2.IncidentDateGROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;
Планирайте за самостоятелно присъединяванеВъншно приложение
ИЗБЕРЕТЕ t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets КАТО t1OUTER APPLY( SELECT TicketAmount FROM dbo.SpeedingHERTicketAmount. IncidentDate) КАТО t2GROUP BY t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER BY t1.LicenseNumber, t1.IncidentDate;
План за външно приложениеSUM OVER() с помощта на RANGE (само за 2012+)
ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) НАД ( PARTITION BY LicenseNumber ORDER BY IncidentDate RANGE НЕОГРАНИЧЕН ПРЕДИШЕН ) ОТ dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate RANGE;
Планирайте SUM OVER() с помощта на RANGESUM OVER() с помощта на ROWS (само за 2012+)
ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) НАД ( PARTITION BY LicenseNumber ORDER BY IncidentDate РЕДОВЕ НЕОГРАНИЧЕНИ ПРЕДИШНИ ) ОТ dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate
Планирайте SUM OVER() с помощта на ROWSИтерация, базирана на набор
С признанието на Hugo Kornelis (@Hugo_Kornelis) за глава #4 в SQL Server MVP Deep Dives том #1, този подход съчетава подход, базиран на набори, и подход на курсора.
DECLARE @x TABLE( LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL, PRIMARYDaN KEYumberte,L In ); INSERT @x(LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() НАД (РАЗДЕЛЕНИЕ ПО номер на лиценз ORDER BY IncidentDate) FROM dboTicketsed; ДЕКЛАРИРАНЕ @rn INT =1, @rc INT =1; ДОКАТО @rc> 0ЗАПОЧВАНЕ НА НАСТРОЙКА @rn +=1; АКТУАЛИЗАЦИЯ [текуща] SET RunningTotal =[последен].RunningTotal + [текущ].TicketAmount ОТ @x AS [текущ] INNER JOIN @x AS [последен] ON [текущ].LicenseNumber =[последен].LicenseNumber И [последен]. rn =@rn - 1 WHERE [текущ].rn =@rn; SET @rc =@@ROWCOUNT;END SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal FROM @x ORDER BY LicenseNumber, IncidentDate;Поради естеството си, този подход създава много идентични планове в процеса на актуализиране на променливата на таблицата, всички от които са подобни на плановете за самостоятелно присъединяване и външно прилагане, но могат да използват търсене:
Един от многото планове UPDATE, създадени чрез итерация, базирана на наборЕдинствената разлика между всеки план във всяка итерация е броят на редовете. При всяка следваща итерация броят на засегнатите редове трябва да остане същият или да намалее, тъй като броят на редовете, засегнати при всяка итерация, представлява броя на шофьорите с билети за този брой дни (или по-точно броя на дните в този „ранг“).
Резултати от производителността
Ето как са подредени подходите, както е показано от SQL Sentry Plan Explorer, с изключение на подхода за итерация, базиран на набори, който, тъй като се състои от много отделни изрази, не се представя добре в сравнение с останалите.
Показатели за време на изпълнение на Plan Explorer за шест от седемте подходаВ допълнение към прегледа на плановете и сравняването на показателите по време на изпълнение в Plan Explorer, аз също измерих необработеното време на изпълнение в Management Studio. Ето резултатите от изпълнение на всяка заявка 10 пъти, като се има предвид, че това включва и времето за изобразяване в SSMS:
Продължителност по време на изпълнение, в милисекунди, за всичките седем подхода (10 итерации )Така че, ако използвате SQL Server 2012 или по-добър, най-добрият подход изглежда е
SUM OVER()
използвайкиROWS UNBOUNDED PRECEDING
. Ако не сте на SQL Server 2012, вторият подход на подзаявка изглежда оптимален по отношение на времето на изпълнение, въпреки големия брой четения в сравнение с, да речем,OUTER APPLY
запитване. Във всички случаи, разбира се, трябва да тествате тези подходи, адаптирани към вашата схема, срещу вашата собствена система. Вашите данни, индекси и други фактори могат да доведат до това, че различно решение е най-оптимално във вашата среда.Други сложности
Сега уникалният индекс означава, че всяка комбинация LicenseNumber + IncidentDate ще съдържа единична обща сума, в случай че конкретен шофьор получи няколко билета в даден ден. Това бизнес правило помага да опростим малко нашата логика, избягвайки необходимостта от тай-брейк за получаване на детерминирани текущи суми.
Ако имате случаи, в които може да имате няколко реда за дадена комбинация LicenseNumber + IncidentDate, можете да прекъснете връзката, като използвате друга колона, която помага да направите комбинацията уникална (очевидно таблицата източник вече няма да има уникално ограничение за тези две колони) . Имайте предвид, че това е възможно дори в случаите, когато
DATE
колоната всъщност еDATETIME
– много хора приемат, че стойностите за дата/час са уникални, но това със сигурност не винаги е гарантирано, независимо от детайлността.В моя случай бих могъл да използвам
IDENTITY
колона,IncidentID
; ето как бих коригирал всяко решение (като признавам, че може да има по-добри начини; просто изхвърлям идеи):/* --------- подзаявка #1 --------- */ ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo. SpeedingTickets AS s WHERE s.LicenseNumber =o.LicenseNumber И (s.IncidentDate=t2.IncidentDate -- добавен е този ред:AND t1.IncidentID>=t2.IncidentID. .TicketAmount ORDER BY t1.LicenseNumber, t1.IncidentDate; /* --------- външно приложение --------- */ ИЗБЕРЕТЕ t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets AS t1OUTER APPLY( ИЗБЕРЕТЕ TicketAmount ОТ dbo.SpeedingTickets КЪДЕ LicenseNumber =t1.LicenseNumber И IncidentDate <=t1.IncidentDate -- добавен е този ред:AND IncidentID <=t1.IncidentID) КАТО t2GROUPLicdentTickets. BY t1.LicenseNumber, t1.IncidentDate; /* --------- SUM() НАД с помощта на RANGE --------- */ ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) НАД ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID RANGE НЕОГРАНИЧЕН ПРЕДИШЕН -- добави тази колона ^^^^^^^^^^^^ ) ОТ dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- SUM() НАД с помощта на РЕДОВЕ --------- */ ИЗБЕРЕТЕ LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) НАД ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID РЕДОВЕ НЕОГРАНИЧЕНИ ПРЕДИШНИ -- добави тази колона ^^^^^^^^^^^^ ) FROM dbo.SpeedingTickets ORDER BY LicenseNumber, IncidentDate; /* --------- базирана на набор итерация --------- */ DECLARE @x TABLE( -- добави тази колона и я направи PK:IncidentID INT PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL); -- добави допълнителната колона към INSERT/SELECT:INSERT @x(IncidentID, LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT IncidentID, LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() НАД (PARTITIONteumber BY LicenseDate BY License , IncidentID) -- и добави тази колона за прекъсване ------------------------------^^^^^^^^^ ^^^^ ОТ dbo.SpeedingTickets; -- останалата част от итерационното решение, базирано на множество, остана непроменено Друго усложнение, на което може да се сблъскате, е, когато не следите цялата таблица, а по-скоро подмножество (да речем, в този случай, първата седмица на януари). Ще трябва да направите корекции, като добавите
WHERE
клаузи и имайте предвид тези предикати, когато имате и свързани подзаявки.