一文读懂MySQL的索引结构及查询优化

同时再次强调,这几篇关于MySQL的探究都是基于5.7版本,相关总结与结论不必定适用于其余版本)html

MySQL官方文档中(https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html)有这样一段描述:mysql

The best way to improve the performance of SELECT operations is to create indexes on one or more of the columns that are tested in the query. But unnecessary indexes waste space and waste time for MySQL to determine which indexes to use. Indexes also add to the cost of inserts, updates, and deletes because each index must be updated. You must find the right balance to achieve fast queries using the optimal set of indexes.算法

就是说提升查询性能最直接有效的方法就是创建索引,可是没必要要的索引会浪费空间,同时也增长了额外的时间成本去判断应该走哪一个索引,此外,索引还会增长插入、更新、删除数据的成本,由于作这些操做的同时还要去维护(更新)索引树。所以,应该学会使用最佳索引集来优化查询。sql

索引结构# 在MySQL中,索引(Index)是帮助高效获取数据的数据结构。这种数据结构MySQL中最经常使用的就是B+树(B+Tree)。数据库

Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows.json

就比如给你一本书和一篇文章标题,若是没有目录,让你找此标题对应的文章,可能须要从第一页翻到最后一页;若是有目录大纲,你可能只须要在目录页寻找此标题,而后迅速定位文章。session

这里咱们能够把书(book)当作是MySQL中的table,把文章(article)当作是table中的一行记录,即row,文章标题(title)当作row中的一列column,目录天然就是对title列创建的索引index了,这样根据文章标题从书中检索文章就对应sql语句select * from book where title = ?,相应的,书中每增长一篇文章(即insert into book (title, ...) values ('华山论剑', ...)),都须要维护一下目录,这样才能从目录中找到新增的文章华山论剑,这一操做对应的是MySQL中每插入(insert)一条记录须要维护title列的索引树(B+Tree)。数据结构

为何使用B+Tree# 首先须要澄清的一点是,MySQL跟B+树没有直接的关系,真正与B+树有关系的是MySQL的默认存储引擎InnoDB,MySQL中存储引擎的主要做用是负责数据的存储和提取,除了InnoDB以外,MySQL中也支持好比MyISAM等其余存储引擎(详情见https://dev.mysql.com/doc/refman/5.7/en/storage-engine-setting.html)做为表的底层存储引擎。app

Copymysql> show engines; +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ | Engine | Support | Comment | Transactions | XA | Savepoints | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ | MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO | | CSV | YES | CSV storage engine | NO | NO | NO | | PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO | | BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO | | InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES | | MyISAM | YES | MyISAM storage engine | NO | NO | NO | | ARCHIVE | YES | Archive storage engine | NO | NO | NO | | MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO | | FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ 复制代码 提到索引,咱们可能会立马想到下面几种数据结构来实现。less

(1) 哈希表 哈希虽然可以提供O(1)的单数据行的查询性能,可是对于范围查询和排序却没法很好支持,需全表扫描。

(2) 红黑树 红黑树(Red Black Tree)是一种自平衡二叉查找树,在进行插入和删除操做时经过特定操做保持二叉查找树的平衡,从而得到较高的查找性能。

通常来讲,索引自己也很大,每每不可能所有存储在内存中,所以索引每每以索引文件的形式存储的磁盘上。这样的话,索引查找过程当中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗远远高于内存,因此评价一个数据结构做为索引的优劣最重要的指标就是查找过程当中磁盘I/O次数。换句话说,索引的结构组织要尽可能减小查找过程当中磁盘I/O的次数。

在这里,磁盘I/O的次数取决于树的高度,因此,在数据量较大时,红黑树会因树的高度较大而形成磁盘IO较多,从而影响查询效率。

(3) B-Tree B树中的B表明平衡(Balance),而不是二叉(Binary),B树是从平衡二叉树演化而来的。

