MySQL
24 October 2010

Подсчет количества событий календаря в каждом месяце года

Постановка задачи:
вывести количество событий (events) каждого месяца года.

Каждое событие имеет два поля
— start_date — дата начала события
— end_date — дата завершения события

Структура таблички с событиями календаря:
CREATE TABLE `events` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `start_date` date default NULL,
  `end_date` date default NULL,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8


* This source code was highlighted with Source Code Highlighter.


Ожидаемый результат:
January 21 February 34 March 47
May 8 June 12 July 23
August 56 September 11 October 35
November 34 Desember 24


Возможные варианты:

— MONTH(start_date) = MONTH(end_date), YEAR(start_date) = YEAR(end_date);
Событие начинается и заканчивается в и том же месяце одного года.

— MONTH(start_date) < MONTH(end_date), YEAR(start_date) = YEAR(end_date);
Событие начинается в одном месяце, а заканчивается в другом месяце одного года.

— MONTH(start_date) = MONTH(end_date), YEAR(start_date) < YEAR(end_date);
Событие начинается и заканчивается в одном и том же месяце но разных лет (просто длится год).

— MONTH(start_date) < MONTH(end_date), YEAR(start_date) < YEAR(end_date);
Событие начинается и заканчивается в разных месяцах разных лет (началось в декабре, закончилось в январе).

Решение:
Решить данную задачу можно двумя способами: вывести значения в строку (каждый столбец — месяц), либо в столбик (каждая строка — месяц с количеством событий).
Намучившись с JOINами было принято решение выводить результаты в строку (неограниченная длительность события стала проблеммой в использовании JOIN). То есть решили решать задачу в «лоб», ограничивать события по году и проверять в каких месяцах проходит данное событие. Для проверки вхождения была написана MYSQL function.

Реализация:
DELIMITER $$

DROP FUNCTION IF EXISTS `isEventInMonth`$$

CREATE FUNCTION `isEventInMonth`(
  startdate DATE,
  enddate DATE,
  _month INT(1),
  _year INT(1)
  ) RETURNS INT(1)
BEGIN
  DECLARE results INT(1);
  
  IF YEAR(startdate) = YEAR(enddate) THEN
      SET results = IF(  MONTH(startdate) = _month
         AND MONTH(enddate)  = _month
         OR
          (   MONTH(startdate) <> MONTH(enddate)
          AND (MONTH(startdate) <= _month
          AND MONTH(enddate)  >= _month)
          )
        ,1,0);
  ELSEIF YEAR(startdate) = _year THEN
      SET results = IF( MONTH(startdate) <= _month ,1,0);  
  ELSE
      SET results = IF( MONTH(enddate) >= _month ,1,0);  
END IF;

  RETURN results;
END$$

DELIMITER ;


* This source code was highlighted with Source Code Highlighter.


Эта функция на вход получает дату начала события, дату конца события, месяц и год, по которым необходимо выполнить проверку, и в качестве результата возвращает 0 либо 1. Иными словами, функция проверяет вхождение текущего месяца в интервал начала и конца события.

Использование:

SELECT
SUM(isEventInMonth(start_date,end_date,1,2011)) AS jan,
SUM(isEventInMonth(start_date,end_date,2,2011)) AS feb,
SUM(isEventInMonth(start_date,end_date,3,2011)) AS mar,
SUM(isEventInMonth(start_date,end_date,4,2011)) AS apr,
SUM(isEventInMonth(start_date,end_date,5,2011)) AS may,
SUM(isEventInMonth(start_date,end_date,6,2011)) AS jun,
SUM(isEventInMonth(start_date,end_date,7,2011)) AS jul,
SUM(isEventInMonth(start_date,end_date,8,2011)) AS aug,
SUM(isEventInMonth(start_date,end_date,9,2011)) AS sep,
SUM(isEventInMonth(start_date,end_date,10,2011)) AS 'oct',
SUM(isEventInMonth(start_date,end_date,11,2011)) AS nov,
SUM(isEventInMonth(start_date,end_date,12,2011)) AS 'dec'
FROM EVENTS WHERE YEAR(start_date) = 2011 OR YEAR(end_date) = 2011


* This source code was highlighted with Source Code Highlighter.


Результат будет в строку, в каждом столбце количество событий в месяц конкретного года.

Резюмируя:
Реализация не притендует на элегантность, я думаю, что кому-нибудь станет полезной при реализации такой стандартной функциональности как календарь событий.
Вопросы и улучшения приветствуются.
PS. не ищите в табличке поля title и description — это переводимые поля и в данной таблице их нет.

UPD:

Пользователь xtender предложил более правильный вариант решения задачи.

SELECT<br>
sum(CASE when t.`start_date`<'2010-02-01' and t.end_date>='2010-01-01' then 1 else 0 end) AS jan,<br>
sum(CASE when t.`start_date`<'2010-03-01' and t.end_date>='2010-02-01' then 1 else 0 end) AS feb,<br>
sum(CASE when t.`start_date`<'2010-04-01' and t.end_date>='2010-03-01' then 1 else 0 end) AS mar,<br>
sum(CASE when t.`start_date`<'2010-05-01' and t.end_date>='2010-04-01' then 1 else 0 end) AS apr,<br>
sum(CASE when t.`start_date`<'2010-06-01' and t.end_date>='2010-05-01' then 1 else 0 end) AS may,<br>
sum(CASE when t.`start_date`<'2010-07-01' and t.end_date>='2010-06-01' then 1 else 0 end) AS jun,<br>
sum(CASE when t.`start_date`<'2010-08-01' and t.end_date>='2010-07-01' then 1 else 0 end) AS jul,<br>
sum(CASE when t.`start_date`<'2010-09-01' and t.end_date>='2010-08-01' then 1 else 0 end) AS aug,<br>
sum(CASE when t.`start_date`<'2010-10-01' and t.end_date>='2010-09-01' then 1 else 0 end) AS sep,<br>
sum(CASE when t.`start_date`<'2010-11-01' and t.end_date>='2010-10-01' then 1 else 0 end) AS oct,<br>
sum(CASE when t.`start_date`<'2010-12-01' and t.end_date>='2010-11-01' then 1 else 0 end) AS nov,<br>
sum(CASE when t.`start_date`<'2011-01-01' and t.end_date>='2010-12-01' then 1 else 0 end) AS dec<br>
FROM events t
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

+1
2.3k 39
Comments 47
Top of the day