如何使用 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 提供的工具来优化这一点的最佳方法是什么?
最后一个约束是这些值在插入数据表之前没有排序,并且它们可能会在以后更新。这可能会影响任何可能的反规范化策略。
【问题讨论】:
表中有哪些索引? 旁注:将表或字段命名为time
或datetime
或date
或float
等是一个坏习惯。
@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 JOIN
s 右侧的列中生成NULL
s。但是d
在这些连接的左侧。因此,value
永远不可能是NULL
。以上是关于如何使用 SQL 有效地确定行之间的更改的主要内容,如果未能解决你的问题,请参考以下文章