Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Exibir a data do próximo evento


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.