根据行顺序获取计数

Get a count based on the row order

我有一个 table 这种结构

Create Table Example (
[order] INT,
[typeID] INT
)

有了这个数据:

order|type
1   7
2   11
3   11
4   18
5   5
6   19
7   5
8   5
9   3
10  11
11  11
12  3

我需要根据订单获取每种类型的数量,例如:

type|count
7      1
11     **2**
18     1
5      1
19     1
5      **2**
3      1
11     **2**
3      1

上下文

假设这个 table 是关于房子的,所以我在订单中列出了房子。所以我有

所以我需要显示该信息的浓缩版。我要说:

因此计数是基于顺序的。如果我能够在分区更改时重置 RANK,DENSE_RANK 函数将对我有帮助。

所以我有一个答案,但我必须警告你,由于它的完成方式,它可能会引起一些人的注意。它使用称为“Quirky Update”的东西。如果你打算实施这个,请看在上帝的份上阅读链接的文章并理解这是一个 "undocumented hack" 需要精确实施以避免意外后果。

如果您有少量数据,为了简单明了,我会逐行处理。但是,如果您有大量数据并且仍然需要高性能,那么这可能就可以了。

要求

  1. Table 必须有一个按您要进行的顺序排列的聚簇索引
  2. Table 必须没有其他索引(这些可能会导致 SQL 从另一个顺序不正确的索引读取数据,导致行顺序的量子叠加崩溃).
  3. Table 操作时必须完全锁定(tablockx)
  4. 更新必须以串行方式进行 (maxdop 1)

它的作用

您知道人们怎么告诉您 table 中的数据没有隐式顺序吗?这在 99% 的情况下仍然是正确的。除了我们知道最终它必须以某种顺序存储在磁盘上。这就是我们在这里利用的顺序。通过强制更新聚集索引以及您可以在更新列的同一更新语句中分配变量这一事实,您可以真正快速地有效滚动数据。

让我们设置数据:

if object_id('tempdb.dbo.#t') is not null drop table #t
create table #t
(
    _order int primary key clustered,
    _type int,
    _grp int
)

insert into #t (_order, _type)
select 1,7
union all select 2,11
union all select 3,11
union all select 4,18
union all select 5,5
union all select 6,19
union all select 7,5
union all select 8,5
union all select 9,3
union all select 10,11
union all select 11,11
union all select 12,3

这是更新声明。我将介绍下面的每个组件

declare @Order int, @Type int, @Grp int

update #t with (tablockx)
set @Order = _order,
    @Grp = case when _order = 1 then 1
                when _type != @Type then @grp + 1
                else @Grp
           end,
    @Type = _type,
    _grp = @Grp
option (maxdop 1)
  1. 使用 (tablockx) 执行更新。如果您使用的是临时 table,您知道 table 没有争用,但这仍然是一个养成的好习惯(如果使用这种方法甚至可以被认为是一个养成的好习惯完全没有)。
  2. 设置@Order = _order。这看起来像是一个毫无意义的声明,而且确实如此。但是,由于 _order 是 table 的主键,因此将其分配给变量是强制 SQL 执行聚集索引更新的原因,这对于此工作至关重要
  3. 填充一个整数来表示您想要的顺序组。这就是魔法发生的地方,你必须从滚动 table 的角度来考虑它。当 _order 为 1(第一行)时,只需将 @Grp 变量设置为 1。如果在任何给定行上,_type 的列值与 [= 的变量值不同18=],我们增加分组变量。如果值相同,我们就坚持使用上一行的 @Grp
  4. _type 列的值更新 @Type 变量。请注意,这必须在分配 @Grp 之后才能获得正确的值。
  5. 最后,设置_grp = @Grp。这是使用步骤 3 的结果更新实际列值的地方。
  6. 这一切都必须用option (maxdop 1)来完成。这意味着最大并行度设置为 1。换句话说,SQL 无法执行任何可能导致排序被关闭的任务并行化。

现在只需按 _grp 字段分组即可。对于 _type.

的每个连续批次,您将拥有一个唯一的 _grp

结论

如果这看起来很疯狂和老套,那确实是。与所有事情一样,您需要对此持保留态度,如果您打算实施它,我建议您真正尝试一下这个概念以完全理解它,因为我保证没有其他人会知道如何解决它如果你半夜接到电话说它坏了。

此解决方案使用递归 CTE 并依赖于无间隙 order 值。如果你没有这个,你可以用 ROW_NUMBER() 即时创建它:

DECLARE @mockup TABLE([order] INT,[type] INT);
INSERT INTO @mockup VALUES
 (1,7)
,(2,11)
,(3,11)
,(4,18)
,(5,5)
,(6,19)
,(7,5)
,(8,5)
,(9,3)
,(10,11)
,(11,11)
,(12,3);

