Necesito convertir datos entre dos sistemas.
El primer sistema almacena los horarios como una lista simple de fechas. Cada fecha que se incluye en el cronograma es una fila. Puede haber varios vacíos en la secuencia de fechas (fines de semana, días festivos y pausas más largas, algunos días de la semana pueden quedar excluidos del horario). No puede haber brechas en absoluto, incluso se pueden incluir los fines de semana. El horario puede durar hasta 2 años. Por lo general, dura unas pocas semanas.
Aquí hay un ejemplo simple de un horario que abarca dos semanas excluyendo los fines de semana (hay ejemplos más complicados en el script a continuación):
+----+------------+------------+---------+--------+
| 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
es único, pero no necesariamente es secuencial (es la clave principal). Las fechas son únicas dentro de cada Contrato (hay un índice único en(ContractID, dt)
).
El segundo sistema almacena los horarios como intervalos con la lista de días de la semana que forman parte del horario. Cada intervalo se define por sus fechas de inicio y finalización (inclusive) y una lista de días de la semana que se incluyen en la programación. En este formato, puede definir eficientemente patrones semanales repetitivos, como de lunes a miércoles, pero se convierte en una molestia cuando un patrón se interrumpe, por ejemplo, en días festivos.
Así es como se verá el simple ejemplo anterior:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
los intervalos que pertenecen al mismo Contrato no deben superponerse.
Necesito convertir datos del primer sistema al formato utilizado por el segundo sistema. Por el momento, estoy resolviendo esto en el lado del cliente en C # para el contrato dado, pero me gustaría hacerlo en T-SQL en el lado del servidor para el procesamiento masivo y la exportación / importación entre servidores. Lo más probable es que se pueda hacer usando CLR UDF, pero en esta etapa no puedo usar SQLCLR.
El desafío aquí es hacer que la lista de intervalos sea lo más breve y amigable posible.
Por ejemplo, este horario:
+-----+------------+------------+---------+--------+
| 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 |
+-----+------------+------------+---------+--------+
debería convertirse en esto:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,no esta:
+------------+------------+------------+----------+----------------------+
| 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, |
+------------+------------+------------+----------+----------------------+
Traté de aplicar un gaps-and-islands
enfoque a este problema. Traté de hacerlo en dos pases. En el primer paso encuentro islas de días simples consecutivos, es decir, el final de la isla es cualquier brecha en la secuencia de días, ya sea fin de semana, día festivo u otra cosa. Para cada isla encontrada, construyo una lista separada por comas de distintas WeekDays
. En el segundo pase, el grupo encontró islas más lejos al observar la brecha en la secuencia de números de semana o un cambio en el WeekDays
.
Con este enfoque, cada semana parcial termina como un intervalo adicional como se muestra arriba, porque a pesar de que los números de semana son consecutivos, el WeekDays
cambio. Además, puede haber brechas regulares dentro de una semana (ver ContractID=3
en datos de muestra, que tiene datos solo para Mon,Wed,Fri,
) y este enfoque generaría intervalos separados para cada día en dicho horario. En el lado positivo, genera un intervalo si el cronograma no tiene ninguna brecha (verContractID=7
en los datos de muestra que incluyen los fines de semana) y en ese caso no importa si la semana de inicio o fin es parcial.
Vea otros ejemplos en el script a continuación para tener una mejor idea de lo que busco. Puede ver que muy a menudo se excluyen los fines de semana, pero también se puede excluir cualquier otro día de la semana. En el ejemplo 3 solamente Mon
, Wed
yFri
son parte de la programación. Además, se pueden incluir los fines de semana, como en el ejemplo 7. La solución debe tratar todos los días de la semana por igual. Cualquier día de la semana puede ser incluido o excluido del horario.
Para verificar que la lista generada de intervalos describe la programación dada correctamente, puede usar el siguiente pseudocódigo:
- recorrer todos los intervalos
- para cada intervalo, recorra todas las fechas del calendario entre las fechas de inicio y finalización (inclusive).
- para cada fecha, verifique si su día de la semana aparece en el
WeekDays
. En caso afirmativo, esta fecha se incluye en el cronograma.
Con suerte, esto aclara en qué casos se debe crear un nuevo intervalo. En los ejemplos 4 y 5 un lunes (2016-05-09
) se elimina de la mitad de la programación y dicha programación no puede representarse por un solo intervalo. En el ejemplo 6 hay una brecha larga en el cronograma, por lo que se necesitan dos intervalos.
Los intervalos representan patrones semanales en el cronograma y cuando un patrón se interrumpe / cambia, se debe agregar el nuevo intervalo. En el ejemplo 11, las primeras tres semanas tienen un patrón Tue
, luego este patrón cambia a Thu
. Como resultado, necesitamos dos intervalos para describir dicho programa.
Estoy usando SQL Server 2008 en este momento, por lo que la solución debería funcionar en esta versión. Si una solución para SQL Server 2008 se puede simplificar / mejorar usando características de versiones posteriores, eso es una ventaja, por favor muéstrela también.
Tengo una Calendar
tabla (lista de fechas) y una Numbers
tabla (lista de números enteros a partir de 1), por lo que está bien usarlas, si es necesario. También está bien crear tablas temporales y tener varias consultas que procesen datos en varias etapas. Sin embargo, el número de etapas en un algoritmo tiene que ser arreglado, los cursores y los WHILE
bucles explícitos no están bien.
Script para datos de muestra y resultados esperados
-- @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;
Comparación de respuestas
La tabla real @Src
tiene 403,555
filas con 15,857
distinto ContractIDs
. Todas las respuestas producen resultados correctos (al menos para mis datos) y todas son razonablemente rápidas, pero difieren en la optimización. Cuantos menos intervalos generados, mejor. Incluí tiempos de ejecución solo por curiosidad. El enfoque principal es el resultado correcto y óptimo, no la velocidad (a menos que tarde demasiado; detuve la consulta no recursiva de Ziggy Crueltyfree Zeitgeister después de 10 minutos).
+--------------------------------------------------------+-----------+---------+
| 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 | | |
+--------------------------------------------------------+-----------+---------+
fuente
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
en @Dst con una filaTue, Thu,
?@Dst
). Las primeras dos semanas del horario solo tienenTue
, por lo que no puede tenerlasWeekDays=Tue,Thu,
durante estas semanas. Las últimas dos semanas del horario solo tienenThu
, por lo que nuevamente no puede tenerlasWeekDays=Tue,Thu,
durante estas semanas. La solución subóptima para ello sería tres filas: soloTue
durante las primeras dos semanas, luegoTue,Thu,
durante la tercera semana que tiene ambasTue
yThu
, luego, soloThu
durante las últimas dos semanas.ContractID
cambios, si el intervalo va más allá de 7 días y el nuevo día de la semana no se ha visto antes, si hay una brecha en la lista de días programados.Respuestas:
Este usa un CTE recursivo. Su resultado es idéntico al ejemplo en la pregunta . Fue una pesadilla pensar ... El código incluye comentarios para facilitar su intrincada lógica.
Otra estrategia
Este debería ser significativamente más rápido que el anterior porque no se basa en el CTE recursivo lento y limitado en SQL Server 2008, aunque implementa más o menos la misma estrategia.
Hay un
WHILE
bucle (no pude idear una forma de evitarlo), pero va por un número reducido de iteraciones (el mayor número de secuencias (menos uno) en cualquier contrato dado).Es una estrategia simple, y podría usarse para secuencias más cortas o más largas que una semana (reemplazando cualquier ocurrencia de la constante 7 por cualquier otro número, y
dowBit
calculada a partir de MODULUS x de enDayNo
lugar deDATEPART(wk)
) y hasta 32.fuente
No es exactamente lo que está buscando, pero quizás podría ser de su interés.
La consulta crea semanas con una cadena separada por comas para los días utilizados en cada semana. Luego encuentra las islas de semanas consecutivas que usan el mismo patrón
Weekdays
.Resultado:
ContractID = 2
muestra cuál es la diferencia en el resultado en comparación con lo que desea. La primera y la última semana se tratarán como períodos separados desdeWeekDays
es diferente.fuente
WeekDays
es un número de 7 bits. Solo 128 combinaciones. Solo hay 128 * 128 = 16384 pares posibles. Construya una tabla temporal con todos los pares posibles, luego descubra un algoritmo basado en conjuntos que marcaría qué pares pueden fusionarse: un patrón de una semana está "cubierto" por un patrón de la próxima semana. Únase al resultado semanal actual (ya que no existeLAG
en 2008) y use esa tabla temporal para decidir qué pares fusionar ... No estoy seguro si esta idea tiene algún mérito.Terminé con un enfoque que brinda la solución óptima en este caso y creo que en general funcionará bien. Sin embargo, la solución es bastante larga, por lo que sería interesante ver si alguien más tiene un enfoque diferente que sea más conciso.
Aquí hay un script que contiene la solución completa .
Y aquí hay un resumen del algoritmo:
ContractId
ContractId
y tenga el mismoWeekDays
WeekDays
de la semana individual coincide con un subconjunto principalWeekDays
de la agrupación anterior, fusionarse en esa agrupación anteriorWeekDays
de la semana coincida con un subconjunto finalWeekDays
de la siguiente agrupación, combínela en la siguiente agrupaciónfuente
(1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),
. Podría representarse como un intervalo, pero su solución produce dos. Admito, este ejemplo no estaba en los datos de la muestra y no es crítico. Intentaré ejecutar su solución en datos reales.No podía entender la lógica detrás de agrupar semanas con brechas, o semanas con fines de semana (por ejemplo, cuando hay dos semanas consecutivas con un fin de semana, ¿a qué semana va el fin de semana?).
La siguiente consulta produce el resultado deseado, excepto que solo agrupa los días laborables consecutivos y las semanas de domingo a sábado (en lugar de lunes a domingo). Si bien no es exactamente lo que desea, tal vez esto pueda proporcionar algunas pistas para una estrategia diferente. La agrupación de días viene de aquí . Las funciones de ventanas utilizadas deberían funcionar con SQLServer 2008, pero no tengo esa versión para probar si realmente funciona.
Resultado
fuente
En aras de la integridad, aquí hay un pase de dos
gaps-and-islands
enfoque de que probé antes de hacer esta pregunta.Mientras lo probaba en los datos reales, encontré pocos casos en los que producía resultados incorrectos y lo solucioné.
Aquí está el algoritmo:
CTE_ContractDays
,CTE_DailyRN
,CTE_DailyIslands
) y calcular un número de la semana para cada fecha de inicio y finalización de una isla. Aquí el número de semana se calcula asumiendo que el lunes es el primer día de la semana.CTE_Weeks
).CTE_FirstResult
).WeekDays
(CTE_SecondRN
,CTE_Schedules
).Maneja bien los casos cuando no hay interrupción en los patrones semanales (1, 7, 8, 10, 12). Maneja bien los casos cuando el patrón tiene días no secuenciales (3).
Pero, desafortunadamente, genera intervalos adicionales para semanas parciales (2, 3, 5, 6, 9, 11).
Resultado
Solución basada en el cursor
Convertí mi código C # en un algoritmo basado en el cursor, solo para ver cómo se compara con otras soluciones en datos reales. Confirma que es mucho más lento que otros enfoques basados en conjuntos o recursivos, pero genera un resultado óptimo.
fuente
Me sorprendió un poco que la solución del cursor de Vladimir fuera tan lenta, así que también intenté optimizar esa versión. Confirmé que usar un cursor también era muy lento para mí.
Sin embargo, a costa de utilizar la funcionalidad no documentada en SQL Server al agregar una variable al procesar un conjunto de filas, pude crear una versión simplificada de esta lógica que produce el resultado óptimo y se ejecuta mucho más rápido que el cursor y mi solución original . Así que úselo bajo su propio riesgo, pero presentaré la solución en caso de que sea de su interés. También sería posible actualizar la solución para usar un
WHILE
bucle de uno al número de fila máximo, buscando el siguiente número de fila en cada iteración del bucle. Esto se adheriría a una funcionalidad totalmente documentada y confiable, pero violaría la restricción (algo artificial) del problema queWHILE
bucles no están permitidos.Tenga en cuenta que si se permitió el uso de SQL 2014, es probable que un procedimiento almacenado compilado de forma nativa que recorra los números de fila y acceda a cada número de fila en una tabla optimizada en memoria sería una implementación de esta misma lógica que se ejecutaría más rápidamente.
Aquí está la solución completa , que incluye expandir los datos de prueba establecidos a aproximadamente medio millón de filas. La nueva solución se completa en unos 3 segundos y, en mi opinión, es mucho más concisa y legible que la solución anterior que ofrecí. Desglosaré los tres pasos involucrados aquí:
Paso 1: preprocesamiento
Primero agregamos un número de fila al conjunto de datos, en el orden en que procesaremos los datos. Al hacerlo, también convertimos cada dowInt en una potencia de 2 para que podamos usar un mapa de bits para representar qué días se han observado en cualquier grupo dado:
Paso 2: Recorriendo los días del contrato para identificar nuevas agrupaciones
Luego hacemos un ciclo sobre los datos, en orden por número de fila. Calculamos solo la lista de números de fila que forman el límite de una nueva agrupación, luego sacamos esos números de fila en una tabla:
Paso 3: Calcular los resultados finales en función de los números de fila de cada límite de agrupación
Luego calculamos las agrupaciones finales mediante el uso de los límites identificados en el bucle anterior para agregar todas las fechas que se incluyen en cada agrupación:
fuente
WHILE
bucles, porque ya sabía cómo resolverlo con el cursor y quería encontrar una solución basada en conjuntos. Además, sospechaba que el cursor sería lento (especialmente con un bucle anidado). Esta respuesta es muy interesante en términos de aprender nuevos trucos y agradezco sus esfuerzos.La discusión seguirá el código.
@Helper
es hacer frente a esta regla:Me permite enumerar los nombres de los días, en orden de número de día, entre dos días. Esto se utiliza al decidir si debe comenzar un nuevo intervalo. Lo relleno con valores de dos semanas para que sea más fácil codificar un fin de semana.
Hay formas más limpias de implementar esto. Una tabla completa de "fechas" sería una. Probablemente también haya una manera inteligente con el número de día y la aritmética de módulo.
El CTE
MissingDays
es generar una lista de nombres de días entre dos días. Se maneja de esta manera torpe porque el CTE recursivo (siguiente) no permite agregados, TOP () u otros operadores. Esto es poco elegante, pero funciona.CTE
Numbered
es hacer cumplir una secuencia conocida y sin brechas en los datos. Evita muchas comparaciones más tarde.CTE
Incremented
es donde sucede la acción. En esencia, uso un CTE recursivo para recorrer los datos y hacer cumplir las reglas. El número de fila generado enNumbered
(arriba) se utiliza para impulsar el procesamiento recursivo.La semilla del CTE recursivo simplemente obtiene la primera fecha para cada ContractID e inicializa los valores que se utilizarán para decidir si se requiere un nuevo intervalo.
Decidir si debe comenzar un nuevo intervalo requiere la fecha de inicio del intervalo actual, la lista de días y la duración de cualquier intervalo en las fechas del calendario. Estos pueden reiniciarse o llevarse adelante, dependiendo de la decisión. Por lo tanto, la parte recursiva es detallada y un poco repetitiva, ya que tenemos que decidir si comenzar un nuevo intervalo para más de un valor de columna.
La lógica de decisión para las columnas
WeekDays
yIntervalStart
debe tener la misma lógica de decisión: se puede cortar y pegar entre ellas. Si la lógica para comenzar un nuevo intervalo fuera a cambiar, este es el código a alterar. Idealmente, sería abstraído, por lo tanto; hacer esto en un CTE recursivo puede ser un desafío.La
EXISTS()
cláusula es la consecuencia de no poder utilizar funciones agregadas en un CTE recursivo. Todo lo que hace es ver si los días que caen dentro de un intervalo ya están en el intervalo actual.No hay nada mágico en la anidación de las cláusulas lógicas. Si es más claro en otra conformación, o usando CASOS anidados, por ejemplo, no hay razón para mantenerlo de esta manera.
La final
SELECT
es dar la salida en el formato deseado.Tener el PK activado
Src.ID
no es útil para este método. Un índice agrupado en(ContractID,dt)
sería bueno, creo.Hay algunos bordes ásperos. Los días no se devuelven en secuencia dow, pero en la secuencia de calendario aparecen en los datos de origen. Todo lo que tiene que ver con @Helper es inconstante y podría suavizarse. Me gusta la idea de usar un bit por día y usar funciones binarias en lugar de
LIKE
. Sin duda, ayudaría separar algunos CTE auxiliares en la tabla temporal con índices adecuados.Uno de los desafíos con esto es que una "semana" no se alinea con un calendario estándar, sino que es impulsada por los datos y se restablece cuando se determina que debe comenzar un nuevo intervalo. Una "semana", o al menos un intervalo, puede durar desde un día hasta abarcar todo el conjunto de datos.
Por el bien de los intereses, aquí están los costos estimados contra los datos de muestra de Geoff (¡gracias por eso!) Después de varios cambios:
El número estimado y real de filas difiere enormemente.
El plan tiene una tabla spoo, probablemente como resultado del CTE recursivo. La mayor parte de la acción está en una mesa de trabajo que sale de eso:
¡Justo como se implementa el recursivo, supongo!
fuente
MAX(g.IntervalStart)
parece extraño, porqueg.IntervalStart
está en elGROUP BY
. Esperaba que diera un error de sintaxis, pero funciona. Debería ser solog.IntervalStart as StartDT
adentroSELECT
? Og.IntervalStart
no debe estar en elGROUP BY
?MissingDays
yNumbered
son reemplazados por tablas temporales con índices adecuados, podría tener un rendimiento decente. ¿Qué índices recomendarías? Podría intentarlo mañana por la mañana.Numbered
con una tabla temporal e índice agrupado en(ContractID, rn)
valdría la pena . Sin un gran conjunto de datos para generar el plan correspondiente, es difícil de adivinar. La fisicalizaciónMissingDates
con índices también(StartDay, FollowingDayInt)
sería buena.