Como selecionar o conjunto de últimos valores não NULL por coluna em um grupo?

9

Estou usando o SQL Server 2016 e os dados que estou consumindo têm o seguinte formato.

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;

insira a descrição da imagem aqui

Gostaria de obter os últimos valores não nulos em colunas val1e val2agrupados por cate ordenados por t. O resultado que estou procurando é

cat  val1 val2
A    1    P
B    10   C

O mais próximo que cheguei é o uso LAST_VALUE, ignorando o ORDER BYque não funcionará, pois preciso do último valor não 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

A tabela real tem mais colunas para cat( colunas de data e sequência) e mais colunas val (colunas de data, sequência e número) para selecionar o último valor não nulo.

Alguma idéia de como fazer essa seleção.

Edmund
fonte
11
@ Vérace Agrupado por catordenado por t.
Edmund
11
@ ypercubeᵀᴹ Não, não há valor Q4 ausente, os tvalores se repetem. Não são dados bem comportados.
Edmund
4
Tudo bem, mas nesse caso, você deve fornecer um pedido que determine um pedido perfeito. PARTITION BY cat ORDER BY t, idpor exemplo. Caso contrário, a mesma consulta (qualquer consulta) poderá fornecer resultados diferentes em execuções separadas. Se as colunas da tabela são apenas as que você mostra, não vejo como podemos ter uma ordem determinada!
ypercubeᵀᴹ
11
@ ypercubeᵀᴹ É aí que está o desafio. Não há coluna de identificação nos dados. Existem várias colunas de agrupamento, uma coluna de seqüência de caracteres que pode ser usada para a ordem de grupo e, em seguida, as várias colunas de valor com nulos intercalados.
Edmund
11
Se você não pode determinar de maneira determinística o SQL Server que ordem as linhas devem ter, como é que qualquer consumidor desses dados saberá a diferença?
Aaron Bertrand

Respostas:

10

O uso da técnica de concatenação de The Last non NULL Puzzle de Itzik Ben Gan ficaria assim com seus tipos de dados de tabela e coluna de amostra.

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;

insira a descrição da imagem aqui

Outra maneira de escrever essa consulta que divide as etapas em CTEs para talvez mostrar melhor o que está acontecendo. Ele fornece exatamente o mesmo plano de execução que a consulta acima.

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 solução usa o fato de que concatenar um valor nulo com algo resulta em um valor nulo. SET CONCAT_NULL_YIELDS_NULL (Transact-SQL)

Mikael Eriksson
fonte
Mikael muito bem destilado. Essa solução me salvou várias vezes, embora eu tenha achado o final do artigo de Itzik confuso no começo. Em que ele rotulou de "Passo 2", quando na realidade era mais como implementar a lógica por trás da etapa 1.
pimbrouwers
2

Basta adicionar uma verificação de NULL na partição fará

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
fonte
0

Isso deve servir. row_number () e uma junção

Se você não tem uma boa classificação, espera que apenas um dos Q3 não seja 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
fonte