J'ai besoin de convertir des données entre deux systèmes.
Le premier système stocke les plannings sous forme de liste simple de dates. Chaque date incluse dans le programme est une ligne. Il peut y avoir diverses lacunes dans la séquence des dates (week-ends, jours fériés et pauses plus longues, certains jours de la semaine peuvent être exclus de l'horaire). Il ne peut y avoir aucune interruption, même les week-ends peuvent être inclus. Le calendrier peut durer jusqu'à 2 ans. Habituellement, cela dure quelques semaines.
Voici un exemple simple d'un horaire qui s'étale sur deux semaines hors week-end (il y a des exemples plus compliqués dans le script ci-dessous):
+----+------------+------------+---------+--------+
| ID | ContractID | dt | dowChar | dowInt |
+----+------------+------------+---------+--------+
| 10 | 1 | 2016-05-02 | Mon | 2 |
| 11 | 1 | 2016-05-03 | Tue | 3 |
| 12 | 1 | 2016-05-04 | Wed | 4 |
| 13 | 1 | 2016-05-05 | Thu | 5 |
| 14 | 1 | 2016-05-06 | Fri | 6 |
| 15 | 1 | 2016-05-09 | Mon | 2 |
| 16 | 1 | 2016-05-10 | Tue | 3 |
| 17 | 1 | 2016-05-11 | Wed | 4 |
| 18 | 1 | 2016-05-12 | Thu | 5 |
| 19 | 1 | 2016-05-13 | Fri | 6 |
+----+------------+------------+---------+--------+
ID
est unique, mais il n'est pas nécessairement séquentiel (c'est la clé primaire). Les dates sont uniques dans chaque contrat (il existe un index unique (ContractID, dt)
).
Le deuxième système stocke les planifications sous forme d'intervalles avec la liste des jours de la semaine qui font partie de la planification. Chaque intervalle est défini par ses dates de début et de fin (inclus) et une liste de jours de semaine inclus dans le planning. Dans ce format, vous pouvez définir efficacement des modèles hebdomadaires répétitifs, tels que du lundi au mercredi, mais cela devient pénible lorsqu'un modèle est perturbé, par exemple par un jour férié.
Voici à quoi ressemblera l'exemple simple ci-dessus:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
les intervalles qui appartiennent au même contrat ne doivent pas se chevaucher.
J'ai besoin de convertir les données du premier système dans le format utilisé par le deuxième système. Pour le moment, je résous ce problème côté client en C # pour le contrat donné, mais j'aimerais le faire en T-SQL côté serveur pour le traitement en bloc et l'exportation / importation entre les serveurs. Très probablement, cela pourrait être fait en utilisant CLR UDF, mais à ce stade, je ne peux pas utiliser SQLCLR.
Le défi ici est de rendre la liste des intervalles aussi courte et conviviale que possible.
Par exemple, ce calendrier:
+-----+------------+------------+---------+--------+
| ID | ContractID | dt | dowChar | dowInt |
+-----+------------+------------+---------+--------+
| 223 | 2 | 2016-05-05 | Thu | 5 |
| 224 | 2 | 2016-05-06 | Fri | 6 |
| 225 | 2 | 2016-05-09 | Mon | 2 |
| 226 | 2 | 2016-05-10 | Tue | 3 |
| 227 | 2 | 2016-05-11 | Wed | 4 |
| 228 | 2 | 2016-05-12 | Thu | 5 |
| 229 | 2 | 2016-05-13 | Fri | 6 |
| 230 | 2 | 2016-05-16 | Mon | 2 |
| 231 | 2 | 2016-05-17 | Tue | 3 |
+-----+------------+------------+---------+--------+
devrait devenir ceci:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,pas ça:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-06 | 2 | Thu,Fri, |
| 2 | 2016-05-09 | 2016-05-13 | 5 | Mon,Tue,Wed,Thu,Fri, |
| 2 | 2016-05-16 | 2016-05-17 | 2 | Mon,Tue, |
+------------+------------+------------+----------+----------------------+
J'ai essayé d'appliquer une gaps-and-islands
approche à ce problème. J'ai essayé de le faire en deux passes. Dans la première passe, je trouve des îles de simples jours consécutifs, c'est-à-dire que la fin de l'île est un écart dans la séquence de jours, que ce soit le week-end, les jours fériés ou autre chose. Pour chaque île trouvée, je construis une liste séparée par des virgules WeekDays
. Dans la deuxième passe, j'ai trouvé des îles plus loin en examinant l'écart dans la séquence des numéros de semaine ou un changement dans le WeekDays
.
Avec cette approche, chaque semaine partielle se termine comme un intervalle supplémentaire comme indiqué ci-dessus, car même si les numéros de semaine sont consécutifs, le WeekDays
changement. En outre, il peut y avoir des écarts réguliers en une semaine (voir ContractID=3
dans les exemples de données, qui ne contiennent des données que pour Mon,Wed,Fri,
) et cette approche générerait des intervalles séparés pour chaque jour dans un tel calendrier. Du bon côté, il génère un intervalle si le calendrier ne comporte aucun écart (voir ContractID=7
dans les exemples de données qui incluent les week-ends) et dans ce cas, peu importe si la semaine de début ou de fin est partielle.
Veuillez voir d'autres exemples dans le script ci-dessous pour avoir une meilleure idée de ce que je recherche. Vous pouvez voir que très souvent les week-ends sont exclus, mais tous les autres jours de la semaine peuvent également être exclus. Dans l'exemple 3 uniquement Mon
, Wed
et Fri
font partie de la planification. En outre, les week-ends peuvent être inclus, comme dans l'exemple 7. La solution doit traiter tous les jours de la semaine de manière égale. N'importe quel jour de la semaine peut être inclus ou exclu de l'horaire.
Pour vérifier que la liste d'intervalles générée décrit correctement la planification donnée, vous pouvez utiliser le pseudo-code suivant:
- parcourir tous les intervalles
- pour chaque intervalle, parcourez toutes les dates du calendrier entre les dates de début et de fin (inclus).
- pour chaque date, vérifiez si son jour de la semaine est répertorié dans le
WeekDays
. Si oui, cette date est incluse dans le calendrier.
Espérons que cela clarifie dans quels cas un nouvel intervalle doit être créé. Dans les exemples 4 et 5, un lundi ( 2016-05-09
) est supprimé du milieu de la planification et cette planification ne peut pas être représentée par un seul intervalle. Dans l'exemple 6, il y a un long intervalle dans le programme, donc deux intervalles sont nécessaires.
Les intervalles représentent des modèles hebdomadaires dans le programme et lorsqu'un modèle est interrompu / modifié, le nouvel intervalle doit être ajouté. Dans l'exemple 11, les trois premières semaines ont un modèle Tue
, puis ce modèle devient Thu
. Par conséquent, nous avons besoin de deux intervalles pour décrire un tel calendrier.
J'utilise SQL Server 2008 pour le moment, donc la solution devrait fonctionner dans cette version. Si une solution pour SQL Server 2008 peut être simplifiée / améliorée à l'aide des fonctionnalités des versions ultérieures, c'est un bonus, veuillez également la montrer.
J'ai un Calendar
tableau (liste des dates) et un Numbers
tableau (liste des nombres entiers à partir de 1), il est donc correct de les utiliser, si nécessaire. Il est également correct de créer des tables temporaires et d'avoir plusieurs requêtes qui traitent les données en plusieurs étapes. Le nombre d'étapes dans un algorithme doit être fixé cependant, les curseurs et les WHILE
boucles explicites ne sont pas OK.
Script pour les exemples de données et les résultats attendus
-- @Src is sample data
-- @Dst is expected result
DECLARE @Src TABLE (ID int PRIMARY KEY, ContractID int, dt date, dowChar char(3), dowInt int);
INSERT INTO @Src (ID, ContractID, dt, dowChar, dowInt) VALUES
-- simple two weeks (without weekend)
(110, 1, '2016-05-02', 'Mon', 2),
(111, 1, '2016-05-03', 'Tue', 3),
(112, 1, '2016-05-04', 'Wed', 4),
(113, 1, '2016-05-05', 'Thu', 5),
(114, 1, '2016-05-06', 'Fri', 6),
(115, 1, '2016-05-09', 'Mon', 2),
(116, 1, '2016-05-10', 'Tue', 3),
(117, 1, '2016-05-11', 'Wed', 4),
(118, 1, '2016-05-12', 'Thu', 5),
(119, 1, '2016-05-13', 'Fri', 6),
-- a partial end of the week, the whole week, partial start of the week (without weekends)
(223, 2, '2016-05-05', 'Thu', 5),
(224, 2, '2016-05-06', 'Fri', 6),
(225, 2, '2016-05-09', 'Mon', 2),
(226, 2, '2016-05-10', 'Tue', 3),
(227, 2, '2016-05-11', 'Wed', 4),
(228, 2, '2016-05-12', 'Thu', 5),
(229, 2, '2016-05-13', 'Fri', 6),
(230, 2, '2016-05-16', 'Mon', 2),
(231, 2, '2016-05-17', 'Tue', 3),
-- only Mon, Wed, Fri are included across two weeks plus partial third week
(310, 3, '2016-05-02', 'Mon', 2),
(311, 3, '2016-05-04', 'Wed', 4),
(314, 3, '2016-05-06', 'Fri', 6),
(315, 3, '2016-05-09', 'Mon', 2),
(317, 3, '2016-05-11', 'Wed', 4),
(319, 3, '2016-05-13', 'Fri', 6),
(330, 3, '2016-05-16', 'Mon', 2),
-- a whole week (without weekend), in the second week Mon is not included
(410, 4, '2016-05-02', 'Mon', 2),
(411, 4, '2016-05-03', 'Tue', 3),
(412, 4, '2016-05-04', 'Wed', 4),
(413, 4, '2016-05-05', 'Thu', 5),
(414, 4, '2016-05-06', 'Fri', 6),
(416, 4, '2016-05-10', 'Tue', 3),
(417, 4, '2016-05-11', 'Wed', 4),
(418, 4, '2016-05-12', 'Thu', 5),
(419, 4, '2016-05-13', 'Fri', 6),
-- three weeks, but without Mon in the second week (no weekends)
(510, 5, '2016-05-02', 'Mon', 2),
(511, 5, '2016-05-03', 'Tue', 3),
(512, 5, '2016-05-04', 'Wed', 4),
(513, 5, '2016-05-05', 'Thu', 5),
(514, 5, '2016-05-06', 'Fri', 6),
(516, 5, '2016-05-10', 'Tue', 3),
(517, 5, '2016-05-11', 'Wed', 4),
(518, 5, '2016-05-12', 'Thu', 5),
(519, 5, '2016-05-13', 'Fri', 6),
(520, 5, '2016-05-16', 'Mon', 2),
(521, 5, '2016-05-17', 'Tue', 3),
(522, 5, '2016-05-18', 'Wed', 4),
(523, 5, '2016-05-19', 'Thu', 5),
(524, 5, '2016-05-20', 'Fri', 6),
-- long gap between two intervals
(623, 6, '2016-05-05', 'Thu', 5),
(624, 6, '2016-05-06', 'Fri', 6),
(625, 6, '2016-05-09', 'Mon', 2),
(626, 6, '2016-05-10', 'Tue', 3),
(627, 6, '2016-05-11', 'Wed', 4),
(628, 6, '2016-05-12', 'Thu', 5),
(629, 6, '2016-05-13', 'Fri', 6),
(630, 6, '2016-05-16', 'Mon', 2),
(631, 6, '2016-05-17', 'Tue', 3),
(645, 6, '2016-06-06', 'Mon', 2),
(646, 6, '2016-06-07', 'Tue', 3),
(647, 6, '2016-06-08', 'Wed', 4),
(648, 6, '2016-06-09', 'Thu', 5),
(649, 6, '2016-06-10', 'Fri', 6),
(655, 6, '2016-06-13', 'Mon', 2),
(656, 6, '2016-06-14', 'Tue', 3),
(657, 6, '2016-06-15', 'Wed', 4),
(658, 6, '2016-06-16', 'Thu', 5),
(659, 6, '2016-06-17', 'Fri', 6),
-- two weeks, no gaps between days at all, even weekends are included
(710, 7, '2016-05-02', 'Mon', 2),
(711, 7, '2016-05-03', 'Tue', 3),
(712, 7, '2016-05-04', 'Wed', 4),
(713, 7, '2016-05-05', 'Thu', 5),
(714, 7, '2016-05-06', 'Fri', 6),
(715, 7, '2016-05-07', 'Sat', 7),
(716, 7, '2016-05-08', 'Sun', 1),
(725, 7, '2016-05-09', 'Mon', 2),
(726, 7, '2016-05-10', 'Tue', 3),
(727, 7, '2016-05-11', 'Wed', 4),
(728, 7, '2016-05-12', 'Thu', 5),
(729, 7, '2016-05-13', 'Fri', 6),
-- no gaps between days at all, even weekends are included, with partial weeks
(805, 8, '2016-04-30', 'Sat', 7),
(806, 8, '2016-05-01', 'Sun', 1),
(810, 8, '2016-05-02', 'Mon', 2),
(811, 8, '2016-05-03', 'Tue', 3),
(812, 8, '2016-05-04', 'Wed', 4),
(813, 8, '2016-05-05', 'Thu', 5),
(814, 8, '2016-05-06', 'Fri', 6),
(815, 8, '2016-05-07', 'Sat', 7),
(816, 8, '2016-05-08', 'Sun', 1),
(825, 8, '2016-05-09', 'Mon', 2),
(826, 8, '2016-05-10', 'Tue', 3),
(827, 8, '2016-05-11', 'Wed', 4),
(828, 8, '2016-05-12', 'Thu', 5),
(829, 8, '2016-05-13', 'Fri', 6),
(830, 8, '2016-05-14', 'Sat', 7),
-- only Mon-Wed included, two weeks plus partial third week
(910, 9, '2016-05-02', 'Mon', 2),
(911, 9, '2016-05-03', 'Tue', 3),
(912, 9, '2016-05-04', 'Wed', 4),
(915, 9, '2016-05-09', 'Mon', 2),
(916, 9, '2016-05-10', 'Tue', 3),
(917, 9, '2016-05-11', 'Wed', 4),
(930, 9, '2016-05-16', 'Mon', 2),
(931, 9, '2016-05-17', 'Tue', 3),
-- only Thu-Sun included, three weeks
(1013,10,'2016-05-05', 'Thu', 5),
(1014,10,'2016-05-06', 'Fri', 6),
(1015,10,'2016-05-07', 'Sat', 7),
(1016,10,'2016-05-08', 'Sun', 1),
(1018,10,'2016-05-12', 'Thu', 5),
(1019,10,'2016-05-13', 'Fri', 6),
(1020,10,'2016-05-14', 'Sat', 7),
(1021,10,'2016-05-15', 'Sun', 1),
(1023,10,'2016-05-19', 'Thu', 5),
(1024,10,'2016-05-20', 'Fri', 6),
(1025,10,'2016-05-21', 'Sat', 7),
(1026,10,'2016-05-22', 'Sun', 1),
-- only Tue for first three weeks, then only Thu for the next three weeks
(1111,11,'2016-05-03', 'Tue', 3),
(1116,11,'2016-05-10', 'Tue', 3),
(1131,11,'2016-05-17', 'Tue', 3),
(1123,11,'2016-05-19', 'Thu', 5),
(1124,11,'2016-05-26', 'Thu', 5),
(1125,11,'2016-06-02', 'Thu', 5),
-- one week, then one week gap, then one week
(1210,12,'2016-05-02', 'Mon', 2),
(1211,12,'2016-05-03', 'Tue', 3),
(1212,12,'2016-05-04', 'Wed', 4),
(1213,12,'2016-05-05', 'Thu', 5),
(1214,12,'2016-05-06', 'Fri', 6),
(1215,12,'2016-05-16', 'Mon', 2),
(1216,12,'2016-05-17', 'Tue', 3),
(1217,12,'2016-05-18', 'Wed', 4),
(1218,12,'2016-05-19', 'Thu', 5),
(1219,12,'2016-05-20', 'Fri', 6);
SELECT ID, ContractID, dt, dowChar, dowInt
FROM @Src
ORDER BY ContractID, dt;
DECLARE @Dst TABLE (ContractID int, StartDT date, EndDT date, DayCount int, WeekDays varchar(255));
INSERT INTO @Dst (ContractID, StartDT, EndDT, DayCount, WeekDays) VALUES
(1, '2016-05-02', '2016-05-13', 10, 'Mon,Tue,Wed,Thu,Fri,'),
(2, '2016-05-05', '2016-05-17', 9, 'Mon,Tue,Wed,Thu,Fri,'),
(3, '2016-05-02', '2016-05-16', 7, 'Mon,Wed,Fri,'),
(4, '2016-05-02', '2016-05-06', 5, 'Mon,Tue,Wed,Thu,Fri,'),
(4, '2016-05-10', '2016-05-13', 4, 'Tue,Wed,Thu,Fri,'),
(5, '2016-05-02', '2016-05-06', 5, 'Mon,Tue,Wed,Thu,Fri,'),
(5, '2016-05-10', '2016-05-20', 9, 'Mon,Tue,Wed,Thu,Fri,'),
(6, '2016-05-05', '2016-05-17', 9, 'Mon,Tue,Wed,Thu,Fri,'),
(6, '2016-06-06', '2016-06-17', 10, 'Mon,Tue,Wed,Thu,Fri,'),
(7, '2016-05-02', '2016-05-13', 12, 'Sun,Mon,Tue,Wed,Thu,Fri,Sat,'),
(8, '2016-04-30', '2016-05-14', 15, 'Sun,Mon,Tue,Wed,Thu,Fri,Sat,'),
(9, '2016-05-02', '2016-05-17', 8, 'Mon,Tue,Wed,'),
(10,'2016-05-05', '2016-05-22', 12, 'Sun,Thu,Fri,Sat,'),
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'),
(11,'2016-05-19', '2016-06-02', 3, 'Thu,'),
(12,'2016-05-02', '2016-05-06', 5, 'Mon,Tue,Wed,Thu,Fri,'),
(12,'2016-05-16', '2016-05-20', 5, 'Mon,Tue,Wed,Thu,Fri,');
SELECT ContractID, StartDT, EndDT, DayCount, WeekDays
FROM @Dst
ORDER BY ContractID, StartDT;
Comparaison des réponses
Le vrai tableau @Src
a des 403,555
lignes 15,857
distinctes ContractIDs
. Toutes les réponses produisent des résultats corrects (au moins pour mes données) et toutes sont raisonnablement rapides, mais elles diffèrent par leur optimalité. Moins il y a d'intervalles, mieux c'est. J'ai inclus les temps d'exécution juste pour la curiosité. L'objectif principal est le résultat correct et optimal, pas la vitesse (sauf si cela prend trop de temps - j'ai arrêté la requête non récursive de Ziggy Crueltyfree Zeitgeister après 10 minutes).
+--------------------------------------------------------+-----------+---------+
| Answer | Intervals | Seconds |
+--------------------------------------------------------+-----------+---------+
| Ziggy Crueltyfree Zeitgeister | 25751 | 7.88 |
| While loop | | |
| | | |
| Ziggy Crueltyfree Zeitgeister | 25751 | 8.27 |
| Recursive | | |
| | | |
| Michael Green | 25751 | 22.63 |
| Recursive | | |
| | | |
| Geoff Patterson | 26670 | 4.79 |
| Weekly gaps-and-islands with merging of partial weeks | | |
| | | |
| Vladimir Baranov | 34560 | 4.03 |
| Daily, then weekly gaps-and-islands | | |
| | | |
| Mikael Eriksson | 35840 | 0.65 |
| Weekly gaps-and-islands | | |
+--------------------------------------------------------+-----------+---------+
| Vladimir Baranov | 25751 | 121.51 |
| Cursor | | |
+--------------------------------------------------------+-----------+---------+
la source
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
y avoir une ligne dans le @DstTue, Thu,
?@Dst
). Il n'y a que les deux premières semaines de l'horaireTue
, vous ne pouvez donc pas en avoirWeekDays=Tue,Thu,
pendant ces semaines. Les deux dernières semaines de l'horaire ont seulementThu
, donc vous ne pouvez pas encore avoirWeekDays=Tue,Thu,
pour ces semaines. La solution sous-optimale serait de trois rangées: justeTue
pour les deux premières semaines, puisTue,Thu,
pour la troisième semaine qui a les deuxTue
etThu
, puis justeThu
pour les deux dernières semaines.ContractID
changement, si l'intervalle dépasse 7 jours et le nouveau jour de la semaine n'a pas été vu auparavant, s'il y a un écart dans la liste des jours programmés.Réponses:
Celui-ci utilise un CTE récursif. Son résultat est identique à l'exemple de la question . C'était un cauchemar à inventer ... Le code inclut des commentaires pour faciliter sa logique alambiquée.
Une autre stratégie
Celui-ci devrait être considérablement plus rapide que le précédent car il ne repose pas sur le CTE récursif lent et limité dans SQL Server 2008, bien qu'il implémente plus ou moins la même stratégie.
Il y a une
WHILE
boucle (je n'ai pas pu imaginer un moyen de l'éviter), mais va pour un nombre réduit d'itérations (le plus grand nombre de séquences (moins une) sur un contrat donné).C'est une stratégie simple, et pourrait être utilisée pour des séquences plus courtes ou plus longues qu'une semaine (remplaçant toute occurrence de la constante 7 pour tout autre nombre, et le
dowBit
calculé à partir de MODULUS xDayNo
plutôt queDATEPART(wk)
) et jusqu'à 32.la source
Pas exactement ce que vous recherchez, mais pourrait peut-être vous intéresser.
La requête crée des semaines avec une chaîne séparée par des virgules pour les jours utilisés dans chaque semaine. Il trouve ensuite les îles de semaines consécutives qui utilisent le même schéma dans
Weekdays
.Résultat:
ContractID = 2
montre quelle est la différence dans le résultat par rapport à ce que vous voulez. La première et la dernière semaine seront traitées comme des périodes distinctes car ellesWeekDays
sont différentes.la source
WeekDays
comme un nombre à 7 bits. Seulement 128 combinaisons. Il n'y a que 128 * 128 = 16384 paires possibles. Construisez une table temporaire avec toutes les paires possibles, puis déterminez un algorithme basé sur un ensemble qui marquerait quelles paires peuvent être fusionnées: un modèle d'une semaine est "couvert" par un modèle de la semaine suivante. Rejoignez vous-même le résultat hebdomadaire actuel (car il n'yLAG
en a pas en 2008) et utilisez cette table temporaire pour décider quelles paires fusionner ... Je ne sais pas si cette idée a du mérite.Je me suis retrouvé avec une approche qui donne la solution optimale dans ce cas et je pense qu'elle fera bien en général. La solution est cependant assez longue, il serait donc intéressant de voir si quelqu'un d'autre a une approche différente qui est plus concise.
Voici un script qui contient la solution complète .
Et voici un aperçu de l'algorithme:
ContractId
ContractId
et ont la mêmeWeekDays
WeekDays
semaine unique correspond à un sous-ensemble principalWeekDays
du groupe précédent, fusionnez dans ce groupe précédentWeekDays
semaine unique correspond à un sous-ensemble de finWeekDays
du groupe suivant, fusionnez dans ce groupe suivantla source
(1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),
. Il peut être représenté comme un intervalle, mais votre solution en produit deux. J'admets que cet exemple n'était pas dans les exemples de données et qu'il n'est pas critique. J'essaierai d'exécuter votre solution sur des données réelles.Je ne pouvais pas comprendre la logique du regroupement des semaines avec des écarts ou des semaines avec des week-ends (par exemple, quand il y a deux semaines consécutives avec un week-end, à quelle semaine le week-end va-t-il?).
La requête suivante produit la sortie souhaitée, sauf qu'elle ne regroupe que les jours de semaine consécutifs et regroupe les semaines du dimanche au samedi (plutôt que du lundi au dimanche). Bien que ce ne soit pas exactement ce que vous voulez, cela peut peut-être fournir des indices pour une stratégie différente. Le regroupement des jours vient d' ici . Les fonctions de fenêtrage utilisées devraient fonctionner avec SQLServer 2008, mais je n'ai pas cette version pour tester si c'est le cas.
Résultat
la source
Par souci d'exhaustivité, voici une
gaps-and-islands
approche en deux passes que j'ai moi-même essayée avant de poser cette question.Comme je le testais sur les données réelles, j'ai trouvé peu de cas où il produisait des résultats incorrects et je l'ai corrigé.
Voici l'algorithme:
CTE_ContractDays
,CTE_DailyRN
,CTE_DailyIslands
) et calculer un numéro de la semaine pour chaque date de début et de fin d'une île. Ici, le numéro de semaine est calculé en supposant que le lundi est le premier jour de la semaine.CTE_Weeks
).CTE_FirstResult
).WeekDays
(CTE_SecondRN
,CTE_Schedules
).Il gère bien les cas où il n'y a pas de perturbation dans les schémas hebdomadaires (1, 7, 8, 10, 12). Il gère bien les cas où le modèle a des jours non séquentiels (3).
Mais, malheureusement, il génère des intervalles supplémentaires pour des semaines partielles (2, 3, 5, 6, 9, 11).
Résultat
Solution basée sur le curseur
J'ai converti mon code C # en un algorithme basé sur le curseur, juste pour voir comment il se compare à d'autres solutions sur des données réelles. Elle confirme qu'elle est beaucoup plus lente que les autres approches basées sur des ensembles ou récursives, mais elle génère un résultat optimal.
la source
J'ai été un peu surpris que la solution du curseur de Vladimir soit si lente, j'ai donc également essayé d'optimiser cette version. J'ai confirmé que l'utilisation d'un curseur était également très lente pour moi.
Cependant, au prix d'utiliser des fonctionnalités non documentées dans SQL Server en les ajoutant à une variable lors du traitement d'un ensemble de lignes, j'ai pu créer une version simplifiée de cette logique qui donne le résultat optimal et s'exécute beaucoup plus rapidement que le curseur et ma solution d'origine. . Donc, utilisez-le à vos risques et périls, mais je présenterai la solution au cas où cela l'intéresserait. Il serait également possible de mettre à jour la solution pour utiliser une
WHILE
boucle de un au numéro de ligne maximum, en cherchant le numéro de ligne suivant à chaque itération de la boucle. Cela resterait fidèle à une fonctionnalité entièrement documentée et fiable, mais violerait la contrainte déclarée (quelque peu artificielle) du problème selon lequel lesWHILE
boucles ne sont pas autorisées.Notez que si l'utilisation de SQL 2014 était autorisée, il est probable qu'une procédure stockée compilée en mode natif qui boucle sur les numéros de ligne et accède à chaque numéro de ligne dans une table optimisée en mémoire serait une implémentation de cette même logique qui s'exécuterait plus rapidement.
Voici la solution complète , y compris l'extension des données d'essai définies à environ un demi-million de lignes. La nouvelle solution se termine en environ 3 secondes et à mon avis est beaucoup plus concise et lisible que la solution précédente que j'ai proposée. Je vais détailler les trois étapes impliquées ici:
Étape 1: prétraitement
Nous ajoutons d'abord un numéro de ligne à l'ensemble de données, dans l'ordre où nous traiterons les données. Ce faisant, nous convertissons également chaque dowInt en une puissance de 2 afin de pouvoir utiliser une image bitmap pour représenter les jours observés dans un groupe donné:
Étape 2: boucle sur les jours du contrat afin d'identifier de nouveaux regroupements
Nous passons ensuite en boucle sur les données, dans l'ordre par numéro de ligne. Nous calculons uniquement la liste des numéros de ligne qui forment la limite d'un nouveau groupe, puis nous exportons ces numéros de ligne dans un tableau:
Étape 3: Calcul des résultats finaux en fonction des numéros de ligne de chaque limite de regroupement
Nous calculons ensuite les groupes finaux en utilisant les limites identifiées dans la boucle ci-dessus pour agréger toutes les dates qui tombent dans chaque groupe:
la source
WHILE
boucles, car je savais déjà comment le résoudre avec le curseur et je voulais trouver une solution basée sur un ensemble. En outre, je soupçonnais que le curseur serait lent (surtout avec une boucle imbriquée). Cette réponse est très intéressante en termes d'apprentissage de nouvelles astuces et j'apprécie vos efforts.La discussion suivra le code.
@Helper
est de faire face à cette règle:Il me permet de lister les noms de jours, dans l'ordre des numéros de jours, entre deux jours donnés. Ceci est utilisé pour décider si un nouvel intervalle doit commencer. Je le remplis avec deux semaines de valeurs pour faciliter le codage d'un week-end.
Il existe des moyens plus propres de mettre en œuvre cela. Un tableau complet des «dates» en serait un. Il existe probablement un moyen intelligent avec le nombre de jours et l'arithmétique modulo.
Le CTE
MissingDays
doit générer une liste de noms de jours entre deux jours donnés. Il est géré de cette manière maladroite car le CTE récursif (suivant) n'autorise pas les agrégats, TOP () ou d'autres opérateurs. C'est inélégant, mais ça marche.CTE
Numbered
doit appliquer une séquence connue et sans lacune sur les données. Cela évite beaucoup de comparaisons plus tard.CTE
Incremented
est l'endroit où l'action se produit. En gros, j'utilise un CTE récursif pour parcourir les données et appliquer les règles. Le numéro de ligne généré dansNumbered
(ci-dessus) est utilisé pour piloter le traitement récursif.La graine du CTE récursif obtient simplement la première date pour chaque ContractID et initialise les valeurs qui seront utilisées pour décider si un nouvel intervalle est requis.
Décider si un nouvel intervalle doit commencer nécessite la date de début de l'intervalle actuel, la liste des jours et la longueur de tout intervalle dans les dates du calendrier. Ceux-ci peuvent être réinitialisés ou reportés, selon la décision. Par conséquent, la partie récursive est verbeuse et un peu répétitive, car nous devons décider de commencer ou non un nouvel intervalle pour plus d'une valeur de colonne.
La logique de décision pour les colonnes
WeekDays
etIntervalStart
doit avoir la même logique de décision - elle peut être copiée-collée entre elles. Si la logique de démarrage d'un nouvel intervalle devait changer, c'est le code à modifier. Idéalement, il serait donc abstrait; cela peut être difficile dans un CTE récursif.La
EXISTS()
clause est la conséquence de ne pas pouvoir utiliser les fonctions d'agrégation dans un CTE récursif. Il ne fait que voir si les jours compris dans un intervalle sont déjà dans l'intervalle actuel.Il n'y a rien de magique dans l'imbrication des clauses logiques. S'il est plus clair dans une autre conformation, ou en utilisant des CAS imbriqués, disons, il n'y a aucune raison de le garder de cette façon.
La dernière
SELECT
consiste à donner la sortie dans le format souhaité.Avoir le PK sous tension
Src.ID
n'est pas utile pour cette méthode. Un index clusterisé sur(ContractID,dt)
serait bien, je pense.Il y a quelques bords rugueux. Les jours ne sont pas renvoyés dans l'ordre du Dow, mais dans l'ordre du calendrier, ils apparaissent dans les données source. Tout ce qui concerne @Helper est maladroit et pourrait être lissé. J'aime l'idée d'utiliser un bit par jour et d'utiliser des fonctions binaires au lieu de
LIKE
. Séparer certains des CTE auxiliaires dans une table temporaire avec des index appropriés aiderait sans aucun doute.L'un des défis à cela est qu'une "semaine" ne s'aligne pas sur un calendrier standard, mais est plutôt guidée par les données et se réinitialise lorsqu'il est déterminé qu'un nouvel intervalle doit commencer. Une "semaine", ou au moins un intervalle, peut durer un jour pour s'étendre à l'ensemble des données.
Par souci d'intérêt, voici les coûts estimés par rapport aux données d'exemple de Geoff (merci pour cela!) Après divers changements:
Le nombre estimé et réel de lignes diffère énormément.
Le plan a une table spoo, probablement en raison de la CTE récursive. La plupart de l'action se trouve dans une table de travail qui se détache:
Juste la façon dont le récursif est implémenté, je suppose!
la source
MAX(g.IntervalStart)
semble étrange, carg.IntervalStart
est dans leGROUP BY
. Je m'attendais à ce qu'il donne une erreur de syntaxe, mais cela fonctionne. Faut - il être justeg.IntervalStart as StartDT
enSELECT
? Oug.IntervalStart
ne devrait pas être dans leGROUP BY
?MissingDays
etNumbered
sont remplacés par des tables temporaires avec des index appropriés, cela pourrait avoir des performances décentes. Quels index recommanderiez-vous? Je pourrais l'essayer demain matin.Numbered
par une table temporaire et un index cluster(ContractID, rn)
vaut la peine. Sans un grand ensemble de données pour générer le plan correspondant, il est difficile de deviner. LaMissingDates
matérialisation avec des index(StartDay, FollowingDayInt)
serait également une bonne chose.