如何获取巨大表的有序列中的最后一个非空值?


13

我有以下输入:

 id | value 
----+-------
  1 |   136
  2 |  NULL
  3 |   650
  4 |  NULL
  5 |  NULL
  6 |  NULL
  7 |   954
  8 |  NULL
  9 |   104
 10 |  NULL

我期望得到以下结果:

 id | value 
----+-------
  1 |   136
  2 |   136
  3 |   650
  4 |   650
  5 |   650
  6 |   650
  7 |   954
  8 |   954
  9 |   104
 10 |   104

简单的解决方案是将表与一个<关系连接起来,然后在中选择一个MAXGROUP BY

WITH tmp AS (
  SELECT t2.id, MAX(t1.id) AS lastKnownId
  FROM t t1, t t2
  WHERE
    t1.value IS NOT NULL
    AND
    t2.id >= t1.id
  GROUP BY t2.id
)
SELECT
  tmp.id, t.value
FROM t, tmp
WHERE t.id = tmp.lastKnownId;

但是,此代码的琐碎执行将在内部创建输入表的行数的平方(O(n ^ 2))。我期望t-sql可以优化它-在块/记录级别,要做的任务非常简单且线性,本质上是for循环(O(n))。

但是,在我的实验中,最新的MS SQL 2016无法正确优化此查询,因此无法对大型输入表执行此查询。

此外,查询必须快速运行,使得基于游标的类似的简单(但非常不同)解决方案不可行。

使用一些由内存支持的临时表可能是一个不错的折衷方案,但是我不确定它是否可以以更快的速度运行,因为考虑到我的使用子查询的示例查询无法正常工作。

我也在考虑从t-sql文档中挖掘出一些窗口函数,可以骗我做什么。例如,累加总和在做一些非常相似的事情,但是我不能欺骗它给出最新的非null元素,而不是以前的元素之和。

理想的解决方案是没有程序代码或临时表的快速查询。另外,使用临时表的解决方案也可以,但按程序迭代表则不行。

Answers:


12

Itzik Ben-Gan在他的文章The Last non NULL Puzzle中给出了解决此类问题的常见方法:

DROP TABLE IF EXISTS dbo.Example;

CREATE TABLE dbo.Example
(
    id integer PRIMARY KEY,
    val integer NULL
);

INSERT dbo.Example
    (id, val)
VALUES
    (1, 136),
    (2, NULL),
    (3, 650),
    (4, NULL),
    (5, NULL),
    (6, NULL),
    (7, 954),
    (8, NULL),
    (9, 104),
    (10, NULL);

SELECT
    E.id,
    E.val,
    lastval =
        CAST(
            SUBSTRING(
                MAX(CAST(E.id AS binary(4)) + CAST(E.val AS binary(4))) OVER (
                    ORDER BY E.id
                    ROWS UNBOUNDED PRECEDING),
            5, 4)
        AS integer)
FROM dbo.Example AS E
ORDER BY
    E.id;

演示:db <> fiddle


11

我期望t-sql可以优化它-在块/记录级别,要做的任务非常简单且线性,本质上是for循环(O(n))。

那不是您编写的查询。根据表模式的其他一些次要细节,它可能不等同于您编写的查询。您对查询优化器的期望过高。

使用正确的索引,您可以通过以下T-SQL获得所需的算法:

SELECT t1.id, ca.[VALUE] 
FROM dbo.[BIG_TABLE(FOR_U)] t1
CROSS APPLY (
    SELECT TOP (1) [VALUE]
    FROM dbo.[BIG_TABLE(FOR_U)] t2
    WHERE t2.ID <= t1.ID AND t2.[VALUE] IS NOT NULL
    ORDER BY t2.ID DESC
) ca; --ORDER BY t1.ID ASC

对于每一行,查询处理器都会向后遍历索引,并在找到具有非空值的行时停止[VALUE]。在我的机器上,源表中的1亿行大约需要90秒才能完成。该查询的运行时间超出了必要的时间,因为在客户端上浪费了所有这些行而浪费了一些时间。

对于我来说,尚不清楚您是否需要有序的结果,或者您打算对如此大的结果集进行什么计划。可以调整查询以满足实际情况。这种方法的最大优点是它不需要查询计划中的排序。这有助于更大的结果集。一个缺点是,如果表中有很多NULL,则性能将不是最佳的,因为将从索引中读取很多行并将其丢弃。您应该能够通过使用过滤索引来排除这种情况下的NULL,从而提高性能。

测试样本数据:

DROP TABLE IF EXISTS #t;

CREATE TABLE #t (
ID BIGINT NOT NULL
);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

DROP TABLE IF EXISTS dbo.[BIG_TABLE(FOR_U)];

CREATE TABLE dbo.[BIG_TABLE(FOR_U)] (
ID BIGINT NOT NULL,
[VALUE] BIGINT NULL
);

INSERT INTO dbo.[BIG_TABLE(FOR_U)] WITH (TABLOCK)
SELECT 10000 * t1.ID + t2.ID, CASE WHEN (t1.ID + t2.ID) % 3 = 1 THEN t2.ID ELSE NULL END
FROM #t t1
CROSS JOIN #t t2;

CREATE UNIQUE CLUSTERED INDEX ADD_ORDERING ON dbo.[BIG_TABLE(FOR_U)] (ID);

7

一种方法,通过使用OVER()MAX()COUNT()基于此源可以是:

SELECT ID, MAX(value) OVER (PARTITION BY Value2) as value
FROM
(
    SELECT ID, value
        ,COUNT(value) OVER (ORDER BY ID) AS Value2
    FROM dbo.HugeTable
) a
ORDER BY ID;

结果

Id  UpdatedValue
1   136
2   136
3   650
4   650
5   650
6   650
7   954
8   954
9   104
10  104

基于此来源的另一种方法与第一个示例密切相关

;WITH CTE As 
( 
SELECT  value,
        Id, 
        COUNT(value) 
        OVER(ORDER BY Id) As  Value2 
FROM dbo.HugeTable
),

CTE2 AS ( 
SELECT Id,
       value,
       First_Value(value)  
       OVER( PARTITION BY Value2
             ORDER BY Id) As UpdatedValue 
FROM CTE 
            ) 
SELECT Id,UpdatedValue 
FROM CTE2;

3
考虑添加有关这些方法如何与“巨大表”一起执行的详细信息。
Joe Obbish
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.