我正在构建一个自定义事件系统,如果你有一个重复的事件,看起来像这样:

事件A从2011年3月3日开始每4天重复一次

or

赛事B从2011年3月1日开始,每两周在周二举行一次

我如何将其存储在数据库中,使其易于查找。如果有大量的事件,我不希望出现性能问题,而且在呈现日历时,我必须遍历每一个事件。


当前回答

虽然目前接受的答案对我来说有很大的帮助,但我想分享一些有用的修改,它们可以简化查询并提高性能。


“简单”重复事件

处理定期发生的事件,例如:

Repeat every other day 

or

Repeat every week on Tuesday 

你应该创建两个表,一个叫events,像这样:

ID    NAME
1     Sample Event
2     Another Event

和一个叫events_meta的表,像这样:

ID    event_id      repeat_start       repeat_interval
1     1             1369008000         604800            -- Repeats every Monday after May 20th 2013
1     1             1369008000         604800            -- Also repeats every Friday after May 20th 2013

其中repeat_start是没有时间的unix时间戳日期(1369008000对应于2013年5月20日),repeat_interval是间隔之间以秒为单位的数量(604800是7天)。

通过循环日历中的每一天,你可以使用这个简单的查询来获得重复的事件:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1299736800 - repeat_start) % repeat_interval = 0 )

只需为日历中的每个日期替换unix时间戳(1299736800)。

注意模数(%符号)的使用。此符号类似于常规除法,但返回“余数”而不是商,因此当当前日期是repeat_interval与repeat_start的精确倍数时,则返回0。

性能比较

这比之前建议的基于“meta_keys”的答案要快得多,如下所示:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
    AND (
        ( CASE ( 1299132000 - EM1.`meta_value` )
            WHEN 0
              THEN 1
            ELSE ( 1299132000 - EM1.`meta_value` )
          END
        ) / EM2.`meta_value`
    ) = 1

如果你运行EXPLAIN这个查询,你会注意到它需要使用一个连接缓冲区:

+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref              | rows | Extra                          |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
|  1 | SIMPLE      | EM1   | ALL    | NULL          | NULL    | NULL    | NULL             |    2 | Using where                    |
|  1 | SIMPLE      | EV    | eq_ref | PRIMARY       | PRIMARY | 4       | bcs.EM1.event_id |    1 |                                |
|  1 | SIMPLE      | EM2   | ALL    | NULL          | NULL    | NULL    | NULL             |    2 | Using where; Using join buffer |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+

上面有一个连接的解决方案不需要这样的缓冲区。


“复杂”模式

您可以添加对更复杂类型的支持,以支持这些类型的重复规则:

Event A repeats every month on the 3rd of the month starting on March 3, 2011

or

Event A repeats second Friday of the month starting on March 11, 2011

你的事件表可以看起来完全相同:

ID    NAME
1     Sample Event
2     Another Event

然后添加对这些复杂规则的支持到events_meta中,如下所示:

ID    event_id      repeat_start       repeat_interval    repeat_year    repeat_month    repeat_day    repeat_week    repeat_weekday
1     1             1369008000         604800             NULL           NULL            NULL          NULL           NULL             -- Repeats every Monday after May 20, 2013
1     1             1368144000         604800             NULL           NULL            NULL          NULL           NULL             -- Repeats every Friday after May 10, 2013
2     2             1369008000         NULL               2013           *               *             2              5                -- Repeats on Friday of the 2nd week in every month    

请注意,您只需要指定一个repeat_interval或一组repeat_year、repeat_month、repeat_day、repeat_week和repeat_weekday数据。

这使得同时选择两种类型非常简单。只需要循环每天并填写正确的值(1370563200表示2013年6月7日,然后是年、月、日、周数和工作日,如下所示):

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1370563200 - repeat_start) % repeat_interval = 0 )
  OR ( 
    (repeat_year = 2013 OR repeat_year = '*' )
    AND
    (repeat_month = 6 OR repeat_month = '*' )
    AND
    (repeat_day = 7 OR repeat_day = '*' )
    AND
    (repeat_week = 2 OR repeat_week = '*' )
    AND
    (repeat_weekday = 5 OR repeat_weekday = '*' )
    AND repeat_start <= 1370563200
  )

它返回所有在第二周的周五重复的事件,以及每个周五重复的事件,因此它同时返回事件ID 1和2:

ID    NAME
1     Sample Event
2     Another Event

*旁注在上面的SQL我使用PHP日期的默认工作日索引,所以“5”为星期五

其他回答

虽然目前接受的答案对我来说有很大的帮助,但我想分享一些有用的修改,它们可以简化查询并提高性能。


“简单”重复事件

处理定期发生的事件,例如:

Repeat every other day 

or

Repeat every week on Tuesday 

你应该创建两个表,一个叫events,像这样:

ID    NAME
1     Sample Event
2     Another Event

和一个叫events_meta的表,像这样:

ID    event_id      repeat_start       repeat_interval
1     1             1369008000         604800            -- Repeats every Monday after May 20th 2013
1     1             1369008000         604800            -- Also repeats every Friday after May 20th 2013

其中repeat_start是没有时间的unix时间戳日期(1369008000对应于2013年5月20日),repeat_interval是间隔之间以秒为单位的数量(604800是7天)。

通过循环日历中的每一天,你可以使用这个简单的查询来获得重复的事件:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1299736800 - repeat_start) % repeat_interval = 0 )

只需为日历中的每个日期替换unix时间戳(1299736800)。

