LAST_VALUE、CURRENT ROW 和 NULL 的意外结果

Posted

技术标签:

【中文标题】LAST_VALUE、CURRENT ROW 和 NULL 的意外结果【英文标题】:Unexpected results with LAST_VALUE, CURRENT ROW, and NULLs 【发布时间】:2014-10-06 12:51:16 【问题描述】:

我有以下数据:

CREATE TABLE #Transfers (
    AddedOn DATETIME2 NOT NULL,
    EmpID INT NOT NULL,
    NewDeptID INT NULL
)

INSERT INTO #Transfers
VALUES 
    ('2013-12-17 17:18:54.3499987', 19, 36),
    ('2013-12-18 13:02:34.1168087', 19, NULL),
    ('2014-01-28 11:41:55.8755928', 22, 100),
    ('2014-02-05 10:36:36.3645703', 22, NULL),
    ('2014-02-16 00:00:00.0000000', 22, 37),
    ('2014-02-17 00:00:00.0000000', 22, NULL)

对于每一行,我都在尝试获取最新的非空 NewDeptID(直到该行):

SELECT *,
    LAST_VALUE(NewDeptID) OVER (
        PARTITION BY EmpID
        ORDER BY IIF(NewDeptID IS NULL,0,1), AddedOn
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS CurrentDeptID
FROM #Transfers
ORDER BY EmpID, AddedOn

如果我理解正确,应该排除空值,因为它们是窗口中的第一个值 -- IIF(NewDeptID IS NULL,0,1)

我希望得到以下结果:

AddedOn                        EmpID  NewDeptID  CurrentDeptID
2013-12-17 17:18:54.3499987    19     36         36
2013-12-18 13:02:34.1168087    19     NULL       36
2014-01-28 11:41:55.8755928    22     100        100
2014-02-05 10:36:36.3645703    22     NULL       100
2014-02-16 00:00:00.0000000    22     37         37
2014-02-17 00:00:00.0000000    22     NULL       37

相反,LAST_VALUE 中的 ORDER BY 子句被忽略,当当前行包含 NULL 时返回 NULL:

AddedOn                        EmpID  NewDeptID  CurrentDeptID
2013-12-17 17:18:54.3499987    19     36         36
2013-12-18 13:02:34.1168087    19     NULL       NULL --
2014-01-28 11:41:55.8755928    22     100        100
2014-02-05 10:36:36.3645703    22     NULL       NULL --
2014-02-16 00:00:00.0000000    22     37         37
2014-02-17 00:00:00.0000000    22     NULL       NULL --

我在 SQL Server 2012 和 2014 中得到了相同的结果。

这是 SQL Server 中的错误,还是我在窗口函数语法中遗漏了什么?


注意:如果我扩展窗口以包含整个分区,则忽略 NULL:

SELECT *,
    LAST_VALUE(NewDeptID) OVER (
        PARTITION BY EmpID
        ORDER BY IIF(NewDeptID IS NULL,0,1), AddedOn
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS CurrentDeptID
FROM #Transfers
ORDER BY EmpID, AddedOn

结果:

AddedOn                        EmpID  NewDeptID  CurrentDeptID
2013-12-17 17:18:54.3499987    19     36         36
2013-12-18 13:02:34.1168087    19     NULL       36
2014-01-28 11:41:55.8755928    22     100        37
2014-02-05 10:36:36.3645703    22     NULL       37
2014-02-16 00:00:00.0000000    22     37         37
2014-02-17 00:00:00.0000000    22     NULL       37

【问题讨论】:

【参考方案1】:

不,你还没有很好地理解窗口函数是如何工作的。 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWORDER BY IIF... 之后应用,因此首先对行进行排序(首先是具有NULL 的行,然后是其余所有行),然后应用ROWS ... 限制。所以,这永远不会解决你的问题。

详细地说,OVER 子句根据PARTITIONORDER BY 创建了这些“窗口”。 因此,例如对于带有AddedOn = '2014-02-17 00:00:00.0000000' 的行,ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 是上面的行及其本身(标有<------ 的两行):

AddedOn                        EmpID  IIF()  NewDeptID 

-- partition EmpID = 19
2013-12-18 13:02:34.1168087    19     0       NULL       
2013-12-17 17:18:54.3499987    19     1        36        

-- partition EmpID = 22
2014-02-05 10:36:36.3645703    22     0       NULL  <---
2014-02-17 00:00:00.0000000    22     0       NULL  <---  this is the LAST_VALUE
2014-01-28 11:41:55.8755928    22     1       100       
2014-02-16 00:00:00.0000000    22     1        37        

所以,CurrentDeptID 列获取这些值:

AddedOn                        EmpID  IIF()  NewDeptID CurrentDeptID

-- partition EmpID = 19 
2013-12-18 13:02:34.1168087    19     0       NULL     NULL
2013-12-17 17:18:54.3499987    19     1        36       36 

-- partition EmpID = 22
2014-02-05 10:36:36.3645703    22     0       NULL     NULL
2014-02-17 00:00:00.0000000    22     0       NULL     NULL
2014-01-28 11:41:55.8755928    22     1       100      100 
2014-02-16 00:00:00.0000000    22     1        37       37

然后根据外部ORDER BY重新排序以获得最终结果


要解决此问题,您可以使用相关子查询:

SELECT *,
    ( SELECT TOP (1) NewDeptID
      FROM #Transfers AS ti
      WHERE ti.EmpID = t.EmpID
        AND ti.NewDeptID IS NOT NULL
        AND ti.AddedOn <= t.AddedOn
      ORDER BY AddedOn DESC
    ) AS CurrentDeptID
FROM #Transfers AS t
ORDER BY EmpID, AddedOn ;

在SQL-Fiddle测试


如果 LAST_VALUE() 函数可以忽略空值,那么您正在尝试做的事情实际上是有意义的。这可以通过 IGNORE NULLS 完成,但不幸的是,此功能尚未在 SQL-Server 中实现。您可以在 Oracle (fiddle-2) 中查看它的工作原理:

SELECT AddedOn, EmpID, NewDeptID,
    LAST_VALUE(NewDeptID) 
      IGNORE NULLS                                -- check this
      OVER (
        PARTITION BY EmpID
        ORDER BY AddedOn
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS CurrentDeptID
FROM Transfers
ORDER BY EmpID, AddedOn ;

在 SQL-Server 中使用窗口函数的另一种方法是首先计算当前行之前的非空 NewDeptID。您可以通过 fiddle-3 进行测试:

WITH cte AS
  ( SELECT AddedOn, EmpID, NewDeptID,
      COUNT(NewDeptID) OVER (
          PARTITION BY EmpID
          ORDER BY AddedOn
          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS cnt
    FROM Transfers 
  )
SELECT AddedOn, EmpID, NewDeptID,
    MIN(NewDeptID) OVER (PARTITION BY EmpID, cnt) AS CurrentDeptID
FROM cte
ORDER BY EmpID, AddedOn ;

【讨论】:

我不明白。如果首先应用ORDER BY,则具有空值的记录首先在分区内。在计算分区内直到当前行的最后一个值时,结果不应为空,除非分区中所有先前记录的值也为空。 但是你说的是,“分区内有空值的记录在前”。如果一行为空,则它之前的所有行也为空。当我写ORDER BY 时,我指的是ORDER BY IIF...,而不是外部订单。 您的IIF() 不允许这样做(所有非空行都放在所有空行之后)。我编辑了我的答案,详细说明了窗口的工作原理。 所以LAST_VALUEROWS BETWEEN ... AND CURRENT ROW 总是会返回当前行的值? FIRST_VALUEROWS BETWEEN CURRENT ROW AND ... 一样吗? 是的,谢谢。已更正。 (是的,正是上一个问题)

以上是关于LAST_VALUE、CURRENT ROW 和 NULL 的意外结果的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server ->> FIRST_VALUE和LAST_VALUE函数

在最后一行之外使用 Lag Function

自定义elementUI表格中的highlight-current-row

Oracle分析函数-first_value()和last_value()

雪花窗函数 last_value 和 max

带有 ASC 的 LAST_VALUE() 和带有 DESC 的 FIRST_VALUE 返回不同的结果