Определите последовательные блоки в таблице SQL Server - PullRequest
1 голос
/ 20 марта 2019

У меня есть эта таблица:

ValueId bigint // (identity) item ID
ListId bigint // group ID
ValueDelta int // item value
ValueCreated datetime2 // item created

Мне нужно найти последовательные значения внутри одной и той же группы, упорядоченные по Created, а не по ID. Создание и ID не гарантируется в том же порядке.

Таким образом, вывод должен быть:

ListID bigint
FirstId bigint // from this ID (first in LID with Value ordered by Date)
LastId bigint // to this ID (last in LID with Value ordered by Date)
ValueDelta int // all share this value
ValueCount // and this many occurrences (number of items between FirstId and LastId)

Я могу сделать это с помощью курсоров, но я уверен, что это не лучшая идея, поэтому мне интересно, можно ли это сделать в запросе.

Пожалуйста, ответьте (если есть) , объясните немного.

ОБНОВЛЕНИЕ : Базовый набор данных SQLfiddle

Ответы [ 3 ]

1 голос
/ 21 марта 2019

Это похоже на проблему пропусков и островов.

Вот один из способов сделать это.Скорее всего, он будет работать быстрее, чем ваш вариант.

Стандартная идея для пробелов и островков - создать два набора номеров строк, разделив их двумя способами.Разница между такими номерами строк (rn1-rn2) останется неизменной в каждом последующем фрагменте.Запустите запрос под CTE-за-CTE и просмотрите промежуточные результаты, чтобы увидеть, что происходит.

WITH
CTE_RN
AS
(
    SELECT
        [ValueId]
        ,[ListId]
        ,[ValueDelta]
        ,[ValueCreated]
        ,ROW_NUMBER() OVER (PARTITION BY ListID ORDER BY ValueCreated) AS rn1
        ,ROW_NUMBER() OVER (PARTITION BY ListID, [ValueDelta] ORDER BY ValueCreated) AS rn2
    FROM [Value]
)
SELECT
    ListID
    ,MIN(ValueID) AS FirstID
    ,MAX(ValueID) AS LastID
    ,MIN(ValueCreated) AS FirstCreated
    ,MAX(ValueCreated) AS LastCreated
    ,ValueDelta
    ,COUNT(*) AS ValueCount
FROM CTE_RN
GROUP BY
    ListID
    ,ValueDelta
    ,rn1-rn2
ORDER BY
    FirstCreated
;

Этот запрос дает тот же результат, что и ваш в вашем наборе данных.

Этоне совсем ясно, могут ли FirstID и LastID быть MIN и MAX, или они действительно должны быть из первой и последней строк (если они упорядочены ValueCreated).Если вам действительно нужны первое и последнее, запрос станет немного сложнее.


В исходных данных выборки «first» и «min» для FirstID одинаковы.Давайте немного изменим примерный набор данных, чтобы подчеркнуть это различие:

insert into [Value]
([ListId], [ValueDelta], [ValueCreated])
values
(1, 1, '2019-01-01 01:01:02'), -- 1.1
(1, 0, '2019-01-01 01:02:01'), -- 2.1
(1, 0, '2019-01-01 01:03:01'), -- 2.2
(1, 0, '2019-01-01 01:04:01'), -- 2.3
(1, -1, '2019-01-01 01:05:01'), -- 3.1
(1, -1, '2019-01-01 01:06:01'), -- 3.2
(1, 1, '2019-01-01 01:01:01'), -- 1.2
(1, 1, '2019-01-01 01:08:01'), -- 4.2
(2, 1, '2019-01-01 01:08:01') -- 5.1
;

Все, что я сделал, это поменял ValueCreated между первой и седьмой строками, поэтому теперь FirstID первой группы равен 7и LastID равно 1.Ваш запрос возвращает правильный результат.Мой простой запрос выше не делает.

Вот вариант, который дает правильный результат.Я решил использовать функции FIRST_VALUE и LAST_VALUE, чтобы получить соответствующие идентификаторы.Опять же, запустите запрос CTE-by-CTE и изучите промежуточные результаты, чтобы увидеть, что происходит.Этот вариант дает тот же результат, что и ваш запрос, даже с настроенным набором данных выборки.

