Source: Invenietis Blog

Invenietis Blog Un peu de Sql : Union d'intervalles

On dispose d'un ensemble de ressources et de leurs disponibilit&eacute;s mod&eacute;lis&eacute;es par une liste d'intervalles disjoints { BegDate , EndDate }. Concr&egrave;tement, cet exercice s'applique sur une unique table :create table dbo.tResourceAvailability ( ResourceID int not null, BegDate datetime not null, EndDate datetime not null, );Etant donn&eacute; un ensemble de ResourceID (typiquement obtenu via d'autres tables et contraintes), il faut construire l'union des intervalles de disponibilit&eacute;.Cela ne semble pas excessivement compliqu&eacute;. Mais ce n'est pas non plus trivial. Bref, c'est une bonne illustration d'un aspect du m&eacute;tier de d&eacute;veloppeur : r&eacute;soudre un probl&egrave;me simple, avec une solution qui, si possible, l'est aussi... &agrave; condition de la trouver.La premi&egrave;re chose &agrave; faire en abordant un probl&egrave;me de ce type est de tout faire pour bien le visualiser mentalement. Un probl&egrave;me bien « vu » est souvent un probl&egrave;me r&eacute;solu. En l'occurrence, il est judicieux de cr&eacute;er un jeu d'essai sur la base de la table ci-dessus. Un tel jeu d'essai doit être &agrave; la fois complet et simple... et parfois on peine. Ici ce n'est pas le cas : on peut se contenter de 5 ressources et de quelques cr&eacute;neaux r&eacute;partis sur une plage d'une vingtaine « d'instants ».La table ci-dessous pr&eacute;sente le terrain de jeux :table.dates { border-collapse: collapse; border: 1px dashed #BBBBBB; } table.dates td { width: 12pt; padding: 0 5.4pt; text-align: center; border: 1px dotted gray; } table.dates td.range { padding: 0 5.4pt; text-align: center; background-color:#CCCCCC } 0123456789101112131415161718P0 B E B E P1 B E BE P2 B E B E BE P3 BE P4 B E BE Et ce que nous cherchons &agrave; obtenir est la ligne « d'union » ci-dessous :U B E B E B E Tiens ! Tout se passe comme si l'on ne retenait que certains B ou certains E.Tilt !... « Retenir » = « Filtrer »... = condition d'une clause where... cela semble compatible avec du Sql...Il suffit de se poser la question : « Qu'est-ce qui fait que je garde tel B ou tel E ? ».Prenez une minute pour regarder les deux tableaux ci-dessus...R&eacute;ponse : Les seuls B (ou E) &agrave; conserver sont ceux qui ne sont pas compris dans un autre intervalle.On « voit » le probl&egrave;me &agrave; r&eacute;soudre l&agrave;... et on en entrevoit même la solution, il est temps de passer &agrave; l'action :insert into dbo.tResourceAvailability( ResourceID, BegDate, EndDate ) values( 0, '2001', '2003' ), ( 0, '2008', '2011' ), ( 1, '2002', '2005' ), ( 1, '2009', '2010' ), ( 2, '2001', '2004' ), ( 2, '2010', '2012' ), ( 2, '2016', '2017' ), ( 3, '2002', '2003' ), ( 4, '2007', '2010' ), ( 4, '2015', '2016' );J'utilise une syntaxe d'insertion de lignes multiples standard Sql (mais disponible qu'&agrave; partir de Sql Server 2008), je ne mets que des ann&eacute;es pour simplifier... et je proc&egrave;de tout doucement, &eacute;tape par &eacute;tape, en commençant par un select simple :declare @T datetime = '2008'; select count(*) from dbo.tResourceAvailability a where BegDate <= @T and @T <= EndDate;Cela calcule le nombre d'intervalles rencontr&eacute;s &agrave; cette date. L'id&eacute;e est la suivante (regarder le tableau) : pour un B que l'on doit conserver (par exemple 2007), le calcul donnera un (il se rencontrera lui-même), et pour un B qui ne doit pas être conserv&eacute;, le calcul donnera 2 ou plus (outre lui-même, d'autres intervalles le contiennent). On progresse non ?Oui mais... Le calcul donne 2 aussi pour 2001... car deux intervalles d&eacute;butent pile sur cette date. Il est donc n&eacute;cessaire d'affiner un peu. Id&eacute;e : on change l&eacute;g&egrave;rement la condition de façon &agrave;, lorsque l'on cherche un B, ignorer les B strictement identiques &agrave; celui que l'on cherche. On enl&egrave;ve donc au moins un au r&eacute;sultat du calcul pr&eacute;c&eacute;dent, et les B que l'on garde seront ceux pour lequel on aura 0 intersection (et non plus une seule). On proc&egrave;de sym&eacute;triquement pour E, on a donc deux requêtes l&eacute;g&egrave;rement diff&eacute;rentes pour les B et les E :declare @B datetime = '2005'; select count(*) from dbo.tResourceAvailability a where BegDate < @B and @B <= EndDate declare @E datetime = '2012'; select count(*) from dbo.tResourceAvailability a where BegDate <= @E and @E < EndDateJ'ai utilis&eacute; l'op&eacute;rateur count pour mieux « voir » mes requêtes, ce que je cherche &agrave; filtrer est tous les B et E qui ont un compte de 0, c'est-&agrave;-dire ceux qui n'existent pas.select year(BegDate) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability f where BegDate < a.BegDate and a.BegDate <= EndDate );R&eacute;sultat : 2001 2001 ==> Doublon : un distinct le fera disparaitre. 2007 2015 select year(EndDate) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate <= a.EndDate and a.EndDate < EndDate );R&eacute;sultat : 2005 2012 2017Ce qui est presque parfait puisque nous cherchons &agrave; obtenir la liste d'intervalles suivante : 2001 - 2005, 2007 - 2012, 2015 - 2017Nous avons le r&eacute;sultat final, mais « en deux morceaux ». Comment les r&eacute;unir ? Avec une « union ». C'est une op&eacute;ration ensembliste on ne peut plus simple :select year(BegDate) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate < a.BegDate and a.BegDate <= EndDate ) union select year(EndDate) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate <= a.EndDate and a.EndDate < EndDate );R&eacute;sultat : 2001 2005 2007 2012 2015 2017On obtient tous les B, puis tous les E. Le doublon a disparu (2001) car c'est le comportement par d&eacute;faut de l'union (pour ne pas filtrer les doublons, il faut explicitement utiliser union all). Il ne reste plus qu'&agrave; ordonner ces &eacute;l&eacute;ments en remarquant que l'on a n&eacute;cessairement une s&eacute;quence B, E, B, E, etc. Pour la rendre plus lisible, on le pr&eacute;cise :select 'B', year(BegDate) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate < a.BegDate and a.BegDate <= EndDate ) union select 'E', year(EndDate) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate <= a.EndDate and a.EndDate < EndDate ) order by 2;R&eacute;sultat : B 2001 E 2005 B 2007 E 2012 B 2015 E 2017On approche mais ce n'est pas encore id&eacute;al car on souhaite une forme de r&eacute;ponse similaire &agrave; celle de la donn&eacute;e source : une liste d'intervalle, donc deux colonnes B et E. Transformer des lignes en colonnes, c'est le rôle de l'op&eacute;rateur pivot, &agrave; condition de s'appuyer une fonction d'agr&eacute;gation (count, sum, max, avg, etc.) : il ne nous est d'aucune utilit&eacute; ici (en r&egrave;gle g&eacute;n&eacute;ral, n'allez chercher l'op&eacute;rateur pivot que s'il y a une agr&eacute;gation possible ou souhaitable). Il nous faut trouver autre chose pour « m&eacute;langer » ces deux requêtes :| 2001 | | 2012 | | 2001, 2005 || 2005 | x | 2015 | ==> | 2007, 2012 || 2007 | | 2017 | | 2015, 2017 |Probl&egrave;me simple non ? Cherchez un peu..................Le principe est de se servir de B comme « g&eacute;n&eacute;rateur », et de chercher la fin de l'intervalle correspondant :select B = a.BegDate, E = << Take the first EndDate greater than a.BegDate >> from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate < a.BegDate and a.BegDate <= EndDate )Ce qui revient &agrave; prendre la plus petite date sup&eacute;rieure &agrave; la date de d&eacute;but, et donne la requête finale suivante :select distinct B = BegDate, E = (select min(EndDate) from dbo.tResourceAvailability ae where ae.EndDate > a.BegDate and not exists( select * from dbo.tResourceAvailability where BegDate <= ae.EndDate and ae.EndDate < EndDate )) from dbo.tResourceAvailability a where not exists( select * from dbo.tResourceAvailability where BegDate < a.BegDate and a.BegDate <= EndDate )Une remarque sur les tris : Les dates obtenues peuvent être dans l'ordre croissant. C'est un hasard et il ne faut en aucun cas s'y fier si vous voulez trier, triez explicitement (avec un order by).

Read full article »
Est. Annual Revenue
$100K-5.0M
Est. Employees
25-100
CEO Avatar

CEO

Update CEO

CEO Approval Rating

- -/100



Invenietis's headquarters is in paris, Île-de-France. Invenietis has a revenue of $1.2M, and 29 employees. Invenietis has 1 followers on Owler.