3

更新: より良い回答を得ることを期待して、質問を編集しています。これはそれほど単純ではありませんが、これまでに述べたものよりも単純な解決策がないとは思えません。私は今、これを最も効率的な方法で処理するための何らかのphp、mysqlソリューションがあるかどうかを調べています。物事を明確にするために、以下の質問を変更しました

次のフィールドを持つテーブルがあります。

  • ユーザーID
  • グループID
  • アクション
  • 行動日

このテーブルは、システム上のユーザーがグループに追加される (アクション = 1) か、グループから削除される (アクション = -1) たびに単純に格納されます。上記のアクションのいずれかが実行されるたびに、日時が ActionDate として記録されます。

グループは、ユーザーがその請求月の少なくとも 15 日間グループの一員であった限り、毎月のすべてのユーザーに対して請求されます (請求月は必ずしも月の初めを意味するわけではなく、1 月 15 日からである可能性があります)。 2月15日まで)

その時点でグループに属しているすべてのユーザーの請求月の初めに、毎月グループに請求します。1 か月の間に、新しいユーザーをグループに追加したり、グループから既存のユーザーを削除したりすることがあります。彼らがユーザーを削除した場合、そのユーザーがその請求月の少なくとも 15 日間グループの一員であったかどうかを知る必要があります。彼がその後何もしなかった場合は、そのユーザーのためにグループに返金する必要があります (彼らは月の初めにユーザーの料金を支払いましたが、彼は 15 日未満でグループに参加していたため) ユーザーを追加した場合ユーザーが少なくとも 15 日間グループに属していた (つまり、請求月の 15 日以内に追加され、15 日が経過する前に削除されなかった) 場合、このユーザーに対してグループに請求する必要があります。ユーザーがグループの一員として 15 日間滞在できなかった場合は、何もしません (無料)。

追加の複雑さのいくつかは次のとおりです。

  • ユーザーは、その請求月中に複数回追加または削除される可能性があり、そのユーザーがグループに属していた合計日数を追跡する必要があります。
  • グループに正しく請求するには、削除されるユーザー (最終的に) または追加されるユーザー (最終的に) を区別できる必要があります。(たとえば、グループの一員として 10 日間滞在しているユーザー - 最終的にグループから削除された場合は、払い戻しを行います。グループに追加された場合は、10 日未満であるため、請求しません)
  • どの請求月でも、ステータスが変更されていないため、ユーザーがこのテーブルに表示されない場合があります。つまり、ユーザーはグループの一員のままであるか、グループの一員ではありませんでした。真実は、これらのユーザーに対して何もする必要がないということです。これらのユーザーは、必要に応じて「今日のグループ内のユーザー数」の基本的な月次計算に含まれます。

単純な mysql ソリューションがないことに気付き始めており、php と mysql の組み合わせが必要です。助けてください!!!

これは私の最近のSQLの試みですが、以下で説明したすべての問題が組み込まれているわけではありません:

SELECT * 
  FROM groupuserlog 
 where action = 1 
   and actiondate >= '2010-02-01' 
   and actiondate < date_add('2010-02-01',INTERVAL 15 DAY) 
   and userid not in (select userid 
                        from groupuserlog 
                       where action = -1 
                         and actiondate < '2010-03-01' 
                         and actiondate > date_add('2010-02-01', INTERVAL 15 DAY))
4

4 に答える 4

1

請求期間のかなり前にユーザーがグループに参加している可能性があり、請求期間中にステータスが変更されない可能性があると想定しています。これには、テーブル全体をスキャンして、次のようなメンバーシップ テーブルを作成する必要があります。

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)

これは、前回の実行以降の変更についてテーブルをスキャンするだけにすることができます。

  1. 「メンバーシップの切り捨て」ステートメントを削除します。
  2. 最後に処理されたタイムスタンプを含む制御テーブルを作成する
  3. この実行に含めたい最後のタイムスタンプを計算します (以前のタイムスタンプで順不同の到着が発生する可能性があるため、 max(ActionDate) は適切ではないことをお勧めします。適切な選択は「00:00:00」です今朝、または月の最初の日の「00:00:00」)。
  4. カーソル クエリを変更して、最後の実行日 (コントロール テーブルから) と計算された最後の日付の間の tbl エントリのみを含めます。
  5. 最後に、計算された最後の日付で制御テーブルを更新します。

それを行う場合は、ゼロから再構築できるようにするフラグを渡すこともお勧めします。通常の手順を実行する前に、制御テーブルを開始時刻にリセットし、メンバーシップ テーブルを切り捨てます。

于 2010-02-26T07:20:15.790 に答える
0

すべての複雑さは、特定の追加アクションの隣接する削除アクションを把握する方法にあると思います。では、後続のアクションの主キーを指す列を追加してはどうでしょうか。

その列がNextIDと呼ばれるとすると、

特定の月にグループに参加し、少なくとも15日間そのグループに参加したユーザーの数:

SELECT COUNT(DISTINCT UserID)
FROM MyTable AS AddedUsers
LEFT OUTER JOIN MyTable
  ON MyTable.ID = AddedUsers.NextID
  AND MyTable.ActionDate > DATE_ADD(AddedUsers.ActionDate, INTERVAL 15 DAY)
  AND MyTable.Action = -1
