索引

本章主要介绍索引的相关内容,包括什么是索引,索引的增、删、改、查,索引类型。其中索引类型包括主键索引、唯一索引、普通索引以及前缀索引。

执行以下SQL语句,该语句可以生成500万条用户数据。

-- 用户表结构
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”的用户:

SELECT *
FROM five_million_user
WHERE name = '用户姓名3999999';
image-20250122125541634

发现查询需要3秒钟,速度非常慢,在工作中这样的查询效率是无法接受的,用户都希望能快速看到数据。如何解决这个问题呢?可以给表创建索引,例如:

#给five_million_user表的name字段创建索引,索引名称为idx_name
CREATE INDEX inx_name ON five_million_user(`name`);
image-20250122125907017

创建索引后,执行效率明显提升,性能提升50倍以上。当然,不同的服务器性能不一样,执行时间会有所差异。

注意在一张数据表中只能有一个主键索引,当表中的数据比较少时(例如少于500行),是不需要创建索引的。另外,当数据重复度大(比如高于30%)的时候,也不需要对这个字段使用索引,例如性别字段,就不需要对它创建索引。

索引增、删、改、查

创建索引

1)创建表的同时创建索引
-- 单字段索引创建语法
CREATE TABLE 表名 (
    字段名 字段数据类型,
    ...,
    [INDEX | KEY | UNIQUE] 索引名 (字段名)
);

-- 多字段索引创建语法
CREATE TABLE 表名 (
    字段名1 字段数据类型,
    字段名2 字段数据类型,
    ...,
    [INDEX | KEY | UNIQUE] 索引名 (字段名1, 字段名2, ...)
);

创建单字段普通索引示例如下:

CREATE TABLE index_test (
    column_a VARCHAR(100) NULL,
    column_b VARCHAR(100) NULL,
    -- 创建 column_a 单字段普通索引,索引名称为 idx_a
    INDEX idx_a (column_a)
);

创建多字段普通索引示例如下:

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)
);

创建单字段唯一索引示例如下:

CREATE TABLE index_test (
    column_a VARCHAR(100) NULL,
    column_b VARCHAR(100) NULL,
    -- 创建 column_a 单字段唯一索引,索引名称为 idx_a
    UNIQUE idx_a (column_a)
);

创建多字段唯一索引示例如下:

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,例如:

CREATE TABLE index_test (
    column_a VARCHAR(100) NULL,
    column_b VARCHAR(100) NULL,
    -- 创建 column_a 单字段全文索引,索引名称为 idx_a
    FULLTEXT idx_a (column_a)
);

创建主键索引

CREATE TABLE index_test (
    column_a VARCHAR(100) NOT NULL,
    column_b VARCHAR(100) NULL,
    -- 设置 column_a 字段为主键,MySQL 会自动创建主键索引和唯一索引
    PRIMARY KEY (column_a)
);
2)通过修改表来创建索引
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语句直接给已存在的表创建索引
# 语法:
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语句:

SHOW INDEX FROM [table_name];

说明:

  • SHOW INDEXSHOW KEYS 都可以用来显示表中的索引信息,它们是等效的。
  • FROM [table_name]:指定要查看索引的表名。
SHOW INDEX FROM five_million_user;
SHOW KEYS FROM five_million_user;

如果想修改索引,一般需要先删除原索引,再根据需要创建一个同名的索引,从而变相地实现修改索引操作。例如:

-- 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语句,语法如下:

DROP INDEX <索引名> ON <表名>;
# 删除索引(可运用在普通索引、前缀索引中)
DROP INDEX idx_name ON index_test;

使用alter table语句,语法如下:

# 删除普通索引
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 值允许) 精确查询、排序 大多数字段类型,如 INTVARCHAR
普通索引 (INDEX) 提高查询速度,适用于常见的等值查询、范围查询等。 可以是 NULL 允许重复 允许 NULL 精确查询、范围查询、排序 支持所有字段类型,如 INTVARCHARDATE
全文索引 (FULLTEXT) 适用于全文搜索,处理文本字段的复杂查询。 必须是 TEXTVARCHAR 允许重复 允许 NULL 模糊查询、关键词搜索、排序(基于相关性) 仅支持文本类型字段,如 TEXTVARCHAR

