如何优化我的 mysql 请求以使用大型数据库

Posted

技术标签:

【中文标题】如何优化我的 mysql 请求以使用大型数据库【英文标题】:How to optimize my mysql request to work with large database 【发布时间】:2019-12-02 15:33:13 【问题描述】:

我正在使用 php/mysql(mariaDB) 开发一个项目,该项目使用一些数据作为输入生成统计信息。填充 DB 的系统是一个电力装置,特别是警报系统。

我的数据库有三个字段:DeviceTimeVariableNamealarmState

alarmState 字段可以有 2 种可能的状态:Normal(当警报响起时)和 Active(当警报处于活动状态时)。

我想在警报出现的时间 (DeviceTime) (Active State) 和警报关闭的时间 (DeviceTime) (Normal state) 之间进行统计

今天我提出了一个可以正常工作的请求,但仅包含少量数据。 当我用所有数据(大约 48k 行)测试请求时,请求花费的时间太长,一段时间后 mysql 崩溃。

这是我的请求,适用于少量数据

select k.deviceTime as stime, k.variableName as svar, k.alarmState as sstate, i.deviceTime, i.variableName, i.alarmState, timediff(i.deviceTime, k.deviceTime) as diff
from imports k
join imports i on i.variableName = k.variableName
                and
                  i.deviceTime = (select t.deviceTime
                                  from imports t
                                  where t.variableName = k.variableName and
                                          t.deviceTime > k.deviceTime and
                                          t.alarmState ='NORMAL'
                                  order by t.deviceTime limit 1
                  )

where k.alarmState = 'ACTIVE'

这是我的数据表:

id  deviceTime              variableName            alarmState
1   2019-07-11T10:05:24.482 B1.d_07QFA11AN001XB08   ACTIVE
2   2019-07-11T10:05:24.282 B1.d_07QFA11AN001XB08   NORMAL
3   2019-07-11T10:05:15.409 G1.PTUR-38-T.228.52     ACTIVE
4   2019-07-11T10:03:51.409 G1.PTUR-38-T.228.52     NORMAL
5   2019-07-11T10:03:37.409 G1.PTUR-38-T.228.52     ACTIVE
6   2019-07-11T10:03:09.409 G1.PTUR-38-T.228.52     NORMAL
7   2019-07-11T10:02:55.409 G1.PTUR-38-T.228.52     ACTIVE
8   2019-07-11T09:52:06.415 B1.d_07QFA11AN001XB08   ACTIVE
9   2019-07-11T09:52:06.214 B1.d_07QFA11AN001XB08   NORMAL
10  2019-07-11T09:51:06.403 B1.d_07QFA11AN001XB08   ACTIVE

数据量少的结果:

stime                   svar                    sstate  deviceTime              variableName            alarmState  diff
2019-07-11T09:52:06.415 B1.d_07QFA11AN001XB08   ACTIVE  2019-07-11T10:05:24.282 B1.d_07QFA11AN001XB08   NORMAL      00:13:17
2019-07-11T10:03:37.409 G1.PTUR-38-T.228.52     ACTIVE  2019-07-11T10:03:51.409 G1.PTUR-38-T.228.52     NORMAL      00:00:14
2019-07-11T10:02:55.409 G1.PTUR-38-T.228.52     ACTIVE  2019-07-11T10:03:09.409 G1.PTUR-38-T.228.52     NORMAL      00:00:14
2019-07-11T09:51:06.403 B1.d_07QFA11AN001XB08   ACTIVE  2019-07-11T09:52:06.214 B1.d_07QFA11AN001XB08   NORMAL      00:00:59

这正是我想要的结果,但是如果有人有优化这个请求的想法,或者另一种方法来构建一个可以返回alarmState 和对应的variableName 之间的时间差的请求。

编辑: 我的 MariaDB 版本是10.4.6-MariaDB

这是表结构

create table imports
(
    id           bigint unsigned auto_increment
        primary key,
    deviceTime   varchar(255) not null,
    variableName varchar(255) not null,
    alarmState   varchar(255) null,
    created_at   timestamp    null,
    updated_at   timestamp    null
);

还有Explain query

id  select_type         table       type    possible_key    key     key_len     ref     rows        Extra
1   PRIMARY             k           ALL     <null>          <null>  <null>      <null>  44679       Using where; Using temporary; Using filesort
1   PRIMARY             i           ALL     <null>          <null>  <null>      <null>  44679       Using where; Using join buffer (flat, BNL join)
2   DEPENDENT SUBQUERY  t           ALL     <null>          <null>  <null>      <null>  44679       Using where; Using filesort

编辑2 我将deviceTime 列的类型更改为DATETIME。我创建了这样的索引

create index imports_alarmstate_index
    on imports (alarmState);

create index imports_devicetime_index
    on imports (deviceTime);

create index imports_variablename_index
    on imports (variableName);

我将查询修改为使用MIN() 而不是mysql Order BY ... Limit 1。 现在我的查询看起来像

select k.deviceTime     as stime,
       k.variableName   as svar,
       k.alarmState     as sstate,
       i.deviceTime,
       i.variableName,
       i.alarmState,
       timestampdiff (second, i.deviceTime, k.deviceTime) as diff
