Java面试前准备---数据库索引

1. MySQL索引原理

索引目的  
索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从上往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有的单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成?

索引原理 
除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的目录等。它们的原理都是一样的,通过不断的缩小想要获得数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是我们总是通过同一种查找方法来锁定数据

数据库也是一样,但显然要复杂许多,因为不仅面临着等值查询,还有范围查询(>、<、between、in)、模糊查询(like)、并集查询(or)等等。数据库应该选择怎么样的方式来应对所有的问题呢?我们回想字典的例子,能不能把数据分成段,然后分段查询呢?最简单的如果1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段……这样查第250条数据,只要找第三段就可以了,一下子去除了90%的无效数据。但如果是1千万的记录呢,分成几段比较好?稍有算法基础的同学会想到搜索树,其平均复杂度是lgN,具有不错的查询性能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的,数据库实现比较复杂,数据保存在磁盘上,而为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本大概是访问内存的100 000 倍左右,所以简单的搜索树难以满足复杂的应用场景。

磁盘IO与预读 
磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1s能转120次,旋转延迟就是1/120/2=4.17ms(为什么要再除一个2?);传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在0点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17=9ms左右,听起来还不错,但要知道一台500-MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。下图是计算机硬件延迟的对比图,供大家参考: 
硬件延迟的对比图 
考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区中,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据 我们成之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生一次IO,这个理论对于索引的数据结构设计非常有帮助。

索引的数据结构 
前面讲了生活中索引的例子,索引的基本原理,数据库的复杂性,又讲了操作系统的相关知识,目的就是让大家了解,任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在总结一下,我们需要这种数据结构能够做些什么,其实很简单,那就是:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。

详解b+树 
btree

如上图,是一颗b+树,关于b+树的定义可以参见b+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实的存在于数据表中。

b+树的查找过程 
如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

b+树性质 
(1)通过上面的分析,我们知道IO次数取决于b+树的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=log(m+1)N,当数据量N一定的情况下,m越大,h越小;而m=磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越大,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。 
当数据项等于1时将会退化成线性表。

(2)当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是暗战从左到右的顺序来建立搜索树的,比如当(张三,20, F)这样的数据来检索时,b+树会优先比较name来确定下一步的搜索方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了,这个是非常重要的性质,即索引的最左匹配特性

MySQL索引的类型 
参考 聚簇索引与非聚簇索引的区别 
MYSQL性能调优: 对聚簇索引和非聚簇索引的认识 
索引分为聚族索引和非聚族索引,聚族索引是按照数据存放的物理位置为顺序的,而非聚族索引与数据物理排序顺序无关。正是因为如此,所以一个表最多只能有一个聚族索引。在《数据库原理》一书中是这么解释聚族索引和非聚族索引的区别的:聚族索引的叶子节点就是数据节点,而非聚族索引的叶子节点仍然是索引结点,只不过有指向对应数据块的指针。 
两种索引的结构如下图所示: 
index

pic

MySQL中MYISAM和Innodb两种引擎的索引结构

如原始数据为:

原始数据

MyISAM引擎的数据存储方式如图:

MyISAM1

MyISAM是按列值与行号来组织索引的。它的叶子节点中保存的实际上是指向存放数据的物理块的指针。从MyISAM存储的物理文件我们能看出,MyISAM引擎的索引文件(.MYI)和数据文件(.MYD)是相互独立的。

而Innodb按聚族索引的形式存储数据,所以它的数据布局有着很大的不同。它存储数据的结构大致如下:

Innodb1

注:聚族索引中的每个叶子节点包含主键值、事务ID、回滚指针(rollback pointer用于事务和MVCC)和余下的列(如col2).

Innodb的二级索引与主键索引有很大的不同。Innodb的二级索引的叶子包含主键值,而不是行指针(row pointers),这减少了移动数据或者数据页面分裂时维护二级索引的开销,因为Innodb不需要更新索引的开销,因为Innodb不需要更新索引的行指针。其结构大致如下: 
Innodb2

Innodb和MyISAM的主键索引与二级索引的对比:

all

Innodb的二级索引的叶子节点存放的是key字段加主键值。因此,通过二级索引查询首先查到的是主键值,然后Innodb再根据查到的主键值索引找到对应的数据块。而MyISAM的二级索引叶子节点存放的是列值与行号的组合,叶子节点中保存的是数据的物理地址。所以可以看出MyISAM的主键索引和二级索引没有任何区别,主键索引仅仅只是一个叫做PRIMARY的唯一、非空的索引,且MYISAM引擎中可以不设主键。

