Total cumulé en SQL

Le rĂ©sultat croissant (cumulatif) a longtemps Ă©tĂ© considĂ©rĂ© comme l'un des appels SQL. Étonnamment, mĂȘme aprĂšs l'apparition des fonctions de la fenĂȘtre, il continue d'ĂȘtre un Ă©pouvantail (en tout cas, pour les dĂ©butants). Aujourd'hui, nous examinons la mĂ©canique des 10 solutions les plus intĂ©ressantes Ă  ce problĂšme - des fonctions de fenĂȘtre aux hacks trĂšs spĂ©cifiques.

Dans des feuilles de calcul comme Excel, le total cumulé est calculé trÚs simplement: le résultat du premier enregistrement correspond à sa valeur:



... puis nous résumons la valeur actuelle et le total précédent.



En d'autres termes

Total1=Value1Total2=Total1+Value2Total3=Total2+Value3 ldotsTotaln=Totaln−1+Valuen


... ou:

 begincasesTotal1=Value1,n=1Totaln=Totaln−1+Valuen,n geq2 endcases



L'apparition de deux ou plusieurs groupes dans le tableau complique quelque peu la tùche: nous comptons maintenant plusieurs résultats (pour chaque groupe séparément). Cependant, ici, la solution se trouve à la surface: à chaque fois, il est nécessaire de vérifier à quel groupe appartient l'enregistrement actuel. Cliquez et faites glisser , et le travail est terminé:



Comme vous pouvez le voir, le calcul du total cumulé est associé à deux composantes inchangées:
(a) trier les données par date et
(b) se référant à la ligne précédente.

Mais qu'est-ce que SQL? Pendant trĂšs longtemps, il n'y avait aucune fonctionnalitĂ© nĂ©cessaire. Un outil nĂ©cessaire - les fonctions de fenĂȘtre - n'est apparu pour la premiĂšre fois que dans la norme SQL: 2003 . À ce stade, ils Ă©taient dĂ©jĂ  dans Oracle (version 8i). Mais l'implĂ©mentation dans d'autres SGBD a Ă©tĂ© retardĂ©e de 5 Ă  10 ans: SQL Server 2012, MySQL 8.0.2 (2018), MariaDB 10.2.0 (2017), PostgreSQL 8.4 (2009), DB2 9 for z / OS (2007 annĂ©e), et mĂȘme SQLite 3.25 (2018).

Données de test
--       -- --   create table test_simple (dt date null, val int null ); --      (  , .  NLS_DATE_FORMAT  Oracle) insert into test_simple (dt, val) values ('2019-11-01', 6); insert into test_simple (dt, val) values ('2019-11-02', 3); insert into test_simple (dt, val) values ('2019-11-03', 3); insert into test_simple (dt, val) values ('2019-11-04', 4); insert into test_simple (dt, val) values ('2019-11-05', 2); insert into test_simple (dt, val) values ('2019-11-06', 4); insert into test_simple (dt, val) values ('2019-11-07', 8); insert into test_simple (dt, val) values ('2019-11-08', 0); insert into test_simple (dt, val) values ('2019-11-09', 6); insert into test_simple (dt, val) values ('2019-11-10', 0); insert into test_simple (dt, val) values ('2019-11-11', 8); insert into test_simple (dt, val) values ('2019-11-12', 8); insert into test_simple (dt, val) values ('2019-11-13', 0); insert into test_simple (dt, val) values ('2019-11-14', 2); insert into test_simple (dt, val) values ('2019-11-15', 8); insert into test_simple (dt, val) values ('2019-11-16', 7); --    create table test_groups (grp varchar null, -- varchar2(1) in Oracle dt date null, val int null ); --      (  , .  NLS_DATE_FORMAT  Oracle) insert into test_groups (grp, dt, val) values ('a', '2019-11-06', 1); insert into test_groups (grp, dt, val) values ('a', '2019-11-07', 3); insert into test_groups (grp, dt, val) values ('a', '2019-11-08', 4); insert into test_groups (grp, dt, val) values ('a', '2019-11-09', 1); insert into test_groups (grp, dt, val) values ('a', '2019-11-10', 7); insert into test_groups (grp, dt, val) values ('b', '2019-11-06', 9); insert into test_groups (grp, dt, val) values ('b', '2019-11-07', 10); insert into test_groups (grp, dt, val) values ('b', '2019-11-08', 9); insert into test_groups (grp, dt, val) values ('b', '2019-11-09', 1); insert into test_groups (grp, dt, val) values ('b', '2019-11-10', 10); insert into test_groups (grp, dt, val) values ('c', '2019-11-06', 4); insert into test_groups (grp, dt, val) values ('c', '2019-11-07', 10); insert into test_groups (grp, dt, val) values ('c', '2019-11-08', 9); insert into test_groups (grp, dt, val) values ('c', '2019-11-09', 4); insert into test_groups (grp, dt, val) values ('c', '2019-11-10', 4); --   -- select * from test_simple order by dt; select * from test_groups order by grp, dt; 


