概述

NoSQL数据库,也就是非SQL的数据库,包括MongoDB、Cassandra、Dynamo等等,它们都不是关系数据库。有很多人鼓吹现代Web程序已经无需关系数据库了,只需要使用NoSQL就可以。但事实上,SQL数据库从始至终从未被取代过。今天,SQL数据库仍然承担了各种应用程序的核心数据存储,而NoSQL数据库作为SQL数据库的补充,两者不再是二选一的问题,而是主从关系。所以,无论使用哪种编程语言,无论是Web开发、游戏开发还是手机开发,掌握SQL,是所有软件开发人员所必须的。

数据库作为一种专门管理数据的软件就出现了。应用程序不需要自己管理数据,而是通过数据库软件提供的接口来读写数据。至于数据本身如何存储到文件,那是数据库软件的事情,应用程序自己并不关心。

数据库按照数据结构来组织,存储和管理数据,数据库一共有三种模型。层次模型,网状模型,关系模型。关系模型把数据看作是一个二维表格,任何数据都可以通过行号+列号来唯一确定,它的数据模型看起来就是一个Excel表。

随着时间的推移和市场竞争,最终,基于关系模型的关系数据库获得了绝对市场份额。因为相比层次模型和网状模型,关系模型理解和使用起来最简单。


通过给定一个班级名称,可以查到一条班级记录,根据班级ID,又可以查到多条学生记录,这样,二维表之间就通过ID映射建立了“一对多”关系。

数据类型


表中列举了最常用的数据类型。很多数据类型还有别名,例如,REAL又可以写成FLOAT(24)。还有一些不常用的数据类型,例如,TINYINT(范围在0~255)。各数据库厂商还会支持特定的数据类型,例如JSON。

选择数据类型的时候,要根据业务规则选择合适的类型。通常来说,BIGINT能满足整数存储的需求,VARCHAR(N)能满足字符串存储的需求,这两种类型是使用最广泛的。

主流关系数据库
目前主流关系数据库分为:

  1. 商用数据库,例如:Oracle,SQL Server,DB2等;
  2. 开源数据库,例如:MySQL,PostgreSQL等;
  3. 桌面数据库,以微软Access为代表,适合桌面应用程序使用;
  4. 嵌入式数据库,以Sqlite为代表,适合手机应用和桌面程序。

SQL
SQL已经被ANSI组织定义为标准,不幸地是,各个不同的数据库对标准的SQL支持不太一致。并且,大部分数据库都在标准的SQL上做了扩展。也就是说,如果只使用标准SQL,理论上所有数据库都可以支持,但如果使用某个特定数据库的扩展SQL,换一个数据库就不能执行了。例如,Oracle把自己扩展的SQL称为PL/SQL,Microsoft把自己扩展的SQL称为T-SQL。

SQL语言关键字不区分大小写!!!但是,针对不同的数据库,对于表名和列名,有的数据库区分大小写,有的数据库不区分大小写。同一个数据库,有的在Linux上区分大小写,有的在Windows上不区分大小写。本文做出约定:SQL关键字总是大写,以示突出,表名和列名均使用小写。

MySQL
MySQL是目前应用最广泛的开源关系数据库。MySQL经过几次收购就变成了Oracle旗下的产品。和其他关系数据库有所不同的是,MySQL本身实际上只是一个SQL接口,它的内部还包含了多种数据引擎,常用的包括:

  1. InnoDB:由Innobase Oy公司开发的一款支持事务的数据库引擎,2006年被Oracle收购;
  2. MyISAM:MySQL早期集成的默认数据库引擎,不支持事务。

MySQL接口和数据库引擎的关系就好比某某浏览器和浏览器引擎(IE引擎或Webkit引擎)的关系。对用户而言,切换浏览器引擎不影响浏览器界面,切换MySQL引擎不影响自己写的应用程序使用MySQL的接口。

使用MySQL时,不同的表还可以使用不同的数据库引擎。如果你不知道应该采用哪种引擎,记住总是选择InnoDB就好了。

由于MySQL是开源的,所以还有其衍生版本。

关系模型

关系数据库是建立在关系模型上的。而关系模型本质上就是若干个存储数据的二维表,可以把它们看作很多Excel表。

