索引第三篇:聪明地使用索引
Posted 毛奇志(公众号:爱奇志)
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了索引第三篇:聪明地使用索引相关的知识,希望对你有一定的参考价值。
一、前言
二、索引优点、索引缺点、什么列上设置索引、什么列上不设置索引、根据表的大小选择索引还是全表扫描
2.1 索引的五个优点
第一,检索,可以大大加快数据的检索速度,这也是创建索引的最主要的原因(金手指:加快检索速度是创建索引的最大原因,是索引的最大优势)。
第二,聚集索引,将随机IO变为顺序IO;
第三,排序和分组,帮助服务器避免排序和临时表,使用分组group by和排序子句order by进行数据检索时,同样可以显著减少查询中分组和排序的时间。
第四,唯一性索引,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
第五,表与表连接,数据完整性:可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
2.2 解释索引三重要的优点(即上面索引的前三个优点)
1、索引可以减少扫描行数,加快检索速度。
2、索引可以帮助服务器避免排序和临时表,sql语句中使用orderby,优化排序。
3、索引可以将随机 IO 变成顺序 IO,从而加快检索速度。
2.2.1 索引可以减少扫描行数
当我们要在新华字典里查某个字(如「先」)具体含义的时候,通常都会拿起一本新华字典来查,你可以先从头到尾查询每一页是否有「先」这个字,这样做(对应数据库中的全表扫描)确实能找到,但效率无疑是非常低下的,更高效的方相信大家也都知道,就是在首页的索引里先查找「先」对应的页数,然后直接跳到相应的页面查找,这样查询时候大大减少了,可以为是 O(1)。
数据库中的索引也是类似的,通过索引定位到要读取的页,大大减少了需要扫描的行数,将全表扫描变为页扫描,B+树中一个节点就是一个页,B+树叶子节点中是递增的,使用二分法就可以找到,能极大的提升效率。
问题:mysql B+树索引,聚集索引+非聚集索引(二级索引+复合索引),为什么可以加快检索速度?
回答:不建立索引,数据是无序的,要全表扫描,就是全磁盘扫描,建立索引,你要访问磁盘的次数,是由这棵树的层数决定的,每一次具体检索访问磁盘的次数,就是叶子节点所在层数(步骤:B+树检索+二分法检索递增数组,先用B+树检索直到叶子节点(一定要到叶子节点,因为具体表数据一定存放在叶子节点,非叶子节点只是检索之用),然后叶子节点是一个递增的数组,那就用二分法)
问题:为什么不建立索引要全表扫描,全磁盘扫描,主键有序啊?
回答:逻辑有序不是物理存储有序。
解释:主键虽然是递增的,但是如果你写入磁盘时,没有去维护有序数组这样一个数据结构(比如你删掉了 4,怎么把 5 往前面挪),那数据在磁盘里依旧是无序的,查找时只能随机查找,而如果你维护了有序数组这样的数据结构,其实也是建了索引,只是建了不一样的数据结构的索引罢了。
2.2.2 索引优化order by,避免再次排序生成临时表
假设我们不用索引,试想运行如下语句
// 这个sql语句中使用到了orderby,可以使用索引优化
SELECT * FROM user order by age desc;
(1)在不是使用索引的情况下,则 MySQL 的流程是这样的,扫描所有行,把所有行加载到内存后,再按 age 排序生成一张临时表,再把这表排序后将相应行返回给客户端,更糟的,如果这张临时表的大小大于 tmp_table_size 的值(默认为 16 M),内存临时表会转为磁盘临时表,性能会更差。
(2)在使用索引的情况下,如果在age字段上加了索引,索引本身是有序的 ,所以从磁盘读的行数本身就是按 age 排序好的,也就不会生成临时表,就不用再额外排序 ,无疑提升了性能。
关于避免排序生成临时表?
不使用索引,age是没有排序的,只有id排序了,需要按 age 排序生成一张临时表; 在age字段上加上二级索引,create index idx_age on user(age),会得到一个age的B+树,是排序好的,即避免排列了。
2.2.3 索引可以将随机 IO 变成顺序 IO
再来看随机 IO 和顺序 IO。先来解释下这两个概念。
相信不少人应该吃过旋转火锅,服务员把一盘盘的菜放在旋转传输带上,然后等到这些菜转到我们面前,我们就可以拿到菜了,假设装一圈需要 4 分钟,则最短等待时间是 0(即菜就在你跟前),最长等待时间是 4 分钟(菜刚好在你跟前错过),那么平均等待时间即为 2 分钟,假设我们现在要拿四盘菜,这四盘菜随机分配在传输带上,则可知拿到这四盘菜的平均等待时间是 8 分钟(随机 IO),如果
这四盘菜刚好紧邻着排在一起,则等待时间只需 2 分钟(顺序 IO)。
随机IO:到磁盘上拿四个页面,平均来看,拿一个页面需要2分钟,拿四个页面需要8分钟。
顺序IO:到磁盘上拿四个页面,平均来看,拿一个页面需要2分钟,但是四个页面顺序放在一起,找到
第一个页面两分钟后,剩余三个就在后面,直接拿下来就好了。
上述中传输带就类比磁道,磁道上的菜就类比扇区(sector)中的信息,磁盘块(block)是由多个相邻的扇区组成的,扇区是操作系统读取的最小单元,如果信息能以 block 的形式聚集在一起(程序的局部性原理),就能极大减少磁盘 IO 时间,这就是顺序 IO 带来的性能提升,下文中我们将会看到 B+ 树索引就起到这样的作用。
如上图示:多个扇区sectors组成了一个块block,如果要读的信息都在这个块 block 中,则只需一次 IO 读,而如果信息在一个磁道中分散地分布在各个扇区中,或者分布在不同磁道的扇区上(寻道时间是随机IO主要瓶颈所在),将会造成随机 IO,影响性能。
我们来看一下一个随机 IO 的时间分布:
seek Time: 寻道时间,磁头移动到扇区所在的磁道,注意,磁道是从内到外,一圈一圈的在磁盘上。
Rotational Latency:完成步骤 1 后,磁头移动到同一磁道扇区对应的位置所需求时间
Transfer Time:从磁盘读取信息传入内存时间,这其中寻道时间占据了绝大多数的时间(大概占据随机 IO 时间的占 40%),所以我们的突破口就在减少寻道时间。
随机 IO 和顺序 IO 大概相差百倍 (随机 IO:10 ms/ page, 顺序 IO 0.1ms / page),可见顺序 IO 性能之高,索引带来的性能提升显而易见!
2.2 索引的三个缺点
第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
第二,因为索引的存在,增加了数据库的存储空间,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
第三,当对表中的数据进行增加、删除和修改的时候,要花费较多的时间,因为索引也要随之变动,也要动态的维护,这样就降低了数据的维护速度。
小结:索引加快了检索,但是减慢了增加、删除和修改(但是这三个操作的检索过程是加快的了);
对于已经添加了索引的字段,从之前的“数据”变为“数据+索引”,占用了额外的物理空间。
2.3 六个列上应该创建索引
索引是建立在数据库表中的某些列的上面。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引:
1、搜索where having(第一作用,检索速度+聚集索引 顺序IO):在经常需要搜索的列上,可以加快搜索的速度;where having
2、等值条件判断where having (检索速度):在经常使用在 WHERE 子句中的列上面创建索引,加快条件的判断速度。
3、范围查找where having (检索速度):在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
4、静态:唯一性列和排列规则(第三四作用,排序分组临时表、唯一性):在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
5、动态:排序order by(检索速度):在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;order by
6、连接join on(第五作用,表连接,数据完整性):在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;join on
2.4 有些列不应该创建索引
一般来说,不应该创建索引的的这些列具有下列特点:
第一,静态:数据量少,数据行少:对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。金手指:加索引,最大优点加快检索速度无法覆盖增加的物理空间。
第二,静态:blob text image bit:对于那些定义为blob text, image 和 bit 数据类型的列不应该增加索引。这是因为,这些列的单个数据量要么相当大,要么取值很少。
第三,动态:查询少:对于那些在查询中很少使用或者参考的列不应该创建索引。
这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。金手指:这些列查询的少,索引的优化效率无法覆盖增加的物理空间。
第四,动态:修改多,当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。金手指:加索引,最大优点加快检索速度无法覆盖增加删除修改的减慢。
索引列的应用法则:就是索引的最大优点和两个缺点的取舍,最大优点是加快检索速度,两个缺点是增加物理空间 + 添加修改删除操作变慢
2.5 根据表的大小选择索引还是全表扫描
对于非常小的表,大部分情况下简单的全表扫描更高效。
对于中到大型的表,索引就非常有效。
对于特大型的表,建立和使用索引的代价将随之增长。则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配,例如可以便用分区战术。
对于表的数量种多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧,对于TB级别的数据,定位单条记录的意义不大,所以经常会使块级别元数据技术来替代索引。
三、高效索引设计法则
3.1 避免索引失效
加了索引却不生效可能会有以下几种原因: 公式、数据类型转换、编码类型转换、select。
3.1.1 公式,索引列是表示式的一部分,或是函数的一部分,导致索引失效
如下 SQL:
SELECT book_id FROM BOOK WHERE book_id + 1 = 5;
或者
SELECT book_id FROM BOOK WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(gmt_create) <= 10
上述两个 SQL 虽然在列 book_id 和 gmt_create 设置了索引 ,但由于它们是表达式或函数的一部分,导致索引无法生效,最终导致全表扫描。
问题:无法避免对索引列使用函数,怎么使用索引
回答:索引列是表示式的一部分,或是函数的一部分,索引不会生效。但是,有时候我们无法避免对索引列使用函数,但这样做会导致全表索引,是否有更好的方式呢。
比如我现在就是想记录 2016 ~ 2018 所有年份 7月份的交易记录总数
mysql SELECT count(*) FROM tradelog WHERE month(t_modified)=7;
由于索引列 t_modified是函数的参数,所以显然无法用到索引,我们可以将它改造成基本字段区间的查找如下
SELECT count(*) FROM tradelog WHERE
- (t_modified = '2016-7-1' AND t_modified<'2016-8-1') or
- (t_modified = '2017-7-1' AND t_modified<'2017-8-1') or
- (t_modified = '2018-7-1' AND t_modified<'2018-8-1');
3.1.2 数据类型转换,隐式类型转换,导致索引失效
假设有以下表:
CREATE TABLE `tradelog` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`operator` int(11) DEFAULT NULL,
`t_modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `tradeid` (`tradeid`),
KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
执行 SQL 语句
SELECT * FROM tradelog WHERE tradeid=110717;
交易编号 tradeid 上有索引,但用 EXPLAIN 执行却发现使用了全表扫描,为啥呢,tradeId 的类型是 varchar(32), 而此 SQL 用 tradeid 一个数字类型进行比较,发生了隐形转换,会隐式地将字符串转成整型,如下:
mysql SELECT * FROM tradelog WHERE CAST(tradid AS signed int) = 110717;
这样也就触发了上文中第一条的规则 ,即:索引列不能是函数的一部分。
3.1.3 数据编码转换,隐式编码转换,导致索引失效
这种情况非常隐蔽,来看下这个例子
CREATE TABLE `tradelog` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`operator` int(11) DEFAULT NULL,
`t_modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `tradeid` (`tradeid`),
KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; // utf8mb4
CREATE TABLE `trade_detail` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`trade_step` int(11) DEFAULT NULL, /*操作步骤*/
`step_info` varchar(32) DEFAULT NULL, /*步骤信息*/
PRIMARY KEY (`id`), KEY `tradeid` (`tradeid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; // utf8
trade_defail 是交易详情, tradelog 是操作此交易详情的记录,现在要查询 id=2 的交易的所有操作步骤信息,则我们会采用如下方式
SELECT d.* FROM tradelog l, trade_detail d WHERE d.tradeid=l.tradeid AND l.id=2;
由于 tradelog 与 trade_detail 这两个表的字符集不同,且 tradelog 的字符集是 utf8mb4,而 trade_detail 字符集是 utf8, utf8mb4 是 utf8 的超集,所以会自动将 utf8 转成 utf8mb4。即上述语句会发生如下转换:
SELECT d.* FROM tradelog l, trade_detail d WHERE (CONVERT(d.traideid USING utf8mb4)))=l.tradeid AND l.id=2;
自然也就触发了 「索引列不能是函数的一部分」这条规则。
两种解决方式
第一种方案当然是把两个表的字符集改成一样,如果业务量比较大,生产上不方便改的话,还有一种方案是把 utf8mb4 转成 utf8,如下
mysql SELECT d.* FROM tradelog l , trade_detail d WHERE d.tradeid=CONVERT(l.tradeid USING utf8) AND l.id=2;
这样索引列就生效了。
3.1.4 select * ,使用 select * 造成的全表扫描
SELECT * FROM user ORDER BY age DESC
上述语句在 age 上加了索引,但依然造成了全表扫描,这是因为我们使用了 SELECT *,导致回表查询,MySQL 认为回表的代价比全表扫描更大,所以不选择使用索引,如果想使用到 age 的索引,我们可以用覆盖索引来代替:
SELECT age FROM user ORDER BY age DESC
或者加上 limit 的条件(数据比较小)
SELECT * FROM user ORDER BY age DESC limit 10
这样就能利用到索引。
3.2 设计好的索引
-
静态:索引列选择 2条。尽量使用数据量少的索引;尽量选择区分度高的列作为索引,区分度的公式是 COUNT(DISTINCT col)/COUNT(*),区分度表示字段不重复的比率,比率越大我们扫描的记录数就越少。
-
动态:重要的字段才索引 3条。做法:为经常作为查询条件的字段建立索引 ;为经常需要排序、分组和联合操作的字段建立索引 ; 避免带函数的查询参与索引(索引列不能参与计算,保持列“干净”,比如,带函数的查询不参与索引。FROM_UNIXTIME(create_time)=‘2016-06-06’ 就不能使用索引,原因很简单,B+树中存储的都是数据表中的字段值,但是进行检索时,需要把所有元素都应用函数才能比较,显然这样的代价太大。所以语句要写成 :create_time=UNIX_TIMESTAMP(‘2016-06-06’))。
-
选择唯一性索引 1条。做法:唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
-
限制索引的数目 2条。理由:越多的索引,会使更新表变得很浪费时间。做法:删除不再使用或者很少使用的索引;尽量的扩展索引,不要新建索引(比如表中已经有了a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可)。
-
限制索引长度 1条。理由:如果索引的值很长,那么查询的速度会受到影响。做法:尽量使用前缀来索引,如果索引字段的值很长,最好使用值的前缀来索引;最左前缀匹配原则,非常重要的原则【(1)适用于二级索引和复合索引;(2)MySQL会一直向右匹配直到遇到范围查询 (,<,BETWEEN,LIKE)就停止匹配。】。
6.最后一点,不要依赖合并索引机制:单个多列组合索引和多个单列索引的检索查询效果不同,虽然存在合并索引(金手指:“合并索引”就是使用多个单列索引,然后将这些结果用“union或者and”来合并起来),但是最好还是不要依赖mysql内部的合并索引机制,还是应该建立起比较好的索引。
四、索引设计准则:经验法则和三星索引
上文我们得出了一个索引列顺序的经验法则:将选择性最高的列放在索引的最前列,这种建立在某些场景可能有用,但通常不如避免随机 IO 和 排序那么重要,这里引入索引设计中非常著名的一个准则:三星索引。
如果一个查询满足三星索引中三颗星的所有索引条件,理论上可以认为我们设计的索引是最好的索引。
什么是三星索引
第一颗星:where子句,WHERE子句 后面参与查询的列可以组成了单列索引或联合索引
第二颗星:order by 避免排序,即如果 SQL 语句中出现 order by colulmn,那么取出的结果集就已经是按照 column 排序好的,不需要再生成临时表
第三颗星:select 避免回表查询,SELECT 对应的列应该尽量是索引列,即尽量避免回表查询(上面就出现过,因为回表查询,mysql没有选择索引而是走全表扫描,避免回表两操作:新建覆盖索引和索引下推,程序员能控制的就是新建覆盖索引)。
所以对于如下语句:
SELECT age, name, city where age = xxx and name = xxx order by age
设计的索引应该是 (age, name,city) 或者 (name, age,city)
设计的索引应该是 (age, name,city) 或者 (name, age,city) 复合索引要有顺序,最左匹配,where和order by city没用,放在最后面
where 子句:where后面的 age name 放到索引里面
order by 子句:age 放到索引里面
select 子句,避免回表:age name city 要都在复合索引里面,这样就不用回表到聚集索引中找了
当然 了三星索引是一个比较理想化的标准,实际操作往往只能满足期望中的一颗或两颗星,考虑如下语句:
SELECT age, name, city where age = 10 AND age <= 20 and city = xxx order by name desc
假设我们分别为这三列建了联合索引,则显然它符合第三颗星(使用了覆盖索引),如果索引是(city, age, name),则虽然满足了第一颗星,但排序无法用到索引,不满足第二颗星,如果索引是 (city, name, age),则第二颗星满足了,但此时 age 在 WHERE 中的搜索条件又无法满足第一星,
另外第三颗星(尽量使用覆盖索引)也无法完全满足,试想我要 SELECT 多列,要把这多列都设置为联合索引吗,这对索引的维护是个问题,因为每一次表的 CURD 都伴随着索引的更新,很可能频繁伴随着页分裂与页合并。
综上所述,三星索引只是给我们构建索引提供了一个参考,索引设计应该尽量靠近三星索引的标准,但实际场景我们一般无法同时满足三星索引,一般我们会优先选择满足第三颗星(因为回表代价较大)至于第一,二颗星就要依赖于实际的成本及实际的业务场景考虑。
where 子句:where后面的 age city 放到索引里面
order by 子句:name 放到索引里面
select 子句,避免回表:age name city 要都在复合索引里面,这样就不用回表到聚集索引中找了
三星索引设计:
如果索引是(city, age, name),则虽然满足了第一颗星age city,但排序name无法用到索引,不满足第二颗星;
如果索引是(city, name, age),则第二颗星name满足了,但此时 age 在 WHERE 中的搜索条件又无法满足第一星。
五、小结
聪明地使用索引,完成了。
天天打码,天天进步!
以上是关于索引第三篇:聪明地使用索引的主要内容,如果未能解决你的问题,请参考以下文章