【问题标题】:Optimize MySQL query - Use of indexes优化 MySQL 查询 - 索引的使用
【发布时间】:2016-11-26 13:36:11
【问题描述】:

我正在尝试优化 MySQL 查询。我正在尝试使用特定商店每 15 分钟的商品价格移动平均值更新表格的列。

我的表结构如下

╔═════╦═════════════════════╦════════════╦══════╦════════════════╗
║ ID  ║      DATETIME       ║   NAME     ║Price ║ 15_MIN_AVERAGE ║
╠═════╬═════════════════════╬════════════╬══════╬════════════════╣
║ 1   ║ 2000-01-01 00:00:05 ║ WALMART    ║   1  ║                ║
║ 2   ║ 2000-01-01 00:00:05 ║ BESTBUY    ║   6  ║                ║
║ 3   ║ 2000-01-01 00:00:05 ║ RADIOSHACK ║   2  ║                ║
║ 4   ║ 2000-01-01 00:00:10 ║ WALMART    ║   6  ║                ║
║ 5   ║ 2000-01-01 00:00:10 ║ BESTBUY    ║   2  ║                ║   
║ 6   ║ 2000-01-01 00:00:10 ║ RADIOSHACK ║   8  ║                ║
║ 7   ║ 2000-01-01 00:00:15 ║ WALMART    ║  10  ║                ║
║ 8   ║ 2000-01-01 00:00:15 ║ BESTBUY    ║   2  ║                ║
║ 9   ║ 2000-01-01 00:00:15 ║ RADIOSHACK ║   3  ║                ║
║ 10  ║ 2000-01-01 00:00:20 ║ WALMART    ║   6  ║                ║
║ 11  ║ 2000-01-01 00:00:20 ║ BESTBUY    ║   4  ║                ║
║ 12  ║ 2000-01-01 00:00:20 ║ RADIOSHACK ║   5  ║                ║
║ 13  ║ 2000-01-01 00:00:25 ║ WALMART    ║   1  ║                ║
║ 14  ║ 2000-01-01 00:00:25 ║ BESTBUY    ║   0  ║                ║
║ 15  ║ 2000-01-01 00:00:25 ║ RADIOSHACK ║   5  ║                ║
║ 16  ║ 2000-01-01 00:00:30 ║ WALMART    ║   1  ║                ║
║ 17  ║ 2000-01-01 00:00:30 ║ BESTBUY    ║   6  ║                ║
║ 18  ║ 2000-01-01 00:00:30 ║ RADIOSHACK ║   2  ║                ║
║ 19  ║ 2000-01-01 00:00:35 ║ WALMART    ║   6  ║                ║
║ 20  ║ 2000-01-01 00:00:35 ║ BESTBUY    ║   2  ║                ║
║ 21  ║ 2000-01-01 00:00:35 ║ RADIOSHACK ║   8  ║                ║
║ 22  ║ 2000-01-01 00:00:40 ║ WALMART    ║  10  ║                ║
║ 23  ║ 2000-01-01 00:00:40 ║ BESTBUY    ║   2  ║                ║
║ 24  ║ 2000-01-01 00:00:40 ║ RADIOSHACK ║   3  ║                ║
║ 25  ║ 2000-01-01 00:00:45 ║ WALMART    ║   6  ║                ║
║ 26  ║ 2000-01-01 00:00:45 ║ BESTBUY    ║   4  ║                ║
║ 27  ║ 2000-01-01 00:00:45 ║ RADIOSHACK ║   5  ║                ║
║ 28  ║ 2000-01-01 00:00:48 ║ WALMART    ║   1  ║                ║
║ 29  ║ 2000-01-01 00:00:48 ║ BESTBUY    ║   0  ║                ║
║ 30  ║ 2000-01-01 00:00:48 ║ RADIOSHACK ║   5  ║                ║
║ 31  ║ 2000-01-01 00:00:50 ║ WALMART    ║   6  ║                ║
║ 32  ║ 2000-01-01 00:00:50 ║ BESTBUY    ║   4  ║                ║
║ 33  ║ 2000-01-01 00:00:50 ║ RADIOSHACK ║   5  ║                ║
║ 34  ║ 2000-01-01 00:00:55 ║ WALMART    ║   1  ║                ║
║ 35  ║ 2000-01-01 00:00:55 ║ BESTBUY    ║   0  ║                ║
║ 36  ║ 2000-01-01 00:00:55 ║ RADIOSHACK ║   5  ║                ║
║ 37  ║ 2000-01-01 00:01:00 ║ WALMART    ║   1  ║                ║
║ 38  ║ 2000-01-01 00:01:00 ║ BESTBUY    ║   0  ║                ║
║ 39  ║ 2000-01-01 00:01:00 ║ RADIOSHACK ║   5  ║                ║
╚═════╩═════════════════════╩════════════╩══════╩════════════════╝

