[database-design] 일정 반복 / 반복 이벤트-최상의 저장 방법

맞춤 이벤트 시스템을 구축 중이며 다음과 같은 반복되는 이벤트가있는 경우 :

2011 년 3 월 3 일부터 4 일마다 이벤트 A가 반복됩니다.

또는

2011 년 3 월 1 일부터 화요일에 2 주마다 이벤트 B가 반복됩니다.

조회하기 쉬운 방식으로 데이터베이스에 저장하는 방법 많은 수의 이벤트가있는 경우 성능 문제를 원하지 않으며 캘린더를 렌더링 할 때 각각의 이벤트를 수행해야합니다.



답변

“간단한”반복 패턴 저장

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는 유닉스 타임 스탬프로 시간이없는 날짜이고 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

{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

유연성을 높이려면 위의 시스템과 함께 사용하는 것이 좋습니다. 이에 대한 테이블은 다음과 같아야합니다.

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

위의 방법과 결합하여 대부분의 반복 / 반복 이벤트 패턴을 처리 할 수 ​​있습니다. 내가 놓친 것이 있으면 의견을 남겨주세요.


답변

현재 받아 들여진 대답은 나에게 큰 도움이되었지만 쿼리를 단순화하고 성능을 향상시키는 유용한 수정 사항을 공유하고 싶었습니다.


“간단한”반복 이벤트

다음과 같이 정기적으로 반복되는 이벤트를 처리하려면 다음을 수행하십시오.

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짧은 시간 (월 20 2013-1369008000 상당)을 가진 UNIX 타임 스탬프 날짜 인 repeat_interval재생 구간 초 양 (604,800 7 일).

달력에서 매일 반복하여 다음 간단한 쿼리를 사용하여 반복 이벤트를 얻을 수 있습니다.

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

달력의 각 날짜에 대해 유닉스 타임 스탬프 (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

이 쿼리를 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 조인 솔루션에는 이러한 버퍼가 필요 없습니다.


“복잡한”패턴

이러한 유형의 반복 규칙을 지원하기 위해 더 복잡한 유형에 대한 지원을 추가 할 수 있습니다.

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”


이것이 원래의 대답이 나에게 도움이 된만큼 다른 사람들에게 도움이되기를 바랍니다!


답변

개선 : 타임 스탬프를 날짜로 교체

ahoffner가 이후에 수정 한 허용 된 답변을 약간 향상 시키면 타임 스탬프 대신 날짜 형식을 사용할 수 있습니다. 장점은 다음과 같습니다.

  1. 데이터베이스에서 읽을 수있는 날짜
  2. 연도> 2038 및 타임 스탬프에 문제가 없음
  3. 계절적으로 조정 된 날짜를 기준으로하는 타임 스탬프에주의해야합니다. 즉, 영국에서는 6 월 28 일부터 12 월 28 일보다 1 시간 일찍 시작하므로 날짜에서 타임 스탬프를 추출하면 재귀 알고리즘이 중단 될 수 있습니다.

이렇게하려면 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)

다른 모든 것은 동일하게 유지됩니다. 단순!


답변

나는이 안내서를 따르겠다 :
https://github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md

또한 휠을 다시 만들지 않도록 iCal 형식을 사용해야하며 규칙 # 0을 기억하십시오.
개별 반복 이벤트 인스턴스를 데이터베이스에 행으로 저장하지 마십시오!


답변

이것에 관심이있는 모든 사람들을 위해 이제 복사하고 붙여 넣기 만하면 몇 분 안에 시작할 수 있습니다. 나는 의견뿐만 아니라 조언을했다. 내가 빠진 것이 있으면 알려주세요.

“복잡한 버전”:

행사

+ ---------- + ---------------- +
| 아이디 | 이름 |
+ ---------- + ---------------- +
| 1 | 샘플 이벤트 1 |
| 2 | 두 번째 이벤트 |
| 3 | 세번째 이벤트 |
+ ---------- + ---------------- +

events_meta

+ ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- +
| 아이디 | event_id | repeat_start | repeat_interval | 반복 _ 년 | repeat_month | 반복 일 | 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
이제 이것을 설정하는 데 몇 분이 걸립니다. 시간이 아닙니다. 🙂


답변

제안 된 솔루션이 작동하는 동안 전체 일정으로 구현하려고 시도했지만 각보기마다 90 건 이상의 데이터베이스 호출이 필요합니다 (현재, 이전 및 다음 달에로드 할 때).

나는 당신이 단순히 데이터베이스에 규칙을 저장하고 모든 관련 규칙을 가져 오기 위해 하나의 쿼리를 재귀 라이브러리 https://github.com/tplaner/When 찾았습니다 .

좋은 해결책을 찾기 위해 많은 시간을 보냈으므로 다른 사람에게 도움이되기를 바랍니다.

편집 :이 라이브러리는 PHP 용입니다


답변

왜 Apache cron 작업과 유사한 메커니즘을 사용하지 않습니까? http://en.wikipedia.org/wiki/Cron

캘린더 / 스케줄링의 경우 표준 캘린더 재발 이벤트를 수용하기 위해 “비트”에 대해 약간 다른 값을 사용합니다. [요일 (0-7), 달 (1-12), 달 (1-31) 대신 시간 (0-23), 최소 (0-59)]

-[년 (N 년마다 반복), 월 (1-12), 월 (1-31), 월 (1-5), 요일 (0-7)과 같은 것을 사용합니다. ]

도움이 되었기를 바랍니다.