Мне нужно конвертировать данные между двумя системами.
Первая система хранит расписания в виде простого списка дат. Каждая дата, включенная в расписание, состоит из одной строки. В последовательности дат могут быть различные промежутки (выходные, праздничные дни и более длинные паузы, некоторые дни недели могут быть исключены из графика). Там не может быть никаких пробелов, даже выходные могут быть включены. График может быть до 2 лет. Обычно это несколько недель.
Вот простой пример расписания, которое охватывает две недели, исключая выходные (в приведенном ниже сценарии есть более сложные примеры):
+----+------------+------------+---------+--------+
| 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
уникален, но не обязательно последовательный (это первичный ключ). Даты уникальны для каждого контракта (есть уникальный индекс (ContractID, dt)
).
Вторая система хранит расписания в виде интервалов со списком дней недели, которые являются частью расписания. Каждый интервал определяется его датами начала и окончания (включительно) и списком дней недели, которые включены в расписание. В этом формате вы можете эффективно определять повторяющиеся еженедельные шаблоны, такие как пн-ср., Но это становится проблемой, когда шаблон нарушается, например, в праздничные дни.
Вот как будет выглядеть приведенный выше простой пример:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
интервалы, которые принадлежат одному и тому же Контракту, не должны пересекаться.
Мне нужно преобразовать данные из первой системы в формат, используемый второй системой. В данный момент я решаю эту проблему на стороне клиента в C # для одного данного Контракта, но я бы хотел сделать это в T-SQL на стороне сервера для массовой обработки и экспорта / импорта между серверами. Скорее всего, это можно сделать с помощью CLR UDF, но на этом этапе я не могу использовать SQLCLR.
Задача состоит в том, чтобы сделать список интервалов как можно более коротким и удобным для человека.
Например, этот график:
+-----+------------+------------+---------+--------+
| 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 |
+-----+------------+------------+---------+--------+
должен стать таким:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,не это:
+------------+------------+------------+----------+----------------------+
| 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, |
+------------+------------+------------+----------+----------------------+
Я пытался применить gaps-and-islands
подход к этой проблеме. Я пытался сделать это в два прохода. В первом проходе я нахожу острова простых последовательных дней, то есть конец острова - это какой-то разрыв в последовательности дней, будь то выходные, праздничные дни или что-то еще. Для каждого такого найденного острова я строю отдельный список запятых WeekDays
. Во втором проходе я обнаружил острова дальше, взглянув на разрыв в последовательности номеров недель или изменение в WeekDays
.
При таком подходе каждая неполная неделя заканчивается как дополнительный интервал, как показано выше, потому что, даже если номера недель последовательны, WeekDays
изменение. Кроме того, могут быть регулярные промежутки в течение недели (см. ContractID=3
В данных выборки, в которых есть данные только для Mon,Wed,Fri,
), и этот подход будет генерировать отдельные интервалы для каждого дня в таком графике. С другой стороны, он генерирует один интервал, если в расписании вообще нет пропусков (см. ContractID=7
Пример данных, который включает выходные), и в этом случае не имеет значения, является ли начало или конец недели частичным.
Пожалуйста, посмотрите другие примеры в приведенном ниже сценарии, чтобы лучше понять, что мне нужно. Вы можете видеть, что довольно часто выходные дни исключаются, но любые другие дни недели также могут быть исключены. Только в примере 3 Mon
, Wed
и Fri
являются частью графика. Кроме того, выходные дни могут быть включены, как в примере 7. Решение должно относиться ко всем дням недели одинаково. Любой день недели может быть включен или исключен из графика.
Чтобы убедиться, что сгенерированный список интервалов правильно описывает данное расписание, вы можете использовать следующий псевдокод:
- цикл через все интервалы
- для каждого интервала цикл всех календарных дат между датами начала и окончания (включительно).
- для каждой даты проверьте, указан ли ее день недели в
WeekDays
. Если да, то эта дата включена в расписание.
Надеюсь, это проясняет, в каких случаях должен быть создан новый интервал. В примерах 4 и 5 один понедельник (2016-05-09
) удаляется из середины расписания, и такое расписание не может быть представлено одним интервалом. В примере 6 в графике имеется большой разрыв, поэтому необходимы два интервала.
Интервалы представляют недельные шаблоны в расписании, и когда шаблон нарушается / изменяется, необходимо добавить новый интервал. В примере 11 первые три недели имеют шаблон Tue
, затем этот шаблон меняется на Thu
. В результате нам нужно два интервала, чтобы описать такое расписание.
Сейчас я использую SQL Server 2008, поэтому решение должно работать в этой версии. Если решение для SQL Server 2008 можно упростить / улучшить с помощью функций более поздних версий, это бонус, пожалуйста, покажите его.
У меня есть Calendar
таблица (список дат) и Numbers
таблица (список целых чисел, начиная с 1), поэтому можно использовать их при необходимости. Также можно создавать временные таблицы и иметь несколько запросов, которые обрабатывают данные в несколько этапов. Число этапов в алгоритме должно быть фиксированным, хотя курсоры и явные WHILE
циклы не в порядке.
Скрипт для выборки данных и ожидаемых результатов
-- @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;
Сравнение ответов
В реальной таблице @Src
есть 403,555
строки с 15,857
отчетливыми ContractIDs
. Все ответы дают правильные результаты (по крайней мере, для моих данных), и все они достаточно быстрые, но отличаются оптимальностью. Чем меньше генерируемых интервалов, тем лучше. Я включил время выполнения просто для любопытства. Основное внимание уделяется правильному и оптимальному результату, а не скорости (если это не займет слишком много времени - я остановил нерекурсивный запрос Ziggy Crueltyfree Zeitgeister через 10 минут).
+--------------------------------------------------------+-----------+---------+
| 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 | | |
+--------------------------------------------------------+-----------+---------+
источник
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
В @Dst не должно быть одной строки сTue, Thu,
?@Dst
). Первые две недели в графике есть толькоTue
, так что вы не можете иметь вWeekDays=Tue,Thu,
течение этих недель. Последние две недели в графике есть толькоThu
, так что вы снова не можете иметь вWeekDays=Tue,Thu,
течение этих недель. Неоптимальным решением для этого было бы три ряда: толькоTue
для первых двух недель, затемTue,Thu,
для третьей недели, которая имеет оба,Tue
иThu
затем толькоThu
для последних двух недель.ContractID
изменения, если интервал выходит за пределы 7 дней, и новый день недели раньше не видел, если в списке запланированных дней есть пробел.Ответы:
Этот использует рекурсивный CTE. Его результат идентичен примеру в вопросе . Это был кошмар, чтобы придумать ... Код включает комментарии, чтобы облегчить его запутанную логику.
Другая стратегия
Этот должен быть значительно быстрее предыдущего, потому что он не использует медленный ограниченный рекурсивный CTE в SQL Server 2008, хотя он реализует более или менее ту же стратегию.
Существует
WHILE
цикл (я не мог придумать способ избежать этого), но он идет на меньшее количество итераций (наибольшее количество последовательностей (минус одна) в любом конкретном контракте).Это простая стратегия, и ее можно использовать для последовательностей, которые короче или длиннее недели (заменив любое вхождение константы 7 на любое другое число и
dowBit
вычисленное по MODULUS xDayNo
вместоDATEPART(wk)
) и до 32.источник
Не совсем то, что вы ищете, но, возможно, может быть интересным для вас.
Запрос создает недели с разделенной запятыми строкой для дней, используемых в каждой неделе. Затем он находит острова последовательных недель, которые используют тот же шаблон в
Weekdays
.Результат:
ContractID = 2
показывает, какая разница в результате по сравнению с тем, что вы хотите. Первая и последняя недели будут рассматриваться как отдельные периоды, поскольку ониWeekDays
разные.источник
WeekDays
же как и 7-битное число. Всего 128 комбинаций. Есть только 128 * 128 = 16384 возможных пар. Создайте временную таблицу со всеми возможными парами, затем определите алгоритм, основанный на множестве, который бы обозначал, какие пары могут быть объединены: образец одной недели «покрыт» образцом следующей недели. Самостоятельно присоединиться к текущему еженедельному результату (посколькуLAG
в 2008 году его нет) и использовать эту временную таблицу, чтобы решить, какие пары объединить ... Не уверен, что эта идея имеет какие-либо достоинства.Я закончил с подходом, который дает оптимальное решение в этом случае, и я думаю, что в целом все будет хорошо. Однако решение довольно длинное, поэтому было бы интересно посмотреть, есть ли у кого-то другой подход, более краткий.
Вот скрипт, который содержит полное решение .
И вот схема алгоритма:
ContractId
ContractId
и ту жеWeekDays
WeekDays
единичная неделя соответствует ведущему подмножествуWeekDays
предыдущей группировки, объединитесь в эту предыдущую группировку.WeekDays
единичная неделя соответствует завершающему подмножествуWeekDays
следующей группы, объединитесь в эту следующую группуисточник
(1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),
. Это может быть представлено как один интервал, но ваше решение дает два. Я признаю, что этот пример не был в данных образца, и это не критично. Я постараюсь запустить ваше решение на реальных данных.Я не мог понять логику группировки недель с перерывами или недель с выходными (например, когда две недели подряд выходных, на какую неделю выходных?).
Следующий запрос дает желаемый результат, за исключением того, что он группирует только последовательные дни недели и группирует недели Sun-Sat (а не Mon-Sun). Хотя это не совсем то, что вы хотите, возможно, это может дать некоторые подсказки для другой стратегии. Отсюда происходит группировка дней . Используемые оконные функции должны работать с SQLServer 2008, но у меня нет этой версии, чтобы проверить, действительно ли она работает.
Результат
источник
Для полноты картины, вот два
gaps-and-islands
подхода, которые я попробовал перед тем, как задавать этот вопрос.Когда я тестировал их на реальных данных, я обнаружил несколько случаев, когда они давали неправильные результаты, и исправил их.
Вот алгоритм:
CTE_ContractDays
,CTE_DailyRN
,CTE_DailyIslands
) и вычислить номер недели для каждой исходной и даты окончания острова. Здесь номер недели рассчитывается исходя из предположения, что понедельник - первый день недели.CTE_Weeks
).CTE_FirstResult
).WeekDays
(CTE_SecondRN
,CTE_Schedules
).Он хорошо обрабатывает случаи, когда в недельных моделях нет нарушений (1, 7, 8, 10, 12). Он хорошо обрабатывает случаи, когда шаблон имеет непоследовательные дни (3).
Но, к сожалению, он генерирует дополнительные интервалы для неполных недель (2, 3, 5, 6, 9, 11).
Результат
Курсорное решение
Я преобразовал свой код C # в алгоритм на основе курсора, просто чтобы увидеть, как он сравнивается с другими решениями на реальных данных. Это подтверждает, что это намного медленнее, чем другие основанные на множестве или рекурсивные подходы, но это дает оптимальный результат.
источник
Я был немного удивлен, что решение курсора Владимира было таким медленным, поэтому я также попытался оптимизировать эту версию. Я подтвердил, что использование курсора было очень медленным для меня.
Однако за счет использования недокументированной функциональности в SQL Server путем добавления к переменной при обработке набора строк мне удалось создать упрощенную версию этой логики, которая дает оптимальный результат и выполняется намного быстрее, чем курсор и мое исходное решение. , Так что используйте на свой страх и риск, но я представлю решение на случай, если оно будет интересно. Также было бы возможно обновить решение, чтобы использовать
WHILE
цикл от одного до максимального номера строки, ища следующий номер строки на каждой итерации цикла. Это будет придерживаться полностью документированной и надежной функциональности, но нарушит (несколько искусственное) установленное ограничение проблемы,WHILE
заключающееся в том, что петли не допускаются.Обратите внимание, что если использование SQL 2014 было разрешено, вполне вероятно, что встроенная хранимая процедура, которая перебирает номера строк и обращается к каждому номеру строки в оптимизированной для памяти таблице, была бы реализацией этой же логики, которая выполнялась бы быстрее.
Вот полное решение , включая расширение данных пробной версии примерно до полумиллиона строк. Новое решение длится около 3 секунд, и, на мой взгляд, оно намного более лаконично и читабельно, чем предыдущее решение, которое я предлагал. Я выделю три этапа, описанных здесь:
Шаг 1: предварительная обработка
Сначала мы добавляем номер строки в набор данных, в том порядке, в котором мы будем обрабатывать данные. При этом мы также преобразуем каждый dowInt в степень 2, чтобы мы могли использовать растровое изображение для представления того, какие дни были отмечены в любой данной группе:
Шаг 2: Цикл дней контракта для определения новых группировок
Далее мы перебираем данные по порядку по номеру строки. Мы вычисляем только список номеров строк, которые формируют границу новой группировки, а затем выводим эти номера строк в таблицу:
Шаг 3: Вычисление окончательных результатов на основе номеров строк каждой границы группировки
Затем мы вычисляем окончательные группировки, используя границы, указанные в цикле выше, чтобы объединить все даты, попадающие в каждую группу:
источник
WHILE
циклы, потому что я уже знал, как решить это с помощью курсора, и я хотел найти решение на основе множеств. Кроме того, я подозревал, что курсор будет медленным (особенно с вложенным циклом в нем). Этот ответ очень интересен с точки зрения изучения новых трюков, и я ценю ваши усилия.Обсуждение будет следовать за кодом.
@Helper
это справиться с этим правилом:Это позволяет мне перечислять названия дней в порядке номеров дней между любыми двумя данными днями. Это используется при принятии решения о начале нового интервала. Я заполняю его значениями за две недели, чтобы упростить кодирование в выходные дни.
Есть более чистые способы реализовать это. Полная таблица «даты» будет один. Вероятно, есть хитрый способ с номером дня и модульной арифметикой.
CTE
MissingDays
должен создать список названий дней между любыми двумя заданными днями. Это обрабатывается таким неуклюжим способом, потому что рекурсивный CTE (следующий) не допускает агрегаты, TOP () или другие операторы. Это не элегантно, но работает.КТР
Numbered
должен обеспечить выполнение известной последовательности данных без пропусков. Это избегает много сравнений позже.CTE
Incremented
- это место, где происходит действие. По сути, я использую рекурсивный CTE для пошагового прохождения данных и обеспечения соблюдения правил. Номер строки, сгенерированный вNumbered
(выше), используется для запуска рекурсивной обработки.Семя рекурсивного CTE просто получает первую дату для каждого ContractID и инициализирует значения, которые будут использоваться для определения необходимости нового интервала.
Для принятия решения о том, следует ли начинать новый интервал, требуются дата начала текущего интервала, список дней и длина любого разрыва в календарных датах. Они могут быть сброшены или перенесены в зависимости от решения. Следовательно, рекурсивная часть многословна и немного повторяется, так как мы должны решить, начинать ли новый интервал для более чем одного значения столбца.
Логика решения для столбцов
WeekDays
иIntervalStart
должна иметь одинаковую логику принятия решения - ее можно вырезать и вставить между ними. Если логика для начала нового интервала должна была измениться, это код для изменения. В идеале это будет абстрагировано; делать это в рекурсивном CTE может быть непросто.Предложение
EXISTS()
является результатом невозможности использования агрегатных функций в рекурсивном CTE. Все, что он делает, это видит, находятся ли дни, попадающие в промежуток, уже в текущем интервале.В вложении логических предложений нет ничего волшебного. Если это яснее в другой конформации или с использованием вложенных CASE, скажем, нет причин сохранять это таким образом.
Финал
SELECT
должен дать вывод в желаемом формате.Включение ПК
Src.ID
не полезно для этого метода. Кластерный индекс на(ContractID,dt)
Я думаю, что был бы неплохим.Есть несколько грубых краев. Дни не возвращаются в последовательности dow, но в календарной последовательности они появляются в исходных данных. Все, что связано с @Helper, клунко и может быть сглажено. Мне нравится идея использовать один бит в день и использовать двоичные функции вместо
LIKE
. Разделение некоторых вспомогательных CTE в временную таблицу с соответствующими индексами, несомненно, поможет.Одна из проблем, связанных с этим, заключается в том, что «неделя» не совпадает со стандартным календарем, а скорее определяется данными и сбрасывается, когда определяется, что должен начинаться новый интервал. «Неделя» или, по крайней мере, интервал может составлять от одного дня до всего набора данных.
Ради интереса, вот примерные затраты по образцу данных Джеффа (спасибо за это!) После различных изменений:
Расчетное и фактическое количество строк сильно разнятся.
План имеет таблицу spoo, вероятно, в результате рекурсивного CTE. Большая часть действия находится в рабочем столе, который:
Полагаю, именно так, как реализован рекурсив!
источник
MAX(g.IntervalStart)
кажется странным, потому чтоg.IntervalStart
вGROUP BY
. Я ожидал, что это даст синтаксическую ошибку, но это работает. Это должно быть толькоg.IntervalStart as StartDT
вSELECT
? Илиg.IntervalStart
не должно быть вGROUP BY
?MissingDays
иNumbered
их заменить временными таблицами с надлежащими индексами, он может иметь приличную производительность. Какие показатели вы бы порекомендовали? Я мог бы попробовать это завтра утром.Numbered
на временную таблицу и кластерный индекс(ContractID, rn)
будет стоить того. Без большого набора данных для генерации соответствующего плана сложно догадаться. ФизикализацияMissingDates
с индексами(StartDay, FollowingDayInt)
тоже была бы хорошей.