from imports k
join imports i on   i.variableName = k.variableName and
                    i.deviceTime = (select MIN(t.deviceTime)
                                    from imports t
                                    where   t.variableName = k.variableName and
                                            t.deviceTime > k.deviceTime and
                                            t.alarmState ='NORMAL'
                                    )
where k.alarmState <> 'NORMAL' 

我使用timestampdiff() 而不是datediff(),因为时间戳格式更易于订购。 我的 where 条件 k.alarmState &lt;&gt; 'NORMAL' 发生了变化,因为有时 alarmState 可以在特定条件下采用另一种状态,但是这种新状态就像 Active 状态

这是我的新EXPLAIN

id  select_type         table   type    possible_keys                                                                   key                         key_len     ref                     rows    filtered    Extra
1   PRIMARY             k       ALL     imports_variablename_index,imports_alarmstate_index                             <null>                      <null>      <null>                  45474   50          Using where
1   PRIMARY             i       ref     imports_devicetime_index,imports_variablename_index                             imports_devicetime_index    5           func                    1       100         Using where
2   DEPENDENT SUBQUERY  t       ref     imports_devicetime_index,imports_variablename_index,imports_alarmstate_index    imports_variablename_index  1022        Alarms.k.variableName   29      50          Using where

当我执行查询时,我得到了

34567 rows retrieved starting from 1 in 3 m 26 s 135 ms (execution: 158 ms, fetching: 3 m 25 s 977 ms)

我觉得 3 分钟有点长,不是吗?还有其他优化或建议吗?

谢谢!

【问题讨论】:

哪个 MySQL/MariaDB 版本? SELECT VERSION(); 另外关于性能的问题还应该包括查询中涉及的每个表的表结构(SHOW CREATE TABLE table)。还有一个EXPLAIN query输出 看起来像一个“groupwise-max”问题。请参阅添加的标签进行优化。 如前所述,为所有 3 列添加覆盖索引(即,1 个索引包含所有 3 列)。通常 MySQL 不会组合单独的索引,因此您需要 1 个索引来覆盖所有列。并且删除子查询可能会有很大帮助(它必须为它尝试加入的每一行执行该子查询)。 【参考方案1】:

索引有很大的不同,

但是有可能在没有子查询的情况下重写您的查询。

如果我正确地阅读了你的 SQL,这样的事情会起作用。

SELECT k.deviceTime as stime,
        k.variableName as svar,
        k.alarmState as sstate,
        i.deviceTime,
        i.variableName,
        i.alarmState,
        timediff(i.deviceTime, k.deviceTime) as diff
FROM imports k
INNER JOIN imports i
ON i.variableName = k.variableName
AND i.deviceTime > k.deviceTime
AND t.alarmState ='NORMAL'
LEFT OUTER JOIN imports t
ON t.variableName = k.variableName
AND t.deviceTime > k.deviceTime
AND t.deviceTime < i.deviceTime
AND t.alarmState ='NORMAL'
WHERE k.alarmState = 'ACTIVE'
AND t.id IS NULL

这是一个连接以查找未来某个时间的同一变量的未来导入,以及一个 OUTER JOIN 以查找介于“活动”和“正常”警报时间之间的任何内容。然后在 OUTER JOIN 中找到任何结果的行将被忽略。

【讨论】:

感谢您的询问,但表现不佳。执行时间很长(如 30 分钟 +)。我不知道为什么。我还使索引和查询工作得更好,但还没有 @Cripsii - 你添加了哪些索引?字段警报状态、变量名和设备时间(按此顺序)的覆盖索引可能会产生很大的不同。【参考方案2】:

尝试在(alarmState, variableName, deviceTime) 上创建复合索引。

为什么?你有这个 WHERE 子句

       where t.variableName = k.variableName
         and t.deviceTime > k.deviceTime
         and t.alarmState ='NORMAL'

这会在variableNamealarmState 上进行相等匹配,然后在deviceTime 上进行范围匹配。相等匹配首先出现在索引中,然后是范围。

并且,尝试重构您的依赖子查询以使用 MIN() 而不是 ORDER BY ... LIMIT 1 像这样:

                            select MIN(t.deviceTime)
                              from imports t
                              where t.variableName = k.variableName and
                                      t.deviceTime > k.deviceTime and
                                      t.alarmState ='NORMAL'

那可以使用所谓的loose index scan来查找下一次。

编辑从您的编辑中我看到您的deviceTime 列是varchar(255)。对于您正在尝试做的事情,这是非常低效的。您可以改用TIMESTAMPDATETIME 数据类型吗?

【讨论】:

以上是关于如何优化我的 mysql 请求以使用大型数据库的主要内容,如果未能解决你的问题,请参考以下文章

如何在 PHP 的 foreach 循环中优化大型 mySQL?

优化解决方案以在大型数据集上找到共同的第三个

《大型网站技术架构》--第三章:大型网站核心架构要素

大型网站MySQL深度优化揭秘2

如何防止大型 MySQL 导入的连接超时

如何优化大型数据集的查询?