表的每一行称为记录(Record),记录是一个逻辑意义上的数据。表的每一列称为字段(Column),同一个表的每一行记录都拥有相同的若干字段。

字段定义了数据类型(整型、浮点型、字符串、日期等),以及是否允许为NULL。注意NULL表示字段数据不存在。一个整型字段如果为NULL不表示它的值为0,同样的,一个字符串型字段为NULL也不表示它的值为空串’’。

通常情况下,字段应该避免允许为NULL。不允许为NULL可以简化查询条件,加快查询速度,也利于应用程序读取数据后无需判断是否为NULL。

和Excel表有所不同的是,关系数据库的表和表之间需要建立“一对多”,“多对一”和“一对一”的关系,这样才能够按照应用程序的逻辑来组织和存储数据。

在关系数据库中,关系是通过主键外键来维护的。

主键

对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复不是指两条记录不完全相同,而是指能够通过某个字段唯一区分出不同的记录,这个字段被称为主键

由于主键的作用十分重要,如何选取主键会对业务开发产生重要影响。如果我们以学生的身份证号作为主键,似乎能唯一定位记录。然而,身份证号也是一种业务场景,如果身份证号升位了,或者需要变更,作为主键,不得不修改的时候,就会对业务产生严重影响。

所以,选取主键的一个基本原则是:不使用任何业务相关的字段作为主键

因此,身份证号、手机号、邮箱地址这些看上去可以唯一的字段,均不可用作主键。作为主键最好是完全业务无关的字段,我们一般把这个字段命名为id。常见的可作为id字段的类型有:

  1. 自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数,这样我们就完全不用担心主键重复,也不用自己预先生成主键;
  2. 全局唯一GUID类型:使用一种全局唯一的字符串作为主键,类似8f55d96b-8acc-4636-8cb8-76bf8abc2f57。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的,大部分编程语言都内置了GUID算法,可以自己预算出主键。

对于大部分应用来说,通常自增类型的主键就能满足需求。我们在students表中定义的主键也是BIGINT NOT NULL AUTO_INCREMENT类型。如果使用INT自增类型,那么当一张表的记录数超过2147483647(约21亿)时,会达到上限而出错。使用BIGINT自增类型则可以最多约922亿亿条记录。

联合主键

关系数据库实际上还允许通过多个字段唯一标识记录,即两个或更多的字段都设置为主键,这种主键被称为联合主键。对于联合主键,允许一列有重复,只要不是所有主键列都重复即可:


id_num和id_type这两列作为联合主键。没有必要的情况下,我们尽量不使用联合主键,因为它给关系表带来了复杂度的上升。

外键

在students表中,通过class_id的字段,可以把数据与另一张表关联起来,这种列称为外键
students表:


classes表:

为了表达这种一对多的关系,我们需要在students表中加入一列class_id,让它的值与classes表的某条记录相对应。

在students表中,通过class_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实现的:


注意:删除外键约束并没有删除外键这一列。删除列是通过DROP COLUMN …实现的。

多对多

通过一个表的外键关联到另一个表,我们可以定义出一对多关系。有些时候,还需要定义“多对多”关系。多对多关系实际上是通过两个一对多关系实现的,即通过一个中间表,关联两个一对多关系,就形成了多对多关系。

多对多关系实际上是通过两个一对多关系实现的,即通过一个中间表,关联两个一对多关系,就形成了多对多关系:
teacher表:


classes表:

中间表teacher_class关联两个一对多关系:

通过中间表,我们就定义了一个“多对多”关系。

一对一

一对一关系是指,一个表的记录对应到另一个表的唯一一个记录。

例如,students表的每个学生可以有自己的联系方式,如果把联系方式存入另一个表contacts,我们就可以得到一个“一对一”关系:这里一对一的前提是student_id不重复。


既然是一对一关系,那为啥不给students表增加一个mobile列,来合二为一。

如果业务允许,完全可以把两个表合为一个表。一对一关系准确地说,是contacts表一对一对应students表。

