识别SQL Server表中的连续块

时间:2019-03-20 18:37:09

标签: sql sql-server tsql sql-server-2017

我有这张桌子:

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

我需要在Created(而不是ID)排序的同一组中查找连续值。不能保证创建的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 basic data set

3 个答案:

答案 0 :(得分:1)

使用CTE,添加一个Row_Number列,该列由GroupIdValue分区,并由Created排序。

然后从CTE中选择GROUP BY GroupIdValue;使用COUNT(*)获取Count,并使用相关子查询选择带有MIN(RowNumber)(始终为1的ValueId),因此您可以使用它代替MIN() MAX(RowNumber)获得FirstIdLastId

尽管现在我已经注意到您正在使用SQL Server 2017,但是您应该能够使用First_Value() and Last_Value()而不是相关的子查询。

答案 1 :(得分:1)

它看起来确实像是一个空白与孤岛的问题。

这是一种方法。它的工作速度可能会比您的变体快。

“空缺与孤岛”的标准思想是生成两组行号,以两种方式对其进行分区。这样的行号(rn1-rn2)之间的差异将在每个连续的块中保持不变。在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]
)
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
;

此查询在样本数据集上产生与您相同的结果。

尚不清楚FirstIDLastID是否可以是MINMAX,或者它们确实必须来自第一行和最后一行(由ValueCreated排序时) )。如果您确实需要第一个和最后一个,则查询将变得更加复杂。


在原始样本数据集中,FirstID的“ first”和“ min”相同。让我们对样本数据集进行一些更改以突出这种区别:

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,所以现在第一组的FirstID7,而LastID1。您的查询返回正确的结果。我上面的简单查询没有。

这里是产生正确结果的变体。我决定使用FIRST_VALUELAST_VALUE函数来获取适当的ID。再次,按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;

答案 2 :(得分:0)

经过多次迭代,我认为我有一个可行的解决方案。我绝对可以肯定,它远非最佳,但可以。

链接在此处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 愚蠢的简单Cursor方法似乎比此方法快得多。仍在测试。