索引是否必须覆盖所有选定的列才能用于 ORDER BY?

egg*_*yal 16 mysql innodb index order-by

在 SO,最近有人问为什么 ORDER BY 不使用索引?

这种情况涉及 MySQL 中的一个简单的 InnoDB 表,包括三列和 10k 行。其中一列,一个整数,被索引——OP 试图检索他在该列上排序的整个表:

SELECT * FROM person ORDER BY age
Run Code Online (Sandbox Code Playgroud)

他附上了EXPLAIN输出,显示这个查询是用 a filesort(而不是索引)解决的,并询问为什么会这样。

尽管提示 FORCE INDEX FOR ORDER BY (age) 导致使用索引,但有人回答(通过其他人的支持评论/赞成)索引仅用于在所选列都从索引中读取时进行排序(即通常Using indexExtra列中指示的EXPLAIN输出)。后来给出了一个解释,遍历索引然后从表中获取列会导致随机 I/O,MySQL 认为这比filesort.

这似乎与关于ORDER BY优化的手册章节背道而驰,它不仅传达了强烈的印象,即满足ORDER BY索引比执行额外排序更可取(实际上,filesort是快速排序和归并排序的组合,因此 必须有一个下限; 虽然按顺序遍历索引并寻找表应该是 - 所以这是完全有道理的),但它也忽略了这种所谓的“优化”,同时还说明了:Ω(nlog n)O(n)

以下查询使用索引来解析ORDER BY部分:

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;
Run Code Online (Sandbox Code Playgroud)

根据我的阅读,在这种情况下正是这种情况(但没有明确提示就不会使用索引)。

我的问题是:

  • 为了让 MySQL 选择使用索引,是否确实需要对所有选定的列进行索引?

    • 如果是这样,这是在哪里记录的(如果有的话)?

    • 如果不是,这里发生了什么?

Rol*_*DBA 14

为了让 MySQL 选择使用索引,是否确实需要对所有选定的列进行索引?

这是一个沉重的问题,因为决定索引是否值得使用的因素有很多。

因素#1

对于任何给定的索引,关键人群是什么?换句话说,索引中记录的所有元组的基数(不同计数)是多少?

因素#2

你用的是什么存储引擎?是否可以从索引访问所有需要的列?

下一步是什么 ???

让我们举一个简单的例子:一个包含两个值(男和女)的表

让我们创建一个这样的表来测试索引使用情况

USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
    id int not null auto_increment,
    gender char(1),
    primary key (id),
    key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';
Run Code Online (Sandbox Code Playgroud)

测试 InnoDB

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

测试 MyISAM

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | mf    | ALL  | gender        | NULL | NULL    | NULL |   40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

InnoDB 分析

当数据作为 InnoDB 加载时,请注意所有四个EXPLAIN计划都使用了gender索引。第三个和第四个EXPLAIN计划使用了gender索引,即使请求的数据是id. 为什么?因为idis inPRIMARY KEY和所有二级索引都有引用指针回到PRIMARY KEY(通过gen_clust_index)。

MyISAM 分析

当数据作为 MyISAM 加载时,请注意前三个EXPLAIN计划使用了gender索引。在第四个EXPLAIN计划中,查询优化器决定根本不使用索引。它选择了全表扫描。为什么?

无论 DBMS 是什么,查询优化器都按照一个非常简单的经验法则进行操作:如果一个索引被筛选为用于执行查找的候选索引,并且查询优化器计算出它必须查找超过总数的 5%表中的行:

  • 如果检索所需的所有列都在所选索引中,则完成完整索引扫描
  • 否则进行全表扫描

结论

