Создание групп последовательных дней, отвечающих заданным критериям - PullRequest
8 голосов
/ 14 февраля 2012

У меня есть таблица следующей структуры данных в SQL Server:

ID  Date        Allocation
 1, 2012-01-01, 0
 2, 2012-01-02, 2
 3, 2012-01-03, 0
 4, 2012-01-04, 0
 5, 2012-01-05, 0
 6, 2012-01-06, 5

и т. Д.

Что мне нужно сделать, это получить все последовательные периоды дня, где Allocation = 0, и вследующая форма:

Start Date    End Date     DayCount
2012-01-01    2012-01-01   1
2012-01-03    2012-01-05   3

и т. д.

Возможно ли это сделать в SQL, и если да, то как?

Ответы [ 6 ]

3 голосов
/ 14 февраля 2012

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

Это пример техники, описанной здесь и здесь .

1) Присоединить таблицу к себе по соседним значениям "id". Это пары соседних рядов. Выберите строки, в которых поле «распределение» изменилось. Сохраните результат во временной таблице, также сохраняя текущий индекс.

SET @idx = 0;
CREATE TEMPORARY TABLE boundaries
SELECT
   (@idx := @idx + 1) AS idx,
   a1.date AS prev_end,
   a2.date AS next_start,
   a1.allocation as allocation
FROM allocations a1
JOIN allocations a2
ON (a2.id = a1.id + 1)
WHERE a1.allocation != a2.allocation;

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

+------+------------+------------+------------+
| idx  | prev_end   | next_start | allocation |
+------+------------+------------+------------+
|    1 | 2012-01-01 | 2012-01-02 |          0 |
|    2 | 2012-01-02 | 2012-01-03 |          2 |
|    3 | 2012-01-05 | 2012-01-06 |          0 |
+------+------------+------------+------------+

2) Нам нужны начало и конец каждого периода в одной и той же строке, поэтому нам нужно снова объединить соседние строки. Сделайте это, создав вторую временную таблицу, такую ​​как boundaries, но имеющую поле idx 1 больше:

+------+------------+------------+
| idx  | prev_end   | next_start |
+------+------------+------------+
|    2 | 2012-01-01 | 2012-01-02 |
|    3 | 2012-01-02 | 2012-01-03 |
|    4 | 2012-01-05 | 2012-01-06 |
+------+------------+------------+

Теперь присоединитесь к полю idx, и мы получим ответ:

SELECT
  boundaries2.next_start AS start,
  boundaries.prev_end AS end,
  allocation
FROM boundaries
JOIN boundaries2
USING(idx);

+------------+------------+------------+
| start      | end        | allocation |
+------------+------------+------------+
| 2012-01-02 | 2012-01-02 |          2 |
| 2012-01-03 | 2012-01-05 |          0 |
+------------+------------+------------+

** Обратите внимание, что этот ответ правильно получает «внутренние» периоды, но пропускает два «крайних» периода, где распределение = 0 в начале и распределение = 5 в конце. Их можно использовать с помощью UNION предложений, но я хотел бы представить основную идею без этого усложнения.

3 голосов
/ 14 февраля 2012

Следование будет одним из способов сделать это Суть этого решения

  • Используйте CTE, чтобы получить список всех последовательных начальных и конечных дат с Allocation = 0
  • Используйте функцию окна ROW_NUMBER, чтобы назначать номера в зависимости от начальных и конечных дат.
  • Выберите только те записи, где оба ROW_NUMBERS равны 1.
  • Используйте DATEDIFF для вычисления DayCount

Оператор SQL

;WITH r AS (
  SELECT  StartDate = Date, EndDate = Date
  FROM    YourTable
  WHERE   Allocation = 0
  UNION ALL
  SELECT  r.StartDate, q.Date
  FROM    r
          INNER JOIN YourTable q ON DATEDIFF(dd, r.EndDate, q.Date) = 1
  WHERE   q.Allocation = 0          
)
SELECT  [Start Date] = s.StartDate
        , [End Date ] = s.EndDate
        , [DayCount] = DATEDIFF(dd, s.StartDate, s.EndDate) + 1
