Отобразить следующую дату события

Дизайн пользовательского интерфейса для хранения метаданных событий и событий

enter image description here

SQL TABLE DESIGN -

CREATE TABLE [dbo].[EVENTS]
([ID] [int] IDENTITY(1,1) NOT NULL,
 [Name] [nvarchar](255) NOT NULL)

и

CREATE TABLE [dbo].[EVENTS_META](
[ID] [int] IDENTITY(1,1) NOT NULL,
[event_id] [int] NOT NULL,
[meta_key] [varchar](255) NOT NULL,
[meta_value] [bigint] NOT NULL)

Данные о событиях Event

Метаданные события Events_Meta table

Я следил за Повторяя события календаря и некоторые окончательные математические данные, и я написал следующий запрос

СПИСОК ВСЕ СОБЫТИЯ СРОКИ ДО ДАЛЬШЕ КОНЕЧНОГО ДАТА

SELECT EV.*
FROM events AS EV
RIGHT JOIN events_meta AS EM1 ON EM1.event_id = EV.id
RIGHT JOIN events_meta AS EM2 ON EM2.meta_key = 'repeat_interval_'+ CAST(EM1.id as Varchar(100))
WHERE EM1.meta_key = 'repeat_start'
AND ((1391040000 - EM1.meta_value ) % EM2.meta_value) = 0

Я ничего не получаю. Я хочу показать все даты после repeat_start с заданным интервалом.

Пример здесь 1-е событие начинается (3 января 2014 г., 10:00) unixtimestamp = 1388743200 и продолжается каждую пятницу (7 дней), мы также планируем первое мероприятие в субботу (Jan 04, 2014) 1388858400 и продолжаем раз в 7 дней (суббота)

Это может быть один раз в месяц/день/и т.д. Таким образом, мы имеем interval, определенные как секунды.

Если я даю некоторый вклад, например, 30 января 2014 года, i.e =1391040000 (30 января 2014 00:00:00)

Ожидаемый результат

Billa Visit, 3 Jan 2014 10 A.M

Billa Visit, 4 Jan 2014 10 A.M

Billa Visit, 10 Jan 2014 10 A.M

Billa Visit, 11 января 2014 г. 10 A.M

Billa Visit, 17 Jan 2014 10 A.M

Billa Visit, 18 января 2014 г. 10 A.M

Billa Visit, 24 января 2014 г. 10 A.M

Billa Visit, 25 января 2014 г. 10 A.M

SQL FIDDLE LINK

Ответ 1

Ваш первый шаг - получить даты начала мероприятия с каждым событием и интервал повторения, чтобы сделать это, вы можете использовать:

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));

Это дает:

EventID | Name         | StartDateTime       | RepeatInterval
--------+--------------+---------------------+-----------------
   1    | Billa Vist   | 2014-01-03 10:00:00 |     604800
   1    | Billa Vist   | 2014-01-04 18:00:00 |     604800

Чтобы это повторилось, вам понадобится таблица чисел для перекрестного соединения, если у вас ее нет, есть несколько способов генерировать "на лету", для простоты я буду использовать:

WITH Numbers AS
(   SELECT  Number = ROW_NUMBER() OVER(ORDER BY a.object_id) - 1
    FROM    sys.all_objects a
)
SELECT  Number
FROM    Numbers;

Для дальнейшего чтения Аарон Бертран провел несколько глубоких сравнений способов генерации последовательных списков чисел:

Если мы ограничим нашу таблицу чисел только 0 - 5 и посмотрим только на первое событие, то перекрестное слияние двух даст:

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

Затем вы можете получить свое присутствие, добавив RepeatInterval * Number к времени начала события:

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;

Это дает ожидаемый результат:

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

Пример скрипта SQL


Я думаю, что схема, которую вы имеете, вызывает сомнения, присоединяется к:

Meta_Key = 'repeat_interval_' + CAST(rs.ID AS VARCHAR(10))

в лучшем случае хлипкий. Я думаю, вам было бы намного лучше сохранить дату начала и повторить интервал, связанный с ним вместе:

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)
);

Это упростит ваши данные:

EventID | StartDateTime       | RepeatInterval | RepeatEndDate
--------+---------------------+----------------+---------------
   1    | 2014-01-03 10:00:00 |    604800      |     NULL
   1    | 2014-01-04 18:00:00 |    604800      |     NULL

Он также позволяет вам добавить дату окончания для повтора, т.е. если вы хотите, чтобы она повторялась в течение одной недели. Это значит, что ваш запрос упрощает:

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;

Пример скрипта SQL


Я не дам вам свою полную схему того, как я это достиг в прошлом, но я приведу очень сокращенный пример, из которого вы можете надеяться создать свой собственный. Я добавлю пример для события, которое происходит еженедельно в Пн-Пт:

enter image description here

