Lịch trình nhóm hàng ngày vào [Ngày bắt đầu; Ngày kết thúc] khoảng thời gian với danh sách các ngày trong tuần


18

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 |
+----+------------+------------+---------+--------+

IDlà 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-islandscá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ự WeekDaysthay đổi. Ngoài ra, có thể có những khoảng trống đều đặn trong vòng một tuần (xem ContractID=3trong 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=7trong 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, WedFrilà 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 Calendarbảng (danh sách ngày) và Numbersbả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à WHILEcá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 @Src403,555các hàng với 15,857sự 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                                                 |           |         |
+--------------------------------------------------------+-----------+---------+

Không nên (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ới Tue, Thu,?
Kin Shah

@Kin, Ví dụ 11 phải có (ít nhất) hai khoảng (hai hàng trong @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ỉ Tuetrong hai tuần đầu tiên, sau đó Tue,Thu,đến tuần thứ ba có cả hai TueThusau đó chỉ Thutrong hai tuần qua.
Vladimir Baranov

1
Bạn có thể giải thích thuật toán theo đó hợp đồng 11 được "tối ưu hóa" chia thành hai khoảng. Bạn đã đạt được điều này trong ứng dụng C #? Làm sao?
Michael Green

@MichaelGreen, xin lỗi tôi không thể trả lời sớm hơn. Có, mã C # chia Hợp đồng 11 thành hai khoảng. Thuật toán thô: Tôi lặp qua các ngày đã lên lịch, từng ngày một, lưu ý đến những ngày trong tuần tôi gặp phải kể từ khi bắt đầu khoảng và xác định xem tôi có nên bắt đầu một khoảng mới không: nếu ContractIDthay đổ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.
Vladimir Baranov

@MichaelGreen, tôi đã chuyển đổi mã C # của mình thành thuật toán dựa trên con trỏ, chỉ để xem cách so sánh với các giải pháp khác trên dữ liệu thực. Tôi đã thêm mã nguồn vào câu trả lời của mình và kết quả vào bảng tóm tắt trong câu hỏi.
Vladimir Baranov

Câu trả lời:


6

Cái này sử dụng CTE đệ quy. Kết quả của nó giống hệt với ví dụ trong câu hỏi . Đó là một cơn ác mộng khi đưa ra ... Mã bao gồm các bình luận để dễ dàng thông qua logic phức tạp của nó.

SET DATEFIRST 1 -- Make Monday weekday=1

DECLARE @Ranked TABLE (RowID int NOT NULL IDENTITY PRIMARY KEY,                   -- Incremental uninterrupted sequence in the right order
                       ID int NOT NULL UNIQUE, ContractID int NOT NULL, dt date,  -- Original relevant values (ID is not really necessary)
                       WeekNo int NOT NULL, dowBit int NOT NULL);                 -- Useful to find gaps in days or weeks
INSERT INTO @Ranked
SELECT ID, ContractID, dt,
       DATEDIFF(WEEK, '1900-01-01', DATEADD(DAY, 1-DATEPART(dw, dt), dt)) AS WeekNo,
       POWER(2, DATEPART(dw, dt)-1) AS dowBit
FROM @Src
ORDER BY ContractID, WeekNo, dowBit

/*
Each evaluated date makes part of the carried sequence if:
  - this is not a new contract, and
    - sequence started this week, or
    - same day last week was part of the sequence, or
    - sequence started last week and today is a lower day than the accumulated weekdays list
  - and there are no sequence gaps since previous day
(otherwise it does not make part of the old sequence, so it starts a new one) */

DECLARE @RankedRanges TABLE (RowID int NOT NULL PRIMARY KEY, WeekDays int NOT NULL, StartRowID int NULL);

WITH WeeksCTE AS -- Needed for building the sequence gradually, and comparing the carried sequence (and previous day) with a current evaluated day
( 
    SELECT RowID, ContractID, dowBit, WeekNo, RowID AS StartRowID, WeekNo AS StartWN, dowBit AS WeekDays, dowBit AS StartWeekDays
    FROM @Ranked
    WHERE RowID = 1 
    UNION ALL
    SELECT RowID, ContractID, dowBit, WeekNo, StartRowID,
           CASE WHEN StartRowID IS NULL THEN StartWN ELSE WeekNo END AS WeekNo,
           CASE WHEN StartRowID IS NULL THEN WeekDays | dowBit ELSE dowBit END AS WeekDays,
           CASE WHEN StartRowID IS NOT NULL THEN dowBit WHEN WeekNo = StartWN THEN StartWeekDays | dowBit ELSE StartWeekDays END AS StartWeekDays
    FROM (
        SELECT w.*, pre.StartWN, pre.WeekDays, pre.StartWeekDays,
               CASE WHEN w.ContractID <> pre.ContractID OR     -- New contract always break the sequence
                         NOT (w.WeekNo = pre.StartWN OR        -- Same week as a new sequence always keeps the sequence
                              w.dowBit & pre.WeekDays > 0 OR   -- Days in the sequence keep the sequence (provided there are no gaps, checked later)
                              (w.WeekNo = pre.StartWN+1 AND (w.dowBit-1) & pre.StartWeekDays = 0)) OR -- Days in the second week when less than a week passed since the sequence started remain in sequence
                         (w.WeekNo > pre.StartWN AND -- look for gap after initial week
                          w.WeekNo > pre.WeekNo+1 OR -- look for full-week gaps
                          (w.WeekNo = pre.WeekNo AND                            -- when same week as previous day,
                           ((w.dowBit-1) ^ (pre.dowBit*2-1)) & pre.WeekDays > 0 -- days between this and previous weekdays, compared to current series
                          ) OR
                          (w.WeekNo > pre.WeekNo AND                                   -- when following week of previous day,
                           ((-1 ^ (pre.dowBit*2-1)) | (w.dowBit-1)) & pre.WeekDays > 0 -- days between this and previous weekdays, compared to current series
                          )) THEN w.RowID END AS StartRowID
        FROM WeeksCTE pre
        JOIN @Ranked w ON (w.RowID = pre.RowID + 1)
        ) w
) 
INSERT INTO @RankedRanges -- days sequence and starting point of each sequence
SELECT RowID, WeekDays, StartRowID
--SELECT *
FROM WeeksCTE
OPTION (MAXRECURSION 0)

--SELECT * FROM @RankedRanges

DECLARE @Ranges TABLE (RowNo int NOT NULL IDENTITY PRIMARY KEY, RowID int NOT NULL);

INSERT INTO @Ranges       -- @RankedRanges filtered only by start of each range, with numbered rows to easily find the end of each range
SELECT StartRowID
FROM @RankedRanges
WHERE StartRowID IS NOT NULL
ORDER BY 1

-- Final result putting everything together
SELECT rs.ContractID, rs.dt AS StartDT, re.dt AS EndDT, re.RowID-rs.RowID+1 AS DayCount,
       CASE WHEN rr.WeekDays & 64 > 0 THEN 'Sun,' ELSE '' END +
       CASE WHEN rr.WeekDays & 1 > 0 THEN 'Mon,' ELSE '' END +
       CASE WHEN rr.WeekDays & 2 > 0 THEN 'Tue,' ELSE '' END +
       CASE WHEN rr.WeekDays & 4 > 0 THEN 'Wed,' ELSE '' END +
       CASE WHEN rr.WeekDays & 8 > 0 THEN 'Thu,' ELSE '' END +
       CASE WHEN rr.WeekDays & 16 > 0 THEN 'Fri,' ELSE '' END +
       CASE WHEN rr.WeekDays & 32 > 0 THEN 'Sat,' ELSE '' END AS WeekDays
FROM (
    SELECT r.RowID AS StartRowID, COALESCE(pos.RowID-1, (SELECT MAX(RowID) FROM @Ranked)) AS EndRowID
    FROM @Ranges r
    LEFT JOIN @Ranges pos ON (pos.RowNo = r.RowNo + 1)
    ) g
JOIN @Ranked rs ON (rs.RowID = g.StartRowID)
JOIN @Ranked re ON (re.RowID = g.EndRowID)
JOIN @RankedRanges rr ON (rr.RowID = re.RowID)


Một chiến lược khác

Cái này phải nhanh hơn đáng kể so với cái trước bởi vì nó không dựa vào CTE đệ quy hạn chế chậm trong SQL Server 2008, mặc dù nó thực hiện ít nhiều cùng một chiến lược.

Có một WHILEvòng lặp (tôi không thể nghĩ ra cách nào để tránh nó), nhưng sẽ giảm số lần lặp (số lượng trình tự cao nhất (trừ một) trong bất kỳ hợp đồng cụ thể nào).

Đây là một chiến lược đơn giản và có thể được sử dụng cho các chuỗi ngắn hơn hoặc dài hơn một tuần (thay thế bất kỳ sự xuất hiện nào của hằng số 7 cho bất kỳ số nào khác và được dowBittính từ MODULUS x DayNothay vì DATEPART(wk)) và lên đến 32.

SET DATEFIRST 1 -- Make Monday weekday=1

-- Get the minimum information needed to calculate sequences
DECLARE @Days TABLE (ContractID int NOT NULL, dt date, DayNo int NOT NULL, dowBit int NOT NULL, PRIMARY KEY (ContractID, DayNo));
INSERT INTO @Days
SELECT ContractID, dt, CAST(CAST(dt AS datetime) AS int) AS DayNo, POWER(2, DATEPART(dw, dt)-1) AS dowBit
FROM @Src

DECLARE @RangeStartFirstPass TABLE (ContractID int NOT NULL, DayNo int NOT NULL, PRIMARY KEY (ContractID, DayNo))

-- Calculate, from the above list, which days are not present in the previous 7
INSERT INTO @RangeStartFirstPass
SELECT r.ContractID, r.DayNo
FROM @Days r
LEFT JOIN @Days pr ON (pr.ContractID = r.ContractID AND pr.DayNo BETWEEN r.DayNo-7 AND r.DayNo-1) -- Last 7 days
GROUP BY r.ContractID, r.DayNo, r.dowBit
HAVING r.dowBit & COALESCE(SUM(pr.dowBit), 0) = 0

-- Update the previous list with all days that occur right after a missing day
INSERT INTO @RangeStartFirstPass
SELECT *
FROM (
    SELECT DISTINCT ContractID, (SELECT MIN(DayNo) FROM @Days WHERE ContractID = d.ContractID AND DayNo > d.DayNo + 7) AS DayNo
    FROM @Days d
    WHERE NOT EXISTS (SELECT 1 FROM @Days WHERE ContractID = d.ContractID AND DayNo = d.DayNo + 7)
    ) d
WHERE DayNo IS NOT NULL AND
      NOT EXISTS (SELECT 1 FROM @RangeStartFirstPass WHERE ContractID = d.ContractID AND DayNo = d.DayNo)

DECLARE @RangeStart TABLE (ContractID int NOT NULL, DayNo int NOT NULL, PRIMARY KEY (ContractID, DayNo));

-- Fetch the first sequence for each contract
INSERT INTO @RangeStart
SELECT ContractID, MIN(DayNo)
FROM @RangeStartFirstPass
GROUP BY ContractID

-- Add to the list above the next sequence for each contract, until all are added
-- (ensure no sequence is added with less than 7 days)
WHILE @@ROWCOUNT > 0
  INSERT INTO @RangeStart
  SELECT f.ContractID, MIN(f.DayNo)
  FROM (SELECT ContractID, MAX(DayNo) AS DayNo FROM @RangeStart GROUP BY ContractID) s
  JOIN @RangeStartFirstPass f ON (f.ContractID = s.ContractID AND f.DayNo > s.DayNo + 7)
  GROUP BY f.ContractID

-- Summarise results
SELECT ContractID, StartDT, EndDT, DayCount,
       CASE WHEN WeekDays & 64 > 0 THEN 'Sun,' ELSE '' END +
       CASE WHEN WeekDays & 1 > 0 THEN 'Mon,' ELSE '' END +
       CASE WHEN WeekDays & 2 > 0 THEN 'Tue,' ELSE '' END +
       CASE WHEN WeekDays & 4 > 0 THEN 'Wed,' ELSE '' END +
       CASE WHEN WeekDays & 8 > 0 THEN 'Thu,' ELSE '' END +
       CASE WHEN WeekDays & 16 > 0 THEN 'Fri,' ELSE '' END +
       CASE WHEN WeekDays & 32 > 0 THEN 'Sat,' ELSE '' END AS WeekDays
FROM (
    SELECT r.ContractID,
           MIN(d.dt) AS StartDT,
           MAX(d.dt) AS EndDT,
           COUNT(*) AS DayCount,
           SUM(DISTINCT d.dowBit) AS WeekDays
    FROM (SELECT *, COALESCE((SELECT MIN(DayNo) FROM @RangeStart WHERE ContractID = rs.ContractID AND DayNo > rs.DayNo), 999999) AS DayEnd FROM @RangeStart rs) r
    JOIN @Days d ON (d.ContractID = r.ContractID AND d.DayNo BETWEEN r.DayNo AND r.DayEnd-1)
    GROUP BY r.ContractID, r.DayNo
    ) d
ORDER BY ContractID, StartDT

@VladimirBaranov Tôi đã thêm một chiến lược mới, sẽ nhanh hơn nhiều. Hãy cho tôi biết làm thế nào nó đánh giá với dữ liệu thực của bạn!
Zitgy Crueltyfree Zeitgeister

2
@ZiggyCrueltyfreeZeitgeister, tôi đã kiểm tra giải pháp cuối cùng của bạn và thêm nó vào danh sách tất cả các câu trả lời trong câu hỏi. Nó tạo ra kết quả chính xác và cùng số lượng khoảng thời gian như CTE đệ quy và tốc độ của nó cũng rất gần. Như tôi đã nói, tốc độ không quan trọng miễn là hợp lý. 1 giây hoặc 10 giây không thực sự quan trọng đối với tôi.
Vladimir Baranov

Các câu trả lời khác cũng rất hay và hữu ích, và tôi ước mình có thể trao phần thưởng cho nhiều hơn một câu trả lời. Tôi đã chọn câu trả lời này, vì tại thời điểm khi tôi bắt đầu tiền thưởng, tôi đã không nghĩ về CTE đệ quy và câu trả lời này là lần đầu tiên đề xuất nó và có một giải pháp hiệu quả. Nói một cách chính xác, CTE đệ quy không phải là một giải pháp dựa trên tập hợp, nhưng nó cho kết quả tối ưu và nhanh chóng hợp lý. Một câu trả lời của @GeoffPatterson là tuyệt vời, nhưng cho kết quả ít tối ưu hơn và, thẳng thắn mà nói, cách quá phức tạp.
Vladimir Baranov

5

Không chính xác những gì bạn đang tìm kiếm nhưng có lẽ có thể được bạn quan tâm.

Truy vấn tạo ra các tuần với một chuỗi được phân tách bằng dấu phẩy cho các ngày được sử dụng trong mỗi tuần. Sau đó, nó tìm thấy các đảo trong những tuần liên tiếp sử dụng mô hình tương tự Weekdays.

with Weeks as
(
  select T.*,
         row_number() over(partition by T.ContractID, T.WeekDays order by T.WeekNumber) as rn
  from (
       select S1.ContractID,
              min(S1.dt) as StartDT,
              max(S1.dt) as EndDT,
              datediff(day, 0, S1.dt) / 7 as WeekNumber, -- Number of weeks since '1900-01-01 (a monday)'
              count(*) as DayCount,
              stuff((
                    select ','+S2.dowChar
                    from @Src as S2
                    where S2.ContractID = S1.ContractID and
                          S2.dt between min(S1.dt) and max(S1.dt)
                    order by S2.dt
                    for xml path('')
                    ), 1, 1, '') as WeekDays
       from @Src as S1
       group by S1.ContractID, 
                datediff(day, 0, S1.dt) / 7
       ) as T
)
select W.ContractID,
       min(W.StartDT) as StartDT,
       max(W.EndDT) as EndDT,
       count(*) * W.DayCount as DayCount,
       W.WeekDays
from Weeks as W
group by W.ContractID,
         W.WeekDays,
         W.DayCount,
         W.rn - W.WeekNumber
order by W.ContractID,
         min(W.WeekNumber);

Kết quả:

ContractID  StartDT    EndDT      DayCount    WeekDays
----------- ---------- ---------- ----------- -----------------------------
1           2016-05-02 2016-05-13 10          Mon,Tue,Wed,Thu,Fri
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
3           2016-05-02 2016-05-13 6           Mon,Wed,Fri
3           2016-05-16 2016-05-16 1           Mon
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-13 4           Tue,Wed,Thu,Fri
5           2016-05-16 2016-05-20 5           Mon,Tue,Wed,Thu,Fri
6           2016-05-05 2016-05-06 2           Thu,Fri
6           2016-05-09 2016-05-13 5           Mon,Tue,Wed,Thu,Fri
6           2016-05-16 2016-05-17 2           Mon,Tue
6           2016-06-06 2016-06-17 10          Mon,Tue,Wed,Thu,Fri
7           2016-05-02 2016-05-08 7           Mon,Tue,Wed,Thu,Fri,Sat,Sun
7           2016-05-09 2016-05-13 5           Mon,Tue,Wed,Thu,Fri
8           2016-04-30 2016-05-01 2           Sat,Sun
8           2016-05-02 2016-05-08 7           Mon,Tue,Wed,Thu,Fri,Sat,Sun
8           2016-05-09 2016-05-14 6           Mon,Tue,Wed,Thu,Fri,Sat
9           2016-05-02 2016-05-11 6           Mon,Tue,Wed
9           2016-05-16 2016-05-17 2           Mon,Tue
10          2016-05-05 2016-05-22 12          Thu,Fri,Sat,Sun
11          2016-05-03 2016-05-10 2           Tue
11          2016-05-17 2016-05-19 2           Tue,Thu
11          2016-05-26 2016-06-02 2           Thu

ContractID = 2cho thấy sự khác biệt trong kết quả được so sánh với những gì bạn muốn. Tuần đầu tiên và tuần trước sẽ được coi là giai đoạn riêng biệt kể từ khiWeekDays khác nhau.


Tôi đã có ý tưởng này, nhưng không có cơ hội để thử nó. Cảm ơn bạn đã cung cấp một truy vấn làm việc. Tôi thích cách nó mang lại một kết quả có cấu trúc hơn. Khi nhóm dữ liệu vào các tuần, mặt trái bị giảm tính linh hoạt (theo cách tiếp cận khoảng cách đơn giản hàng ngày, ví dụ 7 và 8 sẽ được thu gọn thành một khoảng), nhưng đó là mặt sáng cùng một lúc - chúng tôi giảm độ phức tạp của vấn đề. Vì vậy, vấn đề lớn nhất với phương pháp này là một phần tuần khi bắt đầu và kết thúc lịch trình. Một phần tuần như vậy tạo ra một khoảng thời gian thêm ...
Vladimir Baranov

Bạn có thể nghĩ ra một cách để chắp thêm / nhóm / hợp nhất những tuần này vào lịch trình chính không? Tôi chỉ có ý tưởng rất mơ hồ ở giai đoạn này. Nếu chúng ta tìm ra cách hợp nhất một phần chính xác thì kết quả cuối cùng sẽ rất gần với tối ưu.
Vladimir Baranov

@VladimirBaranov Không chắc điều đó sẽ được thực hiện như thế nào. Tôi sẽ cập nhật câu trả lời nếu có gì đó xuất hiện trong đầu.
Mikael Eriksson

Ý tưởng mơ hồ của tôi là thế này: chỉ có 7 ngày trong một tuần, WeekDayssố 7 bit cũng vậy. Chỉ có 128 kết hợp. Chỉ có 128 * 128 = 16384 cặp có thể. Xây dựng bảng tạm thời với tất cả các cặp có thể, sau đó tìm ra thuật toán dựa trên tập hợp để đánh dấu các cặp có thể được hợp nhất: một mẫu trong một tuần được "bao phủ" bởi mẫu của tuần tiếp theo. Tự tham gia kết quả hàng tuần hiện tại (vì không có LAGtrong năm 2008) và sử dụng bảng tạm thời đó để quyết định cặp nào sẽ hợp nhất ... Không chắc ý tưởng này có công đức gì không.
Vladimir Baranov

5

Tôi đã kết thúc với một cách tiếp cận mang lại giải pháp tối ưu trong trường hợp này và tôi nghĩ rằng sẽ làm tốt nói chung. Tuy nhiên, giải pháp này khá dài, vì vậy sẽ rất thú vị để xem liệu ai đó có cách tiếp cận khác ngắn gọn hơn.

Đây là một kịch bản có chứa các giải pháp đầy đủ .

Và đây là một phác thảo của thuật toán:

  • Xoay tập dữ liệu để có một hàng duy nhất đại diện cho mỗi tuần
  • Tính toán các đảo trong vài tuần ContractId
  • Hợp nhất bất kỳ tuần liền kề nào trong cùng ContractIdvà có cùngWeekDays
  • Trong bất kỳ tuần nào (chưa được hợp nhất) trong đó nhóm trước đó nằm trên cùng một hòn đảo và WeekDayscủa tuần duy nhất khớp với một tập hợp con hàng đầu của WeekDaysnhóm trước đó, hãy hợp nhất vào nhóm trước đó
  • Trong bất kỳ tuần nào (chưa được hợp nhất) trong đó nhóm tiếp theo nằm trên cùng một hòn đảo và WeekDaystrong một tuần khớp với một tập hợp con của WeekDaysnhóm tiếp theo, hãy hợp nhất vào nhóm tiếp theo đó
  • Đối với bất kỳ hai tuần liền kề nào trên cùng một hòn đảo không được hợp nhất, hãy hợp nhất chúng lại với nhau nếu cả hai tuần có thể được kết hợp (ví dụ: "Thứ hai, Thứ ba, Thứ tư, Thu" và "Thứ tư, Thu, Thứ bảy" )
  • Đối với bất kỳ tuần duy nhất còn lại (chưa được hợp nhất), nếu có thể, hãy chia tuần thành hai phần và hợp nhất cả hai phần, phần đầu tiên vào nhóm trước đó trên cùng một hòn đảo và phần thứ hai thành nhóm sau trên cùng một hòn đảo

Cảm ơn bạn đã đi rất lâu để tạo ra giải pháp làm việc. Đó là một chút áp đảo, phải trung thực. Tôi nghi ngờ rằng sẽ không đơn giản để hợp nhất một phần tuần, nhưng tôi không thể hy vọng nó sẽ quá phức tạp. Tôi vẫn có hy vọng rằng nó có thể được thực hiện dễ dàng hơn, nhưng tôi không có ý tưởng cụ thể.
Vladimir Baranov

Kiểm tra nhanh xác nhận rằng nó tạo ra kết quả mong đợi cho dữ liệu mẫu, điều này thật tuyệt, nhưng, tôi nhận thấy rằng một số lịch trình nhất định không được xử lý theo cách tối ưu. Ví dụ đơn giản nhất : (1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),. Nó có thể được biểu diễn dưới dạng một khoảng, nhưng giải pháp của bạn tạo ra hai. Tôi thừa nhận, ví dụ này không có trong dữ liệu mẫu và nó không quan trọng. Tôi sẽ cố gắng chạy giải pháp của bạn trên dữ liệu thực.
Vladimir Baranov

Tôi đánh giá cao câu trả lời của bạn. Vào thời điểm khi tôi bắt đầu tiền thưởng, tôi đã không nghĩ về CTE đệ quy và Ziggy Crueltyfree Zeitgeister là người đầu tiên đề xuất nó và đưa ra một giải pháp hiệu quả. Nói một cách chính xác, CTE đệ quy không phải là một giải pháp dựa trên tập hợp, nhưng nó cho kết quả tối ưu, khá phức tạp và nhanh chóng hợp lý. Câu trả lời của bạn là dựa trên thiết lập, nhưng hóa ra quá phức tạp, đến mức không thực tế. Tôi ước tôi có thể chia tiền thưởng, nhưng tiếc là nó không được phép.
Vladimir Baranov

@VladimirBaranov Không có vấn đề gì, tiền thưởng là 100% của bạn để sử dụng như bạn muốn. Lý do tôi thích các câu hỏi về tiền thưởng là vì người đặt câu hỏi thường hấp dẫn hơn nhiều so với một câu hỏi thông thường. Đừng quan tâm quá nhiều về các điểm. Tôi hoàn toàn đồng ý rằng giải pháp này không phải là giải pháp tôi sẽ sử dụng trong mã sản xuất của mình; đó là một cuộc khám phá về một ý tưởng tiềm năng, nhưng cuối cùng lại khá phức tạp.
Geoff Patterson

3

Tôi không thể hiểu logic đằng sau việc nhóm các tuần với các khoảng trống hoặc tuần với các ngày cuối tuần (ví dụ: khi có hai tuần liên tiếp với một ngày cuối tuần, cuối tuần sẽ đi vào tuần nào?).

Truy vấn sau đây tạo ra đầu ra mong muốn ngoại trừ việc nó chỉ nhóm các ngày trong tuần liên tiếp và nhóm tuần Sun-Sat (chứ không phải Mon-Sun). Mặc dù không chính xác những gì bạn muốn, nhưng điều này có thể cung cấp một số manh mối cho một chiến lược khác. Nhóm ngày đến từ đây . Các hàm cửa sổ được sử dụng sẽ hoạt động với SQLServer 2008, nhưng tôi không có phiên bản đó để kiểm tra nếu nó thực sự hoạt động.

WITH 
  mysrc AS (
    SELECT *, RANK() OVER (PARTITION BY ContractID ORDER BY DT) AS rank
    FROM @Src
    ),
  prepos AS (
    SELECT s.*, pos.ID AS posid
    FROM mysrc s
    LEFT JOIN mysrc pos ON (pos.ContractID = s.ContractID AND pos.rank = s.rank+1 AND (pos.DowInt = s.DowInt+1 OR pos.DowInt = 2 AND s.DowInt=6))
    ),
  grped AS (
    SELECT TOP 100 *, (SELECT COUNT(CASE WHEN posid IS NULL THEN 1 END) FROM prepos WHERE contractid = p.contractid AND rank < p.rank) as grp
    FROM prepos p
    ORDER BY ContractID, DT
    )
SELECT ContractID, min(dt) AS StartDT, max(dt) AS EndDT, count(*) AS DayCount,
       STUFF( (SELECT ', ' + dowchar
               FROM (
                 SELECT TOP 100 dowint, dowchar 
                 FROM grped 
                 WHERE ContractID = g.ContractID AND grp = g.grp 
                 GROUP BY dowint, dowchar 
                 ORDER BY 1
                 ) a 
               FOR XML PATH(''), TYPE).value('.','varchar(max)'), 1, 2, '') AS WeekDays
FROM grped g
GROUP BY ContractID, grp
ORDER BY 1, 2

Kết quả

+------------+------------+------------+----------+-----------------------------------+
| ContractID | StartDT    | EndDT      | DayCount | WeekDays                          |
+------------+------------+------------+----------+-----------------------------------+
| 1          | 2/05/2016  | 13/05/2016 | 10       | Mon, Tue, Wed, Thu, Fri           |
| 2          | 5/05/2016  | 17/05/2016 | 9        | Mon, Tue, Wed, Thu, Fri           |
| 3          | 2/05/2016  | 2/05/2016  | 1        | Mon                               |
| 3          | 4/05/2016  | 4/05/2016  | 1        | Wed                               |
| 3          | 6/05/2016  | 9/05/2016  | 2        | Mon, Fri                          |
| 3          | 11/05/2016 | 11/05/2016 | 1        | Wed                               |
| 3          | 13/05/2016 | 16/05/2016 | 2        | Mon, Fri                          |
| 4          | 2/05/2016  | 6/05/2016  | 5        | Mon, Tue, Wed, Thu, Fri           |
| 4          | 10/05/2016 | 13/05/2016 | 4        | Tue, Wed, Thu, Fri                |
| 5          | 2/05/2016  | 6/05/2016  | 5        | Mon, Tue, Wed, Thu, Fri           |
| 5          | 10/05/2016 | 20/05/2016 | 9        | Mon, Tue, Wed, Thu, Fri           |
| 6          | 5/05/2016  | 17/05/2016 | 9        | Mon, Tue, Wed, Thu, Fri           |
| 6          | 6/06/2016  | 17/06/2016 | 10       | Mon, Tue, Wed, Thu, Fri           |
| 7          | 2/05/2016  | 7/05/2016  | 6        | Mon, Tue, Wed, Thu, Fri, Sat      |
| 7          | 8/05/2016  | 13/05/2016 | 6        | Sun, Mon, Tue, Wed, Thu, Fri      |
| 8          | 30/04/2016 | 30/04/2016 | 1        | Sat                               |
| 8          | 1/05/2016  | 7/05/2016  | 7        | Sun, Mon, Tue, Wed, Thu, Fri, Sat |
| 8          | 8/05/2016  | 14/05/2016 | 7        | Sun, Mon, Tue, Wed, Thu, Fri, Sat |
| 9          | 2/05/2016  | 4/05/2016  | 3        | Mon, Tue, Wed                     |
| 9          | 9/05/2016  | 10/05/2016 | 2        | Mon, Tue                          |
+------------+------------+------------+----------+-----------------------------------+

Các cuộc thảo luận về câu trả lời này đã được chuyển sang trò chuyện .
Paul White nói GoFundMonica

3

Để hoàn thiện, đây là gaps-and-islandscách tiếp cận hai bước mà tôi đã thử trước khi đặt câu hỏi này.

Khi tôi đang kiểm tra nó trên dữ liệu thực, tôi đã tìm thấy một vài trường hợp khi nó tạo ra kết quả không chính xác và đã sửa nó.

Đây là thuật toán:

  • Tạo đảo ngày liên tiếp ( CTE_ContractDays, CTE_DailyRN, CTE_DailyIslands) và tính toán một số tuần cho mỗi ngày bắt đầu và kết thúc của một hòn đảo. Ở đây số tuần được tính toán giả định rằng thứ Hai là ngày đầu tiên trong tuần.
  • Nếu lịch biểu có các ngày không tuần tự trong cùng một tuần (như trong ví dụ 3), giai đoạn trước sẽ tạo ra một số hàng cho cùng một tuần. Hàng nhóm chỉ có một hàng mỗi tuần ( CTE_Weeks).
  • Đối với mỗi hàng từ giai đoạn trước, hãy tạo một danh sách các ngày trong tuần ( CTE_FirstResult) được phân tách bằng dấu phẩy .
  • Lần thứ hai của khoảng cách và đảo đến nhóm tuần liên tiếp với cùng WeekDays( CTE_SecondRN, CTE_Schedules).

Nó xử lý tốt các trường hợp khi không có sự gián đoạn trong các mẫu hàng tuần (1, 7, 8, 10, 12). Nó xử lý các trường hợp tốt khi mẫu có ngày không tuần tự (3).

Nhưng, thật không may, nó tạo ra các khoảng thời gian thêm cho một phần tuần (2, 3, 5, 6, 9, 11).

WITH
CTE_ContractDays
AS
(
    SELECT
         S.ContractID
        ,MIN(S.dt) OVER (PARTITION BY S.ContractID) AS ContractMinDT
        ,S.dt
        ,ROW_NUMBER() OVER (PARTITION BY S.ContractID ORDER BY S.dt) AS rn1
        ,DATEDIFF(day, '2001-01-01', S.dt) AS DayNumber
        ,S.dowChar
        ,S.dowInt
    FROM
        @Src AS S
)
,CTE_DailyRN
AS
(
    SELECT
        DayNumber - rn1 AS WeekGroupNumber
        ,ROW_NUMBER() OVER (
            PARTITION BY
                ContractID
                ,DayNumber - rn1
            ORDER BY dt) AS rn2
        ,ContractID
        ,ContractMinDT
        ,dt
        ,rn1
        ,DayNumber
        ,dowChar
        ,dowInt
    FROM CTE_ContractDays
)
,CTE_DailyIslands
AS
(
    SELECT
        ContractID
        ,ContractMinDT
        ,MIN(dt) AS MinDT
        ,MAX(dt) AS MaxDT
        ,COUNT(*) AS DayCount
        -- '2001-01-01' is Monday
        ,DATEDIFF(day, '2001-01-01', MIN(dt)) / 7 AS WeekNumberMin
        ,DATEDIFF(day, '2001-01-01', MAX(dt)) / 7 AS WeekNumberMax
    FROM CTE_DailyRN
    GROUP BY
        ContractID
        ,rn1-rn2
        ,ContractMinDT
)
,CTE_Weeks
AS
(
    SELECT
        ContractID
        ,ContractMinDT
        ,MIN(MinDT) AS MinDT
        ,MAX(MaxDT) AS MaxDT
        ,SUM(DayCount) AS DayCount
        ,WeekNumberMin
        ,WeekNumberMax
    FROM CTE_DailyIslands
    GROUP BY
        ContractID
        ,ContractMinDT
        ,WeekNumberMin
        ,WeekNumberMax
)
,CTE_FirstResult
AS
(
    SELECT
        ContractID
        ,ContractMinDT
        ,MinDT
        ,MaxDT
        ,DayCount
        ,CA_Data.XML_Value AS DaysOfWeek
        ,WeekNumberMin AS WeekNumber
        ,ROW_NUMBER() OVER(PARTITION BY ContractID ORDER BY MinDT) AS rn1
    FROM
        CTE_Weeks
        CROSS APPLY
        (
            SELECT CAST(CTE_ContractDays.dowChar AS varchar(8000)) + ',' AS dw
            FROM CTE_ContractDays
            WHERE
                    CTE_ContractDays.ContractID = CTE_Weeks.ContractID
                AND CTE_ContractDays.dt >= CTE_Weeks.MinDT
                AND CTE_ContractDays.dt <= CTE_Weeks.MaxDT
            GROUP BY
                CTE_ContractDays.dowChar
                ,CTE_ContractDays.dowInt
            ORDER BY CTE_ContractDays.dowInt
            FOR XML PATH(''), TYPE
        ) AS CA_XML(XML_Value)
        CROSS APPLY
        (
            SELECT CA_XML.XML_Value.value('.', 'VARCHAR(8000)')
        ) AS CA_Data(XML_Value)
)
,CTE_SecondRN
AS
(
    SELECT 
        ContractID
        ,ContractMinDT
        ,MinDT
        ,MaxDT
        ,DayCount
        ,DaysOfWeek
        ,WeekNumber
        ,rn1
        ,WeekNumber - rn1 AS SecondGroupNumber
        ,ROW_NUMBER() OVER (
            PARTITION BY
                ContractID
                ,DaysOfWeek
                ,DayCount
                ,WeekNumber - rn1
            ORDER BY MinDT) AS rn2
    FROM CTE_FirstResult
)
,CTE_Schedules
AS
(
    SELECT
        ContractID
        ,MIN(MinDT) AS StartDT
        ,MAX(MaxDT) AS EndDT
        ,SUM(DayCount) AS DayCount
        ,DaysOfWeek
    FROM CTE_SecondRN
    GROUP BY
        ContractID
        ,DaysOfWeek
        ,rn1-rn2
)
SELECT
    ContractID
    ,StartDT
    ,EndDT
    ,DayCount
    ,DaysOfWeek AS WeekDays
FROM CTE_Schedules
ORDER BY
    ContractID
    ,StartDT
;

Kết quả

+------------+------------+------------+----------+------------------------------+
| ContractID |  StartDT   |   EndDT    | DayCount |           WeekDays           |
+------------+------------+------------+----------+------------------------------+
|          1 | 2016-05-02 | 2016-05-13 |       10 | Mon,Tue,Wed,Thu,Fri,         |
|          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,                     |
|          3 | 2016-05-02 | 2016-05-13 |        6 | Mon,Wed,Fri,                 |
|          3 | 2016-05-16 | 2016-05-16 |        1 | Mon,                         |
|          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-13 |        4 | Tue,Wed,Thu,Fri,             |
|          5 | 2016-05-16 | 2016-05-20 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          6 | 2016-05-05 | 2016-05-06 |        2 | Thu,Fri,                     |
|          6 | 2016-05-09 | 2016-05-13 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          6 | 2016-05-16 | 2016-05-17 |        2 | Mon,Tue,                     |
|          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-11 |        6 | Mon,Tue,Wed,                 |
|          9 | 2016-05-16 | 2016-05-17 |        2 | Mon,Tue,                     |
|         10 | 2016-05-05 | 2016-05-22 |       12 | Sun,Thu,Fri,Sat,             |
|         11 | 2016-05-03 | 2016-05-10 |        2 | Tue,                         |
|         11 | 2016-05-17 | 2016-05-19 |        2 | Tue,Thu,                     |
|         11 | 2016-05-26 | 2016-06-02 |        2 | 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,         |
+------------+------------+------------+----------+------------------------------+

Giải pháp dựa trên con trỏ

Tôi đã chuyển đổi mã C # của mình thành thuật toán dựa trên con trỏ, để xem nó so sánh với các giải pháp khác trên dữ liệu thực như thế nào. Nó xác nhận rằng nó chậm hơn nhiều so với các cách tiếp cận dựa trên tập hợp hoặc đệ quy khác, nhưng nó tạo ra một kết quả tối ưu.

CREATE TABLE #Dst_V2 (ContractID bigint, StartDT date, EndDT date, DayCount int, WeekDays varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS);

SET NOCOUNT ON;

DECLARE @VarOldDateFirst int = @@DATEFIRST;
SET DATEFIRST 7;

DECLARE @iFS int;
DECLARE @VarCursor CURSOR;
SET @VarCursor = CURSOR FAST_FORWARD
FOR
    SELECT
        ContractID
        ,dt
        ,dowChar
        ,dowInt
    FROM #Src AS S
    ;

OPEN @VarCursor;

DECLARE @CurrContractID bigint = 0;
DECLARE @Currdt date;
DECLARE @CurrdowChar char(3);
DECLARE @CurrdowInt int;


DECLARE @VarCreateNewInterval bit = 0;
DECLARE @VarTempDT date;
DECLARE @VarTempdowInt int;

DECLARE @LastContractID bigint = 0;
DECLARE @LastStartDT date;
DECLARE @LastEndDT date;
DECLARE @LastDayCount int = 0;
DECLARE @LastWeekDays varchar(255);
DECLARE @LastMonCount int;
DECLARE @LastTueCount int;
DECLARE @LastWedCount int;
DECLARE @LastThuCount int;
DECLARE @LastFriCount int;
DECLARE @LastSatCount int;
DECLARE @LastSunCount int;


FETCH NEXT FROM @VarCursor INTO @CurrContractID, @Currdt, @CurrdowChar, @CurrdowInt;
SET @iFS = @@FETCH_STATUS;
IF @iFS = 0
BEGIN
    SET @LastContractID = @CurrContractID;
    SET @LastStartDT = @Currdt;
    SET @LastEndDT = @Currdt;
    SET @LastDayCount = 1;
    SET @LastMonCount = 0;
    SET @LastTueCount = 0;
    SET @LastWedCount = 0;
    SET @LastThuCount = 0;
    SET @LastFriCount = 0;
    SET @LastSatCount = 0;
    SET @LastSunCount = 0;
    IF @CurrdowInt = 1 SET @LastSunCount = @LastSunCount + 1;
    IF @CurrdowInt = 2 SET @LastMonCount = @LastMonCount + 1;
    IF @CurrdowInt = 3 SET @LastTueCount = @LastTueCount + 1;
    IF @CurrdowInt = 4 SET @LastWedCount = @LastWedCount + 1;
    IF @CurrdowInt = 5 SET @LastThuCount = @LastThuCount + 1;
    IF @CurrdowInt = 6 SET @LastFriCount = @LastFriCount + 1;
    IF @CurrdowInt = 7 SET @LastSatCount = @LastSatCount + 1;
END;

WHILE @iFS = 0
BEGIN

    SET @VarCreateNewInterval = 0;

    -- Contract changes -> start new interval
    IF @LastContractID <> @CurrContractID
    BEGIN
        SET @VarCreateNewInterval = 1;
    END;

    IF @VarCreateNewInterval = 0
    BEGIN
        -- check days of week
        -- are we still within the first week of the interval?
        IF DATEDIFF(day, @LastStartDT, @Currdt) > 6
        BEGIN
            -- we are beyond the first week, check day of the week
            -- have we seen @CurrdowInt before?
            -- we should start a new interval if this is the new day of the week that didn't exist in the first week
            IF @CurrdowInt = 1 AND @LastSunCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 2 AND @LastMonCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 3 AND @LastTueCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 4 AND @LastWedCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 5 AND @LastThuCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 6 AND @LastFriCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 7 AND @LastSatCount = 0 SET @VarCreateNewInterval = 1;

            IF @VarCreateNewInterval = 0
            BEGIN
                -- check the gap between current day and last day of the interval
                -- if the gap between current day and last day of the interval
                -- contains a day of the week that was included in the interval before,
                -- we should create new interval
                SET @VarTempDT = DATEADD(day, 1, @LastEndDT);
                WHILE @VarTempDT < @Currdt
                BEGIN
                    SET @VarTempdowInt = DATEPART(WEEKDAY, @VarTempDT);

                    IF @VarTempdowInt = 1 AND @LastSunCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 2 AND @LastMonCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 3 AND @LastTueCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 4 AND @LastWedCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 5 AND @LastThuCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 6 AND @LastFriCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 7 AND @LastSatCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;

                    SET @VarTempDT = DATEADD(day, 1, @VarTempDT);
                END;
            END;
        END;
        -- else
        -- we are still within the first week, so we can add this day to the interval
    END;

    IF @VarCreateNewInterval = 1
    BEGIN
        -- save the new interval into the final table
        SET @LastWeekDays = '';
        IF @LastSunCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sun,';
        IF @LastMonCount > 0 SET @LastWeekDays = @LastWeekDays + 'Mon,';
        IF @LastTueCount > 0 SET @LastWeekDays = @LastWeekDays + 'Tue,';
        IF @LastWedCount > 0 SET @LastWeekDays = @LastWeekDays + 'Wed,';
        IF @LastThuCount > 0 SET @LastWeekDays = @LastWeekDays + 'Thu,';
        IF @LastFriCount > 0 SET @LastWeekDays = @LastWeekDays + 'Fri,';
        IF @LastSatCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sat,';

        INSERT INTO #Dst_V2 
            (ContractID
            ,StartDT
            ,EndDT
            ,DayCount
            ,WeekDays)
        VALUES
            (@LastContractID
            ,@LastStartDT
            ,@LastEndDT
            ,@LastDayCount
            ,@LastWeekDays);

        -- init the new interval
        SET @LastContractID = @CurrContractID;
        SET @LastStartDT = @Currdt;
        SET @LastEndDT = @Currdt;
        SET @LastDayCount = 1;
        SET @LastMonCount = 0;
        SET @LastTueCount = 0;
        SET @LastWedCount = 0;
        SET @LastThuCount = 0;
        SET @LastFriCount = 0;
        SET @LastSatCount = 0;
        SET @LastSunCount = 0;
        IF @CurrdowInt = 1 SET @LastSunCount = @LastSunCount + 1;
        IF @CurrdowInt = 2 SET @LastMonCount = @LastMonCount + 1;
        IF @CurrdowInt = 3 SET @LastTueCount = @LastTueCount + 1;
        IF @CurrdowInt = 4 SET @LastWedCount = @LastWedCount + 1;
        IF @CurrdowInt = 5 SET @LastThuCount = @LastThuCount + 1;
        IF @CurrdowInt = 6 SET @LastFriCount = @LastFriCount + 1;
        IF @CurrdowInt = 7 SET @LastSatCount = @LastSatCount + 1;

    END ELSE BEGIN

        -- update last interval
        SET @LastEndDT = @Currdt;
        SET @LastDayCount = @LastDayCount + 1;
        IF @CurrdowInt = 1 SET @LastSunCount = @LastSunCount + 1;
        IF @CurrdowInt = 2 SET @LastMonCount = @LastMonCount + 1;
        IF @CurrdowInt = 3 SET @LastTueCount = @LastTueCount + 1;
        IF @CurrdowInt = 4 SET @LastWedCount = @LastWedCount + 1;
        IF @CurrdowInt = 5 SET @LastThuCount = @LastThuCount + 1;
        IF @CurrdowInt = 6 SET @LastFriCount = @LastFriCount + 1;
        IF @CurrdowInt = 7 SET @LastSatCount = @LastSatCount + 1;
    END;


    FETCH NEXT FROM @VarCursor INTO @CurrContractID, @Currdt, @CurrdowChar, @CurrdowInt;
    SET @iFS = @@FETCH_STATUS;
END;

-- save the last interval into the final table
IF @LastDayCount > 0
BEGIN
    SET @LastWeekDays = '';
    IF @LastSunCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sun,';
    IF @LastMonCount > 0 SET @LastWeekDays = @LastWeekDays + 'Mon,';
    IF @LastTueCount > 0 SET @LastWeekDays = @LastWeekDays + 'Tue,';
    IF @LastWedCount > 0 SET @LastWeekDays = @LastWeekDays + 'Wed,';
    IF @LastThuCount > 0 SET @LastWeekDays = @LastWeekDays + 'Thu,';
    IF @LastFriCount > 0 SET @LastWeekDays = @LastWeekDays + 'Fri,';
    IF @LastSatCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sat,';

    INSERT INTO #Dst_V2
        (ContractID
        ,StartDT
        ,EndDT
        ,DayCount
        ,WeekDays)
    VALUES
        (@LastContractID
        ,@LastStartDT
        ,@LastEndDT
        ,@LastDayCount
        ,@LastWeekDays);
END;

CLOSE @VarCursor;
DEALLOCATE @VarCursor;

SET DATEFIRST @VarOldDateFirst;

DROP TABLE #Dst_V2;

2

Tôi hơi ngạc nhiên khi giải pháp con trỏ của Vladimir quá chậm, vì vậy tôi cũng đã cố gắng tối ưu hóa phiên bản đó. Tôi đã xác nhận rằng sử dụng một con trỏ là rất chậm đối với tôi là tốt.

Tuy nhiên, với chi phí sử dụng chức năng không có giấy tờ trong SQL Server bằng cách nối thêm một biến trong khi xử lý một hàng, tôi có thể tạo một phiên bản logic đơn giản này mang lại kết quả tối ưu và thực thi nhanh hơn cả con trỏ và giải pháp ban đầu của tôi . Vì vậy, sử dụng có nguy cơ của riêng bạn, nhưng tôi sẽ trình bày giải pháp trong trường hợp đó là lợi ích. Cũng có thể cập nhật giải pháp sử dụng WHILEvòng lặp từ một đến số hàng tối đa, tìm kiếm số hàng tiếp theo ở mỗi lần lặp của vòng lặp. Điều này sẽ bám vào chức năng được ghi chép đầy đủ và đáng tin cậy, nhưng sẽ vi phạm các ràng buộc (hơi giả tạo) đã nêu của vấn đề mà WHILEcác vòng lặp không được phép.

Lưu ý rằng nếu sử dụng SQL 2014 được cho phép, có thể quy trình được lưu trữ được biên dịch tự nhiên lặp lại các số hàng và truy cập từng số hàng trong bảng được tối ưu hóa bộ nhớ sẽ là một triển khai logic tương tự này sẽ chạy nhanh hơn.

Đây là giải pháp đầy đủ , bao gồm mở rộng dữ liệu dùng thử được đặt ra khoảng nửa triệu hàng. Giải pháp mới hoàn thành trong khoảng 3 giây và theo tôi là ngắn gọn và dễ đọc hơn nhiều so với giải pháp trước đây tôi đưa ra. Tôi sẽ chia ra ba bước liên quan ở đây:

Bước 1: tiền xử lý

Trước tiên chúng tôi thêm một số hàng vào tập dữ liệu, theo thứ tự chúng tôi sẽ xử lý dữ liệu. Trong khi làm như vậy, chúng tôi cũng chuyển đổi mỗi dowInt thành lũy thừa 2 để chúng tôi có thể sử dụng một bitmap để biểu thị ngày nào đã được quan sát trong bất kỳ nhóm nào:

IF OBJECT_ID('tempdb..#srcWithRn') IS NOT NULL
    DROP TABLE #srcWithRn
GO
SELECT rn = IDENTITY(INT, 1, 1), ContractId, dt, dowInt,
    POWER(2, dowInt) AS dowPower, dowChar
INTO #srcWithRn
FROM #src
ORDER BY ContractId, dt
GO
ALTER TABLE #srcWithRn
ADD PRIMARY KEY (rn)
GO

Bước 2: Vòng qua các ngày hợp đồng để xác định các nhóm mới

Chúng tôi tiếp theo vòng lặp trên dữ liệu, theo thứ tự số hàng. Chúng tôi chỉ tính toán danh sách các số hàng tạo thành ranh giới của một nhóm mới, sau đó xuất các số hàng đó thành một bảng:

DECLARE @ContractId INT, @RnList VARCHAR(MAX), @NewGrouping BIT = 0, @DowBitmap INT = 0, @startDt DATE
SELECT TOP 1 @ContractId = ContractId, @startDt = dt, @RnList = ',' + CONVERT(VARCHAR(MAX), rn), @DowBitmap = DowPower
FROM #srcWithRn
WHERE rn = 1

SELECT 
    -- New grouping if new contract, or if we're observing a new day that we did
    -- not observe within the first 7 days of the grouping
    @NewGrouping = CASE
        WHEN ContractId <> @ContractId THEN 1
        WHEN DATEDIFF(DAY, @startDt, dt) > 6
            AND @DowBitmap & dowPower <> dowPower THEN 1
        ELSE 0
        END,
    @ContractId = ContractId,
    -- If this is a newly observed day in an existing grouping, add it to the bitmap
    @DowBitmap = CASE WHEN @NewGrouping = 0 THEN @DowBitmap | DowPower ELSE DowPower END,
    -- If this is a new grouping, reset the start date of the grouping
    @startDt = CASE WHEN @NewGrouping = 0 THEN @startDt ELSE dt END,
    -- If this is a new grouping, add this rn to the list of row numbers that delineate the boundary of a new grouping
    @RnList = CASE WHEN @NewGrouping = 0 THEN @RnList ELSE @RnList + ',' + CONVERT(VARCHAR(MAX), rn) END 
FROM #srcWithRn
WHERE rn >= 2
ORDER BY rn
OPTION (MAXDOP 1)

-- Split the list of grouping boundaries into a table
IF OBJECT_ID('tempdb..#newGroupingRns') IS NOT NULL
    DROP TABLE #newGroupingRns
SELECT splitListId AS rn
INTO #newGroupingRns
FROM dbo.f_delimitedIntListSplitter(SUBSTRING(@RnList, 2, 1000000000), DEFAULT)
GO
ALTER TABLE #newGroupingRns
ADD PRIMARY KEY (rn)
GO

Bước 3: Tính kết quả cuối cùng dựa trên số hàng của từng ranh giới nhóm

Sau đó, chúng tôi tính toán các nhóm cuối cùng bằng cách sử dụng các ranh giới được xác định trong vòng lặp ở trên để tổng hợp tất cả các ngày rơi vào mỗi nhóm:

IF OBJECT_ID('tempdb..#finalGroupings') IS NOT NULL
    DROP TABLE #finalGroupings
GO
SELECT MIN(s.ContractId) AS ContractId,
    MIN(dt) AS StartDT,
    MAX(dt) AS EndDT,
    COUNT(*) AS DayCount,
    CASE WHEN MAX(CASE WHEN dowChar = 'Sun' THEN 1 ELSE 0 END) = 1 THEN 'Sun,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Mon' THEN 1 ELSE 0 END) = 1 THEN 'Mon,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Tue' THEN 1 ELSE 0 END) = 1 THEN 'Tue,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Wed' THEN 1 ELSE 0 END) = 1 THEN 'Wed,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Thu' THEN 1 ELSE 0 END) = 1 THEN 'Thu,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Fri' THEN 1 ELSE 0 END) = 1 THEN 'Fri,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Sat' THEN 1 ELSE 0 END) = 1 THEN 'Sat,' ELSE '' END AS WeekDays