WITH recCTE AS
(
    SELECT m.[order]
          ,m.[type] 
          ,1 AS IncCounter
          ,1 AS [Rank]
    FROM @mockup AS m
    WHERE m.[order]=1

    UNION ALL

    SELECT m.[order]
          ,m.[type]
          ,CASE WHEN m.[type]=r.[type] THEN r.IncCounter+1 ELSE 1 END
          ,CASE WHEN m.[type]<>r.[type] THEN r.[Rank]+1 ELSE r.[Rank] END
    FROM @mockup AS m
    INNER JOIN recCTE AS r ON m.[order]=r.[order]+1
)
SELECT recCTE.[type]
      ,MAX(recCTE.[IncCounter])
      ,recCTE.[Rank]
FROM recCTE
GROUP BY recCTE.[type], recCTE.[Rank];

递归是沿线向下遍历,如果类型不变则增加计数器,如果类型不同则增加等级。

剩下的就简单了GROUP BY

我想我会 post 我想出的另一种方法,我认为更多的是与其他人正在考虑的 dense_rank() 工作相一致。唯一的假设是 _order 是一个连续的整数(即没有间隙)。

与之前相同的数据设置:

if object_id('tempdb.dbo.#t') is not null drop table #t
create table #t
(
    _order int primary key clustered,
    _type int,
    _grp int
)

insert into #t (_order, _type)
select 1,7
union all select 2,11
union all select 3,11
union all select 4,18
union all select 5,5
union all select 6,19
union all select 7,5
union all select 8,5
union all select 9,3
union all select 10,11
union all select 11,11
union all select 12,3

这种方法所做的是 row_number 每个 _type 因此无论 _type 存在于何处以及出现多少次,类型都将具有唯一的 row_number按照 _order 字段的顺序。通过从全局行号(即 _order)中减去特定于类型的行号,您将得到组。这是这个的代码,然后我也会介绍一下。

;with tr as
(
    select 
        -- Create an incrementing integer row_number over each _type (regardless of it's position in the sequence)
        _type_rid = row_number() over (partition by _type order by _order),
        -- This shows that on rows 6-8 (the transition between type 19 and 5), naively they're all assigned the same group
        naive_type_rid = _order - row_number() over (partition by _type order by _order),
        -- By adding a value to the type_rid which is a function of _type, those two values are distinct. 
        -- Originally I just added the value, but I think squaring it ensures that there can't ever be another gap of 1
        true_type_rid = (_order - row_number() over (partition by _type order by _order)) + power(_type, 2),
        _type, 
        _order
    from #t
    -- order by _order -- uncomment this if you want to run the inner select separately
)
select 
    _grp = dense_rank() over (order by max(_order)),
    _type = max(_type)
from tr
group by true_type_rid
order by max(_order)

发生了什么事

要事第一;我不必在 src cte 到 return _type_rid 中创建单独的列。我这样做主要是为了故障排除和清晰度。其次,我也真的不需要对列 _grp 的最终 selection 做第二个 dense_rank。我只是这样做了,所以它与我的其他方法的结果完全匹配。

在每种类型中,type_rid 是唯一的,并且递增 1。_order 也递增 1。因此,只要给定类型继续运行,仅相差 1,_order - _type_rid 将是相同的值。让我们看几个例子(这是 src cte 的结果,按 _order 排序):

_type_rid            naive_type_rid       true_type_rid        _type       _order
-------------------- -------------------- -------------------- ----------- -----------
1                    8                    17                   3           9
2                    10                   19                   3           12
1                    4                    29                   5           5
2                    5                    30                   5           7
3                    5                    30                   5           8
1                    0                    49                   7           1
1                    1                    122                  11          2
2                    1                    122                  11          3
3                    7                    128                  11          10
4                    7                    128                  11          11
1                    3                    327                  18          4
1                    5                    366                  19          6

第一行,_order - _type_rid = 1 - 1 = 0。这会将此行(类型 7)分配给组 0 第二行,2 - 1 = 1。这会将类型 11 分配给组 1 第三行,3 - 2 = 1。这也将第二个顺序类型 11 分配给组 1 第四行,4 - 1 = 3。这会将类型 18 分配给组 3 ...等等。

这些组没有顺序,但它们与 _order 的顺序相同,这是重要的部分。您还会注意到我也将 _type 的值添加到该值。那是因为当我们击中后面的一些行时,组切换了,但序列仍然增加了 1。通过添加 _type,我们可以区分那些差一的值,并且仍然按照正确的顺序进行嗯。

来自 src 的最终外部 select 按 max(_order) 排序(在我不必要的 dense_rank() _grp 修改中,只是一般结果顺序)。

结论

这仍然有点不稳定,但绝对在 "supported functionality" 的范围内。考虑到我 运行 遇到了其中的一个陷阱(差一件事),可能还有其他我没有考虑过的问题,所以再一次,对它持保留态度,并进行一些测试。