如果您没有适当的覆盖索引,或者任何给定元组的键总体超过表的 5%,则必须发生六件事:

  1. 意识到您必须对查询进行概要分析
  2. 从这些查询中查找所有WHEREGROUP BY和 ORDER BY` 子句
  3. 按此顺序制定索引
    • WHERE 具有静态值的子句列
    • GROUP BY
    • ORDER BY
  4. 避免全表扫描(缺少合理WHERE子句的查询)
  5. 避免坏键群(或至少缓存那些坏键群)
  6. 为表决定最好的 MySQL 存储引擎(InnoDBMyISAM

我过去写过关于这个 5% 的经验法则:

更新 2012-11-14 13:05 EDT

我回顾了你的问题和原来的 SO 帖子。然后,我想到了我Analysis for InnoDB之前提到的。它与person桌子重合。为什么?

对于两个表mfperson

  • 存储引擎是 InnoDB
  • 主键是 id
  • 表访问是通过二级索引
  • 如果 table 是 MyISAM,我们会看到一个完全不同的EXPLAIN计划

现在,看看来自 SO question 的查询:select * from person order by age\G。由于没有WHERE子句,您明确要求进行全表扫描。表的默认排序顺序是id(PRIMARY KEY),因为它的 auto_increment 和gen_clust_index(又名聚集索引)按内部 rowid 排序。当您按索引排序时,请记住 InnoDB 二级索引将 rowid 附加到每个索引条目。这会产生每次对全行访问的内部需求。

ORDER BY如果您忽略有关 InnoDB 索引组织方式的这些事实,则在 InnoDB 表上进行设置可能是一项相当艰巨的任务。

回到那个 SO 查询,因为您明确要求进行全表扫描,恕我直言,MySQL 查询优化器做了正确的事情(或者至少选择了阻力最小的路径)。当涉及到 InnoDB 和 SO 查询时,执行全表扫描,然后执行一些,filesort而不是通过 gen_clust_index 为每个二级索引条目执行全索引扫描和行查找要容易得多。

我不提倡使用索引提示,因为它忽略了 EXPLAIN 计划。尽管如此,如果你真的比 InnoDB 更了解你的数据,你将不得不求助于索引提示,尤其是没有WHERE子句的查询。

更新 2012-11-14 14:21 EDT

根据《Understanding MySQL Internals》一书

在此处输入图片说明

第 202 页第 7 段内容如下:

数据存储在称为聚簇索引的特殊结构中,它是一个 B 树,主键作为键值,实际记录(而不是指针)位于数据部分。因此,每个 InnoDB 表都必须有一个主键。如果未提供,则会添加用户通常不可见的特殊行 ID 列作为主键。辅助键将存储标识记录的主键的值。B 树代码可以在innobase/btr/btr0btr.c 中找到

这就是我之前声明的原因:执行全表扫描,然后执行一些文件排序,而不是通过 gen_clust_index 为每个二级索引条目执行全索引扫描和行查找要容易得多InnoDB 每次都会进行双索引查找。这听起来有点残酷,但这就是事实。再次,考虑缺乏WHERE条款。这本身就是对 MySQL 查询优化器进行全表扫描的提示。


egg*_*yal 0

改编自Denis对 SO 另一个问题的回答(经许可) :

由于所有记录(或几乎所有记录)都将由查询获取,因此通常最好不要使用任何索引。原因是,读取索引实际上需要一些费用。

当您要读取整个表时,顺序读取表并在内存中对其行进行排序可能是最便宜的计划。如果您只需要几行并且大多数行都与 where 子句匹配,那么选择最小的索引就可以了。

要了解原因,请想象所涉及的磁盘 I/O。

假设您想要没有索引的整个表。为此,您需要读取 data_page1、data_page2、data_page3 等,按顺序访问涉及的各个磁盘页面,直到到达表的末尾。然后排序并返回。

如果您希望前 5 行没有索引,则需要像以前一样顺序读取整个表,同时对前 5 行进行堆排序。不可否认,这需要对少数行进行大量的读取和排序。

现在假设您希望整个表都有一个索引。为此,您需要按顺序读取index_page1、index_page2 等。然后,这将导致您以完全随机的顺序(排序的行出现在数据中的顺序)访问 data_page3,然后是 data_page1,然后再次访问 data_page3,然后是 data_page2 等。所涉及的 IO 使得顺序读取整个乱七八糟的内容并对内存中的抓取包进行排序变得更便宜。

相反,如果您只需要索引表的前 5 行,则使用索引成为正确的策略。在最坏的情况下,您会在内存中加载 5 个数据页并继续前进。

顺便说一句,一个好的 SQL 查询规划器将根据数据的碎片程度来决定是否使用索引。如果按顺序获取行意味着在表中来回缩放,那么优秀的规划人员可能会认为不值得使用索引。相反,如果表使用相同的索引进行聚集,则可以保证行是按顺序排列的,从而增加了它被使用的可能性。

但是,如果您将同一个查询与另一个表连接起来,并且另一个表有一个非常有选择性的 where 子句,可以使用一个小索引,规划器可能会认为实际上更好,例如获取标记为 、 hash 的行的所有fooID连接表,并在内存中对它们进行堆排序。


归档时间:

查看次数:

5816 次

最近记录:

8 年 前