2025-02-07

This commit is contained in:
smallkun 2025-02-07 01:33:26 +08:00
parent 0e0da0565f
commit 3dfc7d7b72

View File

@ -0,0 +1,984 @@
## 索引
本章主要介绍索引的相关内容,包括什么是索引,索引的增、删、改、查,索引类型。其中索引类型包括主键索引、唯一索引、普通索引以及前缀索引。
执行以下SQL语句该语句可以生成500万条用户数据。
```sql
-- 用户表结构
CREATE TABLE `five_million_user`(
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(50) DEFAULT '' COMMENT '用户名称',
`email` VARCHAR(50) NOT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT '' COMMENT '手机号',
`sex` TINYINT DEFAULT '0' COMMENT '性别0-男 1-女)',
`password` VARCHAR(100) NOT NULL COMMENT '密码',
`age` TINYINT DEFAULT '0' COMMENT '年龄',
`create_time` DATETIME DEFAULT NOW(),
`update_time` DATETIME DEFAULT NOW(),
PRIMARY KEY (`id`)
)ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT='500万用户表';
-- 创建生成500万数据的函数gen_five_million_user
SET GLOBAL log_bin_trust_function_creators=TRUE;
DELIMITER $$
CREATE FUNCTION gen_five_million_user()
RETURNS INT
BEGIN
DECLARE num INT DEFAULT 5000000;
DECLARE i INT DEFAULT 0;
WHILE i < num DO
INSERT INTO five_million_user(`name`,`email`,`phone`,`sex`,`password`,`age`)
VALUES(
CONCAT('用户姓名',i),
CONCAT(CONCAT('18',FLOOR(RAND() * ((999999999 - 100000000) + 1000000000))),'qq.com'),
CONCAT('18',FLOOR(RAND() * ((999999999 - 100000000) + 1000000000))),
FLOOR(RAND() * 2),
UUID(),
FLOOR(RAND() * 100)
);
SET i = i + 1;
END WHILE;
RETURN i;
END $$
DELIMITER ;
-- 执行函数
SELECT gen_five_million_user();
```
### 案例
通过如下SQL语句查询用户名为“用户姓名3999999”的用户
```sql
SELECT *
FROM five_million_user
WHERE name = '用户姓名3999999';
```
<img src="https://yp.smallkun.cn/markdown/image-20250122125541634.png!compress" alt="image-20250122125541634" style="zoom:50%;" />
发现查询需要3秒钟速度非常慢在工作中这样的查询效率是无法接受的用户都希望能快速看到数据。如何解决这个问题呢可以给表创建索引例如
```sql
#给five_million_user表的name字段创建索引索引名称为idx_name
CREATE INDEX inx_name ON five_million_user(`name`);
```
<img src="https://yp.smallkun.cn/markdown/image-20250122125907017.png!compress" alt="image-20250122125907017" style="zoom:50%;" />
创建索引后执行效率明显提升性能提升50倍以上。当然不同的服务器性能不一样执行时间会有所差异。
注意在一张数据表中只能有一个主键索引当表中的数据比较少时例如少于500行是不需要创建索引的。另外当数据重复度大比如高于30%)的时候,也不需要对这个字段使用索引,例如性别字段,就不需要对它创建索引。
### 索引增、删、改、查
#### 创建索引
##### 1创建表的同时创建索引
```sql
-- 单字段索引创建语法
CREATE TABLE 表名 (
字段名 字段数据类型,
...,
[INDEX | KEY | UNIQUE] 索引名 (字段名)
);
-- 多字段索引创建语法
CREATE TABLE 表名 (
字段名1 字段数据类型,
字段名2 字段数据类型,
...,
[INDEX | KEY | UNIQUE] 索引名 (字段名1, 字段名2, ...)
);
```
创建单字段普通索引示例如下:
```sql
CREATE TABLE index_test (
column_a VARCHAR(100) NULL,
column_b VARCHAR(100) NULL,
-- 创建 column_a 单字段普通索引,索引名称为 idx_a
INDEX idx_a (column_a)
);
```
创建多字段普通索引示例如下:
```sql
CREATE TABLE index_test (
column_a VARCHAR(100) NULL,
column_b VARCHAR(100) NULL,
-- 创建 column_a、column_b 多字段普通索引,索引名称为 idx_a_b
INDEX idx_a_b (column_a, column_b)
);
```
创建单字段唯一索引示例如下:
```sql
CREATE TABLE index_test (
column_a VARCHAR(100) NULL,
column_b VARCHAR(100) NULL,
-- 创建 column_a 单字段唯一索引,索引名称为 idx_a
UNIQUE idx_a (column_a)
);
```
创建多字段唯一索引示例如下:
```sql
CREATE TABLE index_test (
column_a VARCHAR(100) NULL,
column_b VARCHAR(100) NULL,
-- 创建 column_a、column_b 多字段唯一索引,索引名称为 idx_a_b
UNIQUE idx_a_b (column_a, column_b)
);
```
创建单字段全文索引类似把index或者unique关键字替换成fulltext关键字即可。创建主键索引也只是把index或者unique关键字替换成primary key并设置字段不为null例如
```sql
CREATE TABLE index_test (
column_a VARCHAR(100) NULL,
column_b VARCHAR(100) NULL,
-- 创建 column_a 单字段全文索引,索引名称为 idx_a
FULLTEXT idx_a (column_a)
);
```
创建主键索引
```sql
CREATE TABLE index_test (
column_a VARCHAR(100) NOT NULL,
column_b VARCHAR(100) NULL,
-- 设置 column_a 字段为主键MySQL 会自动创建主键索引和唯一索引
PRIMARY KEY (column_a)
);
```
##### 2通过修改表来创建索引
```sql
ALTER TABLE 表名 ADD [INDEX | KEY | UNIQUE] 索引名 (字段);
# 主键索引
ALTER TABLE index_test ADD PRIMARY KEY (column_a);
# 唯一索引
ALTER TABLE index_test ADD UNIQUE (column_a);
# 普通索引
ALTER TABLE index_test ADD INDEX idx_name (column_a);
# 全文索引
ALTER TABLE index_test ADD FULLTEXT (column_a);
# 多列索引
ALTER TABLE index_test ADD INDEX index_name (column_a, column_b);
```
##### 3使用create语句直接给已存在的表创建索引
```sql
# 语法:
CREATE INDEX 索引名 ON 表名 (字段);
# 普通索引(单列)
CREATE INDEX idx_test
ON index_test (column_a)
COMMENT '普通索引(单列)';
# 组合索引
CREATE INDEX idx_test
ON index_test (column_a, column_b)
COMMENT '组合索引';
# 唯一索引
CREATE UNIQUE INDEX idx_test
ON index_test (column_a)
COMMENT '唯一索引';
# 组合唯一索引
CREATE UNIQUE INDEX idx_test
ON index_test (column_a, column_b)
COMMENT '组合唯一索引';
# 全文索引
CREATE FULLTEXT INDEX idx_test
ON index_test (column_a)
COMMENT '全文索引';
```
#### 索引的删、改、查
##### 1查看和修改索引
当想查看表的所有索引时可以使用如下SQL语句
```sql
SHOW INDEX FROM [table_name];
```
说明:
- **`SHOW INDEX`** 或 **`SHOW KEYS`** 都可以用来显示表中的索引信息,它们是等效的。
- **`FROM [table_name]`**:指定要查看索引的表名。
```sql
SHOW INDEX FROM five_million_user;
SHOW KEYS FROM five_million_user;
```
如果想修改索引,一般需要先删除原索引,再根据需要创建一个同名的索引,从而变相地实现修改索引操作。例如:
```sql
-- 1先删除索引 idx_name
ALTER TABLE index_test DROP INDEX idx_name;
-- 2再创建新索引
CREATE INDEX idx_name_new ON index_test (column_a) COMMENT '普通索引(单列)';
```
##### 2删除索引
当不再需要索引时可以使用drop index语句或alter table语句来删除索引。
使用drop index语句语法如下
```sql
DROP INDEX <索引名> ON <表名>;
# 删除索引(可运用在普通索引、前缀索引中)
DROP INDEX idx_name ON index_test;
```
使用alter table语句语法如下
```sql
# 删除普通索引
ALTER TABLE <表名> DROP INDEX <索引名>;
# 删除主键索引
ALTER TABLE <表名> DROP PRIMARY KEY;
# 示例:删除普通索引(可运用在普通索引、前缀索引中)
ALTER TABLE index_test DROP INDEX idx_name;
# 示例:删除主键索引
ALTER TABLE index_test DROP PRIMARY KEY;
```
因为一张表只可能有一个primary key主键索引因此不需要指定索引名。
### 索引类型
从上节内容可知,索引的本质目的是帮我们快速定位想要查找的数据。索引也有很多种类:从功能逻辑来划分,索引可分为四种,分别是`主键索引``唯一索引``普通索引``全文索引`
按照物理实现方式,索引可以分为两种,分别是聚集索引和非聚集索引。非聚集索引也称为二级索引或者辅助索引。主键索引属于聚集索引,其他索引属于非聚集索引。一张表只能有一个唯一索引,同理,一张表也只能有一个聚集索引。除全文索引外(使用场景少)。
主键索引、唯一索引、普通索引和全文索引的区别总结:
| **索引类型** | **用途** | **字段要求** | **是否允许重复值** | **是否允许 NULL** | **自动创建索引** | **支持的查询类型** | **支持的字段类型** |
| ---------------------- | ---------------------------------------------------- | --------------------------------------- | ------------------ | --------------------------------- | ---------------- | ---------------------------------------- | ------------------------------------------------ |
| 主键索引 (PRIMARY KEY) | 用于唯一标识表中的每一行数据,确保每一行的数据唯一。 | 必须是 **NOT NULL** | 不允许重复 | 不允许 `NULL` | 是 | 精确查询、排序 | 一般用于主键字段,支持大部分字段类型 |
| 唯一索引 (UNIQUE) | 确保字段中的值唯一,但允许 `NULL` 值存在。 | 可以是 `NULL`(但是 `NULL` 值可以重复) | 不允许重复 | 允许 `NULL`(多个 `NULL` 值允许) | 否 | 精确查询、排序 | 大多数字段类型,如 `INT``VARCHAR` 等 |
| 普通索引 (INDEX) | 提高查询速度,适用于常见的等值查询、范围查询等。 | 可以是 `NULL` | 允许重复 | 允许 `NULL` | 否 | 精确查询、范围查询、排序 | 支持所有字段类型,如 `INT``VARCHAR``DATE` 等 |
| 全文索引 (FULLTEXT) | 适用于全文搜索,处理文本字段的复杂查询。 | 必须是 `TEXT``VARCHAR` | 允许重复 | 允许 `NULL` | 否 | 模糊查询、关键词搜索、排序(基于相关性) | 仅支持文本类型字段,如 `TEXT``VARCHAR` |
**说明:**
- **主键索引**:自动保证唯一性并且不允许 `NULL`,每个表只能有一个主键索引。
- **唯一索引**:确保字段值唯一,但可以包含多个 `NULL` 值,适用于要求唯一的字段。
- **普通索引**:是最常见的索引类型,可以加速查询操作,但不强制唯一性,并且支持重复值。
- **全文索引**:专门用于文本搜索,能够高效地处理关键词匹配,适用于大规模的文本查询。
**创建主键**:它是一个约束,确保字段的唯一性和非空性,并且会自动创建一个主键索引。
**创建主键索引**:它是创建索引的行为,通常是为了加速查询和维护唯一性,在某些情况下可能不涉及字段约束(如在非主键字段上创建唯一索引)。
#### 主键及主键索引
维基百科对主键的定义为“表中经常有一列或多列的组合,其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键”。
还记得创建500万行记录的建表语句吗
```sql
-- 用户表结构
CREATE TABLE `five_million_user`(
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(50) DEFAULT '' COMMENT '用户名称',
`email` VARCHAR(50) NOT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT '' COMMENT '手机号',
`sex` TINYINT DEFAULT '0' COMMENT '性别0-男 1-女)',
`password` VARCHAR(100) NOT NULL COMMENT '密码',
`age` TINYINT DEFAULT '0' COMMENT '年龄',
`create_time` DATETIME DEFAULT NOW(),
`update_time` DATETIME DEFAULT NOW(),
PRIMARY KEY (`id`)
)ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT='500万用户表';
```
five_million_user表的主键字段是idMySQL自动生成基于id字段的主键索引。
又比如创建多字段的主键如下:
```sql
CREATE TABLE primary_key_table (
column_a INT NOT NULL,
column_b INT NOT NULL,
# 多字段主键
PRIMARY KEY (column_a, column_b)
);
```
无论是单字段主键还是多字段的主键主键值必须唯一且不允许为空当插入的值为空时MySQL报“\[23000]\[1048] Column xxx cannot be null”错误。
提示工作中选取主键的一个基本原则不使用任何与业务相关的字段作为主键比如身份证号、手机号、邮箱地址等字段均不可作为主键。单字段的主键一般都用id命名常见的可作为id字段的类型有
- 自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数,这样我们就完全不用担心主键重复,也不用自己预先生成主键。
- 全局唯一GUID类型使用一种全局唯一的字符串作为主键例如0f0ba2c3-bebe-4e97-bc12- 7226b9902e56。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的大部分编程语言都内置了GUID算法可以自己预算出主键。
如果主键使用int自增类型那么当表的记录数超过2147483647约21亿条时会因达到上限而出错。如果主键使用bigint自增类型那么表的记录数最多约922亿条。
#### 唯一索引
在设计数据表的时候唯一的列例如身份证号、邮箱地址、手机号码等因为具有业务含义所以不宜作为主键但是这些列根据业务要求又具有唯一性约束即不能出现两条记录存储了同一个号码。这个时候就可以给该列添加唯一索引。添加唯一索引的SQL如下
```sql
# 给表 five_million_user 添加唯一索引,索引名称为 idx_email索引列为 email
ALTER TABLE five_million_user ADD UNIQUE INDEX idx_email (email);
```
唯一索引列中的值必须是唯一的,但是允许为空值。主键索引是一种特殊的唯一索引,不允许有空值。
除了给单字段添加唯一索引外,也可以创建多字段组合唯一索引:
```sql
# 表 five_million_user 创建组合唯一索引,索引名称为 idx_email_phone索引列为 email 和 phone
ALTER TABLE five_million_user ADD UNIQUE INDEX idx_email_phone (email, phone);
```
#### 普通的单字段索引
单字段索引就是给表中的某个字段建立索引比如要通过用户名name搜索用户就可以把用户名name作为索引字段。选择某个字段作为索引时要选择那些经常被用作筛选条件的字段。
```sql
# 给表 five_million_user 添加普通的单字段索引,索引名称为 idx_name索引列为 name
ALTER TABLE five_million_user ADD INDEX idx_name (name);
```
#### 普通的组合索引
MySQL可以创建组合索引复合索引所谓组合索引就是索引由多个字段组合而成。在工作中往往会遇到比较复杂的多字段查询而且查询频率很高这时可以考虑使用组合索引。
```sql
# 通过 name 和 phone 创建组合索引
CREATE INDEX idx_name_phone ON five_million_user (name, phone);
# 通过 name 和 phone 查询用户数据
SELECT * FROM five_million_user
WHERE name = '用户姓名2999510' AND phone = '181414675715'\G;
```
有了组合索引,再次查询数据时执行效率明显提升。提示创建组合索引时,需要注意索引的顺序问题因为组合索引(x, y, z)和(z, y, x)在使用的时候效率可能会存在差别。这里需要说明的是组合索引遵守最左匹配原则,也就是按照最左优先的方式进行索引的匹配,比如索引顺序(x, y, z)
(1) 如果查询条件是 `WHERE x=1`,就可以匹配上组合索引。
(2) 如果查询条件是 `WHERE x=1 AND y=2`,就可以匹配上组合索引。
(3) 如果查询条件是 `WHERE x=1 AND y=2 AND z=3`,就可以匹配上组合索引。
(4) 如果查询条件是 `WHERE x=1 AND z=2 AND y=3`,就可以匹配上组合索引。
(5) 如果查询条件是 `WHERE z=1 AND x=2 AND z=3`,就可以匹配上组合索引。
(6) 如果查询条件是 `WHERE z>1 AND x=2 AND y=3`,就可以匹配上组合索引。
(7) 如果查询条件是 `WHERE y=2`,则无法匹配上组合索引。
(8) 如果查询条件是 `WHERE y=2 AND z=3`,则无法匹配上组合索引。
(9) 如果查询条件是 `WHERE y>2 AND x=1 AND z=3`,就可以使用索引 `(x,y,z)` 的 x 列和 y 列。
y是范围列索引列最多作用于一个范围列范围列之后的z列无法使用索引。
456条之所以可以匹配上组合索引是因为MySQL在逻辑查询优化阶段会自动进行查询重写。对于等值查询MySQL优化器会调整顺序对于范围条件查询比如<<=、>、>=、between等范围列后面的列无法使用索引。45中的查询条件等价于3中的查询条件6中的查询条件等价于where x=2 and y=3 and z>1索引列最多作用于一个范围列。
#### 前缀索引
前缀索引属于普通索引,所谓前缀索引,就是对字符串的前几个字符建立索引,这样建立起来的索引更小,可以节省空间又不用额外增加太多的查询成本。
为什么不对整个字段建立索引呢一般来说使用前缀索引可能都是因为整个字段的数据量太大没有必要针对整个字段建立索引前缀索引仅仅是选择一个字段的部分字符作为索引一方面可以节约索引空间另一方面则可以提高索引效率。以邮箱为例假如某个系统的用户表的用户邮箱字段的统一格式为xxx@qq.com或者xxx@163.com这里的xxx长度不相等那么可以只为xxx创建索引。
选择多长的xxx作为索引长度呢需要关注区分度区分度越高越能体现索引的价值和优势区分度取值范围为[0,1]。如果区分度为1就是唯一索引搜索效率最高但也最浪费磁盘空间这不符合我们创建前缀索引的初衷。之所以要创建前缀索引而不是唯一索引就是希望在索引的查询性能和所占的存储空间之间找到一个平衡点通过选择足够长的前缀来保证较高的区分度同时索引又不占用太多存储空间。
如何选择一个合适的索引区分度呢?索引前缀应该足够长,以便前缀索引的区分度接近于索引的整个列,即前缀的基数应该接近于完整列的基数。
通过以下SQL得到email的全列区分度
<img src="https://yp.smallkun.cn/markdown/image-20250122152213424.png!compress" alt="image-20250122152213424" style="zoom:50%;" />
通过count(*)计算总记录数通过count(distinct email)计算不重复的email记录数count(distinct email) / count(*)就可以得出email的区分度。email最大的区分度为0.5903说明存在重复的数据这些重复的数据可以忽略与初始化数据的SQL有关这里只是为了演示方便
<img src="https://yp.smallkun.cn/markdown/image-20250122152303202.png!compress" alt="image-20250122152303202" style="zoom:50%;" />
从上述SQL可知当前缀长度选择12时结果最接近于全列区分度。因此可以选择前缀长度12来创建email的前缀索引
```sql
# 使用 col_name(length) 语法指定索引前缀长度
CREATE INDEX idx_email ON five_million_user (email(12));
```
注意对于bolb、text或者很长的varchar类型的列必须使用前缀索引。
---
## 事务
本章主要介绍事务的4大特性ACID、如何使用事务以及事务的四种隔离级别即读未提交、读已提交、可重复读和串行化。
### 事务的4大特性
事务Transaction是指提供一种机制将一个活动涉及的所有操作纳入一个不可分割的执行单元组成事务的所有操作。只有在所有操作均能正常执行的情况下才能提交事务其中任何一个操作执行失败都将导致整个事务的回滚。
事务和存储引擎相关MySQL中InnoDB存储引擎是支持事务的而MyISAM存储引擎不支持事务。
数据库中事务有4大特性ACID
`原子性Atomicity`:事务作为一个整体被执行,不可分割,包含在其中的对数据库的操作要么全部被执行,要么都不执行。原子性是基础,是事务的基本单位。
`一致性Consistency`:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束,也就是说当事务提交后,或者当事务发生回滚后,数据库的完整性约束不能被破坏。
`隔离性Isolation`:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
`持久性Durability`:已被提交的事务对数据库的修改应该永久保存在数据库中,不会因为系统故障而失效。
事务的4大特性比较抽象我们以用船运送货物为例来进行说明如图所示。
<img src="https://yp.smallkun.cn/markdown/image-20250122154240813.png!compress" alt="image-20250122154240813" style="zoom: 67%;" />
原子性比较好理解每个集装箱包含很多物品类似一个事务包含很多操作每个集装箱打包好后就是一个整体不可分割。货船把集装箱从A码头运送到B码头单个集装箱要么被运送到B码头要么出现事故沉到海底要么被原路运回去不会出现集装箱里面的物品一半被运到目的地一半被退回原地的情况。
每条船都负责运送自己的集装箱,走自己的航线,相互不影响,这就是所谓的隔离性。
集装箱在A码头装货到船上这类似于事务提交集装箱在B码头卸货这类似于事务执行完成。当集装箱被卸货后物品就放在那里了不会因为码头的哪条路坏了或是哪个路灯坏了物品就丢了这就是所谓的持久性。
假如B码头规定从哪条船运来的集装箱只能在约定好的固定区域卸货并在固定区域存放甚至规定某区域只能放多少个集装箱集装箱只能叠多高某区域的集装箱被运走后新的集装箱才能被运进来。这些约定大家要遵守且不能乱并且一直有效否则整个码头就会乱成一团这可以简单理解为一致性。又比如说在数据表中我们将手机号字段设置为唯一性约束当事务进行提交或者事务发生回滚的时候如果数据表中的手机号非唯一就破坏了事务的一致性。
事务的ACID是通过InnoDB日志和锁来保证的。事务的隔离性是通过数据库锁的机制实现的。原子性和一致性是通过Undo Log来实现的。Undo Log的原理很简单为了满足事务的原子性在操作任何数据之前首先将数据备份到一个地方这个存储数据备份的地方称为Undo Log然后进行数据的修改如果出现了错误或者用户执行了Rollback语句系统就可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。
持久性是通过Redo Log重做日志来实现的。和Undo Log相反Redo Log记录的是新数据的备份。在事务提交前只需将Redo Log持久化即可不需要将数据持久化。当系统崩溃时虽然数据没有持久化但是Redo Log已经持久化系统可以根据Redo Log的内容将所有数据恢复到最新的状态。
### 使用事务
事务的语法结构如下:
```sql
START TRANSACTION;
-- 或者
BEGIN;
#一组DML语句
COMMIT #提交事务
rollback #事务回滚
```
说明:
start transaction和begin表示开始事务后面的DML操作都是当前事务的一部分。
commit表示提交事务意思是执行当前事务的全部操作让数据更改永久有效。
rollback表示回滚当前事务的操作取消对数据的更改。
设置事务提交模式的语法如下:
```sql
-- 0-关闭自动提交 1-自动提交
SET autocommit = {0 | 1}
```
默认情况下MySQL使用自动提交模式。这意味着当不在事务内部时每个语句都是原子的就像它被start transaction和commit包围一样不能使用rollback来撤销但是如果在语句执行期间发生错误则回滚该语句。如果SET autocommit=0则会关闭当前线程的事务自动提交功能事务将持续存在直到主动执行commit或rollback语句或者断开连接。
事务提交示例如下:
```sql
#创建测试表
CREATE TABLE tansaction_test(
`name` VARCHAR(255),
PRIMARY KEY(`name`)
);
#开启事务
#START TRANSACTION;
BEGIN ;
INSERT INTO tansaction_test(`name`)
VALUES('张三');
COMMIT ;#提交事务
#清空表
TRUNCATE tansaction_test;
#设置事务为手动提交
SET autocommit = 0;
BEGIN;#开始事务
INSERT INTO tansaction_test(`name`)
VALUES('李四');
#报错
INSERT INTO tansaction_test(`name`)
VALUES('李四');
ROLLBACK;#回滚
#查询所有姓名
SELECT *
FROM tansaction_test;
```
上述示例最终查询结果只有“张三”。示例中提交了两个事务,第一个事务提交成功,“张三”被保存到数据库;第二个事务由于重复将“李四”保存到数据库,导致数据库报重复数据异常,事务进行回滚。因此,数据库只有“张三”数据。
关闭MySQL的事务自动提交模式
```sql
#关闭自动提交
SET AUTOCOMMIT = 0;
#查询是否关闭自动提交
SHOW VARIABLES LIKE 'autocommit';
```
上述示例都是演示如何通过客户端开启一个事务工作中还有一种常用的方式就是在应用程序中使用事务注解或者调用相关的事务方法来操作事务。例如在Spring框架中可以使用@Transactional注解来控制事务也可以通过在应用程序中提交包含事务的SQL语句到数据库中来控制事务具体如下图所示。
不过无论使用哪种方式来控制事务本质都是转化为数据库事务SQL并提交到数据库中执行。
<img src="https://yp.smallkun.cn/markdown/image-20250122161355804.png!compress" alt="image-20250122161355804" style="zoom:50%;" />
### 事务的4种隔离级别
隔离性是事务的基本特性之一它可以防止数据库在并发处理时出现数据不一致的情况。MySQL的隔离级别又可以分为4种级别分别是读未提交read uncommitted、读已提交read committed、可重复读repeatable read以及串行化serializable
读未提交:表示可以读取事务中还未提交的被更改的数据。这种情况下可能会产生脏读、不可重复读、幻读等情况。
读已提交:只能读取事务中已经提交的被更改的数据。可以避免脏读的产生。
可重复读保证一个事务在相同查询条件下两次查询得到的数据结果是一致的可以避免不可重复读和脏读但无法避免幻读。这也是MySQL默认的隔离级别。
串行化:将事务进行串行化,也就是在一个队列中按照顺序执行。串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。
不同的事务隔离级别导致的问题,如下图所示。
MySQL提供了set transaction语句该语句可以改变单个会话或全局的事务隔离级别语法格式如下
```sql
#修改事务隔离级别的语法
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL {
REPEATABLE READ |
READ COMMITTED |
READ UNCOMMITTED |
SERIALIZABLE
};
```
<img src="https://yp.smallkun.cn/markdown/image-20250122161536325.png!compress" alt="image-20250122161536325" style="zoom:67%;" />
#### 查看当前数据库隔离级别
```sql
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
#全局级别,当前会话隔离级别
```
<img src="https://yp.smallkun.cn/markdown/image-20250122171955622.png!compress" alt="image-20250122171955622" style="zoom:67%;" />
```sql
# 修改事务的隔离级别为读未提交read uncommitted
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
```
- session表示修改的事务隔离级别将应用于当前session当前cmd窗口内的所有事务。
- global表示修改的事务隔离级别将应用于所有session全局中的所有事务且当前已经存在的session不受影响。
`如果省略session和global则表示修改的事务隔离级别将应用于当前session内的下一个还未开始的事务。`
概念还是很抽象下面我们通过修改MySQL的隔离级别来学习不同的隔离级别下数据是如何变化的。首先准备测试表和数据具体如下
```sql
# 创建数据库tran_test,只有一个字段且name字段为主键
DROP TABLE tran_test;
CREATE TABLE tran_test(
name VARCHAR(255),
PRIMARY KEY(name)
)engine=innodb;
INSERT INTO tran_test(`name`)
VALUES('张三');
#查询当前数据库隔离级别
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
```
#### 隔离级别——读未提交
开启两个事务,事务A和事务B,分别执行如图所示的SQL语句。
<img src="https://yp.smallkun.cn/markdown/image-20250122173319126.png!compress" alt="image-20250122173319126" style="zoom:67%;" />
上述示例中设置隔离级别为读未提交当事务A还未提交时事务B在8:04可以查询到事务A对数据的修改正好验证了读未提交这种隔离级别下可以读取事务中还未提交的被更改的数据这种现象我们称为“`脏读`”。
将隔离级别设置为全局的读未提交:
```sql
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
```
`注意:每个事务单独使用一个数据库连接`
**事务1**
```sql
START TRANSACTION ;
SELECT `name`
FROM tran_test;
UPDATE tran_test
SET `name` = '李四'
WHERE `name` = '张三';
COMMIT;
UPDATE tran_test
SET `name` = '张三'
WHERE `name` = '李四';
```
**事务2**
```sql
START TRANSACTION ;
SELECT `name`
FROM tran_test;
COMMIT;
```
#### 隔离级别——读已提交
<img src="https://yp.smallkun.cn/markdown/image-20250122174330272.png!compress" alt="image-20250122174330272" style="zoom:67%;" />
上述示例中设置隔离级别为读已提交事务B只能读取事务A中已经提交的被更改的数据比如8:04只能查询到“张三”的记录8:06才能查询到“李四”的记录读已提交可以避免脏读的产生但是会出现不可重复读的问题。我们把目光聚焦在事务B事务B在整个事务中8:01、8:04与8:06的查询结果居然不一样这种现象就是`不可重复读`(可重复读:保证一个事务在相同查询条件下两次查询得到的数据结果是一致的)。
```sql
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
```
`注意:每个事务单独使用一个数据库连接`
**事务1**
```sql
START TRANSACTION ;
SELECT `name`
FROM tran_test;
UPDATE tran_test
SET `name` = '李四'
WHERE `name` = '张三';
COMMIT;
UPDATE tran_test
SET `name` = '张三'
WHERE `name` = '李四';
```
**事务2**
```sql
START TRANSACTION ;
SELECT `name`
FROM tran_test
COMMIT;
```
#### 隔离级别——可重复读
如果想解决不可重复读的问题,需要将隔离级别设置为可重复读:
<img src="https://yp.smallkun.cn/markdown/image-20250122181616333.png!compress" alt="image-20250122181616333" style="zoom: 67%;" />
上述示例中,设置隔离级别为`可重复读`因此事务B中的所有查询结果都是“张三”只有等事务B提交后才能查询到事务A对数据的修改满足可重复读的概念。
```sql
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
```
**事务1**
```sql
START TRANSACTION ;
SELECT `name`
FROM tran_test;
UPDATE tran_test
SET `name` = '李四'
WHERE `name` = '张三';
COMMIT;
```
**事务2**
```sql
UPDATE tran_test
SET `name` = '张三'
WHERE `name` = '李四';
```
可重复读可以避免不可重复读和脏读等问题,但无法避免幻读。
所谓幻读就是事务A根据条件查询得到了N条数据但此时事务B增加了M条符合事务A查询条件的数据这样当事务A再次进行查询的时候发现会有N + M条数据于是产生了幻读。在事务的过程中读取符合某个查询条件的数据时第一次读没有读到某个记录而第二次读竟然读到了这个记录像发生了幻觉一样这也是它被称为幻读的原因。幻读仅专指新插入的行重点在于insert不可重复读重点在于update。
<img src="https://yp.smallkun.cn/markdown/image-20250122183039933.png!compress" alt="image-20250122183039933" style="zoom:67%;" />
上述示例中事务B在8:01和8:05执行相同的查询条件得到的结果不一致。在事务A插入数据并提交事务后事务B在第二次执行当前读加了for update的时候读到了事务A最新插入的数据。在快照读的情况下可重复读隔离级别解决了幻读的问题在当前读的情况下可重复读隔离级别没有解决幻读的问题。
**事务1**
```sql
DROP TABLE tran_test;
CREATE TABLE tran_test(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(255)
)engine=innodb;
INSERT INTO tran_test(`name`)
VALUES('张三');
# 事务1
START TRANSACTION ;
INSERT INTO tran_test(`name`)
VALUES('张三');
COMMIT;
TRUNCATE tran_test;
SELECT *
FROM tran_test
WHERE `name`='张三'
```
**事务2**
```sql
START TRANSACTION ;
SELECT *
FROM tran_test
WHERE `name`='张三'
SELECT *
FROM tran_test
WHERE `name`='张三' FOR UPDATE;
COMMIT;
```
快照读和当前读的区别如下:
快照读读取数据的历史版本不对数据加锁例如select查询语句。
```sql
#快照读
SELECT *
FROM animal
WHERE id < 7;
```
当前读读取数据的最新版本并对数据进行加锁例如insert、update、delete、select for update、select lock in share mode。
```sql
#当前读
SELECT *
FROM student
WHERE id < 10
FOR UPDATE;
```
#### 隔离级别——串行化
想解决幻读,可以设置隔离级别为串行化:
```sql
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;
```
重新执行幻读中的SQL语句会发现事务A在8:03时无法插入数据。如果事务A和事务B都只是简单的查询操作则是可以操作的可以自己实践操作一下。串行化隔离级别可以解决脏读、不可重复读和幻读但是性能最差。在实际工作中需要根据具体的业务在性能和数据正确性之间进行取舍选择设置合理的隔离级别。
---
## 用户权限管理
MySQL是一个强大的数据库管理系统它支持多用户访问和权限管理。在实际应用中为了保证数据库的安全性和完整性需要对不同用户设置不同的权限。
### 1. 用户创建
要创建一个新用户,可以使用以下语句:
```sql
CREATE USER 'username'@'host' IDENTIFIED BY 'password';
```
- `username`:用户名
- `host`:允许连接的主机(使用 `%` 表示任何主机)
- `password`:用户的密码
### 2. 授予权限
使用 `GRANT` 语句授予用户权限:
```sql
GRANT privilege ON database.table TO 'username'@'host';
```
- `privilege`:可以是 `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `ALL PRIVILEGES` 等。
- `database.table`:指定数据库和表,使用 `*.*` 表示所有数据库和表。
例如,授予用户对 `mydb` 数据库的所有权限:
```sql
GRANT ALL PRIVILEGES ON mydb.* TO 'username'@'host';
```
### 3. 撤销权限
使用 `REVOKE` 语句撤销权限:
```sql
REVOKE privilege ON database.table FROM 'username'@'host';
```
例如,撤销用户对 `mydb` 数据库的 `SELECT` 权限:
```sql
REVOKE SELECT ON mydb.* FROM 'username'@'host';
```
### 4. 查看用户权限
要查看用户的权限,可以使用以下命令:
```sql
SHOW GRANTS FOR 'username'@'host';
```
### 5. 删除用户
使用 `DROP USER` 语句删除用户:
```sql
DROP USER 'username'@'host';
```
### 6. 刷新权限
在修改权限后,通常需要刷新权限:
```sql
FLUSH PRIVILEGES;
```
## 游标
在 MySQL 5 中,游标是用于在存储过程或函数中逐行处理查询结果的工具。
### 1. 声明游标
在存储过程或函数中,首先需要声明游标。声明游标时,需要指定一个 SQL 查询。
```sql
DECLARE cursor_name CURSOR FOR
SELECT column1, column2 FROM table_name WHERE condition;
```
### 2. 声明处理器
在使用游标之前,通常需要声明一个处理器,以处理可能的异常情况:
```sql
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
```
### 3. 打开游标
使用 `OPEN` 语句打开游标,以便可以开始提取数据:
```sql
OPEN cursor_name;
```
### 4. 读取数据
使用 `FETCH` 语句逐行读取游标中的数据,将结果存入变量中:
```sql
FETCH cursor_name INTO variable1, variable2;
```
### 5. 关闭游标
在完成对游标的操作后,使用 `CLOSE` 语句关闭游标:
```sql
CLOSE cursor_name;
```
### 6. 完整示例
以下是一个完整的示例,演示了如何在存储过程中使用游标:
```sql
DELIMITER $$
CREATE PROCEDURE example_procedure()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE var1 INT;
DECLARE var2 VARCHAR(100);
-- 声明游标
DECLARE example_cursor CURSOR FOR
SELECT id, name FROM students WHERE age > 18;
-- 声明处理器
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 打开游标
OPEN example_cursor;
-- 循环读取数据
read_loop: LOOP
FETCH example_cursor INTO var1, var2;
IF done THEN
LEAVE read_loop;
END IF;
-- 这里可以处理 var1 和 var2例如打印或其他操作
SELECT var1, var2;
END LOOP;
-- 关闭游标
CLOSE example_cursor;
END $$
DELIMITER ;
```
游标在 MySQL 中主要用于在存储过程或函数中逐行处理结果集。使用游标时要注意资源管理,确保在使用完成后关闭游标。