Seu primeiro passo é obter as datas de início do evento com cada evento e o intervalo de repetição, para fazer isso, você pode usar:
SELECT EventID = e.ID,
e.Name,
StartDateTime = DATEADD(SECOND, rs.Meta_Value, '19700101'),
RepeatInterval = ri.Meta_Value
FROM dbo.Events e
INNER JOIN dbo.Events_Meta rs
ON rs.Event_ID = e.ID
AND rs.Meta_Key = 'repeat_start'
INNER JOIN dbo.Events_Meta ri
ON ri.Event_ID = e.ID
AND ri.Meta_Key = 'repeat_interval_' + CAST(e.ID AS VARCHAR(10));
Isto dá:
EventID | Name | StartDateTime | RepeatInterval
--------+--------------+---------------------+-----------------
1 | Billa Vist | 2014-01-03 10:00:00 | 604800
1 | Billa Vist | 2014-01-04 18:00:00 | 604800
Para que isso se repita, você precisará de uma tabela de números para a junção cruzada, se você não tiver uma, existem várias maneiras de gerar uma em tempo real, por motivos de simplicidade, usarei:
WITH Numbers AS
( SELECT Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
)
SELECT Number
FROM Numbers;
Para uma leitura mais aprofundada, Aaron Bertrand fez algumas comparações profundas de maneiras de gerar listas sequenciais de números:
- Gerar um conjunto ou sequência sem loops – parte1
- Gerar um conjunto ou sequência sem loops – parte2
- Gerar um conjunto ou sequência sem loops – parte3
Se limitarmos nossa tabela de números a apenas 0 - 5, e olharmos apenas para o primeiro evento, a junção cruzada dos dois resultará em:
EventID | Name | StartDateTime | RepeatInterval | Number
--------+--------------+---------------------+----------------+---------
1 | Billa Vist | 2014-01-03 10:00:00 | 604800 | 0
1 | Billa Vist | 2014-01-03 10:00:00 | 604800 | 1
1 | Billa Vist | 2014-01-03 10:00:00 | 604800 | 2
1 | Billa Vist | 2014-01-03 10:00:00 | 604800 | 3
1 | Billa Vist | 2014-01-03 10:00:00 | 604800 | 4
1 | Billa Vist | 2014-01-03 10:00:00 | 604800 | 5
Então você pode obter sua ocorrência adicionando
RepeatInterval * Number
para a hora de início do evento:DECLARE @EndDate DATETIME = '20140130';
WITH EventData AS
( SELECT EventID = e.ID,
e.Name,
StartDateTime = DATEADD(SECOND, rs.Meta_Value, '19700101'),
RepeatInterval = ri.Meta_Value
FROM dbo.Events e
INNER JOIN dbo.Events_Meta rs
ON rs.Event_ID = e.ID
AND rs.Meta_Key = 'repeat_start'
INNER JOIN dbo.Events_Meta ri
ON ri.Event_ID = e.ID
AND ri.Meta_Key = 'repeat_interval_' + CAST(rs.ID AS VARCHAR(10))
), Numbers AS
( SELECT Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
)
SELECT e.EventID,
e.Name,
EventDate = DATEADD(SECOND, n.Number * e.RepeatInterval, e.StartDateTime)
FROM EventData e
CROSS JOIN Numbers n
WHERE DATEADD(SECOND, n.Number * e.RepeatInterval, e.StartDateTime) < @EndDate
ORDER BY e.EventID, EventDate;
Isso fornece sua saída esperada:
EVENTID | NAME | EVENTDATE
--------+---------------+--------------------------------
1 | Billa Vist | January, 03 2014 10:00:00+0000
1 | Billa Vist | January, 04 2014 18:00:00+0000
1 | Billa Vist | January, 10 2014 10:00:00+0000
1 | Billa Vist | January, 11 2014 18:00:00+0000
1 | Billa Vist | January, 17 2014 10:00:00+0000
1 | Billa Vist | January, 18 2014 18:00:00+0000
1 | Billa Vist | January, 24 2014 10:00:00+0000
1 | Billa Vist | January, 25 2014 18:00:00+0000
Exemplo no SQL Fiddle
Eu acho que o esquema que você tem é questionável, porém, a junção:
Meta_Key = 'repeat_interval_' + CAST(rs.ID AS VARCHAR(10))
é frágil na melhor das hipóteses. Acho que seria muito melhor armazenar a data de início e o intervalo de repetição associados a ela:
CREATE TABLE dbo.Events_Meta
( ID INT IDENTITY(1, 1) NOT NULL,
Event_ID INT NOT NULL,
StartDateTime DATETIME2 NOT NULL,
IntervalRepeat INT NULL, -- NULLABLE FOR SINGLE EVENTS
RepeatEndDate DATETIME2 NULL, -- NULLABLE FOR EVENTS THAT NEVER END
CONSTRAINT PK_Events_Meta__ID PRIMARY KEY (ID),
CONSTRAINT FK_Events_Meta__Event_ID FOREIGN KEY (Event_ID) REFERENCES dbo.Events (ID)
);
Isso simplificaria seus dados para:
EventID | StartDateTime | RepeatInterval | RepeatEndDate
--------+---------------------+----------------+---------------
1 | 2014-01-03 10:00:00 | 604800 | NULL
1 | 2014-01-04 18:00:00 | 604800 | NULL
Também permite que você adicione uma data de término à sua repetição, ou seja, se você quiser que ela seja repetida apenas por uma semana. Isso, então, sua consulta se simplifica para:
DECLARE @EndDate DATETIME = '20140130';
WITH Numbers AS
( SELECT Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
FROM sys.all_objects a
)
SELECT e.ID,
e.Name,
EventDate = DATEADD(SECOND, n.Number * em.IntervalRepeat, em.StartDateTime)
FROM Events e
INNER JOIN Events_Meta em
ON em.Event_ID = e.ID
CROSS JOIN Numbers n
WHERE DATEADD(SECOND, n.Number * em.IntervalRepeat, em.StartDateTime) <= @EndDate
AND ( DATEADD(SECOND, n.Number * em.IntervalRepeat, em.StartDateTime) <= em.RepeatEndDate
OR em.RepeatEndDate IS NULL
)
ORDER BY EventDate;
Exemplo no SQL Fiddle
Não vou dar meu esquema completo de como consegui isso no passado, mas vou dar um exemplo bem reduzido, a partir do qual você pode construir o seu próprio. Vou adicionar apenas um exemplo para um evento que ocorre semanalmente de segunda a sexta:
No ER RepeatEvent acima armazena as informações básicas para o evento recorrente, dependendo do tipo de repetição (diário, semanal, mensal) uma ou mais das outras tabelas são preenchidas. No exemplo de um evento semanal, ele armazenaria todos os dias da semana em que se repete na tabela
RepeatDay
. Se isso precisasse ser limitado a apenas alguns meses, você poderia armazenar esses meses em RepeatMonth
, e assim por diante. Em seguida, usando uma tabela de calendário, você pode obter todas as datas possíveis após a primeira data e limitá-las apenas às datas que correspondem ao dia da semana/mês do ano etc:
WITH RepeatingEvents AS
( SELECT e.Name,
re.StartDateTime,
re.EndDateTime,
re.TimesToRepeat,
RepeatEventDate = CAST(c.DateKey AS DATETIME) + CAST(re.StartTime AS DATETIME),
RepeatNumber = ROW_NUMBER() OVER(PARTITION BY re.RepeatEventID ORDER BY c.Datekey)
FROM dbo.Event e
INNER JOIN dbo.RepeatEvent re
ON e.EventID = re.EventID
INNER JOIN dbo.RepeatType rt
ON rt.RepeatTypeID = re.RepeatTypeID
INNER JOIN dbo.Calendar c
ON c.DateKey >= re.StartDate
INNER JOIN dbo.RepeatDayOfWeek rdw
ON rdw.RepeatEventID = re.RepeatEventID
AND rdw.DayNumberOfWeek = c.DayNumberOfWeek
WHERE rt.Name = 'Weekly'
)
SELECT Name, StartDateTime, RepeatEventDate, RepeatNumber
FROM RepeatingEvents
WHERE (TimesToRepeat IS NULL OR RepeatNumber <= TimesToRepeat)
AND (EndDateTime IS NULL OR RepeatEventDate <= EndDateTime);
Exemplo no SQL Fiddle
Esta é apenas uma representação muito básica de como eu implementei, por exemplo, eu realmente usei visualizações inteiras de qualquer consulta para os dados repetidos para que qualquer evento sem entradas em
RepeatDayOfWeek
seria assumido para repetir todos os dias, em vez de nunca. Junto com todos os outros detalhes nesta e em outras respostas, esperamos que você tenha mais do que o suficiente para começar.