数据库(一):MySQL
MySQL 是一款开源免费的关系型数据库,特点如下:
- 每个表都是独立的
- 表和表之间通过公共字段来建立关系
下载地址:MySQL Community Downloads
- 自定义
- 版本号
- 路径
- 账号/密码
- 环境变量
基本使用
通常情况下,MySQL 被安装在远程服务器上,所以必须连接到远程服务器并登录,才可以使用 MySQL,格式如下:
mysql -h <serverIP> -P<port> -u<username> -p<password>
示例如下:
mysql -h 127.0.0.1 -P3306 -uroot -p1234
如果 MySQL 服务器在本地,且默认使用 3306 端口号,那么主机地址(-h)和端口号(-P)可以省略,示例如下:
mysql -uroot -p1234
如果在参数 -p 之后未输入任何内容,那么将以隐藏密码的方式登录,示例如下:
mysql -uroot -p
综上所述,最常用的登录远程 MySQL 服务器(端口号:3306)时的指令格式如下:
mysql -h <serverIP> -uroot -p
此外,可以通过
exit;
、quit
和\q
中的任何一个指令退出 MySQL 连接
在登录 MySQL 数据库后,可以通过 show databases;
指令查看所有数据库,输出内容如下:
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)
以上所有的数据库均为系统数据库,内容如下所示:
数据库 | 内容 |
---|---|
information_schema | MySQL 服务器所维护的所有另外数据库的信息 |
mysql | MySQL 服务器的用户名、密码和权限等信息 |
performance_schema | MySQL 服务器性能相关的参数 |
sys | 以视图的形式将 info_schema 和 perf_schema 合并,从而使数据更容易让人理解 |
基本概念
任何一个数据库的核心内容即为增删改查(CRUD)
- 数据库
- 表
- 字段、记录
MySQL 利用 SQL(Structured Query Language)语言实现此功能
SQL 语言是关系型数据库的通用语言,各个厂商通常会在标准 SQL 的基础之上扩展独特的内容,例如 SQL Server 实际上使用的是在 SQL 基础之上扩展的 T-SQL
SQL 语句以功能可以划分为如下类别:
功能 | 名称 | 含义 |
---|---|---|
数据定义 | DDL | 定义实例(库、表、字段等) |
数据操纵 | DML | 操纵记录 |
数据查询 | DQL | 查询数据 |
数据控制 | DCL | 访问权限、安全级别 |
SQL 语言中常用的数据类型如下所示:
数据类型 | 含义 |
---|---|
double | 浮点数 |
char | 固定长度字符串 |
varchar | 可变长度字符串 |
text | 字符串 |
blob | 二进制 |
date | 日期 |
time | 时间 |
datetime | 日期/时间 |
数据库
创建
格式如下:
create database [if not exists] <DBName> [charset=<utf8/gbk>];
示例如下:
create database db01;
上述示例为创建数据库最简单的方式,缺点在于如果创建已存在的数据库,那么将出现错误,此时可以使用 if not exists 约束,示例如下:
create database if not exists db01;
输出内容如下:
Query OK, 1 row affected, 1 warning (0.01 sec)
上述示例的含义是如果存在
db01
数据库,那么自动忽略此语句,如果不存在则创建数据库,此外,在创建数据库时可以指定字符集,默认的字符集为utf8
,示例如下:
create database if not exists db02 charset='gbk';
此时可以通过如下语句查看字符集:
show create database db02;
输出内容如下:
+----------+-------------------------------------------------------------------------------------------------+
| Database | Create Database |
+----------+-------------------------------------------------------------------------------------------------+
| db02 | CREATE DATABASE `db02` /*!40100 DEFAULT CHARACTER SET gbk */ /*!80016 DEFAULT ENCRYPTION='N' */ |
+----------+-------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)
另外,如果创建数据库时以关键字或特殊字符命名,那么必须将数据库名称以 ` 字符包围,示例如下:
create database `create`;
查询
查看数据库的格式如下:
- 所有数据库
show databases;
- 某个数据库
show create database <DBName>;
示例如下:
show create database db01;
输入内容如下:
+----------+--------------------------------------------------------------------------------------------------------------------------------+
| Database | Create Database |
+----------+--------------------------------------------------------------------------------------------------------------------------------+
| db01 | CREATE DATABASE `db01` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */ |
+----------+--------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
删除
格式如下:
drop database [if exists] <DBName>;
示例如下:
drop database db02;
类似于创建数据库,if exists 表示如果待删除数据库存在则删除,否则自动忽略此语句,示例如下:
drop database if exists db02;
输出内容如下:
Query OK, 0 rows affected, 1 warning (0.01 sec)
修改
MySQL 中可以修改数据库的字符集,格式如下:
alter database <DBName> charset=<utf8/gbk>;
示例如下:
alter database db01 charset=gbk;
表
数据库中的每一张表都存储在某个具体的数据库中,所以在操作某个表之前,必须切换到表所在的数据库,MySQL 中切换数据库的格式如下:
use <DBName>;
示例如下:
use db01;
输出内容如下:
Database changed
创建
格式如下:
create table [if not exists] <TableName> (
FieldName DataType,
FieldName DataType,
FieldName DataType,
...
FieldName DataType,
FieldName DataType
);
if not exists 含义表示如果当前数据库中不存在待创建的表则创建,否则跳过此语句
示例如下:
create table if not exists student (
id int,
name text
);
必须注意的是,最后一个字段末尾不能写
,
字符,此时若想查看创建的表,可以通过如下命令:
show tables;
示例如下:
show tables;
输入内容如下:
+----------------+
| Tables_in_db01 |
+----------------+
| student |
+----------------+
1 row in set (0.00 sec)
存储引擎
在 MySQL 中创建表时,可以指定存储引擎类型,格式如下:
create table <TableName> (
FieldName DataType,
FieldName DataType,
...
FieldName DataType
) engine=<EngineName>;
示例如下:
create talbe stu (
id int,
name text
) engine=MyISAM;
MySQL 中存储引擎的相关内容如下所示:
存储引擎 | 特点 |
---|---|
MyISAM | 安全性低、不提供事务和外键 |
InnoDB | 安全性高、提供事务和外键 |
Memory | 访问速度极快、不会永久存储数据 |
默认情况下,表的存储引擎为 InnoDB,实际上,数据库的本质是一个文件,在创建数据库时,系统将自动创建一个目录,在数据库中创建表时,系统将自动创建一系列文件,不同的存储引擎有不同的文件结构:
- InnoDB
- 如果以 InnoDB 为存储引擎,那么在创建表时系统将自动创建以 .ibd 为扩展名的文件,此文件保存了表的结构
- 如果向此表中存储数据,那么数据将被存储在名称为 ibdata1 的文件中,如果数据较多,系统将自动创建 ibdata2 以存储更多的数据
- MyISAM
- 如果以 MyISAM 为存储引擎,那么在创建表时系统将自动创建如下文件:
- .sdi
- 表的结构
- .MYD
- 表的数据
- .MYI
- 表的索引
- .sdi
- 如果以 MyISAM 为存储引擎,那么在创建表时系统将自动创建如下文件:
- Memory
- 如果以 Memory 为存储引擎,那么在创建表时将自动创建以 .sdi 为扩展名的文件,此文件保存了表的结构
- 必须注意的是,以 Memory 为存储引擎的表的数据不会保存到文件中,而是将直接被保存到内存中,此外,Memory 存储引擎不支持 text/blob 数据类型
查询
除了可以查询当前数据库中存在哪些表,也可以查看表中的具体结构,格式如下:
desc <TableName>;
示例如下:
desc student;
输出内容如下:
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
+-------+------+------+-----+---------+-------+
2 rows in set (0.01 sec)
删除
格式如下:
drop table [if exists] <TableName>;
示例如下:
drop table if exists student;
此时数据库中已不存在表,查看表后的内容如下:
Empty set (0.00 sec)
修改
数据库 db01 最初的样式如下所示:
+----------------+
| Tables_in_db01 |
+----------------+
| student |
+----------------+
1 row in set (0.00 sec)
表 student 最初的样式如下所示:
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
+-------+------+------+-----+---------+-------+
2 rows in set (0.00 sec)
- 修改表名
格式如下:
rename table <OriginalTableName> to <NewTableName>;
示例如下:
rename table student to person;
输出内容如下:
+----------------+
| Tables_in_db01 |
+----------------+
| person |
+----------------+
1 row in set (0.00 sec)
- 添加字段
格式如下:
alter table <TableName> add <NewFieldName> <NewFieldDataType> [Location];
示例如下:
alter table person add age int;
输出内容如下:
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
| age | int | YES | | NULL | |
+-------+------+------+-----+---------+-------+
3 rows in set (0.00 sec)
默认情况下,新添加的字段位于表的末尾,如果让新增字段位于第一行,可以设置可选参数 Location 为 first 实现,示例如下:
alter table person add score float first;
输出内容如下:
+-------+-------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------+------+-----+---------+-------+
| score | float | YES | | NULL | |
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
| age | int | YES | | NULL | |
+-------+-------+------+-----+---------+-------+
4 rows in set (0.00 sec)
如果让新增字段位于某一行之后,可以设置可选参数 Location 为 after (FieldName) 实现,示例如下:
alter table person add phone int after name;
输出内容如下:
+-------+-------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------+------+-----+---------+-------+
| score | float | YES | | NULL | |
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
| phone | int | YES | | NULL | |
| age | int | YES | | NULL | |
+-------+-------+------+-----+---------+-------+
5 rows in set (0.00 sec)
- 删除字段
格式如下:
alter table <TableName> drop <FieldName>;
示例如下:
alter table person drop phone;
输出内容如下:
+-------+-------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------+------+-----+---------+-------+
| score | float | YES | | NULL | |
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
| age | int | YES | | NULL | |
+-------+-------+------+-----+---------+-------+
4 rows in set (0.00 sec)
-
修改字段
- 数据类型
格式如下:
alter table <TableName> modify <FieldName> <NewDataType>;
示例如下:
alter table person modify score double;
输出内容如下:
+-------+--------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+--------+------+-----+---------+-------+ | score | double | YES | | NULL | | | id | int | YES | | NULL | | | name | text | YES | | NULL | | | age | int | YES | | NULL | | +-------+--------+------+-----+---------+-------+ 4 rows in set (0.00 sec)
- 名称 & 数据类型
格式如下:
alter table <TableName> change <OriginalFieldName> <NewFieldName> <NewDataType>;
示例如下:
alter table person change age address text;
输出内容如下:
+---------+--------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +---------+--------+------+-----+---------+-------+ | score | double | YES | | NULL | | | id | int | YES | | NULL | | | name | text | YES | | NULL | | | address | text | YES | | NULL | | +---------+--------+------+-----+---------+-------+ 4 rows in set (0.00 sec)
-
修改引擎
格式如下:
alter table <TableName> engine=<EngineName>;
示例如下:
alter table person engine=MyISAM;
记录
记录相关的表的结构如下所示:
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | text | YES | | NULL | |
| age | int | YES | | NULL | |
+-------+------+------+-----+---------+-------+
3 rows in set (0.00 sec)
创建
格式如下:
insert into <TableName> (FieldName-1, FieldName-2, ..., FieldName-N) values (Value-1, Value-2, ..., Value-N) ... (Value-1, Value-2, ..., Value-N);
示例如下:
insert into student (id, name, age) values (1, 'reyn', 21);
此时若想查看创建的记录,可以通过如下命令:
select * from student;
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 1 | reyn | 21 |
+------+------+------+
1 row in set (0.00 sec)
实际上,在创建记录时,若添加数据时的字段顺序和表的结构相同,那么可以省略字段说明,示例如下:
insert into student values (2, 'lily', 20);
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 1 | reyn | 21 |
| 2 | lily | 20 |
+------+------+------+
2 rows in set (0.00 sec)
此外,在创建记录时,可以同时添加多条记录,示例如下:
insert into student values (3, 'jack', 22), (4, 'jhon', 21);
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 1 | reyn | 21 |
| 2 | lily | 20 |
| 3 | jack | 22 |
| 4 | jhon | 21 |
+------+------+------+
4 rows in set (0.00 sec)
实际上,在创建记录时,字段的说明顺序可以和表的结构不一致,但记录值的顺序必须和字段的说明顺序一致,示例如下:
insert into student (name, age, id) values ('jobs', 19, 5);
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 1 | reyn | 21 |
| 2 | lily | 20 |
| 3 | jack | 22 |
| 4 | jhon | 21 |
| 5 | jobs | 19 |
+------+------+------+
5 rows in set (0.00 sec)
若不一致将出现错误,示例如下:
insert into student (name, age, id) values (6, 'miles', 25);
输出内容如下:
ERROR 1366 (HY000): Incorrect integer value: 'miles' for column 'age' at row 1
查询
除了可以查询一个表中的所有数据之外,也可以查询某个或某些在具体条件下的特定字段的取值,格式如下:
select (FieldName-1, FieldName-2, ... FieldName-N) from <TableName> [where <Condition>]
示例如下:
select name, age from student;
输出内容如下:
+------+------+
| name | age |
+------+------+
| reyn | 21 |
| lily | 20 |
| jack | 22 |
| jhon | 21 |
| jobs | 19 |
+------+------+
5 rows in set (0.00 sec)
实际上,可以通过 where 选项附加条件以查询更加准确的内容,条件通常由字段名称和取值组成,由关键字和逻辑运算符连接,如下所示:
连接符 | 含义 |
---|---|
= | 等于 |
!= | 不等于 |
<> | 不等于 |
< | 小于 |
<= | 小于等于 |
> | 大于 |
>= | 大于等于 |
in(...) | 是否在特定集合中 |
between ... and ... | 是否在某个区间 |
and | 逻辑与 |
or | 逻辑或 |
not | 逻辑非 |
like | 模糊查询 |
is null | 空 |
is not null | 非空 |
查询原理是遍历表中的所有记录,判断记录是否满足 where 附加条件,如果结果为真,那么将记录添加到查询结果,示例如下:
- 查询姓名为 reyn 的学生
select * from student where name='reyn';
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 1 | reyn | 21 |
+------+------+------+
1 row in set (0.00 sec)
- 查询年龄大于等于 20 岁的学生姓名
select name from student where age>=20;
输出内容如下:
+------+
| name |
+------+
| reyn |
| lily |
| jack |
| jhon |
+------+
4 rows in set (0.04 sec)
- 查询年龄在 18 到 21 岁的所有学生姓名及年龄
select name,age from student where age between 18 and 21;
输出内容如下:
+------+------+
| name | age |
+------+------+
| reyn | 21 |
| lily | 20 |
| jhon | 21 |
| jobs | 19 |
+------+------+
4 rows in set (0.01 sec)
- 查询年龄为 21 且姓名为 jhon 的学生
select * from student where age=21 and name='jhon';
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 4 | jhon | 21 |
+------+------+------+
1 row in set (0.00 sec)
- 查询年龄为 19 或 20 的所有学生姓名及年龄
select name,age from student where age in(19, 20);
输出内容如下:
+------+------+
| name | age |
+------+------+
| lily | 20 |
| jobs | 19 |
+------+------+
2 rows in set (0.00 sec)
通过逻辑或运算符也可以实现此查询,示例如下:
select name,age from student where age=19 or age=20;
删除
格式如下:
delete from <TableName> [where <Condition>]
示例如下:
delete from student where age>20;
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 2 | lily | 20 |
| 5 | jobs | 19 |
+------+------+------+
2 rows in set (0.00 sec)
实际上,如果不通过 where 附加条件,那么将删除表中的所有数据,示例如下:
delete from student;
修改
格式如下:
update <TableName> set <FieldName-1>=<Value-1>, ..., <FieldName-N>=<Value-N> [where <Condition>]
示例如下:
update student set age=21,name='reyn' where id=2;
输出内容如下:
+------+------+------+
| id | name | age |
+------+------+------+
| 2 | reyn | 21 |
| 5 | jobs | 19 |
+------+------+------+
2 rows in set (0.00 sec)
实际上,如果不通过 where 附加条件,那么将修改表中的所有记录,此处不再演示
数据类型
MySQL 为了更高效的保存数据,提供了多种数据类型,不同的数据类型占用空间大小不同,必须合理的使用数据类型,才能避免资源浪费,以提升数据库的性能
整数类型
整数类型专门用于存储整数,常用整数类型如下:
类型 | 大小(字节) |
---|---|
TINYINT | 1 |
SMALLINT | 2 |
MEDIUMINT | 3 |
INTEGER | 4 |
BIGINT | 8 |
在添加数据时,不能超出数据类型的范围,否则将出现错误,此外,MySQL 中的部分数据类型存在有符号和无符号之分,默认情况下整数类型即为有符号的,若要使用无符号类型,必须在数据类型之后添加 unsigned 关键,示例如下:
CREATE TABLE student ( stuId INTEGER ( 4 ), stuName VARCHAR ( 20 ), stuAge TINYINT UNSIGNED );
上述示例中,由于人的年龄不可能为负数,所以使用了 unsigned 关键字,此外,字段 stuId 的类型为 INT 类型,且指定了位宽,若添加数据时,数据的长度不足 4 位,那么在查询数据时,将在数据之前填充空字符或 0 以补足长度,若数据长度大于等于 4 位,那么将什么都不做,此处不再详细说明
浮点类型
浮点类型专门用于存储小数,常用浮点类型如下:
类型 | 大小(字节) |
---|---|
FLOAT | 4 |
DOUBLE | 8 |
必须注意的是,FLOAT 和 DOUBLE 类型不能用于存储精确的小数数据,二者最大的区别在于精度不同,FLOAT 的精度有 6 位,而 DOUBLE 类型的精度有 16 位,示例如下:
ALTER TABLE student ADD stuHeight DOUBLE ( 3, 2 );
实际上,可以通过小括号的形式指定存储数据的样式,第一个数字表示数据的总位数(不包括小数点),第二个数字表示数据小数的位数,所以上述示例的字段 stuHeight 将以类似于 1.72 的形式保存和显示数据
定点类型
定点类型专门用于存储精确小数,示例如下:
ALTER TABLE student MODIFY stuHeight DECIMAL ( 4, 2 );
实际上,定点类型将数据分为两个部分存储,每个部分都是一个整数,由于过于消耗资源,所以尽量不要滥用,上述示例中,小括号的作用和浮点类型中的括号相同,区别在于 DECIMAL 类型存储的小数一定是精确的
字符类型
字符类型专门用于存储字符,常用字符类型如下:
类型 | 大小(字节) |
---|---|
CHAR | 0 - 255 |
VARCHAR | 0 - 65535 |
CHAR 类型和 VARCHAR 类型的区别在于,如果使用 CHAR 类型,那么系统不会回收多余的字符,如果使用 VARCHAR 类型,那么系统将回收多余的字符,例如,如果通过 CHAR(2) 存储字符
a
,那么存储的结果是a
,如果通过 VARCHAR(2) 存储字符a
,那么存储的结果是a
,此处不再详细说明,此外,必须注意的是,上述表格中 VARCHAR 类型的最大值都是理论上的,VARCHAR 类型真正可以存储的字符数量并不是 65535,具体取决于表所使用的字符集,如果使用 utf8,那么一个字符占用 3 个字节,所以使用 VARCHAR 类型最多可以存储 21845 个字符,如果使用 gbk,那么一个字符占用 2 个字节,所以使用 VARCHAR 类型最多可以存储 32767 个字符
文本类型
由于 MySQL 中每一行所存储的数据存在大小限制,最多可以存储 65535 个字节,为了突破此限制,MySQL 提供了文本类型,专门用于存储长文本,常用文本类型如下:
类型 | 大小(字节) |
---|---|
TINYTEXT | 0 - 255 |
TEXT | 0 - 65535 |
MEDIUMTEXT | 0 - 16777215 |
LONGTEXT | 0 - 4294967295 |
实际上,以文本类型作为数据类型的字段仅占用了 10 个字节用于存储数据实际所在的空间地址,所以可以突破每一行所存储数据的大小限制
枚举类型
枚举类型适用于描述某个字段的值只可能是某个几个值中的一个,示例如下:
ALTER TABLE student ADD stuGender ENUM ( '男', '女' );
实际上,MySQL 中的枚举值本质上是从 1 开始一次递增的整数,例如,上述示例中的
'男'
以整数 1 的形式存储,而'女'
以整数 2 的形式存储,所以在向表中添加数据时,如果存在以枚举类型作为数据类型的字段,必须使用枚举类型中定义的值或其对应的整数,否则将出现错误,此处不再详细说明
集合类型
集合类型适用于描述某个字段的值只可能是某个几个值中的几个,示例如下:
ALTER TABLE student ADD stuHobby SET ( '体育', '游戏', '音乐' );
类似于枚举类型,集合类型中预定义的值同样以整数存储,例如,上述示例中的
'体育'
以整数 2^0(1) 的形式存储、'游戏'
以整数 2^1(2) 的形式存储、'音乐'
以整数 2^2(4) 的形式存储,所以在向表中添加数据时,如果存在以集合类型作为数据类型的字段,必须使用集合类型中定义的值或其对应的整数,否则将出现错误,此处不再详细说明
之所以采用 2 的次幂的形式保存集合中预定的值,原因在于可以简化操作,例如,如果某个学生的喜好包括游戏和音乐,那么在向表中添加数据时,可以以 6
代替 '游戏, 音乐'
,示例如下:
INSERT INTO student VALUES (5, 'Steven Jobs', 21, 1.68, '男', 6);
布尔类型
布尔类型专门用于存储真或假,示例如下:
ALTER TABLE student ADD isGraduated BOOLEAN;
实际上,由于 MySQL 使用 C/C++ 实现,所以遵循非零即真的原则,在向表中添加数据时,除了可以使用关键字 true 或 false 表示真或假,也可以使用 0 表示假,其它任何数值表示真,次数不再详细说明
日期类型
日期类型专门用于存储日期,常用的日期类型如下:
类型 | 大小(字节) | 格式 |
---|---|---|
DATE | 3 | YYYY-MM-DD |
TIME | 3 | HH:MM:SS |
DATETIME | 8 | YYYY-MM-DD HH:MM:SS |
示例如下:
ALTER TABLE student ADD birthday DATE;
必须注意的是,不论哪一种日期类型,在添加以日期类型为数据类型的字段时必须以单引号包裹内容,示例如下:
INSERT INTO student VALUES (8, 'Mary Jane', 20, 1.60, '女', '体育, 音乐', '2001-01-20');
约束
MySQL 中提供多种约束功能以保证存储到数据库中的数据都是正确的,此即为数据库的完整性,内容如下:
- 实体完整性
- 域完整性
- 参照完整性
实体
数据表中的一行即为一个实体(entity),实体完整性即保证数据表中的每一行数据都是唯一的,MySQL 中实现实体完整性的约束如下所示:
- 主键约束(primary key)
- 唯一约束(unique)
- 自动增长约束(auto_increment)
主键约束
数据表中的主键专门用于唯一标识数据表中的每一条数据,在创建表时在某个字段的数据类型之后添加 primary key
关键字即可,示例如下:
CREATE TABLE student ( stuId INT PRIMARY KEY, stuName VARCHAR ( 20 ) );
此外,也可以在所有字段之后说明数据表的主键,示例如下:
CREATE TABLE student ( stuId INT, stuName VARCHAR ( 20 ), PRIMARY KEY ( stuId ) );
主键约束的特点如下所示:
- 取值不能重复
- 取值不能为 null
- 一张表只能有一个主键
通常情况下,我们将数据表中永远不会重复的字段设置为主键,从而实现实体完整性,不过有时在一张表中并不一定能找到具有此特性的字段,此时我们可以通过 2 个或以上的字段来描述表的主键,类似的主键被称为联合主键,示例如下:
CREATE TABLE student ( stuName VARCHAR ( 20 ), stuAge TINYINT UNSIGNED, PRIMARY KEY ( stuName, stuAge ) );
在联合主键中,如果有一个字段不重复,即为不同的实体
此外,动态为表添加主键约束的格式如下:
ALTER TABLE <TableName> ADD PRIMARY KEY ( FieldName );
示例如下:
ALTER TABLE student ADD PRIMARY KEY ( stuId );
唯一约束
唯一约束专门用于确保某个字段的值永远不会重复,在创建表时在某个字段的数据类型之后添加 unique
关键字即可,示例如下:
CREATE TABLE student ( stuId INT UNIQUE, stuName VARCHAR ( 20 ) );
唯一约束和主键约束都可以确保某个字段的取值不会重复,区别在于,一张表中的主键只能有一个,唯一键可以有若干个,且唯一键的取值可以为 null
此外,动态为表添加唯一约束的格式如下:
ALTER TABLE <TableName> ADD UNIQUE ( FieldName );
示例如下:
ALTER TABLE student ADD UNIQUE ( stuId );
自动增长约束
自动增长约束专门用于让某个字段从 1 开始递增,在创建表时在某个字段的数据类型之后添加 auto_increment
关键字即可,此字段必须是主键,示例如下:
CREATE TABLE student ( stuId PRIMARY KEY AUTO_INCREMENT, stuName VARCHAR ( 20 ) );
如果某个字段是主键,那么此字段的取值不能为 null,如果此主键是自动增长,那么在添加数据时,此字段可以为 null 或 default,示例如下:
INSERT INTO student VALUES ( DEFAULT, 'Reyn Morales' );
此外,动态为表添加自动增长约束的格式如下:
ALTER TABLE <TableName> MODIFY <FieldName> <DataType> AUTO_INCREMENT;
示例如下:
ALTER TABLE student MODIFY stuId INT AUTO_INCREMENT;
域
每一行数据的单元格即为一个域,确保每个单元格中的数据正确即为域完整性,通常情况下,我们通过数据类型约束数据的取值范围以实现域完整性,此外,MySQL 中实现域完整性的约束如下所示:
- 非空约束(not null)
- 默认值约束(default)
非空约束
非空约束专门用于确保某个单元格的数据不能为空,在创建表时在某个字段的数据类型之后添加 not null
关键字即可,示例如下:
CREATE TABLE student ( stuId INT PRIMARY KEY, stuName VARCHAR ( 20 ) NOT NULL );
默认值约束
默认值约束可以在添加数据时,若特定的字段没有指定取值,那么系统将自动提供默认值,在创建表时在某个字段的数据类型之后添加 default <Value>
关键字即可,示例如下:
CREATE TABLE student ( stuId INT PRIMARY KEY, stuName VARCHAR ( 20 ) DEFAULT 'Reyn Morales' );
必须注意的是,添加数据时若特定字段为 null,那么此字段的取值即为 null,而不是 default,即 null 也是一种取值
参照
通常情况下,如果将所有数据都存储在一张表中,难免将出现数据冗余等问题,为了解决此问题,通常可以将一张表拆分为若干张表,再将若干张表通过某个字段关联起来,从而降低数据库的体积,提高数据库的存储效率,表与表的关系通常可以分为如下 3 类:
- 一对一
- 一对多
- 例如,一个班有多个学生
- 例如,一个学生有多个学科的成绩
- 多对多
- 例如,一个学生有多个老师,一个老师教多个学生
一对一的表通常不需要拆分,而一对多或多对多的表需要拆分,在拆分表时,为了描述多个表之间的联系,通常将在某个表中添加一个字段,此字段保存了另一个表中字段的数据,示例如下:
- 学生表
id | name | gender |
---|---|---|
1 | reyn | male |
2 | jane | female |
- 成绩表
id | subject | score | stuId |
---|---|---|---|
1 | math | 99 | 1 |
2 | physics | 93 | 1 |
3 | math | 95 | 2 |
4 | physics | 97 | 2 |
此时,若向成绩表中添加数据时,字段 stuId 的取值为 3,由于学生表中没有 id 为 3 的学生,所以将出现逻辑错误,参照完整性即为了避免此类错误的出现,此时可以为表添加外键约束,示例如下:
- 学生表
CREATE TABLE student ( id INT PRIMARY KEY, stuName VARCHAR ( 20 ) NOT NULL );
- 成绩表
CREATE TABLE stu_score (
id INT PRIMARY KEY,
subject VARCHAR ( 20 ) NOT NULL,
score DOUBLE UNSIGNED,
stuId INT,
FOREIGN KEY (stuId) REFERENCES student ( id )
);
某张表中的某个字段指向了另一张表的主键,那么此表的此字段即为外键,上述示例中,成绩表中的 stuId 即为外键,外键的特点如下所示:
- 数据库 MySQL 中只有 InnoDB 存储引擎支持外键约束
- 外键的数据类型必须和另一张表中主键的数据类型一致
- 在一对多的关系中,外键通常定义在多的一方
- 在多对多的关系中,外键通常定义在关联多和多双方的关系表上
- 定义外键的表被称为从表,被外键引用的表被称为主表
动态为表添加外键的格式如下:
ALTER TABLE <TableName> ADD FOREIGN KEY (<FieldName>) REFERENCES <TableName> ( <FieldName> );
必须注意的是,在 FOREIGN KEY 之后指定字段时不能省略小括号
示例如下:
ALTER TABLE stu_score ADD FOREIGN KEY stuId REFERENCES student ( id );
动态为表删除外键的格式如下:
ALTER TABLE <TableName> DROP FOREIGN KEY <FieldName>;
示例如下:
ALTER TABLE stu_score DROP FOREIGN KEY stuId;
此外,可以通过如下命令查看某张表的外键信息:
SHOW CREATE TABLE <TableName>;
示例如下:
SHOW CREATE TABLE stu_score;
实际上,外键约束存在一个问题,即当从表所引用的主表中的数据被删除或主键被修改时,从表中的相关数据如何处理,MySQL 提供了若干模式解决此问题,内容如下:
- 严格(默认)
- 置空
- 级联
严格
规则如下:
- 如果主表中不存在关联的数据,那么从表不允许添加
- 不能在主表中删除被从表引用的数据
- 不能在主表中修改被从表引用的数据
置空
如果删除了主表中被引用的数据,那么从表中所关联的数据被置为空,在添加外键时添加 on delete set null
关键字即可,示例如下:
CREATE TABLE stu_score (
id INT PRIMARY KEY,
SUBJECT VARCHAR ( 20 ) NOT NULL,
score DOUBLE UNSIGNED,
stuId INT,
FOREIGN KEY stuId REFERENCES student ( id ) ON DELETE SET NULL
);
级联
如果修改了主表中被引用的数据,那么从表中所关联的数据也将被修改,在添加外键时添加 on update cascade
关键字即可,示例如下:
CREATE TABLE stu_score (
id INT PRIMARY KEY,
SUBJECT VARCHAR ( 20 ) NOT NULL,
score DOUBLE UNSIGNED,
stuId INT,
FOREIGN KEY stuId REFERENCES student ( id ) ON UPDATE CASCADE
);
实际上,关键字
ON UPDATE
也可以SET NULL
而ON DELETE
也可以CASCADE
,通常情况下,主表删除数据时从表关联数据置空,主表修改数据时从表关联数据级联
高级查询
在说明查询的高级用法之前,我们必须了解一些概念
-
结果集
- 通过查询语句查询出来的结果即为结果集,结果集以表的形式将查询结果显示
- 被查询的表和结果集并不是同一张表
- 被查询的表是真实存在的,是存储到磁盘上的
- 结果集不是真实存在的,是存储到内存中的
-
字段别名
-
在查询表中特定字段的数据时,可以通过 as 关键字为字段取别名,示例如下:
SELECT age + 1 AS grownUp FROM student;
-
-
字段表达式
-
在查询数据时,除了可以查询表中特定字段的数据外,也可以查询表达式的结果,示例如下:
SELECT 2 + 8;
-
-
伪表
-
字段表达式虽然可以查询出表达式的结果,不过并不符合 MySQL 的规范,所以我们可以借助伪表(dual)让字段表达式符合 MySQL 规范,示例如下:
SELECT 2 + 8 FROM DUAL;
-
伪表并不是真实存在的表,且没有任何实际意义
-
单表
如下所示的数据表是用于说明单表查询高级用法的实例:
+-------+----------+--------+----------+----------+
| stuId | stuName | stuAge | stuCity | stuScore |
+-------+----------+--------+----------+----------+
| 1 | Reyn | 21 | 深圳 | 97 |
| 2 | Steven | 19 | 北京 | 93 |
| 3 | Rachel | 18 | 上海 | 87 |
| 4 | Mary | 25 | 深圳 | 42 |
| 5 | Peter | 23 | 北京 | 76 |
| 6 | Chandler | 21 | 太原 | 55 |
+-------+----------+--------+----------+----------+
6 rows in set (0.01 sec)
模糊查询
格式如下:
SELECT <FieldName> FROM <TableName> WHERE <FieldName> LIKE '<MatchFormat>';
在模糊查询中,常用的通配符如下所示:
- _
- 表示任意一个字符
- %
- 表示任意 0 ~ N 个字符
示例如下:
-
查询所有姓名以 R 字母开头的学生信息
SELECT * FROM student WHERE stuName LIKE 'R%';
-
查询所有姓名中第 4 个字母为 n 的学生姓名、年龄和城市
SELECT stuName, stuAge, stuCity FROM student WHERE stuName LIKE '___n%';
排序
查询数据时,可以通过 order by
关键字让结果集以某个字段升序或降序排序,格式如下:
SELECT <FieldName> FROM <TableName> ORDER BY <FieldName> [asc | desc]
关键字 asc 和 desc 是可选的,默认情况下,结果集以特定字段升序排序,即默认值为 asc
示例如下:
-
查询所有学生的所有信息,并以年龄升序排序
SELECT * FROM student ORDER BY stuAge ASC;
-
查询所有学生的所有信息,并以成绩降序排序
SELECT * FROM student ORDER BY stuScore DESC;
实际上,查询数据时可以以多个字段排序,示例如下:
-
查询所有学生的所有信息,并以年龄升序排序,如果年龄相同,那么以成绩降序排序
SELECT * FROM student ORDER BY stuAge ASC, stuScore DESC;
函数
MySQL 数据库提供诸多函数用以实现统计、计算等功能
聚合
- count
- 统计
- sum
- 求和
- avg
- 求平均值
- max
- 求最大值
- min
- 求最小值
示例如下:
-
查询班级中学生的个数
SELECT COUNT(*) AS stuTotal FROM student;
-
查询班级中成绩及格的学生个数
SELECT COUNT(*) AS passGrade FROM student WHERE stuScore >= 60;
-
查询班级中所有学生的成绩总和
SELECT SUM(stuScore) AS scoreTotal FROM student;
-
查询班级中所有学生年龄的平均值
SELECT ROUND(AVG(stuAge)) AS averageAge FROM student;
-
查询班级中所有学生的成绩的最大值
SELECT MAX(stuScore) AS maxScore FROM student;
-
查询班级中所有学生的成绩的最小值
SELECT MIN(stuScore) AS minScore FROM student;
数值
- rand
- 随机数
- round
- 四舍五入
- ceil
- 向上取整
- floor
- 向下取整
- truncate
- 截取小数位
示例如下:
-
查询所有学生信息,显示顺序随机
SELECT * FROM student ORDER BY RAND();
-
查询班级中所有学生年龄的平均值(四舍五入)
SELECT ROUND(AVG(stuAge)) AS averageAge FROM student;
-
查询数值 3.1 向上取整之后的结果
SELECT CEIL(3.1) FROM DUAL;
-
查询数值 3.9 向下取整之后的结果
SELECT FLOOR(3.9) FROM DUAL;
-
查询圆周率的前 10 位小数
SELECT TRUNCATE(PI(), 10) FROM DUAL;
字符串
- ucase
- 将所有字母转换为大写
- lcase
- 将所有字母转换为小写
- left
- 从字符串左侧开始截取到指定位置
- right
- 从字符串右侧开始截取到指定位置
- substring
- 从字符串指定位置开始截取指定数量的字符
示例如下:
-
查询字符串 tenet 中所有字母转换为大写之后的结果
SELECT UCASE('tenet') FROM DUAL;
-
查询字符串 TENET 中所有字母转换为小写之后的结果
SELECT LCASE('TENET') FROM DUAL;
-
查询字符串 tenet 前 2 个字符是什么
SELECT LEFT('tenet', 2) FROM DUAL;
-
查询字符串 tenet 后 2 个字符是什么
SELECT RIGHT('tenet', 2) FROM DUAL;
-
查询字符串 tenet 中间的 3 个字符是什么
SELECT SUBSTRING('tenet', 2, 3) FROM DUAL;
分组
格式如下:
SELECT <GroupedFieldName> [<AggregateFunction>] FROM <TableName> GROUP BY <GroupedFieldName>
示例如下:
-
查询表中的所有城市(不能重复)
SELECT stuCity FROM student GROUP BY stuCity;
-
查询表中的各个城市有多少个人
SELECT stuCity, COUNT(*) AS totalInCity FROM student GROUP BY stuCity;
必须注意的是,将数据分组时,关键字 select 之后的字段必须是分组字段或聚合函数
条件查询
查询数据时,不仅可以通过 where 关键字指定条件,也可以通过 having 关键字指定条件从而实现条件查询,二者的区别在于 where 在数据库中查询符合条件的数据,having 在结果集中查询符合条件的数据,示例如下:
- 查询来自深圳的学生的姓名和年龄
-
where
SELECT stuName, stuAge FROM student WHERE stuCity = '深圳';
-
having
SELECT stuName, stuAge FROM student HAVING stuCity = '深圳';
-
上述示例中,关键字 where 可以查询结果,而关键字 having 将出现错误,原因在于执行语句时,系统将先执行
SELECT stuName, stuAge FROM student
语句,而在执行此语句的结果集中并没有 stuCity 字段可以检索,输出内容如下:
Unknown column 'stuCity' in 'having clause'
-
查询来自各个城市的学生的平局成绩及格的城市
SELECT stuCity, AVG(stuScore) as avgScore FROM student GROUP BY stuCity HAVING avgScore >= 60;
分页
如果被查询出的数据有很多项,那么可以通过 limit 关键字分页查询,格式如下:
SELECT <FieldName> FROM <TableName> LIMIT <Index>, <Number>;
示例如下:
-
查询表中前 3 个学生的所有信息
SELECT * FROM student LIMIT 0, 3;
-
查询表中后 3 个学生的所有信息
SELECT * FROM student LIMIT 3, 3;
查询选项
格式如下:
SELECT [SelectOption] <FieldName> FROM <TableName>;
格式中,查询选项(SelectOption)的内容如下:
- all
- 显示所有查询数据、可以重复(默认)
- distinct
- 显示所有查询数据、不可以重复
示例如下:
-
查询所有学生的姓名、可以重复
SELECT ALL stuName FROM student;
-
查询所有学生的姓名、不可以重复
SELECT DISTINCT stuName FROM student;
必须注意的是,如果通过关键字 distinct 删除重复数据,那么必须数据的所有列相同才会删除,此处不再说明
总结
综上所述,查询语句的完整格式如下:
SELECT [SelectOption] <FieldName> ... <FieldName>
[FROM <TableName>]
[WHERE <Condition>]
[ORDER BY <FieldName>]
[GROUP BY <FieldName>]
[HAVING <Condition>]
[LIMIT <Index>, <Number>];
多表(连接)
如下所示的数据表是用于说明多表查询高级用法的实例:
-
学生表
+-------+----------+--------+---------+ | stuId | stuName | stuAge | stuCity | +-------+----------+--------+---------+ | 1 | Reyn | 21 | 深圳 | | 2 | Steven | 19 | 北京 | | 3 | Rachel | 18 | 上海 | | 4 | Mary | 25 | 深圳 | | 5 | Peter | 23 | 北京 | | 6 | Chandler | 21 | 太原 | +-------+----------+--------+---------+ 6 rows in set (0.00 sec)
-
成绩表
+-------+------------+----------+-------+ | graId | graSubject | graScore | stuId | +-------+------------+----------+-------+ | 1 | math | 97 | 1 | | 2 | english | 89 | 3 | | 3 | chemistry | 72 | 5 | | 4 | physics | 93 | 4 | | 5 | art | 48 | 1 | | 6 | biological | 55 | 2 | +-------+------------+----------+-------+ 6 rows in set (0.00 sec)
多表查询即在单表查询的基础上增加一张表即可,格式如下:
SELECT <FieldName> FROM <TableName1>, <TableName2>;
示例如下:
SELECT * FROM student, stu_grade;
默认情况下,多表查询的结果是一个笛卡尔集,如果一张表有 N 行,另一张表有 M 行,那么查询结果集中共有 N * M 行
通常情况下,我们将多张表的关联字段连接在查询,此即为表的连接查询,数据库 MySQL 提供了多种连接查询,内容如下:
- 内连接
- 外连接
- 左外连接
- 右外连接
- 交叉连接
- 自然连接
- 自然内连接
- 自然外连接
- 自然左外连接
- 自然右外连接
内连接
如果通过内连接实现多表查询,那么只有满足条件的数据才会出现在结果集中,格式如下:
SELECT * FROM <TableName1> INNER JOIN <TableName2> ON <Condition>;
示例如下:
SELECT * FROM student INNER JOIN stu_grade ON student.stuId = stu_grade.stuId;
必须注意的是,内连接不能省略查询条件
实际上,不通过关键字 INNER JOIN 也可以实现此查询,示例如下:
SELECT * FROM student, stu_grade WHERE student.stuId = stu_grade.stuId;
必须注意的是,如果想在多表查询时查询指定的字段,那么必须在字段名称之前加上字段所在表的名称,示例如下:
SELECT
student.stuId,
student.stuName,
stu_grade.graSubject,
stu_grade.graScore
FROM
student
INNER JOIN stu_grade ON student.stuId = stu_grade.stuId;
此外,如果多表的关联字段名称相同,那么可以通过关键字 using
简化查询条件,示例如下:
SELECT * FROM student INNER JOIN stu_grade USING(stuId);
外连接
外连接所实现的功能和内连接类似,区别在于,如果通过内连接实现多表查询,那么只有满足条件的数据才会出现在结果集中,如果通过外连接实现多表查询,那么所连接的表的某一方可以无视查询条件,从而让某张表中的所有数据都可以出现在结果集中,根据无视查询条件在哪一方可以分为左外连接和右外连接
必须注意的是,外连接不能省略查询条件
左外连接
如果通过左外连接实现多表查询,那么在连接时左侧的表将无视查询条件,在结果集中左侧表的所有数据都将出现在结果集中,而右侧表将根据查询条件决定显示哪些数据,示例如下:
SELECT * FROM student LEFT JOIN stu_grade ON student.stuId = stu_grade.stuId;
上述示例中,student 为左侧表,stu_grade 为右侧表
此外,如果多表的关联字段名称相同,那么可以通过关键字 using
简化查询条件,示例如下:
SELECT * FROM student LEFT JOIN stu_grade USING(stuId);
右外连接
如果通过右外连接实现多表查询,那么在连接时右侧的表将无视查询条件,在结果集中右侧表的所有数据都将出现在结果集中,而左侧表将根据查询条件决定显示哪些数据,示例如下:
SELECT * FROM student RIGHT JOIN stu_grade ON student.stuId = stu_grade.stuId;
上述示例中,student 为左侧表,stu_grade 为右侧表
此外,如果多表的关联字段名称相同,那么可以通过关键字 using
简化查询条件,示例如下:
SELECT * FROM student RIGHT JOIN stu_grade USING(stuId);
交叉连接
通过交叉连接实现多表查询的特点如下:
-
如果未指定查询条件,那么返回笛卡尔集,示例如下:
SELECT * FROM student CROSS JOIN stu_grade;
-
如果指定了查询条件,那么查询结果等价于内连接的查询结果,示例如下:
SELECT * FROM student CROSS JOIN stu_grade ON student.stuId = stu_grade.stuId;
自然连接
自然连接专门用于简化内连接和外连接
自然内连接
通过自然内连接实现多表查询时,如果多表的关联字段名称相同,那么可以省略此条件,示例如下:
SELECT * FROM student NATURAL JOIN stu_grade;
必须注意的是,如果没有指定条件、且没有同名字段,那么将返回笛卡尔集,另外,自然连接将自动删除结果集中重复的字段
自然外连接
自然外连接同样的也有左右之分,特点类似于外连接,不过可以省略查询条件,示例如下:
-
自然左外连接
SELECT * FROM student NATURAL LEFT JOIN stu_grade;
-
自然右外连接
SELECT * FROM student NATURAL RIGHT JOIN stu_grade;
子查询
子查询的含义是将某个查询语句的结果作为另一个查询的条件或表来使用,内容如下:
-
将某个查询语句的结果作为另一个查询的条件来使用
-
标准子查询(一个结果)
- 查询成绩最高分的学生的姓名
SELECT stuName FROM student WHERE stuId = ( SELECT stuId FROM stu_grade WHERE graScore = ( SELECT MAX( graScore ) AS maxScore FROM stu_grade ));
-
非标准子查询(多个结果)
- 查询成绩不及格的学生姓名
SELECT stuName FROM student WHERE stuId IN ( SELECT stuId FROM stu_grade WHERE graScore < 60 );
-
-
将某个查询语句的结果作为另一个查询的表来使用
-
查询所有学生的姓名、年龄和城市信息
SELECT * FROM ( SELECT stuName, stuAge, stuCity FROM student ) AS tmp_table;
必须注意的是,如果将某个查询语句的结果作为另一个查询的表来使用,那么必须为子查询取一个别名
-
事务
MySQL 中的事务专门用于处理容易出错的数据,事务可以用于维护数据库的完整性,以确保成批的 SQL 语句或者全部执行,或者全部不执行
MySQL 中必须以 Innodb 为存储引擎才能开启事务
基本使用
如下所示的数据表是用于说明存储过程的实例:
±------±--------±-------±---------+
| stuId | stuName | stuAge | stuScore |
±------±--------±-------±---------+
| 1 | reyn | 20 | 95 |
| 2 | jack | 22 | 87 |
| 3 | mary | 21 | 92 |
| 4 | jobs | 23 | 79 |
±------±--------±-------±---------+
4 rows in set (0.01 sec)
开启
MySQL 中开启事务的语法如下:
START TRANSACTION;
回滚
MySQL 中回滚到事务开启时刻的语法如下:
ROLLBACK;
提交
MySQL 中提交事务的语法如下:
COMMIT;
本质
事务的本质是在开启事务之后,系统将拷贝一张完全相同的表,之后的相关 SQL 语句的作用对象均为此拷贝的表,如果事务执行失败,即执行了 ROLLBACK
命令,那么系统将自动删除拷贝的表,所以失败了并不会影响原表中的数据,如果事务执行成功,即执行了 COMMIT
命令,那么系统将根据此拷贝的表中的数据更新原表,从而影响原表中的数据
回滚点
实际上,可以通过关键字 savepoint
创建回滚点以实现回滚时并不是回滚到事务开启前,而是某个特殊的节点,示例如下:
START TRANSACTION;
INSERT INTO student VALUES(5, 'peter', 19, 48);
SAVEPOINT fstp;
INSERT INTO student VALUEs(6, 'jhon', 23, 88);
SAVEPOINT sndp;
INSERT INTO student VALUEs(8, 'adam', 17, 62); /* ERROR */
ROLLBACK TO sndp;
INSERT INTO student VALUEs(7, 'adam', 17, 62); /* CORRECT */
COMMIT;
特点
- 原子性
- 事务开启后的所有语句或者全部成功、或者全部失败,如果在执行过程中出错,即使我们没有手动回滚,系统也将自动帮我们回滚
- 一致性
- 事务开始之前和结束之后,数据库中所存储的数据始终是正确且合理的
- 持久性
- 事务一旦完成后,事务对数据库的更新是永久的,不可回滚
- 隔离性
- 数据库默认允许多个事务并发执行,同时访问或更新数据库中的数据,隔离性可以防止多个事务并发时由于交叉执行而导致数据的不一致
隔离级别
实际上,在 MySQL 中可以设置不同的隔离级别,内容如下:
隔离级别 | 名称 | 含义 |
---|---|---|
读未提交 | READ UNCOMMITTED | 一个事务可以读取另一个未提交事务的数据 |
读提交 | READ COMMITTED | 一个事务必须等待另一个事务提交之后才能读数据 |
可重复读 | REPEATABLE READ | 一个事务范围内多个相同的查询的结果相同 |
串行化 | SERIALIZABLE | 所有事务必须串行执行,不能同时执行 |
不同的隔离级别存在不同的问题,内容如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 是 | 是 | 是 |
读提交 | 否 | 是 | 是 |
可重复读 | 否 | 否 | 是 |
串行化 | 否 | 否 | 否 |
各个问题的含义如下所示:
问题 | 含义 |
---|---|
脏读 | 某个事务更新了表中的某条数据,在未提交之前,更新的数据被另一个事务读取 |
不可重复读 | 某个事务在表中多次查询的某条数据,在每次查询的间隔期间,另一个事务多次更新此数据,从而导致事务多次查询此数据时的内容都不相同 |
幻读 | 某个事务在查询表中的某条数据之后,并未结束事务,此时,另一个事务更新了此数据,由于在可重复读的隔离条件下而不能立刻提交此次事务,从而导致查询的数据并不是数据的最新的内容 |
可以通过关键字 select
查询隔离级别,语法如下:
-
全局
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
-
当前会话
SELECT @@TRANSACTION_ISOLATION;
可以通过关键字 set
设置隔离级别,语法如下:
-
全局
SET GLOBAL TRANSACTION ISOLATION LEVEL <LevelName>;
-
当前会话
SET SESSION TRANSACTION ISOLATION LEVEL <LevelName>;
由于数据库相关内容是后端开发者必须掌握的内容,所以此处不再详细说明
视图
视图的本质就是将查询的结果集缓存起来,视图是一张虚拟的表,建立在表的基础之上,作用如下:
- 简化 SQL 语句
- 将表的结构隐藏起来,以提升数据的安全性
基本使用
如下所示的数据表是用于说明存储过程的实例:
±------±--------±-------±---------+
| stuId | stuName | stuAge | stuScore |
±------±--------±-------±---------+
| 1 | reyn | 20 | 95 |
| 2 | jack | 22 | 87 |
| 3 | mary | 21 | 92 |
| 4 | jobs | 23 | 79 |
±------±--------±-------±---------+
4 rows in set (0.01 sec)
创建
格式如下:
CREATE [ALGORITHM=MERGE|TEMPTABLE|UNDEFINED] VIEW <ViewName> AS <QuerySQL> [WITH CHECK OPTION];
创建视图的格式中,关键字 algorithm 是可选的,内容如下:
算法 | 名称 | 特点 |
---|---|---|
MERGE | 替代式 | 将创建视图时的 SQL 语句和外层借助视图查询的语句合并之后再执行,此算法允许修改数据 |
TEMPTABLE | 具代式 | 将根据创建视图时的 SQL 语句生成临时表,外层借助视图查询的语句通过此临时查询数据,此算法不允许修改数据 |
UNDEFINED | 未定义(默认) | 由 MySQL 决定使用上述哪一种算法,通常情况下为替代式算法 |
示例如下:
CREATE VIEW stu_view AS SELECT stuName, stuScore FROM student WHERE stuScore>=60;
查询
查询视图的方法和查询表的方法完全相同,示例如下:
SELECT * FROM stu_view WHERE stuName LIKE 'j%';
如果视图通过 MERGE 算法创建,那么向视图中添加数据,或从视图中删除数据,或修改视图中的数据,原表中数据也将发生变化,示例如下:
INSERT INTO stu_view VALUES ('amy', 42);
上述示例中,即使添加的数据并不符合视图中的查询条件,系统也可以将此数据添加的表中,不过通过视图并不能查询到不符合条件的数据,如果想要避免此情形的发生,可以在创建视图时指定
with check option
关键字以通过视图添加数据时必须符合视图的查询条件,示例如下:
CREATE VIEW stu_view AS SELECT stuName, stuScore FROM student WHERE stuScore>=60 WITH CHECK OPTION;
此时不论是添加数据还是修改数据,如果通过此视图,那么必须符合视图的查询条件才能添加或修改,示例如下:
UPDATE stu_view SET stuScore=59 WHERE stuName='jack';
上述 SQL 语句将执行失败,输出内容如下:
1369 - CHECK OPTION failed 'query_db.stu_view'
修改
格式如下:
ALTER VIEW <ViewName> AS SELECT <QuerySQL>;
示例如下:
ALTER VIEW stu_view AS SELECT stuName, stuAge FROM student;
删除
格式如下:
DROP VIEW [IF EXISTS] <ViewName>;
示例如下:
DROP VIEW IF EXISTS stu_view;
视图限制
内容如下:
- 如果创建视图时通过 MERGE 算法,那么可以更新视图
- 如果视图未指定 CHECK OPTION 选项,那么无论数据是否符合视图的查询条件,都可以更新
- 如果视图存在 CHECK OPTION 选项,那么只有符合视图的查询条件的数据才可以更新
- 必须注意的是,所有更新视图的数据来源必须是基于原表的,通过另外的方式(聚合函数、分组等)所产生的视图中的相关字段数据不可更新
预处理
如下所示的数据表是用于说明预处理的实例:
±------±--------±-------+
| stuId | stuName | stuAge |
±------±--------±-------+
| 1 | reyn | 21 |
| 2 | jack | 22 |
| 3 | mary | 20 |
±------±--------±-------+
3 rows in set (0.00 sec)
预处理是由 MySQL 最初提出的一种可以减轻服务器压力的一种技术,默认情况下,MySQL 服务器处理查询的流程如下:
- 客户端准备 SQL 语句
- 客户端发送 SQL 语句到 MySQL 服务器
- MySQL 服务器解析 SQL 语句
- MySQL 服务器编译 SQL 语句
- MySQL 服务器执行 SQL 语句
- MySQL 服务器将查询结果返回到客户端
上述查询处理流程中,即使客户端每次发送的 SQL 语句大同小异,也必须重复传递,即使 MySQL 服务器每次解析、编译的 SQL 语句几乎相同,也必须重复解析、编译
为了避免重复的工作,MySQL 服务器可以将格式相同、查询目标不同(未知)的语句预解析,之后,如果查询相同内容,那么客户端发送查询目标信息即可,MySQL 服务器拿到查询目标后,将查询目标和预解析好的 SQL 语句合并、编译、执行,再将查询结果返回到客户端,此即为预处理,流程如下:
-
客户端准备预处理 SQL 语句、并将此语句发送到 MySQL 服务器
-
格式如下:
PREPARE <PrepareSqlName> FROM <'QuerySQL'>;
-
示例如下:
PREPARE stmt FROM 'SELECT * FROM student WHERE stuId=?';
上述示例中的
?
字符是一个占位符,以便将来替换查询目标(条件)
-
-
MySQL 服务器预解析 SQL 语句(不执行)
-
客户端准备查询目标信息
-
格式如下:
SET @<VariableName>=<SpecificValue>;
-
示例如下:
SET @id = 1;
上述示例中,通过关键字
set
创建的变量是 MySQL 中的全局变量,预处理中必须使用全局变量传递查询目标的相关数据,此外,和其它的编程语言类似,MySQL 中也可以定义局部变量,具体内容将在 “存储过程” 一节中详细说明
-
-
命令 MySQL 服务器编译并执行此语句
- 格式如下:
EXECUTE <PrepareSqlName> USING @<VariableName>;
- 示例如下:
EXECUTE stmt USING @id;
- 格式如下:
查询中借助预处理技术的流程示例如下:
PREPARE stmt FROM 'SELECT * FROM student WHERE stuId=?';
SET @id = 1;
EXECUTE stmt USING @id;
SET @id = 3;
EXECUTE stmt USING @id;
存储过程
如下所示的数据表是用于说明存储过程的实例:
±------±--------±-------+
| stuId | stuName | stuAge |
±------±--------±-------+
| 1 | reyn | 21 |
| 2 | jack | 22 |
| 3 | mary | 20 |
±------±--------±-------+
3 rows in set (0.00 sec)
存储过程类似于其它编程语言的函数,可以用于封装一组特定功能的 SQL 语句
基本使用
创建
格式如下:
CREATE PROCEDURE <ProcedureName>([Parameters: <[IN | OUT | INOUT] ParaName DataType>]);
BEGIN
/* SQL */
END;
示例如下:
CREATE PROCEDURE show_stu_by_id(id INT)
BEGIN
SELECT * FROM student WHERE stuId=id;
END;
调用
格式如下:
CALL <ProcedureName>([Parameters: <ParaSepcificValue>]);
示例如下:
CALL show_stu_by_id(2);
查询
示例如下:
SHOW PROCEDURE STATUS;
上述示例用于查询所有数据库中的所有存储过程,实际上可以通过
where
关键字查询某个数据库中的所有存储过程,示例如下:
SHOW PROCEDURE STATUS WHERE db='query_db';
此外,也可以查询指定存储过程的源代码,格式如下:
SHOW CREATE PROCEDURE <ProcedureName>
示例如下:
SHOW CREATE PROCEDURE show_stu_by_id;
删除
格式如下:
DROP PROCEDURE <ProcedureName>
示例如下:
DROP PROCEDURE show_stu_by_id;
定义变量
全局
格式如下:
-
定义
@<VariableName>;
-
初始化
SET @<VariableName>=<SpecificValue>;
-
赋值
-
关键字 set
SET @<VariableName>=<SpecificValue>;
-
关键字 select
SELECT <FieldName> INTO @<VariableName> FROM <TableName>;
-
由于之后的存储过程和函数将大量使用全局变量,所以此处不再编写示例
局部
局部变量仅可以在存储过程和函数中定义,格式如下:
-
定义
DECLARE <VariableName> <DataType>;
-
初始化
DECLARE <VariableName> <DataType> DEFAULT <SpecificValue>;
-
赋值
-
关键字 set
SET <VariableName>=<SpecificValue>;
-
关键字 select
SELECT <FieldName> INTO <VariableName> FROM <TableName>;
-
由于之后的存储过程和函数将大量使用局部变量,所以此处不再编写示例
传递参数
存储过程中的参数分为三类,内容如下:
-
in
-
输入参数(默认),外部调用存储过程时向存储过程传入的参数,示例如下:
CREATE PROCEDURE show_stu_by_id(id INT) BEGIN SELECT * FROM student WHERE stuId=id; END;
-
-
out
-
输出参数,外部调用存储过程时可以通过此参数获取存储过程中的中间值或结果值,示例如下:
CREATE PROCEDURE show_stu_by_id(IN id INT, OUT name VARCHAR(20)) BEGIN SELECT stuName INTO name FROM student WHERE stuId=id; END; SET @name=''; CALL show_stu_by_id(1, @name); SELECT @name FROM DUAL;
-
-
inout
-
输入输出参数,此类参数同时具有输入参数和输出参数的功能,示例如下:
CREATE PROCEDURE show_stu_by_id(INOUT info INT) BEGIN SELECT stuAge INTO info FROM student WHERE stuId=info; END; SET @info=3; CALL show_stu_by_id(@info); SELECT @info FROM DUAL;
-
函数
MySQL 中的函数和存储过程极为相似,不过不需要手动调用函数,而是和其它的聚合函数一样在 SQL 语句中被自动调用
基本使用
创建
格式如下:
CREATE FUNCTION <FunctionName>([Parameters: <ParaName DataType>]) RETURNS <DataType> <FunctionFeature>
BEGIN
/* SQL */
RETURN <SpecificValue>;
END;
函数特征(FunctionFeature)说明了函数体内部的某些特征,内容如下:
特征 | 含义 |
---|---|
DETERMINISTIC | 不确定的 |
NO SQL | 无 SQL 语句 |
READS SQL DATA | 通过 SQL 语句读取数据,但不修改数据 |
MODIFIES SQL DATA | 过 SQL 语句修改数据 |
CONTAINS SQL | 有 SQL 语句 |
通常使用 DETERMINISTIC 作为函数特征
示例如下:
CREATE FUNCTION minus(a INT, b INT) RETURNS INT DETERMINISTIC
BEGIN
DECLARE res INT DEFAULT 0;
SET res=a-b;
RETURN res;
END;
调用
格式如下:
SELECT <FunctionName>([Parameters: <SpecificValue>]) FROM DUAL;
示例如下:
SELECT minus(5, 3) FROM DUAL;
查询
示例如下:
SHOW FUNCTION STATUS;
上述示例用于查询所有数据库中的所有函数,实际上可以通过
where
关键字查询某个数据库中的所有函数,示例如下:
SHOW FUNCTION STATUS WHERE db='query_db';
此外,也可以查询指定函数的源代码,格式如下:
SHOW CREATE FUNCTION <ProcedureName>;
示例如下:
SHOW CREATE FUNCTION minus;
删除
格式如下:
DROP FUNCTION <FunctionName>;
示例如下:
DROP FUNCTION minus;
语句
条件
IF 语句
格式如下:
IF <Condition> THEN
/* SQL */
ELSEIF <Condition> THEN
/* SQL */
ELSE
/* SQL */
END IF;
示例如下:
CREATE FUNCTION is_grown_up(age INT) RETURNS VARCHAR(10) DETERMINISTIC
BEGIN
DECLARE res VARCHAR(10) DEFAULT '';
IF age>=18 THEN
SET res='成年人';
ELSE
SET res='未成年人';
END IF;
RETURN res;
END;
CASE 语句
格式如下:
CASE <VariableName>
WHEN <Condition> THEN
/* SQL */
WHEN <Condition> THEN
/* SQL */
ELSE
/* SQL */
END CASE;
示例如下:
CREATE FUNCTION is_good(score INT) RETURNS VARCHAR(5) DETERMINISTIC
BEGIN
DECLARE res VARCHAR(5) DEFAULT '';
CASE score
WHEN 100 THEN
SET res='Good';
WHEN 0 THEN
SET res='Bad';
END CASE;
RETURN res;
END;
循环
WHILE 语句
格式如下:
WHILE <Condition> DO
/* SQL */
END WHILE;
示例如下:
CREATE FUNCTION accumulate(num INT) RETURNS INT DETERMINISTIC
BEGIN
DECLARE res INT DEFAULT 0;
DECLARE cur INT DEFAULT 1;
WHILE cur<=num DO
SET res=res+cur;
SET cur=cur+1;
END WHILE;
RETURN res;
END;
REPEAT 语句
格式如下:
REPEAT
/* SQL */
UNTIL <Condition> END REPEAT;
示例如下:
CREATE FUNCTION accumulate(num INT) RETURNS INT DETERMINISTIC
BEGIN
DECLARE res INT DEFAULT 0;
DECLARE cur INT DEFAULT 1;
REPEAT
SET res=res+cur;
SET cur=cur+1;
UNTIL cur>num END REPEAT;
RETURN res;
END;
综合示例
如果必须向数据库中存储 10000 条数据,此时可以通过存储过程或函数,由于函数通常和 SQL 语句一起使用,所以此处使用存储过程
示例一:
CREATE PROCEDURE init_stus(num INT)
BEGIN
DECLARE curId INT DEFAULT 0;
DECLARE curAge INT DEFAULT 0;
DECLARE curName VARCHAR(20) DEFAULT '';
WHILE curId<=num DO
SET curId=curId+1;
SET curAge=FLOOR(RAND()*30);
SET curName=CONCAT(curId, 'user', curAge);
INSERT INTO student VALUES(curId, curAge, curName);
END WHILE;
END;
如果调用上述存储过程,那么运行时间为 8.154s,原因在于 MySQL 在执行此存储过程时,解析、编译、执行了 10000 此 SQL 语句
示例二:
CREATE PROCEDURE init_stus(num INT)
BEGIN
DECLARE curId INT DEFAULT 0;
DECLARE curAge INT DEFAULT 0;
DECLARE curName VARCHAR(20) DEFAULT '';
SET autocommit = 0;
WHILE curId<=num DO
SET curId=curId+1;
SET curAge=FLOOR(RAND()*30);
SET curName=CONCAT(curId, 'user', curAge);
INSERT INTO student VALUES(curId, curAge, curName);
END WHILE;
COMMIT;
END;
如果调用上述存储过程,那么运行时间为 0.79s,原因在于 MySQL 在执行此存储过程时,系统将在所有 SQL 语句均生成之后再统一解析、编译、执行 SQL 语句
示例三:
CREATE PROCEDURE init_stus(num INT)
BEGIN
SET @curId=0;
SET @curAge=0;
SET @curName='';
PREPARE stmt FROM 'INSERT INTO student VALUES(?, ?, ?);';
WHILE @curId<=num DO
SET @curId=@curId+1;
SET @curAge=FLOOR(RAND()*30);
SET @curName=CONCAT(@curId, 'user', @curAge);
EXECUTE stmt USING @curId, @curAge, @curName;
END WHILE;
COMMIT;
END;
如果调用上述存储过程,那么运行时间为 0.982s,原因在于 MySQL 在执行此存储过程时,通过预处理减少了系统执行 SQL 语句时的准备工作
索引
索引相当于字典中的目录,通过目录我们可以快速找到目标结果,所以索引可以提升数据的查询速度,内容如下:
- 优点
- 提升数据的查询速度
- MySQL 中的所有字段都可以用于索引
- 缺点
- 保存的索引越多,数据库的体积越大
- 如果修改用于索引的字段数据,那么系统将花费一定的时间去维护索引
- 原则
- 应当将经常用于查询的字段用于索引(条件、分组、排序等)
- 如果将字段用于主键、外键、唯一键,那么系统将自动创建索引
- 如果表的数据量小,那么通常无需使用索引
- 分类
- 单值索引
- 将某个字段用于索引
- 复合索引
- 将某些字段用于索引
- 唯一索引(唯一键)
- 用于唯一索引的字段的值必须是唯一的
- 主键索引(主键)
- 特殊的唯一索引
- 单值索引
创建
MySQL 中可以在创建表的同时为某个或某些字段添加索引,示例如下:
CREATE TABLE student (
stuId INT,
stuName VARCHAR(20),
INDEX idx(stuId)
);
实际上,也可以在创建表结束之后再为表中的某个或某些字段添加索引,格式如下:
CREATE INDEX <IndexName> ON <TableName>(FieldName);
示例如下:
CREATE INDEX idx ON student(stuId);
此外,也可以通过 alter
关键字为表添加索引,格式如下:
ALTER TABLE <TableName> ADD INDEX <IndexName>(FieldName);
示例如下:
ALTER TABLE student ADD INDEX idx(stuId);
查询
MySQL 中可以通过关键字 explain
查看系统查询某个字段时是否使用了索引,格式如下:
EXPLAIN <QuerySQL>;
如果返回结果中的 key 字段存在内容,即表明系统借助索引执行此查询,如果返回结果中的 key 字段不存在内容,即表明此查询中未使用索引
示例如下:
EXPLAIN SELECT * FROM student WHERE stuId=99999;
删除
格式如下:
DROP INDEX <IndexName> ON <TableName>;
示例如下:
DROP INDEX idx ON student;
索引算法
MySQL 中的索引算法内容如下:
- BTree
- 基于平衡多叉排序树实现,可以缩短查找次数
- Hash
- 基于哈希表实现(仅可以在 memory 存储引擎中使用)
mysql 模块
在 Node.js 中可以通过 mysql 模块或 mysql2 模块操纵数据库,步骤如下:
由于后续的 Sequelize 将利用 mysql2,所以此处介绍 mysql2,二者并没有太大的差别
- 下载 mysql2
npm install mysql2 --save
- 导入 mysql2
const mysql2 = require('mysql2')
- 连接配置
let connection = mysql2.createConnection({
host: '127.0.0.1',
port: 3306,
user: 'root',
password: '1234',
database: 'mydb'
})
选项说明如下:
选项 | 说明 |
---|---|
host | MySQL 服务器地址 |
port | 端口号 |
user | MySQL 服务器用户名 |
password | MySQL 服务器密码 |
database | 数据库 |
- 连接 MySQL 服务器
connection.connect()
- 向 MySQL 服务器发送查询指令
connection.query(
'SELECT * FROM people',
(error, results, fields) => {
if (error) {
throw new Error('MySQL Query Error')
}
console.log(results);
})
通过 MySQL 执行相同的查询语句,输出内容如下:
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | Reyn | 21 |
| 2 | Mary | 20 |
| 3 | Peter | 22 |
| 4 | Amy | 19 |
+----+-------+------+
4 rows in set (0.00 sec)
- 释放连接
connection.end()
Sequelize
基本概念
Sequelize 是一个基于 Promise 的 Node.js ORM 模块,ORM 即为对象关系映射,可以将 JavaScript 中的类和实例与数据库中的表和数据相互映射,从而实现通过类和实例操纵数据库中的表和数据,不用再编写 SQL 语句,在 Sequelize 中,映射内容如下:
- JavaScript 中的类(模型)映射数据库中的表
- JavaScript 中的实例映射数据库中的数据
- JavaScript 中的实例属性映射数据库中的字段
如果通过 Sequelize 定义类(模型),那么 Sequelize 将为此类添加诸多方法以操纵表和数据,之后就可以通过此类以及相关实例操纵数据库中的表和数据
必须注意的是,在使用时必须先通过 npm 下载 sequelize 以及相应的数据库驱动程序,如果利用 MySQL,那么应当下载 mysql2
数据库
如果想要利用 Sequelize 模块操纵数据库中的表和数据,那么必须先和数据库建立连接,示例如下:
/* 1. 导入 Sequelize */
const Sequelize = require('sequelize');
/* 2. 配置连接信息 */
const sequelize = new Sequelize('nalp', 'root', '1234', { /* 数据库名称、用户名、密码 */
host: '127.0.0.1', /* 服务器地址 */
port: 3306, /* 服务器端口号 */
dialect: 'mysql', /* 数据库类型 */
pool: { /* 数据库连接池 */
max: 5, // 最大连接数
min: 0, // 最小连接数
idle: 10000, // 如果连接之后在规定时间内没有任何动作,那么断开连接
acquire: 30000 // 如果连接池中的所有连接均被占用,那么等待多长时间
}
});
/* 3. 测试连接 */
sequelize.authenticate()
.then(() => {
console.log('Success'); // Success
})
.catch((err) => {
console.log(err)
})
在 Sequelize 中包含了数据库连接池的概念,远程使用数据库时,服务器需要创建一个连接,当不再使用数据库时,服务器必须销毁一个连接,如果频繁的创建和销毁连接是相当消耗性能的,所以出现了数据库连接池,默认情况下,在连接池中已经创建好了若干个连接,当用户远程访问数据库时,不再需要创建连接,而是直接利用连接池中已存在的连接操纵数据库,当用户不再访问数据库时,也无需销毁连接,直接离开即可,连接池中所有的连接均可以被复用
表
在 Sequelize 中可以通过 define 定义(创建)模型并将此模型和数据库中的某个表关联,如果创建的模型不存在,那么也可以通过 sync 方法创建表,示例如下:
/* 4. 定义模型 */
const People = sequelize.define('people', {
/* 字段名称 */
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
age: {
type: Sequelize.TINYINT
},
gender: {
type: Sequelize.ENUM(['男', '女']),
defaultValue: '男'
}
}, {
/* 额外信息 */
freezeTableName: true,
timestamps: false,
indexes: [
{
name: 'idx_age',
fields: ['age']
}
]
});
/* 5. 模型同步 */
sequelize.sync();
默认定义好一个模型之后,并不会自动创建相应的表,必须通过 Sequelize 的 sync 实例方法同步,以下内容简略说明了常用的配置属性,详情可以访问官方文档
字段说明常用属性如下所示:
属性 | 说明 |
---|---|
type | 字段类型 |
primaryKey | 字段是否为主键 |
autoIncrement | 字段是否为自动增长 |
allowNull | 字段是否允许为空 |
unique | 字段是否必须唯一 |
defaultValue | 默认值 |
额外信息常用属性如下所示:
属性 | 说明 |
---|---|
timestamps | 是否自动添加 createdAt/updateAt 字段 |
freezeTableName | 是否禁止自动将表名修改为复数 |
tableName | 是否自定义表名 |
indexes | 索引 |
默认情况下,sequelize 根据模型创建表时,自动将表名修改为复数形式,且自动增加 createAt/updateAt 字段
数据
创建
在 Sequelize 中,创建一个模型的实例即为创建一条记录,示例如下:
let p = new People();
p.name = 'Reyn';
p.age = 21;
p.gender = '男';
默认情况下,创建一个实例之后并不会立即将数据同步到表中,必须通过实例的 save 方法才能将数据同步,示例如下:
(async () => {
console.log('Before Save', p.dataValues); // { gender: '男', id: null, name: 'Reyn', age: 21 }
await p.save();
console.log('After Save', p.dataValues); // { gender: '男', id: 1, name: 'Reyn', age: 21 }
})();
必须注意的是,本质上利用 MySQL 执行 SQL 语句是一个异步操作,所以在 Sequelize 中的大部分方法都是异步方法,此外,通过模型创建的实例有一个 dataValues 属性,此属性保存了一条记录的所有信息
实际上可以通过模型的 create 方法在创建实例的同时立即同步数据,示例如下:
(async () => {
let p = await People.create({
name: 'Emma',
age: 20,
gender: '女'
});
console.log(p.dataValues); // { id: 2, name: 'Emma', age: 20, gender: '女' }
})();
查询
在 Sequelize 中,可以通过模型的 findByPk 方法根据主键查询数据,并将此数据相应的实例返回,示例如下:
(async () => {
let p = await People.findByPk(2);
console.log(p.dataValues); // { id: 2, name: 'Emma', age: 20, gender: '女' }
})();
修改
如果通过 Sequelize 中的 findByPk 查询到实例,那么修改实例属性的取值之后,通过实例的 save 方法同步即可修改表中的数据,示例如下:
(async () => {
let p = await People.findByPk(2);
p.age = 18;
await p.save();
})();
实际上,可以通过实例的 set 方法同时更改多个属性的取值,此处不再详细说明,此外,也可以通过模型的 update 方法修改数据,此时可以通过条件查询修改指定的数据,示例如下:
(async () => {
People.update({
age: 20
}, {
where: {
id: 1
}
})
})();
删除
如果通过 Sequelize 中的 findByPk 查询到实例,那么通过实例的 destory 方法即可删除实例在表中所对应的数据,示例如下:
(async () => {
let p = await People.findByPk(2);
await p.destroy();
})();
此外,也可以通过模型的 destory 方法删除数据,此时可以通过条件查询删除指定的数据,示例如下:
(async () => {
People.destroy({
where: {
id: 1
}
});
})();
查询
简单
除了可以通过模型的 findByPk 方法以主键查询数据,也可以通过 findAll 查询所有数据,示例如下:
(async () => {
let ps = await People.findAll();
console.log(ps.map(p => p.dataValues));
})();
此外,也可以向此方法传递参数,说明查询哪几个字段,示例如下:
(async () => {
let ps = await People.findAll({
attributes: ['name', 'age']
});
console.log(ps.map(p => p.dataValues));
})();
条件
在通过 find 系列方法查询数据时,除了可以通过 attributes 属性说明查询哪几个字段,也可以通过 where 属性说明查询条件
关系运算符
Sequelize 提供了很多常量以表示关系运算符,此处不再详细说明,详情可以访问官方文档,示例如下:
(async () => {
let ps = await People.findAll({
attributes: ['name', 'age'],
where: {
age: {
[Sequelize.Op.gte]: 20
}
}
});
console.log(ps.map(p => p.dataValues));
})();
逻辑运算符
默认情况下,属性 where 下的所有属性的关系即为逻辑与,示例如下:
(async () => {
let ps = await People.findAll({
attributes: ['name', 'age'],
where: {
id: 1,
age: {
[Sequelize.Op.gte]: 20
}
}
});
console.log(ps.map(p => p.dataValues));
})();
如果在条件查询中表示逻辑或,可以借助 Sequelize 提供的常量表示,在此属性下的所有属性的关系为逻辑或,示例如下:
(async () => {
let ps = await People.findAll({
attributes: ['name', 'age'],
where: {
[Sequelize.Op.or]: {
id: 1,
age: 18
}
}
});
console.log(ps.map(p => p.dataValues));
})();
分页
在 Sequelize 中,可以通过 offset 属性说明查询记录的开始位置,通过 limit 属性说明查询记录的数量,示例如下:
(async () => {
let ps = await People.findAll({
attributes: ['name', 'age'],
offset: 1,
limit: 2
});
console.log(ps.map(p => p.dataValues));
})();
排序
在 Sequelize 中,可以通过 order 属性说明查询结果的排序情况,示例如下:
(async () => {
let ps = await People.findAll({
order: [
['id', 'desc'],
['age', 'asc']
]
});
console.log(ps.map(p => p.dataValues));
})();
关联
在 Sequelize 中,通过 has 和 belongs 相关的方法可以将模型以某种关系关联起来,从而实现在查询某个模型下的数据时,可以查询到与此模型相关的数据,常见的关系类型如下所示:
- 一对一
- 学生 -1:1- 成绩
- 一对多
- 学生 -1:n- 书
- 多对多
- 学生 -n:n- 老师
一对一
模型详情如下:
/* 定义学生表 */
const Student = sequelize.define('student', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
age: {
type: Sequelize.TINYINT
},
gender: {
type: Sequelize.ENUM('Male', 'Female')
}
}, {
freezeTableName: true,
timestamps: false
});
/* 定义成绩表 */
const Score = sequelize.define('score', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
grades: {
type: Sequelize.FLOAT,
allowNull: false
},
stuId: {
type: Sequelize.INTEGER,
allowNull: false
}
}, {
freezeTableName: true,
timestamps: false
});
/* 关联学生表和成绩表 */
Student.hasOne(Score, { // 一个学生拥有一个成绩
foreignKey: 'stuId',
sourceKey: 'id'
});
Score.belongsTo(Student, { // 一个成绩属于一个学生
foreignKey: 'stuId',
sourceKey: 'id'
});
/* 同步模型 */
sequelize.sync();
学生表中的记录内容如下所示:
+----+-------+------+--------+
| id | name | age | gender |
+----+-------+------+--------+
| 1 | Reyn | 21 | Male |
| 2 | Mary | 20 | Female |
| 3 | David | 22 | Male |
+----+-------+------+--------+
3 rows in set (0.00 sec)
成绩表中的记录内容如下所示:
+----+--------+-------+
| id | grades | stuId |
+----+--------+-------+
| 1 | 89 | 2 |
| 2 | 95 | 1 |
| 3 | 76 | 3 |
+----+--------+-------+
3 rows in set (0.00 sec)
关联查询示例如下:
(async () => {
let stu = await Student.findOne({
where: {
id: 1
},
include: { // 选择被关联的模型
model: Score
}
})
console.log(stu.dataValues);
let sre = await Score.findOne({
where: {
id: 3
},
include: {
model: Student
}
})
console.log(sre.dataValues);
})();
上述示例中,如果在查询选项中的 include 属性中选择了某个被关联的模型,那么和查询记录存在关联的且在关联模型中的记录也将被获取
一对多
模型详情如下:
/* 定义学生表 */
const Student = sequelize.define('student', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
age: {
type: Sequelize.TINYINT
},
gender: {
type: Sequelize.ENUM('Male', 'Female')
}
}, {
freezeTableName: true,
timestamps: false
});
/* 定义书表 */
const Book = sequelize.define('book', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
stuId: {
type: Sequelize.INTEGER,
allowNull: false
}
}, {
freezeTableName: true,
timestamps: false
});
/* 关联学生表和书表 */
Student.hasMany(Book, { // 一个学生拥有多本书
foreignKey: 'stuId',
sourceKey: 'id'
});
Book.belongsTo(Student, { // 一本书属于一个学生
foreignKey: 'stuId',
sourceKey: 'id'
});
/* 同步模型 */
sequelize.sync();
学生表中的记录内容如下所示:
+----+-------+------+--------+
| id | name | age | gender |
+----+-------+------+--------+
| 1 | Reyn | 21 | Male |
| 2 | Mary | 20 | Female |
| 3 | David | 22 | Male |
+----+-------+------+--------+
3 rows in set (0.00 sec)
成绩表中的记录内容如下所示:
+----+-----------------------+-------+
| id | name | stuId |
+----+-----------------------+-------+
| 1 | War and Peace | 1 |
| 2 | Pride and Prejudice | 2 |
| 3 | Ulysses | 1 |
| 4 | Moby-Dick | 3 |
| 5 | To Kill A Mockingbird | 2 |
| 6 | The Big Sleep | 3 |
+----+-----------------------+-------+
6 rows in set (0.00 sec)
关联查询示例如下:
(async () => {
let stu = await Student.findOne({
where: {
id: 3
},
include: {
model: Book
}
})
console.log(stu.dataValues);
let bok = await Book.findOne({
where: {
id: 4
},
include: {
model: Student
}
})
console.log(bok.dataValues);
})();
多对多
模型详情如下:
/* 定义学生表 */
const Student = sequelize.define('student', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
age: {
type: Sequelize.TINYINT
},
gender: {
type: Sequelize.ENUM('Male', 'Female')
}
}, {
freezeTableName: true,
timestamps: false
});
/* 定义老师表 */
const Teacher = sequelize.define('teacher', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type:Sequelize.STRING,
allowNull: false
}
}, {
freezeTableName: true,
timestamps: false
});
/* 定义学生-老师表(关联表) */
const Student_Teacher = sequelize.define('student-teacher', {
studentId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: Student,
key: 'id'
}
},
teacherId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: Teacher,
key: 'id'
}
}
}, {
freezeTableName: true,
timestamps: false
});
/* 关联学生表和老师表 */
Student.belongsToMany(Teacher, { // 一个学生属于多个老师
through: Student_Teacher
});
Teacher.belongsToMany(Student, { // 一个老师属于多个学生
through: Student_Teacher
});
/* 同步模型 */
sequelize.sync();
在定义模型时,可以通过 references 属性指定某个字段为外键
学生表中的记录内容如下所示:
+----+-------+------+--------+
| id | name | age | gender |
+----+-------+------+--------+
| 1 | Reyn | 21 | Male |
| 2 | Mary | 20 | Female |
| 3 | David | 22 | Male |
+----+-------+------+--------+
3 rows in set (0.00 sec)
老师表中的记录内容如下所示:
+----+---------+
| id | name |
+----+---------+
| 1 | Abraham |
| 2 | Maria |
+----+---------+
2 rows in set (0.00 sec)
关联表中的记录内容如下所示:
+-----------+-----------+
| studentId | teacherId |
+-----------+-----------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
+-----------+-----------+
6 rows in set (0.00 sec)
关联查询示例如下:
(async () => {
let stu = await Student.findOne({
where: {
id: 1
},
include: {
model: Teacher
}
})
console.log(stu.dataValues);
let tch = await Teacher.findOne({
where: {
id: 1
},
include: {
model: Student
}
})
console.log(tch.dataValues);
})();
迁移工具
在编程开发中,为了能够更好的管理代码、实现对代码变更的追踪以及在各个不同的版本之间切换,我们可以利用 Git 来管理代码,同样的,为了能够更好的管理数据库、实现对数据库变更的追踪以及在各个不同版本之间的切换,我们可以利用迁移工具来管理数据库,而 Sequelize-CLI 就是一款数据库迁移工具
在使用 Sequelize-CLI 之前,必须下载 Sequelize 和相应的数据库驱动,示例如下:
npm install sequelize sequelize-cli mysql2 --save
可以通过指令
npx sequelize --help
查询帮助
初始化
在 Sequelize-CLI 中,通过指令 npx sequelize init
初始化项目,示例如下:
npx sequelize init
在初始化项目后,在项目目录下将自动创建 4 个子目录,如下所示:
- config
- 数据库配置文件、用于告诉 CLI 如何连接数据库
- migrations
- 数据库迁移文件、用于记录数据库不同版本之间的差异和变更
- models
- 数据库模型文件、用于告诉 CLI 如何创建表
- seeders
- 数据库种子文件、用于编写测试数据
如果查看 config 目录下的配置文件,将有 3 个不同的环境,如下所示:
{
"development": { /* 开发环境 */
"username": "root", // 用户名
"password": "1234", // 密码
"database": "nalp_development", // 数据库名称
"host": "127.0.0.1", // 数据库 IP 地址
"dialect": "mysql" // 数据库类型
},
"test": { /* 测试环境 */
"username": "root",
"password": "1234",
"database": "nalp_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": { /* 生产环境 */
"username": "root",
"password": "1234",
"database": "nalp_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
在不同的环境下可以使用不同的数据库,不过在使用之前必须设置开发环境,示例如下:
set NODE_ENV=development
之后通过指令 npx sequelize db:create
即可创建数据库,示例如下:
npx sequelize db:create
模型
创建
可以通过指令 npx sequelize model:generate --name <ModelName> --attributes <Key:Type>,<Key:Type>,<Key:Type>
,示例如下:
npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string
必须注意的是,选项 name 和 attributes 是必须的,如果存在多个属性,那么利用逗号分隔,且不能存在任何空字符
执行上述指令后将在 migrations 和 models 目录下创建文件,如下所示:
- migrations
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users');
}
};
- models
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class User extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
User.init({
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
email: DataTypes.STRING
}, {
sequelize,
modelName: 'User',
});
return User;
};
根据文件内容不难发现,Sequelize-CLI 是通过迁移文件去操纵数据库的
同步
在创建好模型之后,可以通过指令 npx sequelize db:migrate
同步模型,示例如下:
npx sequelize db:migrate
在执行命令后,数据库中将出现 2 个表,其中 1 个为 users 表,即通过模型创建的表,另 1 个为 sequelizemeta 表,此表记录了所有的迁移文件,Sequelize-CLI 即通过此表中的内容实现在不同版本之间的切换,在每次执行此命令时,Sequelize-CLI 将利用 sequelizemeta 表检查哪些迁移文件不存在,如果不存在,那么将同步不存在的迁移文件
撤销
在 Sequelize-CLI 中,通过 migrate:undo 相关的指令可以实现撤销的功能,从而在不同版本之间切换,指令有 3 种形式:
- 撤销至上一个版本
npx sequelize db:migrate:undo
- 撤销所有版本
npx sequelize db:migrate:undo:all
- 撤销至某个版本
npx sequelize db:migrate:undo --name <MigrationsFileName>
示例如下:
npx sequelize db:migrate:undo --name 20221124030609-create-user.js
执行命令后,表 users 将被删除,表 sequelizemeta 中的特定记录将被删除
修改
我们使用 Sequelize-CLI 管理数据库的目的在于追踪数据库的变更,所以最好不要直接修改表的结构,此时我们必须通过迁移文件修改,如此一来 Sequelize-CLI 就可以帮助我们记录修改操作,以方便于追踪变更和撤销至特定版本,步骤如下:
-
通过指令
npx sequelize migration:generate --name <MigrationsFileName>
创建迁移文件-
示例如下:
npx sequelize migration:generate --name reinit-users
-
-
在迁移文件中编写代码
-
初始化好的迁移文件中的内容如下所示:
'use strict'; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up (queryInterface, Sequelize) { /** * Add altering commands here. * * Example: * await queryInterface.createTable('users', { id: Sequelize.INTEGER }); */ }, async down (queryInterface, Sequelize) { /** * Add reverting commands here. * * Example: * await queryInterface.dropTable('users'); */ } };
- 异步方法 up 用于编写 Sequelize-CLI 同步迁移文件时将执行的代码
- 异步方法 down 用于编写 Sequelize-CLI 撤销迁移文件时将执行的代码
- 实例 queryInterface 中保存了操纵数据库表中的方法,详情可以访问官方文档
-
示例如下:
'use strict'; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up (queryInterface, Sequelize) { await queryInterface.removeColumn('users', 'createdAt'); await queryInterface.removeColumn('users', 'updatedAt'); }, async down (queryInterface, Sequelize) { await queryInterface.addColumn('users', 'createdAt', Sequelize.DATE); await queryInterface.addColumn('users', 'updatedAt', Sequelize.DATE); } };
-
-
通过指令
npx sequelize db:migrate
同步
种子文件
Sequelize-CLI 中的种子文件专门用于编写测试数据,由于在不同的阶段将使用不同的数据库,而新创建的数据库中不存在任何数据,而为了方便演示和调试,我们可以在种子文件中提前编写测试数据,以在不同数据库中快速填充数据
创建
通过指令 npx sequelize seed:generate --name <SeedersFileName>
创建种子文件,示例如下:
npx sequelize seed:generate --name init-users-data
初始化好的种子文件中的内容如下所示:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add seed commands here.
*
* Example:
* await queryInterface.bulkInsert('People', [{
* name: 'John Doe',
* isBetaMember: false
* }], {});
*/
},
async down (queryInterface, Sequelize) {
/**
* Add commands to revert seed here.
*
* Example:
* await queryInterface.bulkDelete('People', null, {});
*/
}
};
类似于种子文件,异步方法 up 用于编写 Sequelize-CLI 同步数据时的代码,异步方法 down 用于编写 Sequelize-CLI 撤销数据时的代码,示例如下:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('users',[{
firstName: 'Reyn',
lastName: 'Morales',
email: 'reyn0120@foxmail.com'
}])
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('users', null)
}
};
同步
通过指令 npx sequelize db:seed --seed <SeedFileName>
可以同步某个种子文件,示例如下:
npx sequelize db:seed --seed 20221124035351-init-users-data.js
必须注意的是,默认情况下种子文件的相关操作将以 JSON 的格式记录,如果想以数据库的形式记录,那么在配置文件中可以增加 seederStorage 的属性为 sequelize,示例如下:
{
"development": {
"username": "root",
"password": "1234",
"database": "nalp_development",
"host": "127.0.0.1",
"dialect": "mysql",
"seederStorage": "sequelize"
}
}
如果以数据库的形式记录种子文件的相关操作,那么将在数据库种创建一个名为 sequelizedata 的表,原理类似于 sequelizemeta 表
实际上,通过指令 npx sequelize db:seed:all
可以同步所有种子文件,此处不再详细说明
撤销
在 Sequelize-CLI 中,通过 seed:undo 相关的指令可以实现撤销的功能,从而在不同版本之间切换,指令有 2 种形式:
- 撤销至某个版本
npx sequelize db:seed:undo --seed <SeedFileName>
- 撤销所有版本
npx sequelize db:seed:undo:all
示例如下:
npx sequelize db:seed:undo --seed 20221124035351-init-users-data.js