WHERE MONTH(AddedUsers.ActionDate) = 3 AND YEAR(AddedUsers.ActionDate) = 2012
  AND AddedUsers.GroupID = 1
  AND AddedUsers.Action = 1
  AND MONTH(DATE_ADD(AddedUsers.ActionDate, INTERVAL 15 DAY)) = 3;

少なくとも15日間グループに残っていなかった、特定の月にグループから削除された人の数:

SELECT COUNT(DISTINCT UserID)
FROM MyTable AS RemovedUsers
INNER JOIN MyTable
  ON MyTable.NextID = RemovedUsers.ID
  AND RemovedUsers.ActionDate <= DATE_ADD(MyTable.ActionDate, INTERVAL 15 DAY)
  AND MyTable.Action = 1
WHERE MONTH(RemovedUsers.ActionDate) = 3 AND YEAR(RemovedUsers.ActionDate) = 2012
  AND RemovedUsers.GroupID = 1
  AND RemovedUsers.Action = -1;
于 2010-02-26T02:48:15.390 に答える
0

あなたのテーブルについてはわかりませんが、おそらく次のようなものですか?

SELECT COUNT(UserID)
FROM MyTable
WHERE MONTH(ActionDate) = 3
AND GroupID = 1
AND Action = 1
GROUP BY UserID
于 2010-02-26T01:41:40.640 に答える
0

私はMartinの提案されたソリューションに取り組み始め、それがおそらく正しい道であることに気づきましたが、複雑なSQLではなくphpである私が最もよく知っているものを使用することにしました。確かに効率は悪くなりますが、テーブルのサイズが大きくなりすぎることはないので、私にとっては最も理にかなっています。

最後に、特定の月のグループ内のすべてのユーザー アクティビティのユーザー履歴を時系列で作成する簡単なクエリを作成しました。

SELECT Concat(firstname,' ',lastname) as name, username, UserID,ACTION , Date(ActionDate), Unix_Timestamp(ActionDate) as UN_Action, DateDiff('$enddate', actiondate ) AS DaysTo, DateDiff( actiondate, '$startdate' ) AS DaysFrom
        FROM `groupuserlog` inner join users on users.id = groupuserlog.userid WHERE groupuserlog.groupid = $row[groupid] AND ( actiondate < '$enddate' AND actiondate >= '$startdate') ORDER BY userid, actiondate

次に、結果セットをループして、各ユーザーのすべてのデータを収集します。月の最初のアクション (追加または削除) は、このユーザーが以前にグループに存在していたユーザーであるかどうかを示します。次に、履歴を調べて、アクティブな日数を計算します。最後に、ユーザーが以前にグループに存在したかどうかに応じて、払い戻しまたは請求が必要かどうかを確認します。

それほどきれいではありませんが、仕事をきれいに行い、必要な追加処理を行うことができます。

助けてくれたみんなに感謝します。

誰かが興味を持っている場合、私のphpコードは次のようになります。

while($logrow = mysql_fetch_row($res2)) {

                list($fullname, $username, $guserid,$action,$actiondate,$uxaction,$daysto,$daysfrom) = $logrow;
                if($action == 1)
                    $actiondesc = "Added";
                else
                    $actiondesc = "Removed";


                //listing each user by individual action and building a history
                //the first action is very important as it defines the previous action

                if($curruserid != $guserid) {

                    if($curruserid > 0) {
                        //new user history so reset and store previous user value
                        if($wasMember) {
                            //this was an existing member so check if need refund (if was not on for 15 days)
                            $count = $basecount + $count;
                            echo "<br>User was member and had $count days usage";
                            if($count< 15) {
                                array_push($refundarrinfo, "$fullname (#$guserid $username)");
                                array_push($refundarr, $guserid);
                                echo " REFUND";
                            } else
                                echo " NONE";

                        } else {
                            //this user was not an existing member - see if need to charge (ie if was on for min 15 days)
                            $count = $basecount + $count;
                            echo "<br>User was not a member and was added for $count days usage";
                            if($count >= 15) {
                                array_push($billarrinfo, "$fullname (#$guserid $username)");
                                array_push($billarr, $guserid);
                                echo " CHARGE";
                            } else
                                echo " NONE";
                        }
                    }

                    $basecount = 0;
                    $count = 0;
                    $prev_uxaction = 0;

                    //setup new user - check first action
                     echo "<br><hr><br>$guserid<br>$actiondesc - $actiondate"; // - $daysto - $daysfrom";
                    if($action == 1)
                        $wasMember = FALSE;
                    else {
                        //for first action - if is a remove then store in basecount the number of days that are for sure in place
                        $basecount = $daysfrom;
                        $wasMember = TRUE; //if doing a remove myust have been a member
                    }

                } else
                    echo "<br>$actiondesc - $actiondate";// - $daysto - $daysfrom";

                $curruserid = $guserid;

               if($action == 1) { //action = add
                    $count = $daysto;
                    $prev_uxaction = $uxaction;  //store this actiondate in case needed for remove calculation
                } else { //action = remove
                    //only do something if this is a remove coming after an add - if not it has been taken care of already
                    if($prev_uxaction != 0) {
                        //calc no. of days between previous date and this date and overwrite count by clearing and storing in basecount
                        $count = ($uxaction - $prev_uxaction)/(60 * 60 * 24);
                        $basecount = $basecount + $count;
                        $count = 0; //clear the count as it is stored in basecount
                    }
                }
于 2010-03-01T15:22:04.807 に答える