MYSQL 索引的设计——注意事项

探讨InnoDB表设计中自增ID作为主键的优势,解析索引原理及优化技巧,包括索引的合理使用、避免全表扫描、前缀索引、冗余索引处理及SQL优化建议。

关于Innodb的primary key 的建议

我们建表使用一个和存储数据无关的自增ID作为primary key,而主键是unique key;原因如下:

  1. 自增id作为primary key,数据插入的时候是循序插入的,插入快,而且是聚簇索引,避免空间的浪费;如果主键作为primary key,一般主键都是长的字符串,插入是随机的,这样调整索引树的结构会很消耗资源;而且局促索引,导致叶节点分裂严重,浪费空间;
  2. 其他索引的叶节点除了存储索引数据,还要存储primary key(用于访问数据项来获取索引以外的其他字段),自增id定义为bigint才占用8字节;然而,主键一般是字符串,会很占据很大空间,浪费资源。

什么列上适合建索引,什么列上不适合建索引

索引一定有益吗?

答案是否定的,因为索引是有代价的
索引就是把数据库的列数据进行特殊数据接口的排序,达到根据改列字段快速查找的目的;所以每次写入数据,索引数据要重新调整,索引需要维护,所以大量的索引必然降低写性能,无用和冗余的索引一定要删除,否则它只会带来维护开销,没有收益;

索引一定比扫描全表快吗?

答案是否定的

mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 0) tmp;
+----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (11.59 sec)
  
mysql> select sql_no_cache count(1) from (select * from novel_agg_info ignore index(idx_public_status)  where public_status = 0) tmp;
+----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (8.46 sec)

第二条语句忽略索引,全表扫描,比使用索引快;因为索引回访表的操作,因为符合条件的数据量特别大,所以大量回放表操作导致大量的IO,性能下降;全表扫描虽说表空间比索引空间大,但是使用多块读写性,一定程度比索引快;


where条件中不要在索引字段侧进行任何运算(包括隐式运算),否则会导致索引不可用,导致全表扫描

select * from tab where id + 1 = 1000; 会导致全表扫描,应该修改为select * from tab where id = 1000 -1; 才可以高效返回.
这里的运算也包括隐式的运算,比如说隐式的类型转换…业务上经常有类型不匹配导致隐式的类型转换的情况.这里经常出现的情况是字符串和整型比较.
比如说表定义字段类型为BIGINT,但业务上传进来一个字符串的; 或者是表定义字段类型为varchar,但业务上传进来一个整型的.这个字段上存在索引时,索引也许是不可用的.

为什么说也许呢?这取决于这种隐式的类型转换发生在了哪侧?是表字段侧,还是业务传入数据侧?

整型和字符串比较,DB中和许多程序语言中的处理方式是一样的,都是字符串转换为整型后和整型比较.

所以表定义字段类型为BIGINT,但业务上传进来一个字符串,字段上的索引依然可用,因为隐式的类型转换发生在业务传入数据侧(这只能说是索引依然可用,没有大的性能影响,但隐式的类型转换照样是有性能损耗的,所以还是一致的好)。

表定义字段类型为varchar,但业务上传进来一个整型,会导致索引不可用,全表扫描.因为隐式的类型转换发生在表字段侧。

建议可以使用INT/BIGINT存储的,尽量定义为INT/BIGINT,这样相对于长的纯数字字符串的VARCHAR定义,INT/BIGINT不仅更节省空间(INT 4个字节,BIGINT 8个字节),性能更好;而且即使类型不匹配了,也不会导致索引不可用的问题.


不要使用%xxx%这种模糊匹配,会导致全表扫描/索引全扫描

where name like ‘%zhao%’ 这种前后统配的模糊查询,会导致索引不可用,全表扫描,稍好的情况是,能使用覆盖索引的话,是索引全扫描,但也高效不了.

如果确实存在这样高频执行的模糊匹配的业务需求,建议走全文检索系统,不要使用MySQL来做这个事情.

但其实很多业务,使用模糊匹配是带有很大的随意性的,完全可以改为精确匹配,从而使用字段上的索引快速定位数据的。

前缀索引和冗余索引

索引的最左原则

index(a,b,c)能同时优化一下查询:
where a=? and b=? and c=?
where a=? and b=?
where a=?
但不能优化 where b=? and c=? 因为索引定义index(a,b,c) 的前缀列a没有出现在where条件中。

