1. 性能下降SQL慢 执行时间长 等待时间长
- 数据过多(分库分表)
- 关联了太多的表,太多join(SQL优化)
- 没有充分利用到索引(索引建立)
- 服务器调优及各个参数设置(调整my.cnf)
2. 常见通用的Join查询
- 注意Mysql没有Full OUTER JOIN(全连接),Oracle有全连接
2.1 Join查询实例
- dept(部门表)
- emp(员工表)
2.2 实例
- 所有门派的人员信息
select * from t_emp a INNER JOIN t_dept b on a.deptId = b.id
- 列出所有的用户,并显示其机构信息
select * from t_emp a left JOIN t_dept b on a.deptId = b.id
- 列出所有的门派
select * from t_dept b
- 列出所有不入门派的人员
select * from t_emp a left JOIN t_dept b on a.deptId = b.id where b.id is null
- 所有没人入的门派
select * from t_dept b left JOIN t_emp a on a.deptId = b.id where a.id is null
- 列出所有人员和机构的对照关系
select a.*,b.* from t_emp a left JOIN t_dept b on a.deptId = b.id
UNION
select a.*,b.* from t_dept b left JOIN t_emp a on a.deptId = b.id where a.id is null
- 列出所有没有人入的门派和没入门派的人
select a.*,b.* from t_emp a left JOIN t_dept b on a.deptId = b.id
where b.id is null
UNION
select a.*,b.* from t_dept b left JOIN t_emp a on a.deptId = b.id
where a.id is null
3. 索引
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。可以得到索引的本质:索引是数据结构。
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。下图就是一种可能的索引方式示例
左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出符合条件的记录。
注意:一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上
3.1 索引的优势和劣势
优势:
- 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗
劣势:
- **虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。**因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息
- 实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的
3.2 mysql的索引结构
3.2.1 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并不真实存在于数据表中。
查找过程:
如果要查找数据项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,显然成本非常非常高。
3.2.2 B+树
3.2.3 B+Tree与B-Tree 的区别
- B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。
- 在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。从这个角度看B-树的性能好像要比B+树好,而在实际应用中却是B+树的性能要好些。因为B+树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比B-树多,树高比B-树小,这样带来的好处是减少磁盘访问次数。尽管B+树找到一个记录所需的比较次数要比B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中B+树的性能可能还会好些,而且B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等),这也是很多数据库和文件系统使用B+树的缘故。
为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
-
B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。 -
B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
4. 索引的分类
索引创建的基本语法
-
创建:CREATE [UNIQUE ] INDEX [indexName] ON table_name(column)) 加上unique就是创建唯一索引
-
删除:DROP INDEX [indexName] ON mytable;
-
查看:SHOW INDEX FROM table_name
-
修改:有四种方式来添加数据表的索引:
4.1 ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
4.2 ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
4.3 ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次。
4.4 ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引。
4.1 单值索引
即一个索引只包含单个列,一个表可以有多个单列索引
# 随表一起建索引:
CREATE TABLE customer (
id INT(10) UNSIGNED AUTO_INCREMENT ,
customer_no VARCHAR(200),
customer_name VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name)
);
# 单独建单值索引:
CREATE INDEX idx_customer_name ON customer(customer_name);
# 删除索引:
DROP INDEX idx_customer_name on customer;
4.2 唯一索引
索引列的值必须唯一,但允许有空值
# 随表一起建索引:
CREATE TABLE customer (
id INT(10) UNSIGNED AUTO_INCREMENT ,
customer_no VARCHAR(200),
customer_name VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name),
UNIQUE (customer_no)
);
# 单独建唯一索引:
CREATE UNIQUE INDEX idx_customer_no ON customer(customer_no);
# 删除索引:
DROP INDEX idx_customer_no on customer ;
4.3 主键索引
设定为主键后数据库会自动建立索引,innodb为聚簇索引
# 随表一起建索引:
CREATE TABLE customer (
id INT(10) UNSIGNED AUTO_INCREMENT ,
customer_no VARCHAR(200),
customer_name VARCHAR(200),
PRIMARY KEY(id)
);
CREATE TABLE customer2 (
id INT(10) UNSIGNED ,
customer_no VARCHAR(200),
customer_name VARCHAR(200),
PRIMARY KEY(id)
);
# 单独建主键索引:
ALTER TABLE customer add PRIMARY KEY customer(customer_no);
# 删除建主键索引:
ALTER TABLE customer drop PRIMARY KEY ;
#修改建主键索引:
#必须先删除掉(drop)原索引,再新建(add)索引
4.4 复合索引
即一个索引包含多个列
# 随表一起建索引:
CREATE TABLE customer (
id INT(10) UNSIGNED AUTO_INCREMENT ,
customer_no VARCHAR(200),
customer_name VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name),
UNIQUE (customer_name),
KEY (customer_no,customer_name)
);
单独建索引:
CREATE INDEX idx_no_name ON customer(customer_no,customer_name);
删除索引:
DROP INDEX idx_no_name on customer ;
5. 创建索引的情况分析
5.1 哪些情况需要创建索引
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引
- 查询中与其它表关联的字段,外键关系建立索引
- 单键/组合索引的选择问题, 组合索引性价比更高
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或者分组字段
5.2 哪些情况不需要创建索引
- 表记录太少
- 经常增删改的表或者字段
- Where条件里用不到的字段不创建索引
- 过滤性不好的不适合建索引
6. 性能分析Explain
使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。
6.1 能干嘛
- 知道表的读取顺序
- 知道哪些索引可以使用
- 数据读取操作的操作类型
- 哪些索引被实际使用
- 表之间的引用
- 每张表有多少行被物理查询
6.2 使用方法
Explain + SQL语句
实例
explain
select a.NAME,c.NAME
from
t_emp a left join t_dept b
on a.deptId = b.id
left join t_emp c
on b.ceo=c.id
上图中字段的解释
-
id:表示查询中执行select子句或操作表的顺序
1.1 id相同,执行顺序由上至下
1.2 id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行(例如子查询)
1.3 id相同不同,同时存在 -
select_type:查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询
-
table:显示这一行的数据是关于哪张表的
-
partitions:代表分区表中的命中情况,非分区表,该项为null
-
type:访问类型排列 显示查询使用了何种类型,从最好到最差依次是:system>const>eq_ref>ref>range>index>ALL
- possible_keys:显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用
- key:实际使用的索引。如果为NULL,则没有使用索引,查询中若使用了覆盖索引,则该索引和查询的select字段重叠
- key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。 key_len字段能够帮你检查是否充分的利用上了索引
- ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值
- rows:rows列显示MySQL认为它执行查询时必须检查的行数。越少越好
- filtered:这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数
- extra:包含不适合在其他列中显示但十分重要的额外信息
7. 实例分析创建索引
先看两张表
dept表
emp表
7.1
系统中经常出现以下语句
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 and deptid=4
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 and deptid=4 AND emp.name = 'abcd'
在建立索引前进行查询语句执行
select SQL_NO_CACHE * from emp where emp.age=30 and deptId = 4 and emp.name = 'abcd'
结果
进行添加索引
CREATE INDEX idx_age_deptid_name ON emp(age,deptid,NAME)
再次执行查询语句
select SQL_NO_CACHE * from emp where emp.age=30 and deptId = 4 and emp.name = 'abcd'
结果
7.2 最佳左前缀法则
如果系统中出现以下sql,那么原来的索引还能使用吗?
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 AND emp.name = 'abcd'
可以看到虽然用上了上述创建的索引,但是只有部分被用上了
结论:过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
7.3
不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
执行以下sql语句,在不包含索引的情况下
EXPLAIN SELECT SQL_NO_CACHE avg(emp.age) FROM emp WHERE emp.age=30 AND emp.name = 'abcd'
结果
给age创建索引
create index idx_age on emp(age)
再次执行查询语句 结果如下图
可以看到没有任何的优化
7.4
存储引擎不能使用索引中范围条件右边的列
7.5 mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描
给name创建索引
CREATE INDEX idx_name ON emp(NAME)
sql语句name=XXX
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name = 'abc'
结果:可以看到key中显示了刚刚创建的索引
sql语句name!=XXX或者sql语句name<>XXX
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name <> 'abc'
结果:可以看到key没有任何索引
7.6 is not null 也无法使用索引,但是is null是可以使用索引的
EXPLAIN SELECT * FROM emp WHERE age IS NULL
EXPLAIN SELECT * FROM emp WHERE age IS NOT NULL
7.7 like以通配符开头(’%abc…’)mysql索引失效会变成全表扫描的操作
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name like '_abc%'
注意:如果like后面的字符串不加%或者_,也是会使用索引的
7.8 字符串不加单引号索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name = 123
7.9 检测题
假设index(a,b,c)
7.10 总结
- 对于单键索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
- 在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引
- 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面
- 书写sql语句时,尽量避免造成索引失效的情况
8. 查询优化
8.1 关联查询
- 保证被驱动表的join字段已经被索引
- left join 时,选择小表作为驱动表,大表作为被驱动表。left join中左边的是驱动表
- inner join 时,mysql会自己帮你把小结果集的表选为驱动表
- 子查询尽量不要放在被驱动表,有可能使用不到索引。
- 能够直接多表关联的尽量直接关联,不用子查询。
8.2 子查询优化
尽量不要使用not in 或者 not exists 用left outer join on xxx is null 替代