为什么 MySQL 在强制执行此 order by 时忽略索引?

Cra*_*lus 15 mysql innodb index optimization explain

我运行一个EXPLAIN

mysql> explain select last_name from employees order by last_name;
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  
Run Code Online (Sandbox Code Playgroud)

我表中的索引:

mysql> show index from employees;  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| Table     | Non_unique | Key_name      | Seq_in_index | Column_name   | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| employees |          0 | PRIMARY       |            1 | subsidiary_id | A         |           6 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          0 | PRIMARY       |            2 | employee_id   | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          1 | idx_last_name |            1 | last_name     | A         |       10031 |      700 | NULL   |      | BTREE      |         |               |  
| employees |          1 | date_of_birth |            1 | date_of_birth | A         |       10031 |     NULL | NULL   | YES  | BTREE      |         |               |  
| employees |          1 | date_of_birth |            2 | subsidiary_id | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
5 rows in set (0.02 sec)  
Run Code Online (Sandbox Code Playgroud)

last_name 上有一个索引,但优化器不使用它。
所以我这样做:

mysql> explain select last_name from employees force index(idx_last_name) order by last_name;  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  
Run Code Online (Sandbox Code Playgroud)

仍然没有使用索引!我在这里做错了什么?
这与索引是 的事实有关NON_UNIQUE吗?顺便说一句,last_name 是VARCHAR(1000)

@RolandoMySQLDBA 请求的更新

mysql> SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;  
+---------------+  
| DistinctCount |  
+---------------+  
|         10000 |  
+---------------+  
1 row in set (0.05 sec)  


mysql> SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;  
+----------+  
| COUNT(1) |  
+----------+  
|        0 |  
+----------+  
1 row in set (0.15 sec)  
Run Code Online (Sandbox Code Playgroud)

Mic*_*bot 20

实际上,这里的问题是这看起来像一个前缀索引。我在问题中没有看到表定义,但是sub_part= 700?您还没有索引整列,因此索引不能用于排序,也不能用作覆盖索引。它只能用于查找“可能”匹配 a 的行,WHERE并且服务器层(存储引擎之上)必须进一步过滤匹配的行。姓氏真的需要 1000 个字符吗?


更新以说明:我有一个表测试表,其中有 500 多行,每行在列中包含网站的域名domain_name VARCHAR(254) NOT NULL,没有索引。

mysql> alter table keydemo add key(domain_name);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0
Run Code Online (Sandbox Code Playgroud)

索引完整列后,查询使用索引:

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table   | type  | possible_keys | key         | key_len | ref  | rows | Extra       |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
|  1 | SIMPLE      | keydemo | index | NULL          | domain_name | 764     | NULL |  541 | Using index |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
1 row in set (0.01 sec)
Run Code Online (Sandbox Code Playgroud)

所以,现在,我将删除该索引,只索引 domain_name 的前 200 个字符。

mysql> alter table keydemo drop key domain_name;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table keydemo add key(domain_name(200));
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | keydemo | ALL  | NULL          | NULL | NULL    | NULL |  541 | Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

瞧。

另请注意,200 个字符的索引比列中最长的值长...

mysql> select max(length(domain_name)) from keydemo;
+--------------------------+
| max(length(domain_name)) |
+--------------------------+
|                       43 |
+--------------------------+
1 row in set (0.04 sec)
Run Code Online (Sandbox Code Playgroud)

……但这没有任何区别。使用前缀长度声明的索引只能用于查找,不能用于排序,也不能用作覆盖索引,因为根据定义,它不包含完整的列值。

此外,上述查询是在 InnoDB 表上运行的,但在 MyISAM 表上运行它们会产生几乎相同的结果。在只有在这种情况下,不同的是,InnoDB的计数rows稍微偏离(541),而MyISAM的显示行的确切数量(563),这是正常的行为,因为这两个存储引擎处理指数潜水非常不同。

我仍然会断言 last_name 列可能比需要的要大,但如果您使用 InnoDB 并运行 MySQL 5.5 或 5.6 ,仍然可以索引整个列:

默认情况下,单列索引的索引键最多可达 767 字节。相同的长度限制适用于任何索引键前缀。见第 13.1.13 节,“CREATE INDEX语法”。例如,假设一个字符集和每个字符最多 3 个字节,您可能会在 aTEXTVARCHAR列上超过 255 个字符的列前缀索引达到此限制UTF-8。当innodb_large_prefix配置选项的功能,这个长度的限制提高到3072字节为单位InnoDB,使用该表DYNAMICCOMPRESSED行格式。

http://dev.mysql.com/doc/refman/5.5/en/innodb-restrictions.html

  • 这个答案应该是公认的。 (9认同)
  • @ypercube 这个答案比我的更准确。+1 为您的评论和 +1 为这个答案。愿这应该被接受,而不是我的。 (2认同)
  • 到目前为止,我可以报告(至少对于 5.7)前缀索引 ** 对索引 null 没有帮助,正如我在上面的评论中所要求的那样。 (2认同)

Rol*_*DBA 7

问题#1

看查询

select last_name from employees order by last_name;
Run Code Online (Sandbox Code Playgroud)

我没有看到有意义的 WHERE 子句,MySQL 查询优化器也没有。没有使用索引的动机。

问题#2

看查询

select last_name from employees force index(idx_last_name) order by last_name; 
Run Code Online (Sandbox Code Playgroud)

你给了它一个索引,但查询优化器接管了。我以前见过这种行为(如何强制 JOIN 使用 MySQL 中的特定索引?

为什么会发生这种情况?

如果没有WHERE子句,查询优化器会对自己说以下内容:

  • 这是一个 InnoDB 表
  • 这是一个索引列
  • 该索引具有gen_clust_index(又名聚集索引)row_id
  • 为什么我应该在什么时候查看索引
    • 没有WHERE条款?
    • 我总是不得不回到桌子上?
  • 由于 InnoDB 表中的所有行都与 gen_clust_index 位于相同的 16K 块中,因此我将改为进行全表扫描。

查询优化器选择了阻力最小的路径。

您会感到有点震惊,但事实就是这样:您知道查询优化器会以完全不同的方式处理 MyISAM 吗?

你可能会说 HUH ???? 如何 ????

MyISAM 将数据存储在一个.MYD文件中,并将所有索引存储在该.MYI文件中。

相同的查询将产生不同的 EXPLAIN 计划,因为索引与数据位于不同的文件中。为什么 ?原因如下:

  • 所需的数据(last_name列)已在.MYI
  • 在最坏的情况下,您将进行完整的索引扫描
  • 您只能last_name从索引访问该列
  • 你不需要筛选不需要的
  • 您不会触发临时文件创建以进行排序

怎么能这么肯定呢?我已经测试了这个关于使用不同存储将如何生成不同 EXPLAIN 计划(有时更好)的工作理论:索引是否必须覆盖所有选定的列才能用于 ORDER BY?

  • -1 @Rolando 这个答案并不比正确的 [Michael-sqlbot 的答案](http://dba.stackexchange.com/a/48104/2047) 精确,但它是错误的,例如 [manual](http://dba.stackexchange.com/a/48104/2047) /dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html) 说:“MySQL 使用索引进行这些操作:(...)如果排序或分组完成,则对表进行排序或分组可用索引的最左边前缀(...)”。此外,您帖子中的其他一些陈述也存在争议。我建议您删除此答案或重新编写它。 (2认同)