Tentando encontrar a última vez que um valor mudou


26

Eu tenho uma tabela que possui uma identificação, um valor e uma data. Existem muitos IDs, valores e datas nesta tabela.

Os registros são inseridos nessa tabela periodicamente. O ID sempre permanecerá o mesmo, mas ocasionalmente o valor será alterado.

Como posso escrever uma consulta que me forneça o ID mais a hora mais recente em que o valor foi alterado? Nota: o valor sempre aumentará.

A partir desses dados de amostra:

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

O resultado deve ser:

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

(Porque 00:05 foi a última vez que Taco_Valuemudou.)


2
Suponho tacoque não tenha nada a ver com a comida?
18713 Kermit

5
Estou com fome e gostaria de comer algumas tacos. Só precisava de um nome para a tabela de amostra.
SqlSandwiches

8
Você escolheu seu nome de usuário em uma base semelhante?
Martin Smith

11
Bastante possível.
SqlSandwiches

Respostas:


13

Essas duas consultas se baseiam na suposição de que Taco_valuesempre aumenta com o tempo.

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

Uma alternativa com menos loucura de função de janela:

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

Exemplos no SQLfiddle


Atualizar

Para quem acompanha, houve uma disputa sobre o que acontece se Taco_valuealguma vez se repetir. Se for de 1 para 2 e depois voltar para 1 Taco_ID, as consultas não funcionarão. Aqui está uma solução para esse caso, mesmo que não seja exatamente a técnica de lacunas e ilhas que alguém como Itzik Ben-Gan possa sonhar, e mesmo que não seja relevante para o cenário do OP - pode ser relevante para um futuro leitor. É um pouco mais complexo, e também adicionei uma variável adicional - uma Taco_IDque só tem uma Taco_value.

Se você deseja incluir a primeira linha de qualquer ID em que o valor não foi alterado em todo o 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;

Se você deseja excluir essas linhas, é um pouco mais complexo, mas ainda há pequenas alterações:

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

Exemplos atualizados do SQLfiddle


Percebi alguns problemas significativos de desempenho com o OVER, mas só o usei algumas vezes e posso estar escrevendo mal. Você notou alguma coisa?
Kenneth Fisher

11
@KennethFisher não especificamente com OVER. Como qualquer outra coisa, as construções de consulta dependem muito do esquema / índices subjacentes para funcionar corretamente. Uma cláusula over de que as partições sofrerão os mesmos problemas que um GROUP BY.
Aaron Bertrand

@KennethFisher, tome cuidado para não tirar conclusões amplas e abrangentes de observações singulares e isoladas. Vejo os mesmos argumentos contra as CTEs - "Bem, eu tive essa CTE recursiva uma vez e seu desempenho foi ruim. Portanto, não uso mais CTEs".
Aaron Bertrand

É por isso que eu pedi. Eu não o usei o suficiente para dizer de uma maneira ou de outra, mas nas poucas vezes em que o usei, consegui obter melhor desempenho com um CTE. Vou continuar brincando com isso.
Kenneth Fisher

@AaronBertrand Eu não acho que estes irão trabalhar se um valuereaparece: Fiddle
ypercubeᵀᴹ

13

Basicamente, esta é a sugestão de @ Taryn "condensada" para um único SELECT sem tabelas 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 solução leva em consideração a estipulação que Taco_valuesó pode aumentar. (Mais exatamente, pressupõe que Taco_valuenão possa voltar a um valor anterior - mesmo que a resposta vinculada, na verdade).

Uma demonstração do SQL Fiddle para a consulta: http://sqlfiddle.com/#!3/91368/2


7
Whoa, aninhado MAX / MIN. MENT BLOWN +1
Aaron Bertrand

7

Você deve poder usar as funções agregadas min()e max()obter o 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

Veja SQL Fiddle com demonstração


5

Mais uma resposta baseada na suposição de que os valores não reaparecem (essa é basicamente a consulta 2 de Aaron 2, condensada em um ninho a 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 ;

Teste em: SQL-Fiddle


E uma resposta para o problema mais geral, onde os valores podem 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 ;

(ou usando CROSS APPLYpara que toda a linha relacionada, incluindo a value, seja mostrada):

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

Teste em: SQL-Fiddle-2


As sugestões para o problema mais geral não funcionam para IDs que não têm alterações. Pode ser corrigido com a adição de entradas falsas ao conjunto original (algo como dbo.Taco UNION ALL SELECT DISTINCT Taco_ID, NULL AS Taco_value, '19000101' AS Taco_date).
precisa

@AndriyM eu sei. Presumi que significa "mudança" que eles querem resultados quando há a 2 valores mínimos, o OP não esclareceu que (e porque era mais fácil escrever :)
ypercubeᵀᴹ

2

FYI +1 por fornecer dados e estrutura da amostra. A única coisa que eu poderia ter pedido é a saída esperada para esses dados.

Edição: Este estava indo me deixar louco. Eu apenas novo, havia uma maneira "simples" de fazer isso. Eu me livrei das soluções incorretas e coloquei uma que acredito estar correta. Aqui está uma solução semelhante ao @bluefeets, mas abrange os testes que o @AaronBertrand deu.

;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
O OP não pede uma data mais recente, ele pergunta quando as valuemudanças são feitas.
ypercubeᵀᴹ

Ahhh, eu vejo o meu erro. Eu elaborei uma resposta, mas é praticamente a mesma do @ Aaron, então não faz sentido postá-la.
Kenneth Fisher

1

Por que não apenas obter a diferença entre o valor do atraso e o valor do lead? se a diferença é zero, não mudou, não é zero, mudou. Isso pode ser feito em uma consulta simples:

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

A lag...função analítica foi introduzida "recentemente" apenas no SQL Server 2012. A pergunta original está solicitando uma solução no SQL Server 2008 R2. Sua solução não funcionaria para o SQL Server 2008 R2.
John aka hot2use

-1

Isso pode ser tão simples quanto o seguinte?

       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 sempre aumenta?

ps Eu mesmo sou iniciante em SQL, aprendendo devagar, mas com segurança.


11
No SQL Server, isso dá o erro. Cannot perform an aggregate function on an expression containing an aggregate or a subquery
Martin Smith

2
Acrescentando um ponto ao comentário de Martin: você está do lado seguro, se alguma vez publicar apenas código testado. Uma maneira fácil pode ser acessada no sqlfiddle.com se você estiver fora do seu playground habitual.
Dez
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.