日历循环/重复事件 – 最佳存储方法

我正在构build一个自定义事件系统,如果您有重复事件,如下所示:

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

要么

B活动从2011年3月1日开始,每2周重复一次

我怎样才能以简单的方式将其存储在数据库中? 如果有大量的事件,我不希望出现性能问题,而且在渲染日历时我必须经历每一个事件。

存储“简单”重复模式

对于我的基于PHP / MySQL的日历,我想尽可能高效地存储重复/循环事件信息。 我不想有大量的行,我想轻松查找在特定date发生的所有事件。

下面的方法非常适合存储每隔一天,每n天,每周,每年每月等重复发生的信息,这包括每周二和周四的types模式,因为它们被存储分别为每星期二开始,星期四开始每星期。

假设我有两个表,一个叫这样的events

 ID NAME 1 Sample Event 2 Another Event 

还有一个名为events_meta的表,像这样:

 ID event_id meta_key meta_value 1 1 repeat_start 1299132000 2 1 repeat_interval_1 432000 

repeat_start是一个没有时间的date作为unix时间戳,repeat_interval是间隔(432000是5天)之间的一个数量(以秒为单位)。

repeat_interval_1与ID 1的repeat_start一致。所以如果我有一个事件在每个星期二和每个星期四重复,那么repeat_interval将是604800(7天),并且会有2个repeat_start和2个repeat_intervals。 表格看起来像这样:

 ID event_id meta_key meta_value 1 1 repeat_start 1298959200 -- This is for the Tuesday repeat 2 1 repeat_interval_1 604800 3 1 repeat_start 1299132000 -- This is for the Thursday repeat 4 1 repeat_interval_3 604800 5 2 repeat_start 1299132000 6 2 repeat_interval_5 1 -- Using 1 as a value gives us an event that only happens once 

然后,如果你有一个每天都在循环的日历,抓住当天的事件,查询将如下所示:

 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 LIMIT 0 , 30 

使用当前date的unix时间戳replace{current_timestamp} (减去时间,所以小时,分钟和秒的值将被设置为0)。

希望这会帮助别人!


存储“复杂”重复模式

这种方法更适合存储复杂的模式,如

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

要么

Event A repeats Friday of the 2nd week of the month starting on March 11, 2011

我build议把这个与上面的系统结合起来,以获得最大的灵活性。 这个表应该像这样:

 ID NAME 1 Sample Event 2 Another Event 

还有一个名为events_meta的表,像这样:

 ID event_id meta_key meta_value 1 1 repeat_start 1299132000 -- March 3rd, 2011 2 1 repeat_year_1 * 3 1 repeat_month_1 * 4 1 repeat_week_im_1 2 5 1 repeat_weekday_1 6 

repeat_week_im表示当前月份的周,可能在1到5之间。 repeat_weekday

现在,假设您正在循环查看日历/星期以在日历中创build月份视图,则可以编写如下所示的查询:

 SELECT EV . * FROM `events` AS EV JOIN `events_meta` EM1 ON EM1.event_id = EV.id AND EM1.meta_key = 'repeat_start' LEFT JOIN `events_meta` EM2 ON EM2.meta_key = CONCAT( 'repeat_year_', EM1.id ) LEFT JOIN `events_meta` EM3 ON EM3.meta_key = CONCAT( 'repeat_month_', EM1.id ) LEFT JOIN `events_meta` EM4 ON EM4.meta_key = CONCAT( 'repeat_week_im_', EM1.id ) LEFT JOIN `events_meta` EM5 ON EM5.meta_key = CONCAT( 'repeat_weekday_', EM1.id ) WHERE ( EM2.meta_value =2011 OR EM2.meta_value = '*' ) AND ( EM3.meta_value =4 OR EM3.meta_value = '*' ) AND ( EM4.meta_value =2 OR EM4.meta_value = '*' ) AND ( EM5.meta_value =6 OR EM5.meta_value = '*' ) AND EM1.meta_value >= {current_timestamp} LIMIT 0 , 30 

结合上述方法可以结合起来,覆盖大多数重复/重复发生的事件模式。 如果我错过了任何内容,请发表评论。

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


“简单”重复事件

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

 Repeat every other day 

要么

 Repeat every week on Tuesday 

你应该创build两个表,一个叫这样的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时间戳date(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 ) 

只需在日历中的每个date在unix时间戳(1299736800)中replace即可。

