Помогите написать запрос mysql - это должно быть сделано 1000 раз раньше, но я борюсь ... пожалуйста, помогите?

Обновление: я редактирую свой вопрос в надежде получить лучший ответ. Я вижу, что это не так просто, но я не могу поверить, что нет более простого решения, чем то, что было упомянуто до сих пор. Теперь я ищу, есть ли какое-то решение php, mysql, чтобы справиться с этим наиболее эффективным способом. Я изменил свой вопрос ниже, чтобы попытаться внести ясность

У меня есть таблица со следующими полями:

  • ID пользователя
  • Идентификатор группы
  • Действие
  • Дата действия

Эта таблица просто сохраняется всякий раз, когда пользователь в моей системе добавляется в группу (действие = 1) или удаляется из группы (действие = -1). Дата и время записывается всякий раз, когда происходит одно из вышеуказанных действий, как ActionDate

Плата за группу взимается за каждого пользователя, которого они имеют каждый месяц, если пользователь был частью группы в течение как минимум 15 дней этого расчетного месяца (оплачиваемый месяц означает не обязательно начало месяца, может быть с 15 января до 15 февраля)

Я выставляю счета своим группам каждый месяц в начале расчетного месяца для всех пользователей, которые в это время входят в их группу. Теперь в течение месяца они могут добавлять новых пользователей в свою группу или удалять существующих пользователей из своей группы. Если они удалили пользователя, мне нужно знать, был ли пользователь частью группы в течение как минимум 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))

person Gotts    schedule 26.02.2010    source источник
comment
Пожалуйста, опубликуйте код, который вы написали до сих пор. Обычно людям не нравится просто писать код за вас.   -  person Mitch Wheat    schedule 26.02.2010
comment
Я понимаю... проблема в том, что я пробовал так много разных вещей. Вот последний sql, который я написал, но он не охватывает все сценарии и все равно потребует некоторого php'инга. SELECT * FROM groupuserlog где action = 1 и actiondate ›= '2010-02-01' и actiondate ‹ date_add('2010-02-01',INTERVAL 15 DAY) и userid не в (выберите userid из groupuserlog где action = -1 и actiondate ‹ '2010-03-01' и actiondate › date_add('2010-02-01', ИНТЕРВАЛ 15 ДНЕЙ))   -  person Gotts    schedule 26.02.2010
comment
Сколько строк в вашей таблице?   -  person Martin    schedule 26.02.2010
comment
мы не говорим об огромной таблице ... в настоящее время у нас всего 500 пользователей, учитывая, что они не очень часто присоединяются и отсоединяются от групп, я думаю, что строки будут соответствовать количеству пользователей ... Я хотел бы увидеть решение хотя это будет работать так же хорошо для 10 000 пользователей   -  person Gotts    schedule 26.02.2010


Ответы (4)


Я предполагаю, что Пользователь мог присоединиться к группе задолго до расчетного периода и не мог изменить статус в течение расчетного периода. Это требует, чтобы вся ваша таблица была просканирована, чтобы построить таблицу членства, которая выглядит следующим образом:

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, чтобы события «присоединения» появлялись перед событиями «выход». Поле подсчета предназначено для того, чтобы помочь справиться с патологическими случаями, когда членство прекращается в один день, затем возобновляется в тот же день, снова заканчивается в тот же день, снова начинается в тот же день и т. д. В этих случаях , мы увеличиваем счетчик для каждого начального события и уменьшаем его для каждого конечного события. Мы закроем членство только тогда, когда конечное событие обнулит счет. В конце заполнения таблицы членства вы можете запросить значение 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;

«Описание действия» должно разорвать связи, чтобы начальные события появлялись перед конечными событиями, если кто-то присоединится к группе и покинет ее в один и тот же день. 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)

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

  1. удалите оператор «усекать членство».
  2. создать контрольную таблицу, содержащую последнюю обработанную отметку времени
  3. рассчитать последнюю метку времени, которую вы хотите включить в этот запуск (я бы предположил, что max(ActionDate) не подходит, потому что могут быть некоторые неупорядоченные поступления с более ранними метками времени. Хороший выбор — «00:00:00» сегодня утром или "00:00:00" первого дня месяца).
  4. измените запрос курсора, чтобы он включал только записи таблицы между датой последнего запуска (из контрольной таблицы) и вычисленной последней датой.
  5. наконец, обновите контрольную таблицу с расчетной последней датой.

Если вы это сделаете, хорошей идеей также будет передать флаг, который позволит вам перестроить с нуля, т.е. сбросить контрольную таблицу до начала времени и усечь таблицу членства перед запуском обычной процедуры.

person Martin    schedule 26.02.2010
comment
Это довольно кусок работы. Мне придется работать с этим медленно. Я чувствую, что это излишество для моих целей. Я думаю просто переписать таблицу, в которой хранится информация, и подойти к ней по-другому. Но сначала попробуем проработать это решение. Благодарность - person Gotts; 26.02.2010
comment
Я попытаюсь написать пример с использованием курсоров на выходных. - person Martin; 26.02.2010
comment
если вы можете собрать что-то вместе, это было бы большой помощью. Мне не нужно, чтобы вы кодировали все это - мне просто нужен подход, так как я застрял!!! - person Gotts; 26.02.2010
comment
UpVote за столько усилий - person Andy; 27.02.2010

Не уверен насчет вашего стола, но, возможно, что-то вроде?

SELECT COUNT(UserID)
FROM MyTable
WHERE MONTH(ActionDate) = 3
AND GroupID = 1
AND Action = 1
GROUP BY UserID
person Jonathan    schedule 26.02.2010
comment
Это скажет мне, сколько пользователей было добавлено в данном месяце. Мне нужно знать, сколько дней они оставались в составе группы, поэтому мне нужно учитывать, были ли они удалены или нет в этом месяце. - person Gotts; 26.02.2010
comment
Итак, вы хотите запросить данный месяц, чтобы узнать, сколько пользователей находится в этой группе не менее 15 дней? - person Jonathan; 26.02.2010
comment
точно, учитывая, что пользователи могли быть добавлены или удалены несколько раз в этом месяце - person Gotts; 26.02.2010
comment
звучит для меня так, как будто вам нужна еще одна таблица со значением, указывающим дни в группе, а затем увеличивайте/уменьшайте по мере выполнения действий. - person Jonathan; 26.02.2010

Я думаю, что вся сложность заключается в том, как определить соседнее действие удаления для данного действия добавления. Итак, как насчет добавления столбца, указывающего на первичный ключ последующего действия?

Предположим, что этот столбец называется 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;
person ento    schedule 26.02.2010
comment
Это выглядит как шаг в правильном направлении. Несколько вопросов: мне не кажется, что это сработает, если в данном месяце будет более одного действия по добавлению или удалению для каждого пользователя. Аналогично, если в данном месяце есть ТОЛЬКО удаление или ТОЛЬКО добавление. С другой стороны, зачем вам нужен столбец NextID, разве не работает простое соединение с первичным ключом обеих таблиц, INNER JOIN MyTable ON MyTable.ID = RemovedUsers.ID - person Gotts; 26.02.2010

Я начал работать над предложенным Мартином решением и понял, что, хотя это, вероятно, правильный путь, я решил, что буду использовать то, что знаю лучше всего, то есть php, а не сложный sql. Хотя, конечно, менее эффективно, поскольку размер моего стола никогда не будет слишком большим, для меня это имеет смысл.

В итоге я написал простой запрос, который создает историю пользователей в хронологическом порядке для всех действий пользователей в группе за заданный месяц.

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
                    }
                }
person Gotts    schedule 01.03.2010