¿Cómo obtener el último valor no nulo en una columna ordenada de una tabla enorme?


13

Tengo la siguiente entrada:

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

Espero el siguiente resultado:

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

La solución trivial sería unir las tablas con una <relación y luego seleccionar el MAXvalor en 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;

Sin embargo, la ejecución trivial de este código crearía internamente el cuadrado del recuento de las filas de la tabla de entrada ( O (n ^ 2) ). Esperaba que t-sql lo optimizara: en un nivel de bloque / registro, la tarea a realizar es muy fácil y lineal, esencialmente un bucle for ( O (n) ).

Sin embargo, en mis experimentos, el último MS SQL 2016 no puede optimizar esta consulta correctamente, lo que hace que esta consulta sea imposible de ejecutar para una tabla de entrada grande.

Además, la consulta debe ejecutarse rápidamente, lo que hace que una solución similarmente fácil (pero muy diferente) basada en el cursor no sea factible.

Usar una tabla temporal respaldada por memoria podría ser un buen compromiso, pero no estoy seguro de si se puede ejecutar significativamente más rápido, considerando que mi consulta de ejemplo usando subconsultas no funcionó.

También estoy pensando en desenterrar alguna función de ventanas de los documentos de t-sql, lo que podría ser engañado para hacer lo que quiero. Por ejemplo, la suma acumulativa está haciendo algo muy similar, pero no pude engañarlo para obtener el último elemento no nulo, y no la suma de los elementos anteriores.

La solución ideal sería una consulta rápida sin código de procedimiento o tablas temporales. Alternativamente, también una solución con tablas temporales está bien, pero iterar la tabla de forma procesal no lo está.

Respuestas:


12

Itzik Ben-Gan ofrece una solución común a este tipo de problema en su artículo 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;

Demostración: db <> fiddle


11

Esperaba que t-sql lo optimizara: en un nivel de bloque / registro, la tarea a realizar es muy fácil y lineal, esencialmente un bucle for (O (n)).

Esa no es la consulta que escribiste. Es posible que no sea equivalente a la consulta que escribió según algunos detalles menores del esquema de la tabla. Esperas demasiado del optimizador de consultas.

Con la indexación correcta puede obtener el algoritmo que busca a través del siguiente 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

Para cada fila, el procesador de consultas atraviesa el índice hacia atrás y se detiene cuando encuentra una fila con un valor no nulo para [VALUE]. En mi máquina, esto termina en aproximadamente 90 segundos para 100 millones de filas en la tabla de origen. La consulta se ejecuta más de lo necesario porque se pierde una cierta cantidad de tiempo en el cliente descartando todas esas filas.

No me queda claro si necesita resultados ordenados o qué planea hacer con un conjunto de resultados tan grande. La consulta se puede ajustar para cumplir con el escenario real. La mayor ventaja de este enfoque es que no requiere una clasificación en el plan de consulta. Eso puede ayudar para conjuntos de resultados más grandes. Una desventaja es que el rendimiento no será óptimo si hay muchos NULL en la tabla porque muchas filas se leerán del índice y se descartarán. Debería poder mejorar el rendimiento con un índice filtrado que excluya NULL para ese caso.

Datos de muestra para la prueba:

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 método, mediante el uso de OVER()y MAX()y COUNT()basado en esta fuente podría ser:

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;

Resultado

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

Otro método basado en esta fuente , estrechamente relacionado con el primer ejemplo.

;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
Considere agregar detalles sobre cómo funcionan estos enfoques con una "tabla enorme".
Joe Obbish
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.