INTO #finalGroupings
FROM #srcWithRn s
CROSS APPLY (
    -- For any row, its grouping is the largest boundary row number that occurs at or before this row
    SELECT TOP 1 rn AS groupingRn
    FROM #newGroupingRns grp
    WHERE grp.rn <= s.rn
    ORDER BY grp.rn DESC
) g
GROUP BY g.groupingRn
ORDER BY g.groupingRn
GO

Cảm ơn bạn. Tôi yêu cầu không sử dụng con trỏ hoặc WHILEvòng lặp, vì tôi đã biết cách giải quyết nó bằng con trỏ và tôi muốn tìm một giải pháp dựa trên tập hợp. Ngoài ra, tôi nghi ngờ rằng con trỏ sẽ chậm (đặc biệt là với một vòng lặp lồng nhau trong đó). Câu trả lời này rất thú vị về việc học các thủ thuật mới và tôi đánh giá cao những nỗ lực của bạn.
Vladimir Baranov

1

Thảo luận sẽ theo mã.

declare @Helper table(
    rn tinyint,
    dowInt tinyint,
    dowChar char(3));
insert @Helper
values  ( 1,1,'Sun'),
        ( 2,2,'Mon'),
        ( 3,3,'Tue'),
        ( 4,4,'Wed'),
        ( 5,5,'Thu'),
        ( 6,6,'Fri'),
        ( 7,7,'Sat'),
        ( 8,1,'Sun'),
        ( 9,2,'Mon'),
        (10,3,'Tue'),
        (11,4,'Wed'),
        (12,5,'Thu'),
        (13,6,'Fri'),
        (14,7,'Sat');