我的理解:

  • 索引的数据结构可以用B+树来实现。
  • 索引的类型有聚族索引和非聚族索引。聚族索引与非聚族索引的区别是聚族索引的叶子节点就是数据节点,而非聚族索引的叶子节点还是索引节点,即存储的是指向数据的物理地址。
  • 不同的引擎在创建主键索引和二级索引所使用的索引类型是不一样的。例如:MyISAM的主键索引和二级索引的叶子节点存储的都是指向数据的物理地址(都采用聚族索引)。而Innodb的二级索引的叶子节点存储的
    是key和主键值,主键索引存储的是直接数据。(这块有点模糊,有错的话还望指出)

2. MySQL索引使用方法

索引的类型

普通索引 
这也是最基本的索引,它没有任何的限制,MyISAM中默认的B树索引,也是我们大多数情况下用到的索引。

创建表语句

-- 直接创建索引
CREATE INDEX index_name on table(column(length));
-- 修改表结构的方式添加索引
ALTER TABLE table_name ADD INDEX index_name ON (column(length))
-- 创建表的时候同时创建索引
CREATE TABLE 'table'(
`id` int(11) NOT NULL AUTO_INCREMENT ,
08
`title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
09
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL ,
10
`time` int(10) NULL DEFAULT NULL ,
11
PRIMARY KEY (`id`),
12
INDEX index_name (title(length))

)
-- 删除索引
DROP INDEX index_name ON table
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

唯一索引 
与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值(注意和主键不同)。如果是组合索引,则列值的组合必须唯一,创建方法和普通索引类似。

-- 创建唯一索引
CREATE UNIQUE INDEX indexName ON table(column(length))
-- 修改表结构(这里只有一个UNIQUE没有INDEX)
ALTER TABLE table_name ADD UNIQUE indexName ON (column(length))
-- 创建表的时候直接指定(这里只有一个UNIQUE没有INDEX)
CREATE TABLE `table` (
07
`id` int(11) NOT NULL AUTO_INCREMENT ,
08
`title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
09
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL ,
10
`time` int(10) NULL DEFAULT NULL ,
11
PRIMARY KEY (`id`),
12
UNIQUE indexName (title(length))
13
);

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

全文索引 
MySQL从3.23.23版开始支持全文索引和全文检索,FULLTEXT索引可用于MyISAM表;他们可以从CHAR、VARCHAR或TEXT列中作为CREATE TABLE语句的一部分被创建,或是随后使用ALTER TABLE 或 CREATE INDEX被添加。

1
–创建表的适合添加全文索引
02
CREATE TABLE `table` (
03
`id` int(11) NOT NULL AUTO_INCREMENT ,
04
`title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
05
`content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL ,
06
`time` int(10) NULL DEFAULT NULL ,
07
PRIMARY KEY (`id`),
08
FULLTEXT (content)
09
);
-- 修改表结构添加全文索引
ALTER TABLE article ADD FULLTEXT index_content(content)
-- 直接创建索引
CREATE FULLTEXT INDEX index_content on article(content)
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

单列索引、多列索引 
多个单列索引与单个多列索引的查询效果不同,因为执行查询时,MySQL只能使用一个索引,会从多个索引中选择一个限制最为严格的索引。

组合索引(最左前缀) 
平时用的SQL查询语句一般都有比较多的限制条件,所以为了进一步榨取MySQL的效率,就要考虑建立组合索引。例如上表中针对titile和time建立一个组合索引:ALTER TABLE article ADD INDEX index_title_time(title(50), time(10))。建立这样的组合索引,其实是相当于分别建立了下面两组组合索引(最左前缀原则): 
- titile, time 
- title

-- 使用到上面的索引
SELECT * FROM article WHERE title='测试' AND time=123456789;
SELECT * FROM article WHERE title='测试';
-- 不适用上面的索引
SELECT * FROM article WHERE time=123456789;
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

什么情况下应该建立索引

一般来说,在wherejoin中出现的列需要建立索引,但也不完全如此,因为MySQL只对<><=>==between in,以及某些时候的like才会使用索引。例如:

select t.name 
from mytable t left join mytable m 
on t.name=m.username where m.age=20 and m.city='郑州'
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

此时就需要对city和age建立索引,由于mytable表中也出现在了join字句中,也有对它建立索引的必要。