为了下降树的高度(也就是减小磁盘I/O次数),把原来瘦高的树结构变得矮胖,B树会在每一个节点存储多个元素(红黑树每一个节点只会存储一个元素),而且节点中的元素从左到右递增排列。以下图所示:

B-Tree结构图 B-Tree在查询的时候比较次数其实不比二叉查找树少,但在内存中的大小比较、二分查找的耗时相比磁盘IO耗时几乎能够忽略。 B-Tree大大下降了树的高度,因此也就极大地提高了查找性能。

(4) B+Tree B+Tree是在B-Tree基础上进一步优化,使其更适合实现存储索引结构。InnoDB存储引擎就是用B+Tree实现其索引结构。

B-Tree结构图中能够看到每一个节点中不只包含数据的key值,还有data值。而每个节点的存储空间是有限的,若是data值较大时将会致使每一个节点能存储的key的数量很小,这样会致使B-Tree的高度变大,增长了查询时的磁盘I/O次数,进而影响查询性能。在B+Tree中,全部data值都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样能够增大每一个非叶子节点存储的key值数量,下降B+Tree的高度,提升效率。

B+Tree结构图 这里补充一点相关知识 在计算机中,磁盘每每不是严格按需读取,而是每次都会预读,即便只须要一个字节,磁盘也会从这个位置开始,顺序向后读取必定长度的数据放入内存。这样作的理论依据是计算机科学中著名的局部性原理:

当一个数据被用到时,其附近的数据也一般会立刻被使用。

因为磁盘顺序读取的效率很高(不须要寻道时间,只需不多的旋转时间),所以对于具备局部性的程序来讲,预读能够提升I/O效率。预读的长度通常为页(page)的整数倍。

页是计算机管理存储器的逻辑块,硬件及操做系统每每将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块称为一页(许多操做系统的页默认大小为4KB),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时操做系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,而后异常返回,程序继续运行。(以下命令能够查看操做系统的默认页大小)

Copy$ getconf PAGE_SIZE 4096 复制代码 数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为操做系统的页大小的整数倍,这样每一个节点只须要一次I/O就能够彻底载入。

InnoDB存储引擎中也有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每一个页的大小为16KB。

Copymysql> show variables like 'innodb_page_size'; +------------------+-------+ | Variable_name | Value | +------------------+-------+ | innodb_page_size | 16384 | +------------------+-------+ 1 row in set (0.01 sec) 复制代码 通常表的主键类型为INT(占4个字节)或BIGINT(占8个字节),指针类型也通常为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(由于是估值,为方便计算,这里的K取值为10^3)。也就是说一个深度为3的B+Tree索引能够维护10^3 * 10^3 * 10^3 = 10亿条记录。

B+Tree的高度通常都在2到4层。mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只须要1到3次磁盘I/O操做。

随机I/O对于MySQL的查询性能影响会很是大,而顺序读取磁盘中的数据会很快,由此咱们也应该尽可能减小随机I/O的次数,这样才能提升性能。在B-Tree中因为全部的节点均可能包含目标数据,咱们老是要从根节点向下遍历子树查找知足条件的数据行,这会带来大量的随机I/O,而B+Tree全部的数据行都存储在叶子节点中,而这些叶子节点经过双向链表依次按顺序链接,当咱们在B+树遍历数据(好比说范围查询)时能够直接在多个叶子节点之间进行跳转,保证顺序、倒序遍历的性能。

另外,对以上提到的数据结构不熟悉的朋友,这里推荐一个在线数据结构可视化演示工具,有助于快速理解这些数据结构的机制:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

主键索引# 上面也有说起,在MySQL中,索引属于存储引擎级别的概念。不一样存储引擎对索引的实现方式是不一样的,这里主要看下MyISAM和InnoDB两种存储引擎的索引实现方式。

MyISAM索引实现# MyISAM引擎使用B+Tree做为索引结构时叶子节点的data域存放的是数据记录的地址。以下图所示:

MyISAM主键索引原理图 由上图能够看出:MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,所以MyISAM的索引方式也叫作非汇集的,之因此这么称呼是为了与InnoDB的汇集索引区分。

InnoDB索引实现# InnoDB的主键索引也使用B+Tree做为索引结构时的实现方式却与MyISAM大相径庭。InnoDB的数据文件自己就是索引文件。在InnoDB中,表数据文件自己就是按B+Tree组织的一个索引结构,这棵树的叶子节点data域保存了完整的数据记录,这个索引的key是数据表的主键,所以InnoDB表数据文件自己就是主索引。

InnoDB主键索引原理图 InnoDB存储引擎中的主键索引(primary key)又叫作汇集索引(clustered index)。由于InnoDB的数据文件自己要按主键汇集,因此InnoDB要求表必须有主键(MyISAM能够没有),若是没有显式指定,则MySQL系统会自动选择一个能够惟一标识数据记录的列做为主键,若是不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段做为主键,这个字段长度为6个字节,类型为长整形。(详情见官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.html)

汇集索引这种实现方式使得按主键搜索十分高效,直接能查出整行数据。

在InnoDB中,用非单调递增的字段做为主键不是个好主意,由于InnoDB数据文件自己是一棵B+Tree,非单增的主键会形成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,于是使用递增字段做为主键则是一个很好的选择。

非主键索引# MyISAM索引实现# MyISAM中,主键索引和非主键索引(Secondary key,也有人叫作辅助索引)在结构上没有任何区别,只是主键索引要求key是惟一的,而辅助索引的key能够重复。这里再也不多加叙述。

InnoDB索引实现# InnoDB的非主键索引data域存储相应记录主键的值。换句话说,InnoDB的全部非主键索引都引用主键的值做为data域。以下图所示:

InnoDB非主键索引原理图 由上图可知:使用非主键索引搜索时须要检索两遍索引,首先检索非主键索引得到主键(primary key),而后用主键到主键索引树中检索得到完整记录。

那么为何非主键索引结构叶子节点存储的是主键值,而不像主键索引那样直接存储完整的一行数据,这样就能避免回表二次检索?显然,这样作一方面节省了大量的存储空间,另外一方面多份冗余数据,更新数据的效率确定低下,另外保证数据的一致性是个麻烦事。

到了这里,也很容易明白为何不建议使用过长的字段做为主键,由于全部的非主键索引都引用主键值,过长的主键值会让非主键索引变得过大。

联合索引# 官方文档:https://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html

好比INDEX idx_book_id_hero_name (book_id, hero_name) USING BTREE,即对book_id, hero_name两列创建了一个联合索引。

A multiple-column index can be considered a sorted array, the rows of which contain values that are created by concatenating the values of the indexed columns.

联合索引是多列按照次序一列一列比较大小,拿idx_book_id_hero_name这个联合索引来讲,先比较book_id,book_id小的排在左边,book_id大的排在右边,book_id相同时再比较hero_name。以下图所示:

InnoDB联合索引原理图 了解了联合索引的结构,就能引入最左前缀法则:

If the table has a multiple-column index, any leftmost prefix of the index can be used by the optimizer to look up rows. For example, if you have a three-column index on (col1, col2, col3), you have indexed search capabilities on (col1), (col1, col2), and (col1, col2, col3).

就是说联合索引中的多列是按照列的次序排列的,若是查询的时候不能知足列的次序,好比说where条件中缺乏col1 = ?,直接就是col2 = ? and col3 = ?,那么就走不了联合索引,从上面联合索引的结构图应该能明显看出,只有col2列没法经过索引树检索符合条件的数据。

