我正在构建一个自定义事件系统,如果你有一个如下所示的重复事件:
事件A每4天重复一次,开始于2011年3月3日
或者
事件B每2周星期二重复一次,开始于2011年3月1日
我应该如何在数据库中存储这些信息以便于查找?我不想因为有大量事件而出现性能问题,在渲染日历时必须逐个遍历每个事件。
我正在构建一个自定义事件系统,如果你有一个如下所示的重复事件:
事件A每4天重复一次,开始于2011年3月3日
或者
事件B每2周星期二重复一次,开始于2011年3月1日
我应该如何在数据库中存储这些信息以便于查找?我不想因为有大量事件而出现性能问题,在渲染日历时必须逐个遍历每个事件。
对于我的基于PHP/MySQL的日历,我希望尽可能高效地存储重复事件信息。我不想有大量行,并且想要轻松查找特定日期上发生的所有事件。
以下方法非常适合存储以规律间隔出现的重复信息,例如每天、每 n 天、每周、每月、每年等等。这也包括像每个星期二和星期四这样的模式,因为它们分别作为从星期二开始的每周和从星期四开始的每周单独存储。
假设我有两个表,一个叫做 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是一个没有时间的日期,以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
那么,如果你有一个遍历每一天并获取该天事件的日历,查询将如下所示:
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
用当前日期的Unix时间戳替换{current_timestamp}
(去掉时间,因此小时、分钟和秒值将被设置为0)。
希望这也能帮助其他人!
对于存储像
事件A每个月在3日重复一次,从2011年3月3日开始
或者
事件A每个月第2周的星期五重复一次,从2011年3月11日开始
这样的复杂模式,建议采用此方法。我建议与上述系统结合使用以获得最大的灵活性。用于此的表应该是这样的:
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
表示一周中的第几天,即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
结合上述方法,这种方法可以覆盖大多数重复/循环事件模式。如果我遗漏了任何内容,请留下评论。
AND ( ( CASE ( 1299132000 - EM1.meta_value ) WHEN 0 THEN 1 ELSE ( 1299132000 - EM1.meta_value) END ) / EM2.meta_value ) = 1
中,这个/ EM2.meta_value
放错了吗? - Murali Murugesan尽管当前被接受的答案对我很有帮助,但我想分享一些有用的修改,可以简化查询并提高性能。
处理定期重复的事件,例如:
Repeat every other day
或者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)替换日历中的每个日期。
请注意使用取模(% 符号)。此符号类似于常规除法,但返回“余数”而非商,因此在当前日期是重复间隔从重复开始的精确倍数时,余数为 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
或者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
数据中的任何一个。
这使得同时选择两种类型非常简单。只需循环遍历每一天并填入正确的值(例如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 中,我使用了 PHP Date 的默认工作日索引,因此星期五是“5”。
*
。因此,对于“每月3日”,只需将repeat_day
设置为3,其他repeat
字段设置为*(将repeat_interval
保留为空),并将repeat_start
设置为2011年3月3日的Unix时间戳作为锚定日期。 - Andy Hoffner作为对被ahoffner优化后接受的答案的小升级 - 可以使用日期格式而不是时间戳。 优点如下:
要实现此操作,请将 DB repeat_start
更改为存储类型为“date”,并且 repeat_interval
现在保存天数而不是秒数。 例如,7表示每7天重复一次。
更改 SQL 行:
WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )
致:
WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)
其它所有的都保持不变。简单易懂!
对于所有对此感兴趣的人,现在你只需复制并粘贴即可在几分钟内开始。我尽可能地采纳了评论中的建议。如果我漏掉了什么,请告诉我。
“复杂版本”:
事件
+----------+----------------+ | ID | NAME | +----------+----------------+ | 1 | 样本事件1 | | 2 | 第二个事件 | | 3 | 第三个事件 | +----------+----------------+
事件元数据
+----+----------+--------------+------------------+-------------+--------------+------------+-------------+----------------+ | 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
现在,设置这个应该只需要几分钟。不是几小时。:)
为什么不使用类似于Apache定时任务的机制呢?http://en.wikipedia.org/wiki/Cron
对于日历调度,我会使用略微不同的“位”值来适应标准日历重复事件 - 而不是 [星期(0-7),月份(1-12),月中的日期(1-31),小时(0-23),分钟(0-59)]
-- 我会使用类似如下的内容 [年(每N年重复一次),月份(1-12),月中的日期(1-31),月中的第N周(1-5),星期(0-7)]
希望这能有所帮助。
https://github.com/tusharmath/sheql/wiki/Rules
规则是可扩展的,您可以根据所需执行的重复逻辑添加任何类型的自定义,而无需担心模式迁移等问题。这很像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:
@Rogue Coder
太棒了!
你可以简单地使用取模运算符(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")
1299132000
被硬编码了吗?如果我需要获取给定结束日期的发生日期和用户,这将会怎样? - Murali Murugesan