还有一些应用会把一个大表拆成两个一对一的表,目的是把经常读取和不经常读取的字段分开,以获得更高的性能。例如,把一个大的用户表分拆为用户基本信息表user_info和用户详细信息表user_profiles,大部分时候,只需要查询user_info表,并不需要查询user_profiles表,这样就提高了查询速度。

索引

在关系数据库中,如果有上万甚至上亿条记录,在查找记录的时候,想要获得非常快的速度,就需要使用索引。

索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。

例如,对于students表:


如果要经常根据score列进行查询,就可以对score列创建索引:

使用ADD INDEX idx_score (score)就创建了一个名称为idx_score,使用列score的索引。索引名称是任意的,索引如果有多列,可以在括号里依次写上,例如:

索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,例如gender列,大约一半的记录值是M,另一半是F,因此,对该列创建索引就没有意义。

可以对一张表创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引,因此,索引越多,插入、更新和删除记录的速度就越慢。

对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。

唯一索引

通过UNIQUE关键字我们就添加了一个唯一索引。也可以只对某一列添加一个唯一约束而不创建唯一索引。


这种情况下,name列没有索引,但仍然具有唯一性保证。

无论是否创建索引,对于用户和应用程序来说,使用关系数据库不会有任何区别。这里的意思是说,当我们在数据库中查询时,如果有相应的索引可用,数据库系统就会自动使用索引来提高查询效率,如果没有索引,查询也能正常执行,只是速度会变慢。因此,索引可以在使用数据库的过程中逐步优化。

查询数据

基本查询

假设表名是students,要查询students表的所有行,我们用如下SQL语句:

SELECT * FROM students;

使用SELECT FROM students时,SELECT是关键字,表示将要执行一个查询, 表示“所有列”,FROM表示将要从哪个表查询,本例中是students表。该SQL将查询出students表的所有数据。注意:查询结果也是一个二维表,它包含列名和每一行的数据。

不带FROM子句的SELECT语句有一个有用的用途,就是用来判断当前到数据库的连接是否有效。许多检测工具会执行一条SELECT 1;来测试数据库连接。

条件查询

SELECT * FROM <表名> WHERE <条件表达式>

条件表达式可以用<条件1> AND <条件2>表达满足条件1并且满足条件2。例如,符合条件“分数在80分或以上”,并且还符合条件“男生”,把这两个条件写出来:

  1. 根据score列的数据判断:score >= 80
  2. 根据gender列的数据判断:gender = ‘M’,注意gender列存储的是字符串,需要用单引号括起来。

除了AND以外,还有NOT以及OR。如果不加括号,条件运算按照NOT、AND、OR的优先级进行,即NOT优先级最高,其次是AND,最后是OR。加上括号可以改变优先级。

常用的条件表达式

%表示0个到多个,_ 表示1个。

投影查询

如果我们只希望返回某些列的数据,而不是所有列的数据,我们可以用SELECT 列1, 列2, 列3 FROM …,让结果集仅包含指定列。这种操作称为投影查询。例如,从students表中返回id、score和name这三列。


这样返回的结果集就只包含了我们指定的列,并且,结果集的列的顺序和原表可以不一样。

使用SELECT 列1, 列2, 列3 FROM …时,还可以给每一列起个别名,这样,结果集的列名就可以与原表的列名不同。它的语法是SELECT 列1 别名1, 列2 别名2, 列3 别名3 FROM …。

例如,以下SELECT语句将列名score重命名为points,而id和name列名保持不变:


投影查询同样可以接WHERE条件,实现复杂的查询。

排序

我们使用SELECT查询时,细心的读者可能注意到,查询结果集通常是按照id排序的,也就是根据主键排序。这也是大部分数据库的做法。如果我们要根据其他条件排序怎么办?可以加上ORDER BY子句。例如按照成绩从低到高进行排序:


如果要反过来,按照成绩从高到底排序,我们可以加上DESC表示“倒序”:

默认的排序规则是ASC:“升序”,即从小到大。ASC可以省略,即ORDER BY score ASC和ORDER BY score效果一样。

如果有WHERE子句,那么ORDER BY子句要放到WHERE子句后面。例如,查询一班的学生成绩,并按照倒序排序:

分页查询