在存在索引index(a,b,c)的情况下,绝大多数情况下,下面的这些索引就冗余了,可以DROP掉的:
index(a)
index(a,b)
上面提到了,这2个索引能优化的查询,index(a,b,c)绝大多数情况下也都能优化,所以它们就冗余了,本着索引越少越好的原则,都可以DROP掉的.
上面提到了绝大多数情况下,冗余了,可以DROP了,但也存在例外的情况,它们的存在还是必要的:

那就是存在下面的查询:
where a=? order by id [limit] (这里id是表的primary key)
这里index(a) ( 实际为index(a,id) ) 可以优化上面的查询,通过使用这个索引,避免物理排序而达到排序的实际效果.
但index(a,b,c) ( 实际为index(a,b,c,id) ) 和index(a,b) (实际为index(a,b,id)) 却达不到这样的效果.
这种情况下,存在index(a,b,c)的情况下,index(a) 是不冗余的,是需要保留的.
如果不存在这种情况,存在index(a,b,c)的情况下,index(a) ,index(a,b) 都是冗余的,建议drop掉.


关于索引中的字段顺序

建议where条件中等值匹配的字段放到索引定义的前部,范围匹配的字段(> < between in等为范围匹配)放到索引定义的后面.

因为前缀索引字段使用了范围匹配后,会导致后续的索引字段不能高效的用于优化查询.

mysql> select count(1) from opLog  where opName='zhangyu21' and createTime between 1492876800 and 1495468800;

索引1. index(opName,createTime)
索引2. index(createTime,opName)

索引1 首先匹配等于opName=‘zhangyu21’,createTime =1492876800 条目,向后扫描,直至opName=‘zhangyu21’,createTime>1495468800或者opName!='zhangyu21’为止,它是相当高效的,扫描的条目就是返回的条目.
索引2,因为在前缀索引字段createTime上使用了范围匹配,所以导致索引定义中后面的字段opName不能作为高效的检索字段(Access),只能作为低效的过滤字段(Filter)了.


sql语句查询优化

使用覆盖索引

index(a,b) 逻辑上是有序的,所以可以用于优化 where a=? order by b desc limit N; 特别是对这种topN操作的优化效果非常好。

等值匹配

index(opName, listType, createTime)

mysql> explain select * from opLog where opName='' and listType in ('cronJob','cronJobNew') order by createTime desc limit 1;
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+
| id | select_type | table | type | possible_keys                  | key                            | key_len | ref   | rows    | Extra                                              |
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+
|  1 | SIMPLE      | opLog | ref  | idx_opName_listType_createTime | idx_opName_listType_createTime | 767     | const | 1252640 | Using index condition; Using where; Using filesort |
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+

执行计划来看,还是有Using filesort,还是需要物理排序的. 为什么不能通过这个索引避免物理排序,快速的返回结果呢?
原因就在于listType in (‘cronJob’,‘cronJobNew’) 在这个索引字段上使用了范围匹配,从而导致索引层面上整体不再有序了。
这样索引只把所有符合条件的筛选出来再进行物理排序。
在排序字段前的所有索引字段上都必须是等值匹配,才能通过索引保证有序性,才能通过索引避免物理排序,快速的返回结果.
避免物理操作:

select *
from
(
     select * from
     (
           select * from opLog where opName='' and listType = 'cronJob' order by createTime desc limit 1
     ) tmp_1
    union all
    select * from
    (
          select * from opLog where opName='' and listType = 'cronJobNew' order by createTime desc limit 1
     ) tmp_2
) tmp
order by createTime desc limit 1;

经过改造为等效的等值匹配,使用索引避免了大的物理排序操作,快速的返回了结果。

sql优化不一定合理

说到通过索引优化排序查询,特别是TOPN操作,必须说一下MySQL在优化器层面的一个问题:
就是说在遇到order by时,myql会优先选择一个可以避免物理排序的索引来优化这个查询,有时候,这种优先选择是不合理的,会导致性能很差.
(特别在涉及到order by id limit N, 这里id是primary key,优化器选择使用PRIMARY KEY来避免物理排序时尤其要注意是否合理了)

index(app_id),id是primary key

SELECT * FROM `layer`  WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1;

上面查询语句sql为了避免物理排序,不会使用index(app_id,id)的索引,而是直接扫描主键id,直到app_id = 2183,所以数据的多少取决于app_id在主键索引的位置,如果app_id不存在,那么就会扫描整张表。

解决方案:强制使用index(app_id,id)组合索引

SELECT * FROM `layer` force index(idx_app_id) WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值