Làm thế nào để có được giá trị không null cuối cùng trong một cột được sắp xếp của một bảng lớn?


13

Tôi có đầu vào sau:

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

Tôi mong đợi kết quả sau:

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

Giải pháp tầm thường sẽ là nối các bảng với một <mối quan hệ và sau đó chọn MAXgiá trị trong 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;

Tuy nhiên, việc thực thi tầm thường của mã này sẽ tạo ra bên trong bình phương số đếm của các hàng của bảng đầu vào ( O (n ^ 2) ). Tôi mong đợi t-sql sẽ tối ưu hóa nó - ở cấp độ khối / bản ghi, công việc phải làm là rất dễ dàng và tuyến tính, về cơ bản là một vòng lặp for ( O (n) ).

Tuy nhiên, trong các thử nghiệm của tôi, MS SQL 2016 mới nhất không thể tối ưu hóa chính xác truy vấn này, khiến cho truy vấn này không thể thực thi đối với bảng đầu vào lớn.

Hơn nữa, truy vấn phải chạy nhanh, làm cho một giải pháp dựa trên con trỏ dễ dàng tương tự (nhưng rất khác nhau) không khả thi.

Sử dụng một số bảng tạm thời được hỗ trợ bộ nhớ có thể là một sự thỏa hiệp tốt, nhưng tôi không chắc liệu nó có thể được chạy nhanh hơn đáng kể hay không, xem xét rằng truy vấn ví dụ của tôi sử dụng các truy vấn con không hoạt động.

Tôi cũng đang suy nghĩ để khai thác một số chức năng cửa sổ từ các tài liệu t-sql, những gì có thể bị lừa để làm những gì tôi muốn. Ví dụ: tổng tích lũy đang thực hiện một số tương tự, nhưng tôi không thể lừa nó để đưa ra phần tử không null mới nhất và không phải là tổng của các phần tử trước đó.

Giải pháp lý tưởng sẽ là một truy vấn nhanh mà không cần mã thủ tục hoặc bảng tạm thời. Ngoài ra, một giải pháp với các bảng tạm thời là được, nhưng lặp lại quy trình bảng thì không.

Câu trả lời:


12

Một giải pháp phổ biến cho loại vấn đề này được đưa ra bởi Itzik Ben-Gan trong bài viết của ông 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;

Bản trình diễn: db <> fiddle


11

Tôi mong đợi t-sql sẽ tối ưu hóa nó - ở cấp độ khối / bản ghi, công việc phải làm là rất dễ dàng và tuyến tính, về cơ bản là một vòng lặp for (O (n)).

Đó không phải là truy vấn mà bạn đã viết. Nó có thể không tương đương với truy vấn mà bạn đã viết tùy thuộc vào một số chi tiết nhỏ khác của lược đồ bảng. Bạn đang mong đợi quá nhiều từ trình tối ưu hóa truy vấn.

Với việc lập chỉ mục đúng, bạn có thể có được thuật toán mà bạn tìm kiếm thông qua T-SQL sau:

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

Đối với mỗi hàng, bộ xử lý truy vấn đi ngang qua chỉ mục và dừng lại khi tìm thấy một hàng có giá trị không null cho [VALUE]. Trên máy của tôi, việc này kết thúc sau khoảng 90 giây cho 100 triệu hàng trong bảng nguồn. Truy vấn chạy lâu hơn mức cần thiết vì một số lượng thời gian bị lãng phí trên máy khách loại bỏ tất cả các hàng đó.

Tôi không rõ nếu bạn cần kết quả theo yêu cầu hoặc bạn dự định làm gì với tập kết quả lớn như vậy. Các truy vấn có thể được điều chỉnh để đáp ứng kịch bản thực tế. Ưu điểm lớn nhất của phương pháp này là nó không yêu cầu sắp xếp trong kế hoạch truy vấn. Điều đó có thể giúp cho các tập kết quả lớn hơn. Một nhược điểm là hiệu suất sẽ không tối ưu nếu có nhiều NULL trong bảng vì nhiều hàng sẽ được đọc từ chỉ mục và bị loại bỏ. Bạn sẽ có thể cải thiện hiệu suất với chỉ mục được lọc loại trừ NULL cho trường hợp đó.

Dữ liệu mẫu cho bài kiểm tra:

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

Một phương pháp, bằng cách sử dụng OVER()MAX()COUNT()dựa trên nguồn này có thể là:

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;

Kết quả

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

Một phương pháp khác dựa trên nguồn này , liên quan chặt chẽ đến ví dụ đầu tiên

;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
Xem xét thêm chi tiết về cách các phương pháp này thực hiện với một "bảng lớn".
Joe Obbish
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.