1. Fonctions de fenĂȘtre


Les fonctions de fenĂȘtre sont probablement le moyen le plus simple. Dans le cas de base (tableau sans groupes), nous considĂ©rons les donnĂ©es triĂ©es par date:

 order by dt 

... mais nous ne sommes intéressés que par les lignes avant la ligne actuelle:

 rows between unbounded preceding and current row 

En fin de compte, nous avons besoin d'une somme avec ces paramĂštres:

 sum(val) over (order by dt rows between unbounded preceding and current row) 

Une demande complĂšte ressemblerait Ă  ceci:

 select s.*, coalesce(sum(s.val) over (order by s.dt rows between unbounded preceding and current row), 0) as total from test_simple s order by s.dt; 

Dans le cas d'un total cumulĂ© pour les groupes (champ grp ), nous n'avons besoin que d'une petite modification. Maintenant, nous considĂ©rons les donnĂ©es comme divisĂ©es en «fenĂȘtres» basĂ©es sur le groupe:



Pour tenir compte de cette séparation, vous devez utiliser la partition by mot-clé:

 partition by grp 

Et, en consĂ©quence, considĂ©rez le montant de ces fenĂȘtres:

 sum(val) over (partition by grp order by dt rows between unbounded preceding and current row) 

Ensuite, la requĂȘte entiĂšre est convertie comme ceci:

 select tg.*, coalesce(sum(tg.val) over (partition by tg.grp order by tg.dt rows between unbounded preceding and current row), 0) as total from test_groups tg order by tg.grp, tg.dt; 

Les performances des fonctions de fenĂȘtre dĂ©pendront des spĂ©cificitĂ©s de votre SGBD (et de sa version!), De la taille de la table et de la disponibilitĂ© des index. Mais dans la plupart des cas, cette mĂ©thode sera la plus efficace. Cependant, les fonctions de fenĂȘtre ne sont pas disponibles dans les anciennes versions du SGBD (qui sont toujours utilisĂ©es). De plus, ils ne se trouvent pas dans des SGBD tels que Microsoft Access et SAP / Sybase ASE. Si une solution indĂ©pendante du fournisseur est nĂ©cessaire, des alternatives doivent ĂȘtre envisagĂ©es.

2. Sous-requĂȘte


Comme mentionnĂ© ci-dessus, les fonctions de fenĂȘtre ont Ă©tĂ© introduites trĂšs tard dans le SGBD principal. Ce retard n'est pas surprenant: en thĂ©orie relationnelle, les donnĂ©es ne sont pas ordonnĂ©es. Beaucoup plus Ă  l'esprit de la thĂ©orie relationnelle correspond Ă  une solution Ă  travers une sous-requĂȘte.

Une telle sous-requĂȘte doit considĂ©rer la somme des valeurs avec une date antĂ©rieure Ă  l'actuelle (et y compris l'actuelle): dtligne leqdtligneactuelle.

À quoi ressemble le code:

 select s.*, (select coalesce(sum(t2.val), 0) from test_simple t2 where t2.dt <= s.dt) as total from test_simple s order by s.dt; 

Une solution lĂ©gĂšrement plus efficace sera dans laquelle la sous-requĂȘte considĂšre le total jusqu'Ă  la date actuelle (mais ne l'inclut pas), puis le rĂ©sume avec la valeur de la ligne:

 select s.*, s.val + (select coalesce(sum(t2.val), 0) from test_simple t2 where t2.dt < s.dt) as total from test_simple s order by s.dt; 

Dans le cas d'un rĂ©sultat cumulatif pour plusieurs groupes, nous devons utiliser une sous-requĂȘte corrĂ©lĂ©e:

 select g.*, (select coalesce(sum(t2.val), 0) as total from test_groups t2 where g.grp = t2.grp and t2.dt <= g.dt) as total from test_groups g order by g.grp, g.dt; 