根据最左前缀法则,咱们知道对INDEX idx_book_id_hero_name (book_id, hero_name)来讲,where book_id = ? and hero_name = ?的查询来讲,确定能够走索引,可是若是是where hero_name = ? and book_id = ?呢,表面上看起来不符合最左前缀法则啊,但MySQL优化器会根据已有的索引,调整查询条件中这两列的顺序,让它符合最左前缀法则,走索引,这里也就回答了上篇《一文学会MySQL的explain工具》中为何用show warnings命令查看时,where中的两个过滤条件hero_name、book_id前后顺序被调换了。

至于对联合索引中的列进行范围查询等各类状况,均可以先想联合索引的结构是如何建立出来的,而后看过滤条件是否知足最左前缀法则。好比说范围查询时,范围列能够用到索引(必须是最左前缀),可是范围列后面的列没法用到索引。同时,索引最多用于一个范围列,所以若是查询条件中有两个范围列则没法全用到索引。

优化建议# 主键的选择# 在使用InnoDB存储引擎时,若是没有特别的须要,尽可能使用一个与业务无关的递增字段做为主键,主键字段不宜过长。缘由上面在讲索引结构时已提过。好比说经常使用雪花算法生成64bit大小的整数(占8个字节,用BIGINT类型)做为主键就是一个不错的选择。

索引的选择# (1) 表记录比较少的时候,好比说只有几百条记录的表,对一些列创建索引的意义可能并不大,因此表记录不大时酌情考虑索引。可是业务上具备惟一特性的字段,即便是多个字段的组合,也建议使用惟一索引(UNIQUE KEY)。

(2) 当索引的选择性很是低时,索引的意义可能也不大。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数Cardinality)与表记录数的比值,即count(distinct 列名)/count(*),常见的场景就是有一列status标识数据行的状态,可能status非0即1,总数据100万行有50万行status为0,50万行status为1,那么是否有必要对这一列单独创建索引呢?

An index is best used when you need to select a small number of rows in comparison to the total rows.

