Jak wybrać zestaw ostatnich wartości innych niż NULL na kolumnę w grupie?

9

Korzystam z programu SQL Server 2016, a dane, które konsumuję, mają następującą postać.

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;

wprowadź opis zdjęcia tutaj

Chciałbym uzyskać ostatnie wartości inne niż null nad kolumnami val1i val2pogrupowane według cati uporządkowane według t. Wynik, którego szukam jest

cat  val1 val2
A    1    P
B    10   C

Najbliższe, które przyszedłem, używa LAST_VALUE, ignorując to, ORDER BYco nie zadziała, ponieważ potrzebuję uporządkowanej ostatniej wartości innej niż null.

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

Rzeczywista tabela zawiera więcej kolumn cat( kolumny daty i łańcucha) i więcej kolumn val (kolumny daty, łańcucha i liczb), aby wybrać ostatnią wartość inną niż null.

Wszelkie pomysły, jak dokonać tego wyboru.

Edmund
źródło
1
@ Vérace Pogrupowane catwedług t.
Edmund
1
@ ypercubeᵀᴹ Nie, nie brakuje brakującej wartości Q4, twartości się powtarzają. Dane nie są dobrze wychowane.
Edmund
4
W porządku, ale w takim przypadku musisz podać zamówienie, które określa idealne zamówienie. PARTITION BY cat ORDER BY t, idna przykład. W przeciwnym razie to samo zapytanie (dowolne zapytanie) może dać różne wyniki dla poszczególnych wykonań. Jeśli kolumny w tabeli są tylko tymi, które wyświetlasz, nie widzę jednak, jak możemy mieć określoną kolejność!
ypercubeᵀᴹ
1
@ ypercubeᵀᴹ Na tym polega wyzwanie. W danych nie ma kolumny identyfikatora. Istnieje wiele kolumn grupujących, kolumna łańcuchowa, której można użyć w ramach grupowania, a następnie kolumny z wieloma wartościami z nullami przeplatanymi.
Edmund,
1
Jeśli nie możesz powiedzieć SQL Serverowi deterministycznie, w jakiej kolejności powinny być rzędy, w jaki sposób każdy konsument tych danych pozna różnicę?
Aaron Bertrand

Odpowiedzi:

10

Przy użyciu techniki konkatenacji z The Last non NULL Puzzle autorstwa Itzika Bena Gana wyglądałoby to tak samo z przykładowymi typami tabel i kolumn.

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;

wprowadź opis zdjęcia tutaj

Inny sposób napisania tego zapytania, który dzieli kroki na CTE, aby być może lepiej pokazać, co się dzieje. Daje dokładnie taki sam plan wykonania jak powyższe zapytanie.

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;

To rozwiązanie wykorzystuje fakt, że konkatenacja wartości null z czymś skutkuje wartością null. ZESTAW CONCAT_NULL_YIELDS_NULL (Transact-SQL)

Mikael Eriksson
źródło
Bardzo dobrze destylowany Mikael. To rozwiązanie uratowało mnie wiele razy, choć początkowo uznałem zakończenie artykułu Itzika za mylące.
Oznaczał
2

Wystarczy dodać sprawdzenie, czy NULL na partycji zrobi

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
kelwin
źródło
0

To powinno to zrobić. row_number () i łączyć

Jeśli nie masz dobrego rodzaju, musisz mieć nadzieję, że tylko jeden z Q3 nie ma wartości zerowej.

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
źródło