MySQL显示“ possible_keys”但不使用它

时间:2019-03-19 06:29:37

标签: mysql indexing

我有一个表,其中包含超过一百万个条目和约42列。我正在尝试在此表上运行SELECT查询,该查询需要执行一分钟。为了减少查询执行时间,我在表上添加了一个索引,但未使用该索引。

表结构如下。尽管该表有42列,但我仅在此处显示与我的查询相关的那些

CREATE TABLE `tas_usage` (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `userid` varchar(255) DEFAULT NULL,
  `companyid` varchar(255) DEFAULT NULL,
  `SERVICE` varchar(2000) DEFAULT NULL,
  `runstatus` varchar(255) DEFAULT NULL,
  `STATUS` varchar(2000) DEFAULT NULL,
  `servertime` datetime DEFAULT NULL,
  `machineId` varchar(2000) DEFAULT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=2992891 DEFAULT CHARSET=latin1

我添加的索引如下

ALTER TABLE TAS_USAGE ADD INDEX last_quarter (SERVERTIME,COMPANYID(20),MACHINEID(20),SERVICE(50),RUNSTATUS(10));

我的选择查询

EXPLAIN SELECT DISTINCT t1.COMPANYID, t1.USERID, t1.MACHINEID FROM TAS_USAGE t1 
LEFT JOIN TAS_INVALID_COMPANY INVL ON INVL.COMPANYID = t1.COMPANYID
LEFT JOIN TAS_INVALID_MACHINE INVL_MAC_ID ON INVL_MAC_ID.MACHINEID = t1.MACHINEID
WHERE t1.SERVERTIME >= '2018-10-01 00:00:00' AND t1.SERVERTIME <= '2018-12-31 00:00:00' AND 
INVL.companyId IS NULL AND INVL_MAC_ID.machineId IS NULL AND 
t1.SERVICE NOT IN ('credentialtest%', 'webupdate%') AND  
t1.RUNSTATUS NOT IN ('Failed', 'Failed Failed', 'Failed Success', 'Success Failed', '');

EXPLAIN结果如下

+----+-------------+-------------+------------+--------+-----------------------+-----------------------+---------+-----------------------------+---------+----------+------------------------------------------------+
| id | select_type | table       | partitions | type   | possible_keys         | key                   | key_len | ref                         | rows    | filtered | Extra                                          |
+----+-------------+-------------+------------+--------+-----------------------+-----------------------+---------+-----------------------------+---------+----------+------------------------------------------------+
|  1 | SIMPLE      | t1          | NULL       | ALL    | last_quarter          | NULL                  | NULL    | NULL                        | 1765296 |    15.68 | Using where; Using temporary                   |
|  1 | SIMPLE      | INVL        | NULL       | ref    | invalid_company_index | invalid_company_index | 502     | servicerunprod.t1.companyid |       1 |   100.00 | Using where; Not exists; Using index; Distinct |
|  1 | SIMPLE      | INVL_MAC_ID | NULL       | eq_ref | machineId             | machineId             | 502     | servicerunprod.t1.machineId |       1 |   100.00 | Using where; Not exists; Using index; Distinct |
+----+-------------+-------------+------------+--------+-----------------------+-----------------------+---------+-----------------------------+---------+----------+------------------------------------------------+

我的查询的解释

我想从表TAS_USAGE中选择所有记录

  1. 介于日期范围(包括)2018年10月1日至31日之间 2018年12月AND
  2. 其中没有COMPANYIDMACHINEID匹配的列 表TAS_INVALID_COMPANYTAS_INVALID_MACHINE AND
  3. 其中不包含值(“ credentialtest%”,“ webupdate%”) SERVICE列和值(“失败”,“失败失败”,“失败” RUNSTATUS列中的“成功”,“成功失败”,``)

3 个答案:

答案 0 :(得分:1)

关注日期范围,MySQL基本上有两个选择:

  1. 连续读取整个表并丢弃不适合日期范围的记录

  2. 使用索引来识别日期范围内的记录,然后使用主键在表中查找每个记录(“随机访问”)

连续读取比随机访问要快得多,但是您需要读取更多数据。在某个收支平衡点上,使用索引会变得比仅读取所有内容慢,并且MySQL认为情况就是如此。如果是的话,正确的选择将在很大程度上取决于它猜测该范围内实际有多少条记录的正确性。如果将范围缩小,则它实际上应该在某个时候使用索引。

如果您知道(或想测试)使用索引的速度更快,则可以强制MySQL将其与

一起使用
... FROM TAS_USAGE t1 force index (last_quarter) LEFT JOIN ...

您应该使用不同的范围对其进行测试,并且如果您动态生成查询,请仅在确定地确定条件后才强制使用索引(因为如果您指定一个包含所有行的范围,MySQL将不会纠正您的要求)。

有一种重要的方法可以解决对表的缓慢随机访问,尽管不幸的是,它不适用于前缀索引,但是我提到了它,以防您可以减小字段大小(或将其更改为查找/枚举)。您可以使用covering index来包含MySQL需要评估查询的每一列:

  

一个索引,其中包含查询所检索的所有列。该查询不使用索引值作为查找完整表行的指针,而是从索引结构返回值,从而节省了磁盘I / O。

如前所述,由于在前缀索引中缺少部分数据,因此不幸的是,这些列仍不能用于覆盖。

实际上,它们也根本不能使用太多,尤其是在进行随机访问之前不过滤记录,以评估whereRUNSTATUS的{​​{1}}条件,无论如何都需要完整的值。因此,您可以检查是否SERVICE非常重要-也许您的记录中有99%处于“失败”状态-在这种情况下,请为 RUNSTATUS(MySQL甚至可以自己选择该索引)。

答案 1 :(得分:1)

   WHERE  t1.SERVERTIME >= '2018-10-01 00:00:00'
     AND  t1.SERVERTIME <= '2018-12-31 00:00:00'

很奇怪。它涵盖3个月减去1天再加上1秒。建议您这样改写:

   WHERE  t1.SERVERTIME >= '2018-10-01'
     AND  t1.SERVERTIME  < '2018-10-01' + INTERVAL 3 MONTH

有多个可能的原因导致INDEX(servertime, ...)未被使用和/或即使被使用也不“有用”:

  • 如果超过20%的表涉及该日期范围,则使用索引的效率可能比仅扫描表的效率低。使用索引将涉及在索引的BTree和数据的BTree之间跳动。
  • 以“范围”开头的索引意味着将不使用索引的其余部分。
  • 索引“前缀”(foo(10))几乎没有用。

您可以做什么:

  • 标准化大多数这些字符串列。您有几台“机器”?大概没有三百万。通过用小的id替换重复的字符串(也许是2个字节的SMALLINT UNSIGNED,最大为65K)将节省此表中的大量空间。反过来,这将加快查询速度,并消除对索引前缀的需求。
  • 如果“归一化”由于实际上确实存在超过300万个不同的值而不切实际,则请查看是否缩短VARCHAR。如果您将其设置为255以下,则不再需要前缀。
  • NOT IN并不乐观。如果您可以反转测试并将其设置为IN(...),则将打开更多可能性,例如INDEX(service, runstatus, servertime)。如果您有足够新的MySQL版本,我认为优化程序将在两个IN列的索引中跳来跳去,并将索引用于时间范围。
  • NOT IN ('credentialtest%', 'webupdate%')-%是字符串的一部分吗?如果您将%用作通配符,则该构造将起作用。您将需要两个LIKE子句。

重新构造查询:

SELECT   t1.COMPANYID, t1.USERID, t1.MACHINEID
    FROM  TAS_USAGE t1
    WHERE  t1.SERVERTIME >= '2018-10-01'
      AND  t1.SERVERTIME  < '2018-10-01' + INTERVAL 3 MONTH
      AND  t1.SERVICE NOT IN ('credentialtest%', 'webupdate%')
      AND  t1.RUNSTATUS NOT IN ('Failed', 'Failed Failed',
                                'Failed Success', 'Success Failed', '')
      AND NOT EXISTS( SELECT 1 FROM  TAS_INVALID_COMPANY WHERE companyId = t1.COMPANYID )
      AND NOT EXISTS( SELECT 1 FROM  TAS_INVALID_MACHINE WHERE MACHINEID = t1.MACHINEID );

如果三人组t1.COMPANYID, t1.USERID, t1.MACHINEID是唯一的,那就摆脱DISTINCT

由于此查询仅使用6列(共42列),因此构建“覆盖”索引可能会有所帮助:

INDEX(SERVERTIME, SERVICE, RUNSTATUS, COMPANYID, USERID, MACHINEID)

这是因为查询可以完全与索引一起执行。在这种情况下,我故意将范围放在第一位。

答案 2 :(得分:0)

distinct子句会干扰索引的使用。由于无法使用索引来帮助进行区分,因此mysql完全拒绝使用索引。

如果您重新排列选择列表,索引和where子句中字段的顺序,则mysql可能决定使用它:

ALTER TABLE TAS_USAGE ADD INDEX last_quarter (COMPANYID(20),MACHINEID(20), SERVERTIME, SERVICE(50),RUNSTATUS(10));


SELECT DISTINCT t1.COMPANYID, t1.MACHINEID, t1.USERID  FROM TAS_USAGE t1 
    LEFT JOIN TAS_INVALID_COMPANY INVL ON INVL.COMPANYID = t1.COMPANYID
    LEFT JOIN TAS_INVALID_MACHINE INVL_MAC_ID ON INVL_MAC_ID.MACHINEID = t1.MACHINEID
    WHERE 
    INVL.companyId IS NULL AND INVL_MAC_ID.machineId IS NULL AND 
    t1.SERVERTIME >= '2018-10-01 00:00:00' AND t1.SERVERTIME <= '2018-12-31 00:00:00' AND
    t1.SERVICE NOT IN ('credentialtest%', 'webupdate%') AND  
    t1.RUNSTATUS NOT IN ('Failed', 'Failed Failed', 'Failed Success', 'Success Failed', '');

通过这种方式,COMPANYID, MACHINEID字段成为唯一标识符,位置和索引中最左边的字段-尽管前缀可能导致索引仍然被丢弃。您可能需要考虑减少varchar(255)字段。