FROM    (
          SELECT  *
                  , rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC)
                  , rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC)
          FROM    r          
        ) s
WHERE   s.rn1 = 1
        AND s.rn2 = 1
OPTION  (MAXRECURSION 0)

Тестовый скрипт

;WITH q (ID, Date, Allocation) AS (
  SELECT * FROM (VALUES
    (1, '2012-01-01', 0)
    , (2, '2012-01-02', 2)
    , (3, '2012-01-03', 0)
    , (4, '2012-01-04', 0)
    , (5, '2012-01-05', 0)
    , (6, '2012-01-06', 5)
  ) a (a, b, c)
)
, r AS (
  SELECT  StartDate = Date, EndDate = Date
  FROM    q
  WHERE   Allocation = 0
  UNION ALL
  SELECT  r.StartDate, q.Date
  FROM    r
          INNER JOIN q ON DATEDIFF(dd, r.EndDate, q.Date) = 1
  WHERE   q.Allocation = 0          
)
SELECT  s.StartDate, s.EndDate, DATEDIFF(dd, s.StartDate, s.EndDate) + 1
FROM    (
          SELECT  *
                  , rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC)
                  , rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC)
          FROM    r          
        ) s
WHERE   s.rn1 = 1
        AND s.rn2 = 1
OPTION  (MAXRECURSION 0)
1 голос
/ 14 февраля 2012

Попробуйте, если он работает на вас Здесь SDATE для вашей ДАТЫ остается тем же, что и ваша таблица.

SELECT SDATE,
CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) >0 THEN(
CASE WHEN (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) IS NULL THEN SDATE
ELSE (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) END
)ELSE (SELECT SDATE FROM TABLE1 WHERE ID = (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID ))END AS EDATE
,CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) <0 THEN 
(SELECT COUNT(*) FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID )) ELSE
(SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) END AS DAYCOUNT
FROM TABLE1 TBL1 WHERE ALLOCATION = 0
AND (((SELECT ALLOCATION FROM TABLE1 WHERE ID=(SELECT MAX(ID) FROM TABLE1  WHERE ID < TBL1.ID))<> 0 ) OR (SELECT MAX(ID) FROM TABLE1  WHERE ID < TBL1.ID)IS NULL); 
1 голос
/ 14 февраля 2012

Используя этот пример данных:

CREATE TABLE MyTable (ID INT, Date DATETIME, Allocation INT);
INSERT INTO MyTable VALUES (1, {d '2012-01-01'}, 0);
INSERT INTO MyTable VALUES (2, {d '2012-01-02'}, 2);
INSERT INTO MyTable VALUES (3, {d '2012-01-03'}, 0);
INSERT INTO MyTable VALUES (4, {d '2012-01-04'}, 0);
INSERT INTO MyTable VALUES (5, {d '2012-01-05'}, 0);
INSERT INTO MyTable VALUES (6, {d '2012-01-06'}, 5);
GO

Попробуйте это:

WITH DateGroups (ID, Date, Allocation, SeedID) AS (
    SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, MyTable.ID
      FROM MyTable
      LEFT JOIN MyTable Prev ON Prev.Date = DATEADD(d, -1, MyTable.Date) 
                            AND Prev.Allocation = 0
     WHERE Prev.ID IS NULL
       AND MyTable.Allocation = 0
    UNION ALL
    SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, DateGroups.SeedID
      FROM MyTable
      JOIN DateGroups ON MyTable.Date = DATEADD(d, 1, DateGroups.Date)
     WHERE MyTable.Allocation = 0

), StartDates (ID, StartDate, DayCount) AS (
    SELECT SeedID, MIN(Date), COUNT(ID)
      FROM DateGroups
     GROUP BY SeedID

), EndDates (ID, EndDate) AS (
    SELECT SeedID, MAX(Date)
      FROM DateGroups
     GROUP BY SeedID

)
SELECT StartDates.StartDate, EndDates.EndDate, StartDates.DayCount
  FROM StartDates
  JOIN EndDates ON StartDates.ID = EndDates.ID;

