Intentando encontrar la última vez que un valor ha cambiado

26

Tengo una tabla que tiene una ID, un valor y una fecha. Hay muchas ID, valores y fechas en esta tabla.

Los registros se insertan en esta tabla periódicamente. La ID siempre se mantendrá igual pero ocasionalmente el valor cambiará.

¿Cómo puedo escribir una consulta que me proporcione la identificación más la última vez que el valor ha cambiado? Nota: el valor siempre aumentará.

De esta muestra de datos:

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

El resultado debería ser:

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

(Porque 00:05 fue la última vez que Taco_Valuecambió).

SqlSandwiches
fuente
2
¿Supongo que tacono tiene nada que ver con la comida?
Kermit
55
Tengo hambre y me gustaría comer algunos tacos. Solo necesitaba un nombre para la tabla de muestra.
SqlSandwiches
8
¿Elegiste tu nombre de usuario de forma similar?
Martin Smith
1
Muy posible.
SqlSandwiches

Respuestas:

13

Estas dos consultas se basan en la suposición de que Taco_valuesiempre aumenta con el tiempo.

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

Una alternativa con menos locura de función de ventana:

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

Ejemplos en SQLfiddle


Actualizar

Para aquellos que siguen la pista, hubo una disputa sobre lo que sucede si Taco_valuealguna vez pudiera repetirse. Si pudiera pasar de 1 a 2 y luego volver a 1 para cualquiera Taco_ID, las consultas no funcionarán. Aquí hay una solución para ese caso, incluso si no es la técnica de brechas e islas que alguien como Itzik Ben-Gan puede soñar, e incluso si no es relevante para el escenario del OP, puede ser relevante para un futuro lector. Es un poco más complejo, y también agregué una variable adicional, una Taco_IDque solo tiene una Taco_value.

Si desea incluir la primera fila para cualquier ID donde el valor no cambió en absoluto en todo el conjunto:

;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 desea excluir esas filas, es un poco más complejo, pero aún cambios menores:

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

Ejemplos actualizados de SQLfiddle

Aaron Bertrand
fuente
He notado algunos problemas de rendimiento importantes con OVER, pero solo lo he usado algunas veces y es posible que lo esté escribiendo mal. ¿Has notado algo?
Kenneth Fisher
1
@KennethFisher no específicamente con OVER. Como cualquier otra cosa, las construcciones de consultas dependen en gran medida del esquema / índices subyacentes para funcionar correctamente. Una cláusula over que las particiones sufrirán los mismos problemas que GROUP BY.
Aaron Bertrand
@KennethFisher, tenga cuidado de no sacar conclusiones amplias y amplias de observaciones singulares y aisladas. Veo los mismos argumentos en contra de los CTE: "Bueno, tuve este CTE recursivo una vez, y su rendimiento apestaba. Así que ya no uso los CTE".
Aaron Bertrand
Por eso pregunté. No lo he usado lo suficiente como para decirlo de una forma u otra, pero las pocas veces que lo usé pude obtener un mejor rendimiento con un CTE. Aunque seguiré jugando con eso.
Kenneth Fisher
@AaronBertrand No creo que esto funcione si valuereaparece: Fiddle
ypercubeᵀᴹ
13

Básicamente, esta es la sugerencia de @Taryn "condensada" en un solo SELECCIONAR sin tablas derivadas:

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

Nota: esta solución tiene en cuenta la estipulación que Taco_valuesolo puede aumentar. (Más exactamente, se supone que Taco_valueno puede volver a un valor anterior, de hecho, igual que la respuesta vinculada).

Una demostración de SQL Fiddle para la consulta: http://sqlfiddle.com/#!3/91368/2

Andriy M
fuente
77
Whoa, anidado MAX / MIN. MENT BLOWN +1
Aaron Bertrand
7

Debe poder usar ambas min()y max()las funciones agregadas obtienen el resultado:

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

Ver SQL Fiddle con Demo

Taryn
fuente
5

Una respuesta más que se basa en la suposición de que los valores no vuelven a aparecer (esto es básicamente la consulta 2 de @ Aaron, condensada en un nido menos):

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

Prueba en: SQL-Fiddle


Y una respuesta al problema más general, donde los valores pueden reaparecer:

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

(o usando CROSS APPLYasí valuese muestra toda la fila relacionada, incluida la ):

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

Prueba en: SQL-Fiddle-2

ypercubeᵀᴹ
fuente
Las sugerencias para el problema más general no funcionan para ID que no tienen cambios. Podría solucionarse agregando entradas ficticias al conjunto original (algo así como dbo.Taco UNION ALL SELECT DISTINCT Taco_ID, NULL AS Taco_value, '19000101' AS Taco_date).
Andriy M
@AndriyM lo sé. Supuse que significa "cambio" que quieren resultados cuando hay al menos 2 valores, el PO no ha aclarado eso (y porque era más fácil de escribir :)
ypercubeᵀᴹ
2

FYI +1 para proporcionar estructura de muestra y datos. Lo único que podría haber pedido es la salida esperada para esos datos.

EDITAR: Este me iba a volver loco. Acabo de saber que había una forma "simple" de hacer esto. Me deshice de las soluciones incorrectas y puse una que creo que es correcta. Aquí hay una solución similar a @bluefeets pero cubre las pruebas que dio @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
Kenneth Fisher
fuente
2
El OP no pide una fecha más reciente, pregunta cuándo valuecambian los cambios.
ypercubeᵀᴹ
Ahhh, veo mi error. Resolví una respuesta, pero es más o menos lo mismo que @ Aaron's, así que no tiene sentido publicarla.
Kenneth Fisher
1

¿Por qué no solo obtener la diferencia del valor de retraso y el valor de plomo? si la diferencia es cero, no cambió, si no es cero, cambió. Esto se puede hacer en una consulta simple:

-- 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
fuente
La lag...función analítica se introdujo "recientemente" en SQL Server 2012. La pregunta original es pedir una solución en SQL Server 2008 R2. Su solución no funcionaría para SQL Server 2008 R2.
John aka hot2use
-1

¿Podría ser esto tan simple como lo siguiente?

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

Dado que taco_value siempre aumenta?

PD: Yo mismo soy bastante principiante en SQL, sin embargo, estoy aprendiendo lenta pero seguramente.

pmc086
fuente
1
En SQL Server esto da el error. Cannot perform an aggregate function on an expression containing an aggregate or a subquery
Martin Smith
2
Agregando un punto al comentario de Martin: estás en el lado seguro si alguna vez publicas solo el código probado. Una manera fácil puede ser visitar sqlfiddle.com si está lejos de su área de juegos habitual.
dezso