注意使用模(%符号)。 这个符号就像常规的分割,但是返回“余数”,而不是商数,只要当前date是repeat_start的repeat_interval的精确倍数,就是0。

性能比较

这比以前build议的基于“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 | +----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+ 

上面有1个join的解决scheme不需要这样的缓冲区。


“复杂”模式

您可以添加对更复杂types的支持来支持这些types的重复规则:

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

要么

 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_yearrepeat_monthrepeat_dayrepeat_weekrepeat_weekday数据即可。

这使得同时select这两种types非常简单。 每天循环并填写正确的值(2013年6月7日的1370563200,然后是年,月,日,星期和星期),如下所示:

 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中的Sidenote我使用了PHP Date的默认周日指数,所以星期五是“5”


希望这可以帮助别人尽可能多的原始答案帮助我!

增强:用datereplace时间戳

作为ahoffner后来改进的接受答案的一个小改进 – 可以使用date格式而不是时间戳。 优点是:

  1. 数据库中可读的date
  2. 2038年和时间戳没有问题
  3. 不需要注意基于季节调整date的时间戳,即在6月28日比12月28日早一个小时的时间开始,因此从date推断出一个时间戳可能会破坏recursionalgorithm。

要做到这一点,更改数据库repeat_start被存储为types“date”和repeat_interval现在持有天而不是秒。 即7天重复7天。

更改sql行:WHERE((1370563200 – repeat_start)%repeat_interval = 0)

到:WHERE(DATEDIFF('2013-6-7',event_start)%repeat_interval = 0)

其他一切保持不变。 Simples!

对于所有对此感兴趣的人,现在只需在几分钟内复制并粘贴即可开始使用。 我尽可能地在意见中听取了意见。 让我知道如果我失去了一些东西。

“复杂版本”:

事件

 + ---------- + ---------------- +
 |  ID |  NAME | 
 + ---------- + ---------------- +
 |  1 | 示例事件1 |
 |  2 | 第二个事件|
 |  3 | 第三个事件|
 + ---------- + ---------------- +

events_meta

 + ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- +
 |  ID |  event_id |  repeat_start |  repeat_interval |  repeat_year |  repeat_month |  repeat_day |  repeat_week |  repeat_weekday |
 + ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- +
 |  1 |  1 |  2014-07-04 |  7 |  NULL |  NULL |  NULL |  NULL |  NULL |
 |  2 |  2 |  2014-06-26 |  NULL |  2014 |  * |  * |  2 |  5 |
 |  3 |  3 |  2014-07-04 |  NULL |  * |  * |  * |  * |  5 |
 + ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- +

