复合索引如何工作以及在什么情况下它们会失败?

How do composite indexes work and under what circumstances will they fail?

在建索引的时候,对复合索引有一些疑问

这是我的 SHOW CREATE TABLE

CREATE TABLE `sys_alarm`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `dept_id` bigint(20) NULL DEFAULT NULL,
  `device_code` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr_value` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `content` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
  PRIMARY KEY (`alarm_id`) USING BTREE,
  INDEX `search`(`dept_id`, `device_code`, `attr_name`, `create_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16610 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;

当我执行下面的语句时,结果让我很困惑:

EXPLAIN
select a.alarm_id, a.dept_id, a.device_code, a.type, a.attr_name, a.attr_value, a.content, a.create_time
FROM sys_alarm a
WHERE a.dept_id = 214 
and a.create_time > "2021-11-06 15:00:17"

EXPLAIN
select a.alarm_id, a.dept_id, a.device_code, a.type, a.attr_name, a.attr_value, a.content, a.create_time
FROM sys_alarm a
WHERE a.create_time > "2021-11-06 15:00:17"
and a.dept_id = 214 

结果:

id  select_type table  partitions   type    possible_keys   key     key_len ref    rows  filtered    Extra
1   SIMPLE      a       null        ref     search          search  9       const   1    33.33 Using index condition

我在MySQL documentation上看了这句话:

MySQL cannot use the index to perform lookups if the columns do not form a leftmost prefix of the index. Suppose that you have the SELECT statements shown here

那么,为什么复合索引仍然有效?只有我去掉dept_id查询条件后,索引才会fail.What是复合索引失效的条件吗?

MySQL 将只使用“最左边的前缀”意味着它将只使用索引的 dept_id 部分,因为它不能跳到 create_time。 EXPLAIN 输出中 key_len 为 9 的事实说明了这一点。我有点困惑,因为我希望它是 8(BIGINT 的字节长度)。我的理解在这里有一个漏洞。希望有人会在评论中解释这一点。

如果你 运行 你的 EXPLAIN 有和没有 create_time 条件你应该看到相同的结果 -

EXPLAIN SELECT `a`.`alarm_id`, `a`.`dept_id`, `a`.`device_code`, `a`.`type`, `a`.`attr_name`, `a`.`attr_value`, `a`.`content`, `a`.`create_time`
FROM `sys_alarm` `a`
WHERE `a`.`dept_id` = 214;

EXPLAIN SELECT `a`.`alarm_id`, `a`.`dept_id`, `a`.`device_code`, `a`.`type`, `a`.`attr_name`, `a`.`attr_value`, `a`.`content`, `a`.`create_time`
FROM `sys_alarm` `a`
WHERE `a`.`dept_id` = 214
AND `a`.`create_time` > '2021-11-06 15:00:17';
# id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE a ref search search 9 const 402 100.00

如果您修改查询以添加 device_code 的条件,您应该会看到 key_len 大小更改为 332,稍后我会回来讨论这个问题。

EXPLAIN
SELECT `a`.`alarm_id`, `a`.`dept_id`, `a`.`device_code`, `a`.`type`, `a`.`attr_name`, `a`.`attr_value`, `a`.`content`, `a`.`create_time`
FROM `sys_alarm` `a`
WHERE `a`.`dept_id` = 214
AND `a`.`device_code` = 'ABCDE';
# id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE a ref search search 332 const const 1 100.00

并添加attr_name-

EXPLAIN
SELECT `a`.`alarm_id`, `a`.`dept_id`, `a`.`device_code`, `a`.`type`, `a`.`attr_name`, `a`.`attr_value`, `a`.`content`, `a`.`create_time`
FROM `sys_alarm` `a`
WHERE `a`.`dept_id` = 214
AND `a`.`device_code` = 'ABCDE'
AND `a`.`attr_name` = 'dsgf';
# id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE a ref search search 535 const const const 1 100.00

最后加回去 create_time -

EXPLAIN
SELECT `a`.`alarm_id`, `a`.`dept_id`, `a`.`device_code`, `a`.`type`, `a`.`attr_name`, `a`.`attr_value`, `a`.`content`, `a`.`create_time`
FROM `sys_alarm` `a`
WHERE `a`.`dept_id` = 214
AND `a`.`device_code` = 'ABCDE'
AND `a`.`attr_name` = 'dsgf'
AND `a`.`create_time` > '2021-11-06 15:00:17';
# id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE a range search search 539 1 100.00 Using index condition

现在回到 key_len 从 9 到 332 的变化。当仅使用 dept_id 时,它只是 BIGINT。一旦添加 device_code,它就会跳转到 332。我不是 100% 确定为什么是 332,但它类似于 - 80(varchar 长度)* 4(由于多字节字符集,每个字符的字节数)= 320 .

ROW_FORMAT = Compact 的使用表明您很清楚数据大小,但您正在使用 BIGINT(8 字节)来存储 dept_id。你真的需要9,223,372,036,854,775,807部门吗?

WHERE 中子句的顺序无关紧要。 INDEX 中的列顺序很重要。

您的两个查询和 nnichols 的前两个查询都会受益于 INDEX(dept_id, create_time)

  • dept_id= 测试,所以它应该排在第一位。
  • create_time被一个'range'测试过,所以应该是最后一个。

我会在 http://mysql.rjweb.org/doc.php/index_cookbook_mysql

中讨论更多细节

nnichols 继续有多个 = 子句,加上一个 > 测试。使用 = 测试的列应该以任何顺序在复合 INDEX 中排在第一位,然后是 create_time.

Key_len 和基数在选择如何排列索引中的列时很重要。

我同意 BIGINT 很少 需要。 BIGINT NOT NULL 在 Explain 中显示为 8; BIGINT NULL显示为9,即神秘的1是因为NULLable。 (这没什么大不了的,只是令人困惑。)同时,DATETIME 将显示为 9 或 10 个字节,具体取决于 NOT NULL.

另一个注意事项:如果您同时拥有 INDEX(dept_id)INDEX(dept_id, create_time),优化器可能会选择前者,即使后者显然更好。所以放弃前一个索引并保留后者。