with MissingDays as
(
    select
        h1.rn as rn1,
        h1.dowChar as StartDay,
        h2.rn as rn2,
        h2.dowInt as FollowingDayInt,
        h2.dowChar as FollowingDayChar
    from @Helper as h1
    inner join @Helper as h2
        on h2.rn > h1.rn
    where h1.rn < 8
    and h2.rn < h1.rn + 8
)
,Numbered as
(
    select
        a.*,
        ROW_NUMBER() over (partition by a.ContractID order by a.dt) as rn
    from #Src as a
)
,Incremented as
(
    select
        b.*,
        convert(varchar(max), b.dowChar)+',' as WeekDays,
        b.dt as IntervalStart
    from Numbered as b
    where b.rn = 1

    union all

    select
        c.*,
        case
            when
                (DATEDIFF(day, d.IntervalStart, c.dt) > 6)      -- interval goes beyond 7 days
            and (
                    (d.WeekDays not like '%'+c.dowChar+'%')     -- the new week day has not been seen before
                or 
                    (DATEDIFF(day, d.dt, c.dt) > 7)
                or 
                    (
                        (DATEDIFF(day, d.dt, c.dt) > 1)
                        and
                        (
                        exists( select
                                    e.FollowingDayChar
                                from MissingDays as e
                                where e.StartDay = d.dowChar
                                and rn2 < (select f.rn2 from MissingDays as f
                                            where f.StartDay = d.dowChar
                                            and f.FollowingDayInt = c.dowInt)
                                and d.WeekDays like '%'+e.FollowingDayChar+'%'
                            )
                        )
                    )
                )
            then convert(varchar(max),c.dowChar)+','
            else
                case
                    when d.WeekDays like '%'+c.dowChar+'%'
                    then d.WeekDays
                    else d.WeekDays+convert(varchar(max),c.dowChar)+','
                end
        end,
        case
            when
                (DATEDIFF(day, d.IntervalStart, c.dt) > 6)      -- interval goes beyond 7 days
            and (
                    (d.WeekDays not like '%'+c.dowChar+'%')     -- the new week day has not been seen before
                or
                    (DATEDIFF(day, d.dt, c.dt) > 7)             -- there is a one week gap
                or 
                    (
                        (DATEDIFF(day, d.dt, c.dt) > 1)         -- there is a gap..
                        and
                        (
                        exists( select                          -- .. and the omitted days are in the preceeding interval
                                    e.FollowingDayChar
                                from MissingDays as e
                                where e.StartDay = d.dowChar
                                and rn2 < (select f.rn2 from MissingDays as f
                                            where f.StartDay = d.dowChar
                                            and f.FollowingDayInt = c.dowInt)
                                and d.WeekDays like '%'+e.FollowingDayChar+'%'
                            )
                        )
                    )
                )
            then c.dt
            else d.IntervalStart
        end
    from Numbered as c
    inner join Incremented as d
    on d.ContractID = c.ContractID
    and d.rn = c.rn - 1
)
select
    g.ContractID,
    g.IntervalStart as StartDT,
    MAX(g.dt) as EndDT,
    COUNT(*) as DayCount,
    MAX(g.WeekDays) as WeekDays
