Помогите написать запрос mysql - это должно было быть сделано 1000 раз, но я изо всех сил .. Пожалуйста, помогите? - PullRequest
3 голосов
/ 26 февраля 2010

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

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

  • Идентификатор_пользователь
  • GroupID
  • Действие
  • ActionDate

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

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

Я выставляю счета своим группам каждый месяц в начале расчетного месяца для всех пользователей, которые в то время были частью их группы. Теперь в течение месяца они могут добавлять новых пользователей в свою группу или удалять существующих пользователей из своей группы. Если они удалили пользователя, мне нужно знать, входил ли пользователь в группу в течение как минимум 15 дней этого расчетного месяца. Если он тогда ничего не делает, если нет, то необходимо вернуть группу за этого пользователя (так как они заплатили за пользователя в начале месяца, но он был частью группы менее 15 дней) Если они добавили пользователя и пользователь находился в группе не менее 15 дней (т. Е. Был добавлен в течение 15 дней с расчетного месяца и не был удален до истечения 15 дней), то с этого пользователя должна быть снята плата. Если у пользователя не было 15 дней в составе группы, мы ничего не делаем (бесплатно).

Некоторые из дополнительных сложностей:

  • Пользователь может быть добавлен или удален несколько раз в течение этого расчетного месяца, и нам необходимо отслеживать общее количество дней, в течение которых он был частью группы
  • Нам необходимо различать пользователей, которые удаляются (в конечном итоге) или добавляются (в конечном итоге), чтобы правильно выставлять счета группе. (например, пользователь, у которого есть 10 дней в составе группы - если он в конечном итоге был удален из группы, мы выдаем возврат. Если он был добавлен в группу, то мы не взимаем плату - потому что менее 10 дней)
  • В любом конкретном месяце выставления счетов пользователь может не отображаться в этой таблице, поскольку его статус не изменился - то есть он оставался частью группы или никогда не входил в группу. Правда в том, что с этими пользователями ничего не нужно делать, так как при необходимости они будут включены в базовый ежемесячный расчет «сколько пользователей в группе сегодня»

Я начинаю понимать, что простого решения для mysql не существует, и мне нужен php, mysql combo. Пожалуйста, помогите !!!

Вот моя последняя попытка 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 ]

1 голос
/ 26 февраля 2010

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

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)

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

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

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

0 голосов
/ 01 марта 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
                    }
                }
0 голосов
/ 26 февраля 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;
0 голосов
/ 26 февраля 2010

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

SELECT COUNT(UserID)
FROM MyTable
WHERE MONTH(ActionDate) = 3
AND GroupID = 1
AND Action = 1
GROUP BY UserID
...