Как эффективно определять изменения между строками с использованием SQL

У меня очень большая таблица MySQL, содержащая данные, считываемые с нескольких датчиков. По существу, есть метка времени и столбец значений. Я опускаю идентификатор датчика, индексирует другие детали здесь:

CREATE TABLE `data` (
  `time` datetime NOT NULL,
  `value` float NOT NULL
)

Столбец value редко изменяется, и мне нужно найти моменты времени, когда происходят эти изменения. Предположим, что есть значение каждую минуту, следующий запрос возвращает именно то, что мне нужно:

SELECT d.*, 
  (SELECT value FROM data WHERE time<d.time ORDER by time DESC limit 1) 
    AS previous_value 
FROM data d 
HAVING d.value<>previous_value OR previous_value IS NULL;

+---------------------+-------+----------------+
| time                | value | previous_value |
+---------------------+-------+----------------+
| 2011-05-23 16:05:00 |     1 |           NULL |
| 2011-05-23 16:09:00 |     2 |              1 |
| 2011-05-23 16:11:00 |   2.5 |              2 |
+---------------------+-------+----------------+

Единственная проблема заключается в том, что это очень неэффективно, в основном из-за зависимого подзапроса. Какой был бы лучший способ оптимизировать это с помощью инструментов, которые MySQL 5.1 может предложить?

Последнее ограничение состоит в том, что значения не упорядочены до того, как они будут вставлены в таблицу данных, и что они могут быть обновлены позднее. Это может повлиять на любые возможные стратегии де-нормализации.

Ответ 1

Вы можете попробовать это - я не буду гарантировать, что он будет работать лучше, но это мой обычный способ сопоставить строку со "предыдущей" строкой:

SELECT
    * --TODO, list columns
FROM
    data d
       left join
    data d_prev
       on
           d_prev.time < d.time --TODO - Other key columns?
       left join
    data d_inter
       on
           d_inter.time < d.time and
           d_prev.time < d_inter.time --TODO - Other key columns?
WHERE
    d_inter.time is null AND
    (d_prev.value is null OR d_prev.value <> d.value)

(Я думаю, что это правильно - можно было бы с некоторыми образцами данных проверить его).

В принципе, идея состоит в том, чтобы присоединить таблицу к себе, и для каждой строки (в d) найдите строки-кандидаты (в d_prev) для "предыдущей" строки. Затем выполните дальнейшее соединение, чтобы попытаться найти строку (в d_inter), которая существует между текущей строкой (в d) и рядом с кандидатом (в d_prev). Если мы не сможем найти такую ​​строку (d_inter.time is null), то этот кандидат действительно был предыдущей строкой.

Ответ 2

Я полагаю, что это не вариант для переключения механизма БД. В случае, если это возможно, то функции окна позволяют писать такие вещи:

SELECT d.*
FROM (
    SELECT d.*, lag(d.value) OVER (ORDER BY d.time) as previous_value 
    FROM data d
  ) as d
WHERE d.value IS DISTINCT FROM d.previous_value;

Если нет, вы можете попробовать переписать запрос так:

select data.*
from data
left join (
    select data.measure_id,
           data.time,
           max(prev_data) as prev_time
    from data
    left join data as prev_data
    on prev_data.time < data.time
    group by data.measure_id, data.time, data.value
    ) as prev_data_time
on prev_data_time.measure_id = data.measure_id
and prev_data_time.time = data.time
left join prev_data_value
on prev_data_value.measure_id = data.measure_id
and prev_data_value.time = prev_data_time.prev_time
where data.value <> prev_data_value.value or prev_data_value.value is null