我的查询是:

UPDATE my_table AS t 
INNER JOIN 
( select ID,
    (select avg(price) from my_table as t2
     where
        t2.datetime between subtime(t1.datetime, '00:14:59') and t1.datetime AND
        t2.name = t1.name
    ) as average
from my_table as t1
where
    minute(datetime) in (0,15,30,45) ) as sel
ON t.ID = sel.ID
SET 15_MIN_AVERAGE = average

我在 DATETIME 列(类型为 DATETIME)上有一个索引,但我认为在 where 子句中使用诸如 minute() 和 subtime() 之类的函数基本上会使索引无效。

我的表有大约 160 万条记录(大约每 5 分钟一条记录)。目前,运行此查询需要很长时间(超过一个小时),这是不可接受的。

你有什么优化建议?

非常感谢!

【问题讨论】:

  • 关于索引你是对的。 MySQL 索引TIPS

标签: mysql optimization


【解决方案1】:

我认为最好为此创建一个range 表。这是一个很好的例子

generate days from date range

这样的表 10 年 * 365 天 * 24 小时 * 4 个季度 = 350k 行。但索引会完美运行。

所以你的表格应该是这样的:

  id    start                 end
  1     2016-11-10 10:00:00   2016-11-10 10:04:59
  2     2016-11-10 10:05:00   2016-11-10 10:09:59
  3     2016-11-10 10:10:00   2016-11-10 10:14:59

您的查询将为每个日期时间分配和 id

 SELECT t.name, r.id, AVG(t.price)
 FROM my_table t
 JOIN range r   
   ON t.`DATETIME` BETWEEN r.start
                       AND r.end
 GROUP BY t.name, r.id

另类

  id    start                 end
  1     2016-11-10 10:00:00   2016-11-10 10:05:00
  2     2016-11-10 10:05:00   2016-11-10 10:10:00
  3     2016-11-10 10:10:00   2016-11-10 10:15:00


 SELECT t.name, r.id, AVG(t.price)
 FROM my_table t
 JOIN range r   
   ON t.`DATETIME` >= r.start AND t.`DATETIME` < r.end
 GROUP BY t.name, r.id

