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

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

Aaron Bertrand
fonte
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

Andriy M
fonte
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

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

ypercubeᵀᴹ
fonte
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
Kenneth Fisher
fonte
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
JJ_Coder4Hire
fonte
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.

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