MySQL优化
一.工具
1.索引
1.1 索引的定义
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构(有序)
。在数据之外,数据库系统还维护者满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)
数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引
左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针
,这样就可以运用二叉查找快速获取到相应数据
一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上
。索引是数据库中用来提高性能的最常用的工具
1.2 索引的优点与缺点
优点:
- 类似于书籍的目录索引,提高数据检索的效率,降低数据库的IO成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
缺点:
-
实际上索引也是一张表,该表中保存了主键与索引字段,并指向实体类的记录,所以索引列也是要占用空间的
-
虽然索引大大提高了查询效率,同时却也降低更新表的速度,如对表进行INSERT、UPDATE、DELETE。因为更新表时,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息
1.3 索引分类
一、单列索引:
一个索引只包含单个列,但一个表中可以有多个单列索引
1.普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一 点
2.唯一索引:索引列中的值必须是唯一的,但是允许有多个
空值
3.主键索引:是一种特殊的唯一索引,不允许有空值(主键约束,就是一个主键索引)
主键索引与唯一索引的区别:
-
主键是一种约束,唯一索引是一种索引,两者在本质上是不同的
-
主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键
-
唯一性索引列允许空值,而主键列不允许为空值
-
主键索引在创建时,已经默认为非空值+ 唯一索引了
-
一个表最多只能创建一个主键索引,但可以创建多个唯一索引
-
主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等
-
主键可以被其他表引用为外键,而唯一索引不能
二、组合索引:
在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合。例如,这里由id、name和age3个字段构成的索引,索引行中就按id/name/age的顺序存放,索引可以索引下面字段组合(id,name,age)、(id,name)或者(id)。如果要查询的字段不构成索引最左面的前缀,那么就不会使用索引,比如,age或者(name,age)组合就不会使用索引查询
唯一组合索引:
对多个字段设置唯一组合索引,则多个字段组成的记录不能一样
三、全文索引:
全文索引,只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引,介绍了要求,说说什么是全文索引,就是在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行,比如有“故事你真的在听吗”,通过故事二字,可能就可以找到该条记录。一般开发中,不会用到全文索引,因为其占用很大的物理空间和降低了记录修改性,故较为少用
1.4 索引语法
创建索引:
CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name [USING index_type] ON tbl_name(index_col_name,...)
index_col_name
: column_name[(length)][ASC | DESC]
UNIQUE:唯一索引
FULLTEXT:全文索引
SPATIAL:空间索引
不选择:普通索引
tbl_name(index_col_name,…):多个参数则创建组合索引
创建复合索引:
CREATE INDEX idx_name_email_status ON tb_seller(NAME,email,STATUS);
就相当于
对name 创建索引 ;
对name , email 创建了索引 ;
对name , email, status 创建了索引 ;
主键索引默认创建
删除索引:DROP INDEX [indexName] ON mytable;
查看索引信息:SHOW INDEX FROM table_name\G;
使用ALTER 命令添加和删除索引
ALTER TABLE tbl_name ADD PRIMARY KEY (column_list): 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL
ALTER TABLE tbl_name ADD UNIQUE index_name (column_list): 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次
ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次
ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引
1.5 索引设计原则
-
对查询频次较高,且数据量比较大的表建立索引
-
索引字段的选择,最佳候选列应当从where子句的条件中提取,如果where子句中的组合比较多,那么应当挑选最常用、过滤效果最好的列的组合
-
使用唯一索引,区分度越高,使用索引的效率越高
-
索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价自然也就水涨船高。对于插入、更新、删除等DML操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低DML操作的效率,增加相应操作的时间消耗。另外索引过多的话,MySQL也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但无疑提高了选择的代价
-
使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的I/O效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升MySQL访问索引的I/O效率
-
利用最左前缀,N个列组合而成的组合索引,那么相当于是创建了N个索引,如果查询时where子句中使用了组成该索引的前几个字段,那么这条查询SQL可以利用组合索引来提升查询效率
2.视图
2.1 视图
将SELECT的结果像表一样保存下来的虚表
就是视图
。视图只是一种信息,它的便利之处在于用户可以按照想要的条件收集某表中某列的数据
。从用户的角度来看,视图和表在使用上没有什么区别。和表一样,视图也可以进行增删改查。如果更新表视图的记录,基表的记录也会更新。基表更新,视图也会更新
。
2.2 创建视图
CREATE VIEW 视图名 AS SELECT 列名 FROM 表名 WHERE 条件;
2.3 查看视图V1
SELECT * FROM V1;
2.4 更新视图V1
UPDATE V1 SET col = ‘’ WHERE xx = '';
2.5 确认视图
SHOW TABLES; // 和表交织显示出来
DESC 视图名; // 显示视图的列结构
2.6 视图插入
对视图的INSERT操作是有限制的,在使用了UNION、JOIN、子查询的视图中,不能执行INSERT和UPDATE。只从一个表中提取了列,那么执行INSERT和UPDATE是没有任何问题的,但执行INSERT时,视图中没有定义的列,基表会设置NULL
。设置了WHERE条件的视图,如下,如果插入一条(1,60),视图可以插入成功而且基表也可以插入成功!为了解决这个问题,可以在创建视图是在末尾加一个WITH CHECK OPTION
,这样再插(1,60)就会不成功了
CREATE VIEW V1 AS SELECT id,grade FROM tb WHERE grade > 90;
1.对于update,有with check option,要保证update后,数据要被视图查询出来
2.对于insert,有with check option,要保证insert后,数据要被视图查询出来
3.对于delete,有无with check option都一样
4.对于没有where 子句的视图,使用with check option是多余的
2.7 替换视图
存在同名的视图时,我们可以用CREATE OR REPLACE
替换该视图,视图1被删除,替换为新的视图
SELECT OR REPLACE V1 AS SELECT NOW();
2.8 修改视图结构
修改视图用ALTER
,呈现新的视图内容
ALTER VIEW V1 AS SELECT name,age FROM tb;
2.9 删除视图
DROP VIEW 视图名;
3.存储过程和函数
3.1 概述
存储过程和函数是 事先经过编译并存储在数据库中的一段 SQL 语句的集合
,调用存储过程和函数可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。存储过程和函数的区别在于函数必须有返回值,而存储过程没有
函数 : 是一个有返回值的过程
过程 : 是一个没有返回值的函数
3.2 创建存储过程
CREATE PROCEDURE procedure_name ([proc_parameter[,...]])
begin
-- SQL语句
end ;
示例:
delimiter $
create procedure pro_test1()
begin
select 'Hello Mysql' ;
end$
delimiter ;
知识小贴士:DELIMITER
该关键字用来声明SQL语句的分隔符 , 告诉 MySQL 解释器,该段命令是否已经结束了,mysql是否可以执行了。默认情况下,delimiter是分号;
在命令行客户端中,如果有一行命令以分号结束,那么回车后,mysql将会执行该命令
3.3 调用存储过程
call procedure_name() ;
3.4 查看存储过程
-- 查询db_name数据库中的所有的存储过程
select name from mysql.proc where db='db_name';
-- 查询存储过程的状态信息
show procedure status;
-- 查询某个存储过程的定义
show create procedure test.pro_test1 \G;
3.5 删除存储过程
DROP PROCEDURE [IF EXISTS] sp_name ;
语法
存储过程是可以编程的,意味着可以使用变量,表达式,控制结构 , 来完成比较复杂的功能
变量
- DECLARE
通过 DECLARE 可以定义一个局部变量,该变量的作用范围只能在 BEGIN…END 块中
DECLARE var_name[,...] type [DEFAULT value]
示例 :
delimiter $
create procedure pro_test2()
begin
declare num int default 5;
select num+ 10;
end$
delimiter ;
变量还可以通过SET
和SLELECT INTO
方式赋值
// SET方式
DELIMITER $
CREATE PROCEDURE pro_test3()
BEGIN
DECLARE NAME VARCHAR(20);
SET NAME = 'MYSQL';
SELECT NAME ;
END$
DELIMITER ;
// SELECT方式赋值
DELIMITER $
CREATE PROCEDURE pro_test5()
BEGIN
declare countnum int;
select count(*) into countnum from city;
select countnum;
END$
DELIMITER ;
if结构语句
if search_condition then statement_list
[elseif search_condition then statement_list] ...
[else statement_list]
end if;
需求:根据定义的身高变量,判定当前身高的所属的身材类型
根据定义的身高变量,判定当前身高的所属的身材类型
180 及以上 ----------> 身材高挑
170 - 180 ---------> 标准身材
170 以下 ----------> 一般身材
示例:
delimiter $
create procedure pro_test6()
begin
declare height int default 175;
declare description varchar(50);
if height >= 180 then
set description = '身材高挑';
elseif height >= 170 and height < 180 then
set description = '标准身材';
else
set description = '一般身材';
end if;
select description ;
end$
delimiter ;
传递参数
create procedure procedure_name([in/out/inout] 参数名 参数类型)
...
IN : 该参数可以作为输入,也就是需要调用方传入值 , 默认
OUT: 该参数作为输出,也就是该参数可以作为返回值
INOUT: 既可以作为输入参数,也可以作为输出参数
IN - 输入
需求 :
根据定义的身高变量,判定当前身高的所属的身材类型
示例 :
delimiter $
create procedure pro_test5(in height int)
begin
declare description varchar(50) default '';
if height >= 180 then
set description='身材高挑';
elseif height >= 170 and height < 180 then
set description='标准身材';
else
set description='一般身材';
end if;
select concat('身高 ', height , '对应的身材类型为:',description);
end$
delimiter ;
OUT-输出
需求 :
根据传入的身高变量,获取当前身高的所属的身材类型
示例:
delimiter $
create procedure pro_test5(in height int , out description varchar(100))
begin
if height >= 180 then
set description='身材高挑';
elseif height >= 170 and height < 180 then
set description='标准身材';
else
set description='一般身材';
end if;
end$
delimiter ;
调用:
call pro_test5(168, @description);
select @description;
小知识
@description : 这种变量要在变量名称前面加上“@”符号,叫做用户会话变量,代表整个会话过程他都是有作用的,这个类似于全局变量一样
@@global.sort_buffer_size : 这种在变量前加上 “@@” 符号, 叫做系统变量
case结构
方式一 :
CASE case_value
WHEN when_value THEN statement_list
[WHEN when_value THEN statement_list] ...
[ELSE statement_list]
END CASE;
方式二 :
CASE
WHEN search_condition THEN statement_list
[WHEN search_condition THEN statement_list] ...
[ELSE statement_list]
END CASE;
需求:
给定一个月份, 然后计算出所在的季度
示例:
delimiter $
create procedure pro_test9(month int)
begin
declare result varchar(20);
case
when month >= 1 and month <=3 then
set result = '第一季度';
when month >= 4 and month <=6 then
set result = '第二季度';
when month >= 7 and month <=9 then
set result = '第三季度';
when month >= 10 and month <=12 then
set result = '第四季度';
end case;
select concat('您输入的月份为 :', month , ' , 该月份为 : ' , result) as content ;
end$
delimiter ;
while循环
while search_condition do
statement_list
end while;
需求:
计算从1加到n的值
示例:
delimiter $
create procedure pro_test8(n int)
begin
declare total int default 0;
declare num int default 1;
while num<=n do
set total = total + num;
set num = num + 1;
end while;
select total;
end$
delimiter ;
repeat结构
有条件的循环控制语句, 当满足条件的时候退出循环 。while 是满足条件才执行,repeat 是满足条件就退出循环
REPEAT
statement_list
UNTIL search_condition
END REPEAT;
需求:
计算从1加到n的值
示例:
delimiter $
create procedure pro_test10(n int)
begin
declare total int default 0;
repeat
set total = total + n;
set n = n - 1;
until n=0
end repeat;
select total ;
end$
delimiter ;
loop和leave
OOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现
示例:
delimiter $
CREATE PROCEDURE pro_test11(n int)
BEGIN
declare total int default 0;
ins: LOOP
IF n <= 0 then
leave ins;
END IF;
set total = total + n;
set n = n - 1;
END LOOP ins;
select total;
END$
delimiter ;
游标
游标是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用光标对结果集进行循环
的处理。光标的使用包括光标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下,FETCH每次指向一条记录
声明光标:
DECLARE cursor_name CURSOR FOR select_statement ;
OPEN 光标:
OPEN cursor_name ;
FETCH 光标:
FETCH cursor_name INTO var_name [, var_name] ...
CLOSE 光标:
CLOSE cursor_name ;
示例:循环输出结果集
DELIMITER $
create procedure pro_test12()
begin
DECLARE id int(11);
DECLARE name varchar(50);
DECLARE age int(11);
DECLARE salary int(11);
DECLARE has_data int default 1;
// 创建游标,存储结果集合
DECLARE emp_result CURSOR FOR select * from emp;
// 当没有结果返回时设置has_data = 0
DECLARE EXIT HANDLER FOR NOT FOUND set has_data = 0;
open emp_result;
repeat
// 每次将一条记录赋值给对应的变量
fetch emp_result into id , name , age , salary;
select concat('id为',id, ', name 为' ,name , ', age为 ' ,age , ', 薪水为: ', salary);
until has_data = 0
end repeat;
close emp_result;
end$
DELIMITER ;
存储函数
CREATE FUNCTION function_name([param type ... ])
RETURNS type
BEGIN
...
END;
案例 : 定义一个存储过程, 请求满足条件的总记录数
delimiter $
create function count_city(countryId int)
returns int
begin
declare cnum int ;
select count(*) into cnum from city where country_id = countryId;
return cnum;
end$
delimiter ;
调用用SELECT
select count_city(1);
4.触发器
触发器是与表有关的数据库对象,指在 insert/update/delete 之前或之后
,触发并执行触发器中定义的SQL语句集合
。触发器的这种特性可以协助应用在数据库端确保数据的完整性 , 日志记录 , 数据校验等操作
使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发
,不支持语句级触发
触发器类型 | NEW 和 OLD的使用 |
---|---|
INSERT 型触发器 | NEW 表示将要或者已经新增的数据 |
UPDATE 型触发器 | OLD 表示修改之前的数据 , NEW 表示将要或已经修改后的数据 |
DELETE 型触发器 | OLD 表示将要或者已经删除的数据 |
创建触发器
create trigger trigger_name
before/after insert/update/delete
on tbl_name
[ for each row ] -- 行级触发器
begin
trigger_stmt ;
end;
删除触发器
语法结构 :
drop trigger [schema_name.]trigger_name
如果没有指定 schema_name,默认为当前数据库
查看触发器
可以通过执行 SHOW TRIGGERS 命令查看触发器的状态、语法等信息
语法结构 :
show triggers ;
二.优化
1.MySQL的体系结构概览
图片来源:https://blog.csdn.net/m0_38075425/article/details/82256315
支持接口:比如JDBC,用来建立java与MySQL的连接
连接池: 由于每次建立建立需要消耗很多时间,连接池的作用就是将这些连接缓存下来,下次可以直接用已经建立好的连接,提升服务器性能
管理服务与工具: 系统管理和控制工具,例如备份恢复、Mysql复制、集群等图
SQL接口: 接受用户的SQL命令,并且返回用户需要查询的结果
解析器: SQL命令传递到解析器的时候会被解析器验证和解析。其功能是:将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错误,那么就说明这个sql语句是不合理的
优化控制器: 查询优化器,SQL语句在查询之前会使用查询优化器对查询进行优化。他使用的是选取-投影-联接
策略进行查询。根据where选取,根据select后的字段进行投影,最后将符合两者的进行拼接
缓存和缓冲池: 查询缓存,如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据
存储引擎: 引擎层会转发服务层解析出来的sql语句告诉存储层要做什么(增删改查)并且告诉存储层要以何种方式做
文件层: 数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互
2.存储引擎
2.1概述
和大多数的数据库不同, MySQL中有一个存储引擎的概念, 针对不同的存储需求可以选择最优的存储引擎
存储引擎就是存储数据,建立索引,更新查询数据等等技术的实现方式 。存储引擎是基于表的,而不是基于库的
。所以存储引擎也可被称为表类型
Oracle,SqlServer等数据库只有一种存储引擎。MySQL提供了插件式的存储引擎架构。所以MySQL存在多种存储引擎,可以根据需要使用相应引擎,或者编写存储引擎
可以通过指定 show engines , 来查询当前数据库支持的存储引擎 :
show variables like '%storage_engine%' ;
创建新表时如果不指定存储引擎,那么系统就会使用默认的存储引擎,MySQL5.5之前的默认存储引擎是MyISAM,5.5之后就改为了InnoDB
2.2引擎分类
特点 | InnoDB | MyISAM | MEMORY | MERGE | NDB |
---|---|---|---|---|---|
存储限制 | 64TB | 有 | 有 | 没有 | 有 |
事务安全 | 支持 | ||||
锁机制 | 行锁(适合高并发) | 表锁 | 表锁 | 表锁 | 行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 |
哈希索引 | 支持 | ||||
全文索引 | 支持(5.6版本之后) | 支持 | |||
集群索引 | 支持 | ||||
数据索引 | 支持 | 支持 | 支持 | ||
索引缓存 | 支持 | 支持 | 支持 | 支持 | 支持 |
数据可压缩 | 支持 | ||||
空间使用 | 高 | 低 | N/A | 低 | 低 |
内存使用 | 高 | 低 | 中等 | 低 | 高 |
批量插入速度 | 低 | 高 | 高 | 高 | 高 |
支持外键 | 支持 |
InnoDB
InnoDB存储引擎是Mysql的默认存储引擎。InnoDB存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全。但是对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引。支持事务控制
和外键约束
在创建外键时,可以指定在删除、更新父表时,对子表进行的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION
RESTRICT和NO ACTION相同, 是指限制在子表有关联记录的情况下, 父表不能更新或者删除;
CASCADE表示父表在更新或者删除时,更新或者删除子表对应的记录;
SET NULL 则表示父表在更新或者删除的时候,子表的对应字段被SET NULL
针对上面创建个表,子表的外键指定是ON DELETE RESTRICT ON UPDATE CASCADE 方式的, 那么在主表删除记录的时候, 如果子表有对应记录, 则不允许删除, 主表在更新记录的时候, 如果子表有对应记录, 则子表对应更新
create table city_innodb(
city_id int NOT NULL AUTO_INCREMENT, // 不为空,自增
city_name varchar(50) NOT NULL,
country_id int NOT NULL,
primary key(city_id), // 主键
key idx_fk_country_id(country_id), // key建立索引
CONSTRAINT `fk_city_country` FOREIGN KEY(country_id) REFERENCES country_innodb(country_id) ON DELETE RESTRICT ON UPDATE CASCADE // constraint 约束名称 建立约束
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
InnoDB 存储表和索引
有以下两种方式 :
①使用共享表空间存储, 这种方式创建的表的表结构保存在.frm文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path定义的表空间中,可以是多个文件
②使用多表空间存储, 这种方式创建的表的表结构仍然存在 .frm 文件中,但是每个表的数据和索引单独保存在 .ibd 中
MyISAM
MyISAM 不支持事务、也不支持外键,其优势是访问的速度快,对事务的完整性没有要求或者以SELECT、INSERT为主的应用基本上都可以使用这个引擎来创建表
存储引擎的选择
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。以下是几种常用的存储引擎的使用环境
-
InnoDB : 是Mysql的默认存储引擎,用于事务处理应用程序,支持外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询意外,还包含很多的更新、删除操作,那么InnoDB存储引擎是比较合适的选择。InnoDB存储引擎除了有效的降低由于删除和更新导致的锁定, 还可以确保事务的完整提交和回滚,对于类似于计费系统或者财务系统等对数据准确性要求比较高的系统,InnoDB是最合适的选择
-
MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的
3.SQL优化步骤
在应用的的开发过程中,由于初期数据量小,开发人员写 SQL 语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多 SQL 语句开始逐渐显露出性能问题,对生产的影响也越来越大,此时这些有问题的 SQL 语句就成为整个系统性能的瓶颈,因此我们必须要对它们进行优化
当面对一个有 SQL 性能问题的数据库时,我们应该从何处入手来进行系统的分析,使得能够尽快定位问题 SQL 并尽快解决问题
3.1 查看SQL执行频率
MySQL 客户端连接成功后,通过 show [session|global] status
命令可以提供服务器状态信息。show [session|global] status 可以根据需要加上参数session或者global来显示 session 级(当前连接)的计结果和 global 级(自数据库上次启动至今)的统计结果。如果不写,默认使用参数是session
show status like 'Com_______';
show status like 'Innodb_rows_%';
Variable_name | 含义 |
---|---|
Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 |
Com_select | 执行 select 操作的次数,一次查询只累加1 |
Com_update | 执行 UPDATE 操作的次数 |
Com_delete | 执行 DELETE 操作的次数 |
Innodb_rows_read | select 查询返回的行数 |
Innodb_rows_inserted | 执行 INSERT 操作插入的行数 |
Innodb_rows_updated | 执行 UPDATE 操作更新的行数 |
Innodb_rows_deleted | 执行 DELETE 操作删除的行数 |
Connections | 试图连接 MySQL 服务器的次数 |
Uptime | 服务器工作时间 |
Slow_queries | 慢查询的次数 |
3.2 定位低效率执行SQL
可以通过以下两种方式定位执行效率较低的 SQL 语句
慢查询日志
: 通过慢查询日志定位那些执行效率较低的 SQL 语句,用–log-slow-queries[=file_name]选项启动时,mysqld 写一个包含所有执行时间超过 long_query_time 秒的 SQL 语句的日志文件show processlist
: 慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题的时候查询慢查询日志并不能定位问题,可以使用show processlist命令查看当前MySQL在进行的线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化
-
id列,用户登录mysql时,系统分配的
connection_id
,可以使用函数connection_id()查看 -
user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句
-
host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户
-
db列,显示这个进程目前连接的是哪个数据库
-
command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等
-
time列,显示这个状态持续的时间,单位是秒
-
state列
,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成 -
Info列,显示这个sql语句,是判断问题语句的一个重要依据
3.3 explain分析执行计划
通过以上步骤查询到效率低的 SQL 语句后,可以通过EXPLAIN或者 DESC命令获取 MySQL如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序
使用:explain + select语句
字段 | 含义 |
---|---|
id | select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序 |
select_type | 表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION 中的第二个或者后面的查询语句)、SUBQUERY(子查询中的第一个 SELECT)等 |
table | 输出结果集的表 |
type | 表示表的连接类型,性能由好到差的连接类型为( system —> const -----> eq_ref ------> ref -------> ref_or_null----> index_merge —> index_subquery -----> range -----> index ------> all ) |
possible_keys | 表示查询时,可能使用的索引 |
key | 表示实际使用的索引 |
key_len | 索引字段的长度 |
rows | 扫描行的数量 |
extra | 执行情况的说明和描述 |
3.3.1 explain 之 id
id 字段是 select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。id 情况有三种 :
- id 相同表示加载表的顺序是从上到下
- id 不同id值越大,优先级越高,越先被执行
- id 有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行
3.3.2 explain 之 select_type
select_type | 含义 |
---|---|
SIMPLE | 查询中不包含子查询、UNION、连接查询 |
PRIMARY | 包含UNION、子查询的总查询,最左边的查询类型就是PRIMARY |
SUBQUERY | 在SELECT 或 WHERE 后使用的子查询 |
DERIVED | 在FROM 列表中包含的子查询,被标记为 DERIVED |
UNION | 若第二个SELECT出现在UNION之后,则标记为UNION |
UNION RESULT | 针对UNION查询去重的临时表的查询类型 |
3.3.3 explain 之 table
展示这一行的数据是关于哪一张表的
3.3.4 explain 之 type
type 显示的是访问类型,是较为重要的一个指标,可取值为:
type | 含义 |
---|---|
NULL | MySQL不访问任何表,索引,直接返回结果 |
system | 表只有一行记录(等于系统表),这是const类型的特例,一般不会出现 |
const | 根据主键或者唯一索引与常数进行等值匹配 |
eq_ref | 执行连接查询时,根据主键或者唯一索引等值匹配 |
ref | 非唯一性索引的等值查询 |
range | 索引的区间范围查询,where 之后出现 between ,< , > , in 等操作(> 10不可以,> 10 < 100才可以) |
index | index 类型只是遍历了索引树, 通常比ALL快,ALL 是遍历数据文件 |
all | 全表扫描 |
3.3.6 explain 之 key
字段 | 含义 |
---|---|
possible_keys | 显示可能应用在这张表的索引,一个或多个 |
key | 实际使用的索引,如果为NULL,则没有使用索引 |
key_len | 表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好 |
3.3.7 explain 之 rows
扫描行的数量
3.3.8 explain 之 extra
字段 | 含义 |
---|---|
Using index | 使用了覆盖索引执行查询,不需要回表 |
Using index condition | 先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件再去过滤这些数据行,适用于二级索引 |
Using temporary | MySQL需要创建一个临时表来保存结果 |
Using where | 搜索条件在server层判断 |
这里详细说一下Using index condition,这说明出现了索引下推(ICP)
,假如有如下一条查询语句,SELECT * FROM tb WHERE key > ‘z’ AND key LIKE ‘%a’ AND key2 = 23; key已经设置了索引。假如没有索引下推,server层首先会调用存储引擎定位到满足key > 'z’的二级索引记录,然后根据主键值进行回表操作,将完整的记录返回给server层,server层再逐一判断其他条件是否成立,成立再返回给客户端
ICP这时候作用就来了,由于key是索引列,是不是可以优化呢?
存储引擎先找到key > 'z’后,不着急回表,而是继续判断所有关于key的条件是否成立,即key > ‘z’ AND key LIKE '%a’是否成立,不成立,直接跳过,成立后回表将完整记录再返回给server层,server层再继续判断其他where条件是否成立
这里再穿插一点自己的理解,出现了Using index condition,说明进行了条件过滤索引
,
比如where后只有key>‘z’,也会显示Using index condition
以上可能会同时出现几个
3.4 show profile分析SQL
通过SELECT @@have_profiling;
可以查看当前MySQL是否支持profile
通过show profiles;
指令,来查看所有SQL语句执行的耗时:
通过show profile for query query_id;
语句可以查看到该SQL执行过程中每个线程的状态和消耗的时间:
4.组合索引的使用与失效
我们假设在一张表上创建一个组合索引
create index idx_seller_name_sta_addr on tb_seller(name,status,address);
最左前缀法则
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列
组合索引失效
- 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效
- 范围查询右边的列,不能使用索引,意思就是如果对三列建立了索引,但第二列的查询条件是范围查询条件,则第三列索引失效
- 在索引列上进行运算操作, 索引将失效
- 字符串不加单引号,造成索引失效(8.0版本不失效了)
- 用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到
- 如果是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效,可以通过覆盖索引来解决问题
- 如果MySQL评估使用索引比全表更慢,则不使用索引。比如有一个地址列一共20条数据,其中19条为北京市,1条为上海市,当使用where = '北京市’时,不会走索引,当使用where = '上海市’时,会走索引
- is NULL , is NOT NULL有时索引失效。NULL多,is NULL不走索引,is NOT NULL走索引;NULL少,is NULL走索引,is NOT NULL不走索引
- in 走索引,not in不走索引
面试:什么情况索引会失效?
范模型数空运最快哦
范:范围查询右边的列,不能使用索引
模:头部模糊查询,索引失效
型:数据类型错误,索引失效
数:对索引字段使用内部函数,索引失效
空:如果列不限制not null,NULL多,is NULL不走索引,is NOT NULL走索引;NULL少,is NULL走索引,is NOT NULL不走索引
运:对索隐列使用加减乘除运算,索引失效
最:不符合最左前缀原则,索引失效
快:数据库认为全表扫描比使用索引快,索引失效
哦:or两边有一个没有索引,索引失效
聚簇索引和非聚簇索引
MySQL的InnoDB索引数据结构是B+树,主键索引叶子节点的值存储的就是MySQL的数据行,辅助索引的叶子节点的值存储的是主键值,这是了解聚簇索引和非聚簇索引的前提
Innobd中的主键索引是一种聚簇索引(一张表只有一个聚簇索引),非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引
聚簇索引
索引和数据在一块,索引结构的叶子结点存储了行数据,找到了索引也就找到了数据
InnoDB聚簇索引的叶子节点存储行记录
,因此, InnoDB必须要有,且只有一个聚集索引:
(1)如果表定义了PK,则PK就是聚簇索引
(2)如果表没有定义PK,则第一个not NULL unique列是聚簇索引
(3)否则,InnoDB会创建一个隐藏的row-id作为聚簇索引
上图中,最小存储单位是页(InnoDB一页为16k),也可以存放数据(叶子结点),也可以存放键值+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据的位置
相关面试题
1.为什么要有主键?
因为 InnoDB 表里面的数据必须要有一个 B+tree 的索引结构来组织、维护我们的整张表的所有数据,从而形成 .idb 文件
2.为什么推荐使用整型自增?
首先整型的占用空间会比字符串小,而且在查找上比大小也会比字符串更快。字符串比大小的时候还要先转换成 ASCII 码再去比较。如果使用自增的话,在插入方面的效率也会提高
不使用自增,可能时不时会往 B+tree 的中间某一位置插入元素,当这个节点位置放满了的时候,节点就要进行分裂操作(效率低)再去维护,有可能树还要进行平衡,又是一个耗性能的操作
都用自增就会永远都往后面插入元素,这样索引节点分裂的概率就会小很多
3.为什么MySQL索引用B+树而不是B树?
假设我们上图的主键是bigint类型,长度为8字节,InnoDB指针一般为6字节,那么主键+指针一共14节点,而一页有16k=16384字节,所以一页可以有16384/14=1170个主键+指针结构,那么一刻高度为2的B+树的叶子节点可以有1170个,实际开发中一条记录一般为1k,也就是说一页可以放16条数据,那么总共可以放1170*16=18720条记录,所以使用B+的存储能力还是挺强的,同理高度为3的B+树可以存放219024000条记录,所以高度为1-3的B+树就可以存放千万条记录
B树不管非叶子节点还是叶子结点,都会保存数据,这样导致非叶子节点能保存的指针变少,所以想要保存大量数据,只能增加树的高度,而在查询数据时,一般根节点是常驻内存的
,一次页的查找代表一次IO,所以B+树的IO次数少,性能好
非聚簇索引
索引的存储和数据的存储是分离的,也就是说找到了索引但没找到数据,需要根据叶子结点上的主键值通过聚簇索引查询数据(回表)
,非聚簇索引也叫做辅助索引或者二级索引
示例
下面我们创建了一个学生表,做三种查询,来说明什么情况下是聚簇索引,什么情况下不是
create table student (
id bigint,
no varchar(20) ,
name varchar(20) ,
address varchar(20) ,
PRIMARY KEY (`branch_id`) USING BTREE,
UNIQUE KEY `idx_no` (`no`) USING BTREE
)ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
第一种,直接根据主键查询获取所有字段数据,此时主键是聚簇索引,因为主键对应的索引叶子节点存储了id=1的所有字段的值
select * from student where id = 1
第二种,根据编号查询编号和名称,编号本身是一个唯一索引,但查询的列包含了学生编号和学生名称,当命中编号索引时,该索引的节点的数据存储的是主键ID,需要根据主键ID重新查询一次才能获得name字段,所以这种查询下no不是聚簇索引
select no,name from student where no = 'test'
第三种,我们根据编号查询编号(有人会问知道编号了还要查询?要,你可能需要验证该编号在数据库中是否存在),这种查询命中编号索引时,直接返回编号,因为所需要的数据就是该索引,不需要回表查询,这种场景下no是聚簇索引
select no from student where no = 'test'
原文链接:https://juejin.cn/post/6844904163185262606
5.SQL优化实践
5.1 大批量插入数据
1) 主键顺序插入
因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。如果InnoDB表没有主键,那么系统会自动默认创建一个内部列作为主键,所以如果可以给表创建一个主键,将可以利用这点,来提高导入数据的效率
2) 关闭唯一性校验
在导入数据前执行 SET UNIQUE_CHECKS=0,关闭唯一性校验,在导入结束后执行SET UNIQUE_CHECKS=1,恢复唯一性校验,可以提高导入的效率
3) 手动提交事务
如果应用使用自动提交的方式,建议在导入前执行 SET AUTOCOMMIT=0,关闭自动提交,导入结束后再执行 SET AUTOCOMMIT=1,打开自动提交,也可以提高导入的效率
5.2 优化insert语句
原始
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Cat');
insert into tb_test values(3,'Jerry');
合并优化
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
事务优化
start transaction;
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Cat');
insert into tb_test values(3,'Jerry');
commit;
5.3 优化order by语句
尽量减少额外的排序,通过索引直接返回有序数据。where 条件和Order by使用相同的索引,并且Order By的顺序和索引顺序相同, 并且Order by的字段都是升序,或者都是降序。否则肯定需要额外的操作,这样就会出现Using FileSort
5.4 优化group by 语句
由于GROUP BY 实际上也同样会进行排序操作,而且与ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引
- 如果查询包含 group by但是用户想要避免排序结果的消耗, 则可以执行order by null禁止排序
- 可以对order by后字段创建索引来提高效率
5.5 优化嵌套查询
使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。有些情况下,子查询是可以被更高效的连接(JOIN)替代
连接(Join)查询之所以更有效率一些 ,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作
5.6 优化OR条件
对于包含OR的查询子句,如果要利用索引,则OR之间的每个条件列都必须用到索引,而且不能使用到复合索引; 如果没有索引,则应该考虑增加索引
建议使用 union 替换 or
select * from emp where id = 1 or age = 30;
select * from emp where id = 1 union select * from emp where age = 30;
5.7 优化分页查询
一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大
select * from tb limit 2000000,10;
5.7.1 优化思路一
在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容
select * from tb t ,(select id from tb order by id limit 2000000,10) a where t.id = a.id;
5.7.2 优化思路二
该方案适用于主键自增
的表,可以把Limit 查询转换成某个位置的查询
select * from tb where id > 2000000 limit 10;
5.8 指定使用索引
如果存在多个索引,我们可以指定要使用的索引
- USE INDEX(索引名称) 指定使用索引
- IGNORE INDEX(索引名称) 忽略索引
- FORCE INDEX(索引名称) 强制使用索引
在FROM之后WHERE之前使用
三.优化进阶(MySQL 5.7版本)
1.应用优化
在实际生产环境中,由于数据库本身的性能局限,就必须要对前台的应用进行一些优化,来降低数据库的访问压力
1.1 使用连接池
对于访问数据库来说,建立连接的代价是比较昂贵的,因为我们频繁的创建关闭连接,是比较耗费资源的,我们有必要建立数据库连接池,以提高访问的性能
1.2 减少对MySQL的访问
1.2.1 避免对数据进行重复检索
在编写应用代码时,需要能够理清对数据库的访问逻辑。能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求
比如 ,需要获取书籍的id 和name字段 , 则查询如下:
select id , name from tb_book;
之后,在业务逻辑中有需要获取到书籍状态信息, 则查询如下:
select id , status from tb_book;
这样,就需要向数据库提交两次请求,数据库就要做两次查询操作。其实完全可以用一条SQL语句得到想要的结果。
select id, name , status from tb_book;
1.2.2 增加cache层
在应用中,我们可以在应用中增加缓存层
来达到减轻数据库负担的目的。缓存层有很多种,也有很多实现方式,只要能达到降低数据库的负担又能满足应用需求就可以
因此可以部分数据从数据库中抽取出来放到应用端以文本方式存储, 或者使用框架(Mybatis, Hibernate)提供的一级缓存/二级缓存,或者使用redis数据库来缓存数据
1.3 负载均衡
负载均衡是应用中使用非常普遍的一种优化方法,它的机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上, 以此来降低单台服务器的负载,达到优化的效果
1.3.1 利用MySQL复制分流查询
通过MySQL的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力
1.3.2 采用分布式数据库架构
分布式数据库架构适合大数据量、负载高的情况,它有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率
2.MySQL内存管理及优化
2.1 内存优化原则
-
将尽量多的内存分配给MySQL做缓存,但要给操作系统和其他程序预留足够内存
-
MyISAM 存储引擎的数据文件读取依赖于操作系统自身的IO缓存,因此,如果有MyISAM表,就要预留更多的内存给操作系统做IO缓存
-
排序区、连接区等缓存是分配给每个数据库会话(session)专用的,其默认值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发连接较高时会导致物理内存耗尽
2.2 MyISAM内存优化
myisam存储引擎使用 key_buffer 缓存索引块,加速myisam索引的读写速度。对于myisam表的数据块,mysql没有特别的缓存机制,完全依赖于操作系统的IO缓存
key_buffer_size
key_buffer_size
决定MyISAM索引块缓存区的大小,直接影响到MyISAM表的存取效率。可以在MySQL参数文件中设置key_buffer_size的值,对于一般MyISAM数据库,建议至少将1/4可用内存分配给key_buffer_size
在/usr/my.cnf 中做如下配置:
key_buffer_size=512M
read_buffer_size
如果需要经常顺序扫描myisam表,可以通过增大read_buffer_size的值来改善性能。但需要注意的是read_buffer_size是每个session独占的,如果默认值设置太大,就会造成内存浪费
read_rnd_buffer_size
对于需要做排序的myisam表的查询,如带有order by子句的sql,适当增加 read_rnd_buffer_size 的值,可以改善此类的sql性能。但需要注意的是 read_rnd_buffer_size 是每个session独占的,如果默认值设置太大,就会造成内存浪费
2.3 InnoDB 内存优化
innodb用一块内存区做IO缓存池,该缓存池不仅用来缓存innodb的索引块,而且也用来缓存innodb的数据块
innodb_buffer_pool_size
该变量决定了 innodb 存储引擎表数据和索引数据的最大缓存区大小。在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高,访问InnoDB表需要的磁盘I/O 就越少,性能也就越高
innodb_buffer_pool_size=512M
innodb_log_buffer_size
决定了innodb重做日志缓存的大小,对于可能产生大量更新记录的大事务,增加innodb_log_buffer_size的大小,可以避免innodb在事务提交前就执行不必要的日志写入磁盘操作
innodb_log_buffer_size=10M
3.MySQL并发参数调整
从实现上来说,MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在Mysql中,控制并发连接和线程的主要参数包括 max_connections、back_log、thread_cache_size、table_open_cahce
3.1 max_connections
采用max_connections 控制允许连接到MySQL数据库的最大数量,默认值是 151
。如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections 的值
MySQL最大可支持的连接数,取决于很多因素,包括给定操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度,期望的响应时间等。在Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定
3.2 back_log
back_log参数控制MySQL监听TCP端口时设置的积压请求栈大小。如果MySql的连接数达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默认值为 50 , 之后的版本默认为 50 + (max_connections / 5), 但最大不超过900
如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大back_log 的值
3.3 table_open_cache
该参数用来控制所有SQL语句执行线程可打开表缓存的数量, 而在执行SQL语句时,每一个SQL执行线程至少要打开 1 个表缓存。该参数的值应该根据设置的最大连接数 max_connections 以及每个连接执行关联查询中涉及的表的最大数量来设定 :max_connections x N
3.4 thread_cache_size
为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,通过参数 thread_cache_size 可控制 MySQL 缓存客户服务线程的数量
3.5 innodb_lock_wait_timeout
该参数是用来设置InnoDB事务等待行锁的时间,默认值是50ms ,可以根据需要进行动态设置。对于需要快速反馈的业务系统来说,可以将行锁的等待时间调小,以避免事务长时间挂起。对于后台运行的批量处理程序来说, 可以将行锁的等待时间调大,以避免发生大的回滚操作
4.锁
4.1 锁概述
锁是计算机协调多个进程或线程并发访问某一资源的机制(避免争抢)
在数据库中,除传统的计算资源(如 CPU、RAM、I/O 等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂
查看锁的使用情况
show open tables;
In_user : 表当前被查询使用的次数。如果该数为零,则表是打开的,但是当前没有被使用
Name_locked:表名称是否被锁定。名称锁定用于取消表或对表进行重命名等操作
show status like 'Table_locks%';
Table_locks_immediate : 指的是能够立即获得表级锁的次数,每立即获取锁,值加1
Table_locks_waited : 指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加1,此值高说明存在着较为严重的表级锁争用情况
4.2 锁分类
从对数据操作的粒度分:
-
表锁:操作时,会锁定整个表
-
行锁:操作时,会锁定当前操作行
不同引擎的锁类型
存储引擎 | 表级锁 | 行级锁 | 页面锁 |
---|---|---|---|
MyISAM | 支持 | 不支持 | 不支持 |
InnoDB | 支持 | 支持 | 不支持 |
MEMORY | 支持 | 不支持 | 不支持 |
BDB | 支持 | 不支持 | 支持 |
锁类型
锁类型 | 特点 |
---|---|
表级锁 | 偏向MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低 |
行级锁 | 偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高 |
页面锁 | 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般 |
4.3 MyISAM的锁
MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁
-
对MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求
-
对MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作
此外,MyISAM 的读写锁调度是写优先,这也是MyISAM不适合做写为主的表的存储引擎的原因。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞
4.4 InnoDB行锁
行锁特点 :偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁
InnoDB行锁
- 读锁(共享锁):当一个事务对某几行上读锁时,
允许其他事务读,允许上读锁
。但不允许其进行写操作,更不能上写锁 - 写锁(排它锁):当一个事务对某几行上写锁时,不允许其他事务读写,更不允许其他事务给这几行上任何锁,但该事务可以上读锁
关于行锁必须知道
1.行锁是基于索引的,没有使用到索引,只能是表锁,因为锁定的是索引而不是具体的行数据
2.开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
行锁实现方式(结合案例)
原文 https://www.jianshu.com/p/95d71b150782
1.记录锁
定义:记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录,是一种排它锁
2.间隙锁
先来看下什么是间隙
对于主键,间隙如下
间隙锁定义:锁定间隙,不包括已经存在的记录,上图间隙锁只会锁定(1,4)、(4,7)等开区间
3.临键锁
定义:锁定间隙,包括已经存在的记录,上图临键锁会锁定(1,4]、(4,7]等左开右闭区间
加锁规则
当然首先要清楚,普通的select语句不加锁的,想加锁要我们手动加锁才可以
等值查询(有索引就可以,不管什么索引)
-
等值查询唯一索引(主键索引是特殊的唯一索引)而且命中的情况下,是记录锁,比如查询上面的id=1的数据
-
等值查询非唯一索引而且命中的情况下,是临键锁
-
等值查询不命中是间隙锁,比如查询上面id=2,那么锁定的是(1,4)这个间隙
范围查询(有索引就可以,不管什么索引)
- 范围命中索引是临键锁,比如where id > 5 ,包含了7,那么是临键锁,锁定的是(4,+supernum],也就是(4,7] 并 (7,+s]
- 范围不命中索引是间隙锁,比如where id > 4 and id < 7,就是(4,7)的间隙锁
在没有索引上不管是锁住单条,还是多条记录,都会产生表锁
4.5 意向锁
意向锁存在的目的就是为了让 InnoDB 中的行锁和表锁更高效的共存
原理:
如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁来快速判断是否可以对某个表使用表锁
意向锁是表级锁,共有两种:
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁
意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁
意向共享锁(IS)和 意向排他锁(IX)都是表锁,不互斥
意向锁是一种 不与行级锁冲突 的表级锁,这一点非常重要
事务及其ACID属性
事务
是由一组SQL语句组成的逻辑处理单元
事务具有以下4个特性,简称为事务ACID属性
ACID属性 | 含义 |
---|---|
原子性(Atomicity) | 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成 |
一致性(Consistent) | 在事务开始前和结束后,数据库的完整性约束没有被破坏 |
隔离性(Isolation) | 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致 |
持久性(Durable) | 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失 |
并发事务处理带来的问题
问题 | 含义 |
---|---|
丢失更新(Lost Update) | 当两个或多个事务选择同一行,最初的事务修改的值,被后面的事务修改的值覆盖 |
脏读(Dirty Reads) | 当一个事务A对某数据进行了修改,而这种修改还没有commit,这时,另外一个事务B访问数据使用了这个数据,但A又回滚了 |
不可重复读(Non-Repeatable Reads) | 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现和以前读出的数据不一致 |
幻读(Phantom Reads) | 一个事务按照相同的查询条件重新读取以前查询过的数据,却发现其他事务插入了满足其查询条件的新数据 |
事务隔离级别
为了解决上述提到的事务并发问题,数据库提供一定的事务隔离机制来解决这个问题。数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使用事务在一定程度上串行化
进行,这显然与并发
是矛盾的
数据库的隔离级别有4个,由低到高依次为Read uncommitted(读未提交)、Read committed(读提交)、Repeatable read(可重复读取)、Serializable(可序化),这四个级别可以逐个解决脏写、脏读、不可重复读、幻读这几类问题
隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read uncommitted | × | √ | √ | √ |
Read committed | × | × | √ | √ |
Repeatable read(默认) | × | × | × | √ |
Serializable | × | × | × | × |
备注 : √ 代表可能出现 , × 代表不会出现
第一种隔离级别:Read uncommitted(读未提交)
加锁机制:读-无锁,写-读锁
在写时加行级读锁,读事务时不加锁
作用及后果:A写某行的时候加了读锁,所以其他事务不能修改此行,也就解决了丢失更新的问题,由于没有限制读,所以其他事务可以读到A未提交但修改了的行,A一旦回滚,其他事务读的数据就是脏读数据
第二种隔离级别:Read committed(读提交)
在 READ COMMITTED 中每次查询都会生成一个实时的 ReadView,做到保证每次提交后的数据是处于当前的可见状态。这样的话,MVCC保证每次读到的数据都是已经提交的,所以解决了脏读
第三种隔离级别:Repeatable read(可重复读取)
-
快照读(普通 select 语句),是通过 MVCC(多版本控制)方式解决了幻读。因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题
-
当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题
第四种隔离级别:Serializable(可序化)
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。读事务时加表级共享锁,写事务时加表级排他锁
如果不通过索引条件(包括索引失效)检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样
4.5 间隙锁
当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁; 对于键值在条件范围内但并不存在的记录,叫做 “间隙(GAP)” ,InnoDB也会对这个 “间隙” 加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)
4.6 InnoDB 行锁争用情况
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg:每次等待所花平均时长
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间
Innodb_row_lock_waits: 系统启动后到现在总共等待的次数
当等待的次数很高,而且每次等待的时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划
5.MVCC
5.1 概念
MVCC(Multi Version Concurrency Control),代表多版本并发控制。我们知道数据库有四种隔离级别,一般都是通过锁机制实现的,但是加锁会导致很大的开销,为了降低开销,就有了MVCC,用来实现读写操作不冲突。可以解决解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
。当然这个读指的是快照读
5.2 快照读与当前读
快照读就是普普通通的SELECT
增删改以及加锁的SELECT语句就是当前读
加锁的SELECT
select * from table where id = 1 lock in share mode; // 加读锁
select * from table where id = 1 for update; // 加写锁
5.3 MVCC的原理
它的实现原理主要是依赖记录中的3个隐式字段,undo日志,Read View 来实现的
3个隐式字段
,我们的每一条记录除了我们自己设定的能看到的字段外,还有我们看不到的3个字段
- DB_ROW_ID:隐式的主键,
如果我们没有设置主键
,那么MySQL会自己设置一个主键用来创建聚簇索引,我们一般都会设置主键,所以这个字段下文将不会再提及。 - DB_TRX_ID:记录
创建
这条记录/最后一次修改该记录
的事务ID(每个执行的事务都有自己的ID,这个字段表示最后一次修改过它的事务ID),DB_TRX_ID为1表示事务1创建了该条记录 - 回滚指针(一个地址),指向这条记录的上一个版本
undo log(update undo log)
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
事务2将name修改为李四,事务3将name修改为王五,即执行两条update语句将该记录的name字段更改两次,生成undo log如下,下图整体类似于数据结构中的链表,整个链表称为版本链
Read View(读视图)
Read View就是事务进行快照读操作的时候生产的读视图(Read View),当然这里的视图不是虚表的那个视图,说白了,Read View就是几个属性的集合
- trx_list
一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID(活跃事务:未提交的事务) - up_limit_id
记录trx_list列表中事务ID最小的ID(即ID最小的活跃事务) - low_limit_id
ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID
的最大值+1
这里要注意 low_limit_id,如果我有个事务ID为5并且该事务提交了,但是ID为4的事务没有提交,则显然trx_list最大的ID为4,但是low_limit_id不是5而是6,请注意low_limit_id的含义
所以我们知道Read View主要是用来做可见性判断的
, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据
一句话,Read View就是通过trx_list、up_limit_id、ow_limit_id来判断当前读事务可以看到版本链中的哪些记录
经过上面的阅读我们知道了3个隐式字段是什么、有什么作用,undo log是什么,有什么数据,Read View是什么、干什么的
下面我们举例说明一下三者是如何相互配合来实现MVCC的
事务1 | 事务2 | 事务3 | 事务4 |
---|---|---|---|
begin | begin | begin | begin |
····· | 快照读 | `````` | 修改 |
进行中 | 进行中 | commit |
当事务2进行快照读的时候,会产生Read View,如下
比较规则
① 如果DB_TRX_ID<up_limit_id,则说明则当前事务可以看到DB_TRX_ID所在的记录,如果大于等于进行下一个判断
② 如果DB_TRX_ID>=low_limit_id,如果成立则代表DB_TRX_ID所在的记录是在Read View生成后才出现的,那么对当前事务肯定不可见,如果小于,进行下一个判断
③ 判断DB_TRX_ID是否在活跃事务中,如果在,则代表Read View生成的时刻,这个事务处于活跃状态,还没有提交,当前读事务肯定看不到,如果不在,则说明这个事务在Read View生成之前就已经开始了提交,那么修改结果是可以看到的
这里建议大家停一下想一下每条比较规则的原理!!!其实很简单!!!
事务2能否看到事务4的修改呢?
事务4的DB_TRX_ID为4,up_limit_id为1,如果DB_TRX_ID>=up_limit_id,继续②
事务4的DB_TRX_ID为4,low_limit_id为5,DB_TRX_ID<low_limit_id,继续③
事务4的DB_TRX_ID为4,不在活跃事务中,所以事务2可以看到事务4的修改
MVCC总结
在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。在 READ COMMITTED 中每次查询都会生成一个实时的 ReadView,做到保证每次提交后的数据是处于当前的可见状态。这样的话,MVCC保证每次读到的数据都是已经提交的,所以解决了脏读,但由于每次查询都会生成一个实时的ReadView,如果一个事务中的两个select语句之间又提交了新数据,那么两次读到的数据就会不一样,因此出现了不可重复读问题,不能解决不可重复读和幻读问题
对于REPEATABLE READ快照读,在当前事务第一次查询时生成当前的ReadView(事务级Read View),并且当前的ReadView 会一直沿用到当前事务提交,保证解决了脏读、不可重复读、幻读。
RR中当前读通过next-key锁(行锁+gap锁)解决不可重复读和幻读问题
四.MySQL高级
1.MySQL内置命令
1.1 mysql
该mysql不是指mysql服务,而是指mysql的客户端工具
mysql [options] [database]
连接选项
参数 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器IP或域名
-P, --port=# 指定连接端口
-e, --execute=name 执行SQL语句并退出
示例 :
mysql -h 127.0.0.1 -P 3306 -u root -p
mysql -h127.0.0.1 -P3306 -uroot -p2143
mysql -uroot -pzks123456 db1 -e "select * from city";
此选项可以在Mysql客户端执行SQL语句,而不用连接到MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便
1.2 mysqladmin
mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等
可以通过 : mysqladmin --help 指令查看帮助文档
以下都是在未连接数据库的前提下才能执行
mysqladmin -uroot -pzks123456 create 'test01';
mysqladmin -uroot -pzks123456 drop 'test01';
mysqladmin -uroot -pzks123456 version;
1.3 mysqlbinlog
由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog 日志管理工具
mysqlbinlog [options] log-files1 log-files2 ...
选项:
-d, --database=name : 指定数据库名称,只列出指定的数据库相关操作
-o, --offset=# : 忽略掉日志中的前n行命令
-r,--result-file=name : 将输出的文本格式日志输出到指定文件
-s, --short-form : 显示简单格式, 省略掉一些信息
--start-datatime=date1 --stop-datetime=date2 : 指定日期间隔内的所有日志
--start-position=pos1 --stop-position=pos2 : 指定位置间隔内的所有日志
1.4 mysqldump
mysqldump [options] db_name [tables]
mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句
1.4.1 连接选项
参数 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器IP或域名
-P, --port=# 指定连接端口
1.4.2 输出内容选项
参数:
--add-drop-database 在每个数据库创建语句前加上 Drop database 语句
--add-drop-table 在每个表创建语句前加上 Drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table)
-n, --no-create-db 不包含数据库的创建语句
-t, --no-create-info 不包含数据表的创建语句
-d, --no-data 不包含数据
-T, --tab=name 自动生成两个文件:一个.sql文件,创建表结构的语句;
一个.txt文件,数据文件,相当于select into outfile
示例:
①将数据库db01中的表emp的表文件备份
mysqldump -uroot -pzks123456 db01 emp > emp.sql
由于我是在docker容器内操作mysql,可以通过find / -name emp.sql
查找emp.sql文件在宿主机的位置
②将数据库db01中表emp的sql文件和txt文件备份到/tmp目录下
mysqldump -uroot -pzks123456 -T /tmp db01 emp
1.5 mysqlimport/source
mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件
mysqlimport [options] db_name textfile1 [textfile2...]
示例:
将/tmp目录下的emp.txt中的记录导入到表中
mysqlimport -uroot -pzks123456 db01 /tmp/emp.txt
如果需要导入sql文件,可以使用mysql中的source 指令 :
source /tmp/emp.sql;
1.6 mysqlshow
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引
①查看每个数据库的表信息
mysqlshow -uroot -pzks123456 --count
②.指定数据库
mysqlshow -uroot -pzks123456 db01 --count
2.MySQL日志
在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的方方面面,以帮助数据库管理员追踪数据库曾经发生过的各种事件。MySQL 也不例外,在 MySQL 中,有 4 种不同的日志,分别是错误日志、二进制日志(BINLOG 日志)、查询日志和慢查询日志,这些日志记录着数据库在不同方面的踪迹
1.错误日志
错误日志是 MySQL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志
通过以下命令查看错误日志的位置,再查看即可
show variables like 'log_error%';
2.二进制日志
二进制日志(BINLOG)记录了所有的DDL(数据定义语言)语句和 DML(数据操纵语言)语句
,但是不包括数据查询语句
。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制,就是通过该binlog实现的
二进制日志,默认情况下是没有开启的,需要到MySQL的配置文件中开启,并配置MySQL日志的格式
开启二进制文件,重启
:
#配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如 : mysqlbin.000001,mysqlbin.000002
log_bin=mysqlbin
#配置二进制日志的格式
binlog_format=STATEMENT
日志格式
STATEMENT
该日志格式在日志文件中记录的都是SQL语句(statement),每一条对数据进行修改的SQL都会记录在日志文件中,通过Mysql提供的mysqlbinlog工具,可以清晰的查看到每条语句的文本。主从复制的时候,从库(slave)会将日志解析为原文本,并在从库重新执行一次。
ROW
该日志格式在日志文件中记录的是每一行的数据变更,而不是记录SQL语句。比如,执行SQL语句 : update tb_book set status=‘1’ , 如果是STATEMENT 日志格式,在日志中会记录一行SQL文件; 如果是ROW,由于是对全表进行更新,也就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更。
MIXED
这是目前MySQL默认的日志格式,即混合了STATEMENT 和 ROW两种格式。默认情况下采用STATEMENT,但是在一些特殊情况下采用ROW来进行记录。MIXED 格式能尽量利用两种模式的优点,而避开他们的缺点。
插入一条语句后查看日志
日志默认在/var/lib/mysql
下
查看日志文件:
mysqlbinlog mysqlbin.000001;
3.日志删除
方式一
Reset Master
方式二
执行指令 purge master logs to 'mysqlbin.******'
,该命令将删除 ******
编号之前的所有日志
4.查询日志
查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句
默认情况下, 查询日志是未开启的。如果需要开启查询日志,可以在配置文件设置以下配置 :
#该选项用来开启查询日志 , 可选值 : 0 或者 1 ; 0 代表关闭, 1 代表开启
general_log=1
#设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.log
general_log_file=file_name
查询日志在/var/lib/mysql
目录下
4.慢查询日志
慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于 min_examined_row_limit 的所有的SQL语句的日志。long_query_time 默认为 10 秒,最小为0,精度可以到微秒
慢查询日志默认是关闭的 。可以通过两个参数来控制慢查询日志 :
# 该参数用来控制慢查询日志是否开启, 可取值: 1 和 0 , 1 代表开启, 0 代表关闭
slow_query_log=1
# 该参数用来指定慢查询日志的文件名
slow_query_log_file=slow_query.log
# 该选项用来配置查询的时间限制, 超过这个时间将认为值慢查询, 将需要进行日志记录, 默认10s
long_query_time=10
查看慢查询日志文件
①直接通过cat 指令查询该日志文件
②如果慢查询日志内容很多,直接查看文件,比较麻烦,这个时候可以借助于mysql自带的mysqldumpslow工具,来对慢查询日志进行分类汇总
3.MySQL复制
复制是指将主数据库的DDL 和 DML 操作通过二进制日志
传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步
MySQL支持一台主库同时向多台从库进行复制, 从库同时也可以作为其他从服务器的主库,实现链状复制
从上层来看,复制分成三步:
-
Master 主库在事务提交时,会把数据变更作为时间Events记录在二进制日志文件 Binlog中
-
主库推送二进制日志文件Binlog 中的日志事件到从库的
中继日志Relay Log
-
slave重做中继日志中的事件,将改变反映它自己的数据
MySQL 复制的优点主要包含以下三个方面:
-
主库出现问题,可以快速切换到从库提供服务
-
可以在从库上执行查询操作,从主库中更新,实现读写分离,降低主库的访问压力
-
可以在从库中执行备份,以避免备份期间影响主库的服务