Comment sélectionner l'ensemble des dernières valeurs non NULL par colonne sur un groupe?

9

J'utilise SQL Server 2016 et les données que je consomme se présentent sous la forme suivante.

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;

entrez la description de l'image ici

Je voudrais obtenir les dernières valeurs non nulles sur les colonnes val1et val2regroupées par catet triées par t. Le résultat que je recherche est

cat  val1 val2
A    1    P
B    10   C

Le plus proche que je suis venu utilise LAST_VALUEen ignorant le ORDER BYqui ne fonctionnera pas car j'ai besoin de la dernière valeur non nulle ordonnée.

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 table réelle contient plus de colonnes pour cat( colonnes de date et chaîne) et plus de colonnes val (colonnes de date, chaîne et nombre) pour sélectionner la dernière valeur non nulle.

Toutes les idées pour faire cette sélection.

Edmund
la source
1
@ Vérace Groupé par catcommandé par t.
Edmund
1
@ ypercubeᵀᴹ Non, il n'y a pas de valeur Q4 manquante, les tvaleurs se répètent. Ce ne sont pas des données bien comportées.
Edmund
4
Très bien, mais dans ce cas, vous devez fournir une commande qui détermine une commande parfaite. PARTITION BY cat ORDER BY t, idpar exemple. Sinon, la même requête (n'importe quelle requête) peut vous donner des résultats différents sur des exécutions distinctes. Si les colonnes du tableau ne sont que celles que vous affichez, je ne vois pas comment nous pouvons avoir un ordre déterminé cependant!
ypercubeᵀᴹ
1
@ ypercubeᵀᴹ C'est là que réside le défi. Il n'y a pas de colonne id dans les données. Il y a plusieurs colonnes de regroupement, une colonne de chaîne qui peut être utilisée pour un classement au sein du groupe, puis les colonnes de valeurs multiples avec des valeurs nulles entrecoupées.
Edmund
1
Si vous ne pouvez pas déterminer de façon déterministe SQL Server dans quel ordre les lignes doivent être, comment tout consommateur de ces données va-t-il connaître la différence?
Aaron Bertrand

Réponses:

10

L'utilisation de la technique de concaténation de The Last non NULL Puzzle par Itzik Ben Gan ressemblerait à ceci avec vos exemples de types de données de table et de colonne.

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;

entrez la description de l'image ici

Une autre façon d'écrire cette requête qui divise les étapes en CTE pour peut-être mieux montrer ce qui se passe. Il donne exactement le même plan d'exécution que la requête ci-dessus.

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;

Cette solution utilise le fait que la concaténation d'une valeur nulle avec quelque chose entraîne une valeur nulle. SET CONCAT_NULL_YIELDS_NULL (Transact-SQL)

Mikael Eriksson
la source
Mikael très bien distillé. Cette solution m'a sauvé un certain nombre de fois, bien que j'ai trouvé la fin de l'article d'Itzik déroutante au début. En ce qu'il l'a étiqueté "étape 2" alors qu'en réalité, cela ressemblait plus à la mise en œuvre de la logique derrière l'étape 1.
pimbrouwers
2

Il suffit d'ajouter une vérification pour NULL dans la partition fera l'affaire

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
la source
0

Cela devrait le faire. row_number () et une jointure

Si vous n'avez pas un bon tri, vous devez espérer qu'un seul des Q3 n'est pas nul.

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