为什么MySQL甚至对这个顺序强制也忽略索引?


14

我运行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)  

我表中的索引:

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)  

在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)  

但是仍然没有使用索引!我在这里做错了什么?
它是否与索引为事实有关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)  

请运行以下两个查询:1)SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;2)SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;。每次计数的结果是什么?
RolandoMySQLDBA

@RolandoMySQLDBA:我用您要求的信息更新了OP。
Cratylus

请再查询两个:1)SELECT COUNT(1) FullTableCount FROM employees;和2)SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;
RolandoMySQLDBA

没关系,我看到了我需要的解释。
RolandoMySQLDBA

2
@Cratylus您接受了错误的答案,您应该接受Michael-sqlbot
miracle173

Answers:


6

问题1

看一下查询

select last_name from employees order by last_name;

我没有看到有意义的WHERE子句,MySQL Query Optimizer也没有。没有使用索引的动机。

问题2

看一下查询

select last_name from employees force index(idx_last_name) order by last_name; 

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

为什么会发生这种情况?

如果没有WHERE子句,Query Optimizer会对自己说以下内容:

  • 这是一个InnoDB表
  • 这是一个索引列
  • 索引具有gen_clust_index的row_id(也称为聚簇索引)
  • 为什么我应该在什么时候查看索引
    • 没有WHERE子句?
    • 我总是必须弹回桌子上吗?
  • 由于InnoDB表中的所有行与gen_clust_index都位于相同的16K块中,因此我将进行全表扫描。

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

您可能会有些震惊,但是事情就这样了:您是否知道查询优化器将以不同的方式处理MyISAM?

您可能在说HU?怎么样 ????

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

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

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

如何确定这一点?我已经测试了这种工作原理,即如何使用不同的存储将如何生成不同的EXPLAIN计划(有时是更好的计划):索引是否必须覆盖所有选定的列才能用于ORDER BY?


1
-1 @Rolando这个答案的准确性并不比Michael-sqlbot的正确答案要精确但是它是错误的,例如,手册说:“ MySQL对这些操作使用索引:(...)对表进行排序或分组,如果排序或分组是在可用索引(...)的最左边的前缀上完成的。此外,您的帖子中的其他一些陈述也是有争议的。我建议您删除此答案或对其进行重新处理。
miracle173

这个答案是不正确的。如果没有排序,即使没有WHERE子句,索引仍然可以使用。
oysteing

19

实际上,这里的问题是,它看起来像一个前缀索引。我没有在问题中看到表定义,但是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

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

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)

因此,现在,我将删除该索引,仅索引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>

瞧。

另请注意,索引(200个字符)比列中的最长值更长。

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

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

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

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

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

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


有趣的观点。专栏是,varchar(1000)但这超出了索引允许的最大上限
〜750

8
这个答案应该是被接受的。
ypercubeᵀᴹ

1
@ypercube这个答案比我的答案更精确。+1表示您的评论,+ 1表示此答案。希望这可以代替我接受。
RolandoMySQLDBA 2013年

1
@Timo,这是一个有趣的问题...我建议将其作为新问题发布在这里,也许带有指向该答案的链接以供参考。从发布的完整输出EXPLAIN SELECT ...,以及SHOW CREATE TABLE ...SELECT @@VERSION;因为不同版本进行修改优化可能是相关的。
Michael-sqlbot

1
到目前为止,我可以报告(至少在5.7中)前缀索引索引null 毫无帮助,正如我在上面的评论中所要求的那样。
蒂莫

2

我做了一个答案,因为注释不支持格式,RolandoMySQL DBA谈到了gen_clust_index和innodb。这对于基于innodb的表非常重要。这比一般的DBA知识更进一步,因为您需要能够分析C代码。

如果您使用的是Innodb,则应该始终创建一个PRIMARY KEY或UNIQUE KEY。如果您不这样做,那么innodb将使用它自己生成的ROW_ID,这可能弊大于利。

由于证明基于C代码,因此我将尝试对其进行简单解释。

/**********************************************************************//**
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t    id;

    mutex_enter(&(dict_sys->mutex));

    id = dict_sys->row_id;

    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
          dict_hdr_flush_row_id();
    }

    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

第一个问题

Mutex_enter(&(dict_sys-> mutex));

这一行确保只有一个线程可以同时访问dict_sys-> mutex。如果已经对该值进行了互斥,该怎么办...是的,线程必须等待,这样您将获得诸如线程锁定之类的不错的随机功能,或者如果您有更多的表没有自己的PRIMARY KEY或UNIQUE KEY,那么您将拥有一个不错的功能innodb' 表锁定 '不是这不是MyISAM被InnoDB取代的原因,因为它有一个不错的功能,那就是基于记录/行的锁定。

第二个问题

(0 ==(id%DICT_HDR_ROW_ID_WRITE_MARGIN))

如果您要进行批量插入,则模(%)的计算速度会很慢,因为每次都需要重新计算...,并且DICT_HDR_ROW_ID_WRITE_MARGIN(值为256)是2的幂,因此可以更快地进行计算。

(0 ==(id&(DICT_HDR_ROW_ID_WRITE_MARGIN-1)))

旁注:如果将C编译器配置为进行优化并且它是一个很好的优化器,则C优化器会将“大量”代码修复为较轻的版本

故事的座右铭始终是创建自己的PRIMARY KEY,或者从头开始创建表时请确保您具有UNIQUE索引


添加基于行的复制,以及行ID在服务器之间不一致的事实,而Raymond关于始终创建主键的观点更为重要。

请不要认为这UNIQUE是足够的-它也只需要包含非NULL列即可将唯一索引提升为PK。
瑞克·詹姆斯

“模数(%)计算很慢”-更重要的INSERT是,此功能所花费的时间百分比是多少。我怀疑是微不足道的。对比一下铲除列的工作,执行BTree操作(包括偶尔的块分割,buffer_pool上的各种互斥锁,更改缓冲区的内容等)的情况
Rick James,

True @RickJames,开销可能很小,但很多小的数字也加起来(仍然是微优化)。除了第一个问题,最大的麻烦是某些
Raymond Nijland
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.