Essayer de trouver la dernière fois qu'une valeur a changé

26

J'ai une table qui a un ID, une valeur et une date. Il existe de nombreux ID, valeurs et dates dans ce tableau.

Des enregistrements sont insérés périodiquement dans ce tableau. L'ID restera toujours le même mais parfois la valeur changera.

Comment puis-je écrire une requête qui me donnera l'ID plus la dernière fois que la valeur a changé? Remarque: la valeur augmentera toujours.

À partir de ces exemples de données:

  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')

Le résultat devrait être:

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

(Parce que 00:05 était la dernière fois Taco_Valuechangé.)

SqlSandwiches
la source
2
Je suppose que cela tacon'a rien à voir avec la nourriture?
Kermit
5
J'ai faim et j'aimerais manger des tacos. Juste besoin d'un nom pour l'exemple de table.
SqlSandwiches
8
Avez-vous choisi votre nom d'utilisateur sur une base similaire?
Martin Smith
1
Tout à fait possible.
SqlSandwiches

Réponses:

13

Ces deux requêtes reposent sur l'hypothèse qui Taco_valueaugmente toujours avec le temps.

;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;

Une alternative avec moins de folie de fonction de fenêtre:

;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;

Exemples chez SQLfiddle


Mise à jour

Pour ceux qui gardent une trace, il y avait une controverse sur ce qui se passerait si cela Taco_valuepouvait se répéter. S'il peut passer de 1 à 2, puis revenir à 1 pour une donnée donnée Taco_ID, les requêtes ne fonctionneront pas. Voici une solution pour ce cas, même si ce n'est pas tout à fait la technique des lacunes et des îles que quelqu'un comme Itzik Ben-Gan peut imaginer, et même si elle n'est pas pertinente pour le scénario du PO - elle peut être pertinentes pour un futur lecteur. C'est un peu plus complexe, et j'ai également ajouté une variable supplémentaire - une Taco_IDqui n'en a jamais qu'une Taco_value.

Si vous souhaitez inclure la première ligne d'un ID dont la valeur n'a pas changé du tout dans l'ensemble:

;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;

Si vous souhaitez exclure ces lignes, c'est un peu plus complexe, mais des modifications mineures restent:

;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;

Exemples SQLfiddle mis à jour

Aaron Bertrand
la source
J'ai remarqué des problèmes de performances significatifs avec OVER, mais je ne l'ai utilisé que quelques fois et je l'ai peut-être mal écrit. Avez-vous remarqué quelque chose?
Kenneth Fisher
1
@KennethFisher pas spécifiquement avec OVER. Comme toute autre chose, les constructions de requête dépendent fortement du schéma / index sous-jacent pour fonctionner correctement. Une clause over que les partitions subiront les mêmes problèmes qu'un GROUP BY.
Aaron Bertrand
@KennethFisher, veillez à ne pas tirer de conclusions générales et générales à partir d'observations singulières et isolées. Je vois les mêmes arguments contre les CTE - "Eh bien, j'ai eu ce CTE récursif une fois, et ses performances sont nulles. Donc je n'utilise plus de CTE."
Aaron Bertrand
C'est pourquoi j'ai demandé. Je ne l'ai pas assez utilisé pour dire d'une manière ou d'une autre, mais les quelques fois où je l'ai utilisé, j'ai pu obtenir de meilleures performances avec un CTE. Je vais continuer à jouer avec.
Kenneth Fisher
@AaronBertrand Je ne pense pas que cela fonctionnera si un valueréapparaît: Fiddle
ypercubeᵀᴹ
13

Fondamentalement, c'est la suggestion de @ Taryn "condensée" en un seul SELECT sans tables dérivées:

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

Remarque: cette solution prend en compte la stipulation qui Taco_valuene peut qu'augmenter. (Plus exactement, il suppose que Taco_valuene peut pas revenir à une valeur précédente - identique à la réponse liée, en fait.)

Une démo SQL Fiddle pour la requête: http://sqlfiddle.com/#!3/91368/2

Andriy M
la source
7
Whoa, imbriqué MAX / MIN. MIND BLOWN +1
Aaron Bertrand
7

Vous devriez pouvoir utiliser les deux fonctions d'agrégation min()et max()obtenir le résultat:

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

Voir SQL Fiddle avec démo

Taryn
la source
5

Une autre réponse basée sur l'hypothèse que les valeurs ne réapparaissent pas (il s'agit essentiellement de la requête 2 d'Aaron, condensée dans un nid de moins):

;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 ;

Testez à: SQL-Fiddle


Et une réponse au problème plus général, où les valeurs peuvent réapparaître:

;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 ;

(ou en utilisant CROSS APPLYainsi toute la ligne associée, y compris le value, est affichée):

;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 ;

Testez à: SQL-Fiddle-2

ypercubeᵀᴹ
la source
Les suggestions pour le problème plus général ne fonctionnent pas pour les ID qui n'ont aucun changement. Pourrait être corrigé en ajoutant des entrées factices à l'ensemble d'origine (quelque chose comme dbo.Taco UNION ALL SELECT DISTINCT Taco_ID, NULL AS Taco_value, '19000101' AS Taco_date).
Andriy M
@AndriyM je sais. J'ai supposé que "changer" signifie qu'ils veulent des résultats quand il y a au moins 2 valeurs, l'OP n'a pas précisé cela (et parce qu'il était plus facile d'écrire :)
ypercubeᵀᴹ
2

FYI +1 pour fournir la structure et les données de l'échantillon. La seule chose que j'aurais pu demander est la sortie attendue pour ces données.

EDIT: Celui-ci allait me rendre fou. Je viens juste de savoir qu'il y avait un moyen "simple" de le faire. Je me suis débarrassé des solutions incorrectes et en ai mis une qui me semble correcte. Voici une solution similaire à @bluefeets mais elle couvre les tests que @AaronBertrand a donnés.

;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
Kenneth Fisher
la source
2
L'OP ne demande pas de date plus récente, il demande quand les valuechangements.
ypercubeᵀᴹ
Ahhh, je vois mon erreur. J'ai trouvé une réponse, mais c'est à peu près la même chose que @ Aaron, donc inutile de la poster.
Kenneth Fisher
1

Pourquoi ne pas simplement obtenir la différence de la valeur de décalage et de la valeur de plomb? si la différence est nulle, elle n'a pas changé, elle est non nulle, alors elle a changé. Cela peut se faire dans une simple requête:

-- 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
JJ_Coder4Hire
la source
La lag...fonction analytique n'a été introduite que "récemment" dans SQL Server 2012. La question d'origine demande une solution sur SQL Server 2008 R2. Votre solution ne fonctionnerait pas pour SQL Server 2008 R2.
John aka hot2use
-1

Cela pourrait-il être aussi simple que le suivant?

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

Étant donné que taco_value augmente toujours?

ps Je suis moi-même un débutant SQL, mais j'apprends lentement mais sûrement.

pmc086
la source
1
Sur SQL Server, cela donne l'erreur. Cannot perform an aggregate function on an expression containing an aggregate or a subquery
Martin Smith
2
Ajout d'un point au commentaire de Martin: vous êtes du bon côté si vous publiez un code testé uniquement. Un moyen simple peut être d'aller sur sqlfiddle.com si vous êtes loin de votre terrain de jeu habituel.
dezso