Come ottenere l'ultimo valore non nullo in una colonna ordinata di una tabella enorme?


13

Ho il seguente input:

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

Mi aspetto il seguente risultato:

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

La banale soluzione sarebbe unire le tabelle con una <relazione e quindi selezionare il MAXvalore in a 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;

Tuttavia, la banale esecuzione di questo codice creerebbe internamente il quadrato del conteggio delle righe della tabella di input ( O (n ^ 2) ). Mi aspettavo che t-sql lo ottimizzasse - a livello di blocco / record, l'attività da svolgere è molto semplice e lineare, essenzialmente un ciclo for ( O (n) ).

Tuttavia, nei miei esperimenti, l'ultimo MS SQL 2016 non è in grado di ottimizzare correttamente questa query, rendendo impossibile eseguire questa query per una tabella di input di grandi dimensioni.

Inoltre, la query deve essere eseguita rapidamente, rendendo impossibile una soluzione altrettanto semplice (ma molto diversa) basata sul cursore.

L'uso di una tabella temporanea supportata dalla memoria potrebbe essere un buon compromesso, ma non sono sicuro che possa essere eseguito in modo significativamente più rapido, considerato che la mia query di esempio che utilizza le subquery non ha funzionato.

Sto anche pensando di estrarre alcune funzioni di windowing dai documenti t-sql, cosa potrebbe essere indotto a fare ciò che voglio. Ad esempio, la somma cumulativa sta facendo qualcosa di molto simile, ma non ho potuto ingannarlo per fornire l'ultimo elemento non nullo e non la somma degli elementi precedenti.

La soluzione ideale sarebbe una query veloce senza codice procedurale o tabelle temporanee. In alternativa, anche una soluzione con tabelle temporanee va bene, ma non è iterare proceduralmente la tabella.

Risposte:


12

Una soluzione comune a questo tipo di problema è data da Itzik Ben-Gan nel suo articolo 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 <> violino


11

Mi aspettavo che t-sql lo ottimizzasse - a livello di blocco / record, l'attività da svolgere è molto semplice e lineare, essenzialmente un ciclo for (O (n)).

Questa non è la query che hai scritto. Potrebbe non essere equivalente alla query che hai scritto a seconda di alcuni dettagli altrimenti minori dello schema della tabella. Ti aspetti troppo da Query Optimizer.

Con l'indicizzazione corretta puoi ottenere l'algoritmo che cerchi attraverso il seguente 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

Per ogni riga, il Query Processor attraversa l'indice all'indietro e si ferma quando trova una riga con un valore non nullo per [VALUE]. Sulla mia macchina questo termina in circa 90 secondi per 100 milioni di righe nella tabella di origine. La query viene eseguita più a lungo del necessario perché una certa quantità di tempo viene sprecata sul client eliminando tutte quelle righe.

Non mi è chiaro se hai bisogno di risultati ordinati o cosa pensi di fare con un set di risultati così ampio. La query può essere adattata per soddisfare lo scenario reale. Il più grande vantaggio di questo approccio è che non richiede un ordinamento nel piano di query. Questo può aiutare per set di risultati più grandi. Uno svantaggio è che le prestazioni non saranno ottimali se ci sono molti NULL nella tabella perché molte righe verranno lette dall'indice e scartate. Dovresti essere in grado di migliorare le prestazioni con un indice filtrato che esclude i NULL per quel caso.

Dati di esempio per il test:

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

Un metodo, usando OVER()e MAX()e COUNT()basato su questa fonte potrebbe essere:

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;

Risultato

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

Un altro metodo basato su questa fonte , strettamente correlato al primo esempio

;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
Prendi in considerazione l'aggiunta di dettagli su come questi approcci si comportano con una "tabella enorme".
Joe Obbish
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.