В приведенном выше ER RepeatEvent хранится основная информация для повторяющегося события, затем в зависимости от типа повтора (ежедневно, еженедельно, ежемесячно) заполняется одна или несколько других таблиц. В примере еженедельного события будут храниться все дни недели, в которые он будет повторяться в таблице RepeatDay. Если это нужно ограничивать только несколькими месяцами, вы можете сохранить эти месяцы в RepeatMonth и т.д.

Затем, используя таблицу календаря, вы можете получить все возможные даты после первой даты и ограничить их только теми датами, которые соответствуют день недели/месяца года и т.д.:

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);

Пример скрипта SQL

Это только очень простое представление о том, как я его реализовал, например, я фактически использовал полностью просматривает любой запрос для повторяющихся данных, так что любое событие без записей в RepeatDayOfWeek будет повторяться каждый день, а не никогда. Наряду со всеми остальными деталями в этом и других ответах вам, надеюсь, будет более чем достаточно, чтобы вы начали.

Ответ 2

Далее будут генерироваться события, основанные на описании StartEvent и MEta с CTE.

Измените значения MaxDate и MaxEvents в соответствии с значениями параметров.

declare @MaxDate datetime = convert(datetime,'12/2/2014', 101);
declare @MaxEvents integer=  200;

; With
    -- number generator by power of 2
    n2(n) as ( select 1 as n union all select 1),
    n4(n) as ( select 1 from n2 t1 cross join n2 t2 ),
    n16(n) as ( select 1 from n4 t1 cross join n4 t2 ),
    n256(n) as ( select 1 from n16 t1 cross join n16 t2 ),
    n65k(n) as ( select 1 from n256 t1 cross join n256 t2 ),
   Numbers (n) as (select row_number() over( order by n) from n65k ),

    -- Start of events 
    StartEvents as 
    ( SELECT 1 as EventNo, EV.Name, EM.ID, EM.Event_Id, EM.Meta_key, dateAdd(second,EM.meta_value,convert(datetime,'01/01/1970', 101)) as EventDate
        FROM events AS EV
        INNER JOIN events_meta  EM 
          ON EM.event_id = EV.id
          AND EM.meta_key = 'repeat_start'),
    -- Repeating events N times
    NextEvents AS
    ( SELECT Numbers.N+1 asEventNo, StartEvents.Name, EM.ID, EM.Event_Id, EM.Meta_key, dateAdd(second,EM.meta_value*Numbers.n,StartEvents.EventDate) as EventDate
        FROM StartEvents 
        INNER JOIN  events_meta EM 
            ON EM.event_id = StartEvents.event_id
            AND  EM.meta_key = 'repeat_interval_'+ ltrim(rtrim(str(StartEvents.ID )))
            AND ((1391040000- EM.meta_value ) % EM.meta_value) = 0 
     cross join Numbers 
     -- not to overflow (dateadd parameter is int type)
     Where Numbers.N < 3000
    )
   -- startEvents union nextEvents
  select EventNo, Name, Meta_key, EventDate
  FROM (
        Select * from StartEvents
        union all select * from NextEvents ) AllEvents
  where EventDate < @MaxDate
   and  EventNo < @MaxEvents
        order by ID ;

Ответ 3

Одним из способов достижения этого является использование цикла и вставка записи в временную таблицу. Затем вы можете просто выбрать из своей таблицы temp. В приведенном ниже примере я сохранил значение unixtimestamp, а также значение, преобразованное в datetime.

declare @enddate bigint, @intervalFactor int, @rowresult int

set @enddate = 1391040000

create table #Results
(
  eventid int,
  eventdate bigint,
  eventdatedate datetime
)

set @rowresult = 1
set @intervalFactor = 0

WHILE (@rowresult > 0)
BEGIN
  Insert #Results (eventid, eventdate, eventdatedate)
  Select events.id, date.meta_value + (intrvl.meta_value * @intervalFactor)
        ,DATEADD(ss,date.meta_value + (intrvl.meta_value * @intervalFactor), CAST('1970-01-01 00:00:00' AS datetime))
      from events
      inner join events_meta date
        on events.id = date.event_id
        AND date.meta_key = 'repeat_start'
      inner join events_meta intrvl
        on events.id = intrvl.event_id
        and intrvl.meta_key = 'repeat_interval_'+ CAST(date.id as Varchar(100))
  where ((@enddate - date.meta_value ) % intrvl.meta_value) >= 0
  and date.meta_value + (intrvl.meta_value * @intervalFactor) <= @enddate


  set @rowresult = @@rowcount
  set @intervalFactor = @intervalFactor + 1

END  

select * from #Results

Другим возможным решением этой проблемы было бы использовать рекурсивный CTE.

Ответ 4

Это сделает это!

