日历重复/重复事件 - 最佳存储方法

Bra*_*ldt 291 database-design calendar

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

活动A从2011年3月3日起每4天重复一次

要么

活动B于2011年3月1日星期二每2周重复一次

如何以一种易于查找的方式将其存储在数据库中.如果有大量事件,我不希望出现性能问题,在渲染日历时我必须经历每一个事件.

Bra*_*ldt 197

存储"简单"重复模式

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

下面的方法非常适合存储定期发生的重复信息,例如每天,每n天,每周,每年每月等等.这包括每周二和周四的类型模式,因为它们是存储的每周从星期二开始,每周从星期四开始.

假设我有两个表,一个events像这样调用:

ID    NAME
1     Sample Event
2     Another Event
Run Code Online (Sandbox Code Playgroud)

还有一个events_meta像这样的表:

ID    event_id      meta_key           meta_value
1     1             repeat_start       1299132000
2     1             repeat_interval_1  432000
Run Code Online (Sandbox Code Playgroud)

假设repeat_start是没有时间作为unix时间戳的日期,repeat_interval是间隔之间的秒数(432000是5天).

repeat_interval_1与ID 1的repeat_start一起使用.因此,如果我的事件在每个星期二和每个星期四重复,则repeat_interval将为604800(7天),并且将有2个repeat_starts和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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

{current_timestamp}用当前日期的unix时间戳替换(减去时间,因此小时,分钟和秒值将设置为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

我建议将其与上述系统相结合,以获得最大的灵活性.这个表应该像:

ID    NAME
1     Sample Event
2     Another Event
Run Code Online (Sandbox Code Playgroud)

还有一个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
Run Code Online (Sandbox Code Playgroud)

repeat_week_im表示当月的周,可能在1到5之间.repeat_weekday在一周的这一天,1-7.

现在假设您在日历/周中循环以在日历中创建月视图,您可以编写如下查询:

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
Run Code Online (Sandbox Code Playgroud)

结合上述方法可以组合以覆盖大多数重复/重复发生的事件模式.如果我错过了什么,请发表评论.

  • 值得注意的是,您不应该使用硬编码值来重复间隔,即一天中的"86400"秒,因为它不会考虑夏令时.在运行中动态计算这些东西更合适,而是存储`interval = daily`和`interval_count = 1`或`interval = monthly`和`interval_count = 1`. (23认同)

aho*_*ner 175

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


"简单"重复事件

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

Repeat every other day 
Run Code Online (Sandbox Code Playgroud)

要么

Repeat every week on Tuesday 
Run Code Online (Sandbox Code Playgroud)

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

ID    NAME
1     Sample Event
2     Another Event
Run Code Online (Sandbox Code Playgroud)

还有一个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
Run Code Online (Sandbox Code Playgroud)

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 )
Run Code Online (Sandbox Code Playgroud)

只需在日历中的每个日期替换unix-timestamp(1299736800)即可.

注意使用模数(%符号).此符号类似于常规除法,但返回"余数"而不是商,因此只要当前日期是repeat_start的repeat_interval的精确倍数,就会返回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
Run Code Online (Sandbox Code Playgroud)

如果你运行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 |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
Run Code Online (Sandbox Code Playgroud)

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


"复杂"模式

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

Event A repeats every month on the 3rd of the month starting on March 3, 2011
Run Code Online (Sandbox Code Playgroud)

要么

Event A repeats second Friday of the month starting on March 11, 2011
Run Code Online (Sandbox Code Playgroud)

您的事件表看起来完全相同:

ID    NAME
1     Sample Event
2     Another Event
Run Code Online (Sandbox Code Playgroud)

然后添加对这些复杂规则的支持,添加列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    
Run Code Online (Sandbox Code Playgroud)

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

这使得同时选择两种类型非常简单.只需遍历每一天并填写正确的值(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
  )
Run Code Online (Sandbox Code Playgroud)

这将返回在第二周的星期五重复的所有事件,以及每周五重复的任何事件,因此它返回事件ID 1和2:

ID    NAME
1     Sample Event
2     Another Event
Run Code Online (Sandbox Code Playgroud)

*上面SQL中的Sidenote我使用了PHP Date的默认工作日索引,所以周五为"5"


希望这能帮助别人,就像原来的答案帮助我一样!

  • 这是一个很好的答案.我已经删除了repeat_interval并添加了repeat_end日期,但这个答案帮助很大. (10认同)
  • 这太棒了,谢谢!您是否知道如何编码"第一个星期一每2个月"或"第一个星期一每3个月"等等? (6认同)
  • 我同意这太棒了.然而,我遇到了乔丹列夫所做的同样困境.repeat_interval字段不适合重复几个月,因为有些月比其他月长.另外,如何限制重复事件的持续时间.即,在第一个星期一每2个月持续8个月.该表应该有某种结束日期. (6认同)
  • 提示:对于复杂模式,可以消除`repeat_interval`列并在后续列中表示它(即`repeat_year`等)对于第一行,可以表示2013年5月20日之后每周一重复的情况在`repeat_weekday`中放置1,在其他列中放置`*`. (3认同)
  • @Abinadi关于"第一个星期一每2个月8个月.",我想我们可以用repeat_month来做,比如填写"1,3,5,7,9,11"; 在这种情况下,我们还需要添加结束日期. (2认同)
  • 有没有人想到一个好的选择来获取一个月的数据而不是一天的数据。我试图认为最坏的情况是进行 31 个选择,但我并不热衷于这样做 (2认同)
  • @OlivierMATROT @milos想法是设置你想要显式修复的字段,其余的设置为通配符`*`.因此,对于"每个月3日",您只需将`repeat_day`设置为3,将其余的`repeat`字段设置为*(将`repeat_interval`保留为null),并将repeat_start设置为2011年3月3日的unix时间码是你的主播日期. (2认同)

