(您可能已经看过another question I have open;当我自己调查时,我会提出这个相关但也许更具体的问题,希望对此的答案能帮助我得到答案!)
考虑下表:
CREATE TABLE `Alarms` (
`AlarmId` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`DeviceId` BINARY(16) NOT NULL,
`Code` BIGINT(20) UNSIGNED NOT NULL,
`Ended` TINYINT(1) NOT NULL DEFAULT '0',
`NaturalEnd` TINYINT(1) NOT NULL DEFAULT '0',
`Pinned` TINYINT(1) NOT NULL DEFAULT '0',
`Acknowledged` TINYINT(1) NOT NULL DEFAULT '0',
`StartedAt` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
`EndedAt` TIMESTAMP NULL DEFAULT NULL,
`MarkedForDeletion` TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`AlarmId`),
KEY `Key1` (`Ended`,`Acknowledged`),
KEY `Key2` (`Pinned`),
KEY `Key3` (`DeviceId`,`Pinned`),
KEY `Key4` (`DeviceId`,`StartedAt`,`EndedAt`),
KEY `Key5` (`DeviceId`,`Ended`,`EndedAt`),
KEY `Key6` (`MarkedForDeletion`),
KEY `KeyB` (`MarkedForDeletion`,`DeviceId`,`StartedAt`,`EndedAt`,`Acknowledged`,`Pinned`)
) ENGINE=INNODB;
Key1
- Key6
来自我的原始设计,假设我能够依赖索引合并(根据我的另一个问题,这并不是似乎就是这样。)
KeyB
是我目前在调查工作中所玩的。
现在,数据库中有几十万行(所有行都符合下面列出的DeviceId
,StartedAt
和MarkedForDeletion
条件,但NOT NULL EndedAt
和Pinned
= FALSE
,因此没有实际匹配的行),只要我告诉它使用KeyB
,以下查询运行得相当快(~100ms):
SET @a = '2018-02-18 00:00:00';
SET @b = '2018-02-18 01:00:00';
SELECT * FROM `Alarms` USE INDEX (`KeyB`) WHERE
(
`Alarms`.`EndedAt` IS NULL
AND `Alarms`.`Acknowledged` = FALSE
AND `Alarms`.`StartedAt` < @b
AND `MarkedForDeletion` = FALSE
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
) OR (
`Alarms`.`EndedAt` IS NOT NULL
AND `Alarms`.`EndedAt` >= @a
AND `Alarms`.`Pinned` = TRUE
AND `Alarms`.`StartedAt` < @b
AND `MarkedForDeletion` = FALSE
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
);
查询计划:
我认为所有这些意味着它构建得尽可能快。但是,正如我们稍后会看到的那样,情况似乎并非如此。
接下来我通过创建一个临时的时间范围表,添加与以前相同的范围,然后加入到该表中来使事情变得更复杂:
DROP TABLE IF EXISTS `_ranges`;
CREATE TEMPORARY TABLE `_ranges` (
`Start` TIMESTAMP NOT NULL DEFAULT 0,
`End` TIMESTAMP NOT NULL DEFAULT 0,
PRIMARY KEY(`Start`, `End`)
);
INSERT INTO `_ranges` VALUES(@a, @b));
现在:
SELECT
UNIX_TIMESTAMP(`_ranges`.`Start`) AS `Start_TS`,
COUNT(`Alarms`.`AlarmId`) AS `n`
FROM `_ranges`
LEFT JOIN `Alarms` USE INDEX (`KeyB`) ON
(
`Alarms`.`EndedAt` IS NULL
AND `Alarms`.`Acknowledged` = FALSE
AND `Alarms`.`StartedAt` < `_ranges`.`End`
AND `MarkedForDeletion` = FALSE
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
) OR (
`Alarms`.`EndedAt` IS NOT NULL
AND `Alarms`.`EndedAt` >= `_ranges`.`Start`
AND `Alarms`.`Pinned` = TRUE
AND `Alarms`.`StartedAt` < `_ranges`.`End`
AND `MarkedForDeletion` = FALSE
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
)
GROUP BY `_ranges`.`Start`
它仍然相当不错 - 约150毫秒,对于我未经训练的眼睛,这是一个不错的查询计划:
但现在让我们再添加一些时间范围:
TRUNCATE TABLE `_ranges`;
INSERT INTO `_ranges` VALUES
(DATE_ADD(@a, INTERVAL 3600*0 SECOND), DATE_ADD(@a, INTERVAL 3600*1 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*1 SECOND), DATE_ADD(@a, INTERVAL 3600*2 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*2 SECOND), DATE_ADD(@a, INTERVAL 3600*3 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*3 SECOND), DATE_ADD(@a, INTERVAL 3600*4 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*4 SECOND), DATE_ADD(@a, INTERVAL 3600*5 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*5 SECOND), DATE_ADD(@a, INTERVAL 3600*6 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*6 SECOND), DATE_ADD(@a, INTERVAL 3600*7 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*7 SECOND), DATE_ADD(@a, INTERVAL 3600*8 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*8 SECOND), DATE_ADD(@a, INTERVAL 3600*9 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*9 SECOND), DATE_ADD(@a, INTERVAL 3600*10 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*10 SECOND), DATE_ADD(@a, INTERVAL 3600*11 SECOND)),
(DATE_ADD(@a, INTERVAL 3600*10 SECOND), DATE_ADD(@a, INTERVAL 3600*12 SECOND))
;
同样的查询现在只需要一秒钟。再次查询计划:
添加更多的时间范围逐渐减慢速度,并且每个时间段的持续时间似乎并不重要,即使这个特定情况的实际结果不会提取任何Alarms
符合条件:
在我的实际使用案例中,时间范围的选择根据需要而变化;例如,在一种情况下,我需要超过12小时的15分钟时间片,这目前需要5秒钟才能产生。在添加KeyB
之前,它需要超过30秒。
如果我稍微翻转一下查询的条件(我也需要这个条件):
SELECT
UNIX_TIMESTAMP(`_ranges`.`Start`) AS `Start_TS`,
COUNT(`Alarms`.`AlarmId`) AS `n`
FROM `_ranges`
LEFT JOIN `Alarms` USE INDEX (`KeyB`) ON
(
`Alarms`.`EndedAt` IS NULL
AND `Alarms`.`Acknowledged` = TRUE
AND `Alarms`.`StartedAt` < `_ranges`.`End`
AND `MarkedForDeletion` = FALSE
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
) OR (
`Alarms`.`EndedAt` IS NOT NULL
AND `Alarms`.`EndedAt` >= `_ranges`.`Start`
AND `Alarms`.`Pinned` = FALSE
AND `Alarms`.`StartedAt` < `_ranges`.`End`
AND `MarkedForDeletion` = FALSE
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
)
GROUP BY `_ranges`.`Start`;
...您可以看到我们现在正在匹配一些Alarms
行:
......但表现仍然差不多。
因此,我认为瓶颈与构建_ranges
的实际联接方式有关。
为什么呢?我该如何解决?
(以上在MariaDB 5.5.56 / CentOS 7上获得的查询计划,但解决方案也必须适用于MySQL 5.1.73 / CentOS 6.)