Existe-t-il un moyen d'accéder à la valeur de la «ligne précédente» dans une instruction SELECT?

93

J'ai besoin de calculer la différence d'une colonne entre deux lignes d'un tableau. Est-il possible que je puisse le faire directement dans SQL? J'utilise Microsoft SQL Server 2008.

Je cherche quelque chose comme ça:

SELECT value - (previous.value) FROM table

Imaginons que la variable "précédente" référence la dernière ligne sélectionnée. Bien sûr, avec une sélection comme celle-là, je me retrouverai avec n-1 lignes sélectionnées dans une table avec n lignes, ce n'est pas probablement, c'est exactement ce dont j'ai besoin.

Est-ce possible d'une manière ou d'une autre?

Edwin Jarvis
la source
6
Eh bien, il suffit d'ajouter un commentaire utile pour les nouveaux téléspectateurs. SQL 2012 a LAG et LEAD maintenant :) Reportez-vous à ce lien blog.sqlauthority.com/2013/09/22
KD

Réponses:

61

SQL n'a pas de notion d'ordre intégrée, vous devez donc classer par colonne pour que cela soit significatif. Quelque chose comme ça:

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

Si vous savez comment ordonner les choses mais pas comment obtenir la valeur précédente étant donné la valeur actuelle (par exemple, vous voulez classer par ordre alphabétique), je ne connais pas de moyen de le faire en SQL standard, mais la plupart des implémentations SQL auront extensions pour le faire.

Voici un moyen pour le serveur SQL qui fonctionne si vous pouvez classer les lignes de sorte que chacune soit distincte:

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

Si vous avez besoin de rompre les liens, vous pouvez ajouter autant de colonnes que nécessaire à ORDER BY.

RossFabricant
la source
C'est bien, l'ordre n'est pas un problème, je viens de le supprimer de l'exemple pour le rendre plus simple, je vais essayer ça.
Edwin Jarvis le
7
ce qui suppose que les clés primaires sont générées séquentiellement et que les lignes ne sont jamais supprimées et que la sélection n'a pas d'autre clause d'ordre et et et ...
MartinStettner
Martin a raison. Bien que cela puisse fonctionner dans certains cas, vous devez vraiment définir exactement ce que vous entendez par «précédent» dans un sens commercial, de préférence sans compter sur un ID généré.
Tom H
Vous avez raison, j'ai ajouté une amélioration en utilisant une extension SQL Server.
RossFabricant
2
En réponse à "C'est bien, l'ordre n'est pas un problème" ... Alors pourquoi ne soustrayez-vous pas simplement une valeur d'arbitrage dans votre requête puisque c'est ce que vous faites si vous ne considérez pas l'ordre?
JohnFx
79

Utilisez la fonction de décalage :

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

Les séquences utilisées pour les identifiants peuvent sauter des valeurs, donc Id-1 ne fonctionne pas toujours.

Hans Ginzel
la source
1
C'est la solution PostgreSQL. La question concerne MSSQL. MSSQL a une telle fonction dans les versions 2012+ ( msdn.microsoft.com/en-us/en-en/library/hh231256(v=sql.120).aspx )
Kromster
10
@KromStern Pas seulement une solution PostgreSQL. Les fonctions de fenêtre SQL ont été introduites dans la norme SQL: 2003 .
Hans Ginzel
La fonction LAG peut prendre trois paramètres: LAG(ExpressionToSelect, NumberOfRowsToLag, DefaultValue). Le nombre de lignes par défaut à retarder est de 1, mais vous pouvez spécifier cela et la valeur par défaut à sélectionner lorsqu'il n'est pas possible de retarder puisque vous êtes au début de l'ensemble.
vaindil
29

Oracle, PostgreSQL, SQL Server et bien d'autres moteurs SGBDR ont des fonctions analytiques appelées LAGet LEADqui font exactement cela.

Dans SQL Server avant 2012, vous devez effectuer les opérations suivantes:

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

, où COL1est la colonne par laquelle vous commandez.

Avoir un index sur (COL1, PK)améliorera considérablement cette requête.

Quassnoi
la source
14
SQL Server 2012 a désormais également LAG et LEAD.
ErikE
Le script Hana SQL prend également en charge LAG et LEAD.
mik
Juste pour ajouter un autre commentaire aux téléspectateurs qui sont arrivés ici à la recherche de faire cela dans Hive. Il a également des fonctions LAG et LEAD. Documentation ici: 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
la source
Cela fonctionne correctement s'il n'y a pas de regroupement dans la requête, mais que se passe-t-il si nous voulons soustraire des valeurs de la valeur précédente uniquement dans un groupe, disons le même EmployeeID, alors comment pouvons-nous faire cela? Coz exécuter cela ne fonctionne que pour les 2 premières lignes de chaque groupe et pas pour le reste des lignes de ce groupe. Pour cela, j'ai utilisé l'exécution de ce code en boucle while, mais cela semble être très lent. Une autre approche que nous pourrions dans ce scénario? Et cela aussi uniquement dans SQL Server 2008?
Hemant Sisodia
10

JOINDRE GAUCHE la table à elle-même, avec la condition de jointure élaborée de sorte que la ligne correspondante dans la version jointe de la table soit une ligne précédente, pour votre définition particulière de "précédente".

Mise à jour: Au début, je pensais que vous voudriez conserver toutes les lignes, avec NULL pour la condition où il n'y avait pas de ligne précédente. En le relisant, vous voulez simplement que les lignes soient éliminées, vous devriez donc une jointure interne plutôt qu'une jointure gauche.


Mettre à jour:

Les versions plus récentes de Sql Server ont également les fonctions de fenêtrage LAG et LEAD qui peuvent également être utilisées pour cela.

Joël Coehoorn
la source
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
la source
2

La réponse sélectionnée ne fonctionnera que s'il n'y a pas de lacunes dans la séquence. Cependant, si vous utilisez un identifiant généré automatiquement, il est probable qu'il y ait des espaces dans la séquence en raison des insertions qui ont été annulées.

Cette méthode devrait fonctionner si vous avez des lacunes

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