Muszę przekonwertować dane między dwoma systemami.
Pierwszy system przechowuje harmonogramy jako zwykłą listę dat. Każda data uwzględniona w harmonogramie to jeden wiersz. W sekwencji dat mogą występować różne luki (weekendy, święta i dłuższe przerwy, niektóre dni tygodnia mogą zostać wyłączone z harmonogramu). Nie może być żadnych luk, nawet weekendy mogą być uwzględnione. Harmonogram może trwać do 2 lat. Zwykle trwa to kilka tygodni.
Oto prosty przykład harmonogramu obejmującego dwa tygodnie z wyłączeniem weekendów (w skrypcie poniżej znajdują się bardziej skomplikowane przykłady):
+----+------------+------------+---------+--------+
| 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
jest unikalny, ale niekoniecznie sekwencyjny (jest to klucz podstawowy). Daty są unikalne w ramach każdej umowy (istnieje niepowtarzalny indeks (ContractID, dt)
).
Drugi system przechowuje harmonogramy jako interwały z listą dni tygodnia, które są częścią harmonogramu. Każdy interwał jest definiowany przez daty rozpoczęcia i zakończenia (włącznie) oraz listę dni tygodnia, które są uwzględnione w harmonogramie. W tym formacie możesz efektywnie definiować powtarzalne cotygodniowe wzorce, takie jak pon.-śr., Ale staje się to bolesne, gdy wzorzec jest zakłócany, na przykład w święta.
Oto jak będzie wyglądał prosty powyższy przykład:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
odstępy czasu należące do tej samej umowy nie powinny się pokrywać.
Muszę przekonwertować dane z pierwszego systemu do formatu używanego przez drugi system. W tej chwili rozwiązuję to po stronie klienta w C # dla pojedynczego danego kontraktu, ale chciałbym to zrobić w języku T-SQL po stronie serwera w celu masowego przetwarzania i eksportu / importu między serwerami. Najprawdopodobniej można to zrobić przy użyciu CLR UDF, ale na tym etapie nie mogę używać SQLCLR.
Wyzwanie polega na tym, aby lista interwałów była jak najkrótsza i przyjazna dla człowieka.
Na przykład ten harmonogram:
+-----+------------+------------+---------+--------+
| 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 |
+-----+------------+------------+---------+--------+
powinno stać się tak:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,nie to:
+------------+------------+------------+----------+----------------------+
| 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, |
+------------+------------+------------+----------+----------------------+
Próbowałem zastosować gaps-and-islands
podejście do tego problemu. Próbowałem to zrobić w dwóch krokach. W pierwszym przejściu znajduję wyspy prostych kolejnych dni, tj. Koniec wyspy to jakakolwiek przerwa w sekwencji dni, czy to weekend, święto państwowe czy coś innego. Dla każdej takiej znalezionej wyspy tworzę listę oddzielonych przecinkami WeekDays
. W drugim przejeździe grupa znalazła wyspy dalej, patrząc na lukę w sekwencji numerów tygodni lub zmianę w WeekDays
.
Dzięki takiemu podejściu każdy częściowy tydzień kończy się jako dodatkowy interwał, jak pokazano powyżej, ponieważ chociaż liczby tygodni są następujące po sobie, WeekDays
zmiana. Poza tym w ciągu tygodnia mogą występować regularne luki (patrz ContractID=3
przykładowe dane, które mają dane tylko dla Mon,Wed,Fri,
), a takie podejście wygenerowałoby osobne przedziały dla każdego dnia w takim harmonogramie. Z drugiej strony generuje jeden interwał, jeśli harmonogram nie ma żadnych luk (patrz ContractID=7
przykładowe dane, które obejmują weekendy), w takim przypadku nie ma znaczenia, czy tydzień początkowy czy końcowy jest częściowy.
Zobacz inne przykłady w skrypcie poniżej, aby uzyskać lepszy obraz tego, czego szukam. Widać, że dość często weekendy są wykluczane, ale inne dni tygodnia mogą być również wykluczone. W przykładzie 3, tylko Mon
, Wed
i Fri
należą do rozkładu. Poza tym można uwzględnić weekendy, jak w przykładzie 7. Rozwiązanie powinno traktować wszystkie dni tygodnia jednakowo. Każdy dzień tygodnia może zostać włączony lub wyłączony z harmonogramu.
Aby sprawdzić, czy wygenerowana lista przedziałów prawidłowo opisuje dany harmonogram, możesz użyć następującego pseudokodu:
- zapętlić wszystkie przedziały
- dla każdej pętli interwału przez wszystkie daty kalendarzowe między datami początkową i końcową (włącznie).
- dla każdej daty sprawdź, czy jej dzień tygodnia jest wymieniony w
WeekDays
. Jeśli tak, data ta jest uwzględniona w harmonogramie.
Mamy nadzieję, że wyjaśnia to, w jakich przypadkach należy utworzyć nowy interwał. W przykładach 4 i 5 jeden poniedziałek ( 2016-05-09
) jest usuwany ze środka harmonogramu i taki harmonogram nie może być reprezentowany przez pojedynczy przedział. W przykładzie 6 w harmonogramie jest długa przerwa, dlatego potrzebne są dwa odstępy.
Odstępy reprezentują tygodniowe wzorce w harmonogramie, a gdy wzorzec jest zakłócany / zmieniany, należy dodać nowy odstęp. W przykładzie 11 pierwsze trzy tygodnie mają wzór Tue
, a następnie ten wzór zmienia się na Thu
. W rezultacie potrzebujemy dwóch przedziałów, aby opisać taki harmonogram.
Obecnie używam SQL Server 2008, więc rozwiązanie powinno działać w tej wersji. Jeśli rozwiązanie dla SQL Server 2008 można uprościć / ulepszyć za pomocą funkcji z późniejszych wersji, jest to premia, proszę również to pokazać.
Mam Calendar
tabelę (listę dat) i Numbers
tabelę (listę liczb całkowitych rozpoczynających się od 1), więc w razie potrzeby można z nich korzystać. Można również tworzyć tabele tymczasowe i mieć kilka zapytań przetwarzających dane w kilku etapach. Liczba etapów w algorytmie musi być jednak ustalona, kursory i wyraźne WHILE
pętle nie są w porządku.
Skrypt dla przykładowych danych i oczekiwanych wyników
-- @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;
Porównanie odpowiedzi
Prawdziwy stół @Src
ma403,555
wiersze z 15,857
wyraźnymi ContractIDs
. Wszystkie odpowiedzi dają prawidłowe wyniki (przynajmniej dla moich danych) i wszystkie są dość szybkie, ale różnią się optymalizacją. Im mniej wygenerowanych interwałów, tym lepiej. Uwzględniłem czasy działania tylko dla ciekawości. Głównym celem jest poprawny i optymalny wynik, a nie szybkość (chyba że zajmuje to zbyt dużo czasu - zatrzymałem nierekurencyjne zapytanie Ziggy Crueltyfree Zeitgeister po 10 minutach).
+--------------------------------------------------------+-----------+---------+
| 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 | | |
+--------------------------------------------------------+-----------+---------+
źródło
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
w @Dst nie powinien znajdować się jeden wierszTue, Thu,
?@Dst
). Pierwsze dwa tygodnie harmonogramu mają tylkoTue
, więc nie możesz miećWeekDays=Tue,Thu,
tych tygodni. Ostatnie dwa tygodnie harmonogramu mają tylkoThu
, więc znowu nie możesz miećWeekDays=Tue,Thu,
na te tygodnie. Nieoptymalne rozwiązanie byłoby trzy rzędy: tylkoTue
dla pierwszych dwóch tygodni, a następnieTue,Thu,
do trzeciego tygodnia, który ma zarównoTue
aThu
, a potem po prostuThu
w ciągu ostatnich dwóch tygodni.ContractID
zmiany, czy interwał przekracza 7 dni, a nowy dzień tygodnia nie był wcześniej widziany, jeśli na liście zaplanowanych dni jest luka.Odpowiedzi:
Ten używa rekurencyjnego CTE. Jego wynik jest identyczny z przykładem w pytaniu . Wymyślenie tego było koszmarem ... Kod zawiera komentarze ułatwiające jego skomplikowaną logikę.
Kolejna strategia
Ten powinien być znacznie szybszy niż poprzedni, ponieważ nie opiera się na wolno ograniczonej rekurencyjnej CTE w SQL Server 2008, chociaż implementuje mniej więcej tę samą strategię.
Tam jest
WHILE
pętla (nie mogłem wymyślić sposobu jej uniknięcia), ale dotyczy ona mniejszej liczby iteracji (najwyższa liczba sekwencji (minus jedna) w danym kontrakcie).Jest to prosta strategia i może być stosowana do sekwencji krótszych lub dłuższych niż tydzień (zastępując dowolne wystąpienie stałej 7 dla dowolnej innej liczby i
dowBit
obliczonej z MODULUSA xDayNo
zamiastDATEPART(wk)
) i do 32.źródło
Nie do końca to, czego szukasz, ale być może zainteresuje Cię.
Kwerenda tworzy tygodnie z ciągiem oddzielonym przecinkami dla dni używanych w każdym tygodniu. Następnie znajduje wyspy kolejnych tygodni, w których stosuje się ten sam wzór
Weekdays
.Wynik:
ContractID = 2
pokazuje różnicę wyniku w porównaniu z tym, czego chcesz. Pierwszy i ostatni tydzień będą traktowane jako osobne okresy, ponieważWeekDays
są różne.źródło
WeekDays
jak liczba 7-bitowa. Tylko 128 kombinacji. Istnieje tylko 128 * 128 = 16384 możliwych par. Zbuduj tabelę temp ze wszystkimi możliwymi parami, a następnie wymyśl algorytm oparty na zestawie, który oznaczałby, które pary można scalić: wzorzec jednego tygodnia jest „pokryty” wzorcem z następnego tygodnia. Dołącz samodzielnie do bieżącego wyniku tygodniowego (ponieważ nie ma goLAG
w 2008 r.) I użyj tej tabeli tymczasowej, aby zdecydować, które pary połączyć ... Nie jestem pewien, czy ten pomysł ma jakąkolwiek wartość.Skończyłem z podejściem, które daje optymalne rozwiązanie w tym przypadku i myślę, że ogólnie będzie dobrze. Rozwiązanie jest jednak dość długie, więc interesujące byłoby sprawdzenie, czy ktoś inny ma bardziej zwięzłe podejście.
Oto skrypt, który zawiera pełne rozwiązanie .
A oto zarys algorytmu:
ContractId
ContractId
i mają takie sameWeekDays
WeekDays
tydzień z jednego tygodnia odpowiada wiodącemu podzbiorowiWeekDays
poprzedniego zgrupowania, połącz się z tym poprzednim zgrupowaniemWeekDays
z jednego tygodnia odpowiada końcowemu podzbiorowiWeekDays
następnego zgrupowania, połącz się z następnym zgrupowaniemźródło
(1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),
. Może być reprezentowany jako jeden przedział, ale twoje rozwiązanie daje dwa. Przyznaję, że tego przykładu nie było w przykładowych danych i nie ma on krytycznego znaczenia. Spróbuję uruchomić twoje rozwiązanie na prawdziwych danych.Nie mogłem zrozumieć logiki grupowania tygodni z przerwami lub tygodni z weekendami (np. Kiedy są dwa kolejne tygodnie z weekendem, do którego tygodnia idzie weekend?).
Poniższe zapytanie generuje pożądane dane wyjściowe, z wyjątkiem tego, że grupuje tylko kolejne dni tygodnia i grupuje tygodnie od poniedziałku do soboty (zamiast od poniedziałku do niedzieli). Nie jest to dokładnie to, czego chcesz, ale może to dostarczyć wskazówek na temat innej strategii. Grupowanie dni pochodzi stąd . Użyte funkcje okienkowania powinny współpracować z SQLServer 2008, ale nie mam tej wersji do przetestowania, czy rzeczywiście działa.
Wynik
źródło
Dla kompletności, oto dwa przejścia
gaps-and-islands
podejście które wypróbowałem sam, zanim zadałem to pytanie.Gdy testowałem to na rzeczywistych danych, znalazłem kilka przypadków, w których przynosiło to nieprawidłowe wyniki, i naprawiłem to.
Oto algorytm:
CTE_ContractDays
,CTE_DailyRN
,CTE_DailyIslands
) i obliczyć liczbę tygodni dla każdego rozpoczynającego i kończącego się dnia wyspie. Tutaj liczba tygodni jest obliczana przy założeniu, że poniedziałek jest pierwszym dniem tygodnia.CTE_Weeks
).CTE_FirstResult
).WeekDays
(CTE_SecondRN
,CTE_Schedules
).Dobrze radzi sobie z przypadkami, gdy nie występują zakłócenia w cotygodniowych wzorcach (1, 7, 8, 10, 12). Dobrze radzi sobie z przypadkami, gdy wzór ma dni niesekwencyjne (3).
Ale niestety generuje dodatkowe odstępy dla częściowych tygodni (2, 3, 5, 6, 9, 11).
Wynik
Rozwiązanie oparte na kursorze
Przekształciłem mój kod C # w algorytm oparty na kursorach, aby zobaczyć, jak się on ma do innych rozwiązań na rzeczywistych danych. Potwierdza, że jest znacznie wolniejszy niż inne podejścia oparte na zestawie lub rekurencyjne, ale generuje optymalny wynik.
źródło
Byłem trochę zaskoczony, że rozwiązanie kursora Vladimira było tak wolne, więc próbowałem również zoptymalizować tę wersję. Potwierdziłem, że używanie kursora również było dla mnie bardzo wolne.
Jednak kosztem korzystania z nieudokumentowanej funkcjonalności w SQL Server przez dołączenie do zmiennej podczas przetwarzania zestawu wierszy byłem w stanie stworzyć uproszczoną wersję tej logiki, która daje optymalny wynik i działa znacznie szybciej niż kursor i moje oryginalne rozwiązanie . Więc skorzystaj na własne ryzyko, ale przedstawię rozwiązanie na wypadek, gdyby było to interesujące. Możliwe byłoby również zaktualizowanie rozwiązania w celu użycia
WHILE
pętli od jednego do maksymalnej liczby wierszy, szukając numeru następnego wiersza przy każdej iteracji pętli. Trzymałoby się to w pełni udokumentowanej i niezawodnej funkcjonalności, ale naruszyłoby (nieco sztuczne) stwierdzone ograniczenie problemu, żeWHILE
pętle nie są dozwolone.Należy pamiętać, że jeśli korzystanie z SQL 2014 było dozwolone, prawdopodobnie jest to natywna kompilowana procedura składowana która zapętla numery wierszy i uzyskuje dostęp do każdego numeru wiersza w tabeli zoptymalizowanej pod kątem pamięci, byłaby implementacją tej samej logiki, która działałaby szybciej.
Oto pełne rozwiązanie , w tym rozszerzenie danych testowych do około pół miliona wierszy. Nowe rozwiązanie kończy się w ciągu około 3 sekund i moim zdaniem jest o wiele bardziej zwięzłe i czytelne niż poprzednie rozwiązanie, które zaoferowałem. Wytłumaczę tutaj trzy kroki:
Krok 1: przetwarzanie wstępne
Najpierw dodajemy numer wiersza do zestawu danych, w kolejności przetwarzania danych. Robiąc to, konwertujemy również każdy dowInt na potęgę 2, dzięki czemu możemy użyć mapy bitowej do przedstawienia dni, które zostały zaobserwowane w danej grupie:
Krok 2: Pętla między dniami umowy w celu zidentyfikowania nowych grup
Następnie zapętlamy dane, w kolejności według numeru wiersza. Obliczamy tylko listę numerów wierszy, które tworzą granicę nowego zgrupowania, a następnie wyprowadzamy te numery wierszy do tabeli:
Krok 3: Obliczanie wyników końcowych na podstawie numerów wierszy każdej granicy grupowania
Następnie obliczamy końcowe grupy, używając granic określonych w powyższej pętli, aby agregować wszystkie daty należące do każdej grupy:
źródło
WHILE
pętli, ponieważ już wiedziałem, jak rozwiązać to za pomocą kursora i chciałem znaleźć rozwiązanie oparte na zestawie. Poza tym podejrzewałem, że kursor będzie wolny (szczególnie z zagnieżdżoną pętlą). Ta odpowiedź jest bardzo interesująca pod względem uczenia się nowych sztuczek i doceniam twój wysiłek.Dyskusja będzie przebiegać zgodnie z kodem.
@Helper
ma poradzić sobie z tą zasadą:To pozwala mi wymienić nazwy dni w kolejności numerów dni między dowolnymi dwoma dniami. Jest to wykorzystywane przy podejmowaniu decyzji, czy należy rozpocząć nowy interwał. Wypełniam go wartościami z dwóch tygodni, aby ułatwić kodowanie w weekendy.
Istnieją czystsze sposoby na wdrożenie tego. Pełna tabela „dat” byłaby jedna. Prawdopodobnie jest też sprytny sposób na liczbę dni i arytmetykę modulo.
CTE
MissingDays
ma wygenerować listę nazw dni między dowolnymi dwoma danymi dniami. Jest obsługiwany w ten niezręczny sposób, ponieważ rekurencyjne CTE (następujące) nie zezwala na agregacje, TOP () lub inne operatory. To nieeleganckie, ale działa.CTE
Numbered
polega na wymuszeniu znanej sekwencji bez przerw w danych. Pozwala to uniknąć wielu porównań później.CTE
Incremented
to miejsce akcji. Zasadniczo używam rekurencyjnego CTE do przechodzenia między danymi i egzekwowania reguł. Wygenerowany numer wierszaNumbered
(powyżej) służy do sterowania przetwarzaniem rekurencyjnym.Ziarno rekurencyjnej CTE po prostu pobiera pierwszą datę dla każdego ContractID i inicjuje wartości, które zostaną wykorzystane do podjęcia decyzji, czy wymagany jest nowy interwał.
Decyzja, czy nowy interwał powinien się rozpocząć, wymaga daty rozpoczęcia bieżącego interwału, listy dni i długości dowolnej przerwy w datach kalendarzowych. Mogą one zostać zresetowane lub przeniesione, w zależności od decyzji. Dlatego część rekurencyjna jest pełna i trochę powtarzalna, ponieważ musimy zdecydować, czy rozpocząć nowy interwał dla więcej niż jednej wartości kolumny.
Logika decyzyjna dla kolumn
WeekDays
iIntervalStart
powinna mieć tę samą logikę decyzyjną - można ją wycinać i wklejać między nimi. Jeśli logika rozpoczęcia nowego interwału miałaby ulec zmianie, należy zmienić kod. Idealnie byłoby zatem abstrakcyjne; robienie tego w rekurencyjnym CTE może być trudne.EXISTS()
Klauzula jest outfall nie jest w stanie korzystać z funkcji agregujących w rekurencyjnej CTE. Wszystko, co robi, to sprawdzić, czy dni mieszczące się w luce są już w bieżącym przedziale.Nie ma nic magicznego w zagnieżdżaniu klauzul logicznych. Jeśli jest wyraźniejszy w innej konformacji lub przy użyciu zagnieżdżonych CASE, powiedzmy, że nie ma powodu, aby tak było.
Ostatnim
SELECT
jest podanie wyniku w pożądanym formacie.Włączenie PK
Src.ID
nie jest przydatne dla tej metody.(ContractID,dt)
Myślę, że indeks klastrowy byłby fajny.Istnieje kilka szorstkich krawędzi. Dni nie są zwracane w sekwencji dow, ale w sekwencji kalendarza pojawiają się w danych źródłowych. Wszystko, co ma związek z @Helper, jest nieporadne i można je wygładzić. Podoba mi się pomysł używania jednego bitu dziennie i używania funkcji binarnych zamiast
LIKE
. Rozdzielenie niektórych pomocniczych CTE na tabelę temp z odpowiednimi indeksami niewątpliwie pomogłoby.Jednym z wyzwań jest to, że „tydzień” nie jest dopasowany do standardowego kalendarza, lecz zależy od danych i resetuje się, gdy zostanie ustalone, że należy rozpocząć nowy interwał. „Tydzień” lub przynajmniej interwał może trwać od jednego dnia do całego zestawu danych.
Dla dobra, oto szacunkowe koszty w porównaniu z przykładowymi danymi Geoffa (dzięki za to!) Po różnych zmianach:
Szacowana i faktyczna liczba wierszy różni się bardzo.
Plan zawiera spoo tabeli, prawdopodobnie w wyniku rekurencyjnego CTE. Większość akcji odbywa się na stole roboczym, który:
Chyba tak, jak zaimplementowano rekurencję!
źródło
MAX(g.IntervalStart)
wydaje się dziwna, ponieważg.IntervalStart
znajduje się wGROUP BY
. Spodziewałem się, że spowoduje to błąd składniowy, ale działa. Powinien on być po prostug.IntervalStart as StartDT
wSELECT
? A możeg.IntervalStart
nie powinno być wGROUP BY
?MissingDays
iNumbered
zostaną zastąpione tabelami temp z odpowiednimi indeksami, może mieć przyzwoitą wydajność. Jakie indeksy poleciłbyś? Mogę spróbować jutro rano.Numbered
tabelą tymczasową i indeksem klastrowym(ContractID, rn)
. Bez dużego zestawu danych do wygenerowania odpowiedniego planu trudno zgadnąć. Fizjalizacja zaMissingDates
pomocą indeksów również(StartDay, FollowingDayInt)
byłaby dobra.