from Incremented as g
group by
    g.ContractID,
    g.IntervalStart
order by
    ContractID,
    StartDT;

@Helper là để đối phó với quy tắc này:

Nếu khoảng cách giữa ngày hiện tại và ngày cuối cùng của khoảng thời gian chứa một ngày trong tuần được bao gồm trong khoảng trước đó, chúng ta nên tạo khoảng thời gian mới

Nó cho phép tôi liệt kê tên ngày, theo thứ tự số ngày, giữa hai ngày bất kỳ. Điều này được sử dụng khi quyết định nếu một khoảng thời gian mới sẽ bắt đầu. Tôi điền vào nó các giá trị trong hai tuần để làm cho việc kết thúc một ngày cuối tuần dễ dàng hơn để viết mã.

Có nhiều cách sạch hơn để thực hiện điều này. Một bảng "ngày" đầy đủ sẽ là một. Có lẽ có một cách thông minh với số ngày và số học modulo.

CTE MissingDayssẽ tạo một danh sách các tên ngày giữa hai ngày bất kỳ. Nó được xử lý theo cách khó hiểu này vì CTE đệ quy (sau) không cho phép tổng hợp, TOP () hoặc các toán tử khác. Điều này là không phù hợp, nhưng nó hoạt động.

CTE Numberedlà để thực thi một chuỗi đã biết, không có khoảng cách trên dữ liệu. Nó tránh được rất nhiều so sánh sau này.

