【问题标题】:Fastest way to get closest data from multiple tables based on time根据时间从多个表中获取最接近数据的最快方法
【发布时间】:2013-01-18 15:58:37
【问题描述】:

我有三个表,设置如下:

TEMPERATURE_1
  time
  zone (FK)
  temperature
TEMPERATURE_2
  time
  zone (FK)
  temperature
TEMPERATURE_3
  time
  zone (FK)
  temperature

每个表中的数据都会定期更新,但不一定同时更新(即时间条目不相同)。

我希望每次都能从每个表中访问最接近的读数,即:

TEMPERATURES
  time
  zone (FK)
  temperature_1
  temperature_2
  temperature_3

换句话说,对于我的三个表中的每个唯一时间,我都希望在 TEMPERATURES 表中有一行,其中 temperature_n 值是与每个原始表在时间上最接近的温度读数。

目前,我使用两个视图进行了设置:

create view temptimes
as select time, zone 
  from temperature_1
union
  select time, zone
  from temperature_2
union
  select time, zone
  from temperature_3;

create view temperatures
as select tt.time,
          tt.zone,
          (select temperature 
           from temperature_1
           order by abs(timediff(time, tt.time))
           limit 1) as temperature_1,
          (select temperature 
           from temperature_2
           order by abs(timediff(time, tt.time))
           limit 1) as temperature_2,
          (select temperature 
           from temperature_3
           order by abs(timediff(time, tt.time))
           limit 1) as temperature_3,
from temptimes as tt
order by tt.time;

这种方法有效,但在生产中使用太慢(对于每个温度约 1000 条记录的小型数据集需要几分钟以上)。

我不擅长使用 SQL,所以我确定我错过了执行此操作的正确方法。我应该如何解决这个问题?

【问题讨论】:

  • zone有什么意义?
  • 它是另一个表的外键,它将温度读数与我的数据模型的其余部分联系起来。本质上,每个区域都会有一组独立的温度读数(所以在一天结束时,我希望能够从 zone= 的温度中选择
  • 如果你提供了一个带有样本数据的测试用例,那真的很有帮助。示例:sqlfiddle.com/#!2/85a7b/1
  • 你可以试试这样的 - sqlfiddle.com/#!9/5db76/4。无论如何,必须在 DB 端完成很多循环,所以我不确定这是否会使其更快。快速做到这一点的一种方法是使用临时表或物化视图,但这需要额外的思考)

标签: mysql sql time-series


【解决方案1】:

代价高昂的部分是相关子查询必须计算每个 temperature_* 表的每一行的时间差,以便为 one 列找到最近的 one一个行在主查询中。

如果您可以根据 index 只在当前时间之后选择 one 行和在当前时间之前选择 one 行,这将大大加快速度计算这两个候选者的时间差。要使其快速运行,您只需在表中的列time 上建立一个索引。

我忽略了zone 专栏,因为它在问题中的作用尚不清楚,它只会给核心问题增加更多噪音。应该很容易添加到查询中。

如果没有额外的视图,这个查询会一次性完成:

SELECT time
      ,COALESCE(temp1
            ,CASE WHEN timediff(time, time1a) > timediff(time1b, time) THEN
                (SELECT t.temperature
                 FROM   temperature_1 t
                 WHERE  t.time = y.time1b)
             ELSE
                (SELECT t.temperature
                 FROM   temperature_1 t
                 WHERE  t.time = y.time1a)
             END) AS temp1

      ,COALESCE(temp2
            ,CASE WHEN timediff(time, time2a) > timediff(time2b, time) THEN
                (SELECT t.temperature
                 FROM   temperature_2 t
                 WHERE  t.time = y.time2b)
             ELSE
                (SELECT t.temperature
                 FROM   temperature_2 t
                 WHERE  t.time = y.time2a)
             END) AS temp2

      ,COALESCE(temp3
            ,CASE WHEN timediff(time, time3a) > timediff(time3b, time) THEN
                (SELECT t.temperature
                 FROM   temperature_3 t
                 WHERE  t.time = y.time3b)
             ELSE
                (SELECT t.temperature
                 FROM   temperature_3 t
                 WHERE  t.time = y.time3a)
             END) AS temp3
FROM  (
  SELECT time
        ,max(t1) AS temp1
        ,max(t2) AS temp2
        ,max(t3) AS temp3

        ,CASE WHEN max(t1) IS NULL THEN
           (SELECT t.time FROM temperature_1 t
            WHERE  t.time < x.time
            ORDER  BY t.time DESC LIMIT 1) ELSE NULL END AS time1a
        ,CASE WHEN max(t1) IS NULL THEN
           (SELECT t.time FROM temperature_1 t
            WHERE  t.time > x.time
            ORDER  BY t.time      LIMIT 1) ELSE NULL END AS time1b
  
        ,CASE WHEN max(t2) IS NULL THEN
           (SELECT t.time FROM temperature_2 t
            WHERE  t.time < x.time
            ORDER  BY t.time DESC LIMIT 1) ELSE NULL END AS time2a
        ,CASE WHEN max(t2) IS NULL THEN
           (SELECT t.time FROM temperature_2 t
            WHERE  t.time > x.time
            ORDER  BY t.time      LIMIT 1) ELSE NULL END AS time2b

        ,CASE WHEN max(t3) IS NULL THEN
           (SELECT t.time FROM temperature_3 t
            WHERE  t.time < x.time
            ORDER  BY t.time DESC LIMIT 1) ELSE NULL END AS time3a
        ,CASE WHEN max(t3) IS NULL THEN
           (SELECT t.time FROM temperature_3 t
            WHERE  t.time > x.time
            ORDER  BY t.time      LIMIT 1) ELSE NULL END AS time3b
  FROM  (
      SELECT time, temperature AS t1, NULL AS t2, NULL AS t3 FROM temperature_1
      UNION ALL
      SELECT time, NULL AS t1, temperature AS t2, NULL AS t3 FROM temperature_2
      UNION ALL
      SELECT time, NULL AS t1, NULL AS t2, temperature AS t3 FROM temperature_3
      ) AS x
  GROUP BY time
  ) y
ORDER BY time;

->sqlfiddle

解释

suqquery x 替换您的视图temptimes 并将温度带入结果。如果所有三个表都同步并且在所有相同的时间点都有温度,那么其余的甚至都不需要并且速度非常快。
对于三个表之一没有行的每个时间点,都按照指示获取温度:从每个表中获取“最接近”的一个。

suqquery y 聚合来自x的行,并根据当前时间从每个温度所在的表中获取上一次(time1a)和下一次(time1b)失踪。这些查找使用索引应该很快。

最终查询从实际缺失的每个温度的最接近时间的行中获取温度。

如果 MySQL 允许从高于当前子查询的多个级别引用列,则此查询可能会更简单。咬它不能。在 PostgreSQL 中工作得很好:->sqlfiddle

如果可以从相关子查询中返回多于一列,那也会更简单,但我不知道如何在 MySQL 中做到这一点。

使用 CTE窗口函数很多简单,但 MySQL 不知道这些现代 SQL 功能(不像其他相关关系型数据库)。

【讨论】:

  • 谢谢 - 我终于明白了这个查询,而且它运行得非常快:)
  • 实际上有一个问题:为什么将 max(t1) 作为 temp1 (etc)?我不明白为什么最高温度会很重要
  • @sapi: 也可以是min()。甚至sum()。关键是(假设在一个基表中不能有两行相同的time),每列最多有一个温度值,其余的是NULL。这些聚合函数中的任何一个都只会考虑非空值(如果没有,则返回NULL)。有关更多信息,请参阅手册 here
