Eu preciso converter dados entre dois sistemas.
O primeiro sistema armazena agendas como uma lista simples de datas. Cada data incluída na agenda é uma linha. Pode haver várias lacunas na sequência de datas (fins de semana, feriados e pausas mais longas, alguns dias da semana podem ser excluídos da programação). Não pode haver lacunas, mesmo os finais de semana podem ser incluídos. O cronograma pode durar até 2 anos. Geralmente leva algumas semanas.
Aqui está um exemplo simples de uma programação que se estende por duas semanas, excluindo fins de semana (há exemplos mais complicados no script abaixo):
+----+------------+------------+---------+--------+
| 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
é único, mas não é necessariamente seqüencial (é a chave primária). As datas são únicas em cada Contrato (existe um índice exclusivo (ContractID, dt)
).
O segundo sistema armazena agendamentos como intervalos com a lista de dias da semana que fazem parte do agendamento. Cada intervalo é definido por suas datas de início e término (inclusive) e uma lista dos dias da semana incluídos na programação. Nesse formato, você pode definir com eficiência padrões semanais repetitivos, como seg-qua, mas isso se torna um problema quando um padrão é interrompido, por exemplo, por feriado.
Aqui está como será o exemplo simples acima:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 1 | 2016-05-02 | 2016-05-13 | 10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
[StartDT;EndDT]
intervalos que pertencem ao mesmo contrato não devem se sobrepor.
Preciso converter dados do primeiro sistema no formato usado pelo segundo sistema. No momento, estou resolvendo isso no lado do cliente em C # para o contrato fornecido, mas eu gostaria de fazê-lo em T-SQL no servidor para processamento em massa e exportação / importação entre servidores. Provavelmente, isso poderia ser feito usando o CLR UDF, mas neste estágio não posso usar o SQLCLR.
O desafio aqui é tornar a lista de intervalos o mais curta e amigável possível.
Por exemplo, este cronograma:
+-----+------------+------------+---------+--------+
| 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 |
+-----+------------+------------+---------+--------+
deve se tornar o seguinte:
+------------+------------+------------+----------+----------------------+
| ContractID | StartDT | EndDT | DayCount | WeekDays |
+------------+------------+------------+----------+----------------------+
| 2 | 2016-05-05 | 2016-05-17 | 9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+
,isso não:
+------------+------------+------------+----------+----------------------+
| 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, |
+------------+------------+------------+----------+----------------------+
Eu tentei aplicar uma gaps-and-islands
abordagem para esse problema. Eu tentei fazer isso em dois passes. No primeiro passo, encontro ilhas de simples dias consecutivos, ou seja, o fim da ilha é qualquer lacuna na sequência de dias, seja no final de semana, feriado ou qualquer outra coisa. Para cada ilha encontrada, construo uma lista separada por vírgulas de distintas WeekDays
. Na segunda passagem, o grupo encontrei ilhas ainda mais, observando a lacuna na sequência dos números das semanas ou uma mudança no WeekDays
.
Com essa abordagem, cada semana parcial termina como um intervalo extra, como mostrado acima, porque, embora os números das semanas sejam consecutivos, a WeekDays
mudança. Além disso, pode haver intervalos regulares dentro de uma semana (veja ContractID=3
dados de amostra, que possuem apenas dados Mon,Wed,Fri,
) e essa abordagem geraria intervalos separados para cada dia em tal cronograma. Pelo lado positivo, ele gera um intervalo se o agendamento não tiver nenhuma lacuna (consulte ContractID=7
os dados de amostra que incluem fins de semana) e, nesse caso, não importa se a semana de início ou fim é parcial.
Veja outros exemplos no script abaixo para ter uma idéia melhor do que estou procurando. Você pode ver que muitas vezes os fins de semana são excluídos, mas outros dias da semana também podem ser excluídos. No único exemplo 3 Mon
, Wed
e Fri
são parte do calendário. Além disso, os fins de semana podem ser incluídos, como no exemplo 7. A solução deve tratar todos os dias da semana igualmente. Qualquer dia da semana pode ser incluído ou excluído da programação.
Para verificar se a lista de intervalos gerada descreve corretamente o agendamento especificado, você pode usar o seguinte pseudocódigo:
- percorrer todos os intervalos
- para cada loop de intervalo em todas as datas do calendário entre as datas de início e término (inclusive).
- para cada data, verifique se o dia da semana está listado em
WeekDays
. Se sim, então esta data está incluída na programação.
Felizmente, isso esclarece em que casos um novo intervalo deve ser criado. Nos exemplos 4 e 5, uma segunda-feira ( 2016-05-09
) é removida do meio da agenda e essa agenda não pode ser representada por um único intervalo. No exemplo 6, há um longo intervalo no planejamento, portanto, são necessários dois intervalos.
Intervalos representam padrões semanais no planejamento e quando um padrão é interrompido / alterado, o novo intervalo deve ser adicionado. No exemplo 11, as primeiras três semanas têm um padrão Tue
, então esse padrão muda para Thu
. Como resultado, precisamos de dois intervalos para descrever esse cronograma.
Estou usando o SQL Server 2008 no momento, portanto, a solução deve funcionar nesta versão. Se uma solução para o SQL Server 2008 puder ser simplificada / aprimorada usando recursos de versões posteriores, isso é um bônus, por favor, mostre-a também.
Eu tenho uma Calendar
tabela (lista de datas) e Numbers
tabela (lista de números inteiros a partir de 1); portanto, não há problema em usá-los, se necessário. Também é bom criar tabelas temporárias e ter várias consultas que processam dados em vários estágios. O número de estágios em um algoritmo precisa ser corrigido, porém, cursores e WHILE
loops explícitos não estão OK.
Script para dados de amostra e 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;
Comparação de respostas
A tabela real @Src
possui 403,555
linhas com 15,857
distintas ContractIDs
. Todas as respostas produzem resultados corretos (pelo menos para os meus dados) e todas são razoavelmente rápidas, mas diferem em termos de otimização. Quanto menos intervalos gerados, melhor. Incluí tempos de execução apenas por curiosidade. O foco principal é o resultado correto e ideal, não a velocidade (a menos que demore muito - parei a consulta não recursiva de Ziggy Crueltyfree Zeitgeister após 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 | | |
+--------------------------------------------------------+-----------+---------+
fonte
(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');
No @Dst não deve haver uma linha comTue, Thu,
?@Dst
). As primeiras duas semanas da programação têm apenasTue
, portanto você não pode terWeekDays=Tue,Thu,
essas semanas. As últimas duas semanas da programação têm apenasThu
, portanto, novamente, você não pode terWeekDays=Tue,Thu,
essas semanas. A solução subótima para isso seria de três linhas: apenasTue
nas duas primeiras semanas, depoisTue,Thu,
na terceira semana que possui as duasTue
eThu
, apenasThu
nas duas últimas semanas.ContractID
alteração, se o intervalo vai além de sete dias e o novo dia da semana não foi visto antes, se houver uma lacuna na lista de dias programados.Respostas:
Este usa um CTE recursivo. Seu resultado é idêntico ao exemplo da pergunta . Foi um pesadelo inventar ... O código inclui comentários para facilitar sua lógica complicada.
Outra estratégia
Este deve ser significativamente mais rápido que o anterior, porque não depende da CTE recursiva limitada lenta no SQL Server 2008, embora implemente mais ou menos a mesma estratégia.
Existe um
WHILE
loop (eu não conseguia imaginar uma maneira de evitá-lo), mas vale para um número reduzido de iterações (o número mais alto de sequências (menos uma) em qualquer contrato).É uma estratégia simples e pode ser usada para sequências menores ou maiores que uma semana (substituindo qualquer ocorrência da constante 7 por qualquer outro número e
dowBit
calculada a partir do MODULUS x emDayNo
vez deDATEPART(wk)
) e até 32.fonte
Não é exatamente o que você está procurando, mas talvez possa ser do seu interesse.
A consulta cria semanas com uma sequência separada por vírgula para os dias usados em cada semana. Em seguida, ele encontra as ilhas de semanas consecutivas que usam o mesmo padrão em
Weekdays
.Resultado:
ContractID = 2
mostra qual a diferença no resultado é comparada com o que você deseja. A primeira e a última semana serão tratadas como períodos separados, uma vez queWeekDays
é diferente.fonte
WeekDays
como um número de 7 bits. Apenas 128 combinações. Existem apenas 128 * 128 = 16384 pares possíveis. Crie uma tabela temporária com todos os pares possíveis e, em seguida, descubra um algoritmo baseado em conjuntos que marcará quais pares podem ser mesclados: um padrão de uma semana é "coberto" por um padrão da semana seguinte. Associe-se automaticamente ao resultado semanal atual (já que não existeLAG
em 2008) e use essa tabela temporária para decidir quais pares serão mesclados ... Não tenho certeza se essa ideia tem algum mérito.Acabei com uma abordagem que produz a solução ideal nesse caso e acho que vai se sair bem em geral. A solução é bastante longa, no entanto, seria interessante ver se outra pessoa tem uma abordagem diferente e mais concisa.
Aqui está um script que contém a solução completa .
E aqui está um esboço do algoritmo:
ContractId
ContractId
e têm a mesmaWeekDays
WeekDays
da semana única corresponda a um subconjunto principal doWeekDays
agrupamento anterior, mescle-o ao agrupamento anteriorWeekDays
da semana única corresponda a um subconjunto à direitaWeekDays
do próximo agrupamento, mescle no próximo agrupamentofonte
(1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),
. Pode ser representado como um intervalo, mas sua solução produz dois. Admito que este exemplo não estava nos dados da amostra e não é crítico. Vou tentar executar sua solução em dados reais.Eu não conseguia entender a lógica por trás do agrupamento de semanas com intervalos ou semanas com fins de semana (por exemplo, quando há duas semanas consecutivas com um final de semana, em que semana o fim de semana vai?).
A consulta a seguir produz a saída desejada, exceto pelo fato de agrupar apenas dias úteis da semana consecutivos e agrupar semanas Dom-Sáb (em vez de Seg-Dom). Embora não seja exatamente o que você deseja, talvez isso possa fornecer algumas pistas para uma estratégia diferente. O agrupamento de dias vem daqui . As funções de janelas usadas devem funcionar com o SQLServer 2008, mas não tenho essa versão para testar se realmente funciona.
Resultado
fonte
Por uma questão de exaustividade, eis uma
gaps-and-islands
abordagem de duas passagens que eu tentei antes de fazer essa pergunta.Como eu estava testando nos dados reais, encontrei alguns casos quando produzia resultados incorretos e os corrigi.
Aqui está o algoritmo:
CTE_ContractDays
,CTE_DailyRN
,CTE_DailyIslands
) e calcular um número da semana cada data de início e término de uma ilha para. Aqui o número da semana é calculado assumindo que segunda-feira é o primeiro dia da semana.CTE_Weeks
).CTE_FirstResult
).WeekDays
(CTE_SecondRN
,CTE_Schedules
).Ele lida com casos bem quando não há interrupção nos padrões semanais (1, 7, 8, 10, 12). Ele lida com casos bem quando o padrão tem dias não sequenciais (3).
Infelizmente, porém, gera intervalos extras por semanas parciais (2, 3, 5, 6, 9, 11).
Resultado
Solução baseada em cursor
Eu converti meu código C # em um algoritmo baseado em cursor, apenas para ver como ele se compara a outras soluções em dados reais. Ele confirma que é muito mais lento que outras abordagens baseadas em conjuntos ou recursivas, mas gera um resultado ideal.
fonte
Fiquei um pouco surpreso que a solução do cursor do Vladimir fosse tão lenta, então também tentei otimizar essa versão. Confirmei que o uso de um cursor também era muito lento para mim.
No entanto, com o custo de usar a funcionalidade não documentada no SQL Server anexando a uma variável durante o processamento de um conjunto de linhas, consegui criar uma versão simplificada dessa lógica que produz o resultado ideal e executa muito mais rapidamente do que o cursor e minha solução original . Portanto, use por sua conta e risco, mas apresentarei a solução caso seja de seu interesse. Também seria possível atualizar a solução para usar um
WHILE
loop de um para o número máximo de linhas, buscando o número da próxima linha a cada iteração do loop. Isso manteria a funcionalidade totalmente documentada e confiável, mas violaria a restrição (um tanto artificial) declarada do problema de queWHILE
loops não são permitidos.Observe que, se o uso do SQL 2014 for permitido, é provável que um procedimento armazenado compilado nativamente que faça um loop sobre os números de linha e acesse cada número de linha em uma tabela com otimização de memória seja uma implementação dessa mesma lógica que seria executada mais rapidamente.
Aqui está a solução completa , incluindo a expansão dos dados de teste estabelecidos para cerca de meio milhão de linhas. A nova solução é concluída em cerca de 3 segundos e, na minha opinião, é muito mais concisa e legível do que a solução anterior que ofereci. Vou detalhar as três etapas envolvidas aqui:
Etapa 1: pré-processamento
Primeiro, adicionamos um número de linha ao conjunto de dados, na ordem em que processamos os dados. Ao fazer isso, também convertemos cada dowInt em uma potência de 2, para que possamos usar um bitmap para representar quais dias foram observados em um determinado agrupamento:
Etapa 2: fazer um loop nos dias contratuais para identificar novos agrupamentos
Em seguida, fazemos um loop sobre os dados, em ordem pelo número da linha. Calculamos apenas a lista de números de linhas que formam o limite de um novo agrupamento e, em seguida, produzimos esses números de linha em uma tabela:
Etapa 3: Computando os resultados finais com base nos números de linha de cada limite de agrupamento
Em seguida, calculamos os agrupamentos finais usando os limites identificados no loop acima para agregar todas as datas que se enquadram em cada agrupamento:
fonte
WHILE
loops, porque já sabia como resolvê-lo com o cursor e queria encontrar uma solução baseada em conjunto. Além disso, eu suspeitava que o cursor fosse lento (especialmente com um loop aninhado). Esta resposta é muito interessante em termos de aprender novos truques e agradeço seus esforços.A discussão seguirá o código.
@Helper
é lidar com esta regra:Permite-me listar os nomes dos dias, em ordem de número de dias, entre dois dias. Isso é usado para decidir se um novo intervalo deve começar. Eu o preencho com valores de duas semanas para facilitar a codificação de um final de semana.
Existem maneiras mais limpas de implementar isso. Uma tabela completa de "datas" seria uma. Provavelmente também há uma maneira inteligente com o número do dia e a aritmética do módulo.
O CTE
MissingDays
deve gerar uma lista de nomes de dias entre dois dias. É tratado dessa maneira desajeitada porque o CTE recursivo (a seguir) não permite agregados, TOP () ou outros operadores. Isso é deselegante, mas funciona.CTE
Numbered
deve aplicar uma sequência conhecida e sem falhas nos dados. Evita muitas comparações mais tarde.CTE
Incremented
é onde a ação acontece. Em essência, eu uso uma CTE recursiva para percorrer os dados e aplicar as regras. O número da linha gerado emNumbered
(acima) é usado para conduzir o processamento recursivo.A semente do CTE recursivo simplesmente obtém a primeira data para cada ContractID e inicializa valores que serão usados para decidir se um novo intervalo é necessário.
Decidir se um novo intervalo deve iniciar requer a data de início do intervalo atual, a lista de dias e a duração de qualquer intervalo nas datas do calendário. Estes podem ser redefinidos ou transportados, dependendo da decisão. Portanto, a parte recursiva é detalhada e um pouco repetitiva, pois temos que decidir se devemos iniciar um novo intervalo para mais de um valor de coluna.
A lógica de decisão para colunas
WeekDays
eIntervalStart
deve ter a mesma lógica de decisão - pode ser recortada e colada entre elas. Se a lógica para iniciar um novo intervalo mudar, esse é o código a ser alterado. Idealmente, seria abstraído, portanto; fazer isso em uma CTE recursiva pode ser desafiador.A
EXISTS()
cláusula é o resultado de não poder usar funções agregadas em um CTE recursivo. Tudo o que faz é ver se os dias que estão dentro de uma lacuna já estão no intervalo atual.Não há nada mágico sobre o aninhamento das cláusulas lógicas. Se for mais claro em outra conformação ou usando CASEs aninhados, digamos, não há razão para mantê-lo dessa maneira.
A final
SELECT
é fornecer a saída no formato desejado.Ter a PK ativada
Src.ID
não é útil para este método. Um índice clusterizado em(ContractID,dt)
seria bom, eu acho.Existem algumas arestas. Os dias não são retornados na sequência do dow, mas na sequência do calendário eles aparecem nos dados de origem. Tudo a ver com @Helper é desajeitado e pode ser suavizado. Eu gosto da idéia de usar um bit por dia e usar funções binárias em vez de
LIKE
. Separar alguns dos CTEs auxiliares na tabela temporária com índices adequados sem dúvida ajudaria.Um dos desafios disso é que uma "semana" não se alinha com um calendário padrão, mas é orientada pelos dados e redefine quando é determinado que um novo intervalo deve começar. Uma "semana", ou pelo menos um intervalo, pode durar de um dia até o conjunto de dados inteiro.
Por uma questão de interesse, eis os custos estimados em relação aos dados de amostra de Geoff (obrigado por isso!) Após várias alterações:
O número estimado e real de linhas diferem bastante.
O plano possui uma tabela, provavelmente como resultado da CTE recursiva. A maior parte da ação está em uma mesa de trabalho que:
Do jeito que a recursiva é implementada, eu acho!
fonte
MAX(g.IntervalStart)
parece estranho, porqueg.IntervalStart
está noGROUP BY
. Eu esperava que desse um erro de sintaxe, mas funciona. Deve ser apenasg.IntervalStart as StartDT
dentroSELECT
? Oug.IntervalStart
não deveria estar noGROUP BY
?MissingDays
eNumbered
forem substituídos por tabelas temporárias com índices adequados, ele possa ter um desempenho decente. Quais índices você recomendaria? Eu poderia tentar amanhã de manhã.Numbered
por uma tabela temporária e um índice clusterizado(ContractID, rn)
valeria a pena. Sem um grande conjunto de dados para gerar o plano correspondente, é difícil adivinhar. FisicalizarMissingDates
com índices(StartDay, FollowingDayInt)
também seria bom.