MySQL进阶篇
存储引擎
MySQL体系结构
-
连接层
最上层是一些客户端和链接服务,主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
-
服务层
第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如 过程、函数等。
-
引擎层
存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。
-
存储层
主要是将数据存储在文件系统之上,并完成与存储引擎的交互。
存储引擎
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。也就是说同一个数据库下的表,所采用的存储引擎可能是不同的。
- 在创建表时,指定存储引擎 (在MySQL 5.5 版本之后,如果指定默认是 INNODB)
CREATE TABLE 表名(
字段1 字段1类型 [COMMENT 字段1注释],
......
字段N 字段N类型 [COMMENT 字段N注释]
) ENGINE = INNODB [COMMENT 表注释];
- 查询当前数据库支持的存储引擎
SHOW ENGINES;
存储引擎特点
InnoDB
-
介绍
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在MySQL 5.5 之后,InnoDB 是默认的MySQL存储引擎。
-
特点
DML (数据的增删改) 操作遵循ACID模型,支持事务
行级锁,提高并发访问性能
支持 外键 FOREIGN KEY 约束,保证数据的完整性和正确性
-
文件
xxx.ibd:xxx 代表的是表名,InnoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm, sdi)、数据和索引。
参数:innodb_file_per_table,这个参数在 8.0 版本之后是默认打开的,表示每一个表都对应一个独立的表空间
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 存储引擎是比较合适的选择。
- MyIASM:如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。
- MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。
索引
索引概述
-
介绍
索引(index)是帮助 MySQL 高效获取数据的 数据结构(有序)。在数据之外,数据库系统还维护着满足 特定查找算法 的数据结构,这些 数据结构 以某种方式**引用(指向)**数据,这样就可以在这些数据结构上实现高级查找算法,这个数据结构就是索引。
-
演示
在没有索引的情况下,查找数据会进行 全表扫描,从第一条数据逐条扫描,性能极低。
在有索引的情况下,可以极大的降低匹配的次数,提高查询的性能
- 优缺点
优势 | 劣势 |
---|---|
提高数据检索的效率,降低数据库的 IO 成本 | 索引列也是要占用资源的 |
通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 | 索引大大提高了查询效率,同时却也降低了更新表的速度(维护索引结构也会消耗资源),如对表进行 增、删、改时,效率降低。 |
索引结构
MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的结构,主要包含以下几种:
索引结构 | 描述 |
---|---|
B+ tree 索引 | 最常见的索引类型,大部分引擎都支持 B+ 树索引 |
Hash 索引 | 底层数据结构是用哈希表实现的,只有精确匹配索引列的查询才有效,不支持范围查询 |
R-Tree (空间索引) | 空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少 |
Full-text(全文索引) | 是一种通过建立倒排索引,快速匹配文档的方式。类似于Lucense,Solr,ES |
不同存储引擎对不同索引的支持情况
索引 | InnoDB | MyISAM | Memory |
---|---|---|---|
B+ tree 索引 | 支持 | 支持 | 支持 |
Hash 索引 | 不支持 | 不支持 | 支持 |
R-Tree 索引 | 不支持 | 支持 | 不支持 |
Full-text (全文索引) | 5.6 版本之后支持 | 支持 | 不支持 |
我们平常所说的索引,如果没有特别指明,都是指 B+树结构组织的索引
二叉树缺点:顺序插入时,会形成一个链表,查询性能大大降低。大数据量情况下,层级较深,检索速度慢。
红黑树缺点:大数据量情况下,层级较深,检索速度慢。
-
B-Tree (多路平衡查找树)
以一棵最大度数(max-degree)为5(5阶)的 B-Tree 为例(每个节点最多存储4个key,5个指针):
树的度数是指一个节点的子节点个数
- B+ Tree
以一棵最大度数(max-degree)为4(4阶)的 B+ Tree为例:
所有节点的数据均存在叶子节点,上层节点仅作索引功能
增加叶子节点指向兄弟节点的指针,形成一个单向链表
MySQL中的B+ Tree
MySQL索引数据结构对经典的 B+ Tree 进行了优化。在原 B+ Tree 的基础上,增加一个指向相邻叶子节点的链表指针(双向指针),就形成了带有顺序指针的 B+ Tree,提高区间访问的性能。(一页的大小是固定的)
-
Hash索引
哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称hash碰撞),可以通过链表来解决。
Hash索引的特点:
- Hash索引只能用于对等比较(=, in),不支持范围查询(between, >, <, …)
- 无法利用索引完成排序操作
- 查询效率高,通常只需要一次检索就可以了,效率通常要高于 B+Tree 索引
存储引擎支持
在 MySQL 中,支持 Hash 索引的是Memory引擎,而 InnoDB 中具有自适应 Hash功能,Hash索引是存储引擎根据 B+ Tree 索引在指定条件下自动构建的。
为什么InnoDB存储引擎选择使用B+Tree索引结构
- 相对于二叉树,层级更少,搜索效率高
- 对于B-Tree ,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低。
- 相对Hash索引,B+ tree支持范围匹配及排序操作
索引分类
分类 | 含义 | 特点(对于一张表来说) | 关键字 |
---|---|---|---|
主键索引 | 针对于表中主键创建的索引 | 默认自动创建,只能有一个 | PRIMARY |
唯一索引 | 避免表中某数据列中的值重复 | 可以有多个 | UNIQUE |
常规索引 | 快速定位特定数据 | 可以有多个 | |
全文索引 | 全文索引查找的是文本中的关键词,而不是比较索引的值 | 可以有多个 | FULLTEXT |
在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:
分类 | 含义 | 特点 |
---|---|---|
聚集索引 ( Clustered Index) | 将数据存储与索引放到一块,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个(默认主键索引) |
二级索引 (Secondary Index) | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
聚集索引选取规则:
- 如果存在主键,主键索引就是聚集索引
- 如果不存在主键,将使用==第一个唯一(UNIQUE)索引 作为 聚集索引==
- 如果表没有主键,或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引
回表查询
SELECT * FROM user WHERE name = 'Arm';
- 先进入到 name 字段对应的二级索引,去查询 对应行数据的 主键 id
- 在根据这个 主键 id 去主键索引里查找 当前 id 对应的行数据
索引语法
-
创建索引
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ...);
唯一索引:数据列不允许重复,允许为 NULL 值,一张表可有多个唯一索引,索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一
普通索引:一张表可以创建多个普通索引,一个普通索引可以包含多个字段,允许数据重复,允许 NULL 值插入
-
查看索引
SHOW INDEX FROM table_name;
-
删除索引
DROP INDEX index_name ON table_name;
SQL性能分析
SQL执行频率
MySQL客户端连接成功后,通过 show [ session | global ] status 命令可以提供服务器状态信息。通过如下指令,可以查看当前数据库的 INSERT、UPDATE、DELETE、SELECT 的访问频次:
SHOW GLOBAL STATUS LIKE 'COM_______'; # 后面跟了七个下划线
# 查询到的结果
Variable_name Value
Com_binlog 0
Com_commit 3
Com_delete 6
Com_import 0
Com_insert 29
Com_repair 0
Com_revoke 0
Com_select 314
Com_signal 0
Com_update 20
Com_xa_end 0
慢查询日志
慢查询日志记录了所有执行超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。
MySQL的慢查询日志默认没有开启,需要再MySQL的配置文件 ( /etc/my.cnf )中配置如下信息:
# 开启 MySQL 慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=1
配置完毕之后,通过以下指令重新启动MySQL服务进行测试,查看慢日志文件中记录的信息 /var /lib /mysql /localhost-slow.log
systemctl restart mysqld;
profile详情
show prifiles 能够在做SQL优化是帮助我们了解时间都耗费哪里去了。通过 have_profiling 参数,能够看到当前 MySQL 是否支持 prodile操作:
SELECT @@have_profiling;
默认profiling 是关闭的,可以通过set 语句在 session / global 级别开启 profiling:
SET profiling= 1;
执行一系列的业务SQL的操作,然后通过如下指令查看指令的执行耗时:
# 查看每一条 SQL 的耗时基本情况
SHOW profiles;
# 查看指定 query_id 的SQL 语句各个阶段的耗时情况
SHOW profile for query query_id;
# 查看指定query_id 的SQL语句 CPU 的使用情况
SHOW profile cpu
explain执行计划
EXPLAIN 或者 DESC 命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中 表如何连接和连接的顺序。
语法:
# 直接在SELECT 语句之前加上关键字 EXPLAIN / DESC
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;
EXPLAIN执行计划各字段含义:
-
id
SELECT 查询的序列号,表示查询中执行SELECT 子句或者是操作表的顺序(id相同,执行顺序从上到下;id 不同,值越大,越先执行)。
# 数据未格式化对齐 id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 PRIMARY <subquery2> ALL 100.00 1 PRIMARY s eq_ref PRIMARY PRIMARY 4 <subquery2>.student_id 1 100.00 2 MATERIALIZED sc ref fk_course_id,fk_student_id fk_course_id 4 const 2 100.00 Using where 3 SUBQUERY c ALL 3 33.33 Using where
-
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的值越大越好。
索引使用
验证索引效率
- 在未建立索引之前,执行如下SQL语句,查看SQL的耗时
SELECT * FROM tb_sku WHERE sn = '10001';
- 针对字段创建索引
CREATE INDEX idx_sku_sn ON tb_sku(sn);
- 然后再次执行相同的SQL语句,再次查看SQL的耗时。
SELECT * FROM tb_sku WHERE sn = '10001';
最左前缀法则
如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。如果跳跃某一列,索引将部分失效(后面字段的索引失效)。索引字段只要存在就行,跟字段的顺序无关。
范围查询
联合索引中,出现范围查询 大于 或 小于( >, < ),范围查询 右侧的列索引失效
如何规避这种情况,在业务允许的情况下,尽量使用 大于等于 或 小于等于
索引的使用
-
索引列运算
不要在索引列上 进行运算操作,否则索引将 失效
# 查找手机号最后两位是 15 的用户信息 EXPLAIN SELECT * FROM tb_user WHERE SUBSTRING(phone, 10, 2) = '15';
-
字符串不加引号
字符串类型字段使用时,不加引号,索引将失效
EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = 0; # 字段 status 字符串类型未加引号 该列索引失效 EXPLAIN SELECT * FROM tb_user WHERE phone = 17799990015; # 字段 phone 字符串类型,未加引号,该字段索引失效
-
模糊查询
如果仅仅是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失败。
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 前的条件中的列 有索引,而后面的列中 没有索引,那么 涉及的索引 都不会被用到。
EXPLAIN SELECT * FROM tb_user WHERE id = 10 OR age = 23; # 主键 id有索引,但是 age 没有,所以索引全部失效 EXPLAIN SELECT * FROM tb_user WHERE phone = '17799990017' OR age = 23;
-
数据分布影响
如果MySQL评估 使用索引 比 全表扫描更慢,则不使用索引。取决于数据的分布情况,不是固定的
SELECT * FROM tb_user WHERE phone >= '17799990005'; SELECT * FROM tb_user WHERE phone >= '17799990015';
SQL提示
SQL提示,是优化数据库的一个重要手段,简单来说,就是在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 *。
# 以下sql使用了 profession, age, status 联合索引,且返回的字段,在索引中均存在,所以使用了 覆盖索引
EXPLAIN SELECT id, profession FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';
EXPLAIN SELECT id, profession, age, status FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';
# 以下sql中返回的字段,存在索引中不存在的字段,需要回表查询
EXPLAIN SELECT id, profession, age, status, name FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';
EXPLAIN SELECT * FROM tb_user WHERE profession = '软件工程' AND age = 31 AND status = '0';
知识小贴士:
using index condtion:查找使用了索引,但是需要回表查询数据(效率低)
using where; using index:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据。(效率高)
前缀索引
当字段类型为字符串(varchar,text等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘 IO,影响查询效率。此时可以 只将字符串的 一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
- 语法:
CREATE INDEX idx_索引名称 ON table_name(column( n )); # n 代表取字符串的前 n 个字符做前缀索引
-
前缀长度
可以根据索引的选择性来决定,而选择性是指 不重复的索引值(基数)和 数据表的记录总数 的比值,索引选择性 越高则 查询效率越高,唯一索引的选择性是 1,这是最好的索引选择性,性能也是最好的。
SELECT COUNT(DISTINCT email) / COUNT(*) FROM tb_user; # email 是唯一性索引,选择性是 1
SELECT COUNT( DISTINCT SUBSTRING(email, 1, 5) ) / COUNT(*) FROM tb_user; # 截取 email 字段的前五位做索引,选择性是 0.9583 但是索引的存储空间减小了很多,降低索引的体积提高查询的效率。
注意:
使用前缀索引无法进行分组和排序
不能使用覆盖索引
单列索引与联合索引
- 单列索引:即一个索引只包含单个列。
- 联合索引:即一个索引包含了多个列。
联合索引会根据多个字段的值去构建索引
在业务场景中,如果存在多个查询条件,考虑针对于 查询字段建立索引时,建议建立联合索引,而非单列索引。
多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询。
使用联合索引,查询结果使用覆盖索引可以避免回表查询,提高查询效率
索引设计原则
- 针对数据量较大,且查询比较频繁的表建立索引
- 针对于常作为查询条件(WHERE)、排序(ORDER BY)、分组(GROUP BY)操作的字段建立索引。
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 如果是字符串类型的字段,字段的长度越长,可以针对于字段的特点,建立前缀索引。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表查询,提高查询效率。
- 要控制索引的数量,索引并不是 多多益善,索引越多维护索引结构的代价也就越大,会影响增删改的效率。
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL 约束它。当优化器知道每列 是否包含NULL值时,它可以更好地确定哪个索引 最有效地用于查询。
SQL优化
插入数据
insert优化
-
批量插入
insert into tb_user values(1, 'tom'), (2, 'cat'), (3, 'jerry'), ...;
一次插入 500 到 1000条的数据,大量的数据可以分为多条 insert语句
-
手动提交事务
start transaction; nsert into tb_user values(1, 'tom'), (2, 'cat'), (3, 'jerry'), ...; nsert into tb_user values(4, 'tom'), (5, 'cat'), (6, 'jerry'), ...; nsert into tb_user values(7, 'tom'), (8, 'cat'), (9, 'jerry'), ...; ... commit;
手动管理事务,待所有的insert操作完成之后,再提交事务。
-
主键顺序插入
主键乱序插入:8 1 9 21 88 2 ... 主键顺序插入:1 2 3 4 5 7 ...
主键 顺序插入的效率比 乱序插入的效率要高
-
大批量插入数据
如果一次性需要插入大批量数据,使用 insert 语句插入性能较低,此时可以使用 MySQL数据库提供的 load指令 进行插入。
# 客户端连接服务端是,加上参数 --local-infile mysql --local-infile -u root -p # 设置全局参数 local_infile为 1 代表开启从本地加载文件导入数据的开关 set global local_infile=1; # 执行load指令将准备好的数据,加载到表结构中 load data local infile '/root/sql1.sql' into table 'tb_user' fields terminated by ','lines terminated by '\n'; # terminated by ',',代表每个字段数据之间用,逗号隔开(也可以不用逗号) # terminated by '\n' 代表一行的数据用 换行符进行分隔
主键优化
数据组织方式
在InnoDB 存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为 索引组织表(index organized table IOT)
-
页分裂
页可以为空,也可以填充一半,可以填充满,每个页包含了 2 - N 行数据(如果一行数据过大,会行溢出),根据 主键排列。
主键乱序插入的时候会导致页分裂
-
页合并
当删除一行记录时,实际上记录并没有被 物理删除,只是记录被标记(flaged)为删除并且它的表空间变允许被其他记录声明使用。
当页中删除的记录达到 MERGE_THRESHOLD(默认为页的 50%),InnoDB 会开始寻找相邻的页(前或后)看看是否可以将两个页合并以优化空间使用。
MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或者创建索引时指定
主键设计原则
- 在满足业务需求的前提下,尽量降低主键的长度
- 插入数据时,尽量选择 顺序插入,选择使用 AUTO_INCREMENT 自增主键
- 尽量不要使用 UUID 做主键 或者是 其他自然主键,如 身份证号
- 业务操作时,避免对主键的修改
order by优化
- Using filrsort:通过表的索引 或 全表扫描,读取满足条件的数据行,然后在排序缓冲区 sort buffer 中完成排序操作,所有不是通过索引直接返回排序结果的排序 都叫 FileSort 排序。
- Using index:通过有序索引顺序扫描 直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高。
索引默认是按升序排序的,可以根据自己的需要修改
# 没有创建索引时,根据 age, phone 进行排序
EXPLAIN SELECT id, age, phone FROM tb_user order by age, phone;
# 创建索引
CREATE INDEX idx_user_age_phone_aa ON tb_user(age, phone);
# 创建索引后,根据 age, phone 进行升序排序
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age, phone;
# 创建索引后,根据age, phone 进行降序排序
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age DESC, phone DESC;
# 根据 age, phone 进行排序一个升序,一个降序
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age DESC, phone DESC;
# 创建索引
CREATE INDEX idx_user_age_phone_ad ON tb_user(age ASC, phone DESC);
# 根据 age, phone 进行排序一个升序,一个降序
EXPLAIN SELECT id, age, phone FROM tb_user ORDER BY age DESC, phone DESC;
注意点:
-
根据排序字段建立合适的索引,多字段排序时,也遵循 最左前缀法则
-
尽量使用覆盖索引
-
多字段排序,一个升序一个降序,此时需要注意联合索引在 创建时的规则( ASC / DESC)
-
如果不可避免的出现 filesort ,大数据量排序时,可以适当 增大排序缓冲区大小 sort_buffer_size (默认 256k)
SHOW VARIABLES LIKE 'sort_buffer_size'; # 查看排序缓冲区大小
group by优化
# 删除掉目前的联合索引 idx_user_pro_age_sta
drop index idx_user_pro_age_sta on tb_user;
# 执行分组操作,根据 profession 字段分组
explain select profession, count(*) from tb_user group by profession; # 此条分组sql由于未使用索引,所以效率较低
# 创建索引
create index idx_user_pro_age_sta on tb_user(profession, age, status);
# 执行分组操作,根据profession字段分组
explain select profession, count(*) from tb_user group by profession; # 会使用到索引
# 执行分组操作,根据age 字段分组
explain select age, count(*) from tb_user group by age; # 由于不满足最左前缀法则,无法使用索引,效率较低
在分组操作时,可以通过索引来提高效率。
在分组操作时,索引的使用也需要满足最左前缀法则。
limit优化
在大数据量的情况下,进行分页的效率很低
一个常见又非常头疼的问题就是 limit 2000000, 10,此时需要 MySQL排序前 2000010 记录,仅仅返回 2000000 - 2000010的记录,其他记录丢弃,查询排序的代价非常大。
select * from tb_sku limit 9000000, 10; # 这样进行分页查询的效率极低
# 优化思路,通过子查询去优化。排序的过程
select id from tb_sku order by id limit 9000000, 10;
# 但是MySQL不支持这种语法
selct * from tb_sku where id in (select id from tb_sku order by id limit 9000000, 10);
# 所以改成下面这种,可以提高效率
SELECT s.* FROM tb_user s, (select id from tb_sku order by id limit 9000000, 10) a where s.id = a.id;
优化思路:一般分页查询时,通过创建 覆盖索引 能够比较好地提高性能,可以通过 覆盖索引 加 子查询 形式进行优化。
count优化
explain select count(*) from tb_user;
MyISAM 引擎 把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数(前提时,这个sql没有where条件),效率很高。
InnoDB 引擎 就麻烦了,它执行count(*)的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
优化思路:自己计数,通过redis等缓存,进行计数,在插入和删除操作时,对数据进行增减
count的几种用法
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加,最后返回累计值。
用法:count(*)、count(主键)、count(字段)、count(1)(这里的数字填啥数都行)
- count(主键)
InnoDB 引擎会遍历整张表,把每一行的 主键id 值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加 (主键不可能为null)。
- count(字段)
没有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为 null,不为 null,计数累加。
没有not null 约束:InnoDB 引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加。
-
count(1)
InnoDB 引擎会遍历整张表,但不取值。服务层对于返回的每一行,放一个数字 ‘1’ 进去,直接按行进行累加
-
count(*)
InnoDB 引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层 直接按行进行累加。
按照效率排序的话,count( 字段 ) < count( 主键 id ) < count( 1 ) 约等于 count( * ),所以尽量使用 count( * )
update优化
UPDATE语句:条件为索引时,锁的是行锁;不为索引时,锁的是表锁
InnoDB的 行锁是针对索引加的锁,不是针对记录加的锁,并且 该索引不能失效,否则 会从行锁升级为表锁。
视图/存储过程/触发器
视图
视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表(也叫 基表),并且是在使用视图时动态生成的。
通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,==主要的工作就落在创建这条SQL查询语句==上。
视图的操作
- 创建视图
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT [WITH[CASCADED | LOCAL] CHECK OPTION];
# 创建一个student表的 id, name字段的视图
CREATE OR REPLACE VIEW stu_v_1 AS SELECT id, name FROM student WHERE id <= 10;
- 查询
# 查看创建视图语句
SHOW CREATE VIEW 视图名称;
SHOW CREATE VIEW stu_v_1;
# 查看视图数据
SELECT * FROM 视图名称 ...; # 和表的查询一致
SELECT * FROM stu_v_1;
SELECT * FROM stu_v_1 WHERE id = 3;
- 修改
# 方式一:OR REPLACE 代表是否替换视图
CREATE OR REPLACE VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH[CASCADED | LOCAL] CHECK OPTION];
CREATE OR REPLACE VIEW stu_v_1 AS SELECT id, name, no FROM student WHERE id <= 10;
# 方式二:
ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH[CASCADED | LOCAL] CHECK OPTION];
ALTER VIEW stu_v_1 AS SELECT id, name FROM student WHERE id <= 10;
- 删除视图
DROP VIEW [IF EXISTS] 视图名称;
视图检查选项
当使用 WITH CHECK OPTION 子句创建视图时,MySQL 会通过视图检查正在更改的每个行,例如 插入,更新,删除,以使其符合视图的定义,不符合定义的,拒绝操作。MySQL 允许基于另一个视图创建视图,它还会检查依赖视图中的规则以保持一致性。为了确定检查范围,MySQL 提供了两个选项:CASCADED 和 LOCAL,默认值为 CASCADED.
啥都不加:操作时 不会检查当前视图的条件
CASCADED(级联):不仅会检查 当前视图的条件,还会向上检查当前视图 所依赖视图的条件(隐式的为当前视图所依赖的视图增加检查选项)
LOCAL:会检查 当前视图的条件,会向上 检查所依赖视图,如果所依赖视图 有检查选项 便会检查,如果没有检查选项 则不会检查
视图的更新
要使视图可更新,视图中的行 与 基础表中的行之间 必须存在一对一的关系。
如果视图包含以下任何一项,则该视图不可更新:
- 聚合函数 或 窗口函数 ( SUM(), MIN(), MAX(), COUNT() 等 )
- DISTINCT
- GROUP BY
- HAVING
- UNION 或者 UNION ALL
视图的作用
-
简单
视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部条件。
-
安全
数据库可以授权,但不能授权到数据库特定行 和 特定的列上。通过视图用户只能查询 和 修改他们所能见到的数据
-
数据独立
视图可以帮助用户屏蔽真实表结构变化带来的影响
存储过程
存储过程介绍
存储过程 ==是事先经过编译并存储在数据库中的一段SQL语句的集合==,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是 数据库SQL语言层面的代码 封装 与 重用。
特点:
- 封装,复用
- 可以接收参数,也可以返回数据
- 减少网络交互,效率提升
创建存储过程
CREATE PROCEDURE 存储过程名称([参数列表])
BEGIN
SQL语句
END;
调用存储过程
CALL 名称([参数]);
# 创建一个存储过程,查询 student表的记录数
CREATE PROCEDURE p1()
BEGIN
SELECT COUNT(*) FROM student;
END;
# 调用 存储过程 p1
CALL p1();
查看
# 查询指定数据库的存储过程及状态信息
SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = '数据库名';
# 查询某个存储过程的定义
SHOW CREATE PROCEDURE 存储过程名称;
删除存储过程
DROP PROCEDURE [IF EXISTS] 存储过程名称;
注意:在命令行,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定 SQL语句的结束符
mysql> delimiter $$
mysql> CREATE PROCEDURE p1()
mysql> BEGIN
mysql> SELECT COUNT(*) FROM student; # 如果没有修改结束符,mysql会认为 SQL语句到这里就结束,所以在命令行中需要修改结束符
mysql> END$$ # 结束符改为 $$才表示此条SQL结束了
# 调用 存储过程 p1
CALL p1()$$
# 创建完毕之后,为了方便操作,可以将结束符修改回来
mysql> delimiter ;
变量
系统变量
系统变量 是MySQL服务器提供的,不是用户自定义的,属于服务器层面。分为 全局变量(GLOBAL)、会话变量(SESSION)
查看系统变量
# 查看所有系统变量
SHOW [SESSION | GLOBAL] VARIABLES;
# 可以通过 LIKE 模糊匹配方式查找变量
SHOW [SESSION | GLOBAL] VARIABLES LIKE '...';
# 查看指定变量的值
SELECT @@[SESSION | GLOBAL].系统变量名; # 选项和 变量名之间有个 点
# 查看系统变量
SELECT @@SESSION.autocommit;
设置系统变量
SET [SESSION | GLOBAL] 系统变量名 = 变量值;
SET @@[SESSION | GLOBAL].系统变量名 = 变量值;
注意:
如果没有指定 SESSION / GLOBAL,默认是 SESSION,会话变量
MySQL服务重启之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。
用户变量
用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 “@变量名” 使用就可以,其作用域为当前连接 (SESSION)。
赋值
SET @变量名 = 变量值 [,@变量名 = 变量值] ...;
# 设置姓名和年龄变量
set @myname = 'xiaoyou';
set @myage = 21;
SET @变量名 := 变量值 [,@变量名 := 变量值] ...;
# 可以批量赋值
set @mygender := '男', @myhobby := 'java';
# 通过SELECT 赋值
SELECT @变量名 := 变量值 [,@变量名 := 变量值] ...;
# 设置颜色变量
SELECT @mycolor := 'red';
# 把从表中查找出来的数据,赋给一个变量
SELECT 字段名 INTO @变量名 FROM 表名;
# 将student表所有记录数,赋值给 studentCount变量
SELECT COUNT(*) INTO @studentCount FROM student;
使用
SELECT @变量名 [,@变量名] ...;
# 可以批量查询
SELECT @myname;
SELECT @myage;
SELECT @myname, @myage, @mygender, @myhobby;
注意:用户自定义的变量无需对其进行声明 或 初始化,只不过获取到的值为 NULL。
SELECT @abc; # abc这个变量,使用前并未赋值,所以取到的值为 NULL,不会报错
局部变量
局部变量 是根据需要定义的在局部生效的变量,访问之前,需要 DECLARE 声明。可用作存储过程内的局部变量 和 输入参数,局部变量的 作用范围 是 在其声明的 BEGIN … END 块内。
声明
DECLARE 变量名 变量类型 [DEFAULT 默认值];
**变量的类型 **就是 数据库字段的类型:INT, BIGINT, CHAR, VARCHAR, DATE, TIME 等。
赋值
SET 变量名 = 值;
SET 变量名 := 值;
SELECT 字段名 INTO 变量名 FROM 表名...;
# 创建一个存储过程
CREATE PROCEDURE p2()
BEGIN
# 定义一个 INT类型 变量 stu_count 赋默认值 0
DECLARE stu_count INT DEFAULT 0;
# 将student表的记录数 赋给 stu_count
SELECT COUNT(*) INTO stu_count FROM student;
# 查看变量的值
SELECT stu_count;
END;
CALL p2(); # 调用存储过程 p2
if 条件判断
语法:
IF 条件1 THEN
...
ELSEIF 条件2 THEN # 可选
...
ELSE # 可选
...
END IF;
# 创建一个存储过程 p3
CREATE PROCEDURE p3()
BEGIN
# 定义一个变量代表分数,默认值 58
DECLARE score INT DEFAULT 58;
# 定义一个变量代表成绩的等级
DECLARE result VARCHAR(10);
IF score >= 85 THEN # 如果成绩大于 85 为优秀
SET result := '优秀';
ELSEIF score >= 60 THEN # 如果成绩小于85大于 60为及格
SET result := '及格';
ELSE
SET result := '不及格'; # 否则成绩不及格
END IF;
SELECT result; # 最后查看当前成绩对应的等级
END;
CALL p3(); # 结果为 不及格
参数
类型 | 含义 | 备注 |
---|---|---|
IN | 该类参数作为 输入,也就是需要调用时 传入值 | 默认 |
OUT | 该类参数作为 输出,也就是该参数可以作为 返回值 | |
INOUT | 既可以作为 输入参数,也可以作为 输出参数 |
CREATE PROCEDURE 存储过程名称([ IN / OUT / INOUT] 参数名 参数类型)
BEGIN
SQL语句
END;
# 创建一个存储过程,一个int类型的输入参数,一个varchar类型的输出参数
CREATE PROCEDURE p4(IN score INT, OUT result VARCHAR(10))
BEGIN
IF score >= 85 THEN
SET result := '优秀';
ELSEIF score >= 60 THEN
SET result := '及格';
ELSE
SET result := '不及格';
END IF;
END;
# 调用存储过程,传递两个参数
CALL p4(68, @result);
# 查看返回值
SELECT @result;
# 将传入的 200分制的分数,进行换算,换算成百分制,然后返回分数
CREATE PROCEDURE p5( INOUT score DOUBLE )
BEGIN
SET score := score / 2;
END;
SET @score := 178;
CALL p5(@score);
SELECT @score;
case语句
语法一
CASE case_value
WHEN when_value1 THEN statement_list1
[WHEN when_value2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;
语法二
CASE
WHEN search_condition1(条件表达式) THEN statement_list1
[WHEN search_condition2 THEN statement_list2]...
[ELSE statement_list]
END CASE;
# 输入一个参数 月份,判断这个月份是 第几季度
CREATE PROCEDURE p6(IN month INT)
BEGIN
DECLARE result VARCHAR(10);
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 := '第四季度';
ELSE
SET result := '非法参数';
END CASE;
SELECT result;
END;
CALL p6(10);
while循环语句
while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。具体语法为:
# 先判定条件,如果条件满足,则执行逻辑,否则 不执行逻辑
WHILE 条件 DO
SQL逻辑...
END WHILE;
# 创建一个循环过程,计算从 1 加到 n 的和
CREATE PROCEDURE p7(IN num INT)
BEGIN
DECLARE result INT DEFAULT(0);
DECLARE i INT DEFAULT(1);
WHILE i <= num DO
SET result := result + i;
SET i := i + 1;
END WHILE;
SELECT result;
END;
CALL p7(10);
repeat循环
repeat 是有条件的循环控制语句,当满足条件的时候退出循环。具体语法为:
# 先执行一次逻辑,然后判断逻辑是偶满足,如果满足 则退出。如果不满足,则继续下一次循环。
REPEAT
SQL逻辑...
UNTIL 条件
END REPEAT;
# 创建一个 repeat 循环过程,计算从 n 加到 1的结果
CREATE PROCEDURE p8(IN num INT)
BEGIN
DECLARE result INT DEFAULT(0);
REPEAT
SET result := result + num;
SET num := num - 1;
UNTIL num <= 0 # 判断满足条件 则结束循环
END REPEAT;
SELECT result;
END;
CALL p8(10);
loop循环
LOOP 实现简单的循环,如果不在 SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。LOOP可以配合一下两个语句使用:
- LEAVE:配合循环使用,退出循环。(相当于 break)
- ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。(相当于 continue)
[begin_label:] LOOP
SQL逻辑...
END LOOP [end_label];
LEAVE label; # 退出指定标记的循环体
ITERATE label; # 直接进入下一次循环
# 创建一个 repeat 循环过程,计算从 n 加到 1的结果
CREATE PROCEDURE p9(IN num INT)
BEGIN
DECLARE result INT DEFAULT(0);
sum:LOOP # 创建一个循环 LOOP
IF num <= 0 THEN
LEAVE sum; # 如果 num 小于等于 0了就结束循环
END IF;
SET result := result + num;
SET num := num - 1;
END LOOP sum;
SELECT result;
END;
CALL p9(10);
# 计算从 1 加到 n 之间的偶数累加的值,n为传入的参数值
CREATE PROCEDURE p10(IN num INT)
BEGIN
DECLARE result INT DEFAULT(0);
sum:LOOP
IF num <= 0 THEN
LEAVE sum; # 如果 num 小于等于 0了就结束循环
END IF;
IF num % 2 = 1 THEN
SET num := num - 1;
ITERATE sum; # 如果 num为奇数就跳过
END IF;
SET result := result + num;
SET num := num - 1;
END LOOP sum;
SELECT result;
END;
CALL p10(100);
游标
游标(CURSOR)是用来存储查询结果集的数据类型,==在存储过程和函数中可以使用游标对结果集进行循环处理==。游标的使用包括游标的声明、OPEN、FETCH、 和 CLOSE,其语法分别如下。
声明游标
DECLARE 游标名称 CURSOR FOR 查询语句;
打开游标
OPEN 游标名称;
在使用游标之前,一定要打开游标
获取游标记录
FETCH 游标名称 INTO 变量[,变量];
将游标中的数据,赋值给变量
关闭游标
CLOSE 游标名称;
游标使用完毕之后要关闭游标
条件处理程序
**条件处理程序(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 代码的简写
# 传入参数uage, 来查询用户表 tb_user中,所有的用户年龄小于等于 uage 的用户姓名(name) 和 专业(profession)
# 并将用户的姓名和专业插入到所创建的一致新表 (id, name, profession)中
CREATE PROCEDURE p11(IN uage INT)
BEGIN
# 创建两个变量,用来 接收游标中的数据
DECLARE uname VARCHAR(50);
DECLARE uprofession VARCHAR(11);
# 创建游标,用来查询结果集
DECLARE u_cursor CURSOR FOR SELECT name, profession FROM tb_user WHERE age <= uage;
# 创建一个条件处理程序,当 结果集中无数据返回时,结束程序,并关闭游标
# DECLARE EXIT HANDLER FOR SQLSTATE '02000' CLOSE u_cursor;
DECLARE EXIT HANDLER FOR NOT FOUND 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(50),
profession VARCHAR(11)
);
# 开启游标
OPEN u_cursor;
# 获取游标中的数据
WHILE true DO
FETCH u_cursor INTO uname, uprofession;
INSERT INTO tb_user_pro(name, profession) VALUES(uname, uprofession);
END WHILE;
# 关闭游标
CLOSE u_cursor;
END;
CALL p11(40);
存储函数
存储函数 ==是有返回值的存储过程==,存储函数的参数只能是 IN 类型的。具体语法如下:
CREATE FUNCTION 存储函数名称 ([参数列表])
RETURNS type [characteristic...] # type 返回值类型
BEGIN
SQL语句
RETURN ...;
END;
characteristic(存储参数特性)说明:
- DETERMINISTIC: 相同的输入参数总是产生相同的结果
- NO SQL: 不包含 SQL 语句
- READS SQL DATA: 包含读取数据的语句,但不包含写入的数据的语句
# 求 1 到 n 的累加和
CREATE FUNCTION fun1(n INT) # 创建存储函数 参数 n
RETURNS INT DETERMINISTIC # 返回 累加和,并且 存储参数特性为 DETERMINISTIC
BEGIN
DECLARE result INT DEFAULT(0);
WHILE n > 0 DO
SET result := result + n;
SET n := n - 1;
END WHILE;
RETURN result; # 返回累加和
END;
SELECT fun1(100); # 调用存储函数
触发器
触发器 是与表有关的数据库对象,指在 insert / update / delete 之前 或 之后,触发并执行触发器中定义的SQL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性,日志记录,数据校验等操作。
使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还支持行级触发(修改几行触发几次),不支持语句级触发(不管修改几行都只触发一次)。
触发器类型 | NEW 和 OLD |
---|---|
INSERT 型触发器 | NEW 表示 将要 或者 已经新增的数据 |
UPDATE 型触发器 | OLD 表示 修改之前的数据,NEW 表示 将要 或 已经修改后的数据 |
DELETE 型触发器 | OLD 表示 将要 或者 已经删除的数据 |
语法
- 创建
CREATE TRIGGER 触发器名称
BEFORE/ AFTER INSERT/ UPDATE/ DELETE
ON 表名 FOR EACH ROW # 行级触发器
BEGIN
触发器语句;
END;
- 查看
SHOW TRIGGERS;
- 删除
DORP TRIGGER [数据库名称] 触发器名称; # 如果没有指定 数据库名称,默认为 当前数据库
创建插入触发器
-- 需求: 通过触发器记录 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;
-- 查看,触发器是否创建成功
show triggers ;
-- 删除
drop trigger tb_user_insert_trigger;
-- 插入数据到tb_user
insert into tb_user(id, name, phone, email, profession, age, gender, status, createtime) VALUES (26,'三皇子','18809091212','erhuangzi@163.com','软件工程',23,'1','1',now());
# 操作数据成功之后,去日志表中查看日志信息是否存在,表明触发器是否被触发
-- 修改数据触发器
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;
show triggers ;
update tb_user set profession = '会计' where id = 23;
update tb_user set profession = '会计' where id <= 5;
-- 删除数据触发器
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;
show triggers ;
delete from tb_user where id = 26;
锁
介绍
锁是计算机协调多个进程 或 线程 并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性 是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
分类
MySQL中的锁,按照锁的粒度分,分为以下三类:
- 全局锁:锁定 数据库中的 所有表
- 表级锁:每次操作锁住 整张表
- 行级锁:每次操作锁住 对应的行数据
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的 DML 的写语句,DDL 语句,已经更新操作的事务提交语句都将被阻塞。
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
通过全局锁实现数据备份
mysql> FLUSH TABLES WITH READ LOCK; # 在 mysql客户端中给数据库加全局锁
c:> mysqldump -u 用户名 -p 密码 数据库名> 保存的路径/ 文件名.sql # 在命令行中使用 mysqldump 备份数据
mysql> UNLOCK TABLES; # 备份完毕时,解锁
数据库中加全局锁,是一个比较重的操作,存在以下问题:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(binlog),会导致主从延迟。
在 InnoDB 引擎中,我们可以在备份时加上参数 single-transaction 参数来完成不加锁的一致性数据备份。
mysqldump --single-transaction -u用户名 -p密码 数据库名> 保存的路径/ 文件名.sql # 在命令行中使用 mysqldump 备份数据
表级锁
表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在 MyISAM、InnoDB、BDB等存储引擎中。
对于表级锁,主要分为以下三类:
- 表锁
- 元数据锁(meta data lock, MDL)
- 意向锁
表锁
对于表锁,分为两类:
- 表共享读锁( read lock )
- 表独占写锁( write lock )
语法:
-
加锁:LOCK TABLES 表名 … read / write
加了读锁之后,所有客户端可以读 但不可以写(阻塞状态)
-
释放锁:UNLOCK TABLES / 客户端断开连接
加了写锁后,当前客户端可以读写,其他客户端不可读写(阻塞状态)
元数据锁(meta data lock, MDL)
MDL 加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁 主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。
在 MySQL 5.5 中引入了 MDL,当对一张表 进行增删改查的时候,加 MDL 读锁(共享)。
当对表结构进行变更操作时,加 MDL 写锁(排他)
对应SQL | 锁类型 | 说明 |
---|---|---|
LOCK TABLES xxx READ / WRITE | SHARED_READ_ONLY / SHARED_NO_READ_WRITE | |
SELECT 、SELECT … LOCK IN SHARE MODE | SHARE_READ | 与 SHARED_READ、SHARED_WRITE 兼容,与 EXCLUSIVE互斥 |
INSERT、UPDATE、DELETE、SELECT … FOR UPDATE | SHARE_WRITE | 与 SHARED_READ、SHARE_WRITE 兼容,与 EXCLUSIVE互斥 |
ALTER TABLE … | EXCLUSIVE(排他) | 与其他的 MDL 都互斥 |
查看元数据锁:
SELECT object_type, object_schema, object_name, lock_type, lock_duration FROM performance_schema.metadata_locks;
注意:只有在执行 增删改查 操作时,系统自动加 MDL 锁,使用完毕自动解锁
意向锁
为了避免 DML 在执行时,加的行锁与表锁的冲突,在 InnoDB 中引入了意向锁,使得表锁不用检查每行数据是否加加锁,使用意向锁来减少表锁的检查。
未加意向锁
加意向锁
加了意向锁之后,当想添加表锁时不需要逐行去检查行锁,只需要去检查 意向锁 或 意向锁是否互斥即可
-
意向共享锁( IS ):由语句 SELECT… LOCK IN SHARE MODE 添加。
与表锁 共享锁( READ )兼容,与表锁 排他锁( WRITE )互斥。
-
意向排他锁( IX ):由 INSERT、UPDATE、DELETE、SELECT … FOR UPDATE 添加。
与表锁 共享锁( READ )及排他锁( WRITE )都互斥。意向锁之间不会互斥。
可以通过以下 SQL,查看意向锁及行锁的加锁情况:
SELECT object_schema, object_name, index_name, lock_type, lock_mode, lock_data FROM performance_schema.data_locks;
客户端1
client1> begin; # 开启事务
client1> select * from tb_user where id = 1 lock in share mode ; # 开启 行锁 和 意向共享锁
客户端2
client2> lock tables tb_user read ; # 客户端2 开启 共享表锁成功,因为共享表锁和 意向共享锁不互斥
client2> unlock tables ;
client2> lock tables tb_user write; # 开启 排他锁失败(会进入阻塞状态,等待进程释放锁),因为和 共享锁互斥
客户端1
client1> begin;
client1> select * from tb_user where id = 1 lock in share mode ;
client1> commit # 客户端1 提交事务后,行锁 和 意向锁 解除
客户端2
client2> lock tables tb_user write; # 客户端1 释放锁之后,客户端2 解除阻塞状态,拿到 表级排他锁
意向排他锁
client1> begin;
client1> update tb_user set age = 50 where id = 1; # 使用更新操作,会自动加 意向排他锁
client2> lock tables tb_user read; # 客户端2 无法加锁,进入阻塞状态,只有等待 客户端1 释放锁才能拿到锁
行级锁
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在 InnoDB 存储引擎中。
InnoDB 的数据是基于 索引组织的,行锁是通过对 索引上的索引项加锁来实现的,而不是对记录加的锁。
对于行级锁,主要分为以下三类:
1.行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此进行 UPDATE 和 DELETE。在 RC、RR 隔离级别下都支持。
2.间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 INSERT,产生幻读。在 RR 隔离级别下都支持。
3.临键锁(Next-Key Lock):行锁 和 间隙锁 组合,同时锁住数据,并锁住 数据 前面所有的 间隙 Gap。在 RR 隔离级别下支持。
行锁
InnoDB 实现了以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得 相同数据集的 排他锁。
- 排他锁(X):允许获得 排他锁的事务 更新数据,阻止其他事务获得 相同数据集的 共享锁 和 排他锁。
锁的类型 | S(共享锁) | X(排他锁) |
---|---|---|
S(共享锁) | 兼容 | 冲突 |
X(排他锁) | 冲突 | 冲突 |
一个事务拿到一行数据的 共享锁后,别的事务只能对其加 共享锁。
一个事务拿到一行数据的 排他锁后,别的事务无法对其加 锁
SQL | 行锁类型 | 说明 |
---|---|---|
INSERT | 排他锁 | 自动加锁 |
UPDATE | 排他锁 | 自动加锁 |
DELETE | 排他锁 | 自动加锁 |
SELECT(正常) | 不加任何锁 | |
SELECT … LOCK IN SHARE MODE | 共享锁 | 需要手动在 SELECT 之后加 LOCK IN SHARE MODE |
SELECT … FOR UPDATE | 排他锁 | 需要手动在 SELECT之后加 FOR UPDATE |
在默认情况下,InnoDB 在 REPEATABLE READ(默认)事务隔离级别运行,InnoDB使用 间隙锁 ( Next-Key Lock )进行搜索 和 索引扫描,防止幻读。
行级锁
1.针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
2.InnoDB 的行锁是针对于索引加的行锁,不通过索引条件检索数据,那么 InnoDB 将对表中的所有记录加锁,此时 就会升级为表锁。
间隙锁 / 临键锁
1.索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁。
2.索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,临键锁(Next-Key Lock)退化为间隙锁。
3.索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止(临键锁)。
注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一个间隙上采用间隙锁。
InnoDB引擎
逻辑存储结构
-
表空间(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 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
架构
MySQL 5.5 版本开始,默认使用 InnoDB 存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。
下面是 InnoDB 架构图
内存结构
缓冲池
更改缓冲区(5.X 版本叫 插入缓冲区,8.0 版本后该叫 更改缓冲区)
日志缓冲区
磁盘结构
后台线程
事务管理
- redo log
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及 重做日志文件(redo log file),前者是在内存中,后者在磁盘中。
当事务提交之后会把修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
- undo log
回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚 和 MVCC (多版本并发控制)
undo log 和 redo log 记录物理日志不一样,它是逻辑日志。
可以认为当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录。
当执行 rollback时,就可以从 undo log中的逻辑记录读取到相应的内容并进行回滚。
Undo log 销毁:undo log 在事务执行时产生,事务提交时,并不会立即删除 undo log,因为这些日志可能还用于 MVCC。
Undo log存储:undo log采用 段的方式进行管理和记录。存放在前面介绍的 rollback segment 回滚段中,内部包含 1024个 undo log segment。
MVCC
当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
对于我们日常的操作,如 select … lock in share mode(共享锁),select … for update、insert、delete(排他锁)都是一种当前读。
快照读
简单的select(不加锁)就是快照读,快照读读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- Read Committed:每次select,都会生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方
- Serializable:快照读会退化为当前读。
MVCC
全称 Muti-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为 MySQL实现 MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。
MVCC-实现原理
记录中的隐藏字段
隐藏字段 | 含义 |
---|---|
DB_TRX_ID | 最近修改事务ID,记录插入这条记录 或 最后一次 修改事务该记录的事务 ID |
DB_POLL_PTR | 回滚指针,指向这条记录的上一版本,用于配合 undo log,指向上一个版本。 |
DB_ROW_ID | 隐藏主键,如果表结构 没有指定主键,将会生成 该隐藏字段。 |
root@localhost 存放ibd文件的目录> ibd2sdi ibd文件名.ibd; # 查看表的结构信息
undo log
回滚日志,在insert、update、delete 的时候产生的便于数据回滚的日志。
当insert 的时候,产生的 undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读是也需要,不会立即被删除。
不同事务 或 相同事务 对同一条记录进行修改,会导致该记录的 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 的时机不同:
READ COMMITTED:在事务中每一次执行快照读时生成 ReadView。
REPEATABLE READ:仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView。
MVCC实现原理
MySQL管理
系统数据库
MySQL数据库安装完成后,自带了以下四个数据库,具体作用如下:
数据库 | 含义 |
---|---|
mysql | 存储MySQL服务器正常运行时所需要的各种信息(时区、主从、用户、权限等) |
information_schema | 提供了访问 数据库元数据 的各种表 和 视图,包含数据库、表、字段类型 及 访问权限等 |
performance_schema | 为 MySQL 服务器运行时状态提供了一个底层监控功能,主要用于收集数据库服务器性能参数 |
sys | 包含了一系列方便 DBA 和 开发人员利用 performance_schema 性能数据库进行 性能调优 和 诊断的视图 |
常用工具
mysql
该mysql不是指 mysql 服务,而是指 mysql 的客户端工具
# 语法:
mysql [options] [database]
# 选项:
-u, --user=name # 指定用户名
-p, --password[=name] # 小写的p,指定用户密码
-h, --host=name # 指定服务器ip 或 域名
-P, --port=port # 大写的P,指定连接端口
-e, --execute=name # 执行SQL语句并退出
-e 选项 可以在Mysql客户端执行SQL语句,而不用连接到 MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便。
示例:
mysql -u用户名 -p密码 数据库名 -e"select * from stu";
mysqladmin
mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置 和 当前状态、创建 并删除数据库等。
# 通过帮助文档查看选项:
mysqladmin --help
# 示例:
# 删除一个数据库
mysqladmin -u用户名 -p密码 drop '数据库名';
# 查看mysql版本
mysqladmin -u用户名 -p密码 version;
mysqlbinlog
由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文件的文本格式,就会用到 mysqlbinlog 日志管理工具
# 语法:
mysqlbinlog [options] log-file1 log-file2 ...
# 选项:
-d, --database=name # 指定数据库名称,只列出指定的数据库相关操作。
-o, --offset=# # 忽略掉日志文件中的前 n 行命令
-r, --result-file=name # 将输出的文本格式日志输出到指定文件
-s, --short-form # 显示简单格式,省略掉一些信息
--start-datetime=date1 --stop-datetime=date2 # 指定日期间隔内的所有日志
--start_datetime=pos1 --stop-position=pos2 # 指定位置间隔内的所有日志
mysqlshow
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些 数据库、数据库中的表、表中的列 或者 索引。
# 语法:
mysqlshow [options][数据库名称][表名[列名]]
# 选项:
--count # 显示数据库 及 表的统计信息(数据库,表 均可以不指定)
-i # 显示指定数据库 或者 指定表的状态信息
# 示例:
# 查询每个数据库的表的数量 及 表中记录的数量
mysqlshow -u用户名 -p密码 --count
# 查询 指定数据库中每个表中的字段数,及行数
mysqlshow -u用户名 -p密码 数据库名 --count
# 查询 指定数据库中 指定表的详细情况
mysqlshow -u用户名 -p密码 数据库名 表名 --count
mysqldump
mysqldump 客户端工具用来备份数据库 或 在不同数据库之间进行数据迁移。
备份内容包含 创建表,及插入表的SQL语句。
# 语法:
# 备份指定的数据库
mysqldump [options] 数据库名[表名]
# 备份哪些数据库
mysqldump [options] --database/-B db1[db2 db3 ...]
# 备份所有的数据库
mysqldump [options] --all-databases/-A
# 输出选项:
--add-drop-dadabase # 在每个数据库创建语句前加上 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 是客户端数据导入工具,用来导入 mysqldump 加 -T 参数后导出的文本文件。
# 语法:
mysqlimport[options] db_name textfile1 [textfile2 ...]
# 示例:
mysqlimport -u用户名 -p密码 数据库名 /tmp/city.txt
如果需要导入 sql文件,可以使用 mysql中的source 指令:
# 语法:
mysql> source /root/xxx.sql
- from stu";
#### mysqladmin
mysqladmin ==是一个执行管理操作的客户端程序==。可以用它来==检查服务器的配置 和 当前状态、创建 并删除数据库等==。
```bash
# 通过帮助文档查看选项:
mysqladmin --help
# 示例:
# 删除一个数据库
mysqladmin -u用户名 -p密码 drop '数据库名';
# 查看mysql版本
mysqladmin -u用户名 -p密码 version;
mysqlbinlog
由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文件的文本格式,就会用到 mysqlbinlog 日志管理工具
# 语法:
mysqlbinlog [options] log-file1 log-file2 ...
# 选项:
-d, --database=name # 指定数据库名称,只列出指定的数据库相关操作。
-o, --offset=# # 忽略掉日志文件中的前 n 行命令
-r, --result-file=name # 将输出的文本格式日志输出到指定文件
-s, --short-form # 显示简单格式,省略掉一些信息
--start-datetime=date1 --stop-datetime=date2 # 指定日期间隔内的所有日志
--start_datetime=pos1 --stop-position=pos2 # 指定位置间隔内的所有日志
mysqlshow
mysqlshow 客户端对象查找工具,用来很快地查找存在哪些 数据库、数据库中的表、表中的列 或者 索引。
# 语法:
mysqlshow [options][数据库名称][表名[列名]]
# 选项:
--count # 显示数据库 及 表的统计信息(数据库,表 均可以不指定)
-i # 显示指定数据库 或者 指定表的状态信息
# 示例:
# 查询每个数据库的表的数量 及 表中记录的数量
mysqlshow -u用户名 -p密码 --count
# 查询 指定数据库中每个表中的字段数,及行数
mysqlshow -u用户名 -p密码 数据库名 --count
# 查询 指定数据库中 指定表的详细情况
mysqlshow -u用户名 -p密码 数据库名 表名 --count
mysqldump
mysqldump 客户端工具用来备份数据库 或 在不同数据库之间进行数据迁移。
备份内容包含 创建表,及插入表的SQL语句。
# 语法:
# 备份指定的数据库
mysqldump [options] 数据库名[表名]
# 备份哪些数据库
mysqldump [options] --database/-B db1[db2 db3 ...]
# 备份所有的数据库
mysqldump [options] --all-databases/-A
# 输出选项:
--add-drop-dadabase # 在每个数据库创建语句前加上 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 是客户端数据导入工具,用来导入 mysqldump 加 -T 参数后导出的文本文件。
# 语法:
mysqlimport[options] db_name textfile1 [textfile2 ...]
# 示例:
mysqlimport -u用户名 -p密码 数据库名 /tmp/city.txt
如果需要导入 sql文件,可以使用 mysql中的source 指令:
# 语法:
mysql> source /root/xxx.sql