如何使用 SQL 有效地确定行之间的更改

Posted

技术标签:

【中文标题】如何使用 SQL 有效地确定行之间的更改【英文标题】:How to efficiently determine changes between rows using SQL 【发布时间】:2011-08-31 21:07:47 【问题描述】:

我有一个非常大的 mysql 表,其中包含从多个传感器读取的数据。本质上,有一个时间戳和一个值列。我将省略传感器 ID,在此处索引其他详细信息:

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 提供的工具来优化这一点的最佳方法是什么?

最后一个约束是这些值在插入数据表之前没有排序,并且它们可能会在以后更新。这可能会影响任何可能的反规范化策略。

【问题讨论】:

表中有哪些索引? 旁注:将表或字段命名为timedatetimedatefloat 等是一个坏习惯。 @ypercube:除了此示例中未显示的列上的键(如合成主键)外,时间列上还有一个唯一键。 @ypercube:感谢您的提示,但这不是我的实际代码。我缩短并浓缩了它以使我的问题更简洁。我可能在时间列方面做得过火了。 :) 【参考方案1】:

我想你不能切换数据库引擎。如果可能,window functions 将允许您编写如下内容:

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

【讨论】:

@Denis,注意group by 已经对其中列出的元素进行了排序,因此不需要最后一个order by .. 是的,但排序是实现的副作用,而不是 SQL 标准。你永远不知道 MySQL 什么时候会降低副作用(Oracle 做到了)。 :-) 您还可以在(value,time)(sensor_id,value,time) 上试验索引,并使用该索引查看查询计划。 @Denis,非常感谢您抽出宝贵时间!您能否解释一下示例中的 measure_id 列?那应该是数据表的主键还是外键? @cg:数据表的主键。【参考方案2】:

你可以试试这个——我不保证它会表现得更好,但这是我将一行与“上一个”行关联起来的常用方法:

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)和候选行(d_prev)之间的行(d_inter)。如果我们找不到这样的行(d_inter.time is null),那么该候选确实是前一行。

【讨论】:

太棒了!这实际上是我正在寻找的那种“技巧”。您的查询比原始查询快很多。它仍然不够快,无法直接使用,但它可能是我需要的数据聚合的基础。非常感谢您的回答。 如果没有更好的解决方案,我会立即投票,并在几天后接受。 我认为您在 WHERE 子句的最后一个括号中的语句中可能还需要 OR d.value is null。 @user1383092 - 来自问题 - value float NOT NULL。我们最终只在LEFT JOINs 右侧的列中生成NULLs。但是d 在这些连接的左侧。因此,value 永远不可能是NULL

以上是关于如何使用 SQL 有效地确定行之间的更改的主要内容,如果未能解决你的问题,请参考以下文章

如何最有效地在 SQL Server 中插入/更新几百万行?

如何更有效地找到 PL/SQL 中分隔符之间的子字符串?

如何在 v-for 循环期间有条件地更改表格行样式?

如何使用 sqlalchemy 有效地管理频繁的模式更改?

如何更有效地使用 RunApp 功能来更改页面

如何有效地对 SQL 数据库中的记录进行版本控制