基于指定值的分区

Ljt*_*Ljt 1 sql-server sql-server-2008

我正在尝试根据值 90 编写 q 查询哪个分区。下面是我的表

create table  #temp(StudentID char(2),    Status int) 
insert #temp  values('S1',75 ) 
insert #temp  values('S1',85 )
insert #temp  values('S1',90)
insert #temp  values('S1',85)
insert #temp  values('S1',83)
insert #temp  values('S1',90 ) 
insert #temp  values('S1',85)
insert #temp  values('S1',90)
insert #temp  values('S1',93 ) 
insert #temp  values('S1',93 ) 
insert #temp  values('S1',93 ) 
Run Code Online (Sandbox Code Playgroud)

要求输出:

ID  Status  Result
S1  75      0
S1  85      0
S1  90      0
S1  85      1
S1  83      1
S1  90      1
S1  85      2
S1  90      2
S1  93      3
S1  93      3   
S1  93      3
Run Code Online (Sandbox Code Playgroud)

请任何人解决基于分区的状态 id 90,结果应该是 1,2,3 ..etc 根据时间值 90 的数量递增

Pan*_*vos 5

假设实际问题是“如何找到递增值的范围/孤岛”,则答案可以使用LAG将当前Status值与基于某个顺序的前一个值进行比较。如果先前的值为 90,则您有一个新岛:

declare @temp table (ID int identity PRIMARY KEY, StudentID char(2),    Status int) 

insert into @temp (StudentID,Status)
values
('S1',75), 
('S1',85),
('S1',90),
('S1',85),
('S1',83),
('S1',90), 
('S1',85),
('S1',90),
('S1',93), 
('S1',93), 
('S1',93);

select 
    * ,
    case LAG(Status,1,0) OVER (PARTITION BY StudentID ORDER BY ID) 
        when 90 then 1 else 0 end as NewIsland
from @temp
Run Code Online (Sandbox Code Playgroud)

这返回:

+----+-----------+--------+-----------+
| ID | StudentID | Status | NewIsland |
+----+-----------+--------+-----------+
|  1 | S1        |     75 |         0 |
|  2 | S1        |     85 |         0 |
|  3 | S1        |     90 |         0 |
|  4 | S1        |     85 |         1 |
|  5 | S1        |     83 |         0 |
|  6 | S1        |     90 |         0 |
|  7 | S1        |     85 |         1 |
|  8 | S1        |     90 |         0 |
|  9 | S1        |     93 |         1 |
| 10 | S1        |     93 |         0 |
| 11 | S1        |     93 |         0 |
+----+-----------+--------+-----------+
Run Code Online (Sandbox Code Playgroud)

您可以通过将当前值之前的所有 NewIsland 值相加,使用 SUM 和OVER的 ROWS 子句,从中创建一个 Island ID :

with islands as 
(
    select 
        * ,
        case LAG(Status,1,0) OVER (PARTITION BY StudentID ORDER BY ID) 
            when 90 then 1 else 0 end as NewIsland
    from @temp
)
select * , 
       SUM(NewIsland) OVER (PARTITION BY StudentID ORDER BY ID ROWS UNBOUNDED PRECEDING) 
from islands
Run Code Online (Sandbox Code Playgroud)

这产生:

+----+-----------+--------+-----------+--------+
| ID | StudentID | Status | NewIsland | Result |
+----+-----------+--------+-----------+--------+
|  1 | S1        |     75 |         0 |      0 |
|  2 | S1        |     85 |         0 |      0 |
|  3 | S1        |     90 |         0 |      0 |
|  4 | S1        |     85 |         1 |      1 |
|  5 | S1        |     83 |         0 |      1 |
|  6 | S1        |     90 |         0 |      1 |
|  7 | S1        |     85 |         1 |      2 |
|  8 | S1        |     90 |         0 |      2 |
|  9 | S1        |     93 |         1 |      3 |
| 10 | S1        |     93 |         0 |      3 |
| 11 | S1        |     93 |         0 |      3 |
+----+-----------+--------+-----------+--------+
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这是 SQL 中更广泛的间隙和岛屿问题的一个案例。

更新

LAG 和 OVER 可用于所有受支持的 SQL Server 版本,即 SQL Server 2012 及更高版本。OVER 在 SQL Server 2008 中也可用,但在 LAG 中不可用。在这些版本中,使用了不同的、较慢的技术来计算岛屿:序列中的间隙和岛屿的 SQL

在大多数情况下,ROW_NUMBER() 用于计算行排序,这会导致一个额外的 CTE。如果所需的排序与 ID 或任何其他唯一递增列相同,则可以避免这种情况。以下查询返回与使用的查询相同的结果LAG:

select 
    * ,
    case when exists (select ID 
                      from @temp t1 
                      where t1.StudentID=t2.StudentID 
                            and t1.ID=t2.ID-1 
                            and t2.status=90) then 1
         else 0 end
        as NewIsland
from @temp t2
Run Code Online (Sandbox Code Playgroud)

如果有任何具有相同 StudentID、Status 90 和 ID 或 ROW_NUMBER 的行,则此查询返回 1,即与 LAG(,1) 相同。

之后,我们只需要对以前的值求和。虽然 SUM OVER 于 2008 年可用,但它仅支持 PARTITION BY。我们需要使用另一个子查询:

;with islands as 
(
    select 
        * ,
    case when exists (select ID from @temp t1 where t1.StudentID=t2.StudentID and t1.ID=t2.ID-1 and t2.status=90) then 1
         else 0 end
        as NewIsland
    from @temp t2
)
select * , 
       (select ISNULL(SUM(NewIsland),0) 
        from islands i1 
        where i1.ID<i2.ID) AS Result
from islands i2
Run Code Online (Sandbox Code Playgroud)

这总结了所有 NewIslandID 小于当前 ID 的行的值。

表现

所有这些子查询都会导致大量重复扫描。令人惊讶的是,较旧的查询速度更快比使用 LAG 的查询,因为第一个查询必须多次排序临时结果并按状态过滤,执行计划成本分别为 45% 和 55%。

添加索引后,情况会发生巨大变化:

declare @temp table ( ID int identity PRIMARY KEY, StudentID char(2),    Status int, 
                      INDEX IX_TMP(StudentID,ID,Status)) 
Run Code Online (Sandbox Code Playgroud)

多重排序消失了,成本变成了 80% 和 20%。查询只扫描索引值一次,而不对中间结果进行排序。

子查询版本无法利用索引

更新 2

uzi 建议删除 LAG 并仅汇总到前一行会更好:

select * , 
       SUM(case when status =90 then 1 else 0 end) 
           OVER (PARTITION BY StudentID 
                 ORDER BY ID ROWS 
                 BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) 
from @temp;
Run Code Online (Sandbox Code Playgroud)

从语义上讲,这是同一件事 - 对于每一行,找到所有以前的行,为 90 年代计算 1,为其他行计算 0,然后将它们相加。

服务器在两种情况下都会生成类似的执行计划。LAG 版本使用了两个流聚合运算符,而没有它的版本使用了一个。不过,这个有限数据集的最终结果基本相同。

对于更大的数据集,结果可能会有所不同,例如,如果服务器必须将数据假脱机,tempdb因为它们不适合内存。