【问题标题】:Display next event date显示下一个活动日期
【发布时间】:2013-11-29 12:53:42
【问题描述】:

用于存储事件和事件元数据的 UI 设计

SQL 表设计

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)

事件数据是

事件元数据是

我关注了Repeating calendar events and some final maths,我写了以下查询

列出给定结束日期之前的所有活动日期

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 之后的所有日期。

此处的示例第一个事件开始于(2014 年 1 月 3 日,上午 10 点)unixtimestamp =1388743200 并在每个星期五(7 天)继续,我们还将第一个事件安排在星期六开始(2014 年 1 月 4 日)1388858400 并且每 7 天继续一次天(星期六)

可以每月/每天/等一次。所以我们将interval 定义为秒。

如果我提供一些输入,例如 2014 年 1 月 30 日,i.e =1391040000(2014 年 1 月 30 日 00:00:00)

预期结果

比拉来访,2014 年 1 月 3 日上午 10 点

比拉来访,2014 年 1 月 4 日上午 10 点

比拉来访,2014 年 1 月 10 日上午 10 点

比拉来访,2014 年 1 月 11 日上午 10 点

比拉来访,2014 年 1 月 17 日上午 10 点

比拉来访,2014 年 1 月 18 日上午 10 点

比拉来访,2014 年 1 月 24 日上午 10 点

比拉来访,2014 年 1 月 25 日上午 10 点

SQL FIDDLE LINK

【问题讨论】:

  • 请注意,涉及 RIGHT JOIN 的线程在 SO 中极为罕见。 (目前 LEFT JOIN = 50,506 次点击 vs RIGHT JOIN = 2,337) - 只是说'
  • @Strawberry,很好。此查询是根据其他问题进行的。我不担心 LEFT 或 RIGHT :) 想要一些解决方案来找到这些日期
  • M$SQL 不是我的事,但是,FWIW,在尝试获取任何东西之前,我总是尝试从 EAV 构造一个(伪规范化)物化视图(甚至只是一个普通的香草视图)有用出来的。哦,顺便说一句,我的印象是,SO 的一些贡献者发现数据图像令人反感。请考虑提供适当的 DDL。
  • 由于这个问题仍然没有答案,我在下面发布了一个解决方案。如果这不是您所需要的,我可以根据您的反馈进行修改。
  • 添加了新的答案,带有全功能的 SQL FIDDLE。我在您的 SQL 小提琴中观察到您的查询位于错误的窗格中。如果将其移至右侧窗格,它会运行,但不会返回任何内容。

标签: sql sql-server sql-server-2008


【解决方案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;

为了进一步阅读,Aaron Bertrand 已经对生成顺序数字列表的一些方法进行了深入比较:

如果我们将数字表限制为仅 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

Example on SQL Fiddle


我认为你的架构是有问题的,加入:

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;

Example on SQL Fiddle


我不会向您提供我过去如何实现这一目标的完整架构,但我会给出一个非常精简的示例,希望您可以从中构建自己的示例。我只会为每周一至周五发生的事件添加一个示例:

在上面的 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);

Example on SQL Fiddle

这只是我如何实现它的一个非常基本的表示,例如,我实际上完全使用视图来查看重复数据的任何查询,以便假定 RepeatDayOfWeek 中没有条目的任何事件每天都重复,而不是绝不。连同此答案和其他答案中的所有其他详细信息,您应该有足够的信息来帮助您入门。

【讨论】:

  • 查看您的答案。在赏金中,我说Creating a booking management system where i need to shedule a doctor visit like every monday 10 AM to 12 A.M and 2 P.M to 6 P.M Other possiblities are every day between, or some days between some time,现在我正在分析我的设计如何适合所有场景。我还看了this component,他们处理的不同,但业务场景(UI)对我来说最多相同
  • 有很多方法可以解决这个问题,根据您的具体要求,您可能会采用不同的方法,但答案的关键点是,一旦您有了开始日期和重复间隔,您需要交叉连接到一个序号表以获取该事件的所有实例。
  • 太棒了。我同意。您能否帮我纠正当前的数据库设计。它对于给定的 UI 是否完美?还是我需要在事件元数据表中添加更多字段?
  • 总而言之,我认为没有正确或错误的方式去做,这完全取决于您需要什么以及您对什么感到满意,我怀疑 SO 上的任何人都会设计您的系统为您服务,最终它是一个问答网站,而不是咨询服务。我很乐意提供更多帮助,但如果我发布了我使用的架构以及随附的文档,这将是一个巨大的答案,我必须编写它,因为它不存在,这也需要我几个小时完整的文档,这对于免费工作来说有点太长了!
  • 基本上你想要做的是多次重复,它没有设置做,你试图每 45 分钟和每天重复一次。您还有一个结束日期,这就是为什么只有一行。我给你的模式不是为了处理每天重复多次的事情,你可以通过为事件创建 4 个重复事件(在 10:00、10:45、11:30 和 12:15)来解决这个问题,然后每天重复这 4 个。 Demo on SQL Fiddle
【解决方案2】:

以下将根据 StartEvent 和带有 CTE 的 MEta 描述生成事件。

根据参数值更改 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 ;

【讨论】:

  • @Billa 我第一次误解了你的要求:(我重写了
【解决方案3】:

实现此目的的一种方法是使用循环并将记录插入临时表。然后你可以简单地从你的临时表中选择。在下面的示例中,我保存了 unixtimestamp 值以及转换为日期时间的值。

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

此问题的另一种可能解决方案是使用recursive CTE

【讨论】:

  • 您的代码有效。缺少其他事件数据,请检查更新的问题和您的结果sqlfiddle.com/#!3/fdfe4
  • 为此添加了赏金
  • 我的代码示例已经更新,应该会得到您预期的结果。我从事件表到别名日期表的连接不正确。我加入了 date.id 而不是 date.event_id。
【解决方案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's 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-30
      • 2023-03-18
      • 1970-01-01
      • 2015-10-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多