巨大なテーブルの順序付けられた列の最後の非ヌル値を取得する方法は?


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

簡単な解決策は、テーブルを<リレーションで結合してから、a のMAX値を選択することですGROUP 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;

ただし、このコードを簡単に実行すると、入力テーブルの行数の2乗(O(n ^ 2))が内部的に作成されます。私はt-sqlがそれを最適化することを期待していました-ブロック/レコードレベルでは、行うべきタスクは非常に簡単で線形であり、本質的にforループ(O(n))です。

ただし、私の実験では、最新のMS SQL 2016はこのクエリを正しく最適化できないため、このクエリを大きな入力テーブルに対して実行することはできません。

さらに、クエリを迅速に実行する必要があり、同様に簡単な(ただし非常に異なる)カーソルベースのソリューションを実行不可能にします。

いくつかのメモリバックアップされた一時テーブルを使用することは良い妥協策となる可能性がありますが、サブクエリを使用したサンプルクエリが機能しなかったため、大幅に高速に実行できるかどうかはわかりません。

また、t-sqlドキュメントからウィンドウ関数を掘り下げることも考えています。たとえば、累積合計は非常によく似ていますが、前の要素の合計ではなく、最新の非ヌル要素を提供するようにだますことができませんでした。

理想的な解決策は、手続き型コードや一時テーブルを使用しない迅速なクエリです。または、一時テーブルを使用したソリューションでも問題ありませんが、テーブルを手続き的に繰り返すことはできません。

回答:


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

行ごとに、クエリプロセッサはインデックスを逆方向に走査し、の値がnull以外の行を見つけると停止し[VALUE]ます。私のマシンでは、ソーステーブルの1億行に対して約90秒終了します。クライアントがこれらの行をすべて破棄するのに時間がかかるため、クエリは必要以上に長く実行されます。

順序付けられた結果が必要かどうか、またはこのような大規模な結果セットで何を行う予定なのかは明確ではありません。クエリは、実際のシナリオに合わせて調整できます。このアプローチの最大の利点は、クエリプランでの並べ替えを必要としないことです。これは、より大きな結果セットに役立ちます。1つの欠点は、テーブルに多くの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()基づく1つの方法は次のとおりです。

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
これらのアプローチが「巨大なテーブル」でどのように機能するかについての詳細を追加することを検討してください。
ジョーオブビッシュ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.