Я предполагаю, что пользователь мог присоединиться к группе задолго до расчетного периода и не мог изменить статус в течение расчетного периода. Для этого необходимо отсканировать всю таблицу, чтобы создать таблицу членства, которая выглядит следующим образом:
create table membership (
UserId int not null,
GroupId int not null,
start datetime not null,
end datetime not null,
count int not null,
primary key (UserId, GroupId, end )
);
Как только это заполнено правильно, вы легко получите нужный ответ:
set @sm = '2009-02-01';
set @em = date_sub( date_add( @sm, interval 1 month), interval 1 day);
# sum( datediff( e, s ) + 1 ) -- +1 needed to include last day in billing
select UserId,
GroupId,
sum(datediff( if(end > @em, @em, end),
if(start<@sm, @sm, start) ) + 1 ) as n
from membership
where start <= @em and end >= @sm
group by UserId, GroupId
having n >= 15;
Сканирование должно выполняться курсором (который не будет быстрым). Нам нужно отсортировать вашу входную таблицу по ActionDate и Action, чтобы события «join» появлялись перед событиями «exit». Поле счета
чтобы помочь справиться с патологическими случаями - когда членство заканчивается в одну дату, затем возобновляется в тот же день и заканчивается снова в тот же день, и начинается снова в тот же день, и т. д. В этих случаях мы увеличиваем количество для каждого начального события и уменьшение для каждого конечного события. Мы закроем членство только тогда, когда конечное событие снизит счет до нуля. В конце заполнения таблицы членства вы можете запросить значение count: закрытое членство должно иметь count = 0, открытое членство (еще не закрытое) должно иметь count = 1. Любые записи с количеством за пределами 0 и 1 должны быть тщательно проверены. - это будет указывать на ошибку где-то.
Запрос курсора:
select UserID as _UserID, GroupID as _GroupID, Date(ActionDate) adate, Action from tbl
order by UserId, GroupId, Date(ActionDate), Action desc;
«Action desc» должен разорвать связи, чтобы начальные события появлялись до конечных событий, если кто-то присоединится и покинет группу в тот же день. ActionDate необходимо преобразовать из даты в дату, потому что мы заинтересованы в единицах дней.
Действия внутри курсора будут следующими:
if (Action = 1) then
insert into membership
set start=ActionDate, end='2037-12-31', UserId=_UserId, GroupId=_GroupId, count=1
on duplicate key update set count = count + 1;
elsif (Action == -1)
update membership
set end= if( count=1, Actiondate, end),
count = count - 1
where UserId=_UserId and GroupId=_GroupId and end = '2037-12-31';
end if
Я не дал вам точного синтаксиса требуемого определения курсора (вы можете найти это в руководстве по MySQL), потому что полный код затеняет идею. Фактически, может быть быстрее выполнить логику курсора в вашем приложении - возможно, даже создать детали членства в приложении.
РЕДАКТИРОВАТЬ: Вот фактический код:
create table tbl (
UserId int not null,
GroupId int not null,
Action int not null,
ActionDate datetime not null
);
create table membership (
UserId int not null,
GroupId int not null,
start datetime not null,
end datetime not null,
count int not null,
primary key (UserId, GroupId, end )
);
drop procedure if exists popbill;
delimiter //
CREATE PROCEDURE popbill()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE _UserId, _GroupId, _Action int;
DECLARE _adate date;
DECLARE cur1 CURSOR FOR
select UserID, GroupID, Date(ActionDate) adate, Action
from tbl order by UserId, GroupId, Date(ActionDate), Action desc;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
truncate table membership;
OPEN cur1;
REPEAT
FETCH cur1 INTO _UserId, _GroupId, _adate, _Action;
IF NOT done THEN
IF _Action = 1 THEN
INSERT INTO membership
set start=_adate, end='2037-12-31',
UserId=_UserId, GroupId=_GroupId, count=1
on duplicate key update count = count + 1;
ELSE
update membership
set end= if( count=1, _adate, end),
count = count - 1
where UserId=_UserId and GroupId=_GroupId and end = '2037-12-31';
END IF;
END IF;
UNTIL done END REPEAT;
CLOSE cur1;
END
//
delimiter ;
Вот некоторые тестовые данные:
insert into tbl values (1, 10, 1, '2009-01-01' );
insert into tbl values (1, 10, -1, '2009-01-02' );
insert into tbl values (1, 10, 1, '2009-02-03' );
insert into tbl values (1, 10, -1, '2009-02-05' );
insert into tbl values (1, 10, 1, '2009-02-05' );
insert into tbl values (1, 10, -1, '2009-02-05' );
insert into tbl values (1, 10, 1, '2009-02-06' );
insert into tbl values (1, 10, -1, '2009-02-06' );
insert into tbl values (2, 10, 1, '2009-02-20' );
insert into tbl values (2, 10, -1, '2009-05-30');
insert into tbl values (3, 10, 1, '2009-01-01' );
insert into tbl values (4, 10, 1, '2009-01-31' );
insert into tbl values (4, 10, -1, '2009-05-31' );
Вот код, который запускается, и результаты:
call popbill;
select * from membership;
+--------+---------+---------------------+---------------------+-------+
| UserId | GroupId | start | end | count |
+--------+---------+---------------------+---------------------+-------+
| 1 | 10 | 2009-01-01 00:00:00 | 2009-01-02 00:00:00 | 0 |
| 1 | 10 | 2009-02-03 00:00:00 | 2009-02-05 00:00:00 | 0 |
| 1 | 10 | 2009-02-06 00:00:00 | 2009-02-06 00:00:00 | 0 |
| 2 | 10 | 2009-02-20 00:00:00 | 2009-05-30 00:00:00 | 0 |
| 3 | 10 | 2009-01-01 00:00:00 | 2037-12-31 00:00:00 | 1 |
| 4 | 10 | 2009-01-31 00:00:00 | 2009-05-31 00:00:00 | 0 |
+--------+---------+---------------------+---------------------+-------+
6 rows in set (0.00 sec)
Затем проверьте, сколько дней выставления счетов указано в фев. 09:
set @sm = '2009-02-01';
set @em = date_sub( date_add( @sm, interval 1 month), interval 1 day);
select UserId,
GroupId,
sum(datediff( if(end > @em, @em, end),
if(start<@sm, @sm, start) ) + 1 ) as n
from membership
where start <= @em and end >= @sm
group by UserId, GroupId;
+--------+---------+------+
| UserId | GroupId | n |
+--------+---------+------+
| 1 | 10 | 4 |
| 2 | 10 | 9 |
| 3 | 10 | 28 |
| 4 | 10 | 28 |
+--------+---------+------+
4 rows in set (0.00 sec)
Это можно сделать, чтобы просто проверить таблицу на наличие изменений с момента последнего запуска:
- удалить оператор "усекать членство".
- создать контрольную таблицу, содержащую последнюю обработанную метку времени
- вычислите последнюю временную метку, которую вы хотите включить в этот прогон (я бы предположил, что max (ActionDate) не годится, потому что могут быть некоторые неупорядоченные прибытия, прибывающие с более ранними временными метками. Хороший выбор - "00:00". : 00 "этим утром или" 00:00:00 "в первый день месяца).
- изменить запрос курсора, чтобы включить только записи tbl между датой последнего запуска (из контрольной таблицы) и вычисленной последней датой.
- окончательно обновить контрольную таблицу с вычисленной последней датой.
Если вы сделаете это, также будет хорошей идеей передать флаг, который позволяет вам восстановить с нуля, т.е. сбросьте управляющую таблицу до начала времени и обрежьте таблицу членства перед выполнением обычной процедуры.