MYSQL(进阶篇)——一篇文章带你深入掌握MYSQL
我们在上篇文章中已经学习了MYSQL的基本语法和概念
在这篇文章中我们将讲解底层结构和一些新的语法帮助你更好的运用MYSQL
温馨提醒:该文章大约20000字,建议关注收藏慢慢观看,希望能给你带来帮助~
进阶篇内容目录
这篇文章我们主要分为七个部分:
- 存储引擎
- 索引
- SQL优化
- 视图/存储过程/触发器
- 锁
- InnoDB引擎
- MYSQL管理
存储引擎
在讲解存储引擎前我们先来了解一下MYSQL的整体体系结构
MYSQL整体分为四个部分:
- 连接层
- 服务层
- 引擎层
- 存储层
存储引擎简介
存储引擎概念:
- 存储数据,建立索引,更新/查询数据等技术的实现方式
存储引擎注意点:
- 存储引擎是基于表的,而不是基于库的,所以不同表可以有不同的存储引擎,同时存储引擎也被称为表类型
首先我们要先直到存储引擎是在哪里定义的:
# 下面是一个表的创建语句
create table Name (
~~~~~~~~
)engine = InnoDB;
# 在上面的engine = 存储引擎类型 就是存储引擎的设计语句
# 我们默认情况下是InoDB存储引擎
同样我们可以通过查看创建方法来得到表的存储引擎类型:
show create table 表名;
存储引擎类型以及特点
我们可以在DG或其他数据库软件中直接查看存储引擎的类型:
# 下述代码会给出该数据库中支持的存储引擎类型
show engines;
在这里我们仅详细介绍三种存储引擎:
- InnoDB
介绍:
- InnoDB是一种兼顾可靠性和高性能的通用存储引擎,在MYSQL5.5之后,称为MYSQL默认存储引擎
特点:
- DML操作遵循ACID模型,支持事务
- 行级锁,提高并发访问性能
- 支持 外键 FOREIGN KEY约束,保证数据的完整性和正确性
文件:
- xxx.ibd:xxx表示表名,innoDB引擎的每张表对应一个表空间文件,存储该表的表结构,数据和索引
- 我们可以通过show variables like ‘innodb_file_per_table‘来查看查看存储引擎分类情况(是否共用一个存储引擎)
- 如果为ON表示一个表分配一个ibd文件
- MyISAM
介绍:
- MyISAM是MYSQL早期的默认存储引擎
特点:
- 不支持事务,不支持外键
- 支持表锁,不支持行锁
- 访问速度快
文件:
- xxx.sdi:存储表结构信息
- xxx.MYD:存储数据
- xxx.MYI:存储索引
- Memory
介绍:
- Memory引擎的表数据是存储在内存中的,由于受到硬件问题或断电问题,只能作为临时表或缓存使用
特点:
- 内存存放
- hash索引(默认)
文件:
- xxx.sdi:存储表结构信息
三者区别:
特点 | InnoDB | MyISAM | Memory |
---|---|---|---|
存储限制 | 64TB | 有 | 有 |
事务安全 | 支持 | - | - |
锁机制 | 行锁 | 表锁 | 表锁 |
B+tree索引 | 支持 | 支持 | 支持 |
Hash索引 | - | - | 支持 |
全文索引 | 支持(5.6版本) | 支持 | - |
空间使用 | 高 | 低 | N/A |
内存使用 | 高 | 低 | 中等 |
批量插入速度 | 低 | 高 | 高 |
支持外键 | 支持 | - | - |
存储引擎选择
我们在各个表都可以选择不同的存储引擎,而存储引擎的选择大多遵循以下特征:
优选InnoDB:
- MYSQL的默认存储引擎,支持事务,外键
- 如果应用对事务的完整性有较高要求,并在并发条件下要求数据的一致性,数据操作除了插入和查询外,还包括很多的更新和删除操作,那么InnoDB存储引擎是比较合理的选择
优选MyISAM:
- 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并对事务的完整性,并发性要求不高,那么MyISAM存储引擎是比较合理的选择
优选Memory:
- 将所有数据保存到内存中,访问速度快,常用于临时表和缓存
- Memory的缺陷是对表的大小有限制,太大的表无法存储在内存中,且无法保证安全性
索引
首先我们来简略的介绍一下索引:
- 索引是帮助MYSQL高效获得数据的数据结构(有序)
- 在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方法引用数据,实现查找
索引的优点:
- 提高数据检索的效率,降低数据库的IO成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗。
索引的缺点:
- 索引列也是要占用空间的。
- 索引大大提高了查询效率,同时却也降低更新表的速度, 如对表进行INSERT、UPDATE、DELETE时,效率降低。
索引结构
MySQL的索引是在存储引擎层实现的,因而不同的存储引擎有不同的索引结构 :
索引结构 | 描述 |
---|---|
B+Tree索引 | 最常见的索引类型,大部分引擎都支持 B+ 树索引 |
Hash索引 | 底层数据结构是用哈希表实现的, 只有精确匹配索引列的查询才有效, 不 支持范围查询 |
R-tree索引 | 空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类 型,通常使用较少 |
Full-text索引 | 是一种通过建立倒排索引,快速匹配文档的方式。类似于 Lucene,Solr,ES |
不同的索引结构也有不同的适配情况:
索引 | InnoDB | MyISAM | Memory |
---|---|---|---|
B+Tree索引 | √ | √ | √ |
Hash索引 | × | × | √ |
R-tree索引 | × | √ | × |
Full-text索引 | √ | √ | × |
在这里我们仅对B+Tree索引和Hash索引做出详细解释:
- B+Tree索引
以一颗最大度数为4的B+Tree结构为例:
图片内容解释:
- 绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。
- 红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。
注意:
- 上面节点仅用于快速定位数据位置,且所有的数据元素均会出现在叶节点
- 叶节点之间用单项链接相连,形成链表类型
MYSQL中的B+Tree索引在B+Tree结构上对叶节点进行了一点改造:
- 在B+Tree结构上增加了一个指向相邻叶子节点的链表指针,形成了带有顺序指针的B+Tree,提高区间访问性能
- Hash索引
我们先来讲解一下hash表:
- 哈希索引采用hash算法,把键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中
- 如果两个或多个键值对映射到同一个相同的槽位上,我们采用链表的方法来解决
Hash索引特点:
- Hash索引只能用于对等比较(=,in),不支持范围查询(between,<,>,...)
- 无法利用索引完成排序操作
- 查询效率高,通常只要一次检索就可以完成,效率通常高于B+Tree索引
Hash索引的存储引擎支持:
- 在MySQL中,支持hash索引的是Memory存储引擎。 而InnoDB中具有自适应hash功能,hash索引是InnoDB存储引擎根据B+Tree索引在指定条件下自动构建的。
接下来我们分析一下InnoDB存储引擎为什么选择B+tree索引结构:
- 相对于二叉树,层级更少,搜索效率高;
- 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
- 相对Hash索引,B+tree支持范围匹配及排序操作;
索引分类
我们根据索引类型常常把索引分为四种:
分类 | 含义 | 特点 | 关键字 |
---|---|---|---|
主键索引 | 针对于表中主键创建的索引 | 默认自动创建, 只能 有一个 | PRIMARY |
唯一索引 | 避免同一个表中某数据列中的值重复 | 可以有多个 | UNIQUE |
常规索引 | 快速定位特定数据 | 可以有多个 | |
全文索引 | 全文索引查找的是文本中的关键词,而不是比 较索引中的值 | 可以有多个 | FULLTEXT |
在InoDB存储引擎中,根据索引的存储形式,我们又可以把他们分为以下两种:
分类 | 含义 | 特点 |
---|---|---|
聚集索引 | 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 | 必须有且只有一个 |
二级索引 | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
聚集索引选择特点:
- 如果存在主键,主键索引就是聚集索引
- 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
- 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。
聚集索引:
- 在叶节点下挂上整行的信息
二级索引:
- 在叶节点下挂上主键的信息
我们在查询时常常采用回表查询:
- 先利用已知条件采用二级索引得到主键,再利用主键用聚集索引获得所有信息
索引语法
索引的语法只有三条:
# 创建索引
-- UNIQUE表示唯一索引 FULLTEXT表示全文索引
-- (index_col_name,... ) 表示可以形成联合索引,一个索引包括多个表内列
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name,... ) ;
# 查看索引
SHOW INDEX FROM table_namne;
# 删除索引
DROP INDEX index_name ON table_name;
我们下面通过一个实例来演示索引的使用方法:
# 以下为构造表
-- 创建表
create table tb_user(
id int primary key auto_increment comment '主键',
name varchar(50) not null comment '用户名',
phone varchar(11) not null comment '手机号',
email varchar(100) comment '邮箱',
profession varchar(11) comment '专业',
age tinyint unsigned comment '年龄',
gender char(1) comment '性别 , 1: 男, 2: 女',
status char(1) comment '状态',
createtime datetime comment '创建时间'
) comment '系统用户表';
-- 添加数据
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('吕布', '17799990000', 'lvbu666@163.com', '软件工程', 23, '1', '6', '2001-02-02 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('曹操', '17799990001', 'caocao666@qq.com', '通讯工程', 33, '1', '0', '2001-03-05 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('赵云', '17799990002', '17799990@139.com', '英语', 34, '1', '2', '2002-03-02 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('孙悟空', '17799990003', '17799990@sina.com', '工程造价', 54, '1', '0', '2001-07-02 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('花木兰', '17799990004', '19980729@sina.com', '软件工程', 23, '2', '1', '2001-04-22 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('大乔', '17799990005', 'daqiao666@sina.com', '舞蹈', 22, '2', '0', '2001-02-07 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('露娜', '17799990006', 'luna_love@sina.com', '应用数学', 24, '2', '0', '2001-02-08 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('程咬金', '17799990007', 'chengyaojin@163.com', '化工', 38, '1', '5', '2001-05-23 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('项羽', '17799990008', 'xiaoyu666@qq.com', '金属材料', 43, '1', '0', '2001-09-18 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('白起', '17799990009', 'baiqi666@sina.com', '机械工程及其自动化', 27, '1', '2', '2001-08-16 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('韩信', '17799990010', 'hanxin520@163.com', '无机非金属材料工程', 27, '1', '0', '2001-06-12 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('荆轲', '17799990011', 'jingke123@163.com', '会计', 29, '1', '0', '2001-05-11 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('兰陵王', '17799990012', 'lanlinwang666@126.com', '工程造价', 44, '1', '1', '2001-04-09 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('狂铁', '17799990013', 'kuangtie@sina.com', '应用数学', 43, '1', '2', '2001-04-10 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('貂蝉', '17799990014', '84958948374@qq.com', '软件工程', 40, '2', '3', '2001-02-12 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('妲己', '17799990015', '2783238293@qq.com', '软件工程', 31, '2', '0', '2001-01-30 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('芈月', '17799990016', 'xiaomin2001@sina.com', '工业经济', 35, '2', '0', '2000-05-03 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('嬴政', '17799990017', '8839434342@qq.com', '化工', 38, '1', '1', '2001-08-08 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('狄仁杰', '17799990018', 'jujiamlm8166@163.com', '国际贸易', 30, '1', '0', '2007-03-12 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('安琪拉', '17799990019', 'jdodm1h@126.com', '城市规划', 51, '2', '0', '2001-08-15 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('典韦', '17799990020', 'ycaunanjian@163.com', '城市规划', 52, '1', '2', '2000-04-12 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('廉颇', '17799990021', 'lianpo321@126.com', '土木工程', 19, '1', '3', '2002-07-18 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('后羿', '17799990022', 'altycj2000@139.com', '城市园林', 20, '1', '0', '2002-03-10 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES ('姜子牙', '17799990023', '37483844@qq.com', '工程造价', 29, '1', '4', '2003-05-26 00:00:00');
# 创建索引
-- 为name创造索引,因为name可能重复,这里创造简单索引
create index idx_user_name on tb_user(name);
-- 为phone创造索引,因为phone只有一个,我们创造唯一索引
create unique index idx_user_phone on tb_user(phone);
-- 创造联合索引,为professin,age,status创造联合索引
create index idx_user_pro_age_status on tb_user(profession,age,status);
-- 为email创造索引
create index idx_user_email on tb_user(email);
# 删除索引
-- 我们删除email的索引
drop index idx_user_email on tb_user;
# 查看索引
show index from tb_user;
SQL性能分析
我们在后面的章节中将会讲到SQL语句的优化
那么优化自然是要针对SQL中性能较差的部分进行优化,因而这部分我们先讲解如何分析其性能差异
语句执行频率
在SQL中为我们提供了SHOW语句来查看当前数据库的INSERT,DELETE,UPDATE,SELECT的访问频率:
# 显示SQL中各种语句的访问频率
# 注意_______是七个_
SHOW GLOBAL STATUS LIKE 'Com_______';
慢查询日志
慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。
MySQL的慢查询日志默认没有开启,我们可以查看一下系统变量 slow_query_log
# 开启MYSQL慢日志查询开关
slow_query_log = 1;
# 设置慢查询日志的时间为2s,即SQL的语句执行时间超过2s就被记录到慢查询日志中
long_query_time = 2;
这部分暂时了解即可,我们在运维篇会重点介绍日志这一章节
profile详情
首先我们需要查看当前MySQL是否支持profile操作:
# 查看是否支持profile操作
SELECT @@have_profiling;
在默认情况下profile操作时关闭的,我们需要通过set语句开启profile:
# 开启profile操作
SET profiling = 1;
profile可以存储我们之前的操作时长,帮助我们在SQL优化中了解时间损耗的具体项目并加以改善:
# 查看每条SQL语句的耗时情况
SHOW profile;
# 查看指定query_id的SQL语句各个阶段的耗时情况(这里的id是SHOW profile的标号id)
SHOW profile for query query_id;
# 查看指定query_id的SQL语句CPU的使用情况(这里的id是SHOW profile的标号id)
show profile cpu for query query_id;
explain执行计划
Explain是很重要的一部分,我们在下面的调试中经常使用并查看
EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序 。
-- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;
我们可以看到一些关于字段的详细内容(针对其内部分析)
接下来我来一一解释EXPLAIN所给出的信息含义:
字段 | 含义 |
---|---|
id | select查询的序列号,表示查询中执行select子句或者是操作表的顺序 (id相同,执行顺序从上到下;id不同,值越大,越先执行)。 |
select_type | 表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接 或者子查询)、PRIMARY(主查询,即外层的查询)、 UNION(UNION 中的第二个或者后面的查询语句)、 SUBQUERY(SELECT/WHERE之后包含了子查询)等 |
type | 表示连接类型,性能由好到差的连接类型为NULL、system、const、 eq_ref、ref、range、 index、all 。 |
possible_key | 显示可能应用在这张表上的索引,一个或多个。 |
Key | 实际使用的索引,如果为NULL,则没有使用索引。 |
Key_len | 表示索引中使用的字节数, 该值为索引字段最大可能长度,并非实际使用长 度,在不损失精确性的前提下, 长度越短越好 。 |
rows | MySQL认为必须要执行查询的行数,在innodb引擎的表中,是一个估计值, 可能并不总是准确的。 |
filtered | 表示返回结果的行数占需读取行数的百分比, filtered 的值越大越好。 |
索引使用
我们在前面已经学了索引的创建,索引实际上大部分是由系统决定使用类型,但我们也有需要注意的地方
时间差异
我们创建索引时需要遍历所有数据,因而创建索引时的时间相当于我们不创建索引而查询数据的时间
但当我们创建索引后去查询数据,就会发现时间大大减少
# 假设我们的table中有1w条数据,当我们直接查询时可能需要10s
SELECT * FROM table WHERE name = 02932131;
# 但当我们创建name索引,这时可能耗时15s
CREATE INDEX table_name_index ON table;
# 然后我们再凭借name而进行数据查询时,耗时将会接近0s
SELECT * FROM table WHERE name = 02932131;
联合索引注意点
我们在使用联合索引时需要注意以下两点:
- 最左前缀法则
如果索引了多列(联合索引),要遵守最左前缀法则。
最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。
# 例如我们在前面建立了一个idx_user_pro_age_status索引包含了pro,age,status三个列
# 当我们从左边往右边逐渐使用时不会产生错误:
explain select * from tb_user where profession = '软件工程' and age = 31 and status= '0';
explain select * from tb_user where profession = '软件工程' and age = 31;
explain select * from tb_user where profession = '软件工程';
# 但当我们中间省略一列,或者缺少最前面的列,后面的索引列将不再被使用
explain select * from tb_user where age = 31 and status = '0';
explain select * from tb_user where status = '0';
注意:
- where后的索引顺序不受约束,最前面的索引可以放在where的任意位置不受影响
- 范围查询
在联合索引中,不允许出现(>,<),范围查询的右侧列索引失效
# 我们如果使用 <,> 后面的索引将不再生效
explain select * from tb_user where profession = '软件工程' and age > 30 and status = '0';
# 但我们使用 <=,>= 则不受影响
explain select * from tb_user where profession = '软件工程' and age >= 30 and status = '0';
索引失效注意点
针对于索引失效的常见情况共有五种:
- 索引列运算
我们不能在索引列上继续运算,否则索引失效
# 如果我们采用substring等操作,索引列将失效
explain select * from tb_user where substring(phone,10,2) = '15';
- 字符串未加引号
字符串类型字段使用时,不加引号,索引将失效
# 这里最后一部分status的0未加引号,数据仍旧可以输出,但不再通过索引查询
explain select * from tb_user where profession = '软件工程' and age = 31 and status= 0;
- 模糊查询
索引中禁止头部出现模糊查询
# 如果头部出现%,索引失效
-- 索引有效
explain select * from tb_user where profession like '软件%';
-- 索引失效
explain select * from tb_user where profession like '%工程';
-- 索引失效
explain select * from tb_user where profession like '%工%';
- or连接条件
用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。
# 如果phone被设置有索引 但age未设置索引 ,则采用普通查询方法不采用索引
explain select * from tb_user where phone = '17799990017' or age = 23;
- 数据分布影响
如果MySQL评估使用索引比全表更慢,则不使用索引。
# 假设我们所查询的数据占用该表的大多数数据,可能不采用索引而直接采用全表查询
-- 假设我们希望查询 phone 不为 NULL的行,但全表大部分都不为NULL,则会直接采用全表查询
SELECT * FROM table WHERE phone is not NULL;
SQL提示
SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。
# 因为我们的SQL系统自动判定时可能不会采用最佳的运行方法
-- 比如 我们有 profession索引 和 profession,age,status联合索引
-- 当我们希望查询含有profession,age,status的数据时,系统却自动选择profession索引导致速度降低
-- 因而我们需要手动设置SQL提示来提高整体运行速度
# 推荐使用索引 use index
explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';
# 拒绝使用索引 ignore index
explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';
# 强迫使用索引 force index
explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';
覆盖索引
我们希望尽量使用覆盖索引,减少select * 操作。
- 覆盖索引是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 。
# 当我们可以一次性获得所有数据时就不再需要回表查询操作,可以大大提高查询速度
-- 例如:我们的主键为id,索引有name
-- 则下述我们在查询name时,可以获得id,这样就获得了所有数据,就不再进行回表查询
SELECT * FROM table WHERE name = 'xxx';
-- 但如果包含其他元素,我们就会进行回表查询,导致速度降低
SELECT * FROM table WHERE name = 'xxx' and status = '0';
前缀查询
当字段类型为字符串(varchar,text,longtext等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘IO, 影响查询效率。
此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
# 选择前缀长度来创建前缀索引
create index idx_xxxx on table_name(column(n)) ;
当然我们也需要得知如何取得最合适的前缀长度:
# 可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高
# 唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
# 我们可以通过下述代码找到 不同元素 和 全部元素 的比例从而获得最佳前缀长度
select count(distinct email) / count(*) from tb_user ;
select count(distinct substring(email,1,5)) / count(*) from tb_user ;
单列/联合索引选择
我们先来回顾一下单列索引和联合索引:
- 单列索引:即一个索引只包含单个列。
- 联合索引:即一个索引包含了多个列。
# 在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
索引设计原则
我们的索引并非说是无条件的任意设计,我们针对索引的设计上有以下七条规则:
- 针对于数据量较大,且查询比较频繁的表建立索引。
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
SQL优化
这部分我们来介绍SQL的优化
SQL的大部分优化主要是属于我们手动的优化以及配合索引的优化
插入数据优化
插入数据优化有四个方面:
- 批量插入数据
# 我们在插入数据时,可以一次插入多条数据以进行SQL优化(可以一次插入500~1000条数据)
insert into tb_test values (1,'TOM'),(2,'JERRY')...;
- 手动提交事务
# SQL在每条语句后都进行提交会影响整体性能,我们可以手动提交以减轻电脑负担
start transaction;
insert into tb_test values (1,'TOM'),(2,'JERRY')...;
insert into tb_test values (3,'TaM'),(4,'JyRRY')...;
insert into tb_test values (5,'TeM'),(6,'JiRRY')...;
commit;
- 主键顺序插入
# 主键的顺序插入会减轻SQL排序操作直接插入加快速度
主键插入:1,2,3,6,9,12,40,60...
- 大批量插入数据(简单介绍)
如果一次性插入超大量数据,insert语句的插入性能就太低了,因而我们采用load方法插入:
# 如果想要更详细了解,可以移步其他大佬的文章介绍~
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
-- 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fieldsterminated by ',' lines terminated by '\n' ;
主键优化
首先我们要先了解InnoDB存储引擎的数据排序:
- 在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表
主键设计原则:
- 满足业务需求的情况下,尽量降低主键的长度。
- 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
- 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。
- 业务操作时,避免对主键的修改。
这里原本应该具有拓展知识:页合并和页分裂
但因不好讲解所以我把黑马邓老师的链接放在这里,有兴趣的小伙伴可以去查看:33. 进阶-SQL优化-主键优化_哔哩哔哩_bilibili
Order by优化
order by排序具有两种排序方式:
-
Using filesort:
- 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sortbuffer中完成排序操作
- 所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。
-
Using index:
- 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index
- 不需要额外排序,操作效率高。
# 我们通常直接排序(在不使用主键或者索引时)使用的是Using filesort
explain select id,age,phone from tb_user order by name;
# 但当我们通过主键排序或者使用索引后,采用Using index,速度提高
create index idx_user_age_phone_aa on tb_user(age,phone);
explain select id,age,phone from tb_user order by age,phone;
explain select id,age,phone from tb_user order by age desc,phone desc;
# 但是请注意:我们的索引排序也有具有ASC和DESC排序,当我们默认时均为ASC
# 当我们采用ASC,ASC或DESC,DESC时可以采用Using index,但若以ASC,DESC或DESC,ASC的形式时使用Using filesort
# 因而我们如果需要ASC,DESC或DESC,ASC的形式时需要再次创建index:create index idx_user_age_phone_aa on tb_user(age ASC,phone DESC);
create index idx_user_age_phone_aa on tb_user(age ASC,phone DESC);
explain select id,age,phone from tb_user order by age ASC,phone DESC;
Order by优化原则:
- 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。
- 尽量使用覆盖索引。
- 多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)。
- 如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buffer_size(默认256k)。
Group by优化
Group by优化同样借助索引进行优化:
# 当我们正常使用时,效率较低
explain select profession , count(*) from tb_user group by profession ;
# 但当我们建立索引后,效率会有明显提升(注意同样满足索引的使用规范)
create index idx_user_pro_age_sta on tb_user(profession , age , status);
explain select profession , count(*) from tb_user group by profession ;
Group by优化原则:
- 在分组操作时,可以通过索引来提高效率。
- 分组操作时,索引的使用也是满足最左前缀法则的。
Limit优化
limit用来作为分页操作,我们常常在数据过多时对limit进行优化:
# 当我们希望获得第900000个数据后的十个数据,就需要完全获得前9000000个数据才可以,这会损耗许多时间
# 优化思路:
# 我们通过select只获得第9000000个后的十个数据的id
# 然后通过id对比来获得整行数据:
explain select * from tb_sku t , (select id from tb_sku order by id limit 9000000,10) a where t.id = a.id;
Count优化
针对count操作,不同存储引擎有不同的处理方式:
- MyISAM:直接把表的总行数存储在磁盘中,当运行count(*)时直接输出
- InnoDB:需要一行一行读取数据,进行累加
优化思路:
- 在添加数据和删除数据时,同时存储其数据数量
在这里我们顺便讲解一下count的四种常见情况:
count用法 | 含义 |
---|---|
count(主键) | InnoDB 引擎会遍历整张表,把每一行的 主键id 值都取出来,返回给服务层。 服务层拿到主键后,直接按行进行累加(主键不可能为null) |
count(字段) | 没有not null 约束 : InnoDB 引擎会遍历整张表把每一行的字段值都取出 来,返回给服务层,服务层判断是否为null,不为null,计数累加。 有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返 回给服务层,直接按行进行累加。 |
count(1) | InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字“1” 进去,直接按行进行累加。 |
count(*) | InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加。 |
注意:
- count(1)和count(*)速度基本相近,均为最快速度
- 按照效率排序的话,count(字段) < count(主键 id) < count(1) ≈ count(*),所以尽量使用 count( *)。
Update优化
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁升级为表锁 。
# 我们所需要注意的就是Update的操作尽量采用索引来进行改变,这样锁就会变成行锁,只控制这一行数据
# 如果我们采用的Update的操作没有使用索引,那么就会采用表锁,导致整个表的数据都无法改变,影响其他人同步修改该表
-- 这个就是采用行锁,你可以在另一个服务器同步修改该表中其他行
update course set name = 'javaEE' where id = 1 ;
-- 这个采用表锁,你无法在另一个服务器同步修改该表
update course set name = 'SpringBoot' where name = 'PHP' ;
视图
视图(View)是一种虚拟存在的表。
视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上。
视图操作
视图的操作主要分为四部分:
- 创建
# 创建视图
# [WITH [CASCADED | LOCAL] CHECK OPTION] 表示限制条件,我们在后续会讲到
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL] CHECK OPTION]
- 查询
# 查询
-- 查看创建视图语句
SHOW CREATE VIEW 视图名称;
-- 查看视图数据
SELECT * FROM 视图名称;
- 修改
# 修改
-- 方法1:
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL] CHECK OPTION]
-- 方法2:
ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL] CHECK OPTION]
- 删除
DROP VIEW [IF EXISTS] 视图名称;
注意:
- 在视图中我们的INSERT等操作都是直接针对创建视图的原表进行操作
CHECK检查操作
视图的CHECK检查操作就是指[WITH [CASCADED | LOCAL] CHECK OPTION]这部分
首先我们要明白为什么需要检查操作:
# 我们在对视图进行INSERT操作时,会直接对原表进行操作
# 但倘若我们对原表操作成功,但是对该视图要求不符合,该操作结构是不会产生在视图中的,导致原表改变但并未达到我们希望的效果
# 另一方面,我们对视图的创建常常建立于另一视图的操作
# 倘若我们不对此设置检查,可能导致视图创建失败或对之前视图操作失败
当使用WITH CHECK OPTION子句创建视图时,MySQL会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图的定义。
MySQL允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。
为了确定检查的范围,mysql提供了两个选项: CASCADED 和 LOCAL,默认值为 CASCADED 。
- CHECK操作
# 首先我们来介绍CHECK操作
-- CHECK操作会对视图要求进行检测并加以约束
-- 假设我们有一个 原表table含有age属性
-- 下述视图没有设置CHECK
CREATE VIEW table_view1 AS SELECT id,age FROM table WHERE age > 20;
-- 当我们对view1操作时,如果添加的数据没有大于20,仍旧会执行成功
INSERT INTO table_view1 values (1,18);
-- 下述视图设置CHECK
CREATE VIEW table_view2 AS SELECT id,age FROM table WHERE age > 20 WITH CASCADED CHECK OPTION;
-- 当我们对view1操作时,如果添加的数据没有大于20,不会执行成功
INSERT INTO table_view2 values (1,18);
- CASCADED操作
# 下面我们介绍CASCADED操作
-- CASCADED:不仅为当前视图检查条件,而且为当前视图的之前视图检查条件
-- 假设我们有一个 原表table含有age属性
-- 下述视图没有设置CHECK
CREATE VIEW table_view1 AS SELECT id,age FROM table WHERE age > 20;
-- 下述视图view2以view1为模板设置检查条件
CREATE VIEW table_view2 AS SELECT id,age FROM table WHERE age < 25 WITH CASCADED CHECK OPTION;
-- 这时,我们所添加的数据不仅需要满足当前条件age<25,并且需要满足上一视图条件age>20
INSERT INTO table_view2 values (1,23);
- LOCAL操作
# 下面我们介绍LOCAL操作
-- LOCAL:只为当前视图检查条件,不为之前视图设置条件
-- 假设我们有一个 原表table含有age属性
-- 下述视图没有设置CHECK
CREATE VIEW table_view1 AS SELECT id,age FROM table WHERE age > 20;
-- 下述视图view2以view1为模板设置检查条件
CREATE VIEW table_view2 AS SELECT id,age FROM table WHERE age < 25 WITH LOCAL CHECK OPTION;
-- 这时,我们所添加的数据只需要满足当前条件age<25即可
INSERT INTO table_view2 values (1,10);
更新
视图的更新具有一定的严格性
要使视图可更新,视图中的行与基本表中的行之间必须存在一对一的关系
如果视图包含以下任意一项,则不可更新:
- 聚合函数或窗口函数(SUM()、 MIN()、 MAX()、 COUNT()等)
- DISTINCT
- GROUP BY
- HAVING
- UNION 或 UNION ALL
作用
视图一般具有四大作用:
- 简单
- 视图不仅可以简化用户对数据的理解,也可以简化他们的操作。
- 那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。
- 安全
- 数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据
- 数据独立
- 视图可帮助用户屏蔽真实表结构变化带来的影响。
- 数据联合显示
- 可用于联合多表的数据展现在一起,方便阅读使用
存储过程和存储函数
存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。
存储过程具有以下三大特点:
- 封装,复用
- 可以把某一业务SQL封装在存储过程中,需要用到的时候直接调用即可。
- 可以接收参数,也可以返回参数
- 再存储过程中,可以传递参数,也可以接收返回值。
- 减少网络交互,效率提高
- 如果涉及到多条SQL,每执行一次都是一次网络传输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。
基本语法
关于存储函数的基本语法我们大致分为四类:
- 创建
# 创建存储过程
CREATE PROCEDURE 存储过程名称([参数列表])
BEGIN
--SQL语句
END;
- 调用
# 调用存储过程
CALL 名称([参数])
- 查看
# 查看存储过程
-- 查询指定数据库的存储过程及状态信息
SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'XXX';
-- 查询某个存储过程的定义
HOW CREATE PROCEDURE 存储过程名称 ;
- 删除
# 删除存储过程
DROP PROCEDURE [IF EXISTS] 存储过程名称;
三种变量
存储过程中存在三种变量:
- 系统变量
- 用户自定义变量
- 局部变量
让我们一一介绍:
- 系统变量
系统变量 是MySQL服务器提供,不是用户定义的,属于服务器层面。分为全局变量(GLOBAL)、会话变量(SESSION)。
我们直接给出相关变量操作代码:
# 查看系统变量
-- 查看所有系统变量
SHOW [SESSION | GLOBAL] VARIABLES;
-- 可以通过LIKE模糊匹配查找变量
SHOW [SESSION | GLOBAL] VARIABLES LIKE '...';
-- 直接查看指定变量
SELECT @@[SESSION | GLOBAL].系统变量名;
# 设置系统变量
SET [SESSION | GLOBAL] 系统变量名 = 值;
SET @@[SESSION | GLOBAL].系统变量名 = 值;
注意:
- 如果没有指定SESSION/GLOBAL,默认是SESSION,会话变量
- mysql服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。
两者区别:
- 全局变量(GLOBAL): 全局变量针对于所有的会话。
- 会话变量(SESSION): 会话变量针对于单个会话,在另外一个会话窗口就不生效了。
- 用户自定义变量
用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 "@变量名" 使用就可以。其作用域为当前连接。
我们直接给出相关变量操作代码:
# 赋值
-- 直接赋值
SET @var_name = expr [, @var_name = expr] ... ;
SET @var_name := expr [, @var_name := expr] ... ;
SELECT @var_name := expr [, @var_name := expr] ... ;
-- 从表中抽取数据进行赋值
SELECT 字段名 INTO @var_name FROM 表名;
# 使用
SELECT @var_name ;
注意:
- 注意: 用户定义的变量无需对其进行声明或初始化,只不过获取到的值为NULL。
- 局部变量
局部变量 是根据需要定义的在局部生效的变量,访问之前,需要DECLARE声明。可用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的BEGIN ... END块。
我们直接给出相关变量操作代码:
# 声明
-- [DEFAULT ...]表示设置初始化值
DECLARE 变量名 变量类型[DEFAULT ...]
# 赋值
SET 变量名 = 值 ;
SET 变量名 := 值 ;
SELECT 字段名 INTO 变量名 FROM 表名 ... ;
七种结构
在MYSQL中同样设置了相关判断和循环结构:
- if
- case
- while
- repeat
- loop
- cursor
- handler
在介绍循环结构之前,我们先来介绍一下参数设置:
类型 | 含义 | 备注 |
---|---|---|
IN | 该类参数作为输入,也就是需要调用时传入值 | 默认 |
OUT | 该类参数作为输出,也就是该参数可以作为返回值 | |
INOUT | 既可以作为输入参数,也可以作为输出参数 |
用法如下:
CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])
BEGIN
-- SQL语句
END ;
接下来让我们一一介绍结构:
- IF结构
if 用于做条件判断,具体的语法结构为:
IF 条件1 THEN
.....
ELSEIF 条件2 THEN -- 可选
.....
ELSE -- 可选
.....
END IF;
- CASE结构
case结构及作用,和我们在基础篇中所讲解的流程控制函数很类似。
# case结构1:
-- 当case_value的值为 when_value1时,执行statement_list1,
-- 当值为 when_value2时,执行statement_list2,
-- 否则就执行 statement_list
CASE case_value
WHEN when_value1 THEN statement_list1
[ WHEN when_value2 THEN statement_list2] ...
[ ELSE statement_list ]
END CASE;
# case结构2:
-- 当条件search_condition1成立时,执行statement_list1,
-- 当条件search_condition2成立时,执行statement_list2,
-- 否则就执行 statement_list
CASE
WHEN search_condition1 THEN statement_list1
[WHEN search_condition2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;
- WHILE结构
while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。
# while循环结构
-- 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DO
SQL逻辑...
END WHILE;
- REPEAT结构
repeat是有条件的循环控制语句, 当满足until声明的条件的时候,则退出循环 。
# repeat循环结构
-- 先执行一次逻辑,然后判定UNTIL条件是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
SQL逻辑...
UNTIL 条件
END REPEAT;
- LOOP结构
LOOP 实现简单的循环,如果不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。
loop结构需要配合两个语句使用:
- LEAVE :配合循环使用,退出循环。
- ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
# loop循环结构
-- begin_label可以自己设置
-- 退出指定标记的循环体:LEAVE label;
-- 直接进入下一次循环: ITERATE label;
[begin_label:] LOOP
SQL逻辑...
END LOOP [end_label];
我们给出相关案例进行解释:
# 要求:计算从1到n之间的偶数累加的值,n为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环 ----> leave xx
-- C. 如果当次累加的数据是奇数, 则直接进入下一次循环. --------> iterate xx
-- 创建存储过程
create procedure p10(in n int)
begin
-- 设置返回值
declare total int default 0;
-- 进入loop循环,sum是label标记
sum:loop
-- 整体结束判断:如果n减到0, 则退出循环
if n<=0 then
leave sum;
end if;
-- 单个判断:如果当次累加的数据是奇数, 则直接进入下一次循环.
if n%2 = 1 then
set n := n - 1;
iterate sum;
end if;
-- 如果没有问题,最后执行语句
set total := total + n;
set n := n - 1;
-- 结束loop循环
end loop sum;
-- 输出结果
select total;
end;
-- 执行存储过程
call p10(100);
- CURSOR结构
游标(CURSOR)是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用游标对结果集进行循环的处理。
游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下:
# 声明游标
DECLARE 游标名称 CURSOR FOR 查询语句 ;
# 打开游标
OPEN 游标名称;
# 获得游标记录
FETCH 游标名称 INTO 变量[,变量];
# 关闭游标
CLOSE 游标名称;
我们给出相关案例进行解释:
# 要求:根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名(name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。
-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
-- 创建存储过程
create procedure p11(in uage int)
begin
-- 定义变量:用户姓名,用户专业
declare uname varchar(100);
declare upro varchar(100);
-- 定义游标,并设置得到之前表中所需数据
declare u_cursor cursor for select name,profession from tb_user where age <=uage;
-- 创建表
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
-- 打开游标
open u_cursor;
-- 获得数据
while true do
-- 将游标数据赋值给变量
fetch u_cursor into uname,upro;
-- 将变量数据上传至新表中
insert into tb_user_pro values (null, uname, upro);
end while;
-- 关闭游标
close u_cursor;
end;
-- 调用存储过程
call p11(30);
- HANDLER结构
条件处理程序(Handler)可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。
# handler结构
DECLARE handler_action HANDLER FOR condition_value [, condition_value]... statement ;
handler_action 的取值:
CONTINUE: 继续执行当前程序
EXIT: 终止执行当前程序
condition_value 的取值:
SQLSTATE sqlstate_value: 状态码,如 02000
SQLWARNING: 所有以01开头的SQLSTATE代码的简写
NOT FOUND: 所有以02开头的SQLSTATE代码的简写
SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写
我们给出相关案例进行解释:
# 在上述的游标案例中,我们的while循环无法关闭,因为无法判断游标中的值何时结束
# 所以我们需要采用HANDLER结构来关闭WHILE循环
-- 创建存储过程
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name,profession from tb_user where age <=uage;
-- 声明条件处理程序 : 当SQL语句执行抛出的状态码为02000时,将关闭游标u_cursor,并退出
-- 这里也可以采用NOT FOUND 状况码进行判断并退出
declare exit handler for SQLSTATE '02000' close u_cursor;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;
-- 调用存储过程
call p11(30);
存储函数
存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的。
# 存储函数创建结构
CREATE FUNCTION 存储函数名称 ([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN
-- SQL语句
RETURN ...;
END ;
# characteristic 可以有以下三种情况:
-- DETERMINISTIC:相同的输入参数总是产生相同的结果
-- NO SQL :不包含 SQL 语句。
-- READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句。
我们给出相关案例进行解释:
# 要求:计算从1累加到n的值,n为传入的参数值。
-- 创建存储函数
create function fun1(n int)
returns int deterministic
begin
declare total int default 0;
while n>0 do
set total := total + n;
set n := n - 1;
end while;
return total;
end;
-- 执行存储函数
select fun1(50);
触发器
触发器是与表有关的数据库对象,指在insert/update/delete之前(BEFORE)或之后(AFTER),触发并执行触发器中定义的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;
# 解释
-- BEFORE/AFTER 表示是在操作进行前/操作进行后触发
-- INSERT/UPDATE/DELETE 表示操作类型
-- FOR EACH ROW 表示行级触发器
- 查看
# 查看触发器
SHOW TRIGGERS ;
- 删除
# 删除触发器
DROP TRIGGER [schema_name.]trigger_name ;
-- 如果没有指定 schema_name,默认为当前数据库 。
案例解释
我们直接给出一个大案例来对触发器进行讲解
要求:
- 通过触发器记录 tb_user 表的数据变更日志,将变更日志插入到日志表user_logs中, 包含增加,修改 , 删除 ;
- 日志表user_logs创建:
create table user_logs(
id int(11) not null auto_increment,
operation varchar(20) not null comment '操作类型, insert/update/delete',
operate_time datetime not null comment '操作时间',
operate_id int(11) not null comment '操作的ID',
operate_params varchar(500) comment '操作参数',
primary key(`id`)
)engine=innodb default charset=utf8;
- 插入数据触发器:
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
VALUES
(null, 'insert', now(), new.id, concat('插入的数据内容为:id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ',profession=', NEW.profession));
end;
- 修改数据触发器 :
create trigger tb_user_update_trigger
after update on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
VALUES
(null, 'update', now(), new.id,concat('更新之前的数据: id=',old.id,',name=',old.name, ', phone=',old.phone, ', email=', old.email, ', profession=', old.profession,' | 更新之后的数据: id=',new.id,',name=',new.name, ', phone=',NEW.phone, ', email=', NEW.email, ', profession=', NEW.profession));
end;
- 删除数据触发器:
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
VALUES
(null, 'delete', now(), old.id,concat('删除之前的数据: id=',old.id,',name=',old.name, ', phone=',old.phone, ', email=', old.email, ', profession=', old.profession));
end;
锁
锁是计算机协调多个进程或线程并发访问某一资源的机制。(类似java的LOCK)
在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。
如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
MySQL中的锁,按照锁的粒度分,分为以下三类:
- 全局锁:锁定数据库的所有表
- 表级锁:每次操作锁住整张表
- 行级锁:每次操作锁住对应的行数据
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
# 这里对 全库的逻辑备份 做出解释
-- 当我们进行全库逻辑备份时,在实际开发不止一台客户端操作该数据库
-- 整个数据库的保存备份需要一定时间,如果我们在保存过程中,有其他客户端用户进行操作,就会导致备份与原稿不同
下面对全局锁的语法进行介绍:
# 设置全局锁
flush tables with read lock;
# 进行备份
mysqldump -uroot -p123456 itcast > itcaset.sql
-- -u后加账号 -p后加密码 itcast为数据库名称 itcast.sql为本地地址
# 关闭全局锁
unlock tables;
全局锁特点:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。
因而为了防止全局业务停摆,MYSQL给出了一种新的备份方法:
# 在InnoDB引擎中,我们可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。
mysqldump --single-transaction -uroot –p123456 itcast > itcast.sql
表级锁
表级锁,每次操作锁住整张表。
锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。
对于表级锁,主要分为以下三类:
- 表锁
- 元数据锁(MDL)
- 意向锁
表锁
表锁一般分为两种:
- 表共享读锁:对于所有客户端均可以进行读操作,但均不可进行写操作
- 表独占写锁:仅对当前客户端可以进行读写操作,其他客户端不可操作
语法:
# 加锁操作:
lock tables 表名... read/write;
# 释放锁:
unlock tables / 断开客户端连接
总结:
- 读锁不会阻塞其他客户端的读,但是会阻塞写。
- 写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。
元数据锁(MDL)
MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。
MDL锁主要作用:
- 维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作
- 为了避免DML与DDL冲突,保证读写的正确性。
- 某一张表涉及到未提交的事务时,是不能够修改这张表的表结构的。
MDL具体操作为:
- 当对一张表进行增删改查的时候,加MDL读锁(共享)
- 当对表结构进行变更操作的时候,加MDL写锁(排他)
对应SQL | 锁类型 | 说明 |
---|---|---|
lock tables xxx read / write | SHARED_READ_ONLY / SHARED_NO_READ_WRITE | |
select 、select ... lock in share mode | SHARED_READ | 与SHARED_READ、 SHARED_WRITE兼容,与 EXCLUSIVE互斥 |
insert 、update、 delete、select ... for update | SHARED_WRITE | 与SHARED_READ、 SHARED_WRITE兼容,与 EXCLUSIVE互斥 |
alter table ... | EXCLUSIVE | 与其他的MDL都互斥 |
下面我们给出案例演示:
- 案例1(SHARED_READ、 SHARED_WRITE)
# 注意:下述客户端1,2号是同时进行,同时开始事务!
# 这里是 客户端1号
-- 首先我们需要开启事务,才能体现锁的作用
begin;
-- 这里我们使用select 、select ... lock in share mode 开启了SHARED_READ锁
select * from table;
-- 然后我们提交事务
commit;
# 这里是 客户端2号
-- 我们同样采用select 、select ... lock in share mode ,这里反馈的是SHARED_READ锁,与前面的SHARED_READ锁兼容,可以运行
select * from table1;
-- 这里我们采用insert方法,反馈为SHARED_WRITE锁,与前面的SHARED_READ锁兼容,可以运行
update table1 set name = '叶' where id = '1';
-- 然后我们提交事务
commit;
- 案例2(SHARED_READ、 SHARED_WRITE 和 EXCLUSIVE)
# 注意:下述客户端1,2号是同时进行,同时开始事务!
# 这里是 客户端1号
-- 首先我们需要开启事务,才能体现锁的作用
begin;
-- 这里我们使用select 、select ... lock in share mode 开启了SHARED_READ锁
select * from table;
-- 然后我们提交事务
commit;
# 这里是 客户端2号
-- 这里我们采用alter方法,这里触发的是EXCLUSIVE锁,与之前的SHARED_READ锁不兼容
-- 则一直等待,直至客户端1号提交后,才会进行操作
alter table table1 add java int;
-- 然后我们提交事务
commit;
下面我们提供一条语句进行锁的查看:
# 查看所有锁
select object_type,object_schema,object_name,lock_type,lock_duration fromperformance_schema.metadata_locks ;
意见锁
为了避免DML在执行时,加的行锁与表锁的冲突
在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。
首先我们先来模拟一下加锁环节:
# 这里是没有意见锁的情况下:
-- 我们在客户端1中使用了行级锁,这时就不能使用表级锁
-- 表级锁在使用前,需要一行一行检查该行是否具有行级锁,在确认所有行均没有行级锁后才可以进行表级锁
-- 这种操作效率低下
# 这里是有意见锁的情况下:
-- 我们在客户端1中使用了行级锁并且设置了意见锁,这时就不能使用表级锁
-- 表级锁在使用前,只需要检查是否含有意见锁,若含有则等待至意见锁消失(客户端1事务结束)才可以进行表级锁
-- 这种操作效率较高
意见锁分为两种:
- 意向共享锁(IS):
- 由语句select ... lock in share mode添加 。
- 与 表锁共享锁(read)兼容,与表锁排他锁(write)互斥。
- 意向排他锁(IX):
- 由insert、update、delete、select...for update添加 。
- 与表锁共享锁(read)及排他锁(write)都互斥,意向锁之间不会互斥。
我们同样给出案例解释:
- 意向共享锁(IS):
# 这里是客户端1
-- 开启事务
begin;
-- 首先我们设置意见锁:
select * from table1 lock in share mode;
-- 提交事务
commit;
#这里是客户端2
-- 开启事务
begin;
-- 在IS中,我们只能进行read表锁操作
lock tables table1 read;-- 运行成功
-- 在IS中,我们无法进行write操作
lock tables table1 write;-- 运行失败
-- 关闭锁
unlock tables;
-- 关闭事务
commit;
- 意向排他锁(IX):
# 这里是客户端1
-- 开启事务
begin;
-- 首先我们设置意见锁:
select * from table1 lock in share mode;
-- 提交事务
commit;
#这里是客户端2
-- 开启事务
begin;
-- 在IX中,我们无法进行read表锁操作
lock tables table1 read;-- 运行失败,进行堵塞,当客户端提交事务后才可进行
-- 在IX中,我们无法进行write操作
lock tables table1 write;-- 运行失败,进行堵塞,当客户端提交事务后才可进行
-- 关闭锁
unlock tables;
-- 关闭事务
commit;
行级表
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。
对于行级锁,主要分为以下三类:
-
行锁:
- 锁定单个行记录的锁,防止其他事务对此行进行update和delete。
- 在RC、RR隔离级别下都支持。
-
间隙锁(Gap Lock):
- 锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。
- 在RR隔离级别下都支持。
-
临键锁(Next-Key Lock):
- 行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。
- 在RR隔离级别下支持。
行锁
InnoDB实现了以下两种类型的行锁 :
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
- 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
注意:
- 仅当共享锁和共享锁共存时兼容
- 其他情况兼不兼容
下面我们给出不同SQL语句相对应的行锁级别:
SQL | 行锁类型 | 说明 |
---|---|---|
INSERT | 排他锁 | 自动加锁 |
UPDATE | 排他锁 | 自动加锁 |
DELETE | 排他锁 | 自动加锁 |
SELECT | 不加锁 | |
SELECT ... LOCK IN SHARE MOOE | 共享锁 | 需要手动在SELECT之后加LOCK IN SHARE MODE |
SELECT ... FOR UPDATE | 排他锁 | 需要手动在SELECT之后加FOR UPDATE |
行锁特点:
- 默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。
- 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
- InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,此时 就会升级为表锁。
我们同样给出案例解释:
- 正常SELECT语句
# 客户端1
-- 开启事务
begin;
-- 正常select语句,不加锁,对另一个客户端无影响
select * from table1;
-- 提交事务
commit;
# 客户端2
- select...lock in share mode,加共享锁,共享锁与共享锁之间兼容。
# 客户端1
-- 开启事务
begin;
-- select...lock in share mode,加共享锁,共享锁与共享锁之间兼容。
select * from table1 in share mode;
-- 提交事务
commit;
# 客户端2
-- 开启事务
begin;
-- 客户端1采用共享锁,这里同样采用共享锁,可以同时触发
select * from table1 in share mode;
-- 提交事务
commit;
- 共享锁与排他锁之间互斥。
# 客户端1
-- 开启事务
begin;
-- select...lock in share mode,加共享锁,
select * from table1 in share mode;
-- 提交事务
commit;
# 客户端2
-- 开启事务
begin;
-- 客户端1采用共享锁,但这里使用update为排他锁,不能同时使用,该事务等待,直至客户端1提交后才可以运行
update table1 set name = 'xxx' where id = '1';
-- 提交事务
commit;
- 排它锁与排他锁之间互斥
# 客户端1
-- 开启事务
begin;
-- 这里设置为排他锁
update table1 set name = 'xxx' where id = '1';
-- 提交事务
commit;
# 客户端2
-- 开启事务
begin;
-- 客户端1采用排他锁,这里也采用排他锁,两者互斥,需要等待客户端1提交事务后才可以运行
update table1 set name = 'xxx' where id = '1';
-- 提交事务
commit;
- 无索引行锁升级为表锁
# 当我们依据索引进行的查找 属于 行锁
-- 例如下述我们采用id进行查找,属于正常行锁
# 客户端1
-- 开启事务
begin;
-- 这里设置为排他锁
update table1 set name = 'xxx' where id = '1';
-- 提交事务
commit;
# 客户端2
-- 开启事务
begin;
-- 因为上述属于行锁,只锁住id为1的行信息,我们下述修改id为2的内容不受影响
update table1 set name = 'xxx' where id = '2';
-- 提交事务
commit;
# 当我们依据非索引进行的查找 属于 表锁
-- 例如下述我们采用name进行查找,这里就属于表锁
# 客户端1
-- 开启事务
begin;
-- 因为这里的查找并未采用索引查找,所以行锁自动变成表锁,整个表无法进行修改(假设为id为2的行信息)
update table1 set age = 18 where name = 'xxx';
-- 提交事务
commit;
# 客户端2
-- 开启事务
begin;
-- 因为客户端1启动了表锁,所以我们下述的修改无法成功
update table1 set name = 'xxx' where id = '1'; -- 运行失败
-- 提交事务
commit;
间隙锁&临键锁
默认情况下,InnoDB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 next-key 锁进行搜索和索引扫描,以防止幻读。
一般出现上述锁有以下三种情况:
- 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。
- 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-keylock 退化为间隙锁。
- 索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。
注意:
- 间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
接下来我们通过案例进行解释:
- 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。
# 我们给出表结构以及信息
-- id name age
-- 1 xxx 15
-- 3 xxx 23
-- 7 xxx 32
-- 15 xxx 16
-- 25 xxx 27
# 客户端1
-- 开启事务
begin;
-- 当我们进行查询,并且查询的信息是不存在的时
-- 我们会在不存在的信息的前后之间加上间隙锁,例如下述代码会导致:3~7之间存在间隙锁,不能对此进行修改
update table set name = 'xxx' where id = 5;
-- 提交事务
commit;
# 客户端2
-- 开启事务
begin;
-- 因为上述我们对3~7之间设置了间隙锁,我们不能在此之间添加信息
insert into table values (6,'xxx',28) -- 运行错误
-- 提交事务
commit;
- 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-keylock 退化为间隙锁。
# 介绍分析一下:
-- 我们知道InnoDB的B+树索引,叶子节点是有序的双向链表。
-- 假如,我们要根据这个二级索引查询值为x的数据,并加上共享锁,我们是只锁定x这一行就可以了吗?
-- 并不是,因为是非唯一索引,这个结构中可能有多个x的存在,所以,在加锁时会继续往后找,找到一个不满足条件的值。
-- 此时会对x加临键锁,并对x到一个不为x的数之前的间隙加锁。
-- 整体目的就是为了防止出现新的 等值信息 导致结果发生变化
# 我们给出表结构以及信息
-- id name age
-- 1 xxx 15
-- 3 xxx 23
-- 7 xxx 32
-- 15 xxx 16
-- 25 xxx 27
# 我们只使用一个客户端
-- 开启事务
begin;
-- 我们设置普通索引,并通过普通索引进行操作
create index ind_table_age on table(age);
-- 我们通过age查找id为3的行信息并加锁
select * from table where age = 23 lock in share mode;
-- 提交事务
commit;
-- 然后我们进行锁查询
-- 查看所有锁
select object_type,object_schema,object_name,lock_type,lock_duration fromperformance_schema.metadata_locks ;
# 接下来我们可以看到的是
-- 在 3~3 之间存在间隙锁
-- 在 3~7 之间存在间隙锁
-- 在 3 上存在行锁
- 索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。
# 稍微先提示一下:
-- 当我们进行范围查询时,我们所需要锁住的信息包括这个范围中的所有信息
# 我们给出表结构以及信息
-- id name age
-- 1 xxx 15
-- 3 xxx 23
-- 7 xxx 32
-- 19 xxx 16
-- 25 xxx 27
# 我们只使用一个客户端
-- 开启事务
begin;
-- 我们希望查找id>=15的值
select * from table where id >=15 lock in share mode;
-- 提交事务
commit;
-- 然后我们进行锁查询
-- 查看所有锁
select object_type,object_schema,object_name,lock_type,lock_duration fromperformance_schema.metadata_locks ;
# 接下来我们可以看到的是
-- 针对单个 id=19 的行锁
-- 在 (19,25] 之间的临键锁
-- 在 (25,正无穷] 之间的临键锁
InnoDB引擎(拓展内容)
这部分内容大多数属于底层解释,稍微查看理解即可。
InnoDB逻辑存储结构
首先我们来查看一张图,该图表示了InnoDB引擎的整体结构
我们依次介绍图中元素:
- 表空间
表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。
- 段
段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点, 索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。
- 区
区,表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。
- 页
页,是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。
- 行
行,InnoDB 存储引擎数据是按行进行存放的
注意行中有两个隐藏字段:
- Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
- Roll_pointer:每次对某条引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
架构
MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。
下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。
接下来我们分别从内存结构和磁盘结构分开介绍,并在最后介绍一下后台线程:
- 内存结构:
-
Buffer Pool
- 缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。
- 缓冲池以Page页为单位,底层采用链表数据结构管理Page。
- 根据状态,将Page分为三种类型:
- free page:空闲page,未被使用。
- clean page:被使用page,数据没有被修改过。
- dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。
-
Change Buffer
- Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。
- 与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。
-
Adaptive Hash Index
- 自适应hash索引,用于优化对Buffer Pool数据的查询。
- InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。
- 自适应哈希索引,无需人工干预,是系统根据情况自动完成。
- 参数: adaptive_hash_index
-
Log Buffer
- 日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log),默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O。
- 参数:innodb_log_buffer_size:缓冲区大小
- 参数:innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,包含以下三种数值
- 1 : 日志在每次事务提交时写入并刷新到磁盘,默认值。
- 0 : 每秒将日志写入并刷新到磁盘一次。
- 2 : 日志在每次事务提交后写入,并每秒刷新到磁盘一次。
- 磁盘结构:
-
System Tablespace
- 系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)
- 参数:innodb_data_file_path
-
File-Per-Table Tablespaces
- 如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。
- 开关参数:innodb_file_per_table ,该参数默认开启。
-
General Tablespaces
- 通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。
-
Undo Tablespaces
- 撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储undo log日志。
-
Temporary Tablespaces
- InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。
-
Doublewrite Buffer Files
- 双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。
-
Redo Log
- 重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo logbuffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。
- 当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。
- 后台线程:
在InnoDB的后台线程中,分为4类,分别是:Master Thread 、IO Thread、Purge Thread、Page Cleaner Thread。
- Master Thread
- 核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收 。
- IO Thread
- 在InnoDB存储引擎中大量使用了AIO来处理IO请求, 这样可以极大地提高数据库的性能,而IOThread主要负责这些IO请求的回调。
- Purge Thread
- 主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收。
- Page Cleaner Thread
- 协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞。
事务原理
在介绍事务原理前,我们先回顾一下事务的基本概念:
- 事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
事务的四大特点:
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
而这四大特点均有相关的技术支持,我们在下面一一介绍:
redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo logfile),前者是在内存中,后者在磁盘中。
当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。
undo log
回滚日志,用于记录数据被修改前的信息 , 作用包含两个 :
- 提供回滚(保证事务的原子性)
- MVCC(多版本并发控制) 。
undo log和redo log记录物理日志不一样,它是逻辑日志:
- 可以认为当delete一条记录时,undolog中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
- 当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
undo log 具有两种操作:
-
Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。
-
Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的 rollback segment回滚段中,内部包含1024个undo log segment。
MVCC多版本并发控制
我们先通过对比来认识一下MVCC的概念
首先我们了解一下当前读:
-
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select ... lock in share mode(共享锁),select ...for update、update、insert、delete(排他锁)都是一种当前读。
然后了解一下快照读:
- 简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,
不加锁,是非阻塞读。 - Read Committed:每次select,都生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方。
- Serializable:快照读会退化为当前读。
最后我们来介绍MVCC多版本并发控制:
- 全称 Multi-Version Concurrency Control,多版本并发控制。
- 指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。
- MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。
实现原理三部曲
- 隐式字段
当我们创建一个表之后,表中的字段不仅仅包括我们创造的字段,还包括三个自动生成的字段:
隐藏字段 | 含义 |
---|---|
DB_TRX_ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。 |
DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。 |
- undo log日志
回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
undo log日志删除条件:
-
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
-
当update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。
- readView
ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
readView包含四个重要字段:
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
creator_trx_id | ReadView创建者的事务ID |
而在readview中就规定了版本链数据的访问规则: (trx_id 代表当前undolog版本链对应事务ID。 )
条件 | 是否可以访问 | 说明 |
---|---|---|
trx_id == creator_trx_id | 可以访问该版本 | 成立,说明数据是当前这个事务更改 |
trx_id < min_trx_id | 可以访问该版本 | 成立,说明数据已经提交了。 |
trx_id > max_trx_id | 不可以访问该版本 | 成立,说明该事务是在 ReadView生成后才开启。 |
min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中, 是可以访问该版本的 | 成立,说明数据已经提交。 |
不同的隔离级别,生成ReadView的时机不同:
- READ COMMITTED :在事务中每一次执行快照读时生成ReadView。
- REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
MYSQL管理(拓展内容)
MYSQL管理主要分为两个部分:
- 自带数据库
- 常用工具
自带数据库
MYSQL系统自下载后自带四个数据库,具体内容如下:
数据库 | 含义 |
---|---|
mysql | 存储MySQL服务器正常运行所需要的各种信息 (时区、主从、用 户、权限等) |
information_schema | 提供了访问数据库元数据的各种表和视图,包含数据库、表、字段类 型及访问权限等 |
performance_schema | 为MySQL服务器运行时状态提供了一个底层监控功能,主要用于收集 数据库服务器性能参数 |
sys | 包含了一系列方便 DBA 和开发人员利用 performance_schema 性能数据库进行性能调优和诊断的视图 |
常用工具
我们将会介绍到六种常用工具:
- mysql
- mysqladmin
- mysqlbinlog
- mysqlshow
- mysqldump
- mysqlimport
mysql
该mysql不是指mysql服务,而是指mysql的客户端工具。
语法 :
mysql [options] [database]
选项 :
-u, --user=name #指定用户名
-p, --password[=name] #指定密码
-h, --host=name #指定服务器IP或域名
-P, --port=port #指定连接端口
-e, --execute=name #执行SQL语句并退出
mysqladmin
mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。
语法:
mysqladmin [options] command ...
选项:
-u, --user=name #指定用户名
-p, --password[=name] #指定密码
-h, --host=name #指定服务器IP或域名
-P, --port=port #指定连接端口
通过帮助文档查看选项:
mysqladmin --help
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 指定位置间隔内的所有日志。
mysqlshow
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。
语法 :
mysqlshow [options] [db_name [table_name [col_name]]]
选项 :
--count 显示数据库及表的统计信息(数据库,表 均可以不指定)
-i 显示指定数据库或者指定表的状态信息
示例:
#查询test库中每个表中的字段书,及行数
mysqlshow -uroot -p2143 test --count
#查询test库中book表的详细情况
mysqlshow -uroot -p2143 test book --count
mysqldump
mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句。
语法 :
mysqldump [options] db_name [tables]
mysqldump [options] --database/-B db1 [db2 db3...]
mysqldump [options] --all-databases/-A
连接选项 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器ip或域名
-P, --port=# 指定连接端口
输出选项:
--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文件,数据文件
mysqlimport/source
- mysqlimport
mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件。
语法 :
mysqlimport [options] db_name textfile1 [textfile2...]
示例 :
mysqlimport -uroot -p2143 test /tmp/city.txt
- source
如果需要导入sql文件,可以使用mysql中的source 指令 :
语法 :
source /root/xxxxx.sql
结束语
好的,关于MYSQL的进阶篇我们就介绍到这里吧,希望能为各位带来帮助!
附录
文章属于学习笔记,学习于B站黑马邓老师的MYSQL数据库
这里为大家附上链接:01. 进阶-课程介绍_哔哩哔哩_bilibili
因为进阶篇的内容基本已经足够就业的需求,所以针对MYSQL的运维篇可能会在以后更新
下一篇文章应该是JDBC,如果觉得还不错希望能继续关注~
文章评论