Próba znalezienia ostatniej zmiany wartości


26

Mam tabelę, która ma identyfikator, wartość i datę. W tej tabeli znajduje się wiele identyfikatorów, wartości i dat.

Rekordy są okresowo wstawiane do tej tabeli. Identyfikator zawsze pozostanie taki sam, ale czasami wartość się zmieni.

Jak mogę napisać zapytanie, które da mi identyfikator oraz datę ostatniej zmiany wartości? Uwaga: wartość zawsze wzrośnie.

Z tych przykładowych danych:

  Create Table Taco
 (  Taco_ID int,
    Taco_value int,
    Taco_date datetime)

Insert INTO Taco 
Values (1, 1, '2012-07-01 00:00:01'),
        (1, 1, '2012-07-01 00:00:02'),
        (1, 1, '2012-07-01 00:00:03'),
        (1, 1, '2012-07-01 00:00:04'),
        (1, 2, '2012-07-01 00:00:05'),
        (1, 2, '2012-07-01 00:00:06'),
        (1, 2, '2012-07-01 00:00:07'),
        (1, 2, '2012-07-01 00:00:08')

Wynik powinien być:

Taco_ID      Taco_date
1            2012-07-01 00:00:05

(Ponieważ 00:05 był ostatnio Taco_Valuezmieniany).


2
Zakładam, tacoże nie ma to nic wspólnego z jedzeniem?
Kermit

5
Jestem głodny i chciałbym zjeść trochę tacos. Potrzebowałem tylko nazwy dla przykładowej tabeli.
SqlSandwiches,

8
Czy wybrałeś swoją nazwę użytkownika na podobnych zasadach?
Martin Smith,

1
Całkiem możliwe.
SqlSandwiches,

Odpowiedzi:


13

Te dwa zapytania opierają się na założeniu, że Taco_valuezawsze rośnie w czasie.

;WITH x AS
(
  SELECT Taco_ID, Taco_date,
    dr = ROW_NUMBER() OVER (PARTITION BY Taco_ID, Taco_Value ORDER BY Taco_date),
    qr = ROW_NUMBER() OVER (PARTITION BY Taco_ID ORDER BY Taco_date)
  FROM dbo.Taco
), y AS
(
  SELECT Taco_ID, Taco_date,
    rn = ROW_NUMBER() OVER (PARTITION BY Taco_ID, dr ORDER BY qr DESC)
  FROM x WHERE dr = 1
)
SELECT Taco_ID, Taco_date
FROM y 
WHERE rn = 1;

Alternatywa z mniejszym szaleństwem funkcji okna:

;WITH x AS
(
  SELECT Taco_ID, Taco_value, Taco_date = MIN(Taco_date)
  FROM dbo.Taco
  GROUP BY Taco_ID, Taco_value
), y AS
(
  SELECT Taco_ID, Taco_date, 
    rn = ROW_NUMBER() OVER (PARTITION BY Taco_ID ORDER BY Taco_date DESC)
  FROM x
)
SELECT Taco_ID, Taco_date FROM y WHERE rn = 1;

Przykłady w SQLfiddle


Aktualizacja

Dla tych, którzy śledzili, istniała spór o to, co się stanie, jeśli Taco_valuekiedykolwiek się powtórzy. Jeśli może on przejść z 1 do 2, a następnie z powrotem do 1 dla dowolnego Taco_ID, zapytania nie będą działać. Oto rozwiązanie tego przypadku, nawet jeśli nie jest to technika z lukami i wyspami, którą ktoś taki jak Itzik Ben-Gan może wymarzyć, a nawet jeśli nie ma to znaczenia dla scenariusza PO - może to być dotyczy przyszłego czytelnika. Jest trochę bardziej skomplikowany, a także dodałem dodatkową zmienną - taką Taco_ID, która ma tylko jedną Taco_value.

Jeśli chcesz dołączyć pierwszy wiersz dla dowolnego identyfikatora, w którym wartość w ogóle nie zmieniła się w całym zestawie:

;WITH x AS
(
  SELECT *, rn = ROW_NUMBER() OVER 
    (PARTITION BY Taco_ID ORDER BY Taco_date DESC)
  FROM dbo.Taco
), rest AS (SELECT * FROM x WHERE rn > 1)
SELECT  
  main.Taco_ID, 
  Taco_date = MIN(CASE 
    WHEN main.Taco_value = rest.Taco_value 
    THEN rest.Taco_date ELSE main.Taco_date 
  END)
FROM x AS main LEFT OUTER JOIN rest
ON main.Taco_ID = rest.Taco_ID AND rest.rn > 1
WHERE main.rn = 1
AND NOT EXISTS 
(
  SELECT 1 FROM rest AS rest2
   WHERE Taco_ID = rest.Taco_ID
   AND rn < rest.rn
   AND Taco_value <> rest.Taco_value
) 
GROUP BY main.Taco_ID;