使用SELECT查询时,如果结果集数据量很大,比如几万行数据,放在一个页面显示的话数据量太大,不如分页显示,每次显示100条。

要实现分页功能,实际上就是从结果集中显示第1~100条记录作为第1页,显示第101~200条记录作为第2页,以此类推。

因此,分页实际上就是从结果集中“截取”出第M~N条记录。这个查询可以通过LIMIT OFFSET 子句实现。我们先把所有学生按照成绩从高到低进行排序,把结果集分页,每页3条记录。要获取第1页的记录,可以使用LIMIT 3 OFFSET 0:


上述查询LIMIT 3 OFFSET 0表示,对结果集从0号记录开始,最多取3条。注意SQL记录集的索引从0开始。如果要查询第2页,那么我们只需要“跳过”头3条记录,也就是对结果集从3号记录开始查询,把OFFSET设定为3:类似的,查询第3页的时候,OFFSET应该设定为6:由于第4页只有1条记录,因此最终结果集按实际数量1显示。LIMIT 3表示的意思是“最多3条记录”。

OFFSET超过了查询的最大数量并不会报错,而是得到一个空的结果集。
OFFSET是可选的,如果只写LIMIT 15,那么相当于LIMIT 15 OFFSET 0。

在MySQL中,LIMIT 15 OFFSET 30还可以简写成LIMIT 30, 15。使用LIMIT OFFSET 分页时,随着N越来越大,查询效率也会越来越低。

聚合查询

对于统计总数、平均数这类计算,SQL提供了专门的聚合函数,使用聚合函数进行查询,就是聚合查询,它可以快速获得结果。仍然以查询students表一共有多少条记录为例,我们可以使用SQL内置的COUNT()函数查询。


COUNT( )表示查询所有列的行数,要注意聚合的计算结果虽然是一个数字,但查询的结果仍然是一个二维表,只是这个二维表只有一行一列,并且列名是COUNT( )。

通常,使用聚合查询时,我们应该给列名设置一个别名,便于处理结果。

COUNT(*)和COUNT(id)实际上是一样的效果。另外注意,聚合查询同样可以使用WHERE条件,因此我们可以方便地统计出有多少男生、多少女生、多少80分以上的学生等:


除了COUNT()函数外,SQL还提供了如下聚合函数:

注意,MAX()和MIN()函数并不限于数值类型。如果是字符类型,MAX()和MIN()会返回排序最后和排序最前的字符。

要统计男生的平均成绩,我们用下面的聚合查询:

要特别注意:如果聚合查询的WHERE条件没有匹配到任何行,COUNT()会返回0,而MAX()、MIN()、MAX()和MIN()会返回NULL。

分组

对于聚合查询,SQL还提供了“分组聚合”的功能。我们观察下面的聚合查询:


执行这个查询,COUNT()的结果不再是一个,而是3个,这是因为,GROUP BY子句指定了按class_id分组,因此,执行该SELECT语句时,会把class_id相同的列先分组,再分别计算,因此,得到了3行结果。

但是这3行结果分别是哪三个班级的,不好看出来,所以我们可以把class_id列也放入结果集中:



聚合查询的列中,只能放入分组的列。

多表查询

SELECT查询不但可以从一张表查询数据,还可以从多张表同时查询数据。查询多张表的语法是:SELECT * FROM <表1> <表2>。

同时从students表和classes表的“乘积”,即查询数据,可以这么写:


这种一次查询两个表的数据,查询的结果也是一个二维表,它是students表和classes表的“乘积”,即students表的每一行与classes表的每一行都两两拼在一起返回。结果集的列数是students表和classes表的列数之和,行数是students表和classes表的行数之积。

这种多表查询又称笛卡尔查询,使用笛卡尔查询时要非常小心,由于结果集是目标表的行数乘积,对两个各自有100行记录的表进行笛卡尔查询将返回1万条记录,对两个各自有1万行记录的表进行笛卡尔查询将返回1亿条记录。使用多表查询可以获取M x N行记录;多表查询的结果集可能非常巨大,要小心使用。

查询的结果集有两列id和两列name,两列id是因为其中一列是students表的id,而另一列是classes表的id,但是在结果集中,不好区分。两列name同理

