請求期間のかなり前にユーザーがグループに参加している可能性があり、請求期間中にステータスが変更されない可能性があると想定しています。これには、テーブル全体をスキャンして、次のようなメンバーシップ テーブルを作成する必要があります。
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」イベントが「leave」イベントの前に表示されるようにする必要があります。カウント フィールドは、メンバーシップがある日付で終了し、同じ日に再開し、同じ日に再び終了し、同じ日に再開するなど、異常なケースに対処するのに役立ちます。これらのケースでは、開始イベントごとにカウントを増やし、終了イベントごとに減らします。終了イベントによってカウントがゼロになった場合にのみ、メンバーシップを終了します。メンバーシップ テーブルへの入力の最後に、count の値をクエリできます。閉鎖されたメンバーシップはカウント = 0、オープン メンバーシップ (まだ閉鎖されていない) はカウント = 1 である必要があります。
カーソル クエリは次のとおりです。
select UserID as _UserID, GroupID as _GroupID, Date(ActionDate) adate, Action from tbl
order by UserId, GroupId, Date(ActionDate), Action desc;
「アクションの説明」は、誰かが同じ日にグループに参加してグループを脱退した場合に、終了イベントの前に開始イベントが表示されるように、関係を壊す必要があります。日数の単位に関心があるため、ActionDate を datetime から date に変換する必要があります。
カーソル内のアクションは次のとおりです。
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)
次に、2009 年 2 月に表示される請求日数を確認します。
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 エントリのみを含めます。
- 最後に、計算された最後の日付で制御テーブルを更新します。
それを行う場合は、ゼロから再構築できるようにするフラグを渡すこともお勧めします。通常の手順を実行する前に、制御テーブルを開始時刻にリセットし、メンバーシップ テーブルを切り捨てます。