Ich muss Daten zwischen zwei Systemen konvertieren.
Das erste System speichert Zeitpläne als einfache Liste von Daten. Jedes Datum, das im Zeitplan enthalten ist, ist eine Zeile. Es kann verschiedene Lücken in der Reihenfolge der Daten geben (Wochenenden, Feiertage und längere Pausen, einige Wochentage können vom Zeitplan ausgeschlossen sein). Es kann überhaupt keine Lücken geben, auch Wochenenden können eingeschlossen werden. Der Zeitplan kann bis zu 2 Jahre lang sein. Normalerweise dauert es einige Wochen.
Hier ist ein einfaches Beispiel für einen Zeitplan, der sich über zwei Wochen ohne Wochenenden erstreckt (das folgende Skript enthält kompliziertere Beispiele):
+----+------------+------------+---------+--------+
| 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
ist eindeutig, aber nicht unbedingt sequentiell (es ist der Primärschlüssel). Termine sind in jedem Vertrag eindeutig (es gibt einen eindeutigen Index für (ContractID, dt)
).
Das zweite System speichert Zeitpläne als Intervalle mit der Liste der Wochentage, die Teil des Zeitplans sind. Jedes Intervall wird durch sein Start- und Enddatum (einschließlich) und eine Liste der Wochentage definiert, die im Zeitplan enthalten sind. In diesem Format können Sie sich wiederholende wöchentliche Muster effizient definieren, z. B. Mo-Mi. Es wird jedoch zu einem Schmerz, wenn ein Muster beispielsweise durch einen Feiertag gestört wird.
So sieht das einfache Beispiel oben aus:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
Intervalle, die zum selben Vertrag gehören, sollten sich nicht überschneiden.
Ich muss Daten vom ersten System in das vom zweiten System verwendete Format konvertieren. Im Moment löse ich dies auf der Clientseite in C # für den einzelnen gegebenen Vertrag, aber ich möchte es in T-SQL auf der Serverseite für die Massenverarbeitung und den Export / Import zwischen Servern tun. Höchstwahrscheinlich könnte es mit CLR UDF gemacht werden, aber in diesem Stadium kann ich SQLCLR nicht verwenden.
Die Herausforderung hierbei besteht darin, die Liste der Intervalle so kurz und menschenfreundlich wie möglich zu gestalten.
Zum Beispiel dieser Zeitplan:
+-----+------------+------------+---------+--------+
| 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 |
+-----+------------+------------+---------+--------+
sollte dies werden:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,nicht das:
+------------+------------+------------+----------+----------------------+
| 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, |
+------------+------------+------------+----------+----------------------+
Ich habe versucht, eine gaps-and-islands
Herangehensweise an dieses Problem anzuwenden . Ich habe es in zwei Durchgängen versucht. Im ersten Durchgang finde ich Inseln von einfachen aufeinanderfolgenden Tagen, dh das Ende der Insel ist eine Lücke in der Reihenfolge der Tage, sei es Wochenende, Feiertag oder etwas anderes. Für jede so gefundene Insel erstelle ich eine durch Kommas getrennte Liste mit verschiedenen WeekDays
. Im zweiten Durchgang fand ich Inseln weiter, indem ich die Lücke in der Abfolge der Wochenzahlen oder eine Änderung der WeekDays
.
Bei diesem Ansatz endet jede Teilwoche wie oben gezeigt als zusätzliches Intervall, da die WeekDays
Änderung , auch wenn die Wochennummern aufeinander folgen. Außerdem kann es innerhalb einer Woche zu regelmäßigen Lücken kommen (siehe ContractID=3
Beispieldaten, für die nur Daten vorliegen Mon,Wed,Fri,
), und dieser Ansatz würde für jeden Tag in einem solchen Zeitplan separate Intervalle generieren. Auf der positiven Seite wird ein Intervall generiert, wenn der Zeitplan überhaupt keine Lücken aufweist (siehe ContractID=7
Beispieldaten, die Wochenenden enthalten). In diesem Fall spielt es keine Rolle, ob die Anfangs- oder Endwoche partiell ist.
Weitere Beispiele finden Sie im folgenden Skript, um eine bessere Vorstellung davon zu bekommen, wonach ich suche. Wie Sie sehen, sind Wochenenden häufig ausgeschlossen, andere Wochentage können jedoch ebenfalls ausgeschlossen werden. In Beispiel 3 nur Mon
, Wed
und Fri
ist Teil des Plans. Außerdem können Wochenenden einbezogen werden, wie in Beispiel 7. Die Lösung sollte alle Wochentage gleich behandeln. Jeder Wochentag kann in den Zeitplan aufgenommen oder daraus ausgeschlossen werden.
Um zu überprüfen, ob die generierte Liste der Intervalle den angegebenen Zeitplan korrekt beschreibt, können Sie den folgenden Pseudocode verwenden:
- Alle Intervalle durchlaufen
- für jede Intervallschleife durch alle Kalenderdaten zwischen Start- und Enddatum (einschließlich).
- Überprüfen Sie für jedes Datum, ob der Wochentag in der Liste aufgeführt ist
WeekDays
. Wenn ja, ist dieses Datum im Zeitplan enthalten.
Hoffentlich wird dadurch klargestellt, in welchen Fällen ein neues Intervall erstellt werden soll. In den Beispielen 4 und 5 wird ein Montag ( 2016-05-09
) aus der Mitte des Zeitplans entfernt, und ein solcher Zeitplan kann nicht durch ein einzelnes Intervall dargestellt werden. In Beispiel 6 gibt es eine große Lücke im Zeitplan, daher werden zwei Intervalle benötigt.
Intervalle stellen wöchentliche Muster im Zeitplan dar und wenn ein Muster unterbrochen / geändert wird, muss das neue Intervall hinzugefügt werden. In Beispiel 11 haben die ersten drei Wochen ein Muster Tue
, dann ändert sich dieses Muster zu Thu
. Daher benötigen wir zwei Intervalle, um einen solchen Zeitplan zu beschreiben.
Ich verwende momentan SQL Server 2008, daher sollte die Lösung in dieser Version funktionieren. Wenn eine Lösung für SQL Server 2008 mithilfe von Funktionen aus späteren Versionen vereinfacht / verbessert werden kann, ist dies ein Bonus. Zeigen Sie dies bitte auch.
Ich habe eine Calendar
Tabelle (Liste der Daten) und eine Numbers
Tabelle (Liste der Ganzzahlen ab 1), daher ist es in Ordnung, sie bei Bedarf zu verwenden. Es ist auch in Ordnung, temporäre Tabellen zu erstellen und mehrere Abfragen zu haben, die Daten in mehreren Schritten verarbeiten. Die Anzahl der Stufen in einem Algorithmus muss jedoch festgelegt werden, da Cursor und explizite WHILE
Schleifen nicht in Ordnung sind.
Skript für Beispieldaten und erwartete Ergebnisse
-- @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;
Vergleich der Antworten
Die reale Tabelle @Src
hat 403,555
Zeilen mit 15,857
unterschiedlichen ContractIDs
. Alle Antworten liefern korrekte Ergebnisse (zumindest für meine Daten) und alle sind relativ schnell, unterscheiden sich jedoch in der Optimalität. Je weniger Intervalle generiert werden, desto besser. Ich habe nur aus Neugier Laufzeiten angegeben. Das Hauptaugenmerk liegt auf dem richtigen und optimalen Ergebnis, nicht auf der Geschwindigkeit (es sei denn, es dauert zu lange - ich habe die nicht-rekursive Abfrage von Ziggy Crueltyfree Zeitgeister nach 10 Minuten gestoppt).
+--------------------------------------------------------+-----------+---------+
| 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 | | |
+--------------------------------------------------------+-----------+---------+
@Dst
) haben. Die ersten zwei Wochen des Zeitplans haben nur Tue
, so dass Sie WeekDays=Tue,Thu,
für diese Wochen nicht haben können . Die letzten zwei Wochen des Zeitplans haben nur Thu
, so dass Sie WeekDays=Tue,Thu,
für diese Wochen wieder nicht haben können . Suboptimale Lösung dafür wären drei Reihen: nur Tue
für die ersten zwei Wochen, dann Tue,Thu,
für die dritte Woche, die beides hat, Tue
und Thu
dann nur Thu
für die letzten zwei Wochen.
ContractID
Intervall ändert, wenn ein Intervall Überschreitet 7 Tage und der neue Wochentag wurde zuvor nicht gesehen, wenn in der Liste der geplanten Tage eine Lücke vorhanden ist.
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
im @Dst nicht eine Zeile mit stehenTue, Thu,
?