Найти дату начала и окончания (на основе набора) в T-SQL - PullRequest
2 голосов
/ 10 января 2011

У меня есть следующее.

Name    Date
A   2011-01-01 01:00:00.000
A   2011-02-01 02:00:00.000
A   2011-03-01 03:00:00.000
B   2011-04-01 04:00:00.000
A   2011-05-01 07:00:00.000

Желаемый результат:

Name       StartDate                        EndDate
-------------------------------------------------------------------
A          2011-01-01 01:00:00.000         2011-04-01 04:00:00.000    
B          2011-04-01 04:00:00.000         2011-05-01 07:00:00.000    
A          2011-05-01 07:00:00.000         NULL

Как добиться того же, используя TSQL в подходе на основе множеств.

DDL соответствует

DECLARE @t TABLE(PersonName VARCHAR(32), [Date] DATETIME) 
INSERT INTO @t VALUES('A', '2011-01-01 01:00:00') 
INSERT INTO @t VALUES('A', '2011-01-02 02:00:00') 
INSERT INTO @t VALUES('A', '2011-01-03 03:00:00') 
INSERT INTO @t VALUES('B', '2011-01-04 04:00:00') 
INSERT INTO @t VALUES('A', '2011-01-05 07:00:00')

Select * from @t

Ответы [ 5 ]

7 голосов
/ 10 января 2011
;WITH cte1
     AS (SELECT *,
                ROW_NUMBER() OVER (ORDER BY Date) -
                ROW_NUMBER() OVER (PARTITION BY PersonName
                ORDER BY Date) AS G
         FROM   @t),
     cte2
     AS (SELECT PersonName,
                MIN([Date]) StartDate,
                ROW_NUMBER() OVER (ORDER BY  MIN([Date])) AS rn
         FROM   cte1
         GROUP  BY PersonName,
                   G)
SELECT a.PersonName,
       a.StartDate,
       b.StartDate AS EndDate
FROM   cte2 a
       LEFT JOIN cte2 b
         ON a.rn + 1 = b.rn  

Поскольку результаты CTE, как правило, не материализуются, тем не менее, вы можете обнаружить, что вы получите лучшую производительность, если материализуете промежуточный результат самостоятельно, как показано ниже.

DECLARE @t2 TABLE (
  rn         INT IDENTITY(1, 1) PRIMARY KEY,
  PersonName VARCHAR(32),
  StartDate  DATETIME );

INSERT INTO @t2
SELECT PersonName,
       MIN([Date]) StartDate
FROM   (SELECT *,
               ROW_NUMBER() OVER (ORDER BY Date) -
               ROW_NUMBER() OVER (PARTITION BY PersonName
               ORDER BY Date) AS G
        FROM   @t) t
GROUP  BY PersonName,
          G
ORDER  BY StartDate

SELECT a.PersonName,
       a.StartDate,
       b.StartDate AS EndDate
FROM   @t2 a
       LEFT JOIN @t2 b
         ON a.rn + 1 = b.rn 
0 голосов
/ 18 мая 2018

Существует очень быстрый способ сделать это, используя немного теории пробелов и островов:

WITH CTE as (SELECT PersonName, [Date]
                   , Row_Number() over (ORDER BY [Date])
                     - Row_Number() over (ORDER BY PersonName, [Date]) as Island
             FROM @t)

Select PersonName, Min([Date]), Max([Date])
from CTE
GROUP BY Island, PersonName
ORDER BY Min([Date])
0 голосов
/ 10 января 2011
SELECT
  PersonName,
  StartDate = MIN(Date),
  EndDate
FROM (
  SELECT
    PersonName,
    Date,
    EndDate = (
      /* get the earliest date after current date
         associated with a different person */
      SELECT MIN(t1.Date)
      FROM @t AS t1
      WHERE t1.Date > t.Date
        AND t1.PersonName <> t.PersonName
    )
  FROM @t AS t
) s
GROUP BY PersonName, EndDate
ORDER BY 2

Как правило, для каждого Date мы находим ближайшую дату после нее, такую, которая связана с другим PersonName.Это дает нам EndDate, что теперь отличает нас для последовательных групп дат для одного и того же человека.

Теперь нам нужно только сгруппировать данные по PersonName & EndDate и получить минимальное Dateв каждой группе как StartDate.И да, конечно, сортируйте данные по StartDate.

0 голосов
/ 10 января 2011

Получить номер строки, чтобы вы знали, где находится предыдущая запись. Затем возьмите запись и следующую запись после нее. Когда состояние меняется, у нас есть строка-кандидат.

select 
  state, 
  min(start_timestamp),
  max(end_timestamp)

from
(
    select
        first.state, 
        first.timestamp_ as start_timestamp,
        second.timestamp_ as end_timestamp

        from
        (
            select
                *, row_number() over (order by timestamp_) as id
            from test
        ) as first

        left outer join
        (
            select
                *, row_number() over (order by timestamp_) as id
            from test
        ) as second
        on 
            first.id = second.id - 1 
            and first.state != second.state
) as agg
group by state
    having max(end_timestamp) is not null 

union

-- last row wont have a ending row
--(select state, timestamp_, null from test order by timestamp_ desc limit 1)
    -- I think it something like this for sql server
     (select top state, timestamp_, null from test order by timestamp_ desc)

order by 2
;

Протестировано с PostgreSQL, но должно работать и с SQL Server

0 голосов
/ 10 января 2011

Другой ответ с cte - хороший. Другой вариант - перебор коллекции в любом случае. Он не основан на множестве, но это еще один способ сделать это.

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

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

...