Czy istnieje sposób uzyskania dostępu do wartości „poprzedniego wiersza” w instrukcji SELECT?

96

Muszę obliczyć różnicę kolumny między dwoma wierszami tabeli. Czy jest jakiś sposób, żebym mógł to zrobić bezpośrednio w SQL? Używam Microsoft SQL Server 2008.

Szukam czegoś takiego:

SELECT value - (previous.value) FROM table

Wyobrażając sobie, że „poprzednia” zmienna odwołuje się do ostatnio wybranego wiersza. Oczywiście z taką selekcją skończę z n-1 wierszami wybranymi w tabeli z n wierszami, to nie jest prawdopodobnie, w rzeczywistości jest dokładnie tym, czego potrzebuję.

Czy to w jakiś sposób możliwe?

Edwin Jarvis
źródło
6
Cóż, po prostu dodaj komentarz przydatny dla nowych widzów. SQL 2012 ma teraz LAG i LEAD :) Skorzystaj z tego linku blog.sqlauthority.com/2013/09/22/…
KD

Odpowiedzi:

65

SQL nie ma wbudowanego pojęcia kolejności, więc aby mieć znaczenie, musisz uporządkować według jakiejś kolumny. Coś takiego:

select t1.value - t2.value from table t1, table t2 
where t1.primaryKey = t2.primaryKey - 1

Jeśli wiesz, jak uporządkować, ale nie wiesz, jak uzyskać poprzednią wartość podaną bieżącą (EG, chcesz uporządkować alfabetycznie), to nie wiem, jak to zrobić w standardowym SQL, ale większość implementacji SQL będzie miała rozszerzenia, aby to zrobić.

Oto sposób na serwer SQL, który działa, jeśli możesz uporządkować wiersze w taki sposób, aby każdy z nich był inny:

select  rank() OVER (ORDER BY id) as 'Rank', value into temp1 from t

select t1.value - t2.value from temp1 t1, temp1 t2 
where t1.Rank = t2.Rank - 1

drop table temp1

Jeśli chcesz zerwać remisy, możesz dodać tyle kolumn, ile potrzeba, do ORDER BY.

RossFabricant
źródło
W porządku, porządek nie jest problemem, po prostu usunąłem go z przykładu, aby było prostsze, spróbuję.
Edwin Jarvis
7
który zakłada, że ​​klucze podstawowe są generowane sekwencyjnie, a wiersze nigdy nie są usuwane, a selekcja nie ma żadnej innej klauzuli kolejności i i i ...
MartinStettner
Martin ma rację. Chociaż może to zadziałać w niektórych przypadkach, naprawdę musisz dokładnie zdefiniować, co masz na myśli przez „poprzedni” w sensie biznesowym, najlepiej bez polegania na wygenerowanym identyfikatorze.
Tom H
Masz rację, dodałem ulepszenie za pomocą rozszerzenia SQL Server.
RossFabricant
2
W odpowiedzi na pytanie „W porządku, porządek nie jest problemem”… W takim razie dlaczego nie odejmiesz po prostu arbitralnej wartości w zapytaniu, ponieważ to właśnie robisz, jeśli nie bierzesz pod uwagę porządku?
JohnFx
85

Użyj funkcji opóźnienia :

SELECT value - lag(value) OVER (ORDER BY Id) FROM table

Sekwencje używane dla identyfikatorów mogą pomijać wartości, więc Id-1 nie zawsze działa.

Hans Ginzel
źródło
1
To jest rozwiązanie PostgreSQL. Pytanie dotyczy MSSQL. MSSQL ma taką funkcję w wersjach 2012+ ( msdn.microsoft.com/en-us/en-en/library/hh231256(v=sql.120).aspx )
Kromster
10
@KromStern Nie tylko rozwiązanie PostgreSQL. Funkcje okna SQL zostały wprowadzone w standardzie SQL: 2003 .
Hans Ginzel
Funkcja LGD może potrwać trzy parametry: LAG(ExpressionToSelect, NumberOfRowsToLag, DefaultValue). Domyślna liczba wierszy do opóźnienia to 1, ale możesz to określić i domyślną wartość do wybrania, gdy nie można opóźnić, ponieważ jesteś na początku zestawu.
vaindil
30