WITH mycte AS
(
    SELECT A.ID, A.Name, A.StartDate, A.StartDate AS [IntervalDate], A.Interval, A.[repeat_startID], A.[repeat_intervalID]
    FROM (
            -- this is your provided query
            -- can you run this derived table only and make sure it return what you expect?
        SELECT
             EV.*
            ,EM1.id AS [repeat_startID]
            ,EM2.id AS [repeat_intervalID]
            -- I convert to datetime becuase Im more familiar with datatype=time manipulations
            ,DATEADD(SECOND,EM1.meta_value,'1970-01-01') AS [StartDate]
            ,EM2.meta_value AS [Interval]
        FROM [EVENTS] AS EV
            -- I used inner joins, please modify that as needed.
            INNER JOIN [EVENTS_META] AS EM1 ON EM1.meta_key = 'repeat_start' AND EM1.event_id = EV.id
            INNER JOIN [EVENTS_META] AS EM2 ON EM2.meta_key = 'repeat_interval_'+ CAST(EM1.id as Varchar(100))
    ) AS A
    UNION ALL
    SELECT  ID, Name, StartDate, DATEADD(SECOND,Interval,[IntervalDate]), Interval, [repeat_startID], [repeat_intervalID]
    FROM    mycte   
    WHERE   DATEADD(SECOND,1,[IntervalDate]) < '2014-01-30 00:00:00.000' -- this is your epoch timestamp
)
SELECT * FROM mycte 
-- it is unclear if the "cutoff" date is for the Last Interval Start Date or the next one
-- examining the results shows there are 2 records after your"cutoff" date
-- add a WHERE statement to fix this if needed?
-- WHERE [IntervalDate] < '2014-01-30 00:00:00.000' -- this is your epoch timestamp
ORDER BY [repeat_startID], StartDate;

-- produces: (Column #4 is what you are interested in)
1   Billy Visit 2014-01-03 10:00:00.000 2014-01-03 10:00:00.000 604800  1   2
1   Billy Visit 2014-01-03 10:00:00.000 2014-01-10 10:00:00.000 604800  1   2
1   Billy Visit 2014-01-03 10:00:00.000 2014-01-17 10:00:00.000 604800  1   2
1   Billy Visit 2014-01-03 10:00:00.000 2014-01-24 10:00:00.000 604800  1   2
1   Billy Visit 2014-01-03 10:00:00.000 2014-01-31 10:00:00.000 604800  1   2 -- occurs after '2014-01-30 00:00:00.000'
1   Billy Visit 2014-01-04 18:00:00.000 2014-01-04 18:00:00.000 604800  3   4
1   Billy Visit 2014-01-04 18:00:00.000 2014-01-11 18:00:00.000 604800  3   4
1   Billy Visit 2014-01-04 18:00:00.000 2014-01-18 18:00:00.000 604800  3   4
1   Billy Visit 2014-01-04 18:00:00.000 2014-01-25 18:00:00.000 604800  3   4
1   Billy Visit 2014-01-04 18:00:00.000 2014-02-01 18:00:00.000 604800  3   4 -- occurs after '2014-01-30 00:00:00.000'

Ответ 5

Этот код работает по своему желанию, все имена полей соответствуют вашему SQL FIDDLE, вывод должен немного измениться, чтобы отобразить ваш собственный формат времени. Я не смог найти преобразование по умолчанию для вашего формата.

http://www.sqlfiddle.com/#!3/057fe/1

Это решение использует CTE для рекурсивного создания новых строк данных, каждый из которых начинается с момента времени, а затем является приращением "Интервала" до тех пор, пока он не встретит дату окончания. После создания таблицы cte требуется простой запрос для извлечения ваших данных.

; -- previous statement must terminate w/ semicolon
WITH mycte AS
(
    SELECT A.ID, A.Name, A.StartDate, A.StartDate AS [IntervalDate], A.[Interval]
    FROM (
            -- this is your provided query
            SELECT EV.*
                -- I added two derived fields to simplify the recursion process.
                ,DATEADD(SECOND,EM1.meta_value,'1970-01-01') AS StartDate
                ,EM2.meta_value AS [Interval]
            FROM events AS EV
            RIGHT JOIN events_meta AS EM1 ON EM1.event_id = EV.id
            RIGHT JOIN events_meta AS EM2 ON EM2.meta_key = 'repeat_interval_'+ CAST(EM1.id as Varchar(100))
            WHERE EM1.meta_key = 'repeat_start'
    ) AS A
    UNION ALL
    SELECT  ID, Name, StartDate, DATEADD(SECOND,Interval,[IntervalDate]), [Interval]
    FROM    mycte   
    WHERE   DATEADD(SECOND,1,[IntervalDate]) < '2014-01-30 00:00:00.000' -- this is your epoch timestamp
)
SELECT Name + ', ' + CONVERT(VARCHAR,[IntervalDate],113)-- format your custom date as needed.
FROM mycte 
WHERE [IntervalDate] < '2014-01-30 00:00:00.000' -- this is your epoch timestamp
ORDER BY StartDate
OPTION (Maxrecursion 1000); -- default is 100 if not stated