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 | | |
+--------------------------------------------------------+-----------+---------+
quelle
(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,
?@Dst
) haben. Die ersten zwei Wochen des Zeitplans haben nurTue
, so dass SieWeekDays=Tue,Thu,
für diese Wochen nicht haben können . Die letzten zwei Wochen des Zeitplans haben nurThu
, so dass SieWeekDays=Tue,Thu,
für diese Wochen wieder nicht haben können . Suboptimale Lösung dafür wären drei Reihen: nurTue
für die ersten zwei Wochen, dannTue,Thu,
für die dritte Woche, die beides hat,Tue
undThu
dann nurThu
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.Antworten:
Dieser verwendet einen rekursiven CTE. Das Ergebnis ist identisch mit dem Beispiel in der Frage . Es war ein Albtraum, sich etwas einfallen zu lassen ... Der Code enthält Kommentare, die durch seine verschlungene Logik erleichtert werden.
Eine andere Strategie
Dieser sollte deutlich schneller als der vorherige sein, da er nicht auf dem langsam begrenzten rekursiven CTE in SQL Server 2008 basiert, obwohl er mehr oder weniger dieselbe Strategie implementiert.
Es gibt eine
WHILE
Schleife (ich konnte mir keinen Weg ausdenken, sie zu umgehen), aber es wird weniger iteriert (die höchste Anzahl von Sequenzen (minus eins) in einem bestimmten Vertrag).Es ist eine einfache Strategie und kann für Sequenzen verwendet werden, die entweder kürzer oder länger als eine Woche sind (wobei jedes Vorkommen der Konstante 7 durch eine andere Zahl ersetzt wird und die
dowBit
aus MODULUS x vonDayNo
statt berechnet wirdDATEPART(wk)
) und bis zu 32.quelle
Nicht genau das, wonach Sie suchen, aber vielleicht für Sie von Interesse sein.
Die Abfrage erstellt Wochen mit einer durch Kommas getrennten Zeichenfolge für die in jeder Woche verwendeten Tage. Es werden dann die Inseln von aufeinanderfolgenden Wochen gefunden, die dasselbe Muster in verwenden
Weekdays
.Ergebnis:
ContractID = 2
zeigt, was der Unterschied im Ergebnis zu dem ist, was Sie wollen. Die erste und die letzte Woche werden als separate Zeiträume behandelt, da diesWeekDays
unterschiedlich ist.quelle
WeekDays
wie eine 7-Bit-Zahl. Nur 128 Kombinationen. Es gibt nur 128 * 128 = 16384 mögliche Paare. Erstellen Sie eine temporäre Tabelle mit allen möglichen Paaren und finden Sie dann einen satzbasierten Algorithmus, der angibt, welche Paare zusammengeführt werden können: Ein Muster von einer Woche wird durch ein Muster der nächsten Woche "abgedeckt". Verbinden Sie sich selbst mit dem aktuellen wöchentlichen Ergebnis (da esLAG
2008 kein Ergebnis gibt ) und verwenden Sie diese temporäre Tabelle, um zu entscheiden, welche Paare zusammengeführt werden sollen ... Sie sind sich nicht sicher, ob diese Idee von Nutzen ist.Am Ende habe ich einen Ansatz gefunden, der in diesem Fall die optimale Lösung liefert, und ich denke, er wird im Allgemeinen gut abschneiden. Die Lösung ist jedoch recht langwierig, daher wäre es interessant zu sehen, ob jemand anderes einen anderen Ansatz verfolgt, der präziser ist.
Hier ist ein Skript, das die vollständige Lösung enthält .
Und hier ist ein Überblick über den Algorithmus:
ContractId
ContractId
und dieselben habenWeekDays
WeekDays
der einzelnen Woche mit einer führenden UntergruppeWeekDays
der vorherigen Gruppierung übereinstimmt , führen Sie sie zu dieser vorherigen Gruppierung zusammenWeekDays
der einzelnen Woche mit einerWeekDays
nachfolgenden Untergruppe der nächsten Gruppierung übereinstimmt , führen Sie sie zu dieser nächsten Gruppierung zusammenquelle
(1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),
. Es könnte als ein Intervall dargestellt werden, aber Ihre Lösung erzeugt zwei. Ich gebe zu, dieses Beispiel war nicht in den Beispieldaten und es ist nicht kritisch. Ich werde versuchen, Ihre Lösung auf realen Daten auszuführen.Ich konnte die Logik hinter der Gruppierung von Wochen mit Lücken oder Wochen mit Wochenenden nicht verstehen (z. B. wenn es zwei aufeinanderfolgende Wochen mit einem Wochenende gibt, auf welche Woche geht das Wochenende?).
Die folgende Abfrage erzeugt die gewünschte Ausgabe, mit der Ausnahme, dass nur aufeinanderfolgende Wochentage und Gruppenwochen von Sonntag bis Samstag (anstelle von Montag bis Sonntag) gruppiert werden. Dies ist zwar nicht genau das, was Sie wollen, kann aber möglicherweise Hinweise auf eine andere Strategie liefern. Die Gruppierung der Tage erfolgt von hier aus . Die verwendeten Fensterfunktionen sollten mit SQL Server 2008 funktionieren, aber ich habe diese Version nicht zum Testen, ob dies tatsächlich der Fall ist.
Ergebnis
quelle
Der Vollständigkeit halber ist hier ein
gaps-and-islands
Ansatz mit zwei Schritten , den ich selbst ausprobiert habe, bevor ich diese Frage gestellt habe.Als ich es an den realen Daten testete, fand ich nur wenige Fälle, in denen es zu falschen Ergebnissen kam, und korrigierte sie.
Hier ist der Algorithmus:
CTE_ContractDays
,CTE_DailyRN
,CTE_DailyIslands
) und berechnen eine Woche Nummer für jedes Start- und Enddatum einer Insel. Hier wird die Wochennummer unter der Annahme berechnet, dass Montag der erste Wochentag ist.CTE_Weeks
).CTE_FirstResult
).WeekDays
(CTE_SecondRN
,CTE_Schedules
) zu gruppieren .Es behandelt gut Fälle, in denen die wöchentlichen Muster nicht gestört sind (1, 7, 8, 10, 12). Es behandelt gut Fälle, in denen das Muster nicht aufeinanderfolgende Tage hat (3).
Leider werden jedoch zusätzliche Intervalle für Teilwochen generiert (2, 3, 5, 6, 9, 11).
Ergebnis
Cursor-basierte Lösung
Ich habe meinen C # -Code in einen Cursorbasierten Algorithmus konvertiert, um zu sehen, wie er mit anderen Lösungen für reale Daten verglichen wird. Es bestätigt, dass es viel langsamer als andere satzbasierte oder rekursive Ansätze ist, aber ein optimales Ergebnis erzielt.
quelle
Ich war ein bisschen überrascht, dass die Cursor-Lösung von Vladimir so langsam war, und habe auch versucht, diese Version zu optimieren. Ich habe bestätigt, dass die Verwendung eines Cursors auch für mich sehr langsam war.
Auf Kosten der Verwendung undokumentierter Funktionen in SQL Server durch Anhängen an eine Variable während der Verarbeitung eines Rowsets konnte ich jedoch eine vereinfachte Version dieser Logik erstellen, die das optimale Ergebnis liefert und viel schneller ausgeführt wird als der Cursor und meine ursprüngliche Lösung . Die Verwendung erfolgt also auf eigenes Risiko, aber ich werde die Lösung vorstellen, falls dies von Interesse ist. Es wäre auch möglich, die Lösung zu aktualisieren, um eine
WHILE
Schleife von einer auf die maximale Zeilennummer zu verwenden, wobei bei jeder Iteration der Schleife nach der nächsten Zeilennummer gesucht wird. Dies würde sich an eine vollständig dokumentierte und zuverlässige Funktionalität halten, würde jedoch die (etwas künstlich) angegebene Einschränkung des Problems verletzen, dassWHILE
Schleifen nicht zulässig sind.Wenn die Verwendung von SQL 2014 zulässig wäre, wäre es wahrscheinlich, dass eine nativ kompilierte gespeicherte Prozedur , die die Zeilennummern durchläuft und auf jede Zeilennummer in einer speicheroptimierten Tabelle zugreift, eine Implementierung derselben Logik ist, die schneller ausgeführt wird.
Hier finden Sie die vollständige Lösung , einschließlich der Erweiterung der Testdaten auf etwa eine halbe Million Zeilen. Die neue Lösung ist in ca. 3 Sekunden fertig und meiner Meinung nach viel prägnanter und lesbarer als die vorherige Lösung, die ich angeboten habe. Ich werde die folgenden drei Schritte ausführen:
Schritt 1: Vorverarbeitung
Wir fügen dem Datensatz zunächst eine Zeilennummer hinzu, in der Reihenfolge, in der wir die Daten verarbeiten. Dabei konvertieren wir auch jedes dowInt in eine Potenz von 2, sodass wir mithilfe einer Bitmap darstellen können, welche Tage in einer bestimmten Gruppierung beobachtet wurden:
Schritt 2: Durchlaufen der Vertragstage, um neue Gruppierungen zu identifizieren
Als nächstes durchlaufen wir die Daten in der Reihenfolge der Zeilennummern. Wir berechnen nur die Liste der Zeilennummern, die die Grenze einer neuen Gruppierung bilden, und geben diese Zeilennummern in eine Tabelle aus:
Schritt 3: Berechnen der Endergebnisse basierend auf den Zeilennummern jeder Gruppierungsgrenze
Wir berechnen dann die endgültigen Gruppierungen, indem wir die in der obigen Schleife angegebenen Grenzen verwenden, um alle Daten zu aggregieren, die in die einzelnen Gruppierungen fallen:
quelle
WHILE
Schleifen zu verwenden, da ich bereits wusste, wie man sie mit dem Cursor löst, und ich wollte eine satzbasierte Lösung finden. Außerdem hatte ich den Verdacht, dass der Cursor langsam ist (insbesondere mit einer verschachtelten Schleife). Diese Antwort ist sehr interessant, um neue Tricks zu lernen, und ich schätze Ihre Bemühungen.Die Diskussion folgt dem Code.
@Helper
ist mit dieser Regel umzugehen:Damit kann ich Tagesnamen in der Reihenfolge der Tagesnummern zwischen zwei beliebigen Tagen auflisten. Dies wird verwendet, um zu entscheiden, ob ein neues Intervall beginnen soll. Ich fülle es mit Werten im Wert von zwei Wochen, um das Umschließen eines Wochenendes einfacher zu programmieren.
Es gibt sauberere Möglichkeiten, dies umzusetzen. Eine vollständige "Datteltabelle" wäre eine. Es gibt wahrscheinlich auch einen klugen Weg mit der Tagzahl und der Modulo-Arithmetik.
Der CTE
MissingDays
generiert eine Liste von Tagesnamen zwischen zwei beliebigen Tagen. Es wird auf diese umständliche Weise gehandhabt, da der rekursive CTE (folgend) keine Aggregate, TOP () oder andere Operatoren zulässt. Das ist unelegant, aber es funktioniert.CTE
Numbered
soll eine bekannte, lückenlose Sequenz für die Daten erzwingen. Es vermeidet viele spätere Vergleiche.CTE
Incremented
ist der Ort, an dem die Aktion stattfindet. Im Wesentlichen verwende ich einen rekursiven CTE, um die Daten zu durchlaufen und die Regeln durchzusetzen. Die in generierte ZeilennummerNumbered
(oben) wird verwendet, um die rekursive Verarbeitung zu steuern.Der Startwert des rekursiven CTE erhält einfach das erste Datum für jede ContractID und initialisiert Werte, anhand derer entschieden wird, ob ein neues Intervall erforderlich ist.
Die Entscheidung, ob ein neues Intervall beginnen soll, erfordert das Startdatum des aktuellen Intervalls, die Tagesliste und die Länge einer eventuellen Lücke in den Kalenderdaten. Diese können je nach Entscheidung zurückgesetzt oder übertragen werden. Daher ist der rekursive Teil ausführlich und ein wenig repetitiv, da wir entscheiden müssen, ob für mehr als einen Spaltenwert ein neues Intervall gestartet werden soll.
Die Entscheidungslogik für Spalten
WeekDays
undIntervalStart
sollte dieselbe Entscheidungslogik haben - sie kann zwischen ihnen ausgeschnitten und eingefügt werden. Wenn sich die Logik zum Starten eines neuen Intervalls ändern sollte, ist dies der zu ändernde Code. Im Idealfall wäre es daher abstrahiert; Dies in einem rekursiven CTE zu tun, kann eine Herausforderung sein.Die
EXISTS()
Klausel ist die Folge davon, dass Aggregatfunktionen in einem rekursiven CTE nicht verwendet werden können. Es wird lediglich geprüft, ob die in eine Lücke fallenden Tage bereits im aktuellen Intervall liegen.Es ist nichts Magisches an der Verschachtelung der Logikklauseln. Wenn es in einer anderen Konformation klarer ist oder geschachtelte CASEs verwendet werden, gibt es keinen Grund, dies so beizubehalten.
Das Letzte
SELECT
ist, die Ausgabe in dem gewünschten Format zu geben.Das Aktivieren der PK
Src.ID
ist für diese Methode nicht hilfreich. Ein Clustered-Index(ContractID,dt)
wäre schön, finde ich.Es gibt ein paar Ecken und Kanten. Die Tage werden nicht in der angegebenen Reihenfolge zurückgegeben, sondern in der Kalendersequenz in den Quelldaten. Alles was mit @Helper zu tun hat ist klobig und könnte geglättet werden. Ich mag die Idee, ein Bit pro Tag und stattdessen binäre Funktionen zu verwenden
LIKE
. Es wäre zweifellos hilfreich, einige der zusätzlichen CTEs in eine temporäre Tabelle mit geeigneten Indizes zu unterteilen.Eine der Herausforderungen dabei ist, dass eine "Woche" nicht mit einem Standardkalender übereinstimmt, sondern von den Daten gesteuert wird und zurückgesetzt wird, wenn bestimmt wird, dass ein neues Intervall beginnen soll. Eine "Woche" oder zumindest ein Intervall kann einen Tag lang sein und den gesamten Datensatz umfassen.
Aus Gründen des Interesses sind hier die geschätzten Kosten für Geoffs Beispieldaten (danke dafür!) Nach verschiedenen Änderungen:
Die geschätzte und tatsächliche Anzahl der Zeilen ist sehr unterschiedlich.
Der Plan enthält einen Tabellenfehler, der wahrscheinlich auf den rekursiven CTE zurückzuführen ist. Der Großteil der Action findet in einem Arbeitstisch statt:
Genau so, wie es rekursiv implementiert ist, denke ich!
quelle
MAX(g.IntervalStart)
mir komisch vor, weilg.IntervalStart
in derGROUP BY
. Ich habe erwartet, dass es einen Syntaxfehler gibt, aber es funktioniert. Sollte es nurg.IntervalStart as StartDT
in seinSELECT
? Oderg.IntervalStart
sollte das nicht seinGROUP BY
?MissingDays
undNumbered
mit temporären Tabellen mit dem richtigen Indizes ersetzt werden, könnte es ordentliche Leistung hat. Welche Indizes würden Sie empfehlen? Ich könnte es morgen früh versuchen.Numbered
durch eine temporäre Tabelle und einen Clustered-Index einen Versuch(ContractID, rn)
wert wäre. Ohne einen großen Datensatz, um den entsprechenden Plan zu generieren, ist es schwierig zu erraten. Auch das PhysikalisierenMissingDates
mit Indizes(StartDay, FollowingDayInt)
wäre gut.