¿Cómo seleccionar el conjunto de los últimos valores no NULL por columna sobre un grupo?

9

Estoy usando SQL Server 2016 y los datos que estoy consumiendo tienen el siguiente formulario.

CREATE TABLE #tab (cat CHAR(1), t CHAR(2), val1 INT, val2 CHAR(1));

INSERT INTO #tab VALUES 
    ('A','Q1',2,NULL),('A','Q2',NULL,'P'),('A','Q3',1,NULL),('A','Q3',NULL,NULL),
    ('B','Q1',5,NULL),('B','Q2',NULL,'P'),('B','Q3',NULL,'C'),('B','Q3',10,NULL);

SELECT *
FROM    #tab;

ingrese la descripción de la imagen aquí

Me gustaría obtener los últimos valores no nulos sobre las columnas val1y val2agruparlos caty ordenarlos por t. El resultado que estoy buscando es

cat  val1 val2
A    1    P
B    10   C

Lo más cerca que he venido es usar LAST_VALUEmientras ignoro lo ORDER BYque no va a funcionar ya que necesito el último valor no nulo ordenado.

SELECT DISTINCT 
        cat, 
        LAST_VALUE(val1) OVER(PARTITION BY cat ORDER BY (SELECT NULL) ) AS val1,
        LAST_VALUE(val2) OVER(PARTITION BY cat ORDER BY (SELECT NULL) ) AS val2
FROM    #tab
cat  val1 val2
A    NULL NULL
B    10   NULL

La tabla real tiene más columnas para cat( columnas de fecha y cadena) y más columnas val (columnas de fecha, cadena y número) para seleccionar el último valor no nulo.

Alguna idea de cómo hacer esta selección.

Edmund
fuente
1
@ Vérace Agrupado por catordenado por t.
Edmund
1
@ ypercubeᵀᴹ No, no falta ningún valor Q4, los tvalores se repiten. No se trata de datos bien comportados.
Edmund
44
Muy bien, pero en ese caso, debe proporcionar un pedido que determine un pedido perfecto. PARTITION BY cat ORDER BY t, idpor ejemplo. De lo contrario, la misma consulta (cualquier consulta) puede brindarle resultados diferentes en ejecuciones separadas. Si las columnas en la tabla son solo las que muestra, ¡no veo cómo podemos tener un orden determinado!
ypercubeᵀᴹ
1
@ ypercubeᵀᴹ Ahí está el desafío. No hay columna de identificación en los datos. Hay varias columnas de agrupación, una columna de cadena que se puede usar para ordenar dentro del grupo, y luego las columnas de valores múltiples con valores nulos intercalados.
Edmund
1
Si no puede decirle a SQL Server de manera determinista en qué orden deben estar las filas, ¿cómo va a saber cualquier consumidor de estos datos la diferencia?
Aaron Bertrand

Respuestas:

10

El uso de la técnica de concatenación de The Last non NULL Puzzle de Itzik Ben Gan se vería así con su tabla de muestra y tipos de datos de columna.

select T.cat,
       cast(substring(
                     max(cast(T.t as binary(2)) + cast(T.val1 as binary(4))),
                     3,
                     4
                     ) as int),
       cast(substring(
                     max(cast(T.t as binary(2)) + cast(T.val2 as binary(1))),
                     3,
                     1
                     ) as char(1))
from #tab as T
group by T.cat;

ingrese la descripción de la imagen aquí

Otra forma de escribir esta consulta que divide los pasos en CTE para quizás mostrar mejor lo que está sucediendo. Da exactamente el mismo plan de ejecución que la consulta anterior.

with C1 as
(
  -- Concatenate the ordering column with the value column
  select T.cat,
        cast(T.t as binary(2)) + cast(T.val1 as binary(4)) as val1,
        cast(T.t as binary(2)) + cast(T.val2 as binary(1)) as val2
  from #tab as T
),
C2 as
(
  -- Get the max concatenated value per group
  select C1.cat,
         max(C1.val1) as val1,
         max(C1.val2) as val2
  from C1
  group by C1.cat
)
-- Extract the value from the concatenated column
select C2.cat,
       cast(substring(C2.val1, 3, 4) as int) as val1,
       cast(substring(C2.val2, 3, 1) as char(1)) as val2
from C2;

Esta solución utiliza el hecho de que concatenar un valor nulo con algo da como resultado un valor nulo. SET CONCAT_NULL_YIELDS_NULL (Transact-SQL)

Mikael Eriksson
fuente
Muy bien destilado Mikael. Esta solución me ha salvado varias veces, aunque el final del artículo de Itzik me pareció confuso al principio. En que la calificó como "paso 2", cuando en realidad se parecía más a la aplicación de la lógica detrás del paso 1.
pimbrouwers
2

Simplemente agregue un cheque para NULL en la partición hará

SELECT DISTINCT 
        cat, 
        FIRST_VALUE(val1) OVER(PARTITION BY cat ORDER BY CASE WHEN val1 is NULL then 0 else 1 END DESC, t desc) AS val1,
        FIRST_VALUE(val2) OVER(PARTITION BY cat ORDER BY CASE WHEN val2 is NULL then 0 else 1 END DESC, t desc) AS val2
FROM    #tab
Kelvin
fuente
0

Esto debería hacerlo. row_number () y una unión

Si no tiene un buen tipo, debe esperar que solo uno de los Q3 no sea nulo.

declare @t TABLE (cat CHAR(1), t CHAR(2), val1 INT, val2 CHAR(1));
INSERT INTO @t VALUES 
    ('A','Q1',2,NULL),('A','Q2',NULL,'P'),('A','Q3',1,NULL),('A','Q3',NULL,NULL),
    ('B','Q1',5,NULL),('B','Q2',NULL,'P'),('B','Q3',NULL,'C'),('B','Q3',10,NULL);

--SELECT *
--     , row_number() over (partition by cat order by t) as rn
--FROM   @t
--where val1 is not null or val2 is not null;

select t1.cat, t1.val1, t2.val2 
from  ( SELECT t.cat, t.val1
             , row_number() over (partition by cat order by t desc) as rn
        FROM   @t t
        where val1 is not null 
       ) t1
join   ( SELECT t.cat, t.val2
             , row_number() over (partition by cat order by t desc) as rn
        FROM   @t t
        where val2 is not null 
       ) t2
   on t1.cat = t2.cat
  and t1.rn = 1
  and t2.rn = 1
paparazzo
fuente