概念
官方定义:索引是帮助mysql高效获取数据的数据结构。所以索引的本质就是数据结构,可以理解为排好序的快速查找的数据结构。
数据本身之外,数据库还维护着一个满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法,这种数据结构就是索引。
优势
- 类似于图书馆书目索引,提高数据检索效率,降低数据库的io成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低cpu的消耗
劣势
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要更新数据,还要更新一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息。同时索引还会占用一定的磁盘空间。
分类
- 单值索引
一个索引只包含单个列,一个表可以有多个单列索引 - 唯一索引
索引列的值必须唯一,但允许空值 - 复合索引
即一个索引包含多个列
语法
- 创建
- CREATE [UNIQUE] INDEX indexName ON tableName(字段名...);
- ALTER TABLE tableName ADD [UNIQUE] INDEX indexName (字段名...);
- 删除
DROP INDEX indexName ON tableName; - 查看
SHOW INDEX FROM tableName
索引的最佳实践
哪些情况创建索引
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引(where 后面的语句或者ORDER BY 语句中出现的列)
- 查询中与其他表关联的字段,外键关系建立索引
- 单键、组合索引的选择问题(在高并发下推荐创建组合索引)
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或者分组的字段
哪些情况不创建索引
- 表记录太少
- 经常增删改的表
- 数据重复且分布平均的表字段,因此只为最经常查询和最经常排序的数据列创建索引。注意,如果某个数据列包含许多重复的内容,为他建立索引就没有太大的实际效果。(比如14亿中国人的国籍都是中国,这种类型字段就可以不建立索引,或者性别)
- Where条件里用不到的字段不创建索引
性能分析
使用EXPLAIN关键字可以模拟优化器执行sql查询语句,从而知道mysql是如何处理你的sql语句。分析你的查询语句或是表结构的性能瓶颈。
语法
explain sql语句
相关字段
每列的定义如下
- id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符
- id相同,执行顺序由上至下
- id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
- select_type: SELECT 查询的类型.主要是用于区别普通查询、联合查询、子查询等的复杂查询
- SIMPLE:简单的 select 查询,查询中不包含子查询或者UNION
- PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为Primary
- DERIVED:在FROM列表中包含的子查询被标记为DERIVED(衍生)MySQL会递归执行这些子查询, 把结果放在临时表里。
- SUBQUERY:在SELECT或WHERE列表中包含了子查询
- UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
- UNCACHEABLE SUBQUREY:无法被缓存的子查询
- DEPENDENT SUBQUERY:在SELECT或WHERE列表中包含了子查询,子查询基于外层
- UNION RESULT:从UNION表获取结果的SELECT
- table: 查询的是哪个表, 显示这一行的数据是关于哪张表的
- type: join 类型(决定性能的指标) 显示查询使用了何种类型,从最好到最差依次是:
system>const>eq_ref>ref>range>index>ALL
- system:表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计
- const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快如将主键置于where列表中,MySQL就能将该查询转换为一个常量
- eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
- ref:非唯一性索引扫描,返回匹配某个单独值的所有行.本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体(例如:根据姓名查询人的信息,姓名是可以重复的,并不是唯一的字段)
- range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引 一般就是在你的where语句中出现了between、<、>、in等的查询这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。
- Index:Full IndexScan,index与ALL区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘中读的)
- All:Full Table Scan,将遍历全表以找到匹配的行
备注:一般来说,得保证查询至少达到range级别,最好能达到ref。
- possible_keys: 此次查询中可能选用的索引显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用
- key: 此次查询中确切使用到的索引
- Key_len: 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。key_len字段能够帮你检查是否充分的利用上了索引
- ref: 哪个字段或常数与key一起被使用.显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值
- rows: 显示此查询一共扫描了多少行. 这个是一个估计值.(找到所需记录扫描的行数)
- filtered: 表示此查询条件所过滤的数据的百分比
- extra: 包含不适合在其他列中显示但十分重要的额外信息
- Using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为“文件排序”
- Using where:表明使用了where过滤
- Using Index :表示索引覆盖(Covering Index),不会回表查询("覆盖索引扫描",表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错)
- Using temporary: 使了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。
- Covering Index:覆盖索引,就是查询的列要被所建的索引覆盖
主要字段:Id/type/key/rows/Extra
查询优化
索引的失效情况以及索引的优化
测试sql
CREATE TABLE staffs (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR (24) NULL DEFAULT '' COMMENT '姓名',
age INT NOT NULL DEFAULT 0 COMMENT '年龄',
pos VARCHAR (20) NOT NULL DEFAULT '' COMMENT '职位',
add_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间'
) CHARSET utf8 COMMENT '员工记录表' ;
INSERT INTO staffs(NAME,age,pos,add_time) VALUES('z3',22,'manager',NOW());
INSERT INTO staffs(NAME,age,pos,add_time) VALUES('July',23,'dev',NOW());
INSERT INTO staffs(NAME,age,pos,add_time) VALUES('2000',23,'dev',NOW());
INSERT INTO staffs(NAME,age,pos,add_time) VALUES(null,23,'dev',NOW());
SELECT * FROM staffs;
ALTER TABLE staffs ADD INDEX idx_staffs_nameAgePos(name, age, pos);
- 全值匹配我最爱
索引 idx_staffs_nameAgePos 建立索引时 以 name , age ,pos 的顺序建立的。全值匹配表示 按顺序匹配的
EXPLAIN SELECT * FROM staffs WHERE NAME = 'July';
EXPLAIN SELECT * FROM staffs WHERE NAME = 'July' AND age = 25; EXPLAIN SELECT * FROM staffs WHERE NAME = 'July' AND age = 25 AND pos = 'dev'; 2. 最佳左前缀法则
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
and 忽略左右关系。既即使没有没有按顺序 由于优化器的存在,会自动优化。
经过试验结论建立了 idx_nameAge 索引id 为主键
当使用覆盖索引的方式时,(select name/age/id from staffs where age=10 (后面没有其他没有索引的字段条件)),即使不是以 name 开头,也会使用 idx_nameAge 索引。 既 select 后的字段 有索引,where 后的字段也有索引,则无关执行顺序。
除开上述条件 才满足最左前缀法则。
EXPLAIN SELECT * FROM staffs WHERE age = 25 AND pos = 'dev';
可以看到并没有使用到索引,原因是查询条件中跳过了索引的第一个字段。EXPLAIN SELECT * FROM staffs WHERE pos = 'dev'; 3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
EXPLAIN SELECT * FROM staffs WHERE left(NAME,4) = 'July'; 4. 存储引擎不能使用索引中范围条件右边的列
EXPLAIN SELECT * FROM staffs WHERE name = 'z3' and age > 20 and pos = 'manmger'; 5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select * 可以看出使用覆盖索引性能会更好
6. mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描
使用 != 和 <> 的字段索引失效(!= 针对数值类型。 <> 针对字符类型
前提 where and 后的字段在混合索引中的位置比当前字段靠后 where age != 10 and name='xxx' ,这种情况下,mysql自动优化,将 name='xxx' 放在 age !=10 之前,name 依然能使用索引。只是 age 的索引失效)
7. is not null 也无法使用索引,但是is null是可以使用索引的
8. like以通配符开头('%abc...')mysql索引失效会变成全表扫描的操作
这种情况会导致索引失效全表扫描,而abc%这种情况下是可以使用索引的
解决方案:这种情况下尽量使用覆盖索引(即select后面的字段和所建立的索引的字段一致),部分情况下可以解决这种索引失效问题
9. 字符串不加单引号索引失效
例如某字段name的名称为100,是VARCHAR类型的但是作为查询条件100并没有加''号,底层进行转换使索引失效,使用了函数造成索引失效
10. 少用or,用它来连接时会索引失效
一般性建议
- 对于单键索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。(避免索引过滤性好的索引失效)
- 在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引
- 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的
单表查询优化
sql
CREATE TABLE IF NOT EXISTS `article` (
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`author_id` INT(10) UNSIGNED NOT NULL,
`category_id` INT(10) UNSIGNED NOT NULL,
`views` INT(10) UNSIGNED NOT NULL,
`comments` INT(10) UNSIGNED NOT NULL,
`title` VARBINARY(255) NOT NULL,
`content` TEXT NOT NULL
);
INSERT INTO `article`(`author_id`, `category_id`, `views`, `comments`, `title`, `content`) VALUES
(1, 1, 1, 1, '1', '1'),
(2, 2, 2, 2, '2', '2'),
(1, 1, 3, 3, '3', '3');
SELECT * FROM article;
- 查询 category_id 为1 且 comments 大于 1 的情况下,views 最多的 article_id。
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
开始优化:
1.1 新建索引+删除索引
ALTER TABLE
article
ADD INDEX idx_article_ccv ( category_id
, comments
, views
);create index idx_article_ccv on article(category_id,comments,views);
DROP INDEX idx_article_ccv ON article
1.2 第2次EXPLAIN
EXPLAIN SELECT id,author_id FROM article
WHERE category_id = 1 AND comments >1 ORDER BY views DESC LIMIT 1;
这是因为按照 BTree 索引的工作原理, 先排序 category_id, 如果遇到相同的 category_id 则再排序 comments,如果遇到相同的 comments 则再排序 views。 当 comments 字段在联合索引里处于中间位置时, 因comments > 1 条件是一个范围值(所谓 range), MySQL 无法利用索引再对后面的 views 部分进行检索,即 range 类型查询字段后面的索引无效。
1.3 删除第一次建立的索引 DROP INDEX idx_article_ccv ON article;
1.4 第2次新建索引
ALTER TABLE article
ADD INDEX idx_article_cv ( category_id
, views
) ;
create index idx_article_cv on article(category_id,views);
1.5 第3次EXPLAIN
EXPLAIN SELECT id,author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
关联查询优化
sql
CREATE TABLE IF NOT EXISTS `class` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
- 下面开始explain分析
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;
添加索引优化
ALTER TABLE
book
ADD INDEX Y ( card
);第2次explain
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card; 可以看到第二行的type变为了ref,rows也变成了优化比较明显。
这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有, 所以右边是我们的关键点,一定需要建立索引。
删除旧索引+新建+第3次explain
DROP INDEX Y ON book;
ALTER TABLE class ADD INDEX X (card);
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;
- 保证被驱动表的join字段已经被索引
- left join 时,选择小表作为驱动表,大表作为被驱动表。
- inner join 时,mysql会自己帮你把小结果集的表选为驱动表。
- 子查询尽量不要放在被驱动表,有可能使用不到索引。
子查询优化
用in 还是 exists
有索引 大表驱动小表
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
select sql_no_cache sum(sal) from emp where exists (select 1 from dept where emp.deptno=dept.deptno);
用 exists 是否存在,存在返回一条记录,exists 是作为一个查询判断用,所以 select 后返回什么不重要。
select sql_no_cache sum(sal) from emp inner join dept on emp.deptno=dept.deptno;
有索引 小表驱动大表
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e where exists (select 1 from emp where e.deptno=emp.deptno);
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e inner join (select distinct deptno from emp) m on m.deptno=e.deptno;
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
有索引小驱动大表性能优于大表驱动小表
无索引 小表驱动大表
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e where exists (select 1 from emp where e.deptno=emp.deptno);
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e inner join (select distinct deptno from emp) m on m.deptno=e.deptno;
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
无索引大表驱动小表
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
select sql_no_cache sum(sal) from emp where exists (select 1 from dept where emp.deptno=dept.deptno);
select sql_no_cache sum(sal) from emp inner join dept on emp.deptno=dept.deptno;
结论
有索引的情况下 用 inner join 是最好的 其次是 in ,exists最糟糕
无索引的情况下用
小表驱动大表 因为join 方式需要distinct ,没有索引distinct消耗性能较大
所以 exists性能最佳 in其次 join性能最差?
无索引的情况下大表驱动小表
in 和 exists 的性能应该是接近的 都比较糟糕 exists稍微好一点 超不过5% 但是inner join 优于使用了 join buffer 所以快很多
如果left join 则最慢
order by关键字优化
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序
MySQL支持二种方式的排序,FileSort和Index,Index效率高.
它指MySQL扫描索引本身完成排序。FileSort方式效率较低。
ORDER BY满足两情况,会使用Index方式排序:
- ORDER BY 语句使用索引最左前列
- 使用Where子句与Order BY子句条件列组合满足索引最左前列
- where子句中如果出现索引的范围查询(即explain中出现range)会导致order by 索引失效。
尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀
如果不在索引列上,filesort有两种算法:mysql就要启动双路排序和单路排序
双路排序
- MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和orderby列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。多路排序需要借助 磁盘来进行排序。所以 取数据,排好了取数据。两次 io操作。比较慢单路排序 ,将排好的数据存在内存中,省去了一次io操作,所以比较快,但是需要内存空间足够。
- 从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。
单路排序
- 取一批数据,要对磁盘进行了两次扫描,众所周知,I\O是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。从磁盘读取查询需要的所有列,按照orderby列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
单路排序引申出的问题
在sort_buffer中,方法B比方法A要多占用很多空间,因为方法B是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取取sort_buffer容量大小,再排……从而多次I/O。本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。
优化策略
- 增大sort_buffer_size参数的设置
用于单路排序的内存大小 - 增大max_length_for_sort_data参数的设置
单次排序字段大小。(单次排序请求) - 去掉select 后面不需要的字段
select后的多了,排序的时候也会带着一起,很占内存,所以去掉没有用的
GROUP BY关键字优化
- group by实质是先排序后进行分组,遵照索引建的最佳左前缀
- 当无法使用索引列,增大max_length_for_sort_data参数的设置+增大sort_buffer_size参数的设置
- where高于having,能写在where限定的条件就不要去having限定了。
去重优化
尽量不要使用distinct关键字去重
例如:
t_mall_sku 表
id shp_id kcdz
------ ------ --------------------
3 1 北京市昌平区
4 1 北京市昌平区
5 5 北京市昌平区
6 3 重庆
8 8 天津
例子:
select kcdz form t_mall_sku where id in( 3,4,5,6,8 ) 将产生重复数据,
select distinct kcdz form t_mall_sku where id in( 3,4,5,6,8 ) 使用 distinct 关键字去重消耗性能
优化: select kcdz form t_mall_sku where id in( 3,4,5,6,8 ) group by kcdz 能够利用到索引
案例
。。。。。。