不得不说,如何创建索引已经是我们开发人员必须掌握的技能之一了。在设计系统数据表时,你可能会根据具体业务需求,给对应的某个表字段添加普通索引或唯一索引;也可能根据最左前缀原则、索引下推特性和覆盖索引,将多个列揉成一个联合索引来使用。
当同事问我一些创建索引的经验时,作为一个久经沙场的老程序员,我建议尽量让每条SQL中的where、group by、order by条件都能最大化使用索引。当然,在写多读少和读多写少的不同场景下使用方式也不尽相同。我们在保证SQL执行效率的同时,还要关注到数据库对索引文件的维护成本,从容应对那些常见又很惹人烦的场景诸如:模糊查询、大文本检索、超大分页等。
一、明确索引的优缺点
知己知彼,百战不殆。想正确的使用索引,首先我们要知道索引的特性以及他的优缺点。
1-1、优点
索引大大减小了服务器需要扫描的数据量(数据页)
索引可以帮助服务器避免排序和临时表
索引可以将随机I/O变成顺序I/O
1-2、缺点
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存索引文件。
建立索引会占用磁盘空间的索引文件。一般情况这个问题不算严重,但如果你在一个大表上创建了多种组合索引,且伴随大量数据量插入,索引文件大小也会快速膨胀。
索引的目的是提高查询效率,和我们在图书馆借书一样:需先定位到分类区 → 书架 → 书 → 章节 →页数。图书馆可以看做数据库,如果将所有数据乱放,相信一天你也找不到你想要的那篇《葵花宝典》。换位思考,其实服务器也很累,对它好点~
其实索引的本质都是通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以高效锁定某数据的同时,还可以快速定位范围以及排序工作。
一般应用系统中的读写比例会在10:1 ~ 15:1甚至更高,而插入操作和更新删除操作(我们成为DML操作)很少在性能上出问题,多只是在事务处理方面。在生产环境中,我们遇到更多的性能问题还是出现在一些复杂的查询SQL中。因此,对查询语句的索引优化显然是重中之重。
说到索引,我们一定要了解他的数据结构以及他的存储和查询方式。拿MysQL来说,InnoDB、MyISAM、Memory每个存储引擎的都有所不同。
二叉排序树 → 二叉平衡树 → B-Tree(B树) → B+Tree(B+树)
对于MySQL最常用的InnoDB引擎,数据结构为B+Tree,选用B+树是经历了漫长的演化(如上)。
需要说明的是,B+Tree的特性是N叉树+有序存储。B+树的叶子节点间按顺序建立了链指针,加强了区间访问性,所以B+树对索引对范围查询和排序有天然的优势。
二、开发中创建索引时要注意哪些(经验之谈)
咱们本文的示例我们构造一张简单的LOL英雄信息表,如下:
#来源公众号【码农编程进阶笔记】
mysql> select * from t_lol;
+----+--------------+--------------+-------+
| id | hero_title | hero_name | price |
+----+--------------+--------------+-------+
| 1 | 刀锋之影 | 泰隆 | 6300 |
| 2 | 迅捷斥候 | 提莫 | 6300 |
| 3 | 光辉女郎 | 拉克丝 | 1350 |
| 4 | 发条魔灵 | 奥莉安娜 | 6300 |
| 5 | 至高之拳 | 李青 | 6300 |
| 6 | 无极剑圣 | 易 | 450 |
| 7 | 疾风剑豪 | 亚索 | 6300 |
| 8 | 女枪 | 好运 | 1350 |
+----+--------------+--------------+-------+
8 rows in set (0.00 sec)
2-1、尽量构造覆盖索引
比如你创建了hero_name,price 的索引 idx_name_price(hero_name,price),查询数据时使用这种姿势:
SELECT * from t_lol where hero_name = '亚索' and price = 6300;
由于该索引中只存有hero_name、price和主键列,命中索引后,select *的其他字段怎么办呢?数据库还必须回到聚集索引中通过主键查找其他列数据,这就是回表,这也是你背的那条:少用select * 的原因,他会使SQL错失对覆盖索引的使用。
我们通过EXPLAIN检查一下SQL执行情况,发现虽然使用上了索引,但确实未达到覆盖索引,发生了回表。当数据量很大时,回表耗时可能会达到覆盖索引的十倍以上。
mysql> EXPLAIN SELECT * from t_lol where hero_name = '亚索' and price = 6300;
+----+-------------+-------+------------+------+--------------------------+----------------+---------+-------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+--------------------------+----------------+---------+-------------+------+----------+-------+
| 1 | SIMPLE | t_lol | NULL | ref | idx_price,idx_name_price | idx_name_price | 136 | const,const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+--------------------------+----------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
而如果只查select hero_name,price两列,或再加上主键id这列,都可以使用上覆盖索引不用再回表。即key=idx_name_price;Extra=Using index;
mysql> EXPLAIN SELECT hero_name,price from t_lol where hero_name = '亚索' and price = 6300;
+----+-------------+-------+------------+------+--------------------------+----------------+---------+-------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+--------------------------+----------------+---------+-------------+------+----------+-------------+
| 1 | SIMPLE | t_lol | NULL | ref | idx_price,idx_name_price | idx_name_price | 136 | const,const | 1 | 100.00 | Using index |
+----+-------------+-------+------------+------+--------------------------+----------------+---------+-------------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
2-2、创建复用性强的索引
还是这张t_lol表,如果增加一个高频接口,通过价格(price)查询英雄绰号(hero_title),那我们创建的idx_name_price(hero_name,price)索引还能用么?
mysql> explain select * from t_lol where price =6300;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | t_lol | NULL | ALL | idx_price | NULL | NULL | NULL | 8 | 62.50 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec
key=NULL;Extra=Using where;显然是没有用上索引idx_name_price(hero_name,price),因为在MySQL中索引履行最左前缀原则。这个最左前缀可以是联合索引的最左X个字段,也可以是字符串索引的最左Y个字符。
最左前缀原则
B+树的节点存储索引顺序是从左向右存储(说明一下,这个左到右只是说逻辑上的单向有序,并不是左边和右边。。别犟),在匹配的时候自然也要满足从左向右匹配;
通常我们在建立联合索引的时候,也就是对多个字段建立索引,相信建立过索引的同学们会发现,无论是Oracle还是 MySQL 都会让我们选择索引的顺序,比如我们想在a,b,c三个字段上建立一个联合索引,我们可以选择自己想要的优先级,a、b、c,或者是b、a、c 或者是c、a、b等顺序。为什么数据库会让我们选择字段的顺序呢?不都是三个字段的联合索引么?这里就引出了数据库索引的最左前缀原理。
在我们开发中经常会遇到明明这个字段建了联合索引,但是SQL查询该字段时却不会使用索引的问题。比如索引abc_index:(a,b,c)是a,b,c三个字段的联合索引,下列sql执行时都无法命中索引abc_index的;
select * from table where c = '1';
select * from table where b ='1' and c ='2'
以下三种情况却会走索引:
select * from table where a = '1';
select * from table where a = '1' and b = '2';
select * from table where a = '1' and b = '2' and c='3';
从上面两个例子大家是否阔以看出点眉目?
是的,索引abc_index:(a,b,c),只会在(a)、(a,b)、(a,b,c) 三种类型的查询中使用。其实这里说的有一点歧义,其实(a,c)也会走,但是只走a字段索引,不会走c字段。
另外还有一个特殊情况说明下,下面这种类型的也只会有 a与b 走索引,c不会走。
select * from table where a = '1' and b > '2' and c='3'
像上面这种类型的sql语句,在a、b走完索引后,c已经是无序了,所以c就没法走索引,优化器会认为还不如全表扫描c字段来的快。
最左前缀:顾名思义,就是最左优先,上例中我们创建了a_b_c多列索引,相当于创建了(a)单列索引,(a,b)组合索引以及(a,b,c)组合索引。
因此,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
我们明白最左前缀原则后发现,根本无法做到让每个请求都最大化利用到索引,总不能一个接口就加一个索引吧?
mysql> select * from t_lol;
+----+--------------+--------------+-------+
| id | hero_title | hero_name | price |
+----+--------------+--------------+-------+
| 1 | 刀锋之影 | 泰隆 | 6300 |
| 2 | 迅捷斥候 | 提莫 | 6300 |
| 3 | 光辉女郎 | 拉克丝 | 1350 |
| 4 | 发条魔灵 | 奥莉安娜 | 6300 |
| 5 | 至高之拳 | 李青 | 6300 |
| 6 | 无极剑圣 | 易 | 450
| 7 | 疾风剑豪 | 亚索 | 6300 |
| 8 | 女枪 | 好运 | 1350 |
+----+--------------+--------------+-------+
8 rows in set (0.00 sec)
回到我们上面提到的问题,如果有一个高频接口:通过价格(price)查询英雄绰号(hero_title),那我岂不是还要新建一个单独的index(price)索引?
其实这里引出了一个问题,在建立联合索引的时候,如何安排索在引内的字段顺序?
也就是索引的复用能力。
因为可以支持最左前缀,所以当已经有了idx_name_price(hero_name,price)这个联合索引后,一般就不需要单独在hero_name上建立索引了。但单独查price时是无法使用该联合索引的,那么如果要使用该索引还能满足通过price列查询的需求。怎么办?正如你所想的,修改索引列顺序。
因此,第一原则是,如果通过调整顺序,可第以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
所以你应该知道,这段开头的问题里,我们既要为高频请求创建(price,hero_name)这个联合索引,并用这个索引支持根据price查询hero_title的需求。那么我们只需将联合索引顺序修改为 idx_name_price(price,hero_name)即可。
2-3、索引不是越多越好
很显然,我们在文章前面提到的索引缺点处就做出了说明,索引是把双刃剑,提高查询效率的同时还需要使用数据库中大量资源去维护他。越来越大的索引文件、越来越慢的DML操作都是需要考虑的后果。
因此我们在创建索引时需要根据实际场景的需求,是读多写少还是读少写多?数据量创建索引的必要性?索引的硬伤?等。
有同学问我数据量少时(几十条?)创建索引和不创建索引查询效率和维护成本上会有多少区别?
2-4、使用索引的一些暖心建议
只要列中包含有null值都将不会被包含在索引中,复合索引中只要有一列含有null值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时建议不要让字段的默认值为null。
2、使用短索引
3、索引列排序
4、like语句操作
一般情况下不推荐使用like操作,如果非使用不可,如何使用也是一个问题。like %陈% 不会使用索引而like 陈%可以使用索引。
5、不要在列上进行运算
这将导致索引失效而进行全表扫描,例如
SELECT * FROM table_name WHERE YEAR(column_name)<2017;
6、不使用not in和<>这类非集操作
这不属于支持的范围查询条件,不会使用索引。
总结
在我们实际操作索引前,建议根据实际需求,结合搜索引擎索引特性,先设计好每张表的索引类型和结构,尽量避免边写边改。数据量剧增后再想修改索引是很麻烦的,需要很长的修改时间,且修改时会锁表。对了,千万不要随意修改线上库的索引,别问我为什么。。
精彩文章推荐