说明:

  • 主键索引:自动保证唯一性并且不允许 NULL,每个表只能有一个主键索引。
  • 唯一索引:确保字段值唯一,但可以包含多个 NULL 值,适用于要求唯一的字段。
  • 普通索引:是最常见的索引类型,可以加速查询操作,但不强制唯一性,并且支持重复值。
  • 全文索引:专门用于文本搜索,能够高效地处理关键词匹配,适用于大规模的文本查询。

创建主键:它是一个约束,确保字段的唯一性和非空性,并且会自动创建一个主键索引。

创建主键索引:它是创建索引的行为,通常是为了加速查询和维护唯一性,在某些情况下可能不涉及字段约束(如在非主键字段上创建唯一索引)。

主键及主键索引

维基百科对主键的定义为“表中经常有一列或多列的组合,其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键”。

还记得创建500万行记录的建表语句吗?

-- 用户表结构
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表的主键字段是id,MySQL自动生成基于id字段的主键索引。

又比如创建多字段的主键如下:

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如下:

# 给表 five_million_user 添加唯一索引,索引名称为 idx_email,索引列为 email
ALTER TABLE five_million_user ADD UNIQUE INDEX idx_email (email);

唯一索引列中的值必须是唯一的,但是允许为空值。主键索引是一种特殊的唯一索引,不允许有空值。

除了给单字段添加唯一索引外,也可以创建多字段组合唯一索引:

# 表 five_million_user 创建组合唯一索引,索引名称为 idx_email_phone,索引列为 email 和 phone
ALTER TABLE five_million_user ADD UNIQUE INDEX idx_email_phone (email, phone);

普通的单字段索引

单字段索引就是给表中的某个字段建立索引,比如要通过用户名name搜索用户,就可以把用户名name作为索引字段。选择某个字段作为索引时,要选择那些经常被用作筛选条件的字段。

# 给表 five_million_user 添加普通的单字段索引,索引名称为 idx_name,索引列为 name
ALTER TABLE five_million_user ADD INDEX idx_name (name);

普通的组合索引

MySQL可以创建组合索引(复合索引),所谓组合索引就是索引由多个字段组合而成。在工作中往往会遇到比较复杂的多字段查询,而且查询频率很高,这时可以考虑使用组合索引。

# 通过 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列无法使用索引。

第(4)、(5)、(6)条之所以可以匹配上组合索引,是因为MySQL在逻辑查询优化阶段会自动进行查询重写。对于等值查询,MySQL优化器会调整顺序;对于范围条件查询,比如<、<=、>、>=、between等,范围列后面的列无法使用索引。(4)和(5)中的查询条件等价于(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的全列区分度:

image-20250122152213424

通过count()计算总记录数,通过count(distinct email)计算不重复的email记录数,count(distinct email) / count()就可以得出email的区分度。email最大的区分度为0.5903,说明存在重复的数据(这些重复的数据可以忽略,与初始化数据的SQL有关,这里只是为了演示方便)。

image-20250122152303202

从上述SQL可知,当前缀长度选择12时,结果最接近于全列区分度。因此,可以选择前缀长度12来创建email的前缀索引:

# 使用 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大特性比较抽象,我们以用船运送货物为例来进行说明,如图所示。

image-20250122154240813

原子性比较好理解,每个集装箱包含很多物品,类似一个事务包含很多操作(增/删/改),每个集装箱打包好后就是一个整体,不可分割。货船把集装箱从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的内容将所有数据恢复到最新的状态。

使用事务

事务的语法结构如下:

START TRANSACTION; 
-- 或者
BEGIN;

#一组DML语句
COMMIT #提交事务
rollback #事务回滚

说明:

start transaction和begin:表示开始事务,后面的DML操作都是当前事务的一部分。

commit:表示提交事务,意思是执行当前事务的全部操作,让数据更改永久有效。

rollback:表示回滚当前事务的操作,取消对数据的更改。

设置事务提交模式的语法如下:

-- 0-关闭自动提交  1-自动提交
SET autocommit = {0 | 1}

默认情况下,MySQL使用自动提交模式。这意味着,当不在事务内部时,每个语句都是原子的,就像它被start transaction和commit包围一样,不能使用rollback来撤销;但是,如果在语句执行期间发生错误,则回滚该语句。如果SET autocommit=0,则会关闭当前线程的事务自动提交功能,事务将持续存在直到主动执行commit或rollback语句,或者断开连接。

事务提交示例如下:

#创建测试表
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的事务自动提交模式

#关闭自动提交
SET AUTOCOMMIT = 0;
#查询是否关闭自动提交
SHOW VARIABLES LIKE 'autocommit';

上述示例都是演示如何通过客户端开启一个事务,工作中,还有一种常用的方式,就是在应用程序中使用事务注解或者调用相关的事务方法来操作事务。例如在Spring框架中,可以使用@Transactional注解来控制事务,也可以通过在应用程序中提交包含事务的SQL语句到数据库中来控制事务,具体如下图所示。

不过,无论使用哪种方式来控制事务,本质都是转化为数据库事务SQL,并提交到数据库中执行。

image-20250122161355804

事务的4种隔离级别

隔离性是事务的基本特性之一,它可以防止数据库在并发处理时出现数据不一致的情况。MySQL的隔离级别又可以分为4种级别,分别是读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)以及串行化(serializable):

