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.


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

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.