一、MySQL中常见索引类型
- 普通索引:仅加速查询
- 主键索引:加速查询、列值唯一、表中只有一个(不可有null)
- 唯一索引:加速查询、列值唯一(可以有null)
- 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
索引合并:使用多个单列索引组合搜索。
覆盖索引:select的数据列只用从索引中就能够取得,不必读取数据行;换句话说,查询列要被所建的索引覆盖。
普通索引
-- 创建表同时添加name字段为普通索引 create table tb( id int not null auto_increment primary key, name char(32) not null, index idx_name(name) ); -- 单独为表指定普通索引 create index idx_name on tb(name); -- 删除索引 drop index idx_name on tb; -- 查看索引 show index from tb;
Table # 表的名称 Non_unique # 如果索引为唯一索引,则为0,如果可以则为1 Key_name # 索引的名称 Seq_in_index # 索引中的列序列号,从1开始 Column_name # 列名称 Collation # 列以什么方式存储在索引中。在MySQL中,有值‘A’(升序)或NULL(无分类) Cardinality # 索引中唯一值的数目的估计值 Sub_part # 如果列只是被部分地编入索引,则为被编入索引的字符的数目。如果整列被编入索引,则为NULL Packed #指示关键字如何被压缩。如果没有被压缩,则为NULL Null # 如果列含有NULL,则含有YES。如果没有,则该列含有NO Index_type # 用过的索引方法(BTREE, FULLTEXT, HASH, RTREE) Comment # 多种评注
主键索引
主键有两个功能:加速查询 和 唯一约束(不可含null)
注意:一个表中最多只能有一个主键索引
-- 创建表同时添加id字段为主键索引 -- 方式一 create table tb( id int not null auto_increment primary key, name char(4) not null ); -- 方式二 create table tb( id int not null auto_increment, name char(4) not null, primary key(id) ); -- 给某个已经存在的表增加主键 alter table tb add primary key(id); -- 删除主键 -- 方式一 alter table tb drop primary key; -- 方式二 -- 如果当前主键为自增主键,则不能直接删除,需要先修改自增属性,再删除 alter table tb modify id int,drop primary key;
唯一索引
唯一索引有两个功能:加速查找 和 唯一约束(可含一个null 值)
create table tb( id int not null auto_increment primary key, name char(4) not null, age int not null, unique index idx_age (age) ); -- 给某个已经存在的表创建唯一索引 create unique index idx_age on tb(age);
组合索引
组合索引是将n个列组合成一个索引
应用场景:频繁的同时使用n列来进行查询,如:select * from tb where name="pd" and id=888;
create table tb( id int not null, name char(4) not null, age int not null, index idx_name_age (name,age) ); -- 给某个已经存在的表创建组合索引 create index idx_name_age on tb(name,age);
二、聚集索引和非聚集索引(辅助索引)
数据库中的 B+tree 索引可以分为:聚集索引和非聚集索引
聚集索引:innodb表/索引组织表,即表中数据按主键B+树存放,叶子节点直接存放整条数据,每张表只能有一个聚集索引。
①当你定义一个主键时,innodb 存储引擎则把它当做聚集索引;
②如果你没有定义一个主键,则 innodb 定位到第一个唯一索引,且该索引的所有列值均飞空的,则将其当做聚集索引;
③如果表没有主键或合适的唯一索引,innodb 会产生一个隐藏的行ID值6字节的行ID聚集索引。
补充:由于实际的数据页只能按照一颗B+tree进行排序,因此每张表只能有一个聚集索引,聚集索引对于主键的排序和范围查找非常有利。
非聚集索引(辅助索引):指叶节点不包含行的全部数据,叶节点除了包含键值之外,还包含一个书签连接,通过该书签再去找相应的行数据。
innodb存储引擎辅助索引获得数据的查找方式如下:
从上图中可以看出,辅助索引叶节点存放的是主键值,获得主键值后,再从聚集索引中查找整行数据。
举个例子,如果在一颗高度为3的辅助索引中查找数据,首先从辅助索引中获得主键值(3次IO),接着从高度为3的聚集索引中查找以获得整行数据(3次IO),总共需6次IO。一个表上可以存在多个辅助索引。
聚集索引与辅助索引区别:
- 相同的是:不管是聚集索引还是辅助索引,其内部都是 B+tree 形式,即高度是平衡的,叶子结点存放着所有的数据。
- 不同的是:聚集索引叶子结点存放的是一整行的信息,而辅助索引叶子结点存放的是单个索引字段信息。
何时使用聚集索引或非聚集索引(重要!!!):
三、测试索引
1、创建表
create table userinfo( id int not null, name varchar(16) default null, age int, gender char(1) not null, email varchar(32) default null )engine=myisam default charset=utf8;
注意:MYISAM存储引擎不产生引擎事务,数据插入速度极快,为方便快速插入测试数据,等我们插完数据,再把存储类型修改为InnoDB。
2、创建存储过程
create procedure insert_userinfo(in num int) begin declare i int default 0; declare n int default 1; -- 循环进行数据插入 while n<=num do set i=rand()*50; insert into userinfo(id,name,age,gender,email) values(n,concat("pink",i),rand()*50,if(i%2=0,"女","男"),concat("pink",n,"@qq.com")); set n=n+1; end while; end;
3、调用存储过程,插入500万条数据
call insert_userinfo(5000000);
4、修改引擎为INNODB
alter table userinfo engine=innodb;
5、测试索引
①在没有索引的前提下测试查询速度
select * from userinfo where id=4567890;
注意:无索引情况,mysql根本就不知道id等于4567890的记录在哪里,只能把数据表从头到尾扫描一遍,此时有多少个磁盘块就需要进行多少IO操作,所以查询速度很慢。
②在表中已经存在大量数据的前提下,为某个字段段建立索引,建立速度会很慢
create index idx_id on userinfo(id);
③在索引建立完毕后,以该字段为查询条件时,查询速度提升明显
select * from userinfo where id=4567890;
注意:
- mysql先去索引表里根据 b+树 的搜索原理很快搜索到id为4567890的数据,IO大大降低,因而速度明显提升
- 我们可以去mysql的data目录下找到该表,可以看到添加索引后该表占用的硬盘空间多了
- 如果使用没有添加索引的字段进行条件查询,速度依旧会很慢(如下图)
四、正确使用索引
数据库表中添加索引后确实会让查询速度起飞,但前提必须是正确的使用索引来查询,如果以错误的方式使用,则即使建立索引也会不奏效。
即使建立索引,索引也不会生效,例如:
-- 范围查询(>、>=、<、<=、!= 、between...and) -- = 等号 select count(*) from userinfo where id=1000; -- 执行索引,索引效率高 -- >、>=、<、<=、between...and 区间查询 select count(*) from userinfo where id<100; -- 执行索引,区间范围越小,索引效率越高 select count(*) from userinfo where id>100; -- 执行索引,区间范围越大,索引效率越低 select count(*) from userinfo where id between 10 and 500000; -- 执行索引,区间范围越大,索引效率越低 -- != 不等于 select count(*) from userinfo where id!=1000; -- 索引范围大,索引效率低 -- like "%xx%" -- 为name字段添加索引 create index idx_name on userinfo(name); select count(*) from userinfo where name like "%xxxx%"; -- 全模糊查询,索引效率低 select count(*) from userinfo where name like "%xxxx"; -- 以什么结尾模糊查询,索引效率低 -- 例外:当like使用以什么开头,索引效率高 select * from userinfo where name like "xxxx%";
-- or select count(*) from userinfo where id=1000 or email="xx"; -- email不是索引字段,索引此查询全表扫描 -- 例外:当or条件中有未建立索引的列才失效,以下会走索引 select count(*) from userinfo where id=1000 or name="pink3"; -- id和name都为索引字段时,or条件也会执行索引
-- 使用函数 select count(*) from userinfo where reverse(name)="1knip"; -- name索引字段,使用函数时,索引失效 -- 例外:索引字段对应的值可以使用函数,我们可以改为以下形式 select count(*) from userinfo where name=reverse("1knip");
-- 类型不一致 -- 如果列是字符串类型,传入条件是必须用引号引起来 select count(*) from userinfo where name=123; -- 慢 -- 类型一致 select count(*) from userinfo where name="123"; -- 快
-- order by -- 排序条件为索引,则select字段必须也是索引字段,否则无法命中 select email from userinfo order by name desc; -- 无法命中索引 select name from userinfo order by name desc; -- 命中索引
五、组合索引
组合索引:是指对表上的多个列组合起来做一个索引。
组合索引好处,简单的说有两个主要原因:
- 一个顶三个;建了一个(a,b,c)的组合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,因为每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,这可是不小的开销!
- 索引列越多,通过索引筛选出的数据越少。有1000W条数据的表,有如下sql:select * from table where a=1 and b=2 and c=3,假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W*10%=100w条数据,然后再回表从100w条数据中找到符合 b=2 and c= 3 的数据,然后再排序,再分页;如果是组合索引,通过索引筛选出1000w*10%*10%*10%=1w,然后再排序、分页,哪个更高效,一眼便知 。
最左匹配原则:从左往右依次使用生效,如果中间某个索引没有使用,那么断点前面的索引部分起作用,断点后面的索引没有起作用。
select * from tb where a=1 and b=2 and c=3; -- abc三个索引都在where条件里面用到了,而且都发挥了作用 select * from tb where c=3 and b=2 and a=1; -- 这条语句列出来只想说明mysql没有那么笨,where里面的条件顺序在查询之前会被mysql自动优化,效果跟上一句一样 select * from tb where a=1 and c=3; -- a用到索引,b没有用,所以c是没有用到索引效果的 select * from tb where a=1 and b>2 and c=3; -- a用到了,b也用到了,c没有用到,这个地方b是范围值,也算断点,只不过自身用到了索引 select * from tb where a>1 and b=2 and c=3; -- a用到了,b没有使用,c没有使用 select * from tb where a=1 order by b; -- a用到了索引,b在结果排序中也用到了索引的效果 select * from tb where a=1 order by c; -- a用到了索引,但是c没有发挥排序效果,因为中间断点了 select * from tb where b=2 order by a; -- b没有用到索引,排序中a也没有发挥索引效果
六、注意事项(重要)
1、避免使用select *
2、其他数据库中使用count(1)或count(列)代替count(*),而mysql数据库中count(*)经过优化后,效率与前两种基本一样
3、创建表时尽量时 char 代替 varchar
4、表的字段顺序固定长度的字段优先
5、组合索引代替多个单列索引(经常使用多个条件查询时)
6、使用连接(JOIN)来代替子查询(Sub-Queries)
7、不要有超过4个以上的表连接(JOIN)
8、优先执行那些能够大量减少结果的连接
9、连表时注意条件类型需一致
10、索引散列值不适合建索引,例:性别不适合
七、慢查询日志
慢查询日志:将mysql服务器中影响数据库性能的相关SQL语句记录到日志文件,通过对这些特殊的SQL语句分析,改进以达到提高数据库性能的目的。
慢查询日志参数:
long_query_time # 设定慢查询的阀值,超出设定值的SQL即被记录到慢查询日志,缺省值为10s slow_query_log # 指定是否开启慢查询日志 log_slow_queries # 指定是否开启慢查询日志(该参数已经被slow_query_log取代,做兼容性保留) slow_query_log_file # 指定慢日志文件存放位置,可以为空,系统会给一个缺省的文件host_name-slow.log log_queries_not_using_indexes # 如果值设置为ON,则会记录所有没有利用索引的查询
查看MySQL慢日志信息:
-- 查询慢日志配置信息 show variables like "%query%"; -- 修改配置信息 set global slow_query_log = on;
查看不使用索引参数状态:
-- 显示参数 show variables like "%log_queries_not_using_indexes"; -- 开启状态 set global log_queries_not_using_indexes = on;
查看慢日志显示的方式:
-- 查看慢日志记录的方式 show variables like "%log_output%"; -- 设置慢日志在文件和表中同时记录 set global log_output="FILE,TABLE";
测试慢查询日志:
-- 查询时间超过10秒就会记录到慢查询日志中 select sleep(3) from userinfo; -- 查看表中的日志 select * from mysql.slow_log;
八、执行计划
explain + 查询SQL:用于显示SQL执行信息参数,根据参考信息可以进行SQL优化。
explain select count(*) from userinfo where id=1;
执行计划:让mysql预估执行操作(一般正确)。 type:查询计划的连接类型, 有多个参数,先从最佳类型到最差类型介绍 性能:null > system/const > eq_ref > ref > ref_or_null > index_merge > range > index > all 慢: explain select * from userinfo where email="pink"; type: ALL(全表扫描) 特别的: select * from userinfo limit 1; 快: explain select * from userinfo where name="alex"; type: ref(走索引)
九、大数据量分页优化
执行此段代码:
select * from userinfo limit 3000000,10;
优化方案:
1、简单粗暴,不允许查看这么靠后的数据。比如百度就是这样的,最多翻到76页就不让你翻了,这种方式就是从业务上解决。
2、在查询下一页时把上一页的行id作为参数传递给客户端程序,然后sql就改成了
select * from userinfo where id>3000000 limit 10;
这条语句执行也是在毫秒级完成的,id>300w 其实就是让mysql直接跳到这里了,不用依次在扫描全面所有的行。
3、延迟关联
分析一下这条语句为什么慢,慢在哪里:
select * from userinfo limit 3000000,10;
慢就慢在这个 * 里面,这个表除了id主键肯定还有其他字段,比如 name、age 之类的,因为select * 所以mysql在沿着id主键走的时候要回行拿数据,走一下拿一下数据;如果把语句改成:
select id from userinfo limit 3000000,10;
你会发现时间缩短了很多;然后我们在拿id分别去取10条数据就行了。语句就改成了这样:
select userinfo.* from userinfo inner join (select id from userinfo limit 3000000,10) as tmp on tmp.id=userinfo.id;
PS:三种方法最先考虑第一种,其次第二种,第三种是别无选择。