我有一个表,其中包含超过一百万个条目和约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
中选择所有记录
COMPANYID
和MACHINEID
匹配的列
表TAS_INVALID_COMPANY
和TAS_INVALID_MACHINE
AND SERVICE
列和值(“失败”,“失败失败”,“失败”
RUNSTATUS
列中的“成功”,“成功失败”,``)答案 0 :(得分:1)
关注日期范围,MySQL基本上有两个选择:
连续读取整个表并丢弃不适合日期范围的记录
使用索引来识别日期范围内的记录,然后使用主键在表中查找每个记录(“随机访问”)
连续读取比随机访问要快得多,但是您需要读取更多数据。在某个收支平衡点上,使用索引会变得比仅读取所有内容慢,并且MySQL认为情况就是如此。如果是的话,正确的选择将在很大程度上取决于它猜测该范围内实际有多少条记录的正确性。如果将范围缩小,则它实际上应该在某个时候使用索引。
如果您知道(或想测试)使用索引的速度更快,则可以强制MySQL将其与
一起使用... FROM TAS_USAGE t1 force index (last_quarter) LEFT JOIN ...
您应该使用不同的范围对其进行测试,并且如果您动态生成查询,请仅在确定地确定条件后才强制使用索引(因为如果您指定一个包含所有行的范围,MySQL将不会纠正您的要求)。>
有一种重要的方法可以解决对表的缓慢随机访问,尽管不幸的是,它不适用于前缀索引,但是我提到了它,以防您可以减小字段大小(或将其更改为查找/枚举)。您可以使用covering index来包含MySQL需要评估查询的每一列:
一个索引,其中包含查询所检索的所有列。该查询不使用索引值作为查找完整表行的指针,而是从索引结构返回值,从而节省了磁盘I / O。
如前所述,由于在前缀索引中缺少部分数据,因此不幸的是,这些列仍不能用于覆盖。
实际上,它们也根本不能使用太多,尤其是在进行随机访问之前不过滤记录,以评估where
或RUNSTATUS
的{{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, ...)
未被使用和/或即使被使用也不“有用”:
foo(10)
)几乎没有用。您可以做什么:
SMALLINT UNSIGNED
,最大为65K)将节省此表中的大量空间。反过来,这将加快查询速度,并消除对索引前缀的需求。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)
字段。