我正在构建一个自定义事件系统,如果你有一个重复的事件,看起来像这样:
事件A从2011年3月3日开始每4天重复一次
or
赛事B从2011年3月1日开始,每两周在周二举行一次
我如何将其存储在数据库中,使其易于查找。如果有大量的事件,我不希望出现性能问题,而且在呈现日历时,我必须遍历每一个事件。
我正在构建一个自定义事件系统,如果你有一个重复的事件,看起来像这样:
事件A从2011年3月3日开始每4天重复一次
or
赛事B从2011年3月1日开始,每两周在周二举行一次
我如何将其存储在数据库中,使其易于查找。如果有大量的事件,我不希望出现性能问题,而且在呈现日历时,我必须遍历每一个事件。
当前回答
我专门为这种情况开发了一种深奥的编程语言。它最好的部分是它是无模式的和平台独立的。你只需要为你的时间表写一个选择器程序,它的语法受到这里描述的一组规则的约束
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:
虽然目前接受的答案对我来说有很大的帮助,但我想分享一些有用的修改,它们可以简化查询并提高性能。
“简单”重复事件
处理定期发生的事件,例如:
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”为星期五
@Rogue编码器
这太棒了!
你可以简单地使用模运算(mysql中的MOD或%)让你的代码在最后变得简单:
而不是:
AND (
( CASE ( 1299132000 - EM1.`meta_value` )
WHEN 0
THEN 1
ELSE ( 1299132000 - EM1.`meta_value` )
END
) / EM2.`meta_value`
) = 1
Do:
$current_timestamp = 1299132000 ;
AND ( ('$current_timestamp' - EM1.`meta_value` ) MOD EM2.`meta_value`) = 1")
为了更进一步,我们可以把那些不会永远重复的事件也包括进来。
可以添加“repeat_interval_1_end”之类的内容,以表示最后一个“repeat_interval_1”的日期。然而,这使得查询更复杂,我真的不知道如何做到这一点…
也许有人可以帮忙!
增强:用日期替换时间戳
作为对公认答案的一个小改进,随后由ahoffner改进-可以使用日期格式而不是时间戳。优点是:
数据库中可读的日期 年份> 2038和时间戳没有问题 删除时需要小心基于季节性调整日期的时间戳,即在英国,6月28日比12月28日早一个小时开始,因此从日期中获得时间戳可能会破坏递归算法。
要做到这一点,将DB repeat_start更改为'date'类型,并且repeat_interval现在保存天数而不是秒。即重复7天。
修改SQL行:
WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )
to:
WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)
其他一切都保持不变。简单的!