Tôi cần chuyển đổi dữ liệu giữa hai hệ thống.
Hệ thống đầu tiên lưu trữ lịch trình như một danh sách đơn giản của ngày. Mỗi ngày được bao gồm trong lịch trình là một hàng. Có thể có nhiều khoảng trống khác nhau trong chuỗi ngày (cuối tuần, ngày lễ và tạm dừng dài hơn, một số ngày trong tuần có thể được loại trừ khỏi lịch trình). Không thể có khoảng trống nào cả, thậm chí cuối tuần có thể được bao gồm. Lịch trình có thể dài đến 2 năm. Thông thường nó là vài tuần dài.
Dưới đây là một ví dụ đơn giản về lịch trình kéo dài hai tuần trừ các ngày cuối tuần (có các ví dụ phức tạp hơn trong kịch bản bên dưới):
+----+------------+------------+---------+--------+
| 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
là duy nhất, nhưng nó không nhất thiết phải tuần tự (nó là khóa chính). Ngày là duy nhất trong mỗi Hợp đồng (có chỉ mục duy nhất trên (ContractID, dt)
).
Hệ thống thứ hai lưu trữ lịch biểu dưới dạng khoảng thời gian với danh sách các ngày trong tuần là một phần của lịch trình. Mỗi khoảng được xác định bởi ngày bắt đầu và ngày kết thúc (bao gồm) và danh sách các ngày trong tuần được bao gồm trong lịch trình. Trong định dạng này, bạn có thể xác định một cách hiệu quả các mẫu hàng tuần lặp đi lặp lại, chẳng hạn như Mon-Wed, nhưng sẽ trở nên khó khăn khi một mẫu bị phá vỡ, ví dụ như vào ngày lễ.
Đây là ví dụ đơn giản ở trên sẽ như thế nào:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
các khoảng thuộc về cùng một Hợp đồng không được trùng nhau.
Tôi cần chuyển đổi dữ liệu từ hệ thống đầu tiên sang định dạng được sử dụng bởi hệ thống thứ hai. Hiện tại tôi đang giải quyết vấn đề này ở phía máy khách trong C # cho Hợp đồng đã cho, nhưng tôi muốn thực hiện điều đó trong T-SQL ở phía máy chủ để xử lý hàng loạt và xuất / nhập giữa các máy chủ. Rất có thể, nó có thể được thực hiện bằng CLR UDF, nhưng ở giai đoạn này tôi không thể sử dụng SQLCLR.
Thách thức ở đây là làm cho danh sách các khoảng thời gian càng ngắn và thân thiện với con người càng tốt.
Ví dụ: lịch trình này:
+-----+------------+------------+---------+--------+
| 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 |
+-----+------------+------------+---------+--------+
nên trở thành thế này:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,không phải cái này:
+------------+------------+------------+----------+----------------------+
| 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, |
+------------+------------+------------+----------+----------------------+
Tôi đã cố gắng áp dụng một gaps-and-islands
cách tiếp cận cho vấn đề này. Tôi đã cố gắng làm điều đó trong hai lần. Trong lần đầu tiên tôi tìm thấy những hòn đảo của những ngày đơn giản liên tiếp, tức là sự kết thúc của hòn đảo là bất kỳ khoảng cách nào trong chuỗi ngày, có thể là cuối tuần, ngày lễ hoặc một cái gì đó khác. Đối với mỗi hòn đảo được tìm thấy như vậy, tôi xây dựng một danh sách riêng biệt được phân tách bằng dấu phẩy WeekDays
. Trong lần vượt qua thứ hai, nhóm tôi đã tìm thấy các hòn đảo xa hơn bằng cách nhìn vào khoảng trống trong chuỗi số tuần hoặc thay đổi trong WeekDays
.
Với cách tiếp cận này, mỗi tuần một phần kết thúc như một khoảng thời gian thêm như được hiển thị ở trên, bởi vì mặc dù số tuần là liên tiếp, sự WeekDays
thay đổi. Ngoài ra, có thể có những khoảng trống đều đặn trong vòng một tuần (xem ContractID=3
trong dữ liệu mẫu, chỉ có dữ liệu cho Mon,Wed,Fri,
) và phương pháp này sẽ tạo ra các khoảng riêng cho mỗi ngày trong lịch trình như vậy. Về mặt sáng sủa, nó tạo ra một khoảng thời gian nếu lịch trình không có bất kỳ khoảng trống nào (xem ContractID=7
trong dữ liệu mẫu bao gồm các ngày cuối tuần) và trong trường hợp đó, không có vấn đề gì nếu tuần bắt đầu hoặc cuối tuần là một phần.
Vui lòng xem các ví dụ khác trong kịch bản bên dưới để hiểu rõ hơn về những gì tôi đang theo dõi. Bạn có thể thấy rằng khá thường xuyên cuối tuần được loại trừ, nhưng bất kỳ ngày nào khác trong tuần cũng có thể được loại trừ. Trong ví dụ 3 chỉ Mon
, Wed
và Fri
là một phần của lịch trình. Bên cạnh đó, cuối tuần có thể được bao gồm, như trong ví dụ 7. Giải pháp nên đối xử bình đẳng với tất cả các ngày trong tuần. Bất kỳ ngày nào trong tuần có thể được bao gồm hoặc loại trừ khỏi lịch trình.
Để xác minh rằng danh sách các khoảng thời gian được tạo mô tả chính xác lịch biểu đã cho, bạn có thể sử dụng mã giả sau đây:
- lặp qua tất cả các khoảng
- cho mỗi vòng lặp thông qua tất cả các ngày theo lịch giữa ngày bắt đầu và ngày kết thúc (bao gồm).
- cho mỗi ngày kiểm tra nếu ngày trong tuần được liệt kê trong
WeekDays
. Nếu có, thì ngày này được bao gồm trong lịch trình.
Hy vọng rằng, điều này làm rõ trong trường hợp nào một khoảng mới nên được tạo ra. Trong các ví dụ 4 và 5 một Thứ Hai ( 2016-05-09
) được xóa khỏi giữa lịch biểu và lịch biểu đó không thể được biểu thị bằng một khoảng duy nhất. Trong ví dụ 6 có một khoảng cách dài trong lịch trình, vì vậy cần có hai khoảng thời gian.
Các khoảng thể hiện các mẫu hàng tuần trong lịch biểu và khi một mẫu bị gián đoạn / thay đổi, khoảng thời gian mới phải được thêm vào. Trong ví dụ 11 ba tuần đầu tiên có một mẫu Tue
, sau đó mẫu này thay đổi thành Thu
. Kết quả là chúng ta cần hai khoảng để mô tả lịch trình như vậy.
Hiện tại tôi đang sử dụng SQL Server 2008, vì vậy giải pháp sẽ hoạt động trong phiên bản này. Nếu một giải pháp cho SQL Server 2008 có thể được đơn giản hóa / cải thiện bằng cách sử dụng các tính năng từ các phiên bản mới hơn, thì đó cũng là một phần thưởng, vui lòng hiển thị nó.
Tôi có một Calendar
bảng (danh sách ngày) và Numbers
bảng (danh sách các số nguyên bắt đầu từ 1), vì vậy có thể sử dụng chúng nếu cần. Cũng có thể tạo các bảng tạm thời và có một số truy vấn xử lý dữ liệu theo nhiều giai đoạn. Tuy nhiên, số lượng các giai đoạn trong một thuật toán phải được sửa, các con trỏ và WHILE
các vòng lặp rõ ràng không ổn.
Kịch bản cho dữ liệu mẫu và kết quả mong đợi
-- @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;
So sánh câu trả lời
Bảng thực @Src
có 403,555
các hàng với 15,857
sự khác biệt ContractIDs
. Tất cả các câu trả lời tạo ra kết quả chính xác (ít nhất là cho dữ liệu của tôi) và tất cả chúng đều nhanh chóng hợp lý, nhưng chúng khác nhau về sự tối ưu. Càng ít khoảng thời gian tạo ra, tốt hơn. Tôi bao gồm thời gian chạy chỉ vì tò mò. Trọng tâm chính là kết quả chính xác và tối ưu, không phải tốc độ (trừ khi mất quá nhiều thời gian - tôi đã dừng truy vấn không đệ quy của Ziggy Crueltyfree Zeitgeister sau 10 phút).
+--------------------------------------------------------+-----------+---------+
| 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
). Hai tuần đầu tiên của lịch trình chỉ có Tue
, vì vậy bạn không thể có WeekDays=Tue,Thu,
trong những tuần này. Hai tuần cuối cùng của lịch trình chỉ có Thu
, vì vậy bạn một lần nữa không thể có WeekDays=Tue,Thu,
trong những tuần này. Giải pháp tối ưu cho nó sẽ là ba hàng: chỉ Tue
trong hai tuần đầu tiên, sau đó Tue,Thu,
đến tuần thứ ba có cả hai Tue
và Thu
sau đó chỉ Thu
trong hai tuần qua.
ContractID
thay đổi, nếu khoảng vượt quá 7 ngày và ngày tuần mới chưa được nhìn thấy trước đây, nếu có một khoảng trống trong danh sách các ngày theo lịch trình.
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
trong @Dst là một hàng vớiTue, Thu,
?