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


15

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

这种情况涉及MySQL中的一个简单的InnoDB表,该表包含三列和1万行。其中一列(整数)被索引了,OP试图检索按该列排序的整个表:

SELECT * FROM person ORDER BY age

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

尽管有提示 FORCE INDEX FOR ORDER BY (age) 导致使用索引提示,但有人回答(带有支持的注释/来自他人的评论)说,仅当从索引中读取所有选定的列时,索引才用于排序(即通常由Using indexExtra列中的指示)的EXPLAIN输出)。稍后给出了一个解释,即遍历索引然后从表中获取列会导致随机I / O,MySQL认为它比a更昂贵filesort

这似乎是在关于ORDER BY优化的手册一章中碰到的,它不仅传达出强烈的印象,即ORDER BY从索引满足比执行其他排序更可取(实际上,它filesort是quicksort和mergesort的组合,因此 必须具有下限;虽然应该按顺序遍历索引并查找表,所以这很有意义),但它也忽略了此所谓的“优化”,同时还指出:Ω(nlog n)O(n)

以下查询使用索引来解析ORDER BY零件:

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

就我的阅读而言,在这种情况下就是这种情况(但没有明确提示就没有使用索引)。

我的问题是:

  • 确实有必要为所有选定的列建立索引以便MySQL选择使用索引吗?

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

    • 如果没有,这里发生了什么?

Answers:


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';

测试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>

测试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>

InnoDB分析

当数据作为InnoDB加载时,请注意所有四个EXPLAIN计划都使用了gender索引。第三和第四EXPLAIN计划使用gender索引,即使请求的数据是id。为什么?因为id在中,PRIMARY 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
  • 通过二级索引访问表
  • 如果表是MyISAM,我们将看到一个完全不同的EXPLAIN计划

现在,查看SO问题中的查询:select * from person order by age\G。由于没有WHERE子句,因此您明确要求进行全表扫描。该表的默认排序顺序为id(PRIMARY KEY),因为其为auto_increment,而gen_clust_index(又称为聚集索引)由内部rowid排序。当您按索引排序时,请记住,InnoDB二级索引具有附加到每个索引条目的行ID。这就产生了每次都需要全行访问的内部需求。

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

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

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

更新2012-11-14 14:21 EDT

根据《了解MySQL内部原理》一书

在此处输入图片说明

页面202第7段说:

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

这就是我早些时候说过的原因:与通过gen_clust_index为每个辅助索引条目进行完整的索引扫描和行查找相比,执行全表扫描然后进行某种文件排序要容易得多InnoDB每次都会进行双索引查找。这听起来有些残酷,但这只是事实。同样,请考虑缺少WHERE子句。这本身就是对MySQL查询优化器进行全表扫描的提示。


罗兰多,谢谢您这么详尽的回答。但是,它似乎与选择索引无关FOR ORDER BY(这是此问题中的特殊情况)。问题确实指出在这种情况下存储引擎是InnoDB(原始SO问题表明1万行在8个项目中相当均匀地分布,基数在这里也不应该成为问题)。可悲的是,我认为这不能回答问题。
eggyal 2012年

这很有趣,因为第一部分也是我的第一个本能(它没有很好的基数,所以mysql选择使用完全扫描)。但是我阅读的内容越多,该规则似乎就不适用于优化订单。您确定它按主键为innodb聚集索引排序吗?这篇文章表明主键被添加到末尾,所以排序是否仍会在索引的显式列上?简而言之,我还是很困惑!
德里克·唐尼

1
filesort选择是由查询优化器决定的,原因很简单:缺乏对所拥有数据的了解。如果您选择使用索引提示(基于问题2)给您带来令人满意的运行时间,那么一定要这样做。我提供的答案只是一个学术练习,目的是展示MySQL Query Optimizer的气质以及建议采取的措施。
RolandoMySQLDBA 2012年

1
我已经阅读并重新阅读了这篇文章和其他文章,我只能同意这与主键上的innodb排序有关,因为我们选择了所有内容(而不是覆盖索引)。我很惊讶在ORDER BY优化文档页面中没有提到这种特定于InnoDB的奇怪特性。反正+1罗兰
德里克·唐尼

1
@eggyal 是本周写的。请注意,相同的EXPLAIN计划,如果数据集不适合内存,则完全扫描将花费更长的时间。
德里克·唐尼

0

改编(经许可)丹尼斯针对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查询计划者btw将根据数据的碎片程度来决定是否使用索引。如果按顺序获取行意味着在表上来回缩放,那么好的计划者可能会认为不值得使用索引。相反,如果表是使用相同的索引进行聚类的,则可以保证行是有序的,从而增加了使用该表的可能性。

但是,如果您将同一个查询与另一个表连接在一起,并且该另一个表的选择性非常强的where子句可以使用较小的索引,那么计划者可能会认为实际上更好,例如,获取标记为foo,哈希的行的所有ID 加入表,并在内存中对它们进行排序。

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.