- MySQL 学习笔记(一)
- MySQL 学习笔记(二)
- MySQL - 索引
- MySQL 学习笔记(三)
别名
当表名或字段名比较长且需要重复使用时,可以为它们取一个别名,简化调用。其中:
- 为表取别名:其语法如下:
表名 [AS] 别名
- 为字段取别名:其语法如下:
字段名 [AS] 别名
数据操作
创建完数据表后,就可以往表里面添加具体的实体数据。下面介绍对实体数据的一些基本操作。
-
增:为数据表插入数据,使用的是
INSERT
命令。其语法如下所示:INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO] tbl_name [PARTITION (partition_name [, partition_name] ...)] [(col_name [, col_name] ...)] { {VALUES | VALUE} (value_list) [, (value_list)] ... | VALUES row_constructor_list } [AS row_alias[(col_alias [, col_alias] ...)]] [ON DUPLICATE KEY UPDATE assignment_list] INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO] tbl_name [PARTITION (partition_name [, partition_name] ...)] [AS row_alias[(col_alias [, col_alias] ...)]] SET assignment_list [ON DUPLICATE KEY UPDATE assignment_list] INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE] [INTO] tbl_name [PARTITION (partition_name [, partition_name] ...)] [(col_name [, col_name] ...)] [AS row_alias[(col_alias [, col_alias] ...)]] {SELECT ... | TABLE table_name} [ON DUPLICATE KEY UPDATE assignment_list] value: {expr | DEFAULT} value_list: value [, value] ... row_constructor_list: ROW(value_list)[, ROW(value_list)][, ...] assignment: col_name = [row_alias.]value assignment_list: assignment [, assignment] ...
INSERT
命令涉及的选项非常多,但是其最常用的格式却简单许多,如下所示:INSERT [INTO] table_name (column_list) VALUES (value_list);
其中:
-
table_name
:表示要插入数据的表名。 -
column_list
:表示要插入数据的列,多个列之间使用逗号(,
)进行分隔。 -
value_list
:表示要插入的具体数据。
举个例子:创建一张表
tmp
,并为其添加新数据:mysql> CREATE TABLE tmp ( # 建表 -> id INT PRIMARY KEY AUTO_INCREMENT, -> data VARCHAR(20) -> ); Query OK, 0 rows affected (1.10 sec) mysql> INSERT INTO tmp VALUES(10,'first data'); # 全量插入(不指定列) Query OK, 1 row affected (0.19 sec) mysql> INSERT INTO tmp(data) VALUES ('second data'); # 局部插入(指定列) Query OK, 1 row affected (0.27 sec) mysql> INSERT INTO tmp(data) VALUES ('third data'),('fourth data'); # 同时插入多条数据 Query OK, 2 rows affected (0.25 sec) Records: 2 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM tmp; # 查表 +----+-------------+ | id | data | +----+-------------+ | 10 | first data | | 11 | second data | | 12 | third data | | 13 | fourth data | +----+-------------+ 4 rows in set (0.00 sec)
注:使用
INSERT
命令时可以忽略指定要插入数据的列名,但这要求数据必须按照表结构定义时列的顺序依次进行插入(全量插入)。
而如果指定要插入数据的列名,则可实现只对某些列进行插入(局部插入),同时不必严格遵循列顺序,并且当表结构被改动时,指定插入的 SQL 语句不受影响。注:使用单条
INSERT
语句插入多个记录效果等同于多次执行单行插入INSERT
语句,但是效率会更高。INSERT
语句还可以将对一个表的查询结果插入到另一个表中。其语法格式如下所示:INSERT [INTO] table_name1 (column_list) SELECT (column_list2) FROM table_name2 WHERE (condition)
举个例子:比如我们创建一个新表
tmp1
,其只有一个字段data
,然后我们将表tmp
中的data
字段复制给新表tmp1
:mysql> CREATE TABLE tmp1 ( data VARCHAR(20) ); Query OK, 0 rows affected (1.35 sec) mysql> INSERT INTO tmp1 -> SELECT data FROM tmp; Query OK, 4 rows affected (0.34 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM tmp1; +-------------+ | data | +-------------+ | first data | | second data | | third data | | fourth data | +-------------+ 4 rows in set (0.00 sec)
注:借助
INSERT
语句提供的查询结果插入功能,我们可以很方便实现对数据表的备份功能。
比如,现在假设我们要备份数据表tmp
,具体操作如下所示:mysql> CREATE TABLE clone_tmp LIKE tmp; # 复制表结构 [0/2853]Query OK, 0 rows affected (1.38 sec) mysql> SHOW CREATE TABLE clone_tmp\G # 查看表结构 *************************** 1. row *************************** Table: clone_tmp Create Table: CREATE TABLE `clone_tmp` ( `id` int NOT NULL AUTO_INCREMENT, `data` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.10 sec) mysql> SELECT * FROM clone_tmp; # 查看表数据 Empty set (0.00 sec) mysql> INSERT INTO clone_tmp # 复制表数据 -> SELECT * FROM tmp; Query OK, 4 rows affected (0.24 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM clone_tmp; # 查看表数据 +----+-------------+ | id | data | +----+-------------+ | 10 | first data | | 11 | second data | | 12 | third data | | 13 | fourth data | +----+-------------+ 4 rows in set (0.00 sec)
-
-
删:从数据表中删除数据使用的是
DELETE
语句。其语法如下所示:DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM tbl_name [[AS] tbl_alias] [PARTITION (partition_name [, partition_name] ...)] [WHERE where_condition] [ORDER BY ...] [LIMIT row_count]
注:
DELETE
语句如果未指定WHERE
子句限定删除范围,则将进行全表删除,即删除表中所有的记录。举个例子:删除表
tmp
中id
在10
到12
之间的记录:mysql> SELECT * FROM tmp; # 原始数据 +----+-------------+ | id | data | +----+-------------+ | 10 | first data | | 11 | second data | | 12 | third data | | 13 | fourth data | +----+-------------+ 4 rows in set (0.00 sec) mysql> DELETE FROM tmp WHERE id BETWEEN 10 AND 12; # 删除数据 Query OK, 3 rows affected (0.64 sec) mysql> SELECT * FROM tmp; # 最新数据 +----+-------------+ | id | data | +----+-------------+ | 13 | fourth data | +----+-------------+ 1 row in set (0.00 sec)
注:如果想删除表中所有记录,一个高效的方法是使用
TRUNCATE
语句。其语法如下所示:TRUNCATE [TABLE] tbl_name
TRUNCATE
语句实际上是将表进行删除,然后再重新创建一个相同结构的新表。也因为TRUNCATE
是直接删除表而不是逐行删除记录,因此它的执行速度比DELETE
快。 -
改:对数据进行修改,使用的是
UPDATE
语句。其语法如下所示:# 格式一:Single-table syntax UPDATE [LOW_PRIORITY] [IGNORE] table_reference SET assignment_list [WHERE where_condition] [ORDER BY ...] [LIMIT row_count] value: {expr | DEFAULT} assignment: col_name = value assignment_list: assignment [, assignment] ... # 格式二:Multiple-table syntax UPDATE [LOW_PRIORITY] [IGNORE] table_references SET assignment_list [WHERE where_condition]
UPDATE
语句最常用的语法结构如下:UPDATE table_name SET column_name1 = value1, column_name2 = value2,..., column_namen = valuen WHERE (condition);
举个例子:找到表
tmp
中id
为10
的记录,将且data
字段的值更改为ten data
:mysql> SELECT * FROM tmp WHERE id = 10; # 原始数据 +----+------------+ | id | data | +----+------------+ | 10 | first data | +----+------------+ 1 row in set (0.02 sec) mysql> UPDATE tmp SET data='ten data' WHERE id=10; # 修改数据 Query OK, 1 row affected (0.21 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> SELECT * FROM tmp WHERE id=10; # 最新数据 +----+----------+ | id | data | +----+----------+ | 10 | ten data | +----+----------+ 1 row in set (0.00 sec)
注:请确保
UPDATE
语句始终带有WHERE
子句,从而限定修改记录范围。如果忽略WHERE
子句,MySQL 将会执行全表更新,即表中所有行都会被更新。 -
查:对数据表进行查询,使用的语句为
select
。其语法如下所示:SELECT [ALL | DISTINCT | DISTINCTROW ] [HIGH_PRIORITY] [STRAIGHT_JOIN] [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT] [SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] select_expr [, select_expr] ... [into_option] [FROM table_references [PARTITION partition_list]] [WHERE where_condition] [GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]] [HAVING where_condition] [WINDOW window_name AS (window_spec) [, window_name AS (window_spec)] ...] [ORDER BY {col_name | expr | position} [ASC | DESC], ... [WITH ROLLUP]] [LIMIT {[offset,] row_count | row_count OFFSET offset}] [into_option] [FOR {UPDATE | SHARE} [OF tbl_name [, tbl_name] ...] [NOWAIT | SKIP LOCKED] | LOCK IN SHARE MODE] [into_option] into_option: { INTO OUTFILE 'file_name' [CHARACTER SET charset_name] export_options | INTO DUMPFILE 'file_name' | INTO var_name [, var_name] ... }
对数据的查询操作包含很多细分内容,以下列举常用的查询操作:
-
基本查询:基本查询就是对数据表字段直接进行查询。其语法如下所示:
SELECT { * |
} FROM , ... 其中:
-
*
:表示选中数据表所有字段。 -
column_list
:表示字段列表,即数据表一个或多个字段的组合,多个字段间使用逗号,
进行分隔。
举个例子:创建一张用户表
user
,为其添加一些数据,然后查询下看添加是否成功:mysql> CREATE TABLE user ( # 建表 -> id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, -> name VARCHAR(30) NOT NULL DEFAULT 'Anonymous', -> gender ENUM('male','female') NOT NULL DEFAULT 'male', -> email VARCHAR(30) -> ); Query OK, 0 rows affected (4.50 sec) mysql> INSERT INTO user (name, gender, email) VALUES # 插入数据 -> ('zhangsan', 'male', '[email protected]'), -> ('lisi', 'male', '[email protected]'), -> ('wangwu', 'female', '[email protected]'); Query OK, 3 rows affected (0.21 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM user; # 全量查询(全字段查询) +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 1 | zhangsan | male | [email protected] | | 2 | lisi | male | [email protected] | | 3 | wangwu | female | [email protected] | +----+----------+--------+------------------------+ 3 rows in set (0.00 sec) mysql> SELECT name, gender, email FROM user; # 局部字段查询 +----------+--------+------------------------+ | name | gender | email | +----------+--------+------------------------+ | zhangsan | male | [email protected] | | lisi | male | [email protected] | | wangwu | female | [email protected] | +----------+--------+------------------------+ 3 rows in set (0.00 sec)
-
-
数据过滤:对查询得到结果进行过滤,使用的是
WHERE
子句结合其他过滤操作符,常用的数据过滤操作有如下几种:-
范围过滤:范围过滤是对
SELECT
得到的结果集中的某些字段在数值范围上进行限定,大概有如下三种操作:-
比较运算符:即基于对结果集字段的数值大小进行比较操作。
MySQL 中常用的比较操作符如下表所示:符号 描述 =
等于 <>, !=
不等于 >
大于 >=
大于或等于 <
小于 <=
小于或等于 <=>
比较两个 NULL
值是否相等其中:
<=>
操作符作用是安全的NULL
相等比较,当比较的两个操作码均为NULL
时,返回1
。而当只有一个操作码为NULL
时,返回0
。如下所示:mysql> SELECT NULL <=> NULL; +---------------+ | NULL <=> NULL | +---------------+ | 1 | +---------------+ 1 row in set (0.01 sec) mysql> SELECT 1 <=> NULL; +------------+ | 1 <=> NULL | +------------+ | 0 | +------------+ 1 row in set (0.05 sec)
更多比较操作符详情,请查看:Comparison Operators
举个例子:查询表
user
中性别为male
的记录:mysql> SELECT * FROM user WHERE gender = 'male'; +----+----------+--------+---------------------+ | id | name | gender | email | +----+----------+--------+---------------------+ | 1 | zhangsan | male | [email protected] | | 2 | lisi | male | [email protected] | +----+----------+--------+---------------------+ 2 rows in set (0.00 sec)
注:比较操作符如果用于对字符串的比较时,字符串需要使用单引号
'
进行限定。 -
IN
:该操作符可以是多个数据的集合,当结果集匹配集合中任一数据时,则将其返回。
其语法如下所示:expr [NOT] IN (value,...)
其中:当
expr
等于任一value
值时,返回1
(true
),否则,返回0
(false
)。举个例子:查询表
user
中姓名为lisi
和wangwu
的记录:mysql> SELECT * FROM user WHERE name IN ('lisi', 'wangwu'); +----+--------+--------+------------------------+ | id | name | gender | email | +----+--------+--------+------------------------+ | 2 | lisi | male | [email protected] | | 3 | wangwu | female | [email protected] | +----+--------+--------+------------------------+ 2 rows in set (0.00 sec)
-
BETWEEN AND
:该操作符用于限定一个区间范围,当结果集落在该区间范围内时,则将其返回。
其语法如下所示:expr [NOT] BETWEEN min AND max
其中:当
expr
值大于等于min
并且小于等于max
时,返回1
,否则返回0
。举个例子:查询表
user
中id
在1
到3
中的所有记录:mysql> SELECT * FROM user WHERE id BETWEEN 1 AND 3; +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 1 | zhangsan | male | [email protected] | | 2 | lisi | male | [email protected] | | 3 | wangwu | female | [email protected] | +----+----------+--------+------------------------+ 3 rows in set (0.06 sec)
注:
IN
操作符是数据集合,BETWEEN AND
操作符是区间集合,两者区别示例如下:-
IN (1, 3)
:表示匹配的数据为1
或3
。 -
BETWEEN 1 AND 3
:表示匹配的数据为1
、2
或3
。
-
-
空值过滤:如果需要检查字段是否为
NULL
,可以使用IS NULL
操作符。
其语法如下所示:IS [NOT] NULL
注:空值与字段为
0
,空字符串和包含空格不同。如下例子所示:mysql> SELECT 1 IS NULL, 0 IS NULL, '' IS NULL, ' ' IS NULL, NULL IS NULL; +-----------+-----------+------------+-------------+--------------+ | 1 IS NULL | 0 IS NULL | '' IS NULL | ' ' IS NULL | NULL IS NULL | +-----------+-----------+------------+-------------+--------------+ | 0 | 0 | 0 | 0 | 1 | +-----------+-----------+------------+-------------+--------------+ 1 row in set (0.00 sec)
-
去重:如果查询结果集存在重复条目,可使用
DISTINCT
关键字去除重复条目。
其语法如下所示:SELECT DISTINCT 字段名 FROM 表名;
举个例子:为表
user
插入一个name
为wangwu
的新记录,这样user
表中字段name
就存在重复条目,然后分别查询在不使用DISTINCT
和使用DISTINCT
关键字的结果集:mysql> INSERT INTO user(name) VALUES ('wangwu'); # 插入重复数据 Query OK, 1 row affected (0.20 sec) mysql> SELECT name FROM user WHERE name = 'wangwu'; # 查表 +--------+ | name | +--------+ | wangwu | # 结果集存在重复 | wangwu | +--------+ 2 rows in set (0.00 sec) mysql> SELECT DISTINCT name FROM user WHERE name = 'wangwu'; # 查表(去重) +--------+ | name | +--------+ | wangwu | # 去重成功 +--------+ 1 row in set (0.00 sec)
-
逻辑过滤:表达式可以结合逻辑运算符来进行范围限定。MySQL 中常用的逻辑运算符如下表所示:
运算符号 描述 AND
与 OR
或 NOT
或!
非 XOR
异或 其中:如果表达式为真,则返回
1
,否则返回0
。注:逻辑或
OR
操作符作用效果与IN
操作符是一样的,但是IN
操作符更加简洁明了,且效率更高,最重要的是IN
操作符支持更加复杂的嵌套查询。注:
OR
可以和AND
一起使用,在默认情况下,AND
的优先级大于OR
,对于复杂的查询操作,建议手动添加括号()
以保证运算顺序符合预期。举个例子:使用逻辑运算符查询用户表
user
:# 逻辑与:选择 name='wangwu' 且 gender='female' 的记录 mysql> SELECT * FROM user WHERE name = 'wangwu' AND gender = 'female'; +----+--------+--------+------------------------+ | id | name | gender | email | +----+--------+--------+------------------------+ | 3 | wangwu | female | [email protected] | +----+--------+--------+------------------------+ 1 row in set (0.00 sec) # 逻辑或:选择 name='lisi' 或 email 为空的记录 mysql> SELECT * FROM user WHERE name = 'lisi' OR email IS NULL; +----+--------+--------+---------------------+ | id | name | gender | email | +----+--------+--------+---------------------+ | 2 | lisi | male | [email protected] | | 4 | wangwu | male | NULL | +----+--------+--------+---------------------+ 2 rows in set (0.00 sec) # 逻辑非:选择 name 不为 wnagwu 的记录 mysql> SELECT * FROM user WHERE NOT name = 'wangwu'; +----+----------+--------+---------------------+ | id | name | gender | email | +----+----------+--------+---------------------+ | 1 | zhangsan | male | [email protected] | | 2 | lisi | male | [email protected] | +----+----------+--------+---------------------+ 2 rows in set (0.00 sec)
-
匹配过滤:MySQL 支持模糊匹配功能,具体可分为如下两种模式:
-
通配符匹配:MySQL 中
LIKE
关键字支持使用通配符进行匹配查找。其语法如下所示:expr [NOT] LIKE pattern [ESCAPE 'escape_char']
注:更多字符串匹配操作,请参考:String Comparison Functions
MySQL 支持多种通配符,常用的通配符有如下两种:
通配符 描述 %
匹配任意长度字符串 _
匹配任意单个字符串 注:通配符
%
无法匹配空值NULL
。举个例子:查找表
user
中名称包含an
的记录,查找表user
中名称长度为4
,且以字符i
结尾的记录:mysql> SELECT * FROM user WHERE name LIKE '%an%'; +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 1 | zhangsan | male | [email protected] | | 3 | wangwu | female | [email protected] | | 4 | wangwu | male | NULL | +----+----------+--------+------------------------+ 3 rows in set (0.00 sec) mysql> SELECT * FROM user WHERE name LIKE '___i'; +----+------+--------+---------------------+ | id | name | gender | email | +----+------+--------+---------------------+ | 2 | lisi | male | [email protected] | +----+------+--------+---------------------+ 1 row in set (0.00 sec)
-
正则匹配:MySQL 中可通过关键字
REGEXP
进行正则表达式查询。其语法如下所示:expr [NOT] REGEXP pattern
注:更多正则匹配内容,请查看:Regular Expressions
举个例子:查找表
user
中带有纯数字邮箱的记录:mysql> SELECT * FROM user WHERE email REGEXP '^\\\d{1,}@.+\\.com$'; +----+----------+--------+---------------------+ | id | name | gender | email | +----+----------+--------+---------------------+ | 1 | zhangsan | male | [email protected] | | 2 | lisi | male | [email protected] | +----+----------+--------+---------------------+ 2 rows in set (0.00 sec)
注:MySQL 中,为了匹配特殊字符,需要使用
\\
前缀进行转义。比如,\\-
表示查找-
,\\.
表示查找.
。
-
-
-
条目限制:MySQL 中可以使用
LIMIT
关键字限制查询结果的数量。其语法如下所示:# 格式一 LIMIT [偏移量,] 行数 # 格式二 LIMIT 行数 OFFSET 偏移量
其中:
-
偏移量
:指从结果集第几行开始选取,其默认值为0
,即从结果集第一条记录开始显示。 -
行数
:指返回的记录条目数。
注:MySQL 8.0 中也可以使用
LIMIT 行数 OFFSET 偏移量
的用法,比如:LIMIT 3 OFFSET 4
与LIMIT 4, 3
效果一样,都表示从结果集第 5 条记录开始选取后面的 3 条记录。举个例子:查询表
user
第一条记录:mysql> SELECT * FROM user LIMIT 1; # 相当于 limit 0, 1 +----+----------+--------+------------------+ | id | name | gender | email | +----+----------+--------+------------------+ | 1 | zhangsan | male | [email protected] | +----+----------+--------+------------------+ 1 row in set (0.00 sec)
-
-
数据排序:如果想对结果集进行排序,可使用
ORDER BY
子句。其语法如下所示:ORDER BY {col_name | expr | position} [ASC | DESC]
其中:
ORDER BY
子句默认采用升序ASC
排序,若需要降序排序,则需显示使用DESC
。ORDER BY
支持多字段排序,只需使用逗号,
分隔多字段即可。多字段排序只有在当前面字段值相同时,才会对这些相同记录采用后续字段进行排序。举个例子:查询表
user
所有记录,先按照字段name
进行升序排序,当name
字段相同的记录,再按照id
进行降序排列:mysql> SELECT * FROM user ORDER BY name; # 按 name 升序排列 +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 2 | lisi | male | [email protected] | | 3 | wangwu | female | [email protected] | | 4 | wangwu | male | NULL | | 1 | zhangsan | male | [email protected] | +----+----------+--------+------------------------+ 4 rows in set (0.00 sec) mysql> SELECT * FROM user ORDER BY name, id DESC; # 先按 name 升序排列,name 相同时再按 id 降序排列 +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 2 | lisi | male | [email protected] | | 4 | wangwu | male | NULL | | 3 | wangwu | female | [email protected] | | 1 | zhangsan | male | [email protected] | +----+----------+--------+------------------------+ 4 rows in set (0.00 sec)
注:
ORDER BY
通常都是使用结果集中的字段进行排序,但是也可以使用非检索的列字段(即原表字段)进行排序。 -
数据汇总:对数据进行查询时,很多时候并不是要对数据进行检索,而是直接对查询出来的数据进行汇总。为了方便对数据进行汇总计算,MySQL 内置了一些常用的聚集函数,方便我们进行数据汇总。
注:聚集函数(aggregate function):指的是运行在行组上,计算和返回单个值的函数。
MySQL 中最常使用到的聚集函数如下表所示:
函数 描述 AGV()
返回某列的平均值 COUNT()
返回某列的行数 MAX()
返回某列的最大值 MIN()
返回某列的最小值 SUM()
返回某列之和 注:更多聚集函数内容,请查看:Aggregate Function Descriptions
举个例子:查询表
user
的字段id
,输出结果集的总行数,并分别求取该字段的最小值,最大值,平均值和总和值:mysql> SELECT COUNT(id), # 总行数 -> MIN(id), # 最小值 -> MAX(id), # 最大值 -> AVG(id), # 平均值 -> SUM(id) # 总和 -> FROM user; +-----------+---------+---------+---------+---------+ | count(id) | min(id) | max(id) | avg(id) | sum(id) | +-----------+---------+---------+---------+---------+ | 4 | 1 | 4 | 2.5000 | 10 | +-----------+---------+---------+---------+---------+ 1 row in set (0.00 sec)
-
分组查询:分组查询是对记录按照某一个或多个字段进行分组,其实质是允许把数据分为多个逻辑组,以便能对每个组进行聚集计算。
MySQL 中使用关键字GROUP BY
对数据进行分组,其语法格式如下所示:[GROUP BY 字段] [HAVING <条件表达式>]
其中:
-
字段
:表示要按照该字段名进行分组。 -
HAVING
:表示对分组进行过滤,满足条件表达式的分组将被显示。
GROUP BY
子句的执行机制为:首先GROUP BY
会将指定字段进行排序,然后将值相同的字段整合为一个组,这样由于字段值的不同,就产生了多个组。然后分别对这些组进行聚集计算。一个需要牢记的点是:
GROUP BY
子句指示 MySQL 分组数据,然后对每个组而不是整个结果集进行聚集。以下列举一些常见的分组操作:
-
创建分组:创建分组直接使用
GROUP BY
子句即可。GROUP BY
通常和聚集函数一起使用,以便能汇总总表内容的子集。注:如果分组列中具有
NULL
值,则NULL
将作为一个分组返回。如果列中有多行NULL
值,它们将分为一组。举个例子:对数据表
user
按照字段name
进行分组,并统计分组数量:mysql> SELECT * FROM user; # 原始数据 +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 1 | zhangsan | male | [email protected] | | 2 | lisi | male | [email protected] | | 3 | wangwu | female | [email protected] | | 4 | wangwu | male | NULL | +----+----------+--------+------------------------+ 4 rows in set (0.00 sec) mysql> SELECT name, COUNT(*) FROM user GROUP BY name; # 按 name 进行分组 +----------+----------+ | name | count(*) | +----------+----------+ | zhangsan | 1 | | lisi | 1 | | wangwu | 2 | +----------+----------+ 3 rows in set (0.00 sec)
如上所示,我们将表
user
按照名称name
进行分组,并统计各个分组数量。可以看到,数据表user
中名称为zhangsan
和lisi
的记录都各自只有一个,而名称为wangwu
的记录总共有两条。 -
分组详情:可以通过函数
GROUP_CONCAT()
将每个分组中的各个字段具体值显示出来。举个例子:将表
user
按性别gender
进行分组,统计并显示各性别组的人的名称。mysql> SELECT gender, COUNT(*) as total, GROUP_CONCAT(name) AS names FROM user GROUP BY gender; +--------+-------+----------------------+ | gender | total | names | +--------+-------+----------------------+ | male | 3 | zhangsan,lisi,wangwu | | female | 1 | wangwu | +--------+-------+----------------------+ 2 rows in set (0.00 sec)
-
分组汇总:可以使用关键子
WITH ROLLUP
来为所有分组记录添加一条汇总记录,该汇总记录会自动计算分组数据总和。举个例子:将表
user
按性别gender
进行分组,并汇总所有分组记录:mysql> SELECT gender, COUNT(*) AS total FROM user GROUP BY gender WITH ROLLUP; +--------+-------+ | gender | total | +--------+-------+ | male | 3 | | female | 1 | | NULL | 4 | # 汇总记录 +--------+-------+ 3 rows in set (0.00 sec)
-
多字段分组:
GROUP BY
子句支持多字段分组,字段之间使用逗号,
进行分隔。GROUP BY
进行多字段分组时,其执行机制为:首先按照第一个字段进行分组,当第一个字段存在相同记录时,再将这些相同的记录以第二个字段进行分组,依次类推。举个例子:对表
user
按照性别gender
进行分组,当性别相同时,再按照姓名进行分组:mysql> SELECT name,gender FROM user GROUP BY gender,name; +----------+--------+ | name | gender | +----------+--------+ | zhangsan | male | | lisi | male | | wangwu | female | | wangwu | male | +----------+--------+ 4 rows in set (0.00 sec)
-
过滤分组:
GROUP BY
子句可结合HAVING
关键字来对分组进行过滤,只有在满足限定条件下,才显示分组内容。注:
HAVING
非常类似于WHERE
,它们之间的区别在于:WHERE
用于行过滤,而HAVING
用于分组过滤。或者说,WHERE
是在数据分组前进行过滤,而HAVING
是在数据分组后进行过滤。举个例子:将表
user
按性别gender
进行分组,然后过滤出性别数量大于 1 的分组:mysql> SELECT gender, GROUP_CONCAT(name) AS names FROM user GROUP BY gender HAVING COUNT(gender) > 1; +--------+----------------------+ | gender | names | +--------+----------------------+ | male | zhangsan,lisi,wangwu | +--------+----------------------+ 1 row in set (0.01 sec)
-
分组排序:
GROUP BY
用于分组,ORDER BY
用于记录排序,两者结合起来,就可以实现对分组进行排序。举个例子:对表
user
按性别gender
进行分组,并统计性别人数,按性别人数从小到大进行排序:mysql> SELECT gender, COUNT(*) AS total FROM user GROUP BY gender; # 未排序 +--------+-------+ | gender | total | +--------+-------+ | male | 3 | | female | 1 | +--------+-------+ 2 rows in set (0.00 sec) mysql> SELECT gender, COUNT(*) AS total FROM user GROUP BY gender ORDER BY total; # 按 total 排序 +--------+-------+ | gender | total | +--------+-------+ | female | 1 | | male | 3 | +--------+-------+ 2 rows in set (0.00 sec)
-
-
多表查询:在关系型数据库中,表与表之间可以存在关联关系,因此可以同时针对多个表进行查询,然后连接这些数据到同一结果集中。
多表查询常涉及的操作有如下几种:-
组合查询:组合查询可以将多条
SELECT
语句组合到一个结果集中,MySQL 中借助UNION
关键字可以实现组合查询。
其语法如下所示:SELECT column,... FROM table_name1 UNION [ALL | DISTINCT] SELECT column,... FROM table_name2
其中:
UNION
合并生成的结果集会自动删除重复条目(即默认使用UNION DISTINCT
),返回的行都是唯一的。如果期望保留重复条目,则需使用UNION ALL
。注:联合查询只要求查询的列数一样,数据类型可不相同,结果集的字段由第一条查询语句决定。
注:
UNION
也可以对同一个表进行组合查询,这种情况下效果与使用逻辑运算符OR
的效果是一样的。举个例子:对表
user
进行组合查询,要求获取性别gender
为女的用户和箱email
不为空的用户:mysql> SELECT name FROM user WHERE gender = 'female'; # 性别为女 +--------+ | name | +--------+ | wangwu | +--------+ 1 row in set (0.00 sec) mysql> SELECT name FROM user WHERE email IS NOT NULL; # 邮箱不为空 +----------+ | name | +----------+ | zhangsan | | lisi | | wangwu | +----------+ 3 rows in set (0.00 sec) mysql> SELECT name FROM user WHERE gender = 'female' -> UNION # 组合查询(去重) -> SELECT name FROM user WHERE email IS NOT NULL; +----------+ | name | +----------+ | wangwu | | zhangsan | | lisi | +----------+ 3 rows in set (0.00 sec) mysql> SELECT name FROM user WHERE gender = 'female' -> UNION ALL # 组合查询(保留重复条目) -> SELECT name FROM user WHERE email IS NOT NULL; +----------+ | name | +----------+ | wangwu | | zhangsan | | lisi | | wangwu | +----------+ 4 rows in set (0.00 sec)
-
子查询(subquery):子查询指的是嵌套在其他查询语句中的查询语句。
子查询的执行机制为:子查询总是由内向外处理,即在
SELECT
子句中,最内部的子查询会最先进行计算,然后将该子查询结果作为临近外层另一个查询的过滤条件,查询可以基于一个表或者多个表。举个例子:查询表
user
,找到id
为1
和3
的记录:mysql> SELECT * FROM user WHERE id IN (1, 3); # 直接查询 +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 1 | zhangsan | male | [email protected] | | 3 | wangwu | female | [email protected] | +----+----------+--------+------------------------+ 2 rows in set (0.00 sec) mysql> SELECT * FROM user WHERE id IN ( -> SELECT id FROM user WHERE id = 1 OR id = 3 # 使用子查询 -> ); +----+----------+--------+------------------------+ | id | name | gender | email | +----+----------+--------+------------------------+ | 1 | zhangsan | male | [email protected] | | 3 | wangwu | female | [email protected] | +----+----------+--------+------------------------+ 2 rows in set (0.00 sec)
上述例子其实就是使用子查询
SELECT id FROM user WHERE id = 1 OR id = 3
得到结果集1,3
,然后该结果集会被外层SELECT
语句使用,相当于外层语句为:SELECT * FROM user WHERE id IN (1, 3)
。另外,子查询中经常会使用一些操作符,增强查询功能。常用的子查询操作符如下表所示:
操作符 描述 ANY
表示只要满足子查询任一比较条件,就返回 SOME
同 ANY
ALL
表示外层查询必须同时满足子查询的所有条件,才返回 EXISTS
用来判断子查询是否返回行,如果子查询至少返回一行,则 EXISTS
返回true
,此时会触发外层查询举个例子:创建两个表
tmp1
和tmp2
,分别使用子查询操作符进行比较:mysql> CREATE TABLE tmp1 ( num1 INT); # 建表 Query OK, 0 rows affected (2.41 sec) mysql> CREATE TABLE tmp2 ( num2 INT); # 建表 Query OK, 0 rows affected (1.84 sec) mysql> INSERT INTO tmp1 VALUES (1), (10), (55), (123); # 插入值 Query OK, 4 rows affected (0.31 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> INSERT INTO tmp2 VALUES (5), (35), (382); # 插入值 Query OK, 4 rows affected (0.17 sec) Records: 4 Duplicates: 0 Warnings: 0 # 子查询返回集合 [5, 35, 382],num1 > ANY:表示 num1 大于 [5, 35, 382] 中任一一个值即可返回 mysql> SELECT * FROM tmp1 WHERE num1 > ANY( SELECT num2 FROM tmp2); +------+ | num1 | +------+ | 10 | | 55 | | 123 | +------+ 3 rows in set (0.00 sec) # 子查询返回集合 [5, 35, 382],num1 < ALL:表示 num1 必须小于 [5, 35, 382] 中所有值才能返回 mysql> SELECT * FROM tmp1 WHERE num1 < ALL( SELECT num2 FROM tmp2); +------+ | num1 | +------+ | 1 | +------+ 1 row in set (0.01 sec) # 当子查询返回结果集不为空时,触发外层查询 mysql> SELECT * FROM tmp1 WHERE EXISTS (SELECT * FROM tmp2); +------+ | num1 | +------+ | 1 | | 10 | | 55 | | 123 | +------+ 4 rows in set (0.01 sec) # 当子查询返回结果集为空时,外层查询不进行查询 mysql> SELECT * FROM tmp1 WHERE EXISTS (SELECT * FROM tmp2 WHERE num2 > 1000); Empty set (0.00 sec)
更多子查询相关内容,请查看:Subqueries
-
连接查询:就是将两张表依据某个条件(连接条件)进行数据拼接的过程。
注:连接实质上就是将两张表的字段拼接成一张表,然后以一定的条件过滤该表,得到结果集。
连接是关系数据库模型的主要特点,连接的过程就是将不同实体信息整合的过程。在具体介绍连接查询前,先创建两张示例表,如下所示:
mysql> CREATE TABLE article ( # 文章表 -> id INT PRIMARY KEY AUTO_INCREMENT, -> title VARCHAR(50) NOT NULL COMMENT 'title of article', -> content TEXT COMMENT 'content of article', -> pubTime TIMESTAMP COMMENT 'publish date' -> ); Query OK, 0 rows affected (2.29 sec) mysql> CREATE TABLE comment ( # 评论表 -> id INT PRIMARY KEY AUTO_INCREMENT, -> content TINYTEXT COMMENT 'content of comment', -> article_id INT NOT NULL, -> FOREIGN KEY(article_id) REFERENCES article(id) # 外键约束 -> ); Query OK, 0 rows affected (4.16 sec) mysql> INSERT INTO article (title, content) VALUES # 插入值 -> ('first article','content one'), -> ('second article', 'content two'), -> ('third article','content three'); Query OK, 3 row affected (0.24 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> INSERT INTO comment (content, article_id) VALUES # 插入值 -> ('comment 1',1), -> ('comment 11',1), -> ('comment 3',3); Query OK, 3 rows affected (0.22 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM article; # 查表 +----+----------------+---------------+---------+ | id | title | content | pubTime | +----+----------------+---------------+---------+ | 1 | first article | content one | NULL | | 2 | second article | content two | NULL | | 3 | third article | content three | NULL | +----+----------------+---------------+---------+ 3 rows in set (0.00 sec) mysql> SELECT * FROM comment; # 查表 +----+------------+------------+ | id | content | article_id | +----+------------+------------+ | 1 | comment 1 | 1 | | 2 | comment 11 | 1 | | 3 | comment 3 | 3 | +----+------------+------------+ 3 rows in set (0.00 sec)
上面我们创建了两张表:文章表
article
和评论表comment
,并为各自填充了一些数据,每篇文章对应一条或多条评论。连接查询是关系数据库中最主要的查询,具体可细分为如下三种连接查询:
-
交叉连接(Cross Join):指不需要连接条件的多表连接。其语法如下所示:
# 格式一:隐式交叉连接 SELECT column1[,...,columnn] from table_name1,table_name2[,...table_namen] # 格式二:显示交叉连接 left_table [CROSS] JOIN right_table
举个例子:交叉查询表
article
和comment
:mysql> SELECT * FROM article AS a, COMMENT AS c ORDER BY a.id, c.id; # 隐式写法 +----+----------------+---------------+---------+----+------------+------------+ | id | title | content | pubTime | id | content | article_id | +----+----------------+---------------+---------+----+------------+------------+ | 1 | first article | content one | NULL | 1 | comment 1 | 1 | | 1 | first article | content one | NULL | 2 | comment 11 | 1 | | 1 | first article | content one | NULL | 3 | comment 3 | 3 | | 2 | second article | content two | NULL | 1 | comment 1 | 1 | | 2 | second article | content two | NULL | 2 | comment 11 | 1 | | 2 | second article | content two | NULL | 3 | comment 3 | 3 | | 3 | third article | content three | NULL | 1 | comment 1 | 1 | | 3 | third article | content three | NULL | 2 | comment 11 | 1 | | 3 | third article | content three | NULL | 3 | comment 3 | 3 | +----+----------------+---------------+---------+----+------------+------------+ 9 rows in set (0.00 sec) mysql> SELECT * FROM article AS a CROSS JOIN comment AS c ORDER BY a.id, c.id; # 显示写法 +----+----------------+---------------+---------+----+------------+------------+ | id | title | content | pubTime | id | content | article_id | +----+----------------+---------------+---------+----+------------+------------+ | 1 | first article | content one | NULL | 1 | comment 1 | 1 | | 1 | first article | content one | NULL | 2 | comment 11 | 1 | | 1 | first article | content one | NULL | 3 | comment 3 | 3 | | 2 | second article | content two | NULL | 1 | comment 1 | 1 | | 2 | second article | content two | NULL | 2 | comment 11 | 1 | | 2 | second article | content two | NULL | 3 | comment 3 | 3 | | 3 | third article | content three | NULL | 1 | comment 1 | 1 | | 3 | third article | content three | NULL | 2 | comment 11 | 1 | | 3 | third article | content three | NULL | 3 | comment 3 | 3 | +----+----------------+---------------+---------+----+------------+------------+ 9 rows in set (0.00 sec)
可以看到,交叉连接产生的结果是笛卡尔积(即两张表记录乘积),这种结果不具备实际数据价值,因此通常不会使用到交叉查询。
-
内连接(Inner Join):指将两张表依据一定的条件连接起来。其语法如下:
# 格式一:WHERE 子句 SELECT column1[,...,columnn] from table_name1,table_name2[,...table_namen] WHERE where_condition # 格式二:使用 ON 子句(建议) left_table [INNER] JOIN right_table ON search_condition # 格式三:使用 using 子句 left_table [INNER] JOIN right_table USING(column_name1 [,...,column_namen])
注:使用内连接查询通常而言两张表内具备相同的字段(通常为外键),可以依据该字段连接两张表,且只有两张表内同时存在的值才会被提取出来。
注:使用
WHERE
子句的内连接定义连接条件比较简单明了,但INNER JOIN
语法是标准的 ANSI SQL 标准规范,且查询效率更高,推荐使用ON
子句进行内连接查询。举个例子:表
comment
的外键字段article_id
指向表article
的主键id
,因此这两个字段是同一字段,现在通过该字段内连两个表,查看下结果(其实就是查询拥有评论的文章列表):# 传统查询 mysql> SELECT title, c.content as comment FROM article AS a, comment AS c WHERE c.article_id = a.id; +---------------+------------+ | title | comment | +---------------+------------+ | first article | comment 1 | | first article | comment 11 | | third article | comment 3 | +---------------+------------+ 3 rows in set (0.00 sec) # 使用 ON 子句查询 mysql> SELECT title, c.content comment FROM article a INNER JOIN comment c ON c.article_id = a.id; +---------------+------------+ | title | comment | +---------------+------------+ | first article | comment 1 | | first article | comment 11 | | third article | comment 3 | +---------------+------------+ 3 rows in set (0.00 sec) # 使用 USING 子句查询 mysql> SELECT c.article_id AS id, title, c.content comment FROM article INNER JOIN comment c USING(id); +----+----------------+------------+ | id | title | comment | +----+----------------+------------+ | 1 | first article | comment 1 | | 1 | second article | comment 11 | | 3 | third article | comment 3 | +----+----------------+------------+ 3 rows in set (0.00 sec)
注:
USING
关键字要求连接查询必须使用同名字段(即using(id) == left_table.id = right_table.id
),不过通过借助为字段取别名可以绕过这一限制。
注:由于存在多个表,当表名称比较长时,可以为表取一个别名,简化调用。 -
外连接:有时候需要包含没有关联的行数据,则此时可以使用外连接。
内连接查询时,只会返回符合查询条件和连接条件的行数据。而有时候可能需要返回与另一张表中没有关联的数据,即查询结果集中不仅包含符合条件的行数据,还包含左表(左外连接)、右表(右外连接)或两个边表(全外连接)中的所有数据行。
在 MySQL 中,只支持左外连接和右外连接,具体如下:
注:要在 MySQL 中实现全外连接,可以借助使用组合查询(UNION
)即可。-
左(外)连接(Left Join):即不仅返回符合查询条件的数据行,同时返回左表所有数据行(对于不符合连接条件的记录,将其对应的右表字段内容设置
NULL
)。
其语法如下所示:left_table LEFT [OUTER] JOIN right_table
举个例子:比如想查看表
article
的所有内容,包含标题,正文和评论,那么就可以使用左外连接:mysql> SELECT title, a.content, c.content comment FROM article a LEFT OUTER JOIN comment c ON a.id = c.article_id; +----------------+---------------+------------+ | title | content | comment | +----------------+---------------+------------+ | first article | content one | comment 1 | | first article | content one | comment 11 | | second article | content two | NULL | # 第二篇文章没有评论 | third article | content three | comment 3 | +----------------+---------------+------------+ 4 rows in set (0.00 sec)
-
右(外)连接(Right Join):即不仅返回符合查询条件的数据行,同时返回右表所有的数据行。其语法如下所示:
left_table RIGHT [OUTER] JOIN right_table
举个例子:比如想查看评论对应的文章(包含没有评论的文章)内容,那么就可以使用右外连接:
mysql> SELECT title, a.content, c.content comment FROM comment c RIGHT OUTER JOIN article a ON c.article_id = a.id; +----------------+---------------+------------+ | title | content | comment | +----------------+---------------+------------+ | first article | content one | comment 1 | | first article | content one | comment 11 | | second article | content two | NULL | | third article | content three | comment 3 | +----------------+---------------+------------+ 4 rows in set (0.00 sec)
注:该例子其实与上述左外连接例子相同,只是调换了左表和右表角色。
-
-
-
-
最后,一个SELECT
语句中,其子句结构具备严格的固定顺序,如下所示:
SELECT DISTINCT
FROM JOIN
ON
WHERE # 行级过滤
GROUP BY # 分组
WITH { CUBE | ROLLUP } # 分组汇总
HAVING # 分组过滤
ORDER BY
LIMIT
存储引擎
数据库对数据进行增删改查底层是通过存储引擎进行的,不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能。
MYSQL 支持多种不同的存储引擎,可以通过命令SHOW ENGINES
查看系统支持的所有存储引擎:
mysql> SHOW ENGINES;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.07 sec)
可以看到,MySQL 支持的存储引擎有:FEDERATED
、MEMORY
、InnoDB
、PERFORMANCE_SCHEMA
、MyISAM
、MRG_MYISAM
、BLACKHOLE
、CSV
和ARCHIVE
。其中:各种存储引擎最明显的区别是是否支持事务,MySQL 8.0 中默认的存储引擎为InnoDB
,且其是 MySQL 唯一一个支持事务的内置存储引擎。
下面介绍下 MySQL 中最常用的两种存储引擎:
-
InnoDB
:InnoDB
支持事务操作,支持行锁定和外键约束,是事务型数据库的首选存储引擎。其主要特性如下所示:- 支持事务机制,具备提交、回滚和奔溃恢复能力。
- 支持大数据量存储,性能十分高效,其 CPU 效率基本优于其余所有基于磁盘的关系数据库引擎。
- 支持外键完整性约束(
FOREIGN KEY
)。 - 存储数据时,每张表的存储都按照主键顺序存放。如果定义表时没有显示指定主键,则
InnoDB
会自动为每一行生成一个6B
的ROWID
,并以此作为主键。
-
MyISAM
:MyISAM
是基于ISAM
的扩展实现,其拥有较高的插入、查询速度,但不支持事务。其主要特性如下所示:- 支持在大文件系统上进行使用。
-
BLOB
和TEXT
列可以被索引。 - 索引支持
NULL
值,该值占用 0~1 个字节空间。 - 所有数字键值以高字节优先被存储,以允许一个更高的索引压缩。
- 每个字符列可以使用不同的字符集。
-
CHAR
和VARCHAR
类型最大值可达 64KB。
其他存储引擎也拥有各自的特性,比如MEMORY
引擎支持将表中数据存储到内存中,使得其具备高效快速的查询能力...
不同的存储引擎有不同的特点,实际使用中应结合业务场景,选择使用合适的存储引擎。
以下是几种常用存储引擎的横向比较,方便查看各引擎的特点:
特性 | InnoDB | MyISAM | MEMORY | MERGE | BerkeleyDB |
---|---|---|---|---|---|
存储限制 | 64TB | 有 | 有 | 有 | |
事务支持 | 支持 | ||||
锁机制 | 行锁 | 表锁 | 表锁 | 表锁 | 行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 |
哈希索引 | 支持 | ||||
全文索引 | 支持 | 支持 | |||
集群索引 | 支持 | ||||
数据缓存 | 支持 | 支持 | 支持 | ||
索引缓存 | 支持 | 支持 | 支持 | 支持 | 支持 |
数据压缩 | 支持 | ||||
空间使用 | 高 | 低 | N/A | 低 | 低 |
内存使用 | 高 | 低 | 中等 | 低 | 高 |
批量插入速度 | 低 | 高 | 高 | 高 | 高 |
外键支持 | 支持 |
事务
关系型数据库中,可以使用 事务(transaction) 来维护数据库完整性,它能保证事务范围内的成批操作要么全部执行,要么全部不执行(除非明确指示)。
简单来说,事务的作用就是使得多条 SQL 语句的操作具备原子性。
在事务的语义下,执行多条 SQL 语句就相当于执行一条 SQL 语句。而当事务中的某条 SQL 语句操作失败时,它会回滚之前成功的操作,使得数据库与未执行该事务前的状态保持一致,维护了数据的完整性。
保证事务可靠性的理论基础是事务具备的四大特性:ACID,具体内容如下:
Atomicity(原子性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个环节结束。
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏,即数据库总是从一个一致性的状态转换到另一个一致性的状态。
注:一致性保证了当事务中间环节出错时,前面执行的语句也不会生效(回滚)。Isolation(隔离性):数据库支持多个并发事务同时对数据进行读写和修改,一个事务所做的修改在最终提交之前,对其他事务是不可见的。
注:由于并发读写可能导致数据的不一致性,为了在不同程度上尽量避免这个问题,数据库提供了几种隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。Durability(持久性):事务完成提交后,对数据的修改就是永久的,即便系统故障也不会丢失。
下面介绍下数据库的事务隔离级别,在介绍之前,先创建一个示例数据表account
,表示一个银行账户:
mysql> CREATE TABLE account (
-> id BIGINT PRIMARY KEY AUTO_INCREMENT,
-> name VARCHAR(20) NOT NULL DEFAULT 'anonymous',
-> money DECIMAL(10,5) NOT NULL DEFAULT 0
-> )ENGINE=InnoDB;
Query OK, 0 rows affected (2.18 sec)
注:MySQL 中只有InnoDB
支持事务。
当多个事务同时操作同一条记录时,由于并发操作可能会造成数据的不一致性,包括脏读、不可重复读、幻读等。也因此,为了在最大可能上保证数据的一致性,数据库提供了以下几种隔离级别:
-
未提交读(Read Uncommitted):在该隔离级别下,可能会遇到脏读(Dirty Read)问题,即当前隔离级别的事务可以读取到其他(任意隔离级别)事务更新后但未提交的数据,如果此时其他事务进行回滚操作,那么当前事务再次读取获得的值就可能与上一次不一致。
注:Read Uncommitted
是隔离级别最低的一种事务级别。举个例子:如下表顺序所示,首先为表添加一条数据,然后分别开启两个 MySQL 客户端,依下表
account
顺序同时执行事务1和事务2:时序 事务1 事务2 1 INSERT INTO account(name, money) VALUES('lisi', 100);
2 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3 BEGIN;
BEGIN;
4 UPDATE account SET MONEY = 200 WHERE name = 'lisi';
5 SELECT * FROM ACCOUNT WHERE name = 'lisi';
# 2006 ROLLBACK;
7 SELECT * FROM account WHERE name = 'lisi';
# 1008 COMMIT;
-
已提交读(Read Committed):在该隔离级别下,可能会遇到不可重复读(Non Repeatable Read)问题,即当前事务多次读取同一数据,当第二次读取时,此时恰好另一个事务修改了这个数据并进行提交,则当前事务读取的数据就可能与第一次的数据不一致。如下示例所示:
时序 事务1 事务2 1 SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
2 BEGIN;
BEGIN;
3 SELECT * FROM account WHERE name = 'lisi';
# 1004 UPDATE account SET money = 200 WHERE name = 'lisi';
5 COMMIT;
6 SELECT * FROM account WHERE name = 'lisi';
# 2007 COMMIT;
将事务隔离级别设置为
READ COMMITTED
后,只有在其他(任意级别)事务进行提交后,修改的数据才会被当前事务读取到,这样就避免了脏读。 -
可重复读(Repeatable Read):在该种隔离级别下,可能会遇到幻读(Phantom Read)问题,即在当前隔离级别事务下,第一次查询某条记录,发现不存在,此时如果有其他事务提交该记录,则再一次进行查询,仍然查询不到,但是对这条“不存在”的记录进行修改时,却能修改成功,并且再次进行查询,就可以查看到修改过的这条记录。如下所示:
时序 事务1 事务2 1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
2 BEGIN
BEGIN
3 SELECT * FROM account WHERE name = 'zhangsan';
# empty set4 INSERT INTO account(name, money) VALUES('zhangsan', 100);
5 COMMIT;
6 SELECT * FROM account WHERE name = 'zhangsan';
# empty set7 UPDATE account SET money = 200 WHERE name = 'zhangsan';
8 SELECT * FROM account WHERE name = 'zhangsan';
# zhangsan 2009 COMMIT;
将事务隔离级别设置为
REPEATABLE READ
后,此时即使其他事务提交修改已存在的数据或添加新数据,当前事务都无法感知得到,REPEATABLE READ
在同一个事务内的查询结果都与事务开始时刻一致,这种隔离级别解决了不可重复读问题(同一事务多次读取数据可能不一致)。注:
REPEATABLE READ
隔离级别相当于在开始事务时,对表进行了一个缓存操作,后续在该事务内进行的操作都基于缓存数据,因此实际表添加或修改的内容不会影响到缓存数据。注:MySQL 默认的数据库事务隔离级别为
REPEATABLE READ
,具体可通过如下命令进行查询:mysql> SELECT @@GLOBAL.transaction_isolation as global, @@SESSION.transaction_isolation as session; +-----------------+-----------------+ | global | session | +-----------------+-----------------+ | REPEATABLE-READ | REPEATABLE-READ | +-----------------+-----------------+ 1 row in set (0.00 sec)
串行化(Serializable):串行化是数据库中最严格的隔离级别,在该种级别下,所有的事务依次进行执行,因此不会出现脏读、不可重复读 和 幻读现象,但是由于事务是串行执行的,因此效率非常低。
总结一下,数据库隔离级别之间的关系如下表所示:
隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read Uncommitted |
最低级别 | 是 | 是 | 是 |
Read Committed |
语句级 | 否 | 是 | 是 |
Repeatable Read |
事务级 | 否 | 否 | 是 |
Serializable |
最高级别,事务级 | 否 | 否 | 否 |
数据库中,对事务的操作主要包含如下几方面:
-
开始事务:开启一个事务可以使用
START TRANSACTION
或BEGIN
语句,其语法具体如下所示:START TRANSACTION [transaction_characteristic [, transaction_characteristic] ...] transaction_characteristic: { WITH CONSISTENT SNAPSHOT | READ WRITE | READ ONLY } BEGIN [WORK]
-
提交事务:指将事务执行结果写入到数据表中,其使用的命令为
COMMIT
,具体语法如下所示:COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
对于单条 SQL 语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
而如果使用START TRANSACTION
或BEGIN
开启且手动使用COMMIT
进行提交的事务,则称为显示事务。
注:默认情况下,MySQL 会自动提交所有更改,即隐式事务会自动提交。可以通过设置autocommit
标志来更改提交行为:SET autocommit = {0 | 1}
其中:
0
表示禁止自动提交,1
表示使能自动提交(默认为1
)。
注:当禁止自动提交时,对表进行的修改不会自动进行持久化(但是可以查询得到),必须使用COMMIT
来将数据持久化到磁盘上或ROLLBACK
撤销修改。如下例子所示:# 禁止自动提交 mysql> SET autocommit = 0; Query OK, 0 rows affected (0.00 sec) # 插入新数据 mysql> INSERT INTO account(name, money) VALUES('wangwu',100); Query OK, 1 row affected (0.00 sec) # 查询(可以获取到新数据) mysql> SELECT * FROM account WHERE name = 'wangwu'; +----+--------+-----------+ | id | name | money | +----+--------+-----------+ | 6 | wangwu | 100.00000 | +----+--------+-----------+ 1 row in set (0.00 sec) # 回滚 mysql> ROLLBACK; Query OK, 0 rows affected (0.09 sec) # 插入的新数据已被撤销 mysql> SELECT * FROM account WHERE name = 'wangwu'; Empty set (0.00 sec) # 恢复自动提交 mysql> set AUTOCOMMIT = 1; Query OK, 0 rows affected (0.00 sec)
-
回滚事务:当事务中某环节出错时,可进行回滚操作,撤销之前执行成功的操作。
回滚事务使用的命令为ROLLBACK
,其具体语法如下所示:ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
注:
ROLLBACK
语句默认回滚START TRANSACTION
或BEGIN
语句之间的操作,可以用来管理INSERT
、UPDATE
和DELETE
操作,但是不能回退SELECT
操作(因为这没有意义),也不能回退CREATE
或DROP
操作。 -
占位符:为了更加细粒度的控制事务回滚过程,可以通过在事务处理块中适当位置插入一个占位符,然后使用
ROLLBACK TO
回退到指定占位符中。
占位符的相关操作命令为SAVEPOINT
,其语法如下所示:# 创建占位符 SAVEPOINT identifier # 回退到占位符处 ROLLBACK [WORK] TO [SAVEPOINT] identifier # 释放占位符 RELEASE SAVEPOINT identifier
使用占位符的一个好处就是可以更加细粒度的控制事务处理过程,对于一些非关键位置的错误,完全可以通过占位符进行捕获回退,避免事务退出。
举个例子:现在假设表account
中用户lisi
转账 100 元给zhangsan
,由于银行转账过程涉及不同用户间金额计算,这个操作过程要求具备原子性,因此使用事务进行处理,确保数据的一致性:
# 初始数据
mysql> SELECT * FROM account;
+----+----------+-----------+
| id | name | money |
+----+----------+-----------+
| 1 | lisi | 200.00000 |
| 5 | zhangsan | 200.00000 |
+----+----------+-----------+
2 rows in set (0.00 sec)
# 开启事务
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
# lisi - 100
mysql> UPDATE account SET money = money - 100 WHERE name = 'lisi';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# zhangsan + 100
mysql> UPDATE account SET money = money + 100 WHERE name = 'zhangsan';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 提交事务
mysql> COMMIT;
Query OK, 0 rows affected (0.14 sec)
# 查看事务执行结果
mysql> SELECT * FROM account;
+----+----------+-----------+
| id | name | money |
+----+----------+-----------+
| 1 | lisi | 100.00000 |
| 5 | zhangsan | 300.00000 |
+----+----------+-----------+
2 rows in set (0.01 sec)