SQL代码:

 CREATE TABLE IF NOT EXISTS `events` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `NAME` varchar(255) NOT NULL, PRIMARY KEY (`ID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ; -- -- Dumping data for table `events` -- INSERT INTO `events` (`ID`, `NAME`) VALUES (1, 'Sample event'), (2, 'Another event'), (3, 'Third event...'); CREATE TABLE IF NOT EXISTS `events_meta` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `event_id` int(11) NOT NULL, `repeat_start` date NOT NULL, `repeat_interval` varchar(255) NOT NULL, `repeat_year` varchar(255) NOT NULL, `repeat_month` varchar(255) NOT NULL, `repeat_day` varchar(255) NOT NULL, `repeat_week` varchar(255) NOT NULL, `repeat_weekday` varchar(255) NOT NULL, PRIMARY KEY (`ID`), UNIQUE KEY `ID` (`ID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ; -- -- Dumping data for table `events_meta` -- INSERT INTO `events_meta` (`ID`, `event_id`, `repeat_start`, `repeat_interval`, `repeat_year`, `repeat_month`, `repeat_day`, `repeat_week`, `repeat_weekday`) VALUES (1, 1, '2014-07-04', '7', 'NULL', 'NULL', 'NULL', 'NULL', 'NULL'), (2, 2, '2014-06-26', 'NULL', '2014', '*', '*', '2', '5'), (3, 3, '2014-07-04', 'NULL', '*', '*', '*', '*', '1'); 

也可以作为MySQL导出 (为了方便访问)

PHP示例代码index.php:

 <?php require 'connect.php'; $now = strtotime("yesterday"); $pushToFirst = -11; for($i = $pushToFirst; $i < $pushToFirst+30; $i++) { $now = strtotime("+".$i." day"); $year = date("Y", $now); $month = date("m", $now); $day = date("d", $now); $nowString = $year . "-" . $month . "-" . $day; $week = (int) ((date('d', $now) - 1) / 7) + 1; $weekday = date("N", $now); echo $nowString . "<br />"; echo $week . " " . $weekday . "<br />"; $sql = "SELECT EV.* FROM `events` EV RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id` WHERE ( DATEDIFF( '$nowString', repeat_start ) % repeat_interval = 0 ) OR ( (repeat_year = $year OR repeat_year = '*' ) AND (repeat_month = $month OR repeat_month = '*' ) AND (repeat_day = $day OR repeat_day = '*' ) AND (repeat_week = $week OR repeat_week = '*' ) AND (repeat_weekday = $weekday OR repeat_weekday = '*' ) AND repeat_start <= DATE('$nowString') )"; foreach ($dbConnect->query($sql) as $row) { print $row['ID'] . "\t"; print $row['NAME'] . "<br />"; } echo "<br /><br /><br />"; } ?> 

PHP示例代码connect.php:

 <? // ---------------------------------------------------------------------------------------------------- // Connecting to database // ---------------------------------------------------------------------------------------------------- // Database variables $username = ""; $password = ""; $hostname = ""; $database = ""; // Try to connect to database and set charset to UTF8 try { $dbConnect = new PDO("mysql:host=$hostname;dbname=$database;charset=utf8", $username, $password); $dbConnect->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $e) { echo 'ERROR: ' . $e->getMessage(); } // ---------------------------------------------------------------------------------------------------- // / Connecting to database // ---------------------------------------------------------------------------------------------------- ?> 

此外,PHP代码可在这里(为了更好的可读性):
的index.php

connect.php
现在设置这应该需要你几分钟。 不是几个小时 🙂

虽然提出的解决scheme的工作,我试图实现完整的日历,这将需要对每个视图超过90个数据库调用(因为它加载当前,上个月和下个月),我不太兴奋。

我find了一个recursion库https://github.com/tplaner/当你只是将规则存储在数据库中,并且一个查询拉取所有相关的规则。;

希望这会帮助别人,因为我花了很多时间来寻找一个好的解决scheme。

编辑:这个库是为PHP

为什么不使用类似于Apache cron作业的机制? http://en.wikipedia.org/wiki/Cron

对于日历\日程安排,我会使用稍微不同的“位”值来适应标准的日历再现事件 – 而不是[星期几(0 – 7),月份(1 – 12),月份的date(1 – 31)小时(0 – 23),分钟(0 – 59)]

– 我会用[年份(每N年重复一次),月份(1 – 12),月份的date(1 – 31),月份的星期几(1-5),星期几(0 – 7) ]

希望这可以帮助。

我会遵循这个指南: https : //github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md

另外请确保使用iCal格式,以免重新发明轮子并记住规则#0: 不要将单个循环事件实例存储为数据库中的行!

@Rogue编码器

这很棒!

您可以简单地使用模运算(MOD或%在MySQL中)来使代码简单:

代替:

 AND ( ( CASE ( 1299132000 - EM1.`meta_value` ) WHEN 0 THEN 1 ELSE ( 1299132000 - EM1.`meta_value` ) END ) / EM2.`meta_value` ) = 1 

做:

 $current_timestamp = 1299132000 ; AND ( ('$current_timestamp' - EM1.`meta_value` ) MOD EM2.`meta_value`) = 1") 

要进一步采取这一行动,可以包括不会重演的事件。

可以添加类似“repeat_interval_1_end”来表示最后一个“repeat_interval_1”的date。 但是,这使查询更复杂,我不能真正弄清楚如何做到这一点…

也许有人可以帮忙!

听起来很像存储在系统表中的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: 

我为这种情况开发了一种深奥的编程语言。 最好的部分是它是模式less,平台独立。 你只需要编写一个select程序,用于你的日程安排,其语法受到这里描述的规则集的约束 –

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

规则是可扩展的,您可以根据您要执行的重复逻辑types添加任何types的定制,而不必担心模式迁移等。

这是完全不同的方法,可能会有一些缺点。

你给出的两个例子非常简单, 他们可以表示为一个简单的时间间隔(第一个是四天,第二个是14天)。 你如何模型化将完全取决于你复发的复杂性。 如果上面的内容真的很简单,那么在重复间隔中存储开始date和天数。

但是,如果你需要支持像

在2011年3月3日开始的月份的第3天,每月重复活动A.

要么

活动A重复2011年3月11日开始的第二个星期五

那么这是一个更复杂的模式。