查询花了将近两秒但只匹配两行 - 为什么索引没有帮助?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了查询花了将近两秒但只匹配两行 - 为什么索引没有帮助?相关的知识,希望对你有一定的参考价值。

表:

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;

它目前有大约300万行。

查询:

SELECT
COUNT(`AlarmId`) AS `n`
FROM `Alarms`
WHERE `StartedAt` < FROM_UNIXTIME(1519101900)
AND (`EndedAt` IS NULL OR `EndedAt` > FROM_UNIXTIME(1519101900))
AND `DeviceId` = UNHEX('00030000000000000000000000000000')
AND `MarkedForDeletion` = FALSE
AND (
     (`Alarms`.`EndedAt` IS NULL AND `Alarms`.`Acknowledged` = FALSE)
  OR ( `Alarms`.`EndedAt` IS NOT NULL AND `Alarms`.`Pinned` = TRUE)
)

查询计划:

id      select_type     table   type    possible_keys   key     key_len ref     rows    Extra
1       SIMPLE  Alarms  range   Key2,Key3,Key4,Key5,Key6,KeyB   KeyB    21              1574778 Using where; Using index

经过时间:1,763,222μs

在这种特殊情况下,查询(正确)甚至不匹配许多行(结果是n = 2)。

从我使用索引合并学到的东西(尽管我还没有那么正确),我尝试重新组织条件(原始是由一些C ++生成的,基于输入条件,因此奇怪的运算符分布):

SELECT COUNT(`AlarmId`) AS `n`
FROM `Alarms`
WHERE 
(
    `EndedAt` IS NULL
    AND `Acknowledged` = FALSE

    AND `StartedAt` < FROM_UNIXTIME(1519101900)
    AND `MarkedForDeletion` = FALSE
    AND `DeviceId` = UNHEX('00030000000000000000000000000000')
) OR (
    `EndedAt` > FROM_UNIXTIME(1519101900)
    AND `Pinned` = TRUE

    AND `StartedAt` < FROM_UNIXTIME(1519101900)
    AND `MarkedForDeletion` = FALSE
    AND `DeviceId` = UNHEX('00030000000000000000000000000000')
);

......但结果是一样的。

那为什么需要这么长时间?如何修改它/索引以使其立即生效?

答案
  • 众所周知,OR难以优化。
  • mysql几乎从不在单个查询中使用两个索引。

为了避免这两者,将OR变成UNION。每个SELECT都可以使用不同的索引。因此,为每个人建立一个最佳的INDEX

实际上,既然你只做COUNT,你也可以评估两个单独的计数并添加它们。

SELECT ( SELECT  COUNT(*)
            FROM  `Alarms`
            WHERE  `EndedAt` IS NULL
              AND  `Acknowledged` = FALSE
              AND  `StartedAt` < FROM_UNIXTIME(1519101900)
              AND  `MarkedForDeletion` = FALSE
              AND  `DeviceId` = UNHEX('00030000000000000000000000000000' )
       ) + 
       ( SELECT  COUNT(*)
            FROM  `Alarms`
            WHERE  `EndedAt` > FROM_UNIXTIME(1519101900)
              AND  `Pinned` = TRUE
              AND  `StartedAt` < FROM_UNIXTIME(1519101900)
              AND  `MarkedForDeletion` = FALSE
              AND  `DeviceId` = UNHEX('00030000000000000000000000000000')
       ) AS `n`;

INDEX(DeviceId, Acknowledged, MarkedForDeletion, EndedAt, StartedAt) -- for first
INDEX(DeviceId, Pinned, MarkedForDeletion, EndedAt, StartedAt) -- for second
INDEX(DeviceId, Pinned, MarkedForDeletion, StartedAt, EndedAt) -- for second

好吧,如果有重叠,这将无效。那么,让我们回到UNION模式:

SELECT  COUNT(*) AS `n`
    FROM
    (
        ( SELECT  AlarmId
            FROM  `Alarms`
            WHERE  `EndedAt` IS NULL
              AND  `Acknowledged` = FALSE
              AND  `StartedAt` < FROM_UNIXTIME(1519101900)
              AND  `MarkedForDeletion` = FALSE
              AND  `DeviceId` = UNHEX('00030000000000000000000000000000')  
        )
        UNION DISTINCT
        ( SELECT  AlarmId
            FROM  `Alarms`
            WHERE  `EndedAt` > FROM_UNIXTIME(1519101900)
              AND  `Pinned` = TRUE
              AND  `StartedAt` < FROM_UNIXTIME(1519101900)
              AND  `MarkedForDeletion` = FALSE
              AND  `DeviceId` = UNHEX('00030000000000000000000000000000')
         )
    );

再次,添加这些索引。

每个INDEX中的前几列可以按任何顺序排列,因为它们是使用=(或IS NULL)测试的。最后一两个是“范围”测试。只有第一个范围用于过滤,但我包含了另一个列,因此索引将是“覆盖”。

我的配方可能比“索引合并”更好。

另一答案

我认为问题在于我试图在索引中途使用范围条件。

我添加了一个键:

(`MarkedForDeletion`,`DeviceId`,`Acknowledged`,`Ended`,`StartedAt`)

然后将查询重写为:

SELECT COUNT(`AlarmId`) AS `n`
FROM `Alarms`
WHERE 
(
    `Ended` = FALSE
    AND `Acknowledged` = FALSE

    AND `StartedAt` < FROM_UNIXTIME(1519101900)
    AND `MarkedForDeletion` = FALSE
    AND `DeviceId` = UNHEX('00030000000000000000000000000000')
) OR (
    `EndedAt` > FROM_UNIXTIME(1519101900)
    AND `Pinned` = TRUE

    AND `StartedAt` < FROM_UNIXTIME(1519101900)
    AND `MarkedForDeletion` = FALSE
    AND `DeviceId` = UNHEX('00030000000000000000000000000000')
);

现在我得到一个索引合并,查询是即时的。

Query plan after fix

以上是关于查询花了将近两秒但只匹配两行 - 为什么索引没有帮助?的主要内容,如果未能解决你的问题,请参考以下文章

四、ES基于索引的基本操作

索引了 1500 万客户,但查询仍然需要将近 5 分钟才能返回结果

MySQL 之全文索引

实战mysql分区

联合索引的最左前缀匹配原则

mergeChangesFromContextDidSaveNotification 花了将近一分钟