Jeśli chcesz wykluczyć te wiersze, jest to nieco bardziej złożone, ale wciąż niewielkie zmiany:

;WITH x AS
(
  SELECT *, rn = ROW_NUMBER() OVER 
    (PARTITION BY Taco_ID ORDER BY Taco_date DESC)
  FROM dbo.Taco
), rest AS (SELECT * FROM x WHERE rn > 1)
SELECT 
  main.Taco_ID, 
  Taco_date = MIN(
  CASE 
    WHEN main.Taco_value = rest.Taco_value 
    THEN rest.Taco_date ELSE main.Taco_date 
  END)
FROM x AS main INNER JOIN rest -- ***** change this to INNER JOIN *****
ON main.Taco_ID = rest.Taco_ID AND rest.rn > 1
WHERE main.rn = 1
AND NOT EXISTS
(
  SELECT 1 FROM rest AS rest2
   WHERE Taco_ID = rest.Taco_ID
   AND rn < rest.rn
   AND Taco_value <> rest.Taco_value
)
AND EXISTS -- ***** add this EXISTS clause ***** 
(
  SELECT 1 FROM rest AS rest2
   WHERE Taco_ID = rest.Taco_ID
   AND Taco_value <> rest.Taco_value
)
GROUP BY main.Taco_ID;

Zaktualizowano przykłady SQLfiddle


Zauważyłem pewne znaczące problemy z wydajnością OVER, ale użyłem go tylko kilka razy i być może źle go piszę. Zauważyłeś coś?
Kenneth Fisher

1
@KennethFisher nie jest specjalnie z OVER. Jak wszystko inne, konstrukcje zapytań w dużym stopniu zależą od podstawowego schematu / indeksów, aby działać poprawnie. Nadrzędna klauzula, że ​​partycje będą miały takie same problemy jak GROUP BY.
Aaron Bertrand

@KennethFisher, proszę uważać, aby nie wyciągać szerokich, obszernych wniosków z pojedynczych, odizolowanych obserwacji. Widzę te same argumenty przeciwko CTE - „Cóż, miałem raz rekurencyjną CTE, a jej wydajność była do bani. Więc nie używam już CTE”.
Aaron Bertrand

Dlatego spytałem. Nie użyłem go wystarczająco, aby powiedzieć w ten czy inny sposób, ale kilka razy użyłem go, byłem w stanie uzyskać lepszą wydajność dzięki CTE. Nadal będę się z tym bawić.
Kenneth Fisher

@AaronBertrand Nie sądzę, że będą działać, jeśli valuepojawi się ponownie: Fiddle
ypercubeᵀᴹ

13

Zasadniczo jest to sugestia @ Taryn „skondensowana” do pojedynczego SELECT bez tabel pochodnych:

SELECT DISTINCT
  Taco_ID,
  Taco_date = MAX(MIN(Taco_date)) OVER (PARTITION BY Taco_ID)
FROM Taco
GROUP BY
  Taco_ID,
  Taco_value
;

Uwaga: to rozwiązanie uwzględnia postanowienie, które Taco_valuemoże tylko wzrosnąć. (Mówiąc dokładniej, zakłada się, że Taco_valuenie może wrócić do poprzedniej wartości - w rzeczywistości takiej samej jak połączona odpowiedź).

Demo SQL Fiddle dla zapytania: http://sqlfiddle.com/#!3/91368/2


7
Whoa, zagnieżdżone MAX / MIN. MIND BLOWN +1
Aaron Bertrand

7

Powinieneś być w stanie korzystać zarówno z funkcji agregujących, jak min()i max()uzyskać wynik:

select t1.Taco_ID, MAX(t1.taco_date) Taco_Date
from taco t1
inner join
(
    select MIN(taco_date) taco_date,
        Taco_ID, Taco_value
    from Taco
    group by Taco_ID, Taco_value
) t2
    on t1.Taco_ID = t2.Taco_ID
    and t1.Taco_date = t2.taco_date
group by t1.Taco_Id

Zobacz SQL Fiddle with Demo


5

Jeszcze jedna odpowiedź, która opiera się na założeniu, że wartości nie pojawiają się ponownie (jest to w zasadzie @ zapytanie Aarona 2, skondensowane w jednym mniejszym gnieździe):

;WITH x AS
(
  SELECT 
    Taco_ID, Taco_value, 
    Rn = ROW_NUMBER() OVER (PARTITION BY Taco_ID
                            ORDER BY MIN(Taco_date) DESC),
    Taco_date = MIN(Taco_date) 
  FROM dbo.Taco
  GROUP BY Taco_ID, Taco_value
)
SELECT Taco_ID, Taco_value, Taco_date
FROM x 
WHERE Rn = 1 ;

Test na: SQL-Fiddle


I odpowiedź na bardziej ogólny problem, w którym wartości mogą się ponownie pojawić:

;WITH x AS
(
  SELECT 
    Taco_ID, Taco_value, 
    Rn = ROW_NUMBER() OVER (PARTITION BY Taco_ID
                            ORDER BY MAX(Taco_date) DESC),    
    Taco_date = MAX(Taco_date) 
  FROM dbo.Taco
  GROUP BY Taco_ID, Taco_value
)
SELECT t.Taco_ID, Taco_date = MIN(t.Taco_date)
FROM x
  JOIN dbo.Taco t
    ON  t.Taco_ID = x.Taco_ID
    AND t.Taco_date > x.Taco_date
WHERE x.Rn = 2 
GROUP BY t.Taco_ID ;

(lub używając CROSS APPLYtak, aby wyświetlić wszystkie powiązane wiersze, w tym value,):

;WITH x AS
(
  SELECT 
    Taco_ID, Taco_value, 
    Rn = ROW_NUMBER() OVER (PARTITION BY Taco_ID
                            ORDER BY MAX(Taco_date) DESC),    
    Taco_date = MAX(Taco_date) 
  FROM dbo.Taco
  GROUP BY Taco_ID, Taco_value
)
SELECT t.*
FROM x
  CROSS APPLY 
  ( SELECT TOP (1) *
    FROM dbo.Taco t
    WHERE t.Taco_ID = x.Taco_ID
      AND t.Taco_date > x.Taco_date
    ORDER BY t.Taco_date
  ) t
WHERE x.Rn = 2 ;

Test na: SQL-Fiddle-2


Sugestie dotyczące bardziej ogólnego problemu nie działają w przypadku identyfikatorów bez zmian. Można to naprawić, dodając pozorne wpisy do oryginalnego zestawu (coś podobnego dbo.Taco UNION ALL SELECT DISTINCT Taco_ID, NULL AS Taco_value, '19000101' AS Taco_date).
Andriy M,

@AndriyM Wiem. Założyłem, że „zmiana” oznacza, że ​​chcą wyników, gdy są co najmniej 2 wartości, OP nie wyjaśnił tego (a ponieważ łatwiej było pisać :)
ypercubeᵀᴹ

2

FYI +1 za dostarczenie struktury próbki i danych. Jedyne, o co mógłbym prosić, to oczekiwany wynik dla tych danych.

EDYCJA: Ten doprowadzał mnie do szału. Właśnie odkryłem, że istnieje „prosty” sposób, aby to zrobić. Pozbyłem się niewłaściwych rozwiązań i umieściłem takie, które moim zdaniem są poprawne. Oto rozwiązanie podobne do @bluefeets, ale obejmuje testy, które dał @AaronBertrand.

;WITH TacoMin AS (SELECT Taco_ID, Taco_value, MIN(Taco_date) InitialValueDate
                FROM Taco
                GROUP BY Taco_ID, Taco_value)
SELECT Taco_ID, MAX(InitialValueDate)
FROM TacoMin
GROUP BY Taco_ID

2
OP nie prosi o bardziej aktualną datę, pyta, kiedy valuezmiany.
ypercubeᵀᴹ

Ahhh, widzę swój błąd. Opracowałem odpowiedź, ale jest prawie taka sama jak @ Aaron, więc nie ma sensu jej publikować.
Kenneth Fisher

1

Dlaczego nie uzyskać różnicy wartości opóźnienia i wartości wiodącej? jeśli różnica wynosi zero, to się nie zmienia, to nie jest zero, to się zmienia. Można to zrobić za pomocą prostego zapytania:

-- example gives the times the value changed in the last 24 hrs
SELECT
    LastUpdated, [DiffValue]
FROM (
  SELECT
      LastUpdated,
      a.AboveBurdenProbe1TempC - coalesce(lag(a.AboveBurdenProbe1TempC) over (order by ProcessHistoryId), 0) as [DiffValue]
  FROM BFProcessHistory a
  WHERE LastUpdated > getdate() - 1
) b
WHERE [DiffValue] <> 0
ORDER BY LastUpdated ASC

Funkcja lag...analityczna została wprowadzona dopiero niedawno w SQL Server 2012. Pierwotne pytanie dotyczy rozwiązania SQL Server 2008 R2. Twoje rozwiązanie nie będzie działać dla SQL Server 2008 R2.
John aka hot2use,

-1

Czy może to być tak proste jak poniżej?

       SELECT taco_id, MAX(
             CASE 
                 WHEN taco_value <> MAX(taco_value) 
                 THEN taco_date 
                 ELSE null 
             END) AS last_change_date

Biorąc pod uwagę, że taco_value zawsze wzrasta?

ps Sam jestem całkiem początkującym językiem SQL, ale uczę się powoli, ale pewnie.


1
Na SQL Server daje to błąd. Cannot perform an aggregate function on an expression containing an aggregate or a subquery
Martin Smith

2
Dodanie punktu do komentarza Martina: jesteś bezpieczny, jeśli kiedykolwiek opublikujesz tylko przetestowany kod. Łatwym sposobem może być przejście do witryny sqlfiddle.com, jeśli jesteś z dala od zwykłego placu zabaw.
dezso,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.