【讨论】:

  • 这些样本范围从一个到下一个都有一分钟的间隔。一个范围的结束点应该等于下一个范围的开始点,然后,DO NOT use BETWEEN 在连接中使用 >= 和
  • @Used_By_Already 我明白你的意思。但我看不出什么日期时间会出现差距或重叠,你能给我举个例子吗?我更喜欢这种设置,因为允许我使用BETWEEN
  • 请在您的回答中查看替代方案。在替代方案中,没有一秒钟的间隙(对不起,我的意思是早 1 秒),并且使用“之间”(包括两个端点 >= 和 sqlblog.com/blogs/aaron_bertrand/archive/2011/10/19/… 请注意 MySQL 现在支持小于 1 秒的时间单位
  • @Used_By_Already 我告诉过你我理解你的选择。只有使用 ms 时才会出现问题。但我稍后会查看 bertrand 的博客。并且不介意更新,但是如果您不喜欢我的答案,则应该将该替代方法发布为替代答案。
  • 但我确实喜欢这个概念,它只是日期范围内 1 秒的间隔会导致问题(例如,如果有人以亚秒级精度使用您的答案)pl。随意从您的答案中删除我的材料我并没有试图接管它我只是需要格式化。 (+当时正在使用手机)
【解决方案2】:

这是 Juan Carlos Oropeza 提出的范围提议的变体。我怀疑实际上只将 15 分钟的平均值存储在它自己的表中是有道理的,但在这里我已按要求应用了它。但是请注意,我不能让自己将列称为“datetime”之类的保留字,因此我使用了“priceatetime”。

有一个固有的假设,即您不需要超过 1000 个 15 分钟的间隔,如果这样做,则需要调整交叉连接的数量等,以将笛卡尔积扩展到更大的东西。

还假设这仅在添加新数据时才需要,逻辑将重新处理存储平均值为空的日期的所有行。

update table1
inner join (
    select 
           dr.start_date
         , dr.end_date
         , avg(t.price) avg_price
    from table1 t
    inner join (
          SELECT
                  (x.a + (y.b*10)+(z.c*100))+ 1 n
                , TRIM(min_date + INTERVAL 15*(x.a + (y.b*10)+(z.c*100)) MINUTE) start_date
                , TRIM(min_date + INTERVAL 15*(x.a + (y.b*10)+(z.c*100)) MINUTE) + INTERVAL 15 MINUTE end_date
          FROM (
                select 
                       cast(date(min(pricedatetime)) as datetime) min_date
                     , cast(date(max(pricedatetime)) as datetime) max_date
                from Table1 
                where 15_MIN_AVERAGE IS NULL
               ) m
          CROSS JOIN (
                    SELECT 0 AS a UNION ALL
                    SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL  
                    SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL 
                    SELECT 9
               ) x
          CROSS JOIN (
                    SELECT 0 AS b UNION ALL
                    SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL  
                    SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL 
                    SELECT 9
               ) y
          CROSS JOIN (
                    SELECT 0 AS c UNION ALL
                    SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL  
                    SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL 
                    SELECT 9
               ) z
          where TRIM(min_date + INTERVAL 15*((x.a + (y.b*10)+(z.c*100))-1) MINUTE) < max_date
        ) dr on t.pricedatetime >= dr.start_date and t.pricedatetime <  dr.end_date
    group by
           dr.start_date
         , dr.end_date
    ) g on table1.pricedatetime >= g.start_date and table1.pricedatetime < g.end_date
set `15_MIN_AVERAGE` = g.avg_price
;

请注意,我非常刻意避免使用 between。对于日期范围而言,不是一个不错的选择,因为它包括下限和上限,因此可能会重复计算行。相反,只需使用 >= 和

以上建议可作为工作演示在:http://sqlfiddle.com/#!9/299150/1

【讨论】:

    【解决方案3】:

    方案 A:升级到 MariaDB 10.2 并使用“窗口函数”来做这样的“移动平均”。

    计划 B:每 15 秒回顾表中的 15 分钟并计算当前 3 行的所有平均值。将它们(通过INSERT,而不是UPDATE)存储到单独的表中。您永远不需要重新计算它们。通过在datetime 上创建索引,您无需查看超过 180 行来进行计算。这比您需要计算下一组平均值之前的 15 秒要少得多。

    新表和旧表上都没有id。你有一个非常好的“自然”主键(name, datetime)。如果您同时需要priceaverage,您可以JOIN 将“汇总表”与原始表一起使用。

    计划 C:切换到“指数移动平均线”;计算起来要简单得多:新的平均值是

    old_average + 0.1 * (new_value - old_average)
    

    如果您希望平均值更平滑,请选择一个较小的值(小于 0.1);更大的值,使其响应更快。

    【讨论】:

      猜你喜欢
      • 2012-01-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-01
      • 2018-11-30
      相关资源
      最近更新 更多