读未提交:表示可以读取事务中还未提交的被更改的数据。这种情况下可能会产生脏读、不可重复读、幻读等情况。

读已提交:只能读取事务中已经提交的被更改的数据。可以避免脏读的产生。

可重复读:保证一个事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。这也是MySQL默认的隔离级别。

串行化:将事务进行串行化,也就是在一个队列中按照顺序执行。串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。

不同的事务隔离级别导致的问题,如下图所示。

MySQL提供了set transaction语句,该语句可以改变单个会话或全局的事务隔离级别,语法格式如下:

#修改事务隔离级别的语法
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL { 
    REPEATABLE READ | 
    READ COMMITTED | 
    READ UNCOMMITTED | 
    SERIALIZABLE 
};
image-20250122161536325

查看当前数据库隔离级别

SELECT @@global.transaction_isolation, @@session.transaction_isolation;
#全局级别,当前会话隔离级别
image-20250122171955622
# 修改事务的隔离级别为读未提交(read uncommitted)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
  • session:表示修改的事务隔离级别将应用于当前session(当前cmd窗口)内的所有事务。
  • global:表示修改的事务隔离级别将应用于所有session(全局)中的所有事务,且当前已经存在的session不受影响。

如果省略session和global,则表示修改的事务隔离级别将应用于当前session内的下一个还未开始的事务。

概念还是很抽象,下面我们通过修改MySQL的隔离级别来学习不同的隔离级别下,数据是如何变化的。首先准备测试表和数据,具体如下:

# 创建数据库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语句。

image-20250122173319126

上述示例中,设置隔离级别为读未提交,当事务A还未提交时,事务B在8:04可以查询到事务A对数据的修改,正好验证了读未提交这种隔离级别下,可以读取事务中还未提交的被更改的数据,这种现象我们称为“脏读”。

将隔离级别设置为全局的读未提交:

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;

注意:每个事务单独使用一个数据库连接

事务1

START TRANSACTION ;
SELECT `name`
FROM tran_test;

UPDATE tran_test
SET `name` = '李四'
WHERE `name` = '张三';
COMMIT;

UPDATE tran_test
SET `name` = '张三'
WHERE `name` = '李四';

事务2

START TRANSACTION ;
SELECT `name`
FROM tran_test;
COMMIT;

隔离级别——读已提交

image-20250122174330272

上述示例中,设置隔离级别为读已提交,事务B只能读取事务A中已经提交的被更改的数据,比如8:04只能查询到“张三”的记录,8:06才能查询到“李四”的记录,读已提交可以避免脏读的产生,但是会出现不可重复读的问题。我们把目光聚焦在事务B,事务B在整个事务中,8:01、8:04与8:06的查询结果居然不一样,这种现象就是不可重复读(可重复读:保证一个事务在相同查询条件下两次查询得到的数据结果是一致的)。

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;

注意:每个事务单独使用一个数据库连接

事务1

START TRANSACTION ;
SELECT `name`
FROM tran_test;

UPDATE tran_test
SET `name` = '李四'
WHERE `name` = '张三';

COMMIT;

UPDATE tran_test
SET `name` = '张三'
WHERE `name` = '李四';

事务2

START TRANSACTION ;
SELECT `name`
FROM tran_test

COMMIT;

隔离级别——可重复读

如果想解决不可重复读的问题,需要将隔离级别设置为可重复读:

image-20250122181616333

上述示例中,设置隔离级别为可重复读,因此,事务B中的所有查询结果都是“张三”,只有等事务B提交后,才能查询到事务A对数据的修改,满足可重复读的概念。

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@global.transaction_isolation, @@session.transaction_isolation;