La condition g.grp = t2.grp vĂ©rifie les lignes Ă  inclure dans le groupe (ce qui, en principe, est similaire au travail de partition by grp dans les fonctions de fenĂȘtre).

3. Connexion interne


Étant donnĂ© que les sous-requĂȘtes et les jointures sont interchangeables, nous pouvons facilement les remplacer les unes par les autres. Pour ce faire, vous devez utiliser Self Join, en connectant deux instances de la mĂȘme table:

 select s.*, coalesce(sum(t2.val), 0) as total from test_simple s inner join test_simple t2 on t2.dt <= s.dt group by s.dt, s.val order by s.dt; 

Comme vous pouvez le voir, la condition de filtrage dans la sous-requĂȘte t2.dt <= s.dt est devenue une condition de jointure. De plus, pour utiliser la fonction d'agrĂ©gation sum() nous devons regrouper par date et valeur par group by s.dt, s.val .

De mĂȘme, vous pouvez faire pour le cas avec diffĂ©rents groupes grp :

 select g.*, coalesce(sum(t2.val), 0) as total from test_groups g inner join test_groups t2 on g.grp = t2.grp and t2.dt <= g.dt group by g.grp, g.dt, g.val order by g.grp, g.dt; 

4. Produit cartésien


Puisque nous avons remplacĂ© la sous-requĂȘte par join, pourquoi ne pas essayer le produit cartĂ©sien? Cette solution ne nĂ©cessitera que des modifications minimales:

 select s.*, coalesce(sum(t2.val), 0) as total from test_simple s, test_simple t2 where t2.dt <= s.dt group by s.dt, s.val order by s.dt; 

Ou pour les groupes:

 select g.*, coalesce(sum(t2.val), 0) as total from test_groups g, test_groups t2 where g.grp = t2.grp and t2.dt <= g.dt group by g.grp, g.dt, g.val order by g.grp, g.dt; 

Les solutions listĂ©es (sous-requĂȘte, jointure interne, jointure cartĂ©sienne) correspondent Ă  SQL-92 et SQL: 1999 , et seront donc disponibles dans presque tous les SGBD. Le principal problĂšme avec toutes ces solutions est une mauvaise performance. Ce n'est pas un gros problĂšme si nous matĂ©rialisons un tableau avec le rĂ©sultat (mais vous voulez quand mĂȘme plus de vitesse!). D'autres mĂ©thodes sont beaucoup plus efficaces (ajustĂ©es pour les spĂ©cificitĂ©s de SGBD spĂ©cifiques et leurs versions dĂ©jĂ  spĂ©cifiĂ©es, la taille de la table, les index).

5. Demande récursive


L'une des approches les plus spĂ©cifiques est une requĂȘte rĂ©cursive dans une expression de table commune. Pour ce faire, nous avons besoin d'une «ancre» - une requĂȘte qui renvoie la toute premiĂšre ligne:

 select dt, val, val as total from test_simple where dt = (select min(dt) from test_simple) 

Ensuite, avec l'aide de union all , les rĂ©sultats d'une requĂȘte rĂ©cursive sont ajoutĂ©s Ă  "l'ancre". Pour ce faire, vous pouvez compter sur le champ date dt , en y ajoutant un jour:

 select r.dt, r.val, cte.total + r.val from cte inner join test_simple r on r.dt = dateadd(day, 1, cte.dt) -- + 1   SQL Server 

La partie du code qui ajoute un jour n'est pas universelle. Par exemple, il s'agit de r.dt = dateadd(day, 1, cte.dt) pour SQL Server, r.dt = cte.dt + 1 pour Oracle, etc.

En combinant «l'ancre» et la demande principale, nous obtenons le résultat final:

 with cte (dt, val, total) as (select dt, val, val as total from test_simple where dt = (select min(dt) from test_simple) union all select r.dt, r.val, cte.total + r.val from cte inner join test_simple r on r.dt = dateadd(day, 1, cte.dt) -- r.dt = cte.dt + 1  Oracle,  .. ) select dt, val, total from cte order by dt; 