这句话我摘自stackoverflow上《MySQL: low selectivity columns = how to index?》下面一我的的回答。(详情见:https://stackoverflow.com/questions/2386852/mysql-low-cardinality-selectivity-columns-how-to-index)

对于上面说的status非0即1,并且这两种状况分布比较均匀的状况,索引可能并无实际意义,实际查询时,MySQL优化器在计算全表扫描和索引树扫描代价后,可能会放弃走索引,由于先从status索引树中遍历出来主键值,再去主键索引树中查最终数据,代价可能比全表扫描还高。

可是若是对于status为1的数据只有1万行,其余99万行数据status为0的状况呢,你怎么看?欢迎有兴趣的朋友在文章下面留言讨论!

补充: 关于MySQL如何选择走不走索引或者选择走哪一个最佳索引,可使用MySQL自带的trace工具一探究竟。具体使用见下面的官方文档。 https://dev.mysql.com/doc/internals/en/optimizer-tracing.html https://dev.mysql.com/doc/refman/5.7/en/information-schema-optimizer-trace-table.html

使用方法:

Copymysql> set session optimizer_trace="enabled=on",end_markers_in_json=on; mysql> select * from tb_hero where hero_id = 1; mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE; 复制代码 注意:开启trace工具会影响MySQL性能,因此只能临时分析sql使用,用完以后应当当即关闭

Copymysql> set session optimizer_trace="enabled=off"; 复制代码 (3) 在varchar类型字段上创建索引时,建议指定索引长度,有些时候可能不必对全字段创建索引,根据实际文本区分度决定索引长度便可【说明:索引的长度与区分度是一对矛盾体,通常对字符串类型数据,长度为20的索引,区分度会高达90%以上,可使用count(distinct left(列名, 索引长度))/count(*)来肯定区分度】。

这种指定索引长度的索引叫作前缀索引(详情见https://dev.mysql.com/doc/refman/5.7/en/column-indexes.html#column-indexes-prefix)。

With col_name(N) syntax in an index specification for a string column, you can create an index that uses only the first N characters of the column. Indexing only a prefix of column values in this way can make the index file much smaller. When you index a BLOB or TEXT column, you must specify a prefix length for the index.

前缀索引语法以下:

Copymysql> alter table tb_hero add index idx_hero_name_skill2 (hero_name, skill(2)); 复制代码 前缀索引兼顾索引大小和查询速度,可是其缺点是不能用于group by和order by操做,也不能用于covering index(即当索引自己包含查询所需所有数据时,再也不访问数据文件自己)。

(4) 当查询语句的where条件或group by、order by含多列时,可根据实际状况优先考虑联合索引(multiple-column index),这样能够减小单列索引(single-column index)的个数,有助于高效查询。

If you specify the columns in the right order in the index definition, a single composite index can speed up several kinds of queries on the same table.

创建联合索引时要特别注意column的次序,应结合上面提到的最左前缀法则以及实际的过滤、分组、排序需求。区分度最高的建议放最左边。

说明:

order by的字段能够做为联合索引的一部分,而且放在最后,避免出现file_sort的状况,影响查询性能。正例:where a=? and b=? order by c会走索引idx_a_b_c,可是WHERE a>10 order by b却没法彻底使用上索引idx_a_b,只会使用上联合索引的第一列a

存在非等号和等号混合时,在建联合索引时,应该把等号条件的列前置。如:where c>? and d=?那么即便c的区分度更高,也应该把d放在索引的最前列,即索引idx_d_c

若是where a=? and b=?,若是a列的几乎接近于惟一值,那么只须要创建单列索引idx_a便可

order by与group by# 尽可能在索引列上完成分组、排序,遵循索引最左前缀法则,若是order by的条件不在索引列上,就会产生Using filesort,下降查询性能。

分页查询# MySQL分页查询大多数写法可能以下:

Copymysql> select * from tb_hero limit offset,N; 复制代码 MySQL并非跳过offset行,而是取offset+N行,而后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就很是的低下。

能够对超过特定阈值的页数进行SQL改写以下:

先快速定位须要获取的id段,而后再关联

Copymysql> select a.* from tb_hero a, (select hero_id from tb_hero where 条件 limit 100000,20 ) b where a.hero_id = b.hero_id; 复制代码 或者这种写法

Copymysql> select a.* from tb_hero a inner join (select hero_id from tb_hero where 条件 limit 100000,20) b on a.hero_id = b.hero_id; 复制代码 多表join# (1) 须要join的字段,数据类型必须绝对一致; (2) 多表join时,保证被关联的字段有索引

覆盖索引# 利用覆盖索引(covering index)来进行查询操做,避免回表,从而增长磁盘I/O。换句话说就是,尽量避免select *语句,只选择必要的列,去除无用的列。

An index that includes all the columns retrieved by a query. Instead of using the index values as pointers to find the full table rows, the query returns values from the index structure, saving disk I/O. InnoDB can apply this optimization technique to more indexes than MyISAM can, because InnoDB secondary indexes also include the primary key columns. InnoDB cannot apply this technique for queries against tables modified by a transaction, until that transaction ends.

Any column index or composite index could act as a covering index, given the right query. Design your indexes and queries to take advantage of this optimization technique wherever possible.

当索引自己包含查询所需所有列时,无需回表查询完整的行记录。对于InnoDB来讲,非主键索引中包含了全部的索引列以及主键值,查询的时候尽可能用这种特性避免回表操做,数据量很大时,查询性能提高很明显。

in和exsits# 原则:小表驱动大表,即小的数据集驱动大的数据集

(1) 当A表的数据集大于B表的数据集时,in优于exists

Copymysql> select * from A where id in (select id from B) 复制代码 (2) 当A表的数据集小于B表的数据集时,exists优于in

Copymysql> select * from A where exists (select 1 from B where B.id = A.id) 复制代码 like# 索引文件具备B+Tree最左前缀匹配特性,若是左边的值未肯定,那么没法使用索引,因此应尽可能避免左模糊(即%xxx)或者全模糊(即%xxx%)。

Copymysql> select * from tb_hero where hero_name like '%无%'; +---------+-----------+--------------+---------+ | hero_id | hero_name | skill | book_id | +---------+-----------+--------------+---------+ | 3 | 张无忌 | 九阳神功 | 3 | | 5 | 花完好 | 移花接玉 | 5 | +---------+-----------+--------------+---------+ 2 rows in set (0.00 sec)

mysql> explain select * from tb_hero where hero_name like '%无%'; +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_hero | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) 复制代码 能够看出全模糊查询时全表扫了,这个时候使用覆盖索引的特性,只选择索引字段能够有所优化。以下:

Copymysql> explain select book_id, hero_name from tb_hero where hero_name like '%无%'; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+ | 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_book_id_hero_name | 136 | NULL | 6 | 16.67 | Using where; Using index | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+ 1 row in set, 1 warning (0.00 sec) 复制代码 count(*)# 阿里巴巴Java开发手册中有这样的规约:

不要使用count(列名)或count(常量)来替代count(),count()是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关【说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行】。 count(distinct col)计算该列除NULL以外的不重复行数,注意count(distinct col1, col2)若是其中一列全为NULL,那么即便另外一列有不一样的值,也返回为0

截取一段官方文档对count的描述(具体见:https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_count)

COUNT(expr): Returns a count of the number of non-NULL values of expr in the rows.The result is a BIGINT value.If there are no matching rows, COUNT(expr) returns 0.

COUNT(*) is somewhat different in that it returns a count of the number of rows, whether or not they contain NULL values.

Prior to MySQL 5.7.18, InnoDB processes SELECT COUNT() statements by scanning the clustered index. As of MySQL 5.7.18, InnoDB processes SELECT COUNT() statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present, the clustered index is scanned.

可见5.7.18以前,MySQL处理count(*)会扫描主键索引,5.7.18以后从非主键索引中选择较小的合适的索引扫描。能够用explain看下执行计划。

Copymysql> select version(); +-----------+ | version() | +-----------+ | 5.7.18 | +-----------+ 1 row in set (0.00 sec)

mysql> explain select count(*) from tb_hero; +----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_skill | 15 | NULL | 6 | 100.00 | Using index | +----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec)

