Bagaimana cara mendapatkan nilai bukan-nol terakhir di kolom tabel besar yang dipesan?

13

Saya memiliki input berikut:

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

Saya mengharapkan hasil berikut:

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

Solusi sepele akan bergabung dengan tabel dengan <relasi, dan kemudian memilih MAXnilai dalam 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;

Namun, eksekusi sepele dari kode ini akan membuat secara internal kuadrat dari jumlah baris dari tabel input ( O (n ^ 2) ). Saya berharap t-sql untuk mengoptimalkannya - pada level blok / catatan, tugas yang harus dilakukan sangat mudah dan linier, pada dasarnya a for loop ( O (n) ).

Namun, pada percobaan saya, MS SQL 2016 terbaru tidak dapat mengoptimalkan kueri ini dengan benar, membuat kueri ini tidak mungkin dijalankan untuk tabel input besar.

Selain itu, kueri harus berjalan cepat, membuat solusi berbasis kursor yang mudah (namun sangat berbeda) tidak layak digunakan.

Menggunakan beberapa tabel sementara yang didukung memori bisa menjadi kompromi yang baik, tetapi saya tidak yakin apakah itu dapat berjalan secara signifikan lebih cepat, menganggap bahwa kueri contoh saya menggunakan subquery tidak berfungsi.

Saya juga berpikir untuk menggali beberapa fungsi windowing dari t-sql docs, apa yang bisa diakali untuk melakukan apa yang saya inginkan. Sebagai contoh, jumlah kumulatif melakukan beberapa hal yang sangat mirip, tetapi saya tidak bisa menipu untuk memberikan elemen non-null terbaru, dan bukan jumlah elemen sebelumnya.

Solusi ideal adalah kueri cepat tanpa kode prosedural atau tabel sementara. Atau, solusi dengan tabel sementara tidak apa-apa, tetapi iterasi tabel secara prosedural tidak.

peterh - Pasang kembali Monica
sumber

Jawaban:

12

Solusi umum untuk jenis masalah ini diberikan oleh Itzik Ben-Gan dalam artikelnya 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;

Demo: db <> biola

Paul White 9
sumber
11

Saya berharap t-sql untuk mengoptimalkannya - pada level blok / catatan, tugas yang harus dilakukan sangat mudah dan linier, pada dasarnya a for loop (O (n)).

Itu bukan permintaan yang Anda tulis. Ini mungkin tidak setara dengan kueri yang Anda tulis tergantung pada beberapa detail kecil skema tabel. Anda terlalu berharap dari pengoptimal kueri.

Dengan pengindeksan yang tepat Anda bisa mendapatkan algoritma yang Anda cari melalui T-SQL berikut:

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

Untuk setiap baris, prosesor kueri menelusuri indeks mundur dan berhenti ketika menemukan baris dengan nilai bukan nol untuk [VALUE]. Di komputer saya ini selesai dalam sekitar 90 detik untuk 100 juta baris dalam tabel sumber. Kueri berjalan lebih lama dari yang diperlukan karena sejumlah waktu terbuang pada klien membuang semua baris itu.

Tidak jelas bagi saya jika Anda perlu hasil yang dipesan atau apa yang Anda rencanakan dengan hasil set besar. Permintaan dapat disesuaikan untuk memenuhi skenario aktual. Keuntungan terbesar dari pendekatan ini adalah tidak memerlukan pengurutan dalam rencana kueri. Itu dapat membantu untuk set hasil yang lebih besar. Salah satu kelemahannya adalah bahwa kinerja tidak akan optimal jika ada banyak NULL dalam tabel karena banyak baris akan dibaca dari indeks dan dibuang. Anda harus dapat meningkatkan kinerja dengan indeks yang difilter yang tidak termasuk NULL untuk kasus itu.

Sampel data untuk pengujian:

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);
Joe Obbish
sumber
7

Salah satu metode, dengan menggunakan OVER()dan MAX()dan COUNT()berdasarkan sumber ini bisa menjadi:

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;

Hasil

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

Metode lain berdasarkan sumber ini , terkait erat dengan contoh pertama

;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;
Randi Vertongen
sumber
3
Pertimbangkan untuk menambahkan detail tentang bagaimana pendekatan ini bekerja dengan "tabel besar".
Joe Obbish