CTE Incrementedlà nơi hành động xảy ra. Về bản chất, tôi sử dụng CTE đệ quy để duyệt qua dữ liệu và thực thi các quy tắc. Số hàng được tạo trong Numbered(ở trên) được sử dụng để thúc đẩy xử lý đệ quy.

Hạt giống của CTE đệ quy chỉ đơn giản là lấy ngày đầu tiên cho mỗi ContractID và khởi tạo các giá trị sẽ được sử dụng để quyết định xem có cần một khoảng thời gian mới hay không.

Quyết định xem một khoảng thời gian mới sẽ bắt đầu yêu cầu ngày bắt đầu, danh sách ngày của khoảng thời gian hiện tại và độ dài của bất kỳ khoảng cách nào trong ngày theo lịch. Đây có thể được thiết lập lại hoặc chuyển tiếp, tùy thuộc vào quyết định. Do đó, phần đệ quy là dài dòng và một chút lặp đi lặp lại, vì chúng ta phải quyết định có nên bắt đầu một khoảng mới cho nhiều hơn một giá trị cột hay không.

Logic quyết định cho các cột WeekDaysIntervalStartphải có cùng logic quyết định - nó có thể được cắt và dán giữa chúng. Nếu logic để bắt đầu một khoảng mới là thay đổi thì đây là mã để thay đổi. Lý tưởng nhất là nó sẽ được trừu tượng hóa, do đó; làm điều này trong CTE đệ quy có thể là một thách thức.