Первым разделом запроса является рекурсивный SELECT, который привязывается ко всем строкам, для которых назначено = 0, и чей предыдущий день либо не существует, либо имеет выделение! = 0. Это фактически возвращает идентификаторы: 1 и 3 которые являются начальными датами периодов времени, которые вы хотите вернуть.

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

Результат пока таков:

ID          Date                    Allocation  SeedID
----------- ----------------------- ----------- -----------
1           2012-01-01 00:00:00.000 0           1
3           2012-01-03 00:00:00.000 0           3
4           2012-01-04 00:00:00.000 0           3
5           2012-01-05 00:00:00.000 0           3

Следующий подзапрос использует простой GROUP BY, чтобы отфильтровать все даты начала для каждого SeedID, а также считает дни.

Последний подзапрос делает то же самое с датами окончания, но на этот раз подсчет дней не нужен, поскольку у нас уже есть это.

Последний запрос SELECT объединяет эти два элемента, чтобы объединить даты начала и окончания, и возвращает их вместе с количеством дней.

1 голос
/ 14 февраля 2012

Альтернативный способ с CTE, но без ROW_NUMBER (),

Пример данных:

if object_id('tempdb..#tab') is not null
    drop table #tab

create table #tab (id int, date datetime, allocation int)

insert into #tab
select 1, '2012-01-01', 0 union
select 2, '2012-01-02', 2 union
select 3, '2012-01-03', 0 union
select 4, '2012-01-04', 0 union
select 5, '2012-01-05', 0 union
select 6, '2012-01-06', 5 union
select 7, '2012-01-07', 0 union
select 8, '2012-01-08', 5 union
select 9, '2012-01-09', 0 union
select 10, '2012-01-10', 0

Запрос:

;with cte(s_id, e_id, b_id) as (
    select s.id, e.id, b.id
    from #tab s
    left join #tab e on dateadd(dd, 1, s.date) = e.date and e.allocation = 0
    left join #tab b on dateadd(dd, -1, s.date) = b.date and b.allocation = 0
    where s.allocation = 0
)
select ts.date as [start date], te.date as [end date], count(*) as [day count] from (
    select c1.s_id as s, (
        select min(s_id) from cte c2 
        where c2.e_id is null and c2.s_id >= c1.s_id
    ) as e
    from cte c1
    where b_id is null
) t
join #tab t1 on t1.id between t.s and t.e and t1.allocation = 0
join #tab ts on ts.id = t.s
join #tab te on te.id = t.e
group by t.s, t.e, ts.date, te.date

Живой пример в data.SE .

0 голосов
/ 14 февраля 2012

Решение без CTE:

SELECT a.aDate AS StartDate
    , MIN(c.aDate) AS EndDate
    , (datediff(day, a.aDate, MIN(c.aDate)) + 1) AS DayCount
FROM (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
    JOIN table1 y ON y.aDate <= x.aDate
    GROUP BY x.id, x.aDate, x.allocation
) AS a
LEFT JOIN (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
    JOIN table1 y ON y.aDate <= x.aDate
    GROUP BY x.id, x.aDate, x.allocation
) AS b ON a.idn = b.idn + 1 AND b.allocation = a.allocation
LEFT JOIN (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
    JOIN table1 y ON y.aDate <= x.aDate
    GROUP BY x.id, x.aDate, x.allocation
) AS c ON a.idn <= c.idn AND c.allocation = a.allocation
LEFT JOIN (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
    JOIN table1 y ON y.aDate <= x.aDate
    GROUP BY x.id, x.aDate, x.allocation
) AS d ON c.idn = d.idn - 1 AND d.allocation = c.allocation
WHERE b.idn IS NULL AND c.idn IS NOT NULL AND d.idn IS NULL AND a.allocation = 0
GROUP BY a.aDate

Пример

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...