【问题标题】:Identify Consecutive Chunks in SQL Server Table识别 SQL Server 表中的连续块
【发布时间】:2019-03-20 18:37:08
【问题描述】:

我有这张桌子:

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

我需要的是在同一组中查找由 Created 排序的连续值,而不是 ID。 Created 和 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)

我可以用 Cursors 做到这一点,但我确信这不是最好的主意,所以我想知道这是否可以在查询中完成。

对于答案(如果有的话),请解释一下。

更新SQLfiddle basic data set

【问题讨论】:

  • 您有足够多的代表知道您需要提供表格的实际详细信息。
  • 听起来你在处理gaps and islands problem
  • 不,这不是差距和岛屿,这是每个组的 TOP 1。除非我看错了问题。
  • 添加了一个示例 SQLfiddle 链接。

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


【解决方案1】:

这看起来确实是一个孤岛问题。

这是一种方法。它可能会比您的变体更快。

gaps-and-islands 的标准想法是生成两组行号,以两种方式对它们进行分区。这样的行号(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,所以现在第一组的 FirstID7LastID1。您的查询返回正确的结果。我上面的简单查询没有。

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

【讨论】:

  • 这是诗。而且比我的粗鲁版本快得多。并且也想通了。保持超过集合的行数差异......很好! 谢谢。
  • 来考虑一下。 FirstIdLastId 完全不相关。这只是建立了条纹、周期和计数的统计视图。如果我需要任何关于集合的明确数据,我总是可以在日期之间进行查询并提取值。因此,如果我从中删除 Id,即使第一个查询也有效。所以将两个查询都变成了视图,一个有 ID,一个没有。填充更多数据后,我还可以比较性能。再次感谢。
  • 欢迎您@CodeAngry。您了解主要思想,因此可以根据具体情况对其进行调整。
【解决方案2】:

使用添加 Row_Number 列的 CTE,按 GroupIdValue 分区并按 Created 排序。

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

虽然我注意到您现在使用的是 SQL Server 2017,但您应该可以使用 First_Value() and Last_Value() 而不是相关子查询。

【讨论】:

  • 我认为您的解决方案与弗拉基米尔所写的差不多。但是我的 SQL 力量不够强大,无法弄清楚。
【解决方案3】:

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

链接在这里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 方法似乎比这快很多。仍在测试中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-10
    • 2016-08-26
    • 2011-01-10
    • 1970-01-01
    • 2023-04-06
    • 1970-01-01
    相关资源
    最近更新 更多