La solution pour le cas avec des groupes ne sera pas beaucoup plus compliquée:

 with cte (dt, grp, val, total) as (select g.dt, g.grp, g.val, g.val as total from test_groups g where g.dt = (select min(dt) from test_groups where grp = g.grp) union all select r.dt, r.grp, r.val, cte.total + r.val from cte inner join test_groups r on r.dt = dateadd(day, 1, cte.dt) -- r.dt = cte.dt + 1  Oracle,  .. and cte.grp = r.grp ) select dt, grp, val, total from cte order by grp, dt; 

6. row_number() récursive avec la fonction row_number()


La dĂ©cision prĂ©cĂ©dente Ă©tait basĂ©e sur la continuitĂ© du champ de date dt avec une augmentation sĂ©quentielle de 1 jour. Nous Ă©vitons cela en utilisant la fonction de fenĂȘtre row_number() , qui numĂ©rote les lignes. Bien sĂ»r, cela est injuste - car nous allons envisager des alternatives aux fonctions de fenĂȘtre. Cependant, cette solution peut ĂȘtre une sorte de preuve de concept : en pratique, il peut y avoir un champ qui remplace les numĂ©ros de ligne (id d'enregistrement). De plus, dans SQL Server, la fonction row_number() est apparue avant la prise en charge complĂšte des fonctions de fenĂȘtre (y compris sum() ).

Donc, pour une requĂȘte rĂ©cursive avec row_number() nous avons besoin de deux STE. Dans le premier, nous ne numĂ©rotons que les lignes:

 with cte1 (dt, val, rn) as (select dt, val, row_number() over (order by dt) as rn from test_simple) 

... et si le numĂ©ro de ligne est dĂ©jĂ  dans le tableau, vous pouvez vous en passer. Dans la requĂȘte suivante, nous faisons dĂ©jĂ  cte1 Ă  cte1 :

 cte2 (dt, val, rn, total) as (select dt, val, rn, val as total from cte1 where rn = 1 union all select cte1.dt, cte1.val, cte1.rn, cte2.total + cte1.val from cte2 inner join cte1 on cte1.rn = cte2.rn + 1 ) 

Et toute la demande ressemble Ă  ceci:

 with cte1 (dt, val, rn) as (select dt, val, row_number() over (order by dt) as rn from test_simple), cte2 (dt, val, rn, total) as (select dt, val, rn, val as total from cte1 where rn = 1 union all select cte1.dt, cte1.val, cte1.rn, cte2.total + cte1.val from cte2 inner join cte1 on cte1.rn = cte2.rn + 1 ) select dt, val, total from cte2 order by dt; 

... ou pour les groupes:

 with cte1 (dt, grp, val, rn) as (select dt, grp, val, row_number() over (partition by grp order by dt) as rn from test_groups), cte2 (dt, grp, val, rn, total) as (select dt, grp, val, rn, val as total from cte1 where rn = 1 union all select cte1.dt, cte1.grp, cte1.val, cte1.rn, cte2.total + cte1.val from cte2 inner join cte1 on cte1.grp = cte2.grp and cte1.rn = cte2.rn + 1 ) select dt, grp, val, total from cte2 order by grp, dt; 

7. CROSS APPLY / LATERAL


L'un des moyens les plus exotiques de calculer un total cumulĂ© consiste Ă  utiliser l'instruction CROSS APPLY (SQL Server, Oracle) ou son Ă©quivalent LATERAL (MySQL, PostgreSQL). Ces opĂ©rateurs sont apparus assez tard (par exemple, dans Oracle uniquement Ă  partir de la version 12c). Et dans certains SGBD (par exemple, MariaDB ), ils ne le sont pas du tout. Par consĂ©quent, cette dĂ©cision est d'un intĂ©rĂȘt purement esthĂ©tique.

Fonctionnellement, l'utilisation de CROSS APPLY ou LATERAL identique Ă  la sous-requĂȘte: nous attachons le rĂ©sultat du calcul Ă  la requĂȘte principale:

 cross apply (select coalesce(sum(t2.val), 0) as total from test_simple t2 where t2.dt <= s.dt ) t2 

... qui ressemble Ă  ceci:

 select s.*, t2.total from test_simple s cross apply (select coalesce(sum(t2.val), 0) as total from test_simple t2 where t2.dt <= s.dt ) t2 order by s.dt; 

La solution pour le cas avec des groupes sera similaire:

 select g.*, t2.total from test_groups g cross apply (select coalesce(sum(t2.val), 0) as total from test_groups t2 where g.grp = t2.grp and t2.dt <= g.dt ) t2 order by g.grp, g.dt; 

Total: nous avons examiné les principales solutions indépendantes de la plateforme. Mais il existe des solutions spécifiques à des SGBD spécifiques! Puisqu'il y a beaucoup d'options ici, nous allons nous attarder sur quelques-unes des plus intéressantes.

8. MODEL (Oracle)


L'instruction MODEL dans Oracle fournit l'une des solutions les plus élégantes. Au début de l'article, nous avons examiné la formule générale du total cumulé:

 begincasesTotal1=Value1,n=1Totaln=Totaln−1+Valuen,n geq2 endcases



MODEL vous permet de mettre en Ɠuvre cette formule littĂ©ralement un Ă  un! Pour ce faire, nous remplissons d'abord le champ total avec les valeurs de la ligne courante

 select dt, val, val as total from test_simple 

... puis nous calculons le numéro de ligne comme row_number() over (order by dt) as rn (ou utilisez le champ fini avec le numéro, le cas échéant). Et enfin, nous introduisons une rÚgle pour toutes les lignes sauf la premiÚre: total[rn >= 2] = total[cv() - 1] + val[cv()] .

La fonction cv() est ici responsable de la valeur de la ligne courante. Et toute la demande ressemblera Ă  ceci:

 select dt, val, total from (select dt, val, val as total from test_simple) t model dimension by (row_number() over (order by dt) as rn) measures (dt, val, total) rules (total[rn >= 2] = total[cv() - 1] + val[cv()]) order by dt; 

9. Curseur (SQL Server)


Un total cumulĂ© est l'un des rares cas oĂč le curseur dans SQL Server est non seulement utile, mais Ă©galement prĂ©fĂ©rable Ă  d'autres solutions (au moins jusqu'Ă  la version 2012, oĂč les fonctions de fenĂȘtre sont apparues).

L'implémentation via le curseur est assez banale. Vous devez d'abord créer un tableau temporaire et le remplir avec les dates et les valeurs du principal:

 create table #temp (dt date primary key, val int null, total int null ); insert #temp (dt, val) select dt, val from test_simple order by dt; 

Ensuite, nous définissons les variables locales à travers lesquelles la mise à jour aura lieu:

 declare @VarTotal int, @VarDT date, @VarVal int; set @VarTotal = 0; 

AprĂšs cela, nous mettons Ă  jour la table temporaire via le curseur:

 declare cur cursor local static read_only forward_only for select dt, val from #temp order by dt; open cur; fetch cur into @VarDT, @VarVal; while @@fetch_status = 0 begin set @VarTotal = @VarTotal + @VarVal; update #temp set total = @VarTotal where dt = @VarDT; fetch cur into @VarDT, @VarVal; end; close cur; deallocate cur; 

Et enfin, nous obtenons le résultat souhaité:

 select dt, val, total from #temp order by dt; drop table #temp; 

10. Mettre Ă  jour via une variable locale (SQL Server)


La mise Ă  jour via une variable locale dans SQL Server est basĂ©e sur un comportement non documentĂ©, elle ne peut donc pas ĂȘtre considĂ©rĂ©e comme fiable. NĂ©anmoins, c'est peut-ĂȘtre la solution la plus rapide, et c'est intĂ©ressant.

Créons deux variables: une pour les totaux cumulatifs et une variable de table:

 declare @VarTotal int = 0; declare @tv table (dt date null, val int null, total int null ); 

Tout d'abord, remplissez @tv données de la table principale

 insert @tv (dt, val, total) select dt, val, 0 as total from test_simple order by dt; 

Ensuite, @tv jour la variable de table @tv utilisant @VarTotal :

 update @tv set @VarTotal = total = @VarTotal + val from @tv; 

... aprÚs quoi nous obtenons le résultat final:

 select * from @tv order by dt; 

RĂ©sumĂ©: Nous avons examinĂ© les 10 meilleures façons de calculer les totaux cumulatifs en SQL. Comme vous pouvez le voir, mĂȘme sans fonctions de fenĂȘtre, ce problĂšme est complĂštement rĂ©soluble et la mĂ©canique de la solution ne peut pas ĂȘtre qualifiĂ©e de compliquĂ©e.

Source: https://habr.com/ru/post/fr474458/


All Articles