要解决这个问题,我们仍然可以利用投影查询的“设置列的别名”来给两个表各自的id和name列起别名:


注意,多表查询时,要使用表名.列名这样的方式来引用列和设置别名,这样就避免了结果集的列名重复问题。但是,用表名.列名这种方式列举两个表的所有列实在是很麻烦,所以SQL还允许给表设置一个别名,让我们在投影查询中引用起来稍微简洁一点:

注意到FROM子句给表设置别名的语法是FROM <表名1> <别名1>, <表名2> <别名2>。这样我们用别名s和c分别表示students表和classes表。

多表查询也是可以添加WHERE条件的。

连接查询

连接查询是另一种类型的多表查询。连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择性地“连接”在主表结果集上。

例如,我们想要选出students表的所有学生信息,可以用一条简单的SELECT语句完成,假设我们希望结果集同时包含所在班级的名称,上面的结果集只有class_id列,缺少对应班级的name列。存放班级名称的name列存储在classes表中,只有根据students表的class_id,找到classes表对应的行,再取出name列,就可以获得班级名称。

连接查询就派上了用场。我们先使用最常用的一种内连接——INNER JOIN来实现。


INNER JOIN查询的写法是:

  1. 先确定主表,仍然使用FROM <表1>的语法;
  2. 再确定需要连接的表,使用INNER JOIN <表2>的语法;
  3. 然后确定连接条件,使用ON <条件…>,这里的条件是s.class_id = c.id,表示students表的class_id列与classes表的id列相同的行需要连接;
  4. 可选:加上WHERE子句、ORDER BY等子句。

使用别名不是必须的,但可以更好地简化查询语句。有内连接(INNER JOIN)就有外连接(OUTER JOIN)。

有RIGHT OUTER JOIN,就有LEFT OUTER JOIN,以及FULL OUTER JOIN。它们的区别是:

INNER JOIN只返回同时存在于两张表的行数据,由于students表的class_id包含1,2,3,classes表的id包含1,2,3,4,所以,INNER JOIN根据条件s.class_id = c.id返回的结果集仅包含1,2,3。

RIGHT OUTER JOIN返回右表都存在的行。如果某一行仅在右表存在,那么结果集就会以NULL填充剩下的字段。

LEFT OUTER JOIN则返回左表都存在的行。如果我们给students表增加一列,并添加class_id=5,由于classes表并不存在id=5的列,所以,LEFT OUTER JOIN的结果会增加一列,对应的class_name是NULL。

inner join

left outer join

right outer join

full outer join

修改数据

INSERT

插入数据基本语法:

如果一个字段有默认值,那么在INSERT语句中也可以不出现。

要注意,字段顺序不必和数据库表的字段顺序一致,但值的顺序必须和字段顺序一致。也就是说,可以写INSERT INTO students (score, gender, name, class_id) …,但是对应的VALUES就得变成(80, ‘M’, ‘大牛’, 2)。

还可以一次性添加多条记录,只需要在VALUES子句中指定多个记录值,每个记录是由(…)包含的一组值:

UPDATE

更新数据库表中的记录,我们就必须使用UPDATE语句。


例如,我们想更新students表id=1的记录的name和score这两个字段,先写出UPDATE students SET name=’大牛’, score=66,然后在WHERE子句中写出需要更新的行的筛选条件id=1:

UPDATE语句的WHERE条件和SELECT语句的WHERE条件其实是一样的,因此完全可以一次更新多条记录:

在UPDATE语句中,更新字段时可以使用表达式。例如,把所有80分以下的同学的成绩加10分:

如果WHERE条件没有匹配到任何记录,UPDATE语句不会报错,也不会有任何记录被更新。要特别小心的是,UPDATE语句可以没有WHERE条件,这时,整个表的所有记录都会被更新。

所以,在执行UPDATE语句时要非常小心,最好先用SELECT语句来测试WHERE条件是否筛选出了期望的记录集,然后再用UPDATE更新。

在使用MySQL这类真正的关系数据库时,UPDATE语句会返回更新的行数以及WHERE条件匹配的行数。

DELETE

删除数据库表中的记录,我们可以使用DELETE语句。
基本语法是:

DELETE语句的WHERE条件也是用来筛选需要删除的行,因此和UPDATE类似,DELETE语句也可以一次删除多条记录:


如果WHERE条件没有匹配到任何记录,DELETE语句不会报错,也不会有任何记录被删除。要特别小心的是,和UPDATE类似,不带WHERE条件的DELETE语句会删除整个表的数据。

整个表的所有记录都会被删除。所以,在执行DELETE语句时也要非常小心,最好先用SELECT语句来测试WHERE条件是否筛选出了期望的记录集,然后再用DELETE删除。

在使用MySQL这类真正的关系数据库时,DELETE语句也会返回删除的行数以及WHERE条件匹配的行数。

实用SQL语句

插入或替换

如果我们希望插入一条新记录(INSERT),但如果记录已经存在,就先删除原记录,再插入新记录。此时,可以使用REPLACE语句,这样就不必先查询,再决定是否先删除再插入。


若id=1的记录不存在,REPLACE语句将插入新记录,否则,当前id=1的记录将被删除,然后再插入新记录。

插入或更新

如果我们希望插入一条新记录(INSERT),但如果记录已经存在,就更新该记录,此时,可以使用INSERT INTO … ON DUPLICATE KEY UPDATE …语句:


若id=1的记录不存在,INSERT语句将插入新记录,否则,当前id=1的记录将被更新,更新的字段由UPDATE指定。

插入或忽略

如果我们希望插入一条新记录(INSERT),但如果记录已经存在,就啥事也不干直接忽略,此时,可以使用INSERT IGNORE INTO …语句。


若id=1的记录不存在,INSERT语句将插入新记录,否则,不执行任何操作。

快照

如果想要对一个表进行快照,即复制一份当前表的数据到一个新表,可以结合CREATE TABLE和SELECT。


新创建的表结构和SELECT使用的表结构完全一致。

写入查询结果集

如果查询结果集需要写入到表中,可以结合INSERT和SELECT,将SELECT语句的结果集直接插入到指定表中。

例如,创建一个统计成绩的表statistics,记录各班的平均成绩:


可以用一条语句写入各班的平均成绩:

确保INSERT语句的列和SELECT语句的列能一一对应,就可以在statistics表中直接保存查询的结果:

事务

在执行SQL语句的时候,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分。例如,一个转账操作:


这两条SQL语句必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。

这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。

数据库事务有ACID这4个特性。

  1. A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
  2. C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
  3. I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
  4. D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。

对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务

要手动把多条SQL语句作为一个事务执行,使用BEGIN开启一个事务,使用COMMIT提交一个事务,这种事务被称为显式事务,例如,把上述的转账操作作为一个显式事务:


很显然多条SQL语句要想作为一个事务执行,就必须使用显式事务。

COMMIT是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT语句执行失败了,整个事务也会失败。

有些时候,我们希望主动让事务失败,这时,可以用ROLLBACK回滚事务,整个事务会失败:


数据库事务是由数据库系统保证的,我们只需要根据业务逻辑使用它就可以。

隔离级别

对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。

SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:

Read Uncommitted

Read Uncommitted是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。


事务B两次读到的数据不一致。

Read Committed

在Read Committed隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。

不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。

表中数据现在为Alice。

Repeatable Read

在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。

幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。幻读就是没有读到的记录,以为不存在,但其实是可以更新成功的,并且,更新成功后,再次读取,就出现了。


事务B在第3步第一次读取id=99的记录时,读到的记录为空,说明不存在id=99的记录。随后,事务A在第4步插入了一条id=99的记录并提交。事务B在第6步再次读取id=99的记录时,读到的记录仍然为空,但是,事务B在第7步试图更新这条不存在的记录时,竟然成功了,并且,事务B在第8步再次读取id=99的记录时,记录出现了。

Repeatable Read下在同一个事务内的查询都是与事务开始时刻一致,所以在B事务过程中是不会读到期间A中insert的值的。

Serializable

Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。

虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用Serializable隔离级别。

默认隔离级别

如果没有指定隔离级别,数据库就会使用默认的隔离级别。在MySQL中,如果使用InnoDB,默认的隔离级别是Repeatable Read。