mysql> explain select count(1) from tb_hero; +----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_hero | NULL | index | NULL | idx_skill | 15 | NULL | 6 | 100.00 | Using index | +----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) 复制代码 有人纠结count()、count(1)到底哪一种写法更高效,从上面的执行计划来看都同样,若是你还不放心的话,官方文档中也明确指明了InnoDB对count()、count(1)的处理彻底一致。

InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

其余# 索引列上作任何操做(表达式、函数计算、类型转换等)时没法使用索引会致使全表扫描

实战# 前几周测试同事对公司的某产品进行压测,某单表写入了近2亿条数据,过程当中发现配的报表有几个数据查询时间太长,因此重点看了几个慢查询SQL。避免敏感信息,这里对其提取简化作个记录。

Copymysql> select count() from tb_alert; +-----------+ | count() | +-----------+ | 198101877 | +-----------+ 复制代码 表join慢# 表join后,取前10条数据就花了15秒,看了下SQL执行计划,以下:

Copymysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 10; 10 rows in set (15.46 sec)

mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 10; +----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+ | 1 | SIMPLE | tb_alert | NULL | ALL | NULL | NULL | NULL | NULL | 190097118 | 100.00 | NULL | | 1 | SIMPLE | tb_situation_alert | NULL | ALL | NULL | NULL | NULL | NULL | 8026988 | 100.00 | Using where; Using join buffer (Block Nested Loop) | +----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+ 2 rows in set, 1 warning (0.00 sec) 复制代码 能够看出join的时候没有用上索引,tb_situation_alert表上联合主键是这样的PRIMARY KEY (situation_id, alert_id),参与表join字段是alert_id,原来是不符合联合索引的最左前缀法则,仅从这条sql看,解决方案有两种,一种是对tb_situation_alert表上的alert_id单独创建索引,另一种是调换联合主键的列的次序,改成PRIMARY KEY (alert_id, situation_id)。固然不能由于多配一张报表,就改其余产线的表的主键索引,这并不合理。在这里,应该对alert_id列单独创建索引。