刚才提到只有某些时候的like才需要建立索引。因为在以通配符%和_开头作查询时,MySQL不会使用索引。例如下句会使用索引:

select * from mytable where username like 'admin%'
 
 
  • 1
  • 1

而下句就不会使用:

SELECT * FROM mytable WHEREt Name like'%admin' 
 
 
  • 1
  • 1

建立索引的几个原则 
(1)最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、 like)就停止匹配,比如a=1 and b=2 and c>3 and d=4如果建立(a, b, c, d)顺序的索引,d是用不到索引的,如果建立(a, b, d, c)的索引则都可以用到,a, b, d的顺序可以任意调整。

(2)=和in可以乱序,比如a=1 and b=2 and c=3建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

(3)尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

(4)索引列不能参数计算,保持列“干净”,比如from_unixtime(create_time)=’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大,所以语句应该写成create_time=unix_timestamp(‘2014-05-29’);

(5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a, b)的索引,那么只需要 修改原来的索引即可。

索引的不足之处 
上面都在说使用索引的好处,但过多的使用索引将会造成滥用。索引也会有它的缺点:

  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT, UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
  • 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。
  • 索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。

使用索引的注意事项

  • 索引不会包含有NULL值的列 
    只要列中包含有NULl值都将不会被包含在索引中,复合索引中只要有一列有NULL值,那么这一列对于此复合索引都是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

  • 使用短索引 
    对串行进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是唯一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

  • 索引列排序 
    MySQL查询只是用一个索引,因此如果where字句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。

  • like语句操作 
    一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。

  • 不要在列上进行运算

select * from users where YEAR(adddate)<2007; 
 
 
  • 1
  • 1

将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成

select * from users where adddate<‘2007-01-01’; 
 
 
  • 1
  • 1
  • 不使用NOT IN 和<>操作

3. Explain

在开发过程中,对每个上线的SQL查询指纹(query figerprint)的质量都应有估算;而估算DB查询质量最直接的方法,就是分析其查询执行计划(Query Execution Plan,即QEP)。 
MySQL数据库,通过Explain指令查看SELECT(5.6.3+版本开始支持update/delete/insert等)。 
下面sql为创建一个sakila.actor表和一个主键过滤查询的执行计划。

drop table if EXISTS actor;
create  table actor(
actor_id smallint(5) UNSIGNED NOT NULL auto_increment,
first_name VARCHAR(45) not null,
last_name VARCHAR(45) not null,
last_update TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP on UPDATE CURRENT_TIMESTAMP,
PRIMARY key (actor_id),
key inx_actor_last_name (last_name)
)ENGINE=INNODB auto_increment=201 default charset= utf8;
insert into actor  select  * from (
    select 1, 'sun', 'moss', null UNION
    select 2, 'su', 'moss', null UNION
    select 3, 'sun', 'moss', null UNION
    select 4, 'sun', 'moss', null UNION
    select 5, 'sun', 'moss', null UNION
    select 6, 'sun', 'moss', null UNION
    select 7, 'sun', 'moss', null UNION
    select 8, 'sun', 'moss', null UNION
    select 9, 'sun', 'moss', null UNION
    select 10, 'sun', 'moss', null UNION
    select 11, 'sun', 'moss', null UNION
    select 12, 'sun', 'moss', null UNION
    select 13, 'sun', 'moss', null 
)temp;
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

主键过滤查询的执行计划

explain select * from sakila.actor where actor_id = 10
 
 
  • 1
  • 1

下面将会解释explain的输出结果 
explain

id字段 
表示SELECT查询标识符,用于标识执行顺序,基本是数字; 
执行顺序原则:id数据值大的优先执行,id值相同的从上往下书序执行;

select_type 
表示select字句的类型,常值有: 
SIMPLE:简单查询,查询字句不包含UNION或子查询 
PRIMARY:最外层的SELECT子句 
UNION: UNION子句右侧的SELECT子句 
SUBQUERY:子查询中第一个SELECT 
DERIVED:衍生表的SELECT子句

示例如下: 
type

table字段 
表示查询涉及的表或衍生表;

type字段[重要]