注意模数(%符号)的使用。此符号类似于常规除法,但返回“余数”而不是商,因此当当前日期是repeat_interval与repeat_start的精确倍数时,则返回0。

性能比较

这比之前建议的基于“meta_keys”的答案要快得多,如下所示:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
    AND (
        ( CASE ( 1299132000 - EM1.`meta_value` )
            WHEN 0
              THEN 1
            ELSE ( 1299132000 - EM1.`meta_value` )
          END
        ) / EM2.`meta_value`
    ) = 1

如果你运行EXPLAIN这个查询,你会注意到它需要使用一个连接缓冲区:

+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref              | rows | Extra                          |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
|  1 | SIMPLE      | EM1   | ALL    | NULL          | NULL    | NULL    | NULL             |    2 | Using where                    |
|  1 | SIMPLE      | EV    | eq_ref | PRIMARY       | PRIMARY | 4       | bcs.EM1.event_id |    1 |                                |
|  1 | SIMPLE      | EM2   | ALL    | NULL          | NULL    | NULL    | NULL             |    2 | Using where; Using join buffer |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+

上面有一个连接的解决方案不需要这样的缓冲区。


“复杂”模式

您可以添加对更复杂类型的支持,以支持这些类型的重复规则:

Event A repeats every month on the 3rd of the month starting on March 3, 2011

or

Event A repeats second Friday of the month starting on March 11, 2011

你的事件表可以看起来完全相同:

ID    NAME
1     Sample Event
2     Another Event

然后添加对这些复杂规则的支持到events_meta中,如下所示:

ID    event_id      repeat_start       repeat_interval    repeat_year    repeat_month    repeat_day    repeat_week    repeat_weekday
1     1             1369008000         604800             NULL           NULL            NULL          NULL           NULL             -- Repeats every Monday after May 20, 2013
1     1             1368144000         604800             NULL           NULL            NULL          NULL           NULL             -- Repeats every Friday after May 10, 2013
2     2             1369008000         NULL               2013           *               *             2              5                -- Repeats on Friday of the 2nd week in every month    

请注意,您只需要指定一个repeat_interval或一组repeat_year、repeat_month、repeat_day、repeat_week和repeat_weekday数据。

这使得同时选择两种类型非常简单。只需要循环每天并填写正确的值(1370563200表示2013年6月7日,然后是年、月、日、周数和工作日,如下所示):

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1370563200 - repeat_start) % repeat_interval = 0 )
  OR ( 
    (repeat_year = 2013 OR repeat_year = '*' )
    AND
    (repeat_month = 6 OR repeat_month = '*' )
    AND
    (repeat_day = 7 OR repeat_day = '*' )
    AND
    (repeat_week = 2 OR repeat_week = '*' )
    AND
    (repeat_weekday = 5 OR repeat_weekday = '*' )
    AND repeat_start <= 1370563200
  )

它返回所有在第二周的周五重复的事件,以及每个周五重复的事件,因此它同时返回事件ID 1和2:

ID    NAME
1     Sample Event
2     Another Event

*旁注在上面的SQL我使用PHP日期的默认工作日索引,所以“5”为星期五

我专门为这种情况开发了一种深奥的编程语言。它最好的部分是它是无模式的和平台独立的。你只需要为你的时间表写一个选择器程序,它的语法受到这里描述的一组规则的约束

https://github.com/tusharmath/sheql/wiki/Rules

这些规则是可扩展的,您可以根据想要执行的重复逻辑类型添加任何类型的自定义,而不用担心模式迁移等问题。

这是一种完全不同的方法,它本身可能有一些缺点。

你举的两个例子很简单;它们可以表示为一个简单的间隔(第一个是4天,第二个是14天)。如何建模完全取决于递归的复杂度。如果上面的内容真的很简单,那么就存储一个开始日期和重复间隔的天数。

然而,如果你需要支持一些事情,比如

事件A从2011年3月3日开始,每月3日重复发生

Or

事件A从2011年3月11日开始在每月的第二个星期五发生

这是一个更复杂的模式。

听起来很像MySQL中存储在系统表中的事件。你可以查看结构并找出哪些列是不需要的:

   EVENT_CATALOG: NULL
    EVENT_SCHEMA: myschema
      EVENT_NAME: e_store_ts
         DEFINER: jon@ghidora
      EVENT_BODY: SQL
EVENT_DEFINITION: INSERT INTO myschema.mytable VALUES (UNIX_TIMESTAMP())
      EVENT_TYPE: RECURRING
      EXECUTE_AT: NULL
  INTERVAL_VALUE: 5
  INTERVAL_FIELD: SECOND
        SQL_MODE: NULL
          STARTS: 0000-00-00 00:00:00
            ENDS: 0000-00-00 00:00:00
          STATUS: ENABLED
   ON_COMPLETION: NOT PRESERVE
         CREATED: 2006-02-09 22:36:06
    LAST_ALTERED: 2006-02-09 22:36:06
   LAST_EXECUTED: NULL
   EVENT_COMMENT:

虽然建议的解决方案是可行的,但我试图用Full Calendar实现,它需要为每个视图调用超过90个数据库(因为它加载当前、以前和下个月),这让我不太兴奋。

我找到了一个递归库https://github.com/tplaner/When,您只需将规则存储在数据库中,并使用一个查询来提取所有相关规则。

希望这能帮助到其他人,因为我花了这么多小时试图找到一个好的解决方案。

编辑:这个库是PHP的