小智 25

增强功能:将时间戳替换为日期

作为随后由ahoffner提炼的已接受答案的一个小改进 - 可以使用日期格式而不是时间戳.优点是:

  1. 数据库中的可读日期
  2. 没有问题> 2038年和时间戳
  3. 删除需要注意基于季节性调整日期的时间戳,即英国6月28日比12月28日提前一小时开始,因此从日期得到时间戳可以打破递归算法.

要执行此操作,请将repeat_start要存储的数据库更改为"日期"类型,repeat_interval现在可以保留数天而不是秒.即7天重复7天.

更改sql行:

WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )
Run Code Online (Sandbox Code Playgroud)

至:

WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)
Run Code Online (Sandbox Code Playgroud)

其他一切都是一样的.Simples!

  • @ George02如果事件是年度,你将离开repeat_interval NULL并且repeat_year是*然后取决于你可以设置repeat_month和repeat_day的重复次数,例如3月11日或者repeat_month,repeat_week和repeat_weekday来设置4月的第2个星期二. (2认同)

Ale*_*lex 24

对于所有对此感兴趣的人,现在您只需复制并粘贴即可在几分钟内开始使用.我尽可能地在评论中接受了建议.如果我遗失了某些东西,请告诉我.

"复杂版本":

事件

+----------+----------------+
| ID       | NAME           | 
+----------+----------------+
| 1        | Sample event 1 |
| 2        | Second  event  |
| 3        | Third event    |
+----------+----------------+

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');
Run Code Online (Sandbox Code Playgroud)

也可用作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 />";
    }
?>
Run Code Online (Sandbox Code Playgroud)

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
// ----------------------------------------------------------------------------------------------------
?>
Run Code Online (Sandbox Code Playgroud)

此处还提供了php代码(为了更好的可读性):
index.php

connect.php
现在设置它应该花费你几分钟.不是几个小时 :)

  • 我如何查询以获取日期范围内的所有重复事件...即获取2014-10-01至2014-12-30之间的所有重复事件.谢谢你的帖子 (2认同)
  • 感谢您的代码。然而,我必须指出,您的数据库/查询实现有点令人不安,而且效率很低。例如,为什么对如此简单的列使用 varchar(255) (正如 @OlivierMATROT 提到的,您可以使用整数,即使不可以,为什么使用 255?)。如果您重复查询 30 次,为什么不使用语句或过程呢?只是为了说是否有人要实施这个。 (2认同)

Gal*_*cha 23

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

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

  • 您将如何对参加过特定实例的跟踪用户进行建模?在这种情况下,打破规则 #0 有意义吗? (2认同)
  • @DannySullivan 在我的脑海中,我会有另一个实体 `attendedEvent` 带有 `baseInstanceId` 和 `instanceStartDate` - 例如,您创建重复规则日历视图并使用开始日期指定信息的基本事件在那个特定实例上然后这个实体也可以有类似`attendedListId`的东西,它会导致另一个`id`表,`attendedUserId` (2认同)
  • 这应该是正确的答案.虽然其他答案很好而且非常详细,但这是正确的行业标准方法. (2认同)

Tim*_*sey 15

虽然提出的解决方案有效,但我试图使用完整日历实现,并且每个视图需要超过90个数据库调用(因为它加载当前,上个月和下个月),我并不太兴奋.

我找到了一个递归库https://github.com/tplaner/当你只是将规则存储在数据库中时,我会找到一个查询来提取所有相关规则.

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

编辑:此库适用于PHP


小智 14

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

对于calendar\scheduling,我会使用略微不同的"bits"值来容纳标准日历reoccurence事件 - 而不是[day of week(0 - 7),month(1 - 12),day of month(1 - 31),小时(0 - 23),分钟(0 - 59)]

- 我会使用类似[年份(重复每N年),月份(1 - 12),月份(1 - 31),月份(1-5),星期几(0 - 7)的内容]

希望这可以帮助.

  • 我认为这是太多的一周选择.1-7或0-6似乎更准确. (6认同)
  • 使用cron来存储重复是很好的.但问题是查找起来非常困难. (2认同)

Rav*_*hal 8

RRULE 标准正是为这个要求而建立的,即保存和理解重复。微软和谷歌都在他们的日历活动中使用它。请仔细阅读此文档以获取更多详细信息。 https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html


tus*_*ath 5

我为这种情况开发了一种深奥的编程语言。关于它的最好的部分是它没有模式并且与平台无关。您只需要为您的计划编写一个选择器程序,其语法受此处描述的一组规则的约束 -

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

这些规则是可扩展的,您可以根据要执行的重复逻辑类型添加任何类型的自定义,而无需担心架构迁移等。

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