表示查询的access type,表示查询是否为“全表扫描”, “索引扫描”等 
常见以下几种类型,查询效率由最差到最好 
all < index < range~index_merge < ref < eq_ref < const <system(效率理论从最差到最好)

  • all 
    表示“全表扫描”(full table scan),性能是最差的几种查询之一,如果查询的表比较大,且查询频次高,对MySQL数据库有致命的性能影响。 
    见下图:因last_update字段没有索引,所以是全表扫描,扫描rows 200条 
    all

  • index 
    表示“全表索引扫描”(full index scan),其类型和ALL较类似,性能也是比较差;和ALL区别在于只对索引树进行扫描,但下面的sql没有用到索引进行过滤。 
    index

  • range 
    range表示“索引范围扫描“,通过索引字段范围获取表中部分数据记录;常常用于in, <, >, between等操作,查询效率一般不错。下图rows只扫描了4行数据,就获取到指定数据 
    range

  • index_merge 
    MySQL查询优化器发现查询可以同时使用多个索引查询结果集进行并集或交集的情况,就会使用index_merge type。 
    此时key字段有两个或多个索引,key_len/rows都分别有两个数值;如果是并集操作“Using interesect”,往往通过两个索引的字段,合并为一个索引,避免index_merge查询 
    下图是一个AND的SQL,Using interesect使用两个索引后的交集

    index-merge

一个OR的SQL, Using union表示两个索引后的并集 
or

  • ref 
  • eq_ref 
  • const 
  • system 

possible_keys字段 
表示MySQL查询优化器发现当前查询可能被使用的索引,但不一定会利用,如果possible_key的列举的索引越多,往往说明索引创建不合理,查询效率不是最高效; 
因为 优化器会分析尽可能多的索引,评估哪个索引的“成本”消耗局部最低,这个评估过程消耗时间和资源的。

key[重要] 
表示查询优化器真正使用的索引(可能多个,如前index_merge),如果是索引覆盖,那么索引不会在possible_keys中出现的;注意:对于组合索引,查询可能只使用其部分索引,详细见下面key_len计算分析

key_len[重要] 
表示查询优化器使用了索引的字节数,可以评估组合索引是否完全被使用,或只是最左部分字段使用。 
key_len字节的计算规则:

  • 字符串:char(n):n字节,varchar(n):n字节+2字节(变长),多字节charset*[1~4]字节(utf8为3字节,utf8mb4为4字节计算)(不懂多字节这个?)
  • 数值类型:TINYINT-1字节,SMALLINT-2字节,MEDIUMINT-3字节,INT-4字节,BIGINT-8字节
  • 时间类型:DATE-3字节 TIMESTAMP-4字节 DATETIME-8字节
  • 字段属性:NULL属性+1字节 
    计算demo 
    下图两个SQL使用相同的索引,但索引的效果和key_len却分别是9字节和119字节 
    SQL1key_len计算:pay_user_id字段8字节(bigint not null + product_type字段1字节=共9个字节)说明SQL1只使用了idx_userid组合索引的前两个字段,product_id字段未使用,过滤性不好。

SQL key_len计算: 前两字段9字节+ product_id字段110字节 ( varchar(36) utf8字符集和2字节变长, 36*3+2为110字节) = 9 + 110=119字节, SQL2使用了前三个字段,过滤性较好。

key_len

row[重要] 
MySQl查询优化器根据统计信息,估算SQL要查找到结果哦需要扫描读取的数据行数,这个值非常直观显示SQL的效率好坏,原则row越少越好。

extra字段[重要]

  • using filesort 
    MySQL需额外的排序操作,不能通过索引顺序达到排序效果; 
    又叫“文件排序”,易错误理论为排序结果过大,内存中不够需写磁盘文件排序。 
    一般有filesort,都建议优化去掉,CPU资源消耗大。下图last_update排序,但此字段无索引,故需filesort
  • using index 
    “覆盖索引扫描”,表示查询在索引树中就可以查找到所需数据,不用回表数据文件(回表操作),往往说明性能不错
  • using temporary 
    查询有使用临时表,一般出现于排序,分组和夺标join的情况,查询效率不高,建议优化

4. 慢查询优化基本步骤

(1)先运行看看是否真的很慢,注意设置SQL_NO_CACHE 
注:  
SQL_NO_CACHE:对当前query产生的结果集不缓存至系统query cache里, 
但如果缓存里面已经存在当前查询的记录,则会返回缓存里面的数据。可以使用RESET QUERY CACHE来清楚所有的缓存。 Same effect if you change 
the table, because this makes all cached queries invalid.
(change表的结构还是什么?)

(2)where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高 
(3)explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询) 
(4)order by limit形式的sql语句让排序的表优先查 
(5)了解业务方使用场景 
(6)加索引时参照建索引的几大原则 
(7)观察结果,不符合预期继续从1开始分析

