数据库相关
数据模型
数据库按照数据结构来组织、存储和管理数据,实际上,数据库一共有三种模型:
-
层次模型
-
网状模型
-
关系模型
层次模型就是以“上下级”的层次关系来组织数据的一种方式,层次模型的数据结构看起来就像一颗树:
┌─────┐ │ │ └─────┘ │ ┌───────┴───────┐ │ │ ┌─────┐ ┌─────┐ │ │ │ │ └─────┘ └─────┘ │ │ ┌───┴───┐ ┌───┴───┐ │ │ │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ │ │ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘
网状模型把每个数据节点和其他很多节点都连接起来,它的数据结构看起来就像很多城市之间的路网:
┌─────┐ ┌─────┐ ┌─│ │──────│ │──┐ │ └─────┘ └─────┘ │ │ │ │ │ │ └──────┬─────┘ │ │ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │─────│ │─────│ │ └─────┘ └─────┘ └─────┘ │ │ │ │ ┌─────┴─────┐ │ │ │ │ │ │ ┌─────┐ ┌─────┐ │ └──│ │─────│ │──┘ └─────┘ └─────┘
关系模型把数据看作是一个二维表格,任何数据都可以通过行号+列号来唯一确定,它的数据模型看起来就是一个Excel表:
┌─────┬─────┬─────┬─────┬─────┐ │ │ │ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ │ │ ├─────┼─────┼─────┼─────┼─────┤ │ │ │ │ │ │ └─────┴─────┴─────┴─────┴─────┘
数据类型
对于一个关系表,除了定义每一列的名称外,还需要定义每一列的数据类型。关系数据库支持的标准数据类型包括数值、字符串、时间等:
名称 | 类型 | 说明 |
---|---|---|
INT | 整型 | 4字节整数类型,范围约+/-21亿 |
BIGINT | 长整型 | 8字节整数类型,范围约+/-922亿亿 |
REAL | 浮点型 | 4字节浮点数,范围约+/-1038 |
DOUBLE | 浮点型 | 8字节浮点数,范围约+/-10308 |
DECIMAL(M,N) | 高精度小数 | 由用户指定精度的小数,例如,DECIMAL(20,10)表示一共20位,其中小数10位,通常用于财务计算 |
CHAR(N) | 定长字符串 | 存储指定长度的字符串,例如,CHAR(100)总是存储100个字符的字符串 |
VARCHAR(N) | 变长字符串 | 存储可变长度的字符串,例如,VARCHAR(100)可以存储0~100个字符的字符串 |
BOOLEAN | 布尔类型 | 存储True或者False |
DATE | 日期类型 | 存储日期,例如,2018-06-22 |
TIME | 时间类型 | 存储时间,例如,12:20:59 |
DATETIME | 日期和时间类型 | 存储日期+时间,例如,2018-06-22 12:20:59 |
SQL
什么是SQL?SQL是结构化查询语言的缩写,用来访问和操作数据库系统。SQL语句既可以查询数据库中的数据,也可以添加、更新和删除数据库中的数据,还可以对数据库进行管理和维护操作。不同的数据库,都支持SQL,这样,我们通过学习SQL这一种语言,就可以操作各种不同的数据库。
虽然SQL已经被ANSI组织定义为标准,不幸地是,各个不同的数据库对标准的SQL支持不太一致。并且,大部分数据库都在标准的SQL上做了扩展。也就是说,如果只使用标准SQL,理论上所有数据库都可以支持,但如果使用某个特定数据库的扩展SQL,换一个数据库就不能执行了。例如,Oracle把自己扩展的SQL称为PL/SQL,Microsoft把自己扩展的SQL称为T-SQL。
现实情况是,如果我们只使用标准SQL的核心功能,那么所有数据库通常都可以执行。不常用的SQL功能,不同的数据库支持的程度都不一样。而各个数据库支持的各自扩展的功能,通常我们把它们称之为“方言”。
总的来说,SQL语言定义了这么几种操作数据库的能力:
DDL:Data Definition Language
DDL允许用户定义数据,也就是创建表、删除表、修改表结构这些操作。通常,DDL由数据库管理员执行。
DML:Data Manipulation Language
DML为用户提供添加、删除、更新数据的能力,这些是应用程序对数据库的日常操作。
DQL:Data Query Language
DQL允许用户查询数据,这也是通常最频繁的数据库日常操作。
语法特点
SQL语言关键字不区分大小写!!!但是,针对不同的数据库,对于表名和列名,有的数据库区分大小写,有的数据库不区分大小写。同一个数据库,有的在Linux上区分大小写,有的在Windows上不区分大小写。
所以,本教程约定:SQL关键字总是大写,以示突出,表名和列名均使用小写。
关系模型
表的每一行称为记录(Record),记录是一个逻辑意义上的数据。
表的每一列称为字段(Column),同一个表的每一行记录都拥有相同的若干字段。
字段定义了数据类型(整型、浮点型、字符串、日期等),以及是否允许为NULL。注意NULL表示字段数据不存在。一个整型字段如果为NULL不表示它的值为0,同样的,一个字符串型字段为NULL也不表示它的值为空串''。
通常情况下,字段应该避免允许为NULL。不允许为NULL可以简化查询条件,加快查询速度,也利于应用程序读取数据后无需判断是否为NULL。
关系数据库的表和表之间需要建立“一对多”,“多对一”和“一对一”的关系,这样才能够按照应用程序的逻辑来组织和存储数据。
在关系数据库中,关系是通过主键和外键来维护的。
每一条记录都包含若干定义好的字段。同一个表的所有记录都有相同的字段定义。
主键
对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复不是指两条记录不完全相同,而是指能够通过某个字段唯一区分出不同的记录,这个字段被称为主键。
例如,假设我们把name字段作为主键,那么通过名字小明或小红就能唯一确定一条记录。但是,这么设定,就没法存储同名的同学了,因为插入相同主键的两条记录是不被允许的。
对主键的要求,最关键的一点是:记录一旦插入到表中,主键最好不要再修改,因为主键是用来唯一定位记录的,修改了主键,会造成一系列的影响。
由于主键的作用十分重要,如何选取主键会对业务开发产生重要影响。如果我们以学生的身份证号作为主键,似乎能唯一定位记录。然而,身份证号也是一种业务场景,如果身份证号升位了,或者需要变更,作为主键,不得不修改的时候,就会对业务产生严重影响。
所以,选取主键的一个基本原则是:不使用任何业务相关的字段作为主键。
因此,身份证号、手机号、邮箱地址这些看上去可以唯一的字段,均不可用作主键。
作为主键最好是完全业务无关的字段,我们一般把这个字段命名为id。常见的可作为id字段的类型有:
-
自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数,这样我们就完全不用担心主键重复,也不用自己预先生成主键;
-
全局唯一GUID类型:使用一种全局唯一的字符串作为主键,类似8f55d96b-8acc-4636-8cb8-76bf8abc2f57。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的,大部分编程语言都内置了GUID算法,可以自己预算出主键。
对于大部分应用来说,通常自增类型的主键就能满足需求。我们在students表中定义的主键也是BIGINT NOT NULL AUTO_INCREMENT类型。
如果使用INT自增类型,那么当一张表的记录数超过2147483647(约21亿)时,会达到上限而出错。使用BIGINT自增类型则可以最多约922亿亿条记录。
联合主键
关系数据库实际上还允许通过多个字段唯一标识记录,即两个或更多的字段都设置为主键,这种主键被称为联合主键。
对于联合主键,允许一列有重复,只要不是所有主键列都重复即可:
id_num | id_type | other columns... |
---|---|---|
1 | A | ... |
2 | A | ... |
2 | B | ... |
如果我们把上述表的id_num和id_type这两列作为联合主键,那么上面的3条记录都是允许的,因为没有两列主键组合起来是相同的。
没有必要的情况下,我们尽量不使用联合主键,因为它给关系表带来了复杂度的上升。
小结
主键是关系表中记录的唯一标识。主键的选取非常重要:主键不要带有业务含义,而应该使用BIGINT自增或者GUID类型。主键也不应该允许NULL。
可以使用多个列作为联合主键,但联合主键并不常用。
外键
在students表中,通过class_id的字段,可以把数据与另一张表关联起来,这种列称为外键。
外键并不是通过列名实现的,而是通过定义外键约束实现的:
ALTER TABLE studentsADD CONSTRAINT fk_class_idFOREIGN KEY (class_id)REFERENCES classes (id);
其中,外键约束的名称fk_class_id可以任意,FOREIGN KEY (class_id)指定了class_id作为外键,REFERENCES classes (id)指定了这个外键将关联到classes表的id列(即classes表的主键)。
通过定义外键约束,关系数据库可以保证无法插入无效的数据。即如果classes表不存在id=99的记录,students表就无法插入class_id=99的记录。
由于外键约束会降低数据库的性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。这种情况下,class_id仅仅是一个普通的列,只是它起到了外键的作用而已。
要删除一个外键约束,也是通过ALTER TABLE实现的:
ALTER TABLE studentsDROP FOREIGN KEY fk_class_id;
注意:删除外键约束并没有删除外键这一列。删除列是通过DROP COLUMN ...实现的。
多对多
通过一个表的外键关联到另一个表,我们可以定义出一对多关系。有些时候,还需要定义“多对多”关系。例如,一个老师可以对应多个班级,一个班级也可以对应多个老师,因此,班级表和老师表存在多对多关系。
多对多关系实际上是通过两个一对多关系实现的,即通过一个中间表,关联两个一对多关系,就形成了多对多关系:
一对一
一对一关系是指,一个表的记录对应到另一个表的唯一一个记录。
小结
关系数据库通过外键可以实现一对多、多对多和一对一的关系。外键既可以通过数据库来约束,也可以不设置约束,仅依靠应用程序的逻辑来保证。
索引
索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。
id | class_id | name | gender | score |
---|---|---|---|---|
1 | 1 | 小明 | M | 90 |
2 | 1 | 小红 | F | 95 |
3 | 1 | 小军 | M | 88 |
如果要经常根据score列进行查询,就可以对score列创建索引:
ALTER TABLE studentsADD INDEX idx_score (score);
使用ADD INDEX idx_score (score)就创建了一个名称为idx_score,使用列score的索引。索引名称是任意的,索引如果有多列,可以在括号里依次写上,例如:
ALTER TABLE studentsADD INDEX idx_name_score (name, score);
索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,例如gender列,大约一半的记录值是M,另一半是F,因此,对该列创建索引就没有意义。
可以对一张表创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引,因此,索引越多,插入、更新和删除记录的速度就越慢。
对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。
唯一索引
在设计关系数据表的时候,看上去唯一的列,例如身份证号、邮箱地址等,因为他们具有业务含义,因此不宜作为主键。
但是,这些列根据业务要求,又具有唯一性约束:即不能出现两条记录存储了同一个身份证号。这个时候,就可以给该列添加一个唯一索引。例如,我们假设students表的name不能重复:
ALTER TABLE studentsADD UNIQUE INDEX uni_name (name);
通过UNIQUE关键字我们就添加了一个唯一索引。
也可以只对某一列添加一个唯一约束而不创建唯一索引:
ALTER TABLE studentsADD CONSTRAINT uni_name UNIQUE (name);
这种情况下,name列没有索引,但仍然具有唯一性保证。
无论是否创建索引,对于用户和应用程序来说,使用关系数据库不会有任何区别。这里的意思是说,当我们在数据库中查询时,如果有相应的索引可用,数据库系统就会自动使用索引来提高查询效率,如果没有索引,查询也能正常执行,只是速度会变慢。因此,索引可以在使用数据库的过程中逐步优化。
小结
通过对数据库表创建索引,可以提高查询速度。
通过创建唯一索引,可以保证某一列的值具有唯一性。
数据库索引对于用户和应用程序来说都是透明的。
查询数据
基本查询
要查询数据库表的数据,我们使用如下的SQL语句:
SELECT * FROM <表名>
SELECT
是关键字,表示将要执行一个查询,*
表示“所有列”,FROM
表示将要从哪个表查询
SELECT
语句其实并不要求一定要有FROM
子句。
SELECT 100+200;
不带FROM
子句的SELECT
语句有一个有用的用途,就是用来判断当前到数据库的连接是否有效。许多检测工具会执行一条SELECT 1;
来测试数据库连接。
条件查询
SELECT语句可以通过WHERE
条件来设定查询条件,查询结果是满足查询条件的记录。
因此,条件查询的语法就是:
SELECT * FROM <表名> WHERE <条件表达式>
条件表达式可以用<条件1> AND <条件2>
表达满足条件1并且满足条件2。
第二种条件是<条件1> OR <条件2>
,表示满足条件1或者满足条件2。
第三种条件是NOT <条件>
,表示“不符合该条件”的记录。
要组合三个或者更多的条件,就需要用小括号()
表示如何进行条件运算。
如果不加括号,条件运算按照NOT
、AND
、OR
的优先级进行,即NOT
优先级最高,其次是AND
,最后是OR
。加上括号可以改变优先级。
常用的条件表达式
条件 | 表达式举例1 | 表达式举例2 | 说明 |
---|---|---|---|
使用=判断相等 | score = 80 | name = 'abc' | 字符串需要用单引号括起来 |
使用>判断大于 | score > 80 | name > 'abc' | 字符串比较根据ASCII码,中文字符比较根据数据库设置 |
使用>=判断大于或相等 | score >= 80 | name >= 'abc' | |
使用<判断小于 | score < 80 | name <= 'abc' | |
使用<=判断小于或相等 | score <= 80 | name <= 'abc' | |
使用<>判断不相等 | score <> 80 | name <> 'abc' | |
使用LIKE判断相似 | name LIKE 'ab%' | name LIKE '%bc%' | %表示任意字符,例如'ab%'将匹配'ab','abc','abcd' |
投影查询
如果我们只希望返回某些列的数据,而不是所有列的数据,我们可以用SELECT 列1, 列2, 列3 FROM ...
,让结果集仅包含指定列。这种操作称为投影查询。
使用SELECT 列1, 列2, 列3 FROM ...
时,还可以给每一列起个别名,这样,结果集的列名就可以与原表的列名不同。它的语法是SELECT 列1 别名1, 列2 别名2, 列3 别名3 FROM ...
。
投影查询同样可以接WHERE
条件,实现复杂的查询:
排序
可以加上ORDER BY
子句进行排序
SELECT id, name, gender, score FROM students ORDER BY score;
如果要反过来,按照成绩从高到底排序,我们可以加上DESC
表示“倒序”:
SELECT id, name, gender, score FROM students ORDER BY score DESC;
如果score
列有相同的数据,要进一步排序,可以继续添加列名。例如,使用ORDER BY score DESC, gender
表示先按score
列倒序,如果有相同分数的,再按gender
列排序:
SELECT id, name, gender, score FROM students ORDER BY score DESC, gender;
默认的排序规则是ASC
:“升序”,即从小到大。ASC
可以省略,即ORDER BY score ASC
和ORDER BY score
效果一样。
如果有WHERE
子句,那么ORDER BY
子句要放到WHERE
子句后面。
分页查询
要实现分页功能,实际上就是从结果集中显示第1~100条记录作为第1页,显示第101~200条记录作为第2页,以此类推。
因此,分页实际上就是从结果集中“截取”出第M~N条记录。这个查询可以通过LIMIT <M> OFFSET <N>
子句实现
把结果集分页,每页3条记录。要获取第1页的记录,可以使用LIMIT 3 OFFSET 0
:
上述查询LIMIT 3 OFFSET 0
表示,对结果集从0号记录开始,最多取3条。注意SQL记录集的索引从0开始。
如果要查询第2页,那么我们只需要“跳过”头3条记录,也就是对结果集从3号记录开始查询,把OFFSET
设定为3:
分页查询的关键在于,首先要确定每页需要显示的结果数量pageSize
(这里是3),然后根据当前页的索引pageIndex
(从1开始),确定LIMIT
和OFFSET
应该设定的值:
-
LIMIT
总是设定为pageSize
; -
OFFSET
计算公式为pageSize * (pageIndex - 1)
。
注意
OFFSET
是可选的,如果只写LIMIT 15
,那么相当于LIMIT 15 OFFSET 0
。
在MySQL中,LIMIT 15 OFFSET 30
还可以简写成LIMIT 30, 15
。
使用LIMIT <M> OFFSET <N>
分页时,随着N
越来越大,查询效率也会越来越低。
聚合查询
对于统计总数、平均数这类计算,SQL提供了专门的聚合函数,使用聚合函数进行查询,就是聚合查询,它可以快速获得结果。
COUNT(*)
表示查询所有列的行数,要注意聚合的计算结果虽然是一个数字,但查询的结果仍然是一个二维表,只是这个二维表只有一行一列,并且列名是COUNT(*)
。
SELECT COUNT(*) num FROM <表名>; --num为别名
COUNT(*)
和COUNT(id)
实际上是一样的效果。另外注意,聚合查询同样可以使用WHERE
条件
除了COUNT()
函数外,SQL还提供了如下聚合函数:
函数 | 说明 |
---|---|
SUM | 计算某一列的合计值,该列必须为数值类型 |
AVG | 计算某一列的平均值,该列必须为数值类型 |
MAX | 计算某一列的最大值 |
MIN | 计算某一列的最小值 |
注意,MAX()
和MIN()
函数并不限于数值类型。如果是字符类型,MAX()
和MIN()
会返回排序最后和排序最前的字符。
要特别注意:如果聚合查询的WHERE
条件没有匹配到任何行,COUNT()
会返回0,而SUM()
、AVG()
、MAX()
和MIN()
会返回NULL
:
分组
SELECT COUNT(*) num FROM students GROUP BY class_id; --按照class_id进行分组
多表查询
询多张表的语法是:SELECT * FROM <表1> <表2>
。
这种一次查询两个表的数据,查询的结果也是一个二维表,它是students
表和classes
表的“乘积”,即students
表的每一行与classes
表的每一行都两两拼在一起返回。结果集的列数是students
表和classes
表的列数之和,行数是students
表和classes
表的行数之积。
这种多表查询又称笛卡尔查询,使用笛卡尔查询时要非常小心,由于结果集是目标表的行数乘积,对两个各自有100行记录的表进行笛卡尔查询将返回1万条记录,对两个各自有1万行记录的表进行笛卡尔查询将返回1亿条记录。
多表查询时,要使用表名.列名
这样的方式来引用列和设置别名,这样就避免了结果集的列名重复问题。但是,用表名.列名
这种方式列举两个表的所有列实在是很麻烦,所以SQL还允许给表设置一个别名,让我们在投影查询中引用起来稍微简洁一点:
注意到FROM
子句给表设置别名的语法是FROM <表名1> <别名1>, <表名2> <别名2>
。
多表查询也是可以添加WHERE
条件
连接查询
连接查询是另一种类型的多表查询。连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择性地“连接”在主表结果集上。
内连接INNER JOIN查询的写法是:
-
先确定主表,仍然使用
FROM <表1>
的语法; -
再确定需要连接的表,使用
INNER JOIN <表2>
的语法; -
然后确定连接条件,使用
ON <条件...>
,这里的条件是s.class_id = c.id
,表示students
表的class_id
列与classes
表的id
列相同的行需要连接; -
可选:加上
WHERE
子句、ORDER BY
等子句。
使用别名不是必须的,但可以更好地简化查询语句。
假设查询语句是:
SELECT ... FROM tableA ??? JOIN tableB ON tableA.column1 = tableB.column2;
我们把tableA看作左表,把tableB看成右表,那么INNER JOIN是选出两张表都存在的记录:
LEFT OUTER JOIN是选出左表存在的记录:
RIGHT OUTER JOIN是选出右表存在的记录:
FULL OUTER JOIN则是选出左右表都存在的记录:
修改数据
INSERT
当我们需要向数据库表中插入一条新记录时,就必须使用INSERT
语句。
INSERT
语句的基本语法是:
INSERT INTO <表名> (字段1, 字段2, ...) VALUES (值1, 值2, ...);
自增主键可以由数据库自己推算出来。此外,如果一个字段有默认值,那么在INSERT
语句中也可以不出现。
要注意,字段顺序不必和数据库表的字段顺序一致,但值的顺序必须和字段顺序一致。
还可以一次性添加多条记录,只需要在VALUES
子句中指定多个记录值,每个记录是由(...)
包含的一组值:
INSERT INTO students (class_id, name, gender, score) VALUES
(1, '大宝', 'M', 87),
(2, '二宝', 'M', 81);
SELECT * FROM students;
UPDATE
如果要更新数据库表中的记录,我们就必须使用UPDATE
语句。
UPDATE
语句的基本语法是:
UPDATE <表名> SET 字段1=值1, 字段2=值2, ... WHERE ...;
PDATE
语句的WHERE
条件和SELECT
语句的WHERE
条件其实是一样的,因此完全可以一次更新多条记录:
-- 更新id=5,6,7的记录
UPDATE students SET name='小牛', score=77 WHERE id>=5 AND id<=7;
-- 查询并观察结果:
SELECT * FROM students;
在UPDATE
语句中,更新字段时可以使用表达式
-- 更新score<80的记录
UPDATE students SET score=score+10 WHERE score<80;
-- 查询并观察结果:
SELECT * FROM students;
如果WHERE
条件没有匹配到任何记录,UPDATE
语句不会报错,也不会有任何记录被更新。
UPDATE
语句可以没有WHERE
条件,整个表的所有记录都会被更新。所以,在执行UPDATE
语句时要非常小心,最好先用SELECT
语句来测试WHERE
条件是否筛选出了期望的记录集,然后再用UPDATE
更新。
DELETE
如果要删除数据库表中的记录,我们可以使用DELETE
语句。
DELETE
语句的基本语法是:
DELETE FROM <表名> WHERE ...;
DELETE
语句的WHERE
条件也是用来筛选需要删除的行,因此和UPDATE
类似,DELETE
语句也可以一次删除多条记录:
- 删除id=5,6,7的记录
DELETE FROM students WHERE id>=5 AND id<=7;
-- 查询并观察结果:
SELECT * FROM students;
如果WHERE
条件没有匹配到任何记录,DELETE
语句不会报错,也不会有任何记录被删除
不带WHERE
条件的DELETE
语句会删除整个表的数据:所以,在执行DELETE
语句时也要非常小心,最好先用SELECT
语句来测试WHERE
条件是否筛选出了期望的记录集,然后再用DELETE
删除。
事物
在执行SQL语句的时候,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分。例如有两条SQL语句,两条必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。
这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
可见,数据库事务具有ACID这4个特性:
-
A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
-
C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
-
I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
-
D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。
对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
要手动把多条SQL语句作为一个事务执行,使用BEGIN
开启一个事务,使用COMMIT
提交一个事务,这种事务被称为显式事务
多条SQL语句要想作为一个事务执行,就必须使用显式事务。
COMMIT
是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT
语句执行失败了,整个事务也会失败。
有些时候,我们希望主动让事务失败,这时,可以用ROLLBACK
回滚事务,整个事务会失败:
隔离级别
对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。
SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted | Yes | Yes | Yes |
Read Committed | - | Yes | Yes |
Repeatable Read | - | - | Yes |
Serializable | - | - | - |
Read Uncommitted
Read Uncommitted是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。在Read Uncommitted隔离级别下,一个事务可能读取到另一个事务更新但未提交的数据,这个数据有可能是脏数据。
Read Committed
在Read Committed隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。
不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
Repeatable Read
在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。
幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。
Serializable
Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。
虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
默认隔离级别
如果没有指定隔离级别,数据库就会使用默认的隔离级别。在MySQL中,如果使用InnoDB,默认的隔离级别是Repeatable Read。