【问题标题】:Improving an SQL query to get sensor events for a particular date & device type改进 SQL 查询以获取特定日期和设备类型的传感器事件
【发布时间】:2018-03-04 05:57:22
【问题描述】:

我们有一个 event mysql 表,我们在其中存储从不同类型的传感器生成的事件。下面是同一张表的创建表查询。

  CREATE TABLE `event` (
  `id` varchar(36) NOT NULL,
  `device_id` varchar(36) NOT NULL,
  `device_type` varchar(45) NOT NULL,
  `data` text NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id_UNIQUE` (`id`),
  KEY `fk_event_device_idx` (`device_id`),
  KEY `event_device_type` (`device_type`),
  KEY `event_created_at_idx` (`created_at`),
  CONSTRAINT `fk_event_device` FOREIGN KEY (`device_id`) REFERENCES `device` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

我们有一个来自 device 表的 device_id 外键,而设备表有一个来自 zone 的 zone_id 外键强>表。

我们想要获取特定 zonedevice_type(例如 THL 传感器) 在某个日期(例如 2017 年 2 月 26 日)的事件。下面是我正在运行的查询。

select e.data from event e 
left join device d on d.id = e.device_id 
where d.type = 'mdc' and d.zone_id = 'e451b2a1-5f6c-4a75-8038-30854926a9c0' and DATE(e.created_at) = '2018-03-01';

解释计划给出了相同的结果。

+----+-------------+-------+------------+------+--------------------------------------+---------------------+---------+--------------+------+----------+------------------------------------+
    | id | select_type | table | partitions | type | possible_keys                        | key                 | key_len | ref          | rows | filtered | Extra                              |
    +----+-------------+-------+------------+------+--------------------------------------+---------------------+---------+--------------+------+----------+------------------------------------+
    |  1 | SIMPLE      | d     | NULL       | ref  | PRIMARY,id_UNIQUE,fk_device_zone_idx | fk_device_zone_idx  | 110     | const        |   23 |    10.00 | Using index condition; Using where |
    |  1 | SIMPLE      | e     | NULL       | ref  | fk_event_device_idx                  | fk_event_device_idx | 110     | senzopt.d.id |  197 |   100.00 | Using where                        |
    +----+-------------+-------+------------+------+--------------------------------------+---------------------+---------+--------------+------+----------+------------------------------------+

事件表中的记录总数约为 500 万条,上述查询大约需要 1 秒的时间来执行并提供结果。我希望改善 sql 执行时间。需要相同的建议。请让我知道我能做对的所有事情。

注意:我知道我应该迁移到 NOSQL(Kafka/Cas​​sandra/Spark) 来做同样的事情。为此,我们也在并行工作。但是,我希望改进查询,以便在当前情况下更好地为我的客户服务。

【问题讨论】:

    标签: mysql


    【解决方案1】:

    以下是您的查询以更易读的格式重复:

    SELECT
        e.data
    FROM event e 
    LEFT JOIN device d
        ON d.id = e.device_id 
    WHERE
        d.type = 'mdc' AND
        d.zone_id = 'e451b2a1-5f6c-4a75-8038-30854926a9c0' AND
        DATE(e.created_at) = '2018-03-01';
    

    我们可以通过添加适当的索引来提高此查询的性能,也可以对其进行改写。

    首先,您可以在(type, zone_id) 上的device 表中创建一个复合索引。这应该有助于WHERE 子句。请注意,假设device.id 是该表的主键,它应该已经被索引,这意味着您拥有的LEFT JOIN 条件应该是最佳的。

    您还可以在event 表中的event.created_at 列上创建索引。但是为了利用它,我们不得不重写非SARGable条件WHERE DATE(e.created_at) = '2018-03-01'

    WHERE e.created_at >= '2018-03-01' AND e.created_at < '2018-03-02'
    

    以上含义相同,但没有将created_at 列包装在函数中。

    您的最终查询可能如下所示:

    SELECT
        e.data
    FROM event e 
    LEFT JOIN device d
        ON d.id = e.device_id     -- d.id already has an index
    WHERE
        d.type = 'mdc' AND        -- index (type, zone_id)
        d.zone_id = 'e451b2a1-5f6c-4a75-8038-30854926a9c0' AND   -- same index as above
        e.created_at >= '2018-03-01' AND e.created_at < '2018-03-02'
    

    【讨论】:

    • 为什么要在查询中保留LEFT 关键字? WHERE 子句中来自d 的列上的相等谓词意味着不会返回 NULL 值。结果等同于 INNER 连接。
    • @tim-biegeleisen 非常感谢!查询改进了很多。
    【解决方案2】:

    就查询而言,WHERE 子句中的谓词否定了 LEFT JOIN 的外部性。也就是说,LEFT 关键字是多余的。

    在函数中包装列会禁用 MySQL 执行范围扫描操作的能力。条件

     DATE(e.created_date) = '2018-03-01'
    

    导致 MySQL 为表中的每一行计算左侧的表达式,(或者至少是尚未被其他谓词消除的每一行),然后将结果与右侧的文字进行比较边。

    为了有效地使用索引,最好将其写成引用裸列

         e.created_date >= '2018-03-01'
     AND e.created_date <  '2018-03-01' + INTERVAL 1 DAY
    

    这样,MySQL 可以对合适的索引进行范围扫描。


    下一部分将提供一个合适的索引。鉴于此查询中的条件...device_id 上的相等性和created_date 上的范围,我们在合适索引处的第一次尝试将是

    ... ON `event` (`device_id`, `created_date`)
    

    创建该索引后,我们可以仅删除 device_id 上的冗余索引...具有 device_id 前导列的新索引足以支持外键约束。

    除非有特定原因导致多余的id_UNIQUE 索引 [on event (id)],否则我会删除它。

    不需要强制唯一性,PRIMARY KEY 约束已经做到了。当然,这可能是为边缘情况创建的,这是有益的(它是特定查询的覆盖索引。如果没有,它不是必需的,并且会拖累 DML 性能。

    DROP INDEX id_UNIQUE ON event ;
    

    对于此查询,device 表上的有益索引将是

    `ON device (zone_id, device)`
    

    我们希望 MySQL 在 Extra 列的 EXPLAIN 输出中显示“使用索引”。

    有了合适的索引,我会更清楚地编写查询,消除多余的LEFT关键字。

    SELECT e.data 
      FROM event e
    
      JOIN device d
        ON d.id = e.device_id 
       AND d.type = 'mdc'
       AND d.zone_id = 'e451b2a1-5f6c-4a75-8038-30854926a9c0'
    
     WHERE e.created_at >= '2018-03-01'
       AND e.created_at <  '2018-03-01' + INTERVAL 1 DAY 
    

    【讨论】:

    • 为什么要从查询中删除LEFT JOIN?稍后的重构可能会使您的建议不可行。
    • @TimBiegeleisen LEFT 关键字是虚假的...查询正在执行内部联接。我在查询实际执行外连接时指定外连接,而不是在执行内连接时指定外连接。在没有执行外连接时指定外连接对我来说没有意义。另外,根据过去的经验,我不会依赖 SQL 优化器来识别它是一个 INNER JOIN 并可能将自己限制为一个效率较低的计划来支持不需要的外部连接。 (我知道优化器现在比以前在 5.1 和更早版本中要好得多。但仍然......)
    • 你说得对,很好。我想主要的区别是强制左连接会强制扫描一个表而不是另一个表,而实际上扫描另一侧可能会更快。
    • @TimBiegeleisen:完全正确。但在 5.6 及更高版本中,这对优化器来说可能不是问题......它可能会将其视为 INNER 连接,但为什么要冒险呢。另一个大问题是未来的读者,避免不必要的混淆。
    • @spencer7593 非常感谢!查询改进了很多。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-30
    • 1970-01-01
    • 2021-09-15
    • 1970-01-01
    • 1970-01-01
    • 2011-07-09
    • 1970-01-01
    相关资源
    最近更新 更多