Điều EXISTS()khoản này là kết quả của việc không thể sử dụng các hàm tổng hợp trong CTE đệ quy. Tất cả những gì nó làm là xem liệu những ngày nằm trong khoảng cách có trong khoảng thời gian hiện tại không.

Không có gì kỳ diệu về việc lồng các mệnh đề logic. Nếu nó rõ ràng hơn trong một hình dạng khác, hoặc sử dụng các CASE lồng nhau, giả sử, không có lý do gì để giữ nó theo cách này.

Cuối cùng SELECTlà đưa ra đầu ra ở định dạng mong muốn.

Có PK trên Src.IDkhông hữu ích cho phương pháp này. Một chỉ số cụm trên (ContractID,dt)sẽ là tốt đẹp, tôi nghĩ.

Có một vài cạnh thô. Các ngày không được trả về theo thứ tự dow, nhưng trong chuỗi lịch chúng xuất hiện trong dữ liệu nguồn. Mọi thứ để làm với @Helper là klunky và có thể được làm mịn. Tôi thích ý tưởng sử dụng một bit mỗi ngày và sử dụng các hàm nhị phân thay vì LIKE. Việc tách một số CTE phụ trợ vào bảng tạm thời với các chỉ số thích hợp chắc chắn sẽ giúp ích.

