views:

500

answers:

4

Lets say that I have a table with a timestamp column full of records and I want to calculate the smallest time difference between two consecutive records using only one query.

Maybe a table like...

CREATE TABLE `temperatures` (
  `temperature` double,
  `time` timestamp DEFAULT CURRENT_TIMESTAMP
);
A: 

You could try this:

SELECT
    T1.*,
    (SELECT MIN(T2.time)
     FROM temperatures T2
     WHERE T2.time > T1.time)-T1.time diff
FROM
    temperatures T1
ORDER BY
    T1.time
Lasse V. Karlsen
Works fine in a db that supports subqueries well--- takes ages on MySQL with a test of about 10,000 rows :(Also produces odd values for `diff`, e.g. -20090500997993
araqnid
+2  A: 

Try a query like this:

select 
    cur.timestamp as CurrentTime,
    prev.timestamp as PreviousTime,
    timediff(cur.timestamp,prev.timestamp) as TimeDifference,
    cur.temperature - prev.temperature as TemperatureDifference
from temperatures cur
left join temperatures prev on prev.timestamp < cur.timestamp
left join temperatures inbetween
    on prev.timestamp < inbetween.timestamp
    and inbetween.timestamp < cur.timestamp
where inbetween.timestamp is null

The first join seeks all previous rows for the current ("cur") row. The second join seeks rows in between the first and the second row. The where statement says there cannot be any rows in between the first and the second row. That way, you get a list of rows with their preceeding row.

Andomar
This will wor, but will be quite slow and won't handle the duplicate timestamps correctly.
Quassnoi
Don't you need a MIN() function applied to the time difference, and appropriate GROUP BY clause? You don't need to do a check that there is no reading between the two because if there was, the time differences between the first and second would be smaller than the difference between the first and third and hence MIN() would get rid of it. In other contexts, you have to ensure there is no intermediate reading between current and previous, and that complicates things no end.
Jonathan Leffler
@Jonathan: there is a join with INBETWEEN that does this check.
Quassnoi
+4  A: 

What you need is analytical functions LAG and MIN.

They are missing in MySQL, but can be easily emulated using session variables.

This query returns all differences between consecutive records:

SELECT  (temperature - @r) AS diff,
        @r := temperature
FROM    (
        SELECT  @r := 0
        ) vars,
        temperatures
ORDER BY
        time

This one returns minimal time difference:

SELECT  (
        SELECT  id,
                @m := LEAST(@m, TIMEDIFF(time, @r)) AS mindiff,
                @r := time
        FROM    (
                SELECT  @m := INTERVAL 100 YEAR,
                        @r := NULL
                ) vars,
                temperatures
        ORDER BY
                time, id
        ) qo
WHERE   qo.id = 
        (
        SELECT  id
        FROM    temperatures
        ORDER BY
                time DESC, id DESC
        LIMIT 1
        )

See this article in my blog on how to emulate analytic functions in MySQL:

If you add a PRIMARY KEY to you table (which you should always, always do!), then you may use more SQL-ish solution:

SELECT  temperature -
        (
        SELECT temperature
        FROM   temperatures ti
        WHERE  (ti.timestamp, ti.id) < (to.timestamp, to.id)
        ORDER BY
               ti.timestamp DESC, ti.id DESC
        LIMIT 1
        )
FROM    temperatures to
ORDER BY
       to.timestamp, to.id

This solution, though, is quite inefficient in MySQL due to the bug 20111.

The subquery will not use the range access path, though it will use an index on (timestamp, id) for ordering.

This may be worked around by creating a UDF that returns previous temperature, given the current record's id.

See this article in my blog for details:

IF you don't use any filtering conditions, then the solution which uses session variable will be the most efficient, though MySQL specific.

Similar solutions for SQL Server will look like this:

SELECT  temperature -
        (
        SELECT TOP 1 temperature
        FROM   temperatures ti
        WHERE  ti.timestamp < to.timestamp
               OR (ti.timestamp = to.timestamp AND ti.id < to.id)
        ORDER BY
               ti.timestamp DESC, ti.id DESC
        )
FROM    temperatures to
ORDER BY
       to.timestamp, to.id

and

SELECT  MIN(mindiff)
FROM    (
        SELECT  timestamp -
                (
                SELECT TOP 1 timestamp
                FROM   temperatures ti
                WHERE  ti.timestamp < to.timestamp
                       OR (ti.timestamp = to.timestamp AND ti.id < to.id)
                ORDER BY
                       ti.timestamp DESC, ti.id DESC
                ) AS mindiff
        FROM    temperatures to
        ORDER BY
               to.timestamp, to.id
        ) q

In SQL Server, this will work OK, provided you have an index on (timestamp, id) (or just on (timestamp), if your PRIMARY KEY is clustered)

Quassnoi
Perverse. So wrong it's right.
araqnid
@araqnid: MySQL (sigh).
Quassnoi
+1 Very nice. Tried it on Sql Server but "A SELECT statement that assigns a value to a variable must not be combined with data-retrieval operations."
Andomar
@Andomar: no, it's MySQL specific. Oracle has native support for LAG, and in SQL Server you'll need to emulate it with subqueries.
Quassnoi
@Andomar: I updated the post with SQL Server solution
Quassnoi
Also the Andomar and Lasse V. Karlsen are great answers, but to me this one is cleaner, thanks for the blog post btw!
coma
+3  A: 

Assuming that there is a unique constraint on the time stamp (to prevent there being two recordings at the same time):

SELECT MIN(timediff(t1.`time`, t2.`time`)) AS delta_t,
    FROM temperatures t1 JOIN temperatures t2 ON t1.`time` < t2.`time`

This answers the questions rather precisely - and doesn't convey other useful information (such as which two timestamps or temperatures).

Jonathan Leffler
What makes you think @op want the time difference? Temperature difference makes more sence in this query.
Quassnoi
Nevermind, I missed the question title as usual :)
Quassnoi