Copymysql> create index idx_alert_id on tb_situation_alert (alert_id);

mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 100; 100 rows in set (0.01 sec)

mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 100; +----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+ | 1 | SIMPLE | tb_alert | NULL | ALL | NULL | NULL | NULL | NULL | 190097118 | 100.00 | NULL | | 1 | SIMPLE | tb_situation_alert | NULL | ref | idx_alert_id | idx_alert_id | 8 | tb_alert.alert_id | 2 | 100.00 | NULL | +----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+ 2 rows in set, 1 warning (0.00 sec) 复制代码 优化后,执行计划能够看出join的时候走了索引,查询前100条0.01秒,和以前的取前10条数据就花了15秒天壤之别。

分页查询慢# 从第10000000条数据日后翻页时,25秒才能出结果,这里就能使用上面的分页查询优化技巧了。上面讲优化建议时,没看执行计划,这里正好看一下。

Copymysql> select * from tb_alert limit 10000000, 10; 10 rows in set (25.23 sec)

mysql> explain select * from tb_alert limit 10000000, 10; +----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+ | 1 | SIMPLE | tb_alert | NULL | ALL | NULL | NULL | NULL | NULL | 190097118 | 100.00 | NULL | +----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+ 1 row in set, 1 warning (0.00 sec) 复制代码 再看下使用上分页查询优化技巧的sql的执行计划

Copymysql> select * from tb_alert a inner join (select alert_id from tb_alert limit 10000000, 10) b on a.alert_id = b.alert_id; 10 rows in set (2.29 sec)

mysql> explain select * from tb_alert a inner join (select alert_id from tb_alert a2 limit 10000000, 10) b on a.alert_id = b.alert_id; +----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+ | 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 10000010 | 100.00 | NULL | | 1 | PRIMARY | a | NULL | eq_ref | PRIMARY | PRIMARY | 8 | b.alert_id | 1 | 100.00 | NULL | | 2 | DERIVED | a2 | NULL | index | NULL | idx_processed | 5 | NULL | 190097118 | 100.00 | Using index | +----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+ 3 rows in set, 1 warning (0.00 sec) 复制代码 分组聚合慢# 分析SQL后,发现根本上并不是分组聚合慢,而是扫描联合索引后,回表致使性能低下,去除没必要要的字段,使用覆盖索引。

这里避免敏感信息,只演示分组聚合前的简化SQL,主要问题也是在这。 表上有联合索引KEY idx_alert_start_host_template_id ( alert_start, alert_host, template_id),优化前的sql为

Copymysql> select alert_start, alert_host, template_id, alert_service from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000; 10000 rows in set (1 min 5.22 sec) 复制代码 使用覆盖索引,去掉template_id列,就能避免回表,查询时间从1min多变为0.03秒,以下:

Copymysql> select alert_start, alert_host, template_id from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000; 10000 rows in set (0.03 sec)

mysql> explain select alert_start, alert_host, template_id from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000; +----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+ | 1 | SIMPLE | tb_alert | NULL | range | idx_alert_start_host_template_id | idx_alert_start_host_template_id | 9 | NULL | 95048559 | 100.00 | Using where; Using index | +----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+ 1 row in set, 1 warning (0.01 sec) 复制代码 总结# 任何不考虑应用场景的设计都不是最好的设计,就好比说表结构的设计、索引的建立,都应该权衡数据量大小、查询需求、数据更新频率等。

1)宁滥勿缺。认为一个查询就须要建一个索引 2)宁缺勿滥。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度

相关文章
相关标签/搜索