Một trong những thách thức với điều này là "tuần" không phù hợp với lịch tiêu chuẩn, mà được điều khiển bởi dữ liệu và đặt lại khi xác định rằng một khoảng thời gian mới sẽ bắt đầu. Một "tuần", hoặc ít nhất là một khoảng thời gian, có thể kéo dài từ một ngày cho đến toàn bộ tập dữ liệu.


Vì lợi ích, đây là chi phí ước tính đối với dữ liệu mẫu của Geoff (cảm ơn vì điều đó!) Sau những thay đổi khác nhau:

                                             estimated cost

My submission as is w/ CTEs, Geoff's data:      791682
Geoff's data, cluster key on (ContractID, dt):   21156.2
Real table for MissingDays:                      21156.2
Numbered as table UCI=(ContractID, rn):             16.6115    26s elapsed.
                  UCI=(rn, ContractID):             41.9845    26s elapsed.
MissingDays as refactored to simple lookup          16.6477    22s elapsed.
Weekdays as varchar(30)                             13.4013    30s elapsed.

Số lượng hàng ước tính và thực tế khác nhau rất nhiều.

Kế hoạch có một bảng spoo, có thể là kết quả của CTE đệ quy. Hầu hết các hành động là trong một bàn làm việc sắp ra rằng:

Table 'Worktable'.   Scan count       2, logical reads 4 196 269, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'MissingDays'. Scan count 464 116, logical reads   928 232, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Numbered'.    Scan count 484 122, logical reads 1 475 467, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Chỉ là cách thực hiện đệ quy, tôi đoán vậy!


Cảm ơn bạn. Nó cho kết quả chính xác và tối ưu trên dữ liệu mẫu. Bây giờ tôi sẽ kiểm tra nó trên dữ liệu thực. Một lưu ý phụ: MAX(g.IntervalStart)có vẻ kỳ lạ, bởi vì g.IntervalStartlà trong GROUP BY. Tôi dự kiến ​​nó sẽ đưa ra một lỗi cú pháp, nhưng nó hoạt động. Có nên chỉ g.IntervalStart as StartDTtrong SELECT? Hoặc g.IntervalStartkhông nên ở trong GROUP BY?
Vladimir Baranov

Tôi đã cố chạy truy vấn trên dữ liệu thực và tôi phải dừng nó sau 10 phút. Rất có khả năng rằng nếu CTE MissingDaysNumberedđược thay thế bằng các bảng tạm thời với các chỉ mục thích hợp, nó có thể có hiệu suất tốt. Những chỉ số nào bạn muốn giới thiệu? Tôi có thể thử nó vào sáng mai.
Vladimir Baranov

Tôi nghĩ rằng việc thay thế Numberedbằng một bảng tạm thời và chỉ mục cụm trên (ContractID, rn)sẽ đáng để đi. Nếu không có một tập dữ liệu lớn để tạo ra kế hoạch tương ứng, thật khó để đoán. Vật lý MissingDatesvới các chỉ số (StartDay, FollowingDayInt)cũng sẽ tốt.
Michael Green

Cảm ơn. Tôi không thể thử nó ngay bây giờ, nhưng tôi sẽ sáng mai.
Vladimir Baranov

Tôi đã thử điều này trên bộ dữ liệu nửa triệu hàng (bộ dữ liệu hiện có, được sao chép 4.000 lần với các Hợp đồng khác nhau). Nó đã chạy được khoảng 15 phút và đã chiếm tới 30 GB dung lượng tempdb. Vì vậy, tôi nghĩ rằng một số tối ưu hóa có thể là cần thiết. Dưới đây là dữ liệu thử nghiệm mở rộng trong trường hợp bạn thấy nó hữu ích.
Geoff Patterson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.