2013-09-02 2 views
2

Это заставляет мою голову болеть! : PВычислить перекрывающиеся длительности в MySQL/PHP

У меня есть таблица assignments, и я бы хотел рассчитать продолжительность члена на основе их назначений. В упрощенной форме это будет относительно прямолинейно.

------------------------------------------------------------------------- 
| id | member_id | unit_id | start_date | end_date | 
------------------------------------------------------------------------- 
| 1 | 2  | 23  | 2013-01-01 | 2013-02-01 | 
------------------------------------------------------------------------- 
| 2 | 2  | 25  | 2013-02-01 | 2013-03-01 | 
------------------------------------------------------------------------- 
| 3 | 2  | 27  | 2013-03-01 | NULL  | 
------------------------------------------------------------------------- 

Это просто вопрос того, чтобы делать SUM() из DATEDIFF() на start_date и end_date. Проблема заключается в том, что участники могут иметь одновременные задания.

------------------------------------------------------------------------- 
| id | member_id | unit_id | start_date | end_date | 
------------------------------------------------------------------------- 
| 1 | 2  | 23  | 2013-01-01 | 2013-02-01 | 
------------------------------------------------------------------------- 
| 2 | 2  | 25  | 2013-02-01 | 2013-03-01 | 
------------------------------------------------------------------------- 
| 3 | 2  | 30  | 2013-02-15 | 2013-03-01 |* 
------------------------------------------------------------------------- 
| 4 | 2  | 27  | 2013-03-01 | NULL  | 
------------------------------------------------------------------------- 

Теперь я должен как-то понять, что # 3 произошло в то же время, как # 2, так что я не должен добавить его в SUM().

Идет дальше, что, если у члена есть пробелы в их продолжительности?

------------------------------------------------------------------------- 
| id | member_id | unit_id | start_date | end_date | 
------------------------------------------------------------------------- 
| 1 | 2  | 23  | 2013-01-01 | 2013-02-01 | 
------------------------------------------------------------------------- 
| 2 | 2  | 25  | 2013-02-01 | 2013-02-05 |* 
------------------------------------------------------------------------- 
| 3 | 2  | 30  | 2013-02-15 | 2013-03-01 |* 
------------------------------------------------------------------------- 
| 4 | 2  | 27  | 2013-03-01 | NULL  | 
------------------------------------------------------------------------- 

Кроме того, NULL означает "текущий", так что бы CURDATE().

Любые идеи?

+0

Может быть проще создавать таблицы в SQLFiddle, как для вас, так и для нас. – skv

+0

Также вы можете дать более четкое представление о ожидаемом результате, выглядит немного неясным, что вы хотите использовать для СУММЫ() – skv

+0

@skv Я ищу # дней "назначенных" –

ответ

1

Вот эта идея. Разбивайте каждую запись на две, чтобы получить список дат, когда задания начинаются и останавливаются. Затем определите, сколько заданий активно в заданную дату - в основном добавляя «1» для каждого запуска и «-1» для каждого конца и принимая кумулятивную сумму.

Затем вам нужно определить, когда следующая дата должна получить периоды, прежде чем делать окончательную агрегацию.

Первая часть обрабатывается этим запросом:

select member_id, thedate, 
     @sumstart := if(@prevmemberid = memberid, @sumstart + isstart, isstart) as sumstart, 
     @prevmemberid := memberid 
from (select member_id, start_date as thedate, 1 as isstart 
     from assignments 
     union all 
     select member_id, end_date, -1 as isstart 
     from assignments 
     order by member_id, thedate 
    ) a cross join 
    (select @sumstart := 0, @prevmemberid := NULL) const; 

Остальные затем использует несколько переменных:

select member_id, 
     sum(case when sumstart > 0 then datediff(nextdate, thedate) end) as daysactive 
from (select member_id, thedate, sumstart, 
     if(@prevmemberid = memberid, @nextdate, NULL) as nextdate, 
     @prevmemberid := memberid, 
     @nextdate = thedate 
     from (select member_id, thedate, 
        @sumstart := if(@prevmemberid = memberid, @sumstart + isstart, isstart) as sumstart, 
        @prevmemberid := memberid 
      from (select member_id, start_date as thedate, 1 as isstart 
        from assignments 
        union all 
        select member_id, coalesce(end_date, CURDATE()), -1 as isstart 
        from assignments 
        order by member_id, thedate 
       ) a cross join 
       (select @sumstart := 0, @prevmemberid := NULL) const; 
      ) a cross join 
      (select @nextmemberid := NULL, @nextdate := NULL) const 
     order by member_id, thedate desc; 
    ) a 
group by member_id; 

Я не люблю использовать переменные таким образом, потому что MySQL не гарантирует упорядочение переменных присвоений в заданном select. На практике, однако, они оцениваются в порядке написания (от которого зависит этот запрос). Хотя это можно было бы написать без переменных, без инструкции with, оконных функций или даже представлений, которые принимают подзапросы в предложении from, результирующий SQL будет много углее.

0

Я думаю, что легче выполнять фильтрацию перекрывающихся назначений в коде, а не в SQL. Вы можете получить все задания для определенного member_id, заказанных датой_начал:

select * from assignments where member_id='2' order by start_date asc 

Вы можете затем цикл над этими назначениями и отфильтровать пересекающиеся задания. Два назначения A и B не перекрываются, если A заканчивается до начала B или если B заканчивается до начала A.

Поскольку мы заказали результаты в соответствии с датой начала, мы можем смело игнорировать второй случай: B никогда не запустится до A, поэтому он не может закончиться до начала A. После этого мы получим что-то вроде:

for i=0..assignments.length 
    for j=i+1..assignments.length 
     if (assignments[j].start_date < assignments[i].end_date) 
      assignments[j] = null; // it overlaps -> get rid of it 

Затем цикл по заданиям и просуммировать длительности для непустых заданий. Это должно быть легко