事务1

START TRANSACTION ;
SELECT `name`
FROM tran_test;

UPDATE tran_test
SET `name` = '李四'
WHERE `name` = '张三';

COMMIT;

事务2

UPDATE tran_test
SET `name` = '张三'
WHERE `name` = '李四';

可重复读可以避免不可重复读和脏读等问题,但无法避免幻读。

所谓幻读,就是事务A根据条件查询得到了N条数据,但此时事务B增加了M条符合事务A查询条件的数据,这样当事务A再次进行查询的时候发现会有N + M条数据,于是产生了幻读。在事务的过程中读取符合某个查询条件的数据时,第一次读没有读到某个记录,而第二次读竟然读到了这个记录,像发生了幻觉一样,这也是它被称为幻读的原因。幻读仅专指新插入的行,重点在于insert,不可重复读重点在于update。

image-20250122183039933

上述示例中,事务B在8:01和8:05执行相同的查询条件,得到的结果不一致。在事务A插入数据并提交事务后,事务B在第二次执行当前读(加了for update)的时候,读到了事务A最新插入的数据。在快照读的情况下,可重复读隔离级别解决了幻读的问题;在当前读的情况下,可重复读隔离级别没有解决幻读的问题。

事务1

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

START TRANSACTION ;
SELECT *
FROM tran_test
WHERE `name`='张三'

SELECT *
FROM tran_test
WHERE `name`='张三' FOR UPDATE;

COMMIT;

快照读和当前读的区别如下:

快照读:读取数据的历史版本,不对数据加锁,例如select查询语句。

#快照读
SELECT *
FROM animal
WHERE id < 7;

当前读:读取数据的最新版本,并对数据进行加锁,例如insert、update、delete、select for update、select lock in share mode。

#当前读
SELECT *
FROM student
WHERE id < 10
FOR UPDATE;

隔离级别——串行化

想解决幻读,可以设置隔离级别为串行化:

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. 用户创建

要创建一个新用户,可以使用以下语句:

CREATE USER 'username'@'host' IDENTIFIED BY 'password';
  • username:用户名
  • host:允许连接的主机(使用 % 表示任何主机)
  • password:用户的密码

2. 授予权限

使用 GRANT 语句授予用户权限:

GRANT privilege ON database.table TO 'username'@'host';
  • privilege:可以是 SELECT, INSERT, UPDATE, DELETE, ALL PRIVILEGES 等。
  • database.table:指定数据库和表,使用 *.* 表示所有数据库和表。

例如,授予用户对 mydb 数据库的所有权限:

GRANT ALL PRIVILEGES ON mydb.* TO 'username'@'host';

3. 撤销权限

使用 REVOKE 语句撤销权限:

REVOKE privilege ON database.table FROM 'username'@'host';

例如,撤销用户对 mydb 数据库的 SELECT 权限:

REVOKE SELECT ON mydb.* FROM 'username'@'host';

4. 查看用户权限

要查看用户的权限,可以使用以下命令:

SHOW GRANTS FOR 'username'@'host';

5. 删除用户

使用 DROP USER 语句删除用户:

DROP USER 'username'@'host';

6. 刷新权限

在修改权限后,通常需要刷新权限:

FLUSH PRIVILEGES;

游标

在 MySQL 5 中,游标是用于在存储过程或函数中逐行处理查询结果的工具。

1. 声明游标

在存储过程或函数中,首先需要声明游标。声明游标时,需要指定一个 SQL 查询。

DECLARE cursor_name CURSOR FOR
SELECT column1, column2 FROM table_name WHERE condition;

2. 声明处理器

在使用游标之前,通常需要声明一个处理器,以处理可能的异常情况:

DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

3. 打开游标

使用 OPEN 语句打开游标,以便可以开始提取数据:

OPEN cursor_name;

4. 读取数据

使用 FETCH 语句逐行读取游标中的数据,将结果存入变量中:

FETCH cursor_name INTO variable1, variable2;

5. 关闭游标

在完成对游标的操作后,使用 CLOSE 语句关闭游标:

CLOSE cursor_name;

6. 完整示例

以下是一个完整的示例,演示了如何在存储过程中使用游标:

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 中主要用于在存储过程或函数中逐行处理结果集。使用游标时要注意资源管理,确保在使用完成后关闭游标。