WITH
CTE_RN
AS
(
    SELECT
        [ValueId]
        ,[ListId]
        ,[ValueDelta]
        ,[ValueCreated]
        ,ROW_NUMBER() OVER (PARTITION BY ListID ORDER BY ValueCreated) AS rn1
        ,ROW_NUMBER() OVER (PARTITION BY ListID, ValueDelta ORDER BY ValueCreated) AS rn2
    FROM [Value]
)
,CTE2
AS
(
    SELECT
        ValueId
        ,ListId
        ,ValueDelta
        ,ValueCreated
        ,rn1
        ,rn2
        ,rn1-rn2 AS Diff
        ,FIRST_VALUE(ValueID) OVER(
            PARTITION BY ListID, ValueDelta, rn1-rn2 ORDER BY ValueCreated
            ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS FirstID
        ,LAST_VALUE(ValueID) OVER(
            PARTITION BY ListID, ValueDelta, rn1-rn2 ORDER BY ValueCreated
            ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS LastID
    FROM CTE_RN
)
SELECT
    ListID
    ,FirstID
    ,LastID
    ,MIN(ValueCreated) AS FirstCreated
    ,MAX(ValueCreated) AS LastCreated
    ,ValueDelta
    ,COUNT(*) AS ValueCount
FROM CTE2
GROUP BY
    ListID
    ,ValueDelta
    ,rn1-rn2
    ,FirstID
    ,LastID
ORDER BY FirstCreated;
1 голос
/ 20 марта 2019

Используйте CTE, который добавляет столбец Row_Number, разделенный на GroupId и Value и упорядоченный по Created.

Затем выберите из CTE, GROUP BY GroupId и Value; используйте COUNT (*), чтобы получить Count, и используйте коррелированные подзапросы, чтобы выбрать ValueId с MIN (RowNumber) (который всегда будет равен 1, так что вы можете просто использовать его вместо MIN) и MAX (RowNumber) ) чтобы получить FirstId и LastId.

Хотя теперь, когда я заметил, что вы используете SQL Server 2017, вы должны иметь возможность использовать First_Value () и Last_Value () вместо коррелированных подзапросов.

0 голосов
/ 20 марта 2019

После многих итераций я думаю, что у меня есть рабочее решение.Я абсолютно уверен, что он далек от оптимального, но он работает.

Ссылка здесь : http://sqlfiddle.com/#!18/4ee9f/3

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

create table [Value]
(
    [ValueId] bigint not null identity(1,1),
    [ListId] bigint not null,
    [ValueDelta] int not null,
    [ValueCreated] datetime2 not null,
    constraint [PK_Value] primary key clustered ([ValueId])
);

insert into [Value]
([ListId], [ValueDelta], [ValueCreated])
values
(1, 1, '2019-01-01 01:01:01'), -- 1.1
(1, 0, '2019-01-01 01:02:01'), -- 2.1
(1, 0, '2019-01-01 01:03:01'), -- 2.2
(1, 0, '2019-01-01 01:04:01'), -- 2.3
(1, -1, '2019-01-01 01:05:01'), -- 3.1
(1, -1, '2019-01-01 01:06:01'), -- 3.2
(1, 1, '2019-01-01 01:01:02'), -- 1.2
(1, 1, '2019-01-01 01:08:01'), -- 4.2
(2, 1, '2019-01-01 01:08:01') -- 5.1

Запрос, который, кажется, работает:

-- this is the actual order of data
select *
from [Value]
order by [ListId] asc, [ValueCreated] asc;

-- there are 4 sets here
-- set 1 GroupId=1, Id=1&7, Value=1
-- set 2 GroupId=1, Id=2-4, Value=0
-- set 3 GroupId=1, Id=5-6, Value=-1
-- set 4 GroupId=1, Id=8-8, Value=1
-- set 5 GroupId=2, Id=9-9, Value=1

with [cte1] as
(
    select [v1].[ListId]
        ,[v2].[ValueId] as [FirstId], [v2].[ValueCreated] as [FirstCreated]
        ,[v1].[ValueId] as [LastId], [v1].[ValueCreated] as [LastCreated]
        ,isnull([v1].[ValueDelta], 0) as [ValueDelta]
    from [dbo].[Value] [v1]
        join [dbo].[Value] [v2] on [v2].[ListId] = [v1].[ListId]
            and isnull([v2].[ValueDeltaPrev], 0) = isnull([v1].[ValueDeltaPrev], 0)
            and [v2].[ValueCreated] <= [v1].[ValueCreated] and not exists (
                select 1
                from [dbo].[Value] [v3]
                where 1=1
                    and ([v3].[ListId] = [v1].[ListId])
                    and ([v3].[ValueCreated] between [v2].[ValueCreated] and [v1].[ValueCreated])
                    and [v3].[ValueDelta] != [v1].[ValueDelta]
            )
), [cte2] as
(
    select [t1].*
    from [cte1] [t1]
    where not exists (select 1 from [cte1] [t2] where [t2].[ListId] = [t1].[ListId]
        and ([t1].[FirstId] != [t2].[FirstId] or [t1].[LastId] != [t2].[LastId])
        and [t1].[FirstCreated] between [t2].[FirstCreated] and [t2].[LastCreated]
        and [t1].[LastCreated] between [t2].[FirstCreated] and [t2].[LastCreated]
        )
)
select [ListId], [FirstId], [LastId], [FirstCreated], [LastCreated], [ValueDelta] as [ValueDelta]
    ,(select count(*) from [dbo].[Value] where [ListId] = [t].[ListId] and [ValueCreated] between [t].[FirstCreated] and [t].[LastCreated]) as [ValueCount]
from [cte2] [t];

Как это работает:

  • присоединить таблицу к себе в том же списке, но толькодля более старых (или равных дат для обработки отдельных наборов) значений
  • снова объединяются на себя и исключают любые перекрытия, сохраняя только самый большой набор дат
  • , как только мы идентифицируем самые большие наборы, мы затем считаем записи в датах набора

Если кто-то может найти лучшее / более дружелюбное решение, вы получите ответ.

PS : Тупой прямойКурсорный подход кажется намного быстрее, чем этот.Все еще тестирую.

...