5. 慢查询优化几个案例

例1:

select
   count(*) 
from
   task 
where
   status=2 
   and operator_id=20839 
   and operate_time>1371169729 
   and operate_time<1371174603 
   and type=2;
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

根据最左前缀匹配原则,最开始的sql语句的索引应该是operator_id,operator_time, type, stauts的联合索引;其中status, operator_id, type的顺序可以打乱。 
还有下面这些查询

select * from task where status = 0 and type = 12 limit 10;
select count(*) from task where status = 0;
 
 
  • 1
  • 2
  • 1
  • 2

那么索引建立成(status, type, operator_id, operator_time)就是非常正确的,因为可以覆盖到所以情况。这个就是利用了索引的最左前缀匹配原则。

例2: 
很多情况下,我们写SQL只是为了实现功能,这只是第一步,不同的语句书写方式对于效率往往有本质的的区别,这要去我们对sql的执行计划和索引原则有非常清楚的认识,请看下面的语句

select
   distinct cert.emp_id 
from
   cm_log cl 
inner join
   (
      select
         emp.id as emp_id,
         emp_cert.id as cert_id 
      from
         employee emp 
      left join
         emp_certificate emp_cert 
            on emp.id = emp_cert.emp_id 
      where
         emp.is_deleted=0
   ) cert 
      on (
         cl.ref_table='Employee' 
         and cl.ref_oid= cert.emp_id
      ) 
      or (
         cl.ref_table='EmpCertificate' 
         and cl.ref_oid= cert.cert_id
      ) 
where
   cl.last_upd_date >='2013-11-07 15:03:00' 
   and cl.last_upd_date<='2013-11-08 16:00:00';
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

(1).先运行一下,53条记录 1.87秒,又没有用聚合语句,比较慢

53 rows in set (1.87 sec)
 
 
  • 1
  • 1

(2).explain

explain2

(3)分析: 
简述一下执行计划,首先mysql根据idx_last_upd_date索引扫描cm_log表获得379条记录;然后查表扫描了63727条记录,分为两部分,derived表示构造表,也就是不存在的 
表,可以简单理解成一个语句形成的结果集,后面的数字表示语句的ID。derived2表示的是ID=2的查询构造了虚拟表,并且返回了 63727条记录。我们再来看看ID=2的语句究竟写了什么返回了这么大量的数据,首先全表扫描employee表13317条记录,然后根据索引emp_certificate_empid关联emp_certificate表,rows=1表示,每个关联都只锁定了一条记录,效率比较高。获得后,再和cm_log的379条记录根据规则关联。从执行过程上可以看出返回了太多的数据,返回的数据绝大部分cm_log都用不到,因为cm_log只锁定了379条记录。

(4)优化: 
如何优化呢?可以看到我们在运行完后还是要和cm_log做join,那么我们能不能之前和cm_log做join呢?仔细分析语句不难发现,其基本思想是如果cm_log的ref_talbe是EmpCertificate就关联emp_certificate表,如果 ref_table是Employee就关联employee表,我们完全可以拆成两部分,并用union连接起来,注意这里用union,而不用union all是因为原语句有“distinct”来得到唯一的记录,而union恰好具备了这种功能。如果原语句中没有distinct不需要去重,我们就可以直接使用union all了,因为使用union需要去重的动作,会影响SQL性能。 
优化过的语句如下

select
   emp.id 
from
   cm_log cl 
inner join
   employee emp 
      on cl.ref_table = 'Employee' 
      and cl.ref_oid = emp.id  
where
   cl.last_upd_date >='2013-11-07 15:03:00' 
   and cl.last_upd_date<='2013-11-08 16:00:00' 
   and emp.is_deleted = 0  
union
select
   emp.id 
from
   cm_log cl 
inner join
   emp_certificate ec 
      on cl.ref_table = 'EmpCertificate' 
      and cl.ref_oid = ec.id  
inner join
   employee emp 
      on emp.id = ec.emp_id  
where
   cl.last_upd_date >='2013-11-07 15:03:00' 
   and cl.last_upd_date<='2013-11-08 16:00:00' 
   and emp.is_deleted = 0
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

(5) 不需要了解业务场景,只需要改造的语句和改造之前的语句保持结果一致 
(6)现有索引可以满足,不需要建索引 
(7)用改造后的语句实验一下,只需要10ms降低了近200倍! 

explain1


原博

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值