【解决方案2】:

这很慢的原因是它需要 3 次表扫描来计算和排序差异。

我假设您已经在时区列上建立了索引 - 由于表扫描问题,目前它们无济于事。

根据您的需要和数据收集率,有多种选择可以避免这种情况。

您已经说过数据是定期收集的,但不是同时收集的。这提出了一些选择。

  1. 您需要临时数据的重要性级别 - 天、小时、分钟等。仅将时区信息存储到该重要性级别(或有另一个列)并对此进行查询.
  2. 如果您知道 3 个壁橱时间将在特定时间范围内(小时、天等),请在 where 子句中将计算限制为可能的候选时间。您正在有效地构建直方图类型的存储桶 - 您需要一个日历表才能有效地做到这一点。
  3. 进行单向比较,即仅将考虑限制在您要查找的时间之后的那些时间,因此如果您要查找 12:00:00,则 13:45:32 是候选时间,但 11:59:59 不是t.

我了解您想要实现的目标 - 问问自己为什么以及更简单的解决方案是否能满足您的需求。

【讨论】:

    【解决方案3】:

    我的建议是您不要采用最接近的时间,而是在给定时间或之前采用第一次。原因很简单:通常给定时间的数据是当时已知的。对于大多数用途而言,纳入未来信息通常不是一个好主意。

    通过此更改,您可以修改查询以利用 time 上的索引。查询中的索引的问题是该函数排除了对索引的使用。

    因此,如果您想要最近的温度,请对每个变量使用它:

          (select temperature 
           from temperature_1 t2
           where t2.time <= tt.time
           order by t2.time desc
           limit 1
          ) as temperature_1,
    

    其实也可以这样构造:

          (select time 
           from temperature_1 t2
           where t2.time <= tt.time
           order by t2.time desc
           limit 1
          ) as time_1,
    

    然后将温度信息重新加入。这将是有效的,使用索引。

    考虑到这一点,您实际上可以有两个变量time_1_beforetime_1_after,分别表示最佳时间或之前以及最佳时间或之后。您可以在选择中使用逻辑来选择最接近的值。使用索引返回温度的连接应该是有效的。

    但是,我要重申,我认为最后一个温度或之前的温度可能是最好的选择。

    【讨论】:

    • 您在选择中描述的逻辑是否会为调用增加很多开销?这听起来很像我所追求的。 (我同意,如果数据是在阶跃变化时立即记录的,那么最后一个或之前将是正确的选择。但是,对于定期记录,在我看来,采用最接近的读数更准确,因为没有知道变化发生在间隔中的位置的方法。)
    • 查询应该使用time 字段上的索引高效运行。有开销,但索引不应该特别昂贵。对于定期记录,我建议使用一种修改方法,该方法从一个周期时间值表开始,并使用我的解决方案中建议的查找方法。
    • 在使用两个变量查找逻辑之前,我在问题中描述的 temptimes 视图是否适合初始值表?
    • 您的 temptimes 视图相当昂贵,因为它先合并然后删除重复项。拥有一个实际的表将使查询更有效率。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-08-12
    • 1970-01-01
    • 1970-01-01
    • 2014-03-16
    • 2019-05-27
    • 2016-03-23
    • 2015-10-27
    相关资源
    最近更新 更多