Oracle, PostgreSQL, SQL Server i wiele więcej RDBMS silniki mają funkcji analitycznych nazwie LAGi LEADże zrobić tę samą rzecz.

W programie SQL Server sprzed 2012 r. Należałoby wykonać następujące czynności:

SELECT  value - (
        SELECT  TOP 1 value
        FROM    mytable m2
        WHERE   m2.col1 < m1.col1 OR (m2.col1 = m1.col1 AND m2.pk < m1.pk)
        ORDER BY 
                col1, pk
        )
FROM mytable m1
ORDER BY
      col1, pk

, gdzie COL1jest kolumna, według której zamawiasz.

Posiadanie indeksu (COL1, PK)znacznie poprawi to zapytanie.

Quassnoi
źródło
14
SQL Server 2012 ma teraz również LAG i LEAD.
ErikE,
Skrypt Hana SQL obsługuje również LAG i LEAD.
mik
Chcę tylko dodać kolejny komentarz do widzów, którzy przybyli tutaj i szukają tego w Hive. Posiada również funkcje LAG i LEAD. Dokumentacja tutaj: cwiki.apache.org/confluence/display/Hive/ ...
Jaime Caffarel
27
WITH CTE AS (
  SELECT
    rownum = ROW_NUMBER() OVER (ORDER BY columns_to_order_by),
    value
  FROM table
)
SELECT
  curr.value - prev.value
FROM CTE cur
INNER JOIN CTE prev on prev.rownum = cur.rownum - 1
Jeremy Stein
źródło
Działa poprawnie, jeśli w zapytaniu nie ma grupowania, ale co, jeśli chcemy odjąć wartości od poprzedniej wartości tylko w obrębie grupy, powiedzmy ten sam EmployeeID, to jak możemy to zrobić? Ponieważ uruchomienie tego działa tylko dla 2 górnych wierszy z każdej grupy, a nie dla pozostałych wierszy w tej grupie. W tym celu użyłem tego kodu w pętli while, ale wydaje się, że jest to bardzo wolne. Jakieś inne podejście, które moglibyśmy zastosować w tym scenariuszu? I to też tylko w SQL Server 2008?
Hemant Sisodia
10

LEWY DOŁĄCZ tabelę do samej siebie, z warunkiem łączenia opracowanym w taki sposób, aby wiersz dopasowany w połączonej wersji tabeli znajdował się o jeden wiersz wcześniej, dla określonej definicji „poprzedniego”.

Aktualizacja: Na początku myślałem, że chcesz zachować wszystkie wiersze, z wartością NULL dla warunku, w którym nie było poprzedniego wiersza. Czytając go ponownie, chcesz po prostu usunąć te wiersze, więc powinieneś raczej połączyć wewnętrzne niż lewe.


Aktualizacja:

Nowsze wersje Sql Server mają również funkcje LAG i LEAD Windowing, których można również użyć do tego.

Joel Coehoorn
źródło
3
select t2.col from (
select col,MAX(ID) id from 
(
select ROW_NUMBER() over(PARTITION by col order by col) id ,col from testtab t1) as t1
group by col) as t2
user1920851
źródło
2

Wybrana odpowiedź będzie działać tylko wtedy, gdy nie ma przerw w sekwencji. Jeśli jednak używasz automatycznie generowanego identyfikatora, prawdopodobnie wystąpią przerwy w sekwencji spowodowane wstawieniami, które zostały wycofane.

Ta metoda powinna działać, jeśli masz luki

declare @temp (value int, primaryKey int, tempid int identity)
insert value, primarykey from mytable order by  primarykey

select t1.value - t2.value from @temp  t1
join @temp  t2 
on t1.tempid = t2.tempid - 1
HLGEM
źródło