Як отримати останнє ненулеве значення в упорядкованому стовпчику величезної таблиці?


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

Тривіальним рішенням буде з'єднання таблиць із <відношенням, а потім вибір 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;

Однак тривіальне виконання цього коду створило би внутрішньо квадрат лічильників рядків вхідної таблиці ( O (n ^ 2) ). Я очікував, що t-sql її оптимізує - на рівні блоку / запису завдання зробити дуже просто та лінійно, по суті для циклу ( O (n) ).

Однак, на моїх експериментах, останній MS SQL 2016 не може правильно оптимізувати цей запит, що робить цей запит неможливим для виконання великої таблиці введення.

Крім того, запит повинен запускатися швидко, роблячи аналогічно легким (але дуже різним) рішення на основі курсору нездійсненним.

Використання деякої підтримуваної пам’яттю тимчасової таблиці може бути хорошим компромісом, але я не впевнений, чи можна її запустити значно швидше, вважаючи, що мій приклад запиту за допомогою підзапитів не спрацював.

Я також замислююся над тим, щоб викопати якусь функцію вікон у документах t-sql, що можна підманути робити те, що я хочу. Наприклад, сукупна сума робить дуже схожу, але я не міг її обдурити, щоб дати останній ненульовий елемент, а не суму елементів раніше.

Ідеальним рішенням буде швидкий запит без процедурного коду або тимчасових таблиць. Як варіант, також рішення з тимчасовими таблицями нормально, але процедуру повторення таблиці - це не так.

Відповіді:


12

Поширене рішення цього типу проблеми дає Іцік Бен-Ган у своїй статті "Остання нерегулярна головоломка" :

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 її оптимізує - на рівні блоку / запису завдання зробити дуже просто та лінійно, по суті для циклу (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]. На моїй машині це закінчується приблизно за 90 секунд за 100 мільйонів рядків у вихідній таблиці. Запит триває довше, ніж це потрібно, тому що деяка кількість часу витрачається на клієнта, видаляючи всі ці рядки.

Мені не зрозуміло, чи потрібні вам замовлені результати чи що ви плануєте робити з таким великим набором результатів. Запит можна скорегувати відповідно до фактичного сценарію. Найбільша перевага такого підходу полягає в тому, що він не потребує сортування в плані запитів. Це може допомогти для більшого набору результатів. Один недолік полягає в тому, що продуктивність не буде оптимальною, якщо в таблиці багато 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
Розглянемо, як додати деталі про те, як ці підходи виконуються за допомогою "величезної таблиці".
Джо Оббіш
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.