更新了部分文档
parent
71b2cf87e0
commit
620c7495bc
|
@ -0,0 +1,415 @@
|
|||
## SQL详解之DDL
|
||||
|
||||
我们通常可以将 SQL 分为四类,分别是 DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)和 DCL(数据控制语言)。DDL 主要用于创建、删除、修改数据库中的对象,比如创建、删除和修改二维表,核心的关键字包括`create`、`drop`和`alter`;DML 主要负责数据的插入、删除和更新,关键词包括`insert`、`delete`和`update`;DQL 负责数据查询,最重要的一个关键词是`select`;DCL 通常用于授予和召回权限,核心关键词是`grant`和`revoke`。
|
||||
|
||||
> **说明**:SQL 是不区分大小写的语言,有人会建议将关键字大写,其他部分小写。为了书写和识别方便,下面的 SQL 我都是使用小写字母进行书写的。 如果公司的 SQL 编程规范有强制规定,那么就按照公司的要求来,个人的喜好不应该凌驾于公司的编程规范之上,这一点对职业人来说应该是常识。
|
||||
|
||||
### 建库建表
|
||||
|
||||
下面我们来实现一个非常简单的学校选课系统的数据库。我们将数据库命名为`school`,四个关键的实体分别是学院、老师、学生和课程,其中,学生跟学院是从属关系,这个关系从数量上来讲是多对一关系,因为一个学院可以有多名学生,而一个学生通常只属于一个学院;同理,老师跟学院的从属关系也是多对一关系。一名老师可以讲授多门课程,一门课程如果只有一个授课老师的话,那么课程跟老师也是多对一关系;如果允许多个老师合作讲授一门课程,那么课程和老师就是多对多关系。简单起见,我们将课程和老师设计为多对一关系。学生和课程是典型的多对多关系,因为一个学生可以选择多门课程,一门课程也可以被多个学生选择,而关系型数据库需要借助中间表才能维持维持两个实体的多对多关系。最终,我们的学校选课系统一共有五张表,分别是学院表(`tb_college`)、学生表(`tb_student`)、教师表(`tb_teacher`)、课程表(`tb_course`)和选课记录表(`tb_record`),其中选课记录表就是维持学生跟课程多对多关系的中间表。
|
||||
|
||||
```SQL
|
||||
-- 如果存在名为school的数据库就删除它
|
||||
drop database if exists `school`;
|
||||
|
||||
-- 创建名为school的数据库并设置默认的字符集和排序方式
|
||||
create database `school` default character set utf8mb4 collate utf8mb4_general_ci;
|
||||
|
||||
-- 切换到school数据库上下文环境
|
||||
use `school`;
|
||||
|
||||
-- 创建学院表
|
||||
create table `tb_college`
|
||||
(
|
||||
`col_id` int unsigned auto_increment comment '编号',
|
||||
`col_name` varchar(50) not null comment '名称',
|
||||
`col_intro` varchar(500) default '' comment '介绍',
|
||||
primary key (`col_id`)
|
||||
) engine=innodb auto_increment=1 comment '学院表';
|
||||
|
||||
-- 创建学生表
|
||||
create table `tb_student`
|
||||
(
|
||||
`stu_id` int unsigned not null comment '学号',
|
||||
`stu_name` varchar(20) not null comment '姓名',
|
||||
`stu_sex` boolean default 1 not null comment '性别',
|
||||
`stu_birth` date not null comment '出生日期',
|
||||
`stu_addr` varchar(255) default '' comment '籍贯',
|
||||
`col_id` int unsigned not null comment '所属学院',
|
||||
primary key (`stu_id`),
|
||||
constraint `fk_student_col_id` foreign key (`col_id`) references `tb_college` (`col_id`)
|
||||
) engine=innodb comment '学生表';
|
||||
|
||||
-- 创建教师表
|
||||
create table `tb_teacher`
|
||||
(
|
||||
`tea_id` int unsigned not null comment '工号',
|
||||
`tea_name` varchar(20) not null comment '姓名',
|
||||
`tea_title` varchar(10) default '助教' comment '职称',
|
||||
`col_id` int unsigned not null comment '所属学院',
|
||||
primary key (`tea_id`),
|
||||
constraint `fk_teacher_col_id` foreign key (`col_id`) references `tb_college` (`col_id`)
|
||||
) engine=innodb comment '老师表';
|
||||
|
||||
-- 创建课程表
|
||||
create table `tb_course`
|
||||
(
|
||||
`cou_id` int unsigned not null comment '编号',
|
||||
`cou_name` varchar(50) not null comment '名称',
|
||||
`cou_credit` int not null comment '学分',
|
||||
`tea_id` int unsigned not null comment '授课老师',
|
||||
primary key (`cou_id`),
|
||||
constraint `fk_course_tea_id` foreign key (`tea_id`) references `tb_teacher` (`tea_id`)
|
||||
) engine=innodb comment '课程表';
|
||||
|
||||
-- 创建选课记录表
|
||||
create table `tb_record`
|
||||
(
|
||||
`rec_id` bigint unsigned auto_increment comment '选课记录号',
|
||||
`stu_id` int unsigned not null comment '学号',
|
||||
`cou_id` int unsigned not null comment '课程编号',
|
||||
`sel_date` date not null comment '选课日期',
|
||||
`score` decimal(4,1) comment '考试成绩',
|
||||
primary key (`rec_id`),
|
||||
constraint `fk_record_stu_id` foreign key (`stu_id`) references `tb_student` (`stu_id`),
|
||||
constraint `fk_record_cou_id` foreign key (`cou_id`) references `tb_course` (`cou_id`),
|
||||
constraint `uk_record_stu_cou` unique (`stu_id`, `cou_id`)
|
||||
) engine=innodb comment '选课记录表';
|
||||
```
|
||||
|
||||
上面的DDL有几个地方需要强调一下:
|
||||
|
||||
- 首先,上面 SQL 中的数据库名、表名、字段名都被反引号(`)包裹起来,反引号并不是必须的,但是却可以解决表名、字段名等跟 SQL 关键字(SQL 中有特殊含义的单词)冲突的问题。
|
||||
|
||||
- 创建数据库时,我们通过`default character set utf8mb4`指定了数据库默认使用的字符集为`utf8mb4`(最大`4`字节的`utf-8`编码),我们推荐使用该字符集,它也是 MySQL 8.x 默认使用的字符集,因为它能够支持国际化编码,还可以存储 Emoji 字符。可以通过下面的命令查看 MySQL 支持的字符集以及默认的排序规则。
|
||||
|
||||
```SQL
|
||||
show character set;
|
||||
```
|
||||
|
||||
```
|
||||
+----------+---------------------------------+---------------------+--------+
|
||||
| Charset | Description | Default collation | Maxlen |
|
||||
+----------+---------------------------------+---------------------+--------+
|
||||
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
|
||||
| dec8 | DEC West European | dec8_swedish_ci | 1 |
|
||||
| cp850 | DOS West European | cp850_general_ci | 1 |
|
||||
| hp8 | HP West European | hp8_english_ci | 1 |
|
||||
| koi8r | KOI8-R Relcom Russian | koi8r_general_ci | 1 |
|
||||
| latin1 | cp1252 West European | latin1_swedish_ci | 1 |
|
||||
| latin2 | ISO 8859-2 Central European | latin2_general_ci | 1 |
|
||||
| swe7 | 7bit Swedish | swe7_swedish_ci | 1 |
|
||||
| ascii | US ASCII | ascii_general_ci | 1 |
|
||||
| ujis | EUC-JP Japanese | ujis_japanese_ci | 3 |
|
||||
| sjis | Shift-JIS Japanese | sjis_japanese_ci | 2 |
|
||||
| hebrew | ISO 8859-8 Hebrew | hebrew_general_ci | 1 |
|
||||
| tis620 | TIS620 Thai | tis620_thai_ci | 1 |
|
||||
| euckr | EUC-KR Korean | euckr_korean_ci | 2 |
|
||||
| koi8u | KOI8-U Ukrainian | koi8u_general_ci | 1 |
|
||||
| gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 |
|
||||
| greek | ISO 8859-7 Greek | greek_general_ci | 1 |
|
||||
| cp1250 | Windows Central European | cp1250_general_ci | 1 |
|
||||
| gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 |
|
||||
| latin5 | ISO 8859-9 Turkish | latin5_turkish_ci | 1 |
|
||||
| armscii8 | ARMSCII-8 Armenian | armscii8_general_ci | 1 |
|
||||
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
|
||||
| ucs2 | UCS-2 Unicode | ucs2_general_ci | 2 |
|
||||
| cp866 | DOS Russian | cp866_general_ci | 1 |
|
||||
| keybcs2 | DOS Kamenicky Czech-Slovak | keybcs2_general_ci | 1 |
|
||||
| macce | Mac Central European | macce_general_ci | 1 |
|
||||
| macroman | Mac West European | macroman_general_ci | 1 |
|
||||
| cp852 | DOS Central European | cp852_general_ci | 1 |
|
||||
| latin7 | ISO 8859-13 Baltic | latin7_general_ci | 1 |
|
||||
| utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
|
||||
| cp1251 | Windows Cyrillic | cp1251_general_ci | 1 |
|
||||
| utf16 | UTF-16 Unicode | utf16_general_ci | 4 |
|
||||
| utf16le | UTF-16LE Unicode | utf16le_general_ci | 4 |
|
||||
| cp1256 | Windows Arabic | cp1256_general_ci | 1 |
|
||||
| cp1257 | Windows Baltic | cp1257_general_ci | 1 |
|
||||
| utf32 | UTF-32 Unicode | utf32_general_ci | 4 |
|
||||
| binary | Binary pseudo charset | binary | 1 |
|
||||
| geostd8 | GEOSTD8 Georgian | geostd8_general_ci | 1 |
|
||||
| cp932 | SJIS for Windows Japanese | cp932_japanese_ci | 2 |
|
||||
| eucjpms | UJIS for Windows Japanese | eucjpms_japanese_ci | 3 |
|
||||
| gb18030 | China National Standard GB18030 | gb18030_chinese_ci | 4 |
|
||||
+----------+---------------------------------+---------------------+--------+
|
||||
41 rows in set (0.00 sec)
|
||||
```
|
||||
|
||||
如果要设置 MySQL 服务启动时默认使用的字符集,可以修改MySQL的配置并添加以下内容。
|
||||
|
||||
```INI
|
||||
[mysqld]
|
||||
character-set-server=utf8
|
||||
```
|
||||
|
||||
> **提示**:如果不清楚如何修改 MySQL 的配置文件就先不要管它。
|
||||
|
||||
- 创建和删除数据库时,关键字`database`也可以替换为`schema`,二者作用相同。
|
||||
|
||||
- 建表语句中的`not null`是非空约束,它限定了字段不能为空;`default`用于为字段指定默认值,我们称之为默认值约束;`primary key`是主键约束,它设定了能够唯一确定一条记录的列,也确保了每条记录都是独一无二的,因为主键不允许重复;`foreign key`是外键约束,它维持了两张表的参照完整性,举个例子,由于学生表中为 col_id 字段添加了外键约束,限定其必须引用(`references`)学院表中的 col_id,因此学生表中的学院编号必须来自于学院表中的学院编号,不能够随意为该字段赋值。如果需要给主键约束、外键约束等起名字,可以使用`constriant`关键字并在后面跟上约束的名字。
|
||||
|
||||
- 建表语句中的`comment` 关键字用来给列和表添加注释,增强代码的可读性和可维护性。
|
||||
|
||||
- 在创建表的时候,可以自行选择底层的存储引擎。MySQL 支持多种存储引擎,可以通过`show engines`命令进行查看。MySQL 5.5 以后的版本默认使用的存储引擎是 InnoDB,它是我们推荐大家使用的存储引擎(因为更适合当下互联网应用对高并发、性能以及事务支持等方面的需求),为了 SQL 语句的向下兼容性,我们可以在建表语句结束处右圆括号的后面通过`engine=innodb`来指定使用 InnoDB 存储引擎。
|
||||
|
||||
```SQL
|
||||
show engines\G
|
||||
```
|
||||
|
||||
> **说明**:上面的 \G 是为了换一种输出方式,在命令行客户端中,如果表的字段很多一行显示不完,就会导致输出的内容看起来非常不舒服,使用 \G 可以将记录的每个列以独占整行的的方式输出,这种输出方式在命令行客户端中看起来会舒服很多。
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
Engine: InnoDB
|
||||
Support: DEFAULT
|
||||
Comment: Supports transactions, row-level locking, and foreign keys
|
||||
Transactions: YES
|
||||
XA: YES
|
||||
Savepoints: YES
|
||||
*************************** 2. row ***************************
|
||||
Engine: MRG_MYISAM
|
||||
Support: YES
|
||||
Comment: Collection of identical MyISAM tables
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 3. row ***************************
|
||||
Engine: MEMORY
|
||||
Support: YES
|
||||
Comment: Hash based, stored in memory, useful for temporary tables
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 4. row ***************************
|
||||
Engine: BLACKHOLE
|
||||
Support: YES
|
||||
Comment: /dev/null storage engine (anything you write to it disappears)
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 5. row ***************************
|
||||
Engine: MyISAM
|
||||
Support: YES
|
||||
Comment: MyISAM storage engine
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 6. row ***************************
|
||||
Engine: CSV
|
||||
Support: YES
|
||||
Comment: CSV storage engine
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 7. row ***************************
|
||||
Engine: ARCHIVE
|
||||
Support: YES
|
||||
Comment: Archive storage engine
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 8. row ***************************
|
||||
Engine: PERFORMANCE_SCHEMA
|
||||
Support: YES
|
||||
Comment: Performance Schema
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 9. row ***************************
|
||||
Engine: FEDERATED
|
||||
Support: NO
|
||||
Comment: Federated MySQL storage engine
|
||||
Transactions: NULL
|
||||
XA: NULL
|
||||
Savepoints: NULL
|
||||
9 rows in set (0.00 sec)
|
||||
```
|
||||
|
||||
下面的表格对MySQL几种常用的数据引擎进行了简单的对比。
|
||||
|
||||
| 特性 | InnoDB | MRG_MYISAM | MEMORY | MyISAM |
|
||||
| ------------ | ------------ | ---------- | ------ | ------ |
|
||||
| 存储限制 | 有 | 没有 | 有 | 有 |
|
||||
| 事务 | 支持 | | | |
|
||||
| 锁机制 | 行锁 | 表锁 | 表锁 | 表锁 |
|
||||
| B树索引 | 支持 | 支持 | 支持 | 支持 |
|
||||
| 哈希索引 | | | 支持 | |
|
||||
| 全文检索 | 支持(5.6+) | | | 支持 |
|
||||
| 集群索引 | 支持 | | | |
|
||||
| 数据缓存 | 支持 | | 支持 | |
|
||||
| 索引缓存 | 支持 | 支持 | 支持 | 支持 |
|
||||
| 数据可压缩 | | | | 支持 |
|
||||
| 内存使用 | 高 | 低 | 中 | 低 |
|
||||
| 存储空间使用 | 高 | 低 | | 低 |
|
||||
| 批量插入性能 | 低 | 高 | 高 | 高 |
|
||||
| 是否支持外键 | 支持 | | | |
|
||||
|
||||
通过上面的比较我们可以了解到,InnoDB 是唯一能够支持外键、事务以及行锁的存储引擎,所以我们之前说它更适合互联网应用,而且在较新版本的 MySQL 中,它也是默认使用的存储引擎。
|
||||
|
||||
- 在定义表结构为每个字段选择数据类型时,如果不清楚哪个数据类型更合适,可以通过 MySQL 的帮助系统来了解每种数据类型的特性、数据的长度和精度等相关信息。
|
||||
|
||||
```SQL
|
||||
? data types
|
||||
```
|
||||
|
||||
> **说明**:在 MySQLWorkbench 中,不能使用`?`获取帮助,要使用对应的命令`help`。
|
||||
|
||||
```
|
||||
You asked for help about help category: "Data Types"
|
||||
For more information, type 'help <item>', where <item> is one of the following
|
||||
topics:
|
||||
AUTO_INCREMENT
|
||||
BIGINT
|
||||
BINARY
|
||||
BIT
|
||||
BLOB
|
||||
BLOB DATA TYPE
|
||||
BOOLEAN
|
||||
CHAR
|
||||
CHAR BYTE
|
||||
DATE
|
||||
DATETIME
|
||||
DEC
|
||||
DECIMAL
|
||||
DOUBLE
|
||||
DOUBLE PRECISION
|
||||
ENUM
|
||||
FLOAT
|
||||
INT
|
||||
INTEGER
|
||||
LONGBLOB
|
||||
LONGTEXT
|
||||
MEDIUMBLOB
|
||||
MEDIUMINT
|
||||
MEDIUMTEXT
|
||||
SET DATA TYPE
|
||||
SMALLINT
|
||||
TEXT
|
||||
TIME
|
||||
TIMESTAMP
|
||||
TINYBLOB
|
||||
TINYINT
|
||||
TINYTEXT
|
||||
VARBINARY
|
||||
VARCHAR
|
||||
YEAR DATA TYPE
|
||||
```
|
||||
|
||||
获取 varchar 类型的帮助:
|
||||
|
||||
```SQL
|
||||
? varchar
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```
|
||||
Name: 'VARCHAR'
|
||||
Description:
|
||||
[NATIONAL] VARCHAR(M) [CHARACTER SET charset_name] [COLLATE
|
||||
collation_name]
|
||||
|
||||
A variable-length string. M represents the maximum column length in
|
||||
characters. The range of M is 0 to 65,535. The effective maximum length
|
||||
of a VARCHAR is subject to the maximum row size (65,535 bytes, which is
|
||||
shared among all columns) and the character set used. For example, utf8
|
||||
characters can require up to three bytes per character, so a VARCHAR
|
||||
column that uses the utf8 character set can be declared to be a maximum
|
||||
of 21,844 characters. See
|
||||
http://dev.mysql.com/doc/refman/5.7/en/column-count-limit.html.
|
||||
|
||||
MySQL stores VARCHAR values as a 1-byte or 2-byte length prefix plus
|
||||
data. The length prefix indicates the number of bytes in the value. A
|
||||
VARCHAR column uses one length byte if values require no more than 255
|
||||
bytes, two length bytes if values may require more than 255 bytes.
|
||||
|
||||
*Note*:
|
||||
|
||||
MySQL follows the standard SQL specification, and does not remove
|
||||
trailing spaces from VARCHAR values.
|
||||
|
||||
VARCHAR is shorthand for CHARACTER VARYING. NATIONAL VARCHAR is the
|
||||
standard SQL way to define that a VARCHAR column should use some
|
||||
predefined character set. MySQL uses utf8 as this predefined character
|
||||
set. http://dev.mysql.com/doc/refman/5.7/en/charset-national.html.
|
||||
NVARCHAR is shorthand for NATIONAL VARCHAR.
|
||||
|
||||
URL: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
|
||||
```
|
||||
|
||||
在数据类型的选择上,保存字符串数据通常都使用 VARCHAR 和 CHAR 两种类型,前者通常称为变长字符串,而后者通常称为定长字符串;对于 InnoDB 存储引擎,行存储格式没有区分固定长度和可变长度列,因此 VARCHAR 类型和 CHAR 类型没有本质区别,后者不一定比前者性能更好。如果要保存的很大字符串,可以使用 TEXT 类型;如果要保存很大的字节串,可以使用 BLOB(二进制大对象)类型。在 MySQL 中,TEXT 和 BLOB又分别包括 TEXT、MEDIUMTEXT、LONGTEXT 和 BLOB、MEDIUMBLOB、LONGBLOB 三种不同的类型,它们主要的区别在于存储数据的最大大小不同。保存浮点数可以用 FLOAT 或 DOUBLE 类型,FLOAT 已经不推荐使用了,而且在 MySQL 后续的版本中可能会被移除掉。而保存定点数应该使用 DECIMAL 类型,它可以指定小数点前后有效数字的位数。如果要保存时间日期,DATETIME 类型优于 TIMESTAMP 类型,因为前者能表示的时间日期范围更大,后者底层其实就是一个整数,记录了指定的日期时间和 1970-01-01 00:00:00 相差多少个毫秒,该类型在 2038-01-19 03:14:07 之后就会溢出。
|
||||
|
||||
对于自增字段 AUTO_INCREMENT,如果使用 MySQL 5.x 版本要注意自增字段的回溯问题,当然这个问题在 MySQL 8.x 中已经得到了很好的解决,当然,MySQL 8.x 还有很多其他的好处,不管是功能还是性能上都有很多的优化和调整,因此强烈推荐大家使用 MySQL 8.x 版本。对于高并发访问数据库的场景,AUTO_INCREMENT 不仅存在性能上的问题,还可能在多机结构上产生重复的 ID 值,在这种场景下,使用分布式 ID 生成算法(SnowFlake、TinyID等)才是最好的选择,有兴趣的读者可以自行研究。
|
||||
|
||||
### 删除表和修改表
|
||||
|
||||
下面以学生表为例,为大家说明如何删除表和修改表。删除表可以使用`drop table`,代码如下所示。
|
||||
|
||||
```SQL
|
||||
drop table `tb_student`;
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```SQL
|
||||
drop table if exists `tb_student`;
|
||||
```
|
||||
|
||||
需要注意的是,如果学生表已经录入了数据而且该数据被其他表引用了,那么就不能删除学生表,否则上面的操作会报错。在下一课中,我们会讲解如何向表中插入数据,到时候大家可以试一试,能否顺利删除学生表。
|
||||
|
||||
如果要修改学生表,可以使用`alter table`,具体可以分为以下几种情况:
|
||||
|
||||
修改表,添加一个新列,例如给学生表添加一个联系电话的列。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` add column `stu_tel` varchar(20) not null comment '联系电话';
|
||||
```
|
||||
|
||||
> **注意**:如果新增列的时候指定了非空约束(`not null`),那么学生表不能够有数据,否则原来的数据增加了 stu_tel 列之后是没有数据的,这就违反了非空约束的要求;当然,我们在添加列的时候也可以使用默认值约束来解决这个问题。
|
||||
|
||||
修改表,删除指定的列,例如将上面添加的联系电话列删除掉。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` drop column `stu_tel`;
|
||||
```
|
||||
|
||||
修改表,修改列的数据类型,例如将学生表的 stu_sex 修改为字符。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` modify column `stu_sex` char(1) not null default 'M' comment '性别';
|
||||
```
|
||||
|
||||
修改表,修改列的命名,例如将学生表的 stu_sex 修改为 stu_gender。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` change column `stu_sex` `stu_gender` boolean default 1 comment '性别';
|
||||
```
|
||||
|
||||
修改表,删除约束条件,例如删除学生表的 col_id 列的外键约束。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` drop foreign key `fk_student_col_id`;
|
||||
```
|
||||
|
||||
修改表,添加约束条件,例如给学生表的 col_id 列加上外键约束。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` add foreign key (`col_id`) references `tb_college` (`col_id`);
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` add constraint `fk_student_col_id` foreign key (`col_id`) references `tb_college` (`col_id`);
|
||||
```
|
||||
|
||||
> **说明**:在添加外键约束时,还可以通过`on update`和`on delete`来指定在被引用的表发生删除和更新操作时,应该进行何种处理,二者的默认值都是`restrict`,表示如果存在外键约束,则不允许更新和删除被引用的数据。除了`restrict`之外,这里可能的取值还有`cascade`(级联操作)和`set null`(设置为空),有兴趣的读者可以自行研究。
|
||||
|
||||
修改表的名字,例如将学生表的名字修改为 tb_stu_info。
|
||||
|
||||
```SQL
|
||||
alter table `tb_student` rename to `tb_stu_info`;
|
||||
```
|
||||
|
||||
> **提示**:一般情况下,请不要轻易修改数据库或表的名字。
|
804
第41课:SQL详解.md
804
第41课:SQL详解.md
|
@ -1,804 +0,0 @@
|
|||
## 第41课:SQL详解
|
||||
|
||||
我们通常可以将 SQL 分为四类,分别是 DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)和 DCL(数据控制语言)。DDL 主要用于创建、删除、修改数据库中的对象,比如创建、删除和修改二维表,核心的关键字包括`create`、`drop`和`alter`;DML 主要负责数据的插入、删除和更新,关键词包括`insert`、`delete`和`update`;DQL 负责数据查询,最重要的一个关键词是`select`;DCL 通常用于授予和召回权限,核心关键词是`grant`和`revoke`。
|
||||
|
||||
> **说明**:SQL 是不区分大小写的语言,为了书写和识别方便,下面的 SQL 都使用了小写字母来书写。
|
||||
|
||||
### DDL(数据定义语言)
|
||||
|
||||
下面我们来实现一个选课系统的数据库,如下所示的 SQL 创建了名为`school`的数据库和五张表,分别是学院表(`tb_college`)、学生表(`tb_student`)、教师表(`tb_teacher`)、课程表(`tb_course`)和选课记录表(`tb_record`),其中学生和教师跟学院之间是多对一关系,课程跟老师之间也是多对一关系,学生和课程是多对多关系,选课记录表就是维持学生跟课程多对多关系的中间表。
|
||||
|
||||
```SQL
|
||||
-- 如果存在名为school的数据库就删除它
|
||||
drop database if exists `school`;
|
||||
|
||||
-- 创建名为school的数据库并设置默认的字符集和排序方式
|
||||
create database `school` default character set utf8mb4 collate utf8mb4_general_ci;
|
||||
|
||||
-- 切换到school数据库上下文环境
|
||||
use `school`;
|
||||
|
||||
-- 创建学院表
|
||||
create table `tb_college`
|
||||
(
|
||||
`col_id` int unsigned auto_increment comment '编号',
|
||||
`col_name` varchar(50) not null comment '名称',
|
||||
`col_intro` varchar(500) default '' comment '介绍',
|
||||
primary key (`col_id`)
|
||||
) engine=innodb auto_increment=1 comment '学院表';
|
||||
|
||||
-- 创建学生表
|
||||
create table `tb_student`
|
||||
(
|
||||
`stu_id` int unsigned not null comment '学号',
|
||||
`stu_name` varchar(20) not null comment '姓名',
|
||||
`stu_sex` boolean default 1 not null comment '性别',
|
||||
`stu_birth` date not null comment '出生日期',
|
||||
`stu_addr` varchar(255) default '' comment '籍贯',
|
||||
`col_id` int unsigned not null comment '所属学院',
|
||||
primary key (`stu_id`),
|
||||
constraint `fk_student_col_id` foreign key (`col_id`) references `tb_college` (`col_id`)
|
||||
) engine=innodb comment '学生表';
|
||||
|
||||
-- 创建教师表
|
||||
create table `tb_teacher`
|
||||
(
|
||||
`tea_id` int unsigned not null comment '工号',
|
||||
`tea_name` varchar(20) not null comment '姓名',
|
||||
`tea_title` varchar(10) default '助教' comment '职称',
|
||||
`col_id` int unsigned not null comment '所属学院',
|
||||
primary key (`tea_id`),
|
||||
constraint `fk_teacher_col_id` foreign key (`col_id`) references `tb_college` (`col_id`)
|
||||
) engine=innodb comment '老师表';
|
||||
|
||||
-- 创建课程表
|
||||
create table `tb_course`
|
||||
(
|
||||
`cou_id` int unsigned not null comment '编号',
|
||||
`cou_name` varchar(50) not null comment '名称',
|
||||
`cou_credit` int not null comment '学分',
|
||||
`tea_id` int unsigned not null comment '授课老师',
|
||||
primary key (`cou_id`),
|
||||
constraint `fk_course_tea_id` foreign key (`tea_id`) references `tb_teacher` (`tea_id`)
|
||||
) engine=innodb comment '课程表';
|
||||
|
||||
-- 创建选课记录表
|
||||
create table `tb_record`
|
||||
(
|
||||
`rec_id` bigint unsigned auto_increment comment '选课记录号',
|
||||
`stu_id` int unsigned not null comment '学号',
|
||||
`cou_id` int unsigned not null comment '课程编号',
|
||||
`sel_date` date not null comment '选课日期',
|
||||
`score` decimal(4,1) comment '考试成绩',
|
||||
primary key (`rec_id`),
|
||||
constraint `fk_record_stu_id` foreign key (`stu_id`) references `tb_student` (`stu_id`),
|
||||
constraint `fk_record_cou_id` foreign key (`cou_id`) references `tb_course` (`cou_id`),
|
||||
constraint `uk_record_stu_cou` unique (`stu_id`, `cou_id`)
|
||||
) engine=innodb comment '选课记录表';
|
||||
```
|
||||
|
||||
上面的DDL有几个地方需要强调一下:
|
||||
|
||||
- 创建数据库时,我们通过`default character set utf8mb4`指定了数据库默认使用的字符集为`utf8mb4`(最大`4`字节的`utf-8`编码),我们推荐使用该字符集,它也是 MySQL 8.x 默认使用的字符集,因为它能够支持国际化编码,还可以存储 Emoji 字符。可以通过下面的命令查看 MySQL 支持的字符集以及默认的排序规则。
|
||||
|
||||
```SQL
|
||||
show character set;
|
||||
```
|
||||
|
||||
```
|
||||
+----------+---------------------------------+---------------------+--------+
|
||||
| Charset | Description | Default collation | Maxlen |
|
||||
+----------+---------------------------------+---------------------+--------+
|
||||
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
|
||||
| dec8 | DEC West European | dec8_swedish_ci | 1 |
|
||||
| cp850 | DOS West European | cp850_general_ci | 1 |
|
||||
| hp8 | HP West European | hp8_english_ci | 1 |
|
||||
| koi8r | KOI8-R Relcom Russian | koi8r_general_ci | 1 |
|
||||
| latin1 | cp1252 West European | latin1_swedish_ci | 1 |
|
||||
| latin2 | ISO 8859-2 Central European | latin2_general_ci | 1 |
|
||||
| swe7 | 7bit Swedish | swe7_swedish_ci | 1 |
|
||||
| ascii | US ASCII | ascii_general_ci | 1 |
|
||||
| ujis | EUC-JP Japanese | ujis_japanese_ci | 3 |
|
||||
| sjis | Shift-JIS Japanese | sjis_japanese_ci | 2 |
|
||||
| hebrew | ISO 8859-8 Hebrew | hebrew_general_ci | 1 |
|
||||
| tis620 | TIS620 Thai | tis620_thai_ci | 1 |
|
||||
| euckr | EUC-KR Korean | euckr_korean_ci | 2 |
|
||||
| koi8u | KOI8-U Ukrainian | koi8u_general_ci | 1 |
|
||||
| gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 |
|
||||
| greek | ISO 8859-7 Greek | greek_general_ci | 1 |
|
||||
| cp1250 | Windows Central European | cp1250_general_ci | 1 |
|
||||
| gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 |
|
||||
| latin5 | ISO 8859-9 Turkish | latin5_turkish_ci | 1 |
|
||||
| armscii8 | ARMSCII-8 Armenian | armscii8_general_ci | 1 |
|
||||
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
|
||||
| ucs2 | UCS-2 Unicode | ucs2_general_ci | 2 |
|
||||
| cp866 | DOS Russian | cp866_general_ci | 1 |
|
||||
| keybcs2 | DOS Kamenicky Czech-Slovak | keybcs2_general_ci | 1 |
|
||||
| macce | Mac Central European | macce_general_ci | 1 |
|
||||
| macroman | Mac West European | macroman_general_ci | 1 |
|
||||
| cp852 | DOS Central European | cp852_general_ci | 1 |
|
||||
| latin7 | ISO 8859-13 Baltic | latin7_general_ci | 1 |
|
||||
| utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
|
||||
| cp1251 | Windows Cyrillic | cp1251_general_ci | 1 |
|
||||
| utf16 | UTF-16 Unicode | utf16_general_ci | 4 |
|
||||
| utf16le | UTF-16LE Unicode | utf16le_general_ci | 4 |
|
||||
| cp1256 | Windows Arabic | cp1256_general_ci | 1 |
|
||||
| cp1257 | Windows Baltic | cp1257_general_ci | 1 |
|
||||
| utf32 | UTF-32 Unicode | utf32_general_ci | 4 |
|
||||
| binary | Binary pseudo charset | binary | 1 |
|
||||
| geostd8 | GEOSTD8 Georgian | geostd8_general_ci | 1 |
|
||||
| cp932 | SJIS for Windows Japanese | cp932_japanese_ci | 2 |
|
||||
| eucjpms | UJIS for Windows Japanese | eucjpms_japanese_ci | 3 |
|
||||
| gb18030 | China National Standard GB18030 | gb18030_chinese_ci | 4 |
|
||||
+----------+---------------------------------+---------------------+--------+
|
||||
41 rows in set (0.00 sec)
|
||||
```
|
||||
|
||||
如果要设置 MySQL 服务启动时默认使用的字符集,可以修改MySQL的配置并添加以下内容。
|
||||
|
||||
```INI
|
||||
[mysqld]
|
||||
character-set-server=utf8
|
||||
```
|
||||
|
||||
- 在创建表的时候,可以自行选择底层的存储引擎。MySQL 支持多种存储引擎,可以通过`show engines`命令进行查看。MySQL 5.5 以后的版本默认使用的存储引擎是 InnoDB,它是我们推荐大家使用的存储引擎(因为更适合当下互联网应用对高并发、性能以及事务支持等方面的需求),为了 SQL 语句的向下兼容性,我们可以在建表语句结束处右圆括号的后面通过`engine=innodb`来指定使用 InnoDB 存储引擎。
|
||||
|
||||
```SQL
|
||||
show engines\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
Engine: InnoDB
|
||||
Support: DEFAULT
|
||||
Comment: Supports transactions, row-level locking, and foreign keys
|
||||
Transactions: YES
|
||||
XA: YES
|
||||
Savepoints: YES
|
||||
*************************** 2. row ***************************
|
||||
Engine: MRG_MYISAM
|
||||
Support: YES
|
||||
Comment: Collection of identical MyISAM tables
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 3. row ***************************
|
||||
Engine: MEMORY
|
||||
Support: YES
|
||||
Comment: Hash based, stored in memory, useful for temporary tables
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 4. row ***************************
|
||||
Engine: BLACKHOLE
|
||||
Support: YES
|
||||
Comment: /dev/null storage engine (anything you write to it disappears)
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 5. row ***************************
|
||||
Engine: MyISAM
|
||||
Support: YES
|
||||
Comment: MyISAM storage engine
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 6. row ***************************
|
||||
Engine: CSV
|
||||
Support: YES
|
||||
Comment: CSV storage engine
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 7. row ***************************
|
||||
Engine: ARCHIVE
|
||||
Support: YES
|
||||
Comment: Archive storage engine
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 8. row ***************************
|
||||
Engine: PERFORMANCE_SCHEMA
|
||||
Support: YES
|
||||
Comment: Performance Schema
|
||||
Transactions: NO
|
||||
XA: NO
|
||||
Savepoints: NO
|
||||
*************************** 9. row ***************************
|
||||
Engine: FEDERATED
|
||||
Support: NO
|
||||
Comment: Federated MySQL storage engine
|
||||
Transactions: NULL
|
||||
XA: NULL
|
||||
Savepoints: NULL
|
||||
9 rows in set (0.00 sec)
|
||||
```
|
||||
|
||||
下面的表格对MySQL几种常用的数据引擎进行了简单的对比。
|
||||
|
||||
| 特性 | InnoDB | MRG_MYISAM | MEMORY | MyISAM |
|
||||
| ------------ | ------------ | ---------- | ------ | ------ |
|
||||
| 存储限制 | 有 | 没有 | 有 | 有 |
|
||||
| 事务 | 支持 | | | |
|
||||
| 锁机制 | 行锁 | 表锁 | 表锁 | 表锁 |
|
||||
| B树索引 | 支持 | 支持 | 支持 | 支持 |
|
||||
| 哈希索引 | | | 支持 | |
|
||||
| 全文检索 | 支持(5.6+) | | | 支持 |
|
||||
| 集群索引 | 支持 | | | |
|
||||
| 数据缓存 | 支持 | | 支持 | |
|
||||
| 索引缓存 | 支持 | 支持 | 支持 | 支持 |
|
||||
| 数据可压缩 | | | | 支持 |
|
||||
| 内存使用 | 高 | 低 | 中 | 低 |
|
||||
| 存储空间使用 | 高 | 低 | | 低 |
|
||||
| 批量插入性能 | 低 | 高 | 高 | 高 |
|
||||
| 是否支持外键 | 支持 | | | |
|
||||
|
||||
通过上面的比较我们可以了解到,InnoDB 是唯一能够支持外键、事务以及行锁的存储引擎,所以我们之前说它更适合互联网应用,而且在较新版本的 MySQL 中,它也是默认使用的存储引擎。
|
||||
|
||||
- 在定义表结构为每个字段选择数据类型时,如果不清楚哪个数据类型更合适,可以通过 MySQL 的帮助系统来了解每种数据类型的特性、数据的长度和精度等相关信息。
|
||||
|
||||
```SQL
|
||||
? data types
|
||||
```
|
||||
|
||||
```
|
||||
You asked for help about help category: "Data Types"
|
||||
For more information, type 'help <item>', where <item> is one of the following
|
||||
topics:
|
||||
AUTO_INCREMENT
|
||||
BIGINT
|
||||
BINARY
|
||||
BIT
|
||||
BLOB
|
||||
BLOB DATA TYPE
|
||||
BOOLEAN
|
||||
CHAR
|
||||
CHAR BYTE
|
||||
DATE
|
||||
DATETIME
|
||||
DEC
|
||||
DECIMAL
|
||||
DOUBLE
|
||||
DOUBLE PRECISION
|
||||
ENUM
|
||||
FLOAT
|
||||
INT
|
||||
INTEGER
|
||||
LONGBLOB
|
||||
LONGTEXT
|
||||
MEDIUMBLOB
|
||||
MEDIUMINT
|
||||
MEDIUMTEXT
|
||||
SET DATA TYPE
|
||||
SMALLINT
|
||||
TEXT
|
||||
TIME
|
||||
TIMESTAMP
|
||||
TINYBLOB
|
||||
TINYINT
|
||||
TINYTEXT
|
||||
VARBINARY
|
||||
VARCHAR
|
||||
YEAR DATA TYPE
|
||||
```
|
||||
|
||||
```SQL
|
||||
? varchar
|
||||
```
|
||||
|
||||
```
|
||||
Name: 'VARCHAR'
|
||||
Description:
|
||||
[NATIONAL] VARCHAR(M) [CHARACTER SET charset_name] [COLLATE
|
||||
collation_name]
|
||||
|
||||
A variable-length string. M represents the maximum column length in
|
||||
characters. The range of M is 0 to 65,535. The effective maximum length
|
||||
of a VARCHAR is subject to the maximum row size (65,535 bytes, which is
|
||||
shared among all columns) and the character set used. For example, utf8
|
||||
characters can require up to three bytes per character, so a VARCHAR
|
||||
column that uses the utf8 character set can be declared to be a maximum
|
||||
of 21,844 characters. See
|
||||
http://dev.mysql.com/doc/refman/5.7/en/column-count-limit.html.
|
||||
|
||||
MySQL stores VARCHAR values as a 1-byte or 2-byte length prefix plus
|
||||
data. The length prefix indicates the number of bytes in the value. A
|
||||
VARCHAR column uses one length byte if values require no more than 255
|
||||
bytes, two length bytes if values may require more than 255 bytes.
|
||||
|
||||
*Note*:
|
||||
|
||||
MySQL follows the standard SQL specification, and does not remove
|
||||
trailing spaces from VARCHAR values.
|
||||
|
||||
VARCHAR is shorthand for CHARACTER VARYING. NATIONAL VARCHAR is the
|
||||
standard SQL way to define that a VARCHAR column should use some
|
||||
predefined character set. MySQL uses utf8 as this predefined character
|
||||
set. http://dev.mysql.com/doc/refman/5.7/en/charset-national.html.
|
||||
NVARCHAR is shorthand for NATIONAL VARCHAR.
|
||||
|
||||
URL: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
|
||||
```
|
||||
|
||||
在数据类型的选择上,保存字符串数据通常都使用`VARCHAR`和`CHAR`两种类型,前者通常称为变长字符串,而后者通常称为定长字符串;对于 InnoDB 存储引擎,行存储格式没有区分固定长度和可变长度列,因此`VARCHAR`类型和`CHAR`类型没有本质区别,后者不一定比前者性能更好。如果要保存的很大字符串,可以使用`TEXT`类型;如果要保存很大的字节串,可以使用`BLOB`(二进制大对象)类型。在 MySQL 中,`TEXT`和`BLOB`又分别包括`TEXT`、`MEDIUMTEXT`、`LONGTEXT`和`BLOB`、`MEDIUMBLOB`、`LONGBLOB`三种不同的类型,它们主要的区别在于存储数据的最大大小不同。保存浮点数可以用`FLOAT`或`DOUBLE`类型,`FLOAT`已经不推荐使用了,而且在 MySQL 后续的版本中可能会被移除掉。而保存定点数应该使用`DECIMAL`类型。如果要保存时间日期,`DATETIME`类型优于`TIMESTAMP`类型,因为前者能表示的时间日期范围更大。
|
||||
|
||||
### DML(数据操作语言)
|
||||
|
||||
我们通过如下所示的 SQL 给上面创建的表添加数据。
|
||||
|
||||
```SQL
|
||||
use school;
|
||||
|
||||
-- 插入学院数据
|
||||
insert into `tb_college`
|
||||
(`col_name`, `col_intro`)
|
||||
values
|
||||
('计算机学院', '计算机学院1958年设立计算机专业,1981年建立计算机科学系,1998年设立计算机学院,2005年5月,为了进一步整合教学和科研资源,学校决定,计算机学院和软件学院行政班子合并统一运作、实行教学和学生管理独立运行的模式。 学院下设三个系:计算机科学与技术系、物联网工程系、计算金融系;两个研究所:图象图形研究所、网络空间安全研究院(2015年成立);三个教学实验中心:计算机基础教学实验中心、IBM技术中心和计算机专业实验中心。'),
|
||||
('外国语学院', '外国语学院设有7个教学单位,6个文理兼收的本科专业;拥有1个一级学科博士授予点,3个二级学科博士授予点,5个一级学科硕士学位授权点,5个二级学科硕士学位授权点,5个硕士专业授权领域,同时还有2个硕士专业学位(MTI)专业;有教职员工210余人,其中教授、副教授80余人,教师中获得中国国内外名校博士学位和正在职攻读博士学位的教师比例占专任教师的60%以上。'),
|
||||
('经济管理学院', '经济学院前身是创办于1905年的经济科;已故经济学家彭迪先、张与九、蒋学模、胡寄窗、陶大镛、胡代光,以及当代学者刘诗白等曾先后在此任教或学习。');
|
||||
|
||||
-- 插入学生数据
|
||||
insert into `tb_student`
|
||||
(`stu_id`, `stu_name`, `stu_sex`, `stu_birth`, `stu_addr`, `col_id`)
|
||||
values
|
||||
(1001, '杨过', 1, '1990-3-4', '湖南长沙', 1),
|
||||
(1002, '任我行', 1, '1992-2-2', '湖南长沙', 1),
|
||||
(1033, '王语嫣', 0, '1989-12-3', '四川成都', 1),
|
||||
(1572, '岳不群', 1, '1993-7-19', '陕西咸阳', 1),
|
||||
(1378, '纪嫣然', 0, '1995-8-12', '四川绵阳', 1),
|
||||
(1954, '林平之', 1, '1994-9-20', '福建莆田', 1),
|
||||
(2035, '东方不败', 1, '1988-6-30', null, 2),
|
||||
(3011, '林震南', 1, '1985-12-12', '福建莆田', 3),
|
||||
(3755, '项少龙', 1, '1993-1-25', '四川成都', 3),
|
||||
(3923, '杨不悔', 0, '1985-4-17', '四川成都', 3);
|
||||
|
||||
-- 插入老师数据
|
||||
insert into `tb_teacher`
|
||||
(`tea_id`, `tea_name`, `tea_title`, `col_id`)
|
||||
values
|
||||
(1122, '张三丰', '教授', 1),
|
||||
(1133, '宋远桥', '副教授', 1),
|
||||
(1144, '杨逍', '副教授', 1),
|
||||
(2255, '范遥', '副教授', 2),
|
||||
(3366, '韦一笑', default, 3);
|
||||
|
||||
-- 插入课程数据
|
||||
insert into `tb_course`
|
||||
(`cou_id`, `cou_name`, `cou_credit`, `tea_id`)
|
||||
values
|
||||
(1111, 'Python程序设计', 3, 1122),
|
||||
(2222, 'Web前端开发', 2, 1122),
|
||||
(3333, '操作系统', 4, 1122),
|
||||
(4444, '计算机网络', 2, 1133),
|
||||
(5555, '编译原理', 4, 1144),
|
||||
(6666, '算法和数据结构', 3, 1144),
|
||||
(7777, '经贸法语', 3, 2255),
|
||||
(8888, '成本会计', 2, 3366),
|
||||
(9999, '审计学', 3, 3366);
|
||||
|
||||
-- 插入选课数据
|
||||
insert into `tb_record`
|
||||
(`stu_id`, `cou_id`, `sel_date`, `score`)
|
||||
values
|
||||
(1001, 1111, '2017-09-01', 95),
|
||||
(1001, 2222, '2017-09-01', 87.5),
|
||||
(1001, 3333, '2017-09-01', 100),
|
||||
(1001, 4444, '2018-09-03', null),
|
||||
(1001, 6666, '2017-09-02', 100),
|
||||
(1002, 1111, '2017-09-03', 65),
|
||||
(1002, 5555, '2017-09-01', 42),
|
||||
(1033, 1111, '2017-09-03', 92.5),
|
||||
(1033, 4444, '2017-09-01', 78),
|
||||
(1033, 5555, '2017-09-01', 82.5),
|
||||
(1572, 1111, '2017-09-02', 78),
|
||||
(1378, 1111, '2017-09-05', 82),
|
||||
(1378, 7777, '2017-09-02', 65.5),
|
||||
(2035, 7777, '2018-09-03', 88),
|
||||
(2035, 9999, '2019-09-02', null),
|
||||
(3755, 1111, '2019-09-02', null),
|
||||
(3755, 8888, '2019-09-02', null),
|
||||
(3755, 9999, '2017-09-01', 92);
|
||||
```
|
||||
|
||||
> **注意**:上面的`insert`语句使用了批处理的方式来插入数据,这种做法插入数据的效率比较高。
|
||||
|
||||
### DQL(数据查询语言)
|
||||
|
||||
接下来,我们完成如下所示的查询。
|
||||
|
||||
```SQL
|
||||
-- 查询所有学生的所有信息
|
||||
select * from `tb_student`;
|
||||
|
||||
-- 查询学生的学号、姓名和籍贯(投影)
|
||||
select `stu_id`, `stu_name`, `stu_addr` from `tb_student`;
|
||||
|
||||
-- 查询所有课程的名称及学分(投影和别名)
|
||||
select `cou_name` as 课程名称, `cou_credit` as 学分 from `tb_course`;
|
||||
|
||||
-- 查询所有女学生的姓名和出生日期(筛选)
|
||||
select `stu_name`, `stu_birth` from `tb_student` where `stu_sex`=0;
|
||||
|
||||
-- 查询籍贯为“四川成都”的女学生的姓名和出生日期(筛选)
|
||||
select `stu_name`, `stu_birth` from `tb_student` where `stu_sex`=0 and `stu_addr`='四川成都';
|
||||
|
||||
-- 查询籍贯为“四川成都”或者性别为“女生”的学生
|
||||
select `stu_name`, `stu_birth` from `tb_student` where `stu_sex`=0 or `stu_addr`='四川成都';
|
||||
|
||||
-- 查询所有80后学生的姓名、性别和出生日期(筛选)
|
||||
select `stu_name`, `stu_sex`, `stu_birth` from `tb_student`
|
||||
where `stu_birth`>='1980-1-1' and `stu_birth`<='1989-12-31';
|
||||
|
||||
select `stu_name`, `stu_sex`, `stu_birth` from `tb_student`
|
||||
where `stu_birth` between '1980-1-1' and '1989-12-31';
|
||||
|
||||
-- 补充:将表示性别的 1 和 0 处理成 “男” 和 “女”
|
||||
select
|
||||
`stu_name` as 姓名,
|
||||
if(`stu_sex`, '男', '女') as 性别,
|
||||
`stu_birth` as 出生日期
|
||||
from `tb_student`
|
||||
where `stu_birth` between '1980-1-1' and '1989-12-31';
|
||||
|
||||
select
|
||||
`stu_name` as 姓名,
|
||||
case `stu_sex` when 1 then '男' else '女' end as 性别,
|
||||
`stu_birth` as 出生日期
|
||||
from `tb_student`
|
||||
where `stu_birth` between '1980-1-1' and '1989-12-31';
|
||||
|
||||
-- 查询学分大于2的课程的名称和学分(筛选)
|
||||
select `cou_name`, `cou_credit` from `tb_course` where `cou_credit`>2;
|
||||
|
||||
-- 查询学分是奇数的课程的名称和学分(筛选)
|
||||
select `cou_name`, `cou_credit` from `tb_course` where `cou_credit`%2<>0;
|
||||
|
||||
select `cou_name`, `cou_credit` from `tb_course` where `cou_credit` mod 2<>0;
|
||||
|
||||
-- 查询选择选了1111的课程考试成绩在90分以上的学生学号(筛选)
|
||||
select `stu_id` from `tb_record` where `cou_id`=1111 and `score`>90;
|
||||
|
||||
-- 查询名字叫“杨过”的学生的姓名和性别
|
||||
select `stu_name`, `stu_sex` from `tb_student` where `stu_name`='杨过';
|
||||
|
||||
-- 查询姓“杨”的学生姓名和性别(模糊)
|
||||
-- % - 通配符(wildcard),它可以匹配0个或任意多个字符
|
||||
select `stu_name`, `stu_sex` from `tb_student` where `stu_name` like '杨%';
|
||||
|
||||
-- 查询姓“杨”名字两个字的学生姓名和性别(模糊)
|
||||
-- _ - 通配符(wildcard),它可以精确匹配一个字符
|
||||
select `stu_name`, `stu_sex` from `tb_student` where `stu_name` like '杨_';
|
||||
|
||||
-- 查询姓“杨”名字三个字的学生姓名和性别(模糊)
|
||||
select `stu_name`, `stu_sex` from `tb_student` where `stu_name` like '杨__';
|
||||
|
||||
-- 查询名字中有“不”字或“嫣”字的学生的姓名(模糊)
|
||||
select `stu_name` from `tb_student` where `stu_name` like '%不%' or `stu_name` like '%嫣%';
|
||||
|
||||
-- 将“岳不群”改名为“岳不嫣”,比较下面两个查询的区别
|
||||
update `tb_student` set `stu_name`='岳不嫣' where `stu_id`=1572;
|
||||
|
||||
select `stu_name` from `tb_student` where `stu_name` like '%不%'
|
||||
union
|
||||
select `stu_name` from `tb_student` where `stu_name` like '%嫣%';
|
||||
|
||||
select `stu_name` from `tb_student` where `stu_name` like '%不%'
|
||||
union all
|
||||
select `stu_name` from `tb_student` where `stu_name` like '%嫣%';
|
||||
|
||||
-- 查询姓“杨”或姓“林”名字三个字的学生的姓名(正则表达式模糊查询)
|
||||
select `stu_name` from `tb_student` where `stu_name` regexp '[杨林].{2}';
|
||||
|
||||
-- 查询没有录入籍贯的学生姓名(空值处理)
|
||||
select `stu_name` from `tb_student` where `stu_addr` is null;
|
||||
|
||||
select `stu_name` from `tb_student` where `stu_addr` <=> null;
|
||||
|
||||
-- 查询录入了籍贯的学生姓名(空值处理)
|
||||
select `stu_name` from `tb_student` where `stu_addr` is not null;
|
||||
|
||||
-- 下面的查询什么也查不到,三值逻辑 --> true / false / unknown
|
||||
select `stu_name` from `tb_student` where `stu_addr`=null or `stu_addr`<>null;
|
||||
|
||||
-- 查询学生选课的所有日期(去重)
|
||||
select distinct `sel_date` from `tb_record`;
|
||||
|
||||
-- 查询学生的籍贯(去重)
|
||||
select distinct `stu_addr` from `tb_student` where `stu_addr` is not null;
|
||||
|
||||
-- 查询男学生的姓名和生日按年龄从大到小排列(排序)
|
||||
-- 升序:从小到大 - asc,降序:从大到小 - desc
|
||||
select `stu_id`, `stu_name`, `stu_birth` from `tb_student`
|
||||
where `stu_sex`=1 order by `stu_birth` asc, `stu_id` desc;
|
||||
|
||||
-- 补充:将上面的生日换算成年龄(日期函数、数值函数)
|
||||
select
|
||||
`stu_id` as 学号,
|
||||
`stu_name` as 姓名,
|
||||
floor(datediff(curdate(), `stu_birth`)/365) as 年龄
|
||||
from `tb_student`
|
||||
where `stu_sex`=1 order by 年龄 desc, `stu_id` desc;
|
||||
|
||||
-- 查询年龄最大的学生的出生日期(聚合函数)
|
||||
select min(`stu_birth`) from `tb_student`;
|
||||
|
||||
-- 查询年龄最小的学生的出生日期(聚合函数)
|
||||
select max(`stu_birth`) from `tb_student`;
|
||||
|
||||
-- 查询编号为1111的课程考试成绩的最高分(聚合函数)
|
||||
select max(`score`) from `tb_record` where `cou_id`=1111;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的最低分(聚合函数)
|
||||
select min(`score`) from `tb_record` where `stu_id`=1001;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的平均分(聚合函数)
|
||||
select avg(`score`) from `tb_record` where `stu_id`=1001;
|
||||
|
||||
select sum(`score`) / count(`score`) from `tb_record` where `stu_id`=1001;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的平均分,如果有null值,null值算0分(聚合函数)
|
||||
select sum(`score`) / count(*) from `tb_record` where `stu_id`=1001;
|
||||
|
||||
select avg(ifnull(`score`, 0)) from `tb_record` where `stu_id`=1001;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的标准差(聚合函数)
|
||||
select std(`score`), variance(`score`) from `tb_record` where `stu_id`=1001;
|
||||
|
||||
-- 查询男女学生的人数(分组和聚合函数)
|
||||
select
|
||||
case `stu_sex` when 1 then '男' else '女' end as 性别,
|
||||
count(*) as 人数
|
||||
from `tb_student` group by `stu_sex`;
|
||||
|
||||
-- 查询每个学院学生人数(分组和聚合函数)
|
||||
select
|
||||
`col_id` as 学院,
|
||||
count(*) as 人数
|
||||
from `tb_student` group by `col_id` with rollup;
|
||||
|
||||
-- 查询每个学院男女学生人数(分组和聚合函数)
|
||||
select
|
||||
`col_id` as 学院,
|
||||
if(`stu_sex`, '男', '女') as 性别,
|
||||
count(*) as 人数
|
||||
from `tb_student` group by `col_id`, `stu_sex`;
|
||||
|
||||
-- 查询每个学生的学号和平均成绩(分组和聚合函数)
|
||||
select
|
||||
`stu_id`,
|
||||
round(avg(`score`), 1) as avg_score
|
||||
from `tb_record` group by `stu_id`;
|
||||
|
||||
-- 查询平均成绩大于等于90分的学生的学号和平均成绩
|
||||
-- 分组以前的筛选使用where子句,分组以后的筛选使用having子句
|
||||
select
|
||||
`stu_id`,
|
||||
round(avg(`score`), 1) as avg_score
|
||||
from `tb_record`
|
||||
group by `stu_id` having avg_score>=90;
|
||||
|
||||
-- 查询1111、2222、3333三门课程平均成绩大于等于90分的学生的学号和平均成绩
|
||||
select
|
||||
`stu_id`,
|
||||
round(avg(`score`), 1) as avg_score
|
||||
from `tb_record` where `cou_id` in (1111, 2222, 3333)
|
||||
group by `stu_id` having avg_score>=90;
|
||||
|
||||
-- 查询年龄最大的学生的姓名(子查询/嵌套查询)
|
||||
-- 嵌套查询:把一个select的结果作为另一个select的一部分来使用
|
||||
select `stu_name` from `tb_student`
|
||||
where `stu_birth`=(
|
||||
select min(`stu_birth`) from `tb_student`
|
||||
);
|
||||
|
||||
-- 查询选了两门以上的课程的学生姓名(子查询/分组条件/集合运算)
|
||||
select `stu_name` from `tb_student`
|
||||
where `stu_id` in (
|
||||
select `stu_id` from `tb_record`
|
||||
group by `stu_id` having count(*)>2
|
||||
);
|
||||
|
||||
-- 查询学生的姓名、生日和所在学院名称
|
||||
select `stu_name`, `stu_birth`, `col_name`
|
||||
from `tb_student`, `tb_college`
|
||||
where `tb_student`.`col_id`=`tb_college`.`col_id`;
|
||||
|
||||
select `stu_name`, `stu_birth`, `col_name`
|
||||
from `tb_student` inner join `tb_college`
|
||||
on `tb_student`.`col_id`=`tb_college`.`col_id`;
|
||||
|
||||
select `stu_name`, `stu_birth`, `col_name`
|
||||
from `tb_student` natural join `tb_college`;
|
||||
|
||||
-- 查询学生姓名、课程名称以及成绩(连接查询/联结查询)
|
||||
select `stu_name`, `cou_name`, `score`
|
||||
from `tb_student`, `tb_course`, `tb_record`
|
||||
where `tb_student`.`stu_id`=`tb_record`.`stu_id`
|
||||
and `tb_course`.`cou_id`=`tb_record`.`cou_id`
|
||||
and `score` is not null;
|
||||
|
||||
select `stu_name`, `cou_name`, `score` from `tb_student`
|
||||
inner join `tb_record` on `tb_student`.`stu_id`=`tb_record`.`stu_id`
|
||||
inner join `tb_course` on `tb_course`.`cou_id`=`tb_record`.`cou_id`
|
||||
where `score` is not null;
|
||||
|
||||
select `stu_name`, `cou_name`, `score` from `tb_student`
|
||||
natural join `tb_record`
|
||||
natural join `tb_course`
|
||||
where `score` is not null;
|
||||
|
||||
-- 补充:上面的查询结果取前5条数据(分页查询)
|
||||
select `stu_name`, `cou_name`, `score`
|
||||
from `tb_student`, `tb_course`, `tb_record`
|
||||
where `tb_student`.`stu_id`=`tb_record`.`stu_id`
|
||||
and `tb_course`.`cou_id`=`tb_record`.`cou_id`
|
||||
and `score` is not null
|
||||
order by `score` desc
|
||||
limit 0,5;
|
||||
|
||||
-- 补充:上面的查询结果取第6-10条数据(分页查询)
|
||||
select `stu_name`, `cou_name`, `score`
|
||||
from `tb_student`, `tb_course`, `tb_record`
|
||||
where `tb_student`.`stu_id`=`tb_record`.`stu_id`
|
||||
and `tb_course`.`cou_id`=`tb_record`.`cou_id`
|
||||
and `score` is not null
|
||||
order by `score` desc
|
||||
limit 5 offset 5;
|
||||
|
||||
-- 补充:上面的查询结果取第11-15条数据(分页查询)
|
||||
select `stu_name`, `cou_name`, `score`
|
||||
from `tb_student`, `tb_course`, `tb_record`
|
||||
where `tb_student`.`stu_id`=`tb_record`.`stu_id`
|
||||
and `tb_course`.`cou_id`=`tb_record`.`cou_id`
|
||||
and `score` is not null
|
||||
order by `score` desc
|
||||
limit 5 offset 10;
|
||||
|
||||
-- 查询选课学生的姓名和平均成绩(子查询和连接查询)
|
||||
select `stu_name`, `avg_score`
|
||||
from `tb_student` inner join (
|
||||
select `stu_id` as `sid`, round(avg(`score`), 1) as avg_score
|
||||
from `tb_record` group by `stu_id`
|
||||
) as `t2` on `stu_id`=`sid`;
|
||||
|
||||
-- 查询学生的姓名和选课的数量
|
||||
select `stu_name`, `total` from `tb_student` as `t1`
|
||||
inner join (
|
||||
select `stu_id`, count(*) as `total`
|
||||
from `tb_record` group by `stu_id`
|
||||
) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
|
||||
|
||||
-- 查询每个学生的姓名和选课数量(左外连接和子查询)
|
||||
-- 左外连接:左表(写在join左边的表)的每条记录都可以查出来,不满足连表条件的地方填充null。
|
||||
select `stu_name`, coalesce(`total`, 0) as `total`
|
||||
from `tb_student` as `t1`
|
||||
left outer join (
|
||||
select `stu_id`, count(*) as `total`
|
||||
from `tb_record` group by `stu_id`
|
||||
) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
|
||||
|
||||
-- 修改选课记录表,去掉 stu_id 列的外键约束
|
||||
alter table `tb_record` drop foreign key `fk_record_stu_id`;
|
||||
|
||||
-- 插入两条新纪录(注意:没有学号为 5566 的学生)
|
||||
insert into `tb_record`
|
||||
values
|
||||
(default, 5566, 1111, '2019-09-02', 80),
|
||||
(default, 5566, 2222, '2019-09-02', 70);
|
||||
|
||||
-- 右外连接:右表(写在join右边的表)的每条记录都可以查出来,不满足连表条件的地方填充null。
|
||||
select `stu_name`, `total` from `tb_student` as `t1`
|
||||
right outer join (
|
||||
select `stu_id`, count(*) as `total`
|
||||
from `tb_record` group by `stu_id`
|
||||
) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
|
||||
|
||||
-- 全外连接:左表和右表的每条记录都可以查出来,不满足连表条件的地方填充null。
|
||||
-- 说明:MySQL不支持全外连接,所以用左外连接和右外连接的并集来表示。
|
||||
select `stu_name`, `total`
|
||||
from `tb_student` as `t1`
|
||||
left outer join (
|
||||
select `stu_id`, count(*) as `total`
|
||||
from `tb_record` group by `stu_id`
|
||||
) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`
|
||||
union
|
||||
select `stu_name`, `total` from `tb_student` as `t1`
|
||||
right outer join (
|
||||
select `stu_id`, count(*) as `total`
|
||||
from `tb_record` group by `stu_id`
|
||||
) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
|
||||
```
|
||||
|
||||
上面的DQL有几个地方需要加以说明:
|
||||
|
||||
1. MySQL目前的版本不支持全外连接,上面我们通过`union`操作,将左外连接和右外连接的结果求并集实现全外连接的效果。大家可以通过下面的图来加深对连表操作的认识。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211121135117.png" style="zoom:50%">
|
||||
|
||||
2. MySQL 中支持多种类型的运算符,包括:算术运算符(`+`、`-`、`*`、`/`、`%`)、比较运算符(`=`、`<>`、`<=>`、`<`、`<=`、`>`、`>=`、`BETWEEN...AND..`.、`IN`、`IS NULL`、`IS NOT NULL`、`LIKE`、`RLIKE`、`REGEXP`)、逻辑运算符(`NOT`、`AND`、`OR`、`XOR`)和位运算符(`&`、`|`、`^`、`~`、`>>`、`<<`),我们可以在 DML 中使用这些运算符处理数据。
|
||||
|
||||
3. 在查询数据时,可以在`SELECT`语句及其子句(如`WHERE`子句、`ORDER BY`子句、`HAVING`子句等)中使用函数,这些函数包括字符串函数、数值函数、时间日期函数、流程函数等,如下面的表格所示。
|
||||
|
||||
常用字符串函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| --------------------------- | ----------------------------------------------------- |
|
||||
| `CONCAT` | 将多个字符串连接成一个字符串 |
|
||||
| `FORMAT` | 将数值格式化成字符串并指定保留几位小数 |
|
||||
| `FROM_BASE64` / `TO_BASE64` | BASE64解码/编码 |
|
||||
| `BIN` / `OCT` / `HEX` | 将数值转换成二进制/八进制/十六进制字符串 |
|
||||
| `LOCATE` | 在字符串中查找一个子串的位置 |
|
||||
| `LEFT` / `RIGHT` | 返回一个字符串左边/右边指定长度的字符 |
|
||||
| `LENGTH` / `CHAR_LENGTH` | 返回字符串的长度以字节/字符为单位 |
|
||||
| `LOWER` / `UPPER` | 返回字符串的小写/大写形式 |
|
||||
| `LPAD` / `RPAD` | 如果字符串的长度不足,在字符串左边/右边填充指定的字符 |
|
||||
| `LTRIM` / `RTRIM` | 去掉字符串前面/后面的空格 |
|
||||
| `ORD` / `CHAR` | 返回字符对应的编码/返回编码对应的字符 |
|
||||
| `STRCMP` | 比较字符串,返回-1、0、1分别表示小于、等于、大于 |
|
||||
| `SUBSTRING` | 返回字符串指定范围的子串 |
|
||||
|
||||
常用数值函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| -------------------------------------------------------- | ---------------------------------- |
|
||||
| `ABS` | 返回一个数的绝度值 |
|
||||
| `CEILING` / `FLOOR` | 返回一个数上取整/下取整的结果 |
|
||||
| `CONV` | 将一个数从一种进制转换成另一种进制 |
|
||||
| `CRC32` | 计算循环冗余校验码 |
|
||||
| `EXP` / `LOG` / `LOG2` / `LOG10` | 计算指数/对数 |
|
||||
| `POW` | 求幂 |
|
||||
| `RAND` | 返回[0,1)范围的随机数 |
|
||||
| `ROUND` | 返回一个数四舍五入后的结果 |
|
||||
| `SQRT` | 返回一个数的平方根 |
|
||||
| `TRUNCATE` | 截断一个数到指定的精度 |
|
||||
| `SIN` / `COS` / `TAN` / `COT` / `ASIN` / `ACOS` / `ATAN` | 三角函数 |
|
||||
|
||||
常用时间日期函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| ----------------------------- | ------------------------------------- |
|
||||
| `CURDATE` / `CURTIME` / `NOW` | 获取当前日期/时间/日期和时间 |
|
||||
| `ADDDATE` / `SUBDATE` | 将两个日期表达式相加/相减并返回结果 |
|
||||
| `DATE` / `TIME` | 从字符串中获取日期/时间 |
|
||||
| `YEAR` / `MONTH` / `DAY` | 从日期中获取年/月/日 |
|
||||
| `HOUR` / `MINUTE` / `SECOND` | 从时间中获取时/分/秒 |
|
||||
| `DATEDIFF` / `TIMEDIFF` | 返回两个时间日期表达式相差多少天/小时 |
|
||||
| `MAKEDATE` / `MAKETIME` | 制造一个日期/时间 |
|
||||
|
||||
常用流程函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| -------- | ------------------------------------------------ |
|
||||
| `IF` | 根据条件是否成立返回不同的值 |
|
||||
| `IFNULL` | 如果为NULL则返回指定的值否则就返回本身 |
|
||||
| `NULLIF` | 两个表达式相等就返回NULL否则返回第一个表达式的值 |
|
||||
|
||||
其他常用函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| -------------------------- | ----------------------------- |
|
||||
| `MD5` / `SHA1` / `SHA2` | 返回字符串对应的哈希摘要 |
|
||||
| `CHARSET` / `COLLATION` | 返回字符集/校对规则 |
|
||||
| `USER` / `CURRENT_USER` | 返回当前用户 |
|
||||
| `DATABASE` | 返回当前数据库名 |
|
||||
| `VERSION` | 返回当前数据库版本 |
|
||||
| `FOUND_ROWS` / `ROW_COUNT` | 返回查询到的行数/受影响的行数 |
|
||||
| `LAST_INSERT_ID` | 返回最后一个自增主键的值 |
|
||||
| `UUID` / `UUID_SHORT` | 返回全局唯一标识符 |
|
||||
|
||||
### DCL(数据控制语言)
|
||||
|
||||
数据控制语言用于给指定的用户授权或者从召回指定用户的指定权限,这组操作对数据库管理员来说比较重要,将一个用户的权限最小化(刚好够用)是非常重要的,对数据库的安全至关重要。
|
||||
|
||||
```SQL
|
||||
-- 创建名为 wangdachui 的账号并为其指定口令,允许该账号从任意主机访问
|
||||
create user 'wangdachui'@'%' identified by '123456';
|
||||
|
||||
-- 授权 wangdachui 可以对名为school的数据库执行 select 和 insert 操作
|
||||
grant select, insert on `school`.* to 'wangdachui'@'%';
|
||||
|
||||
-- 召回 wangdachui 对school数据库的 insert 权限
|
||||
revoke insert on `school`.* from 'wangdachui'@'%';
|
||||
```
|
||||
|
||||
> **说明**:创建一个可以允许任意主机登录并且具有超级管理员权限的用户在现实中并不是一个明智的决定,因为一旦该账号的口令泄露或者被破解,数据库将会面临灾难级的风险。
|
|
@ -0,0 +1,159 @@
|
|||
## SQL详解之DML
|
||||
|
||||
我们接着上一课中创建的学校选课系统数据库,为大家讲解 DML 的使用。DML 可以帮助将数据插入到二维表(`insert`操作)、从二维表删除数据(`delete`操作)以及更新二维表的数据(`update`操作)。在执行 DML 之前,我们先通过下面的`use`命令切换到`school`数据库。
|
||||
|
||||
```SQL
|
||||
use `school`;
|
||||
```
|
||||
|
||||
### insert操作
|
||||
|
||||
顾名思义,`insert`是用来插入行到二维表中的,插入的方式包括:插入完整的行、插入行的一部分、插入多行、插入查询的结果。我们通过如下所示的 SQL 向学院表中添加一个学院。
|
||||
|
||||
```SQL
|
||||
insert into `tb_college` values (default, '计算机学院', '学习计算机科学与技术的地方');
|
||||
```
|
||||
|
||||
其中,由于学院表的主键是一个自增字段,因此上面的 SQL 中用`default`表示该列使用默认值,我们也可以使用下面的方式完成同样的操作。
|
||||
|
||||
```SQL
|
||||
insert into `tb_college` (`col_name`, `col_intro`) values ('计算机学院', '学习计算机科学与技术的地方');
|
||||
```
|
||||
|
||||
我们推荐大家使用下面这种做法,指定为哪些字段赋值,这样做可以不按照建表时设定的字段顺序赋值,可以按照`values`前面的元组中给定的字段顺序为字段赋值,但是需要注意,除了允许为`null`和有默认值的字段外,其他的字段都必须要一一列出并在`values`后面的元组中为其赋值。如果希望一次性插入多条记录,我们可以在`values`后面跟上多个元组来实现批量插入,代码如下所示。
|
||||
|
||||
```SQL
|
||||
insert into `tb_college`
|
||||
(`col_name`, `col_intro`)
|
||||
values
|
||||
('外国语学院', '学习歪果仁的语言的学院'),
|
||||
('经济管理学院', '经世济民,治理国家;管理科学,兴国之道'),
|
||||
('体育学院', '发展体育运动,增强人民体质');
|
||||
```
|
||||
|
||||
在插入数据时,要注意主键是不能重复的,如果插入的数据与表中已有记录主键相同,那么`insert`操作将会产生 Duplicated Entry 的报错信息。再次提醒大家,如果`insert`操作省略了某些列,那么这些列要么有默认值,要么允许为`null`,否则也将产生错误。在业务系统中,为了让`insert`操作不影响其他操作(主要是后面要讲的`select`操作)的性能,可以在`insert`和`into`之间加一个`low_priority`来降低`insert`操作的优先级,这个做法也适用于下面要讲的`delete`和`update`操作。
|
||||
|
||||
假如有一张名为`tb_temp`的表中有`a`和`b`两个列,分别保存了学院的名称和学院的介绍,我们也可以通过查询操作获得`tb_temp`表的数据并插入到学院表中,如下所示,其中的`select`就是我们之前提到的 DQL,在下一课中会详细讲解。
|
||||
|
||||
```SQL
|
||||
insert into `tb_college`
|
||||
(`col_name`, `col_intro`)
|
||||
select `a`, `b` from `tb_temp`;
|
||||
```
|
||||
|
||||
### delete 操作
|
||||
|
||||
如果需要从表中删除数据,可以使用`delete`操作,它可以帮助我们删除指定行或所有行,例如我们要删除编号为`1`的学院,就可以使用如下所示的 SQL。
|
||||
|
||||
```SQL
|
||||
delete from `tb_college` where col_id=1;
|
||||
```
|
||||
|
||||
注意,上面的`delete`操作中的`where`子句是用来指定条件的,只有满足条件的行会被删除。如果我们不小心写出了下面的 SQL,就会删除学院表中所有的记录,这是相当危险的,在实际工作中通常也不会这么做。
|
||||
|
||||
```SQL
|
||||
delete from `tb_college`;
|
||||
```
|
||||
|
||||
需要说明的是,即便删除了所有的数据,`delete`操作不会删除表本身,也不会让 AUTO_INCREMENT 字段的值回到初始值。如果需要删除所有的数据而且让 AUTO_INCREMENT 字段回到初始值,可以使用`truncate table`执行截断表操作,`truncate`的本质是删除原来的表并重新创建一个表,它的速度其实更快,因为不需要逐行删除数据。但是请大家记住一点,用`truncate table`删除数据是非常危险的,因为它会删除所有的数据,而且由于原来的表已经被删除了,要想恢复误删除的数据也会变得极为困难。
|
||||
|
||||
### update 操作
|
||||
|
||||
如果要修改表中的数据,可以使用`update`操作,它可以用来删除指定的行或所有的行。例如,我们将学生表中的“杨过”修改为“杨逍”,这里我们假设“杨过”的学号为`1001`,代码如下所示。
|
||||
|
||||
```SQL
|
||||
update `tb_student` set `stu_name`='杨逍' where `stu_id`=1001;
|
||||
```
|
||||
|
||||
注意上面 SQL 中的`where`子句,我们使用学号作为条件筛选出对应的学生,然后通过前面的赋值操作将其姓名修改为“杨逍”。这里为什么不直接使用姓名作为筛选条件,那是因为学生表中可能有多个名为“杨过”的学生,如果使用 stu_name 作为筛选条件,那么我们的`update`操作有可能会一次更新多条数据,这显然不是我们想要看到的。还有一个需要注意的地方是`update`操作中的`set`关键字,因为 SQL 中的`=`并不表示赋值,而是判断相等的运算符,只有出现在`set` 关键字后面的`=`,才具备赋值的能力。
|
||||
|
||||
如果要同时修改学生的姓名和生日,我们可以对上面的`update`语句稍作修改,如下所示。
|
||||
|
||||
```SQL
|
||||
update `tb_student` set `stu_name`='杨逍', `stu_birth`='1975-12-29' where `stu_id`=1001;
|
||||
```
|
||||
|
||||
`update`语句中也可以使用查询的方式获得数据并以此来更新指定的表数据,有兴趣的读者可以自行研究。在书写`update`语句时,通常都会有`where`子句,因为实际工作中几乎不太会用到更新全表的操作,这一点大家一定要注意。
|
||||
|
||||
### 完整的数据
|
||||
|
||||
下面我们给出完整的向 school 数据库的五张表中插入数据的 SQL。
|
||||
|
||||
```SQL
|
||||
use `school`;
|
||||
|
||||
-- 插入学院数据
|
||||
insert into `tb_college`
|
||||
(`col_name`, `col_intro`)
|
||||
values
|
||||
('计算机学院', '计算机学院1958年设立计算机专业,1981年建立计算机科学系,1998年设立计算机学院,2005年5月,为了进一步整合教学和科研资源,学校决定,计算机学院和软件学院行政班子合并统一运作、实行教学和学生管理独立运行的模式。 学院下设三个系:计算机科学与技术系、物联网工程系、计算金融系;两个研究所:图象图形研究所、网络空间安全研究院(2015年成立);三个教学实验中心:计算机基础教学实验中心、IBM技术中心和计算机专业实验中心。'),
|
||||
('外国语学院', '外国语学院设有7个教学单位,6个文理兼收的本科专业;拥有1个一级学科博士授予点,3个二级学科博士授予点,5个一级学科硕士学位授权点,5个二级学科硕士学位授权点,5个硕士专业授权领域,同时还有2个硕士专业学位(MTI)专业;有教职员工210余人,其中教授、副教授80余人,教师中获得中国国内外名校博士学位和正在职攻读博士学位的教师比例占专任教师的60%以上。'),
|
||||
('经济管理学院', '经济学院前身是创办于1905年的经济科;已故经济学家彭迪先、张与九、蒋学模、胡寄窗、陶大镛、胡代光,以及当代学者刘诗白等曾先后在此任教或学习。');
|
||||
|
||||
-- 插入学生数据
|
||||
insert into `tb_student`
|
||||
(`stu_id`, `stu_name`, `stu_sex`, `stu_birth`, `stu_addr`, `col_id`)
|
||||
values
|
||||
(1001, '杨过', 1, '1990-3-4', '湖南长沙', 1),
|
||||
(1002, '任我行', 1, '1992-2-2', '湖南长沙', 1),
|
||||
(1033, '王语嫣', 0, '1989-12-3', '四川成都', 1),
|
||||
(1572, '岳不群', 1, '1993-7-19', '陕西咸阳', 1),
|
||||
(1378, '纪嫣然', 0, '1995-8-12', '四川绵阳', 1),
|
||||
(1954, '林平之', 1, '1994-9-20', '福建莆田', 1),
|
||||
(2035, '东方不败', 1, '1988-6-30', null, 2),
|
||||
(3011, '林震南', 1, '1985-12-12', '福建莆田', 3),
|
||||
(3755, '项少龙', 1, '1993-1-25', '四川成都', 3),
|
||||
(3923, '杨不悔', 0, '1985-4-17', '四川成都', 3);
|
||||
|
||||
-- 插入老师数据
|
||||
insert into `tb_teacher`
|
||||
(`tea_id`, `tea_name`, `tea_title`, `col_id`)
|
||||
values
|
||||
(1122, '张三丰', '教授', 1),
|
||||
(1133, '宋远桥', '副教授', 1),
|
||||
(1144, '杨逍', '副教授', 1),
|
||||
(2255, '范遥', '副教授', 2),
|
||||
(3366, '韦一笑', default, 3);
|
||||
|
||||
-- 插入课程数据
|
||||
insert into `tb_course`
|
||||
(`cou_id`, `cou_name`, `cou_credit`, `tea_id`)
|
||||
values
|
||||
(1111, 'Python程序设计', 3, 1122),
|
||||
(2222, 'Web前端开发', 2, 1122),
|
||||
(3333, '操作系统', 4, 1122),
|
||||
(4444, '计算机网络', 2, 1133),
|
||||
(5555, '编译原理', 4, 1144),
|
||||
(6666, '算法和数据结构', 3, 1144),
|
||||
(7777, '经贸法语', 3, 2255),
|
||||
(8888, '成本会计', 2, 3366),
|
||||
(9999, '审计学', 3, 3366);
|
||||
|
||||
-- 插入选课数据
|
||||
insert into `tb_record`
|
||||
(`stu_id`, `cou_id`, `sel_date`, `score`)
|
||||
values
|
||||
(1001, 1111, '2017-09-01', 95),
|
||||
(1001, 2222, '2017-09-01', 87.5),
|
||||
(1001, 3333, '2017-09-01', 100),
|
||||
(1001, 4444, '2018-09-03', null),
|
||||
(1001, 6666, '2017-09-02', 100),
|
||||
(1002, 1111, '2017-09-03', 65),
|
||||
(1002, 5555, '2017-09-01', 42),
|
||||
(1033, 1111, '2017-09-03', 92.5),
|
||||
(1033, 4444, '2017-09-01', 78),
|
||||
(1033, 5555, '2017-09-01', 82.5),
|
||||
(1572, 1111, '2017-09-02', 78),
|
||||
(1378, 1111, '2017-09-05', 82),
|
||||
(1378, 7777, '2017-09-02', 65.5),
|
||||
(2035, 7777, '2018-09-03', 88),
|
||||
(2035, 9999, '2019-09-02', null),
|
||||
(3755, 1111, '2019-09-02', null),
|
||||
(3755, 8888, '2019-09-02', null),
|
||||
(3755, 9999, '2017-09-01', 92);
|
||||
```
|
||||
|
||||
> **注意**:上面的`insert`语句使用了批处理的方式来插入数据,这种做法插入数据的效率比较高。
|
||||
|
||||
|
||||
|
661
第42课:深入MySQL.md
661
第42课:深入MySQL.md
|
@ -1,661 +0,0 @@
|
|||
## 第42课:深入MySQL
|
||||
|
||||
### 索引
|
||||
|
||||
索引是关系型数据库中用来提升查询性能最为重要的手段。关系型数据库中的索引就像一本书的目录,我们可以想象一下,如果要从一本书中找出某个知识点,但是这本书没有目录,这将是意见多么可怕的事情!我们估计得一篇一篇的翻下去,才能确定这个知识点到底在什么位置。创建索引虽然会带来存储空间上的开销,就像一本书的目录会占用一部分篇幅一样,但是在牺牲空间后换来的查询时间的减少也是非常显著的。
|
||||
|
||||
MySQL 数据库中所有数据类型的列都可以被索引。对于MySQL 8.0 版本的 InnoDB 存储引擎来说,它支持三种类型的索引,分别是 B+ 树索引、全文索引和 R 树索引。这里,我们只介绍使用得最为广泛的 B+ 树索引。使用 B+ 树的原因非常简单,因为它是目前在基于磁盘进行海量数据存储和排序上最有效率的数据结构。B+ 树是一棵[平衡树](https://zh.wikipedia.org/zh-cn/%E5%B9%B3%E8%A1%A1%E6%A0%91),树的高度通常为3或4,但是却可以保存从百万级到十亿级的数据,而从这些数据里面查询一条数据,只需要3次或4次 I/O 操作。
|
||||
|
||||
B+ 树由根节点、中间节点和叶子节点构成,其中叶子节点用来保存排序后的数据。由于记录在索引上是排序过的,因此在一个叶子节点内查找数据时可以使用二分查找,这种查找方式效率非常的高。当数据很少的时候,B+ 树只有一个根节点,数据也就保存在根节点上。随着记录越来越多,B+ 树会发生分裂,根节点不再保存数据,而是提供了访问下一层节点的指针,帮助快速确定数据在哪个叶子节点上。
|
||||
|
||||
在创建二维表时,我们通常都会为表指定主键列,主键列上默认会创建索引,而对于 MySQL InnoDB 存储引擎来说,因为它使用的是索引组织表这种数据存储结构,所以主键上的索引就是整张表的数据,而这种索引我们也将其称之为**聚集索引**(clustered index)。很显然,一张表只能有一个聚集索引,否则表的数据岂不是要保存多次。我们自己创建的索引都是二级索引(secondary index),更常见的叫法是**非聚集索引**(non-clustered index)。通过我们自定义的非聚集索引只能定位记录的主键,在获取数据时可能需要再通过主键上的聚集索引进行查询,这种现象称为“回表”,因此通过非聚集索引检索数据通常比使用聚集索引检索数据要慢。
|
||||
|
||||
接下来我们通过一个简单的例子来说明索引的意义,比如我们要根据学生的姓名来查找学生,这个场景在实际开发中应该经常遇到,就跟通过商品名称查找商品是一个道理。我们可以使用 MySQL 的`explain`关键字来查看 SQL 的执行计划(数据库执行 SQL 语句的具体步骤)。
|
||||
|
||||
```SQL
|
||||
explain select * from tb_student where stuname='林震南'\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: tb_student
|
||||
partitions: NULL
|
||||
type: ALL
|
||||
possible_keys: NULL
|
||||
key: NULL
|
||||
key_len: NULL
|
||||
ref: NULL
|
||||
rows: 11
|
||||
filtered: 10.00
|
||||
Extra: Using where
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
```
|
||||
|
||||
在上面的 SQL 执行计划中,有几项值得我们关注:
|
||||
|
||||
1. `select_type`:查询的类型。
|
||||
- `SIMPLE`:简单 SELECT,不需要使用 UNION 操作或子查询。
|
||||
- `PRIMARY`:如果查询包含子查询,最外层的 SELECT 被标记为 PRIMARY。
|
||||
- `UNION`:UNION 操作中第二个或后面的 SELECT 语句。
|
||||
- `SUBQUERY`:子查询中的第一个 SELECT。
|
||||
- `DERIVED`:派生表的 SELECT 子查询。
|
||||
2. `table`:查询对应的表。
|
||||
3. `type`:MySQL 在表中找到满足条件的行的方式,也称为访问类型,包括:`ALL`(全表扫描)、`index`(索引全扫描,只遍历索引树)、`range`(索引范围扫描)、`ref`(非唯一索引扫描)、`eq_ref`(唯一索引扫描)、`const` / `system`(常量级查询)、`NULL`(不需要访问表或索引)。在所有的访问类型中,很显然 ALL 是性能最差的,它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。
|
||||
4. `possible_keys`:MySQL 可以选择的索引,但是**有可能不会使用**。
|
||||
5. `key`:MySQL 真正使用的索引,如果为`NULL`就表示没有使用索引。
|
||||
6. `key_len`:使用的索引的长度,在不影响查询的情况下肯定是长度越短越好。
|
||||
7. `rows`:执行查询需要扫描的行数,这是一个**预估值**。
|
||||
8. `extra`:关于查询额外的信息。
|
||||
- `Using filesort`:MySQL 无法利用索引完成排序操作。
|
||||
- `Using index`:只使用索引的信息而不需要进一步查表来获取更多的信息。
|
||||
- `Using temporary`:MySQL 需要使用临时表来存储结果集,常用于分组和排序。
|
||||
- `Impossible where`:`where`子句会导致没有符合条件的行。
|
||||
- `Distinct`:MySQL 发现第一个匹配行后,停止为当前的行组合搜索更多的行。
|
||||
- `Using where`:查询的列未被索引覆盖,筛选条件并不是索引的前导列。
|
||||
|
||||
从上面的执行计划可以看出,当我们通过学生名字查询学生时实际上是进行了全表扫描,不言而喻这个查询性能肯定是非常糟糕的,尤其是在表中的行很多的时候。如果我们需要经常通过学生姓名来查询学生,那么就应该在学生姓名对应的列上创建索引,通过索引来加速查询。
|
||||
|
||||
```SQL
|
||||
create index idx_student_name on tb_student(stuname);
|
||||
```
|
||||
|
||||
再次查看刚才的 SQL 对应的执行计划。
|
||||
|
||||
```SQL
|
||||
explain select * from tb_student where stuname='林震南'\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: tb_student
|
||||
partitions: NULL
|
||||
type: ref
|
||||
possible_keys: idx_student_name
|
||||
key: idx_student_name
|
||||
key_len: 62
|
||||
ref: const
|
||||
rows: 1
|
||||
filtered: 100.00
|
||||
Extra: NULL
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
```
|
||||
|
||||
可以注意到,在对学生姓名创建索引后,刚才的查询已经不是全表扫描而是基于索引的查询,而且扫描的行只有唯一的一行,这显然大大的提升了查询的性能。MySQL 中还允许创建前缀索引,即对索引字段的前N个字符创建索引,这样的话可以减少索引占用的空间(但节省了空间很有可能会浪费时间,**时间和空间是不可调和的矛盾**),如下所示。
|
||||
|
||||
```SQL
|
||||
create index idx_student_name_1 on tb_student(stuname(1));
|
||||
```
|
||||
|
||||
上面的索引相当于是根据学生姓名的第一个字来创建的索引,我们再看看 SQL 执行计划。
|
||||
|
||||
```SQL
|
||||
explain select * from tb_student where stuname='林震南'\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: tb_student
|
||||
partitions: NULL
|
||||
type: ref
|
||||
possible_keys: idx_student_name
|
||||
key: idx_student_name
|
||||
key_len: 5
|
||||
ref: const
|
||||
rows: 2
|
||||
filtered: 100.00
|
||||
Extra: Using where
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
```
|
||||
|
||||
不知道大家是否注意到,这一次扫描的行变成了2行,因为学生表中有两个姓“林”的学生,我们只用姓名的第一个字作为索引的话,在查询时通过索引就会找到这两行。
|
||||
|
||||
如果要删除索引,可以使用下面的SQL。
|
||||
|
||||
```SQL
|
||||
alter table tb_student drop index idx_student_name;
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```SQL
|
||||
drop index idx_student_name on tb_student;
|
||||
```
|
||||
|
||||
在创建索引时,我们还可以使用复合索引、函数索引(MySQL 5.7 开始支持),用好复合索引实现**索引覆盖**可以减少不必要的排序和回表操作,这样就会让查询的性能成倍的提升,有兴趣的读者可以自行研究。
|
||||
|
||||
我们简单的为大家总结一下索引的设计原则:
|
||||
|
||||
1. **最适合**索引的列是出现在**WHERE子句**和连接子句中的列。
|
||||
2. 索引列的基数越大(取值多、重复值少),索引的效果就越好。
|
||||
3. 使用**前缀索引**可以减少索引占用的空间,内存中可以缓存更多的索引。
|
||||
4. **索引不是越多越好**,虽然索引加速了读操作(查询),但是写操作(增、删、改)都会变得更慢,因为数据的变化会导致索引的更新,就如同书籍章节的增删需要更新目录一样。
|
||||
5. 使用 InnoDB 存储引擎时,表的普通索引都会保存主键的值,所以**主键要尽可能选择较短的数据类型**,这样可以有效的减少索引占用的空间,提升索引的缓存效果。
|
||||
|
||||
最后,还有一点需要说明,InnoDB 使用的 B-tree 索引,数值类型的列除了等值判断时索引会生效之外,使用`>`、`<`、`>=`、`<=`、`BETWEEN...AND... `、`<>`时,索引仍然生效;对于字符串类型的列,如果使用不以通配符开头的模糊查询,索引也是起作用的,但是其他的情况会导致索引失效,这就意味着很有可能会做全表查询。
|
||||
|
||||
### 视图
|
||||
|
||||
视图是关系型数据库中将一组查询指令构成的结果集组合成可查询的数据表的对象。简单的说,视图就是虚拟的表,但与数据表不同的是,数据表是一种实体结构,而视图是一种虚拟结构,你也可以将视图理解为保存在数据库中被赋予名字的 SQL 语句。
|
||||
|
||||
使用视图可以获得以下好处:
|
||||
|
||||
1. 可以将实体数据表隐藏起来,让外部程序无法得知实际的数据结构,让访问者可以使用表的组成部分而不是整个表,降低数据库被攻击的风险。
|
||||
2. 在大多数的情况下视图是只读的(更新视图的操作通常都有诸多的限制),外部程序无法直接透过视图修改数据。
|
||||
3. 重用 SQL 语句,将高度复杂的查询包装在视图表中,直接访问该视图即可取出需要的数据;也可以将视图视为数据表进行连接查询。
|
||||
4. 视图可以返回与实体数据表不同格式的数据,在创建视图的时候可以对数据进行格式化处理。
|
||||
|
||||
创建视图。
|
||||
|
||||
```SQL
|
||||
-- 创建视图
|
||||
create view `vw_avg_score`
|
||||
as
|
||||
select `stu_id`, round(avg(`score`), 1) as `avg_score`
|
||||
from `tb_record` group by `stu_id`;
|
||||
|
||||
-- 基于已有的视图创建视图
|
||||
create view `vw_student_score`
|
||||
as
|
||||
select `stu_name`, `avg_score`
|
||||
from `tb_student` natural join `vw_avg_score`;
|
||||
```
|
||||
|
||||
> **提示**:因为视图不包含数据,所以每次使用视图时,都必须执行查询以获得数据,如果你使用了连接查询、嵌套查询创建了较为复杂的视图,你可能会发现查询性能下降得很厉害。因此,在使用复杂的视图前,应该进行测试以确保其性能能够满足应用的需求。
|
||||
|
||||
使用视图。
|
||||
|
||||
```SQL
|
||||
select * from `vw_student_score` order by `avg_score` desc;
|
||||
```
|
||||
|
||||
```
|
||||
+--------------+----------+
|
||||
| stuname | avgscore |
|
||||
+--------------+----------+
|
||||
| 杨过 | 95.6 |
|
||||
| 任我行 | 53.5 |
|
||||
| 王语嫣 | 84.3 |
|
||||
| 纪嫣然 | 73.8 |
|
||||
| 岳不群 | 78.0 |
|
||||
| 东方不败 | 88.0 |
|
||||
| 项少龙 | 92.0 |
|
||||
+--------------+----------+
|
||||
```
|
||||
|
||||
既然视图是一张虚拟的表,那么视图的中的数据可以更新吗?视图的可更新性要视具体情况而定,以下类型的视图是不能更新的:
|
||||
|
||||
1. 使用了聚合函数(`SUM`、`MIN`、`MAX`、`AVG`、`COUNT`等)、`DISTINCT`、`GROUP BY`、`HAVING`、`UNION`或者`UNION ALL`的视图。
|
||||
2. `SELECT`中包含了子查询的视图。
|
||||
3. `FROM`子句中包含了一个不能更新的视图的视图。
|
||||
4. `WHERE`子句的子查询引用了`FROM`子句中的表的视图。
|
||||
|
||||
删除视图。
|
||||
|
||||
```SQL
|
||||
drop view vw_student_score;
|
||||
```
|
||||
|
||||
> **说明**:如果希望更新视图,可以先用上面的命令删除视图,也可以通过`create or replace view`来更新视图。
|
||||
|
||||
视图的规则和限制。
|
||||
|
||||
1. 视图可以嵌套,可以利用从其他视图中检索的数据来构造一个新的视图。视图也可以和表一起使用。
|
||||
2. 创建视图时可以使用`order by`子句,但如果从视图中检索数据时也使用了`order by`,那么该视图中原先的`order by`会被覆盖。
|
||||
3. 视图无法使用索引,也不会激发触发器(实际开发中因为性能等各方面的考虑,通常不建议使用触发器,所以我们也不对这个概念进行介绍)的执行。
|
||||
|
||||
### 函数
|
||||
|
||||
MySQL 中的函数跟 Python 中的函数太多的差异,因为函数都是用来封装功能上相对独立且会被重复使用的代码的。如果非要找出一些差别来,那么 MySQL 中的函数是可以执行 SQL 语句的。下面的例子,我们通过自定义函数实现了截断超长字符串的功能。
|
||||
|
||||
```SQL
|
||||
delimiter $$
|
||||
|
||||
create function truncate_string(
|
||||
content varchar(10000),
|
||||
max_length int unsigned
|
||||
) returns varchar(10000) no sql
|
||||
begin
|
||||
declare result varchar(10000) default content;
|
||||
if char_length(content) > max_length then
|
||||
set result = left(content, max_length);
|
||||
set result = concat(result, '……');
|
||||
end if;
|
||||
return result;
|
||||
end $$
|
||||
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
> **说明1**:函数声明后面的`no sql`是声明函数体并没有使用 SQL 语句;如果函数体中需要通过 SQL 读取数据,需要声明为`reads sql data`。
|
||||
>
|
||||
> **说明2**:定义函数前后的`delimiter`命令是为了修改定界符,因为函数体中的语句都是用`;`表示结束,如果不重新定义定界符,那么遇到的`;`的时候代码就会被截断执行,显然这不是我们想要的效果。
|
||||
|
||||
在查询中调用自定义函数。
|
||||
|
||||
```SQL
|
||||
select truncate_string('和我在成都的街头走一走,直到所有的灯都熄灭了也不停留', 10) as short_string;
|
||||
```
|
||||
|
||||
```
|
||||
+--------------------------------------+
|
||||
| short_string |
|
||||
+--------------------------------------+
|
||||
| 和我在成都的街头走一…… |
|
||||
+--------------------------------------+
|
||||
```
|
||||
|
||||
### 过程
|
||||
|
||||
过程(又称存储过程)是事先编译好存储在数据库中的一组 SQL 的集合,调用过程可以简化应用程序开发人员的工作,减少与数据库服务器之间的通信,对于提升数据操作的性能也是有帮助的。其实迄今为止,我们使用的 SQL 语句都是针对一个或多个表的单条语句,但在实际开发中经常会遇到某个操作需要多条 SQL 语句才能完成的情况。例如,电商网站在受理用户订单时,需要做以下一系列的处理。
|
||||
|
||||
1. 通过查询来核对库存中是否有对应的物品以及库存是否充足。
|
||||
2. 如果库存有物品,需要锁定库存以确保这些物品不再卖给别人, 并且要减少可用的物品数量以反映正确的库存量。
|
||||
3. 如果库存不足,可能需要进一步与供应商进行交互或者至少产生一条系统提示消息。
|
||||
4. 不管受理订单是否成功,都需要产生流水记录,而且需要给对应的用户产生一条通知信息。
|
||||
|
||||
我们可以通过过程将复杂的操作封装起来,这样不仅有助于保证数据的一致性,而且将来如果业务发生了变动,只需要调整和修改过程即可。对于调用过程的用户来说,过程并没有暴露数据表的细节,而且执行过程比一条条的执行一组 SQL 要快得多。
|
||||
|
||||
下面的过程实现了查询某门课程的最高分、最低分和平均分。
|
||||
|
||||
```SQL
|
||||
drop procedure if exists sp_score_stat;
|
||||
|
||||
delimiter $$
|
||||
|
||||
create procedure sp_score_stat(
|
||||
courseId int,
|
||||
out maxScore decimal(4,1),
|
||||
out minScore decimal(4,1),
|
||||
out avgScore decimal(4,1)
|
||||
)
|
||||
begin
|
||||
select max(score) into maxScore from tb_record where cou_id=courseId;
|
||||
select min(score) into minScore from tb_record where cou_id=courseId;
|
||||
select avg(score) into avgScore from tb_record where cou_id=courseId;
|
||||
end $$
|
||||
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
> **说明**:在定义过程时,因为可能需要书写多条 SQL,而分隔这些 SQL 需要使用分号作为分隔符,如果这个时候,仍然用分号表示整段代码结束,那么定义过程的 SQL 就会出现错误,所以上面我们用`delimiter $$`将整段代码结束的标记定义为`$$`,那么代码中的分号将不再表示整段代码的结束,整段代码只会在遇到`end $$`时才会执行。在定义完过程后,通过`delimiter ;`将结束符重新改回成分号(恢复现场)。
|
||||
|
||||
上面定义的过程有四个参数,其中第一个参数是输入参数,代表课程的编号,后面的参数都是输出参数,因为过程不能定义返回值,只能通过输出参数将执行结果带出,定义输出参数的关键字是`out`,默认情况下参数都是输入参数。
|
||||
|
||||
调用过程。
|
||||
|
||||
```SQL
|
||||
call sp_score_stat(1111, @a, @b, @c);
|
||||
```
|
||||
|
||||
获取输出参数的值。
|
||||
|
||||
```SQL
|
||||
select @a as 最高分, @b as 最低分, @c as 平均分;
|
||||
```
|
||||
|
||||
删除过程。
|
||||
|
||||
```SQL
|
||||
drop procedure sp_score_stat;
|
||||
```
|
||||
|
||||
在过程中,我们可以定义变量、条件,可以使用分支和循环语句,可以通过游标操作查询结果,还可以使用事件调度器,这些内容我们暂时不在此处进行介绍。虽然我们说了很多过程的好处,但是在实际开发中,如果频繁的使用过程并将大量复杂的运算放到过程中,会给据库服务器造成巨大的压力,而数据库往往都是性能瓶颈所在,使用过程无疑是雪上加霜的操作。所以,对于互联网产品开发,我们一般建议让数据库只做好存储,复杂的运算和处理交给应用服务器上的程序去完成,如果应用服务器变得不堪重负了,我们可以比较容易的部署多台应用服务器来分摊这些压力。
|
||||
|
||||
如果大家对上面讲到的视图、函数、过程包括我们没有讲到的触发器这些知识有兴趣,建议大家阅读 MySQL 的入门读物[《MySQL必知必会》](https://item.jd.com/12818982.html)进行一般性了解即可,因为这些知识点在大家将来的工作中未必用得上,学了也可能仅仅是为了应付面试而已。
|
||||
|
||||
### MySQL 新特性
|
||||
|
||||
#### JSON类型
|
||||
|
||||
很多开发者在使用关系型数据库做数据持久化的时候,常常感到结构化的存储缺乏灵活性,因为必须事先设计好所有的列以及对应的数据类型。在业务发展和变化的过程中,如果需要修改表结构,这绝对是比较麻烦和难受的事情。从 MySQL 5.7 版本开始,MySQL引入了对 JSON 数据类型的支持(MySQL 8.0 解决了 JSON 的日志性能瓶颈问题),用好 JSON 类型,其实就是打破了关系型数据库和非关系型数据库之间的界限,为数据持久化操作带来了更多的便捷。
|
||||
|
||||
JSON 类型主要分为 JSON 对象和 JSON数组两种,如下所示。
|
||||
|
||||
1. JSON 对象
|
||||
|
||||
```JSON
|
||||
{"name": "骆昊", "tel": "13122335566", "QQ": "957658"}
|
||||
```
|
||||
|
||||
2. JSON 数组
|
||||
|
||||
```JSON
|
||||
[1, 2, 3]
|
||||
```
|
||||
|
||||
```JSON
|
||||
[{"name": "骆昊", "tel": "13122335566"}, {"name": "王大锤", "QQ": "123456"}]
|
||||
```
|
||||
|
||||
哪些地方需要用到JSON类型呢?举一个简单的例子,现在很多产品的用户登录都支持多种方式,例如手机号、微信、QQ、新浪微博等,但是一般情况下我们又不会要求用户提供所有的这些信息,那么用传统的设计方式,就需要设计多个列来对应多种登录方式,可能还需要允许这些列存在空值,这显然不是很好的选择;另一方面,如果产品又增加了一种登录方式,那么就必然要修改之前的表结构,这就更让人痛苦了。但是,有了 JSON 类型,刚才的问题就迎刃而解了,我们可以做出如下所示的设计。
|
||||
|
||||
```SQL
|
||||
create table `tb_test`
|
||||
(
|
||||
`user_id` bigint unsigned,
|
||||
`login_info` json,
|
||||
primary key (`user_id`)
|
||||
) engine=innodb;
|
||||
|
||||
insert into `tb_test` values
|
||||
(1, '{"tel": "13122335566", "QQ": "654321", "wechat": "jackfrued"}'),
|
||||
(2, '{"tel": "13599876543", "weibo": "wangdachui123"}');
|
||||
```
|
||||
|
||||
如果要查询用户的手机和微信号,可以用如下所示的 SQL 语句。
|
||||
|
||||
```SQL
|
||||
select
|
||||
`user_id`,
|
||||
json_unquote(json_extract(`login_info`, '$.tel')) as 手机号,
|
||||
json_unquote(json_extract(`login_info`, '$.wechat')) as 微信
|
||||
from `tb_test`;
|
||||
```
|
||||
|
||||
```
|
||||
+---------+-------------+-----------+
|
||||
| user_id | 手机号 | 微信 |
|
||||
+---------+-------------+-----------+
|
||||
| 1 | 13122335566 | jackfrued |
|
||||
| 2 | 13599876543 | NULL |
|
||||
+---------+-------------+-----------+
|
||||
```
|
||||
|
||||
因为支持 JSON 类型,MySQL 也提供了配套的处理 JSON 数据的函数,就像上面用到的`json_extract`和`json_unquote`。当然,上面的 SQL 还有更为便捷的写法,如下所示。
|
||||
|
||||
```SQL
|
||||
select
|
||||
`user_id`,
|
||||
`login_info` ->> '$.tel' as 手机号,
|
||||
`login_info` ->> '$.wechat' as 微信
|
||||
from `tb_test`;
|
||||
```
|
||||
|
||||
再举个例子,如果我们的产品要实现用户画像功能(给用户打标签),然后基于用户画像给用户推荐平台的服务或消费品之类的东西,我们也可以使用 JSON 类型来保存用户画像数据,示意代码如下所示。
|
||||
|
||||
创建画像标签表。
|
||||
|
||||
```SQL
|
||||
create table `tb_tags`
|
||||
(
|
||||
`tag_id` int unsigned not null comment '标签ID',
|
||||
`tag_name` varchar(20) not null comment '标签名',
|
||||
primary key (`tag_id`)
|
||||
) engine=innodb;
|
||||
|
||||
insert into `tb_tags` (`tag_id`, `tag_name`)
|
||||
values
|
||||
(1, '70后'),
|
||||
(2, '80后'),
|
||||
(3, '90后'),
|
||||
(4, '00后'),
|
||||
(5, '爱运动'),
|
||||
(6, '高学历'),
|
||||
(7, '小资'),
|
||||
(8, '有房'),
|
||||
(9, '有车'),
|
||||
(10, '爱看电影'),
|
||||
(11, '爱网购'),
|
||||
(12, '常点外卖');
|
||||
```
|
||||
|
||||
为用户打标签。
|
||||
|
||||
```SQL
|
||||
create table `tb_users_tags`
|
||||
(
|
||||
`user_id` bigint unsigned not null comment '用户ID',
|
||||
`user_tags` json not null comment '用户标签'
|
||||
) engine=innodb;
|
||||
|
||||
insert into `tb_users_tags` values
|
||||
(1, '[2, 6, 8, 10]'),
|
||||
(2, '[3, 10, 12]'),
|
||||
(3, '[3, 8, 9, 11]');
|
||||
```
|
||||
|
||||
接下来,我们通过一组查询来了解 JSON 类型的巧妙之处。
|
||||
|
||||
1. 查询爱看电影(有`10`这个标签)的用户ID。
|
||||
|
||||
```SQL
|
||||
select * from `tb_users` where 10 member of (user_tags->'$');
|
||||
```
|
||||
|
||||
2. 查询爱看电影(有`10`这个标签)的80后(有`2`这个标签)用户ID。
|
||||
|
||||
```
|
||||
select * from `tb_users` where json_contains(user_tags->'$', '[2, 10]');
|
||||
|
||||
3. 查询爱看电影或80后或90后的用户ID。
|
||||
|
||||
```SQL
|
||||
select `user_id` from `tb_users_tags` where json_overlaps(user_tags->'$', '[2, 3, 10]');
|
||||
```
|
||||
|
||||
> **说明**:上面的查询用到了`member of`谓词和两个 JSON 函数,`json_contains`可以检查 JSON 数组是否包含了指定的元素,而`json_overlaps`可以检查 JSON 数组是否与指定的数组有重叠部分。
|
||||
|
||||
#### 窗口函数
|
||||
|
||||
MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源数据库早已提供了对窗口函数的支持,有的也将其称之为 OLAP(联机分析和处理)函数,听名字就知道跟统计和分析相关。为了帮助大家理解窗口函数,我们先说说窗口的概念。
|
||||
|
||||
窗口可以理解为记录的集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数。窗口函数和我们上面讲到的聚合函数比较容易混淆,二者的区别主要在于聚合函数是将多条记录聚合为一条记录,窗口函数是每条记录都会执行,执行后记录条数不会变。窗口函数不仅仅是几个函数,它是一套完整的语法,函数只是该语法的一部分,基本语法如下所示:
|
||||
|
||||
```SQL
|
||||
<窗口函数> over (partition by <用于分组的列名> order by <用户排序的列名>)
|
||||
```
|
||||
|
||||
上面语法中,窗口函数的位置可以放以下两种函数:
|
||||
|
||||
1. 专用窗口函数,包括:`lead`、`lag`、`first_value`、`last_value`、`rank`、`dense_rank`和`row_number`等。
|
||||
2. 聚合函数,包括:`sum`、`avg`、`max`、`min`和`count`等。
|
||||
|
||||
下面为大家举几个使用窗口函数的简单例子,我们先用如下所示的 SQL 建库建表。
|
||||
|
||||
```SQL
|
||||
-- 创建名为hrs的数据库并指定默认的字符集
|
||||
create database `hrs` default charset utf8mb4;
|
||||
|
||||
-- 切换到hrs数据库
|
||||
use `hrs`;
|
||||
|
||||
-- 创建部门表
|
||||
create table `tb_dept`
|
||||
(
|
||||
`dno` int not null comment '编号',
|
||||
`dname` varchar(10) not null comment '名称',
|
||||
`dloc` varchar(20) not null comment '所在地',
|
||||
primary key (`dno`)
|
||||
);
|
||||
|
||||
-- 插入4个部门
|
||||
insert into `tb_dept` values
|
||||
(10, '会计部', '北京'),
|
||||
(20, '研发部', '成都'),
|
||||
(30, '销售部', '重庆'),
|
||||
(40, '运维部', '深圳');
|
||||
|
||||
-- 创建员工表
|
||||
create table `tb_emp`
|
||||
(
|
||||
`eno` int not null comment '员工编号',
|
||||
`ename` varchar(20) not null comment '员工姓名',
|
||||
`job` varchar(20) not null comment '员工职位',
|
||||
`mgr` int comment '主管编号',
|
||||
`sal` int not null comment '员工月薪',
|
||||
`comm` int comment '每月补贴',
|
||||
`dno` int not null comment '所在部门编号',
|
||||
primary key (`eno`),
|
||||
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
|
||||
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
|
||||
);
|
||||
|
||||
-- 插入14个员工
|
||||
insert into `tb_emp` values
|
||||
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
|
||||
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
|
||||
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
|
||||
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
|
||||
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
|
||||
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
|
||||
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
|
||||
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
|
||||
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
|
||||
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
|
||||
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
|
||||
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
|
||||
(3577, '杨过', '会计', 5566, 2200, null, 10),
|
||||
(3588, '朱九真', '会计', 5566, 2500, null, 10);
|
||||
```
|
||||
|
||||
例子1:查询按月薪从高到低排在第4到第6名的员工的姓名和月薪。
|
||||
|
||||
```SQL
|
||||
select * from (
|
||||
select
|
||||
`ename`, `sal`,
|
||||
row_number() over (order by `sal` desc) as `rank`
|
||||
from `tb_emp`
|
||||
) `temp` where `rank` between 4 and 6;
|
||||
```
|
||||
|
||||
> **说明**:上面使用的函数`row_number()`可以为每条记录生成一个行号,在实际工作中可以根据需要将其替换为`rank()`或`dense_rank()`函数,三者的区别可以参考官方文档或阅读[《通俗易懂的学会:SQL窗口函数》](https://zhuanlan.zhihu.com/p/92654574)进行了解。在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。
|
||||
>
|
||||
> ```SQL
|
||||
> select `rank`, `ename`, `sal` from (
|
||||
> select @a:=@a+1 as `rank`, `ename`, `sal`
|
||||
> from `tb_emp`, (select @a:=0) as t1 order by `sal` desc
|
||||
> ) as `temp` where `rank` between 4 and 6;
|
||||
> ```
|
||||
|
||||
例子2:查询每个部门月薪最高的两名的员工的姓名和部门名称。
|
||||
|
||||
```SQL
|
||||
select `ename`, `sal`, `dname`
|
||||
from (
|
||||
select
|
||||
`ename`, `sal`, `dno`,
|
||||
rank() over (partition by `dno` order by `sal` desc) as `rank`
|
||||
from `tb_emp`
|
||||
) as `temp` natural join `tb_dept` where `rank`<=2;
|
||||
```
|
||||
|
||||
> 说明:在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。
|
||||
>
|
||||
> ```SQL
|
||||
> select `ename`, `sal`, `dname` from `tb_emp` as `t1`
|
||||
natural join `tb_dept`
|
||||
where (
|
||||
select count(*) from `tb_emp` as `t2`
|
||||
where `t1`.`dno`=`t2`.`dno` and `t2`.`sal`>`t1`.`sal`
|
||||
)<2 order by `dno` asc, `sal` desc;
|
||||
> ```
|
||||
|
||||
### 其他内容
|
||||
|
||||
#### 范式理论
|
||||
|
||||
范式理论是设计关系型数据库中二维表的指导思想。
|
||||
|
||||
1. 第一范式:数据表的每个列的值域都是由原子值组成的,不能够再分割。
|
||||
2. 第二范式:数据表里的所有数据都要和该数据表的键(主键与候选键)有完全依赖关系。
|
||||
3. 第三范式:所有非键属性都只和候选键有相关性,也就是说非键属性之间应该是独立无关的。
|
||||
|
||||
> **说明**:实际工作中,出于效率的考虑,我们在设计表时很有可能做出反范式设计,即故意降低方式级别,增加冗余数据来获得更好的操作性能。
|
||||
|
||||
#### 数据完整性
|
||||
|
||||
1. 实体完整性 - 每个实体都是独一无二的
|
||||
|
||||
- 主键(`primary key`) / 唯一约束(`unique`)
|
||||
2. 引用完整性(参照完整性)- 关系中不允许引用不存在的实体
|
||||
|
||||
- 外键(`foreign key`)
|
||||
3. 域(domain)完整性 - 数据是有效的
|
||||
- 数据类型及长度
|
||||
|
||||
- 非空约束(`not null`)
|
||||
|
||||
- 默认值约束(`default`)
|
||||
|
||||
- 检查约束(`check`)
|
||||
|
||||
> **说明**:在 MySQL 8.x 以前,检查约束并不起作用。
|
||||
|
||||
#### 数据一致性
|
||||
|
||||
1. 事务:一系列对数据库进行读/写的操作,这些操作要么全都成功,要么全都失败。
|
||||
|
||||
2. 事务的 ACID 特性
|
||||
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
|
||||
- 一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态
|
||||
- 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
|
||||
- 持久性:已被提交的事务对数据库的修改应该永久保存在数据库中
|
||||
|
||||
3. MySQL 中的事务操作
|
||||
|
||||
- 开启事务环境
|
||||
|
||||
```SQL
|
||||
start transaction
|
||||
```
|
||||
|
||||
- 提交事务
|
||||
|
||||
```SQL
|
||||
commit
|
||||
```
|
||||
|
||||
- 回滚事务
|
||||
|
||||
```SQL
|
||||
rollback
|
||||
```
|
||||
|
||||
4. 查看事务隔离级别
|
||||
|
||||
```SQL
|
||||
show variables like 'transaction_isolation';
|
||||
```
|
||||
|
||||
```
|
||||
+-----------------------+-----------------+
|
||||
| Variable_name | Value |
|
||||
+-----------------------+-----------------+
|
||||
| transaction_isolation | REPEATABLE-READ |
|
||||
+-----------------------+-----------------+
|
||||
```
|
||||
|
||||
可以看出,MySQL 默认的事务隔离级别是`REPEATABLE-READ`。
|
||||
|
||||
5. 修改(当前会话)事务隔离级别
|
||||
|
||||
```SQL
|
||||
set session transaction isolation level read committed;
|
||||
```
|
||||
|
||||
重新查看事务隔离级别,结果如下所示。
|
||||
|
||||
```
|
||||
+-----------------------+----------------+
|
||||
| Variable_name | Value |
|
||||
+-----------------------+----------------+
|
||||
| transaction_isolation | READ-COMMITTED |
|
||||
+-----------------------+----------------+
|
||||
```
|
||||
|
||||
关系型数据库的事务是一个很大的话题,因为当存在多个并发事务访问数据时,就有可能出现三类读数据的问题(脏读、不可重复读、幻读)和两类更新数据的问题(第一类丢失更新、第二类丢失更新)。想了解这五类问题的,可以阅读我发布在 CSDN 网站上的[《Java面试题全集(上)》](https://blog.csdn.net/jackfrued/article/details/44921941)一文的第80题。为了避免这些问题,关系型数据库底层是有对应的锁机制的,按锁定对象不同可以分为表级锁和行级锁,按并发事务锁定关系可以分为共享锁和独占锁。然而直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定适当的事务隔离级别,数据库就会通过分析 SQL 语句,然后为事务访问的资源加上合适的锁。此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的。想了解 MySQL 事务和锁的细节知识,推荐大家阅读进阶读物[《高性能MySQL》](https://item.jd.com/11220393.html),这也是数据库方面的经典书籍。
|
||||
|
||||
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示。需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定到底使用哪种事务隔离级别,这个地方没有万能的原则。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211121225327.png" style="zoom:50%;">
|
||||
|
||||
### 总结
|
||||
|
||||
关于 MySQL 的知识肯定远远不止上面列出的这些,比如 MySQL 性能调优、MySQL 运维相关工具、MySQL 数据的备份和恢复、监控 MySQL 服务、部署高可用架构等,这一系列的问题在这里都没有办法逐一展开来讨论,那就留到有需要的时候再进行讲解吧,各位读者也可以自行探索。
|
|
@ -0,0 +1,445 @@
|
|||
## SQL详解之DQL
|
||||
|
||||
接下来,我们利用之前创建的学校选课系统数据库,为大家讲解 DQL 的应用。无论对于开发人员还是数据分析师,DQL 都是非常重要的,它关系着我们能否从关系数据库中获取我们需要的数据。建议大家把上上一节课中建库建表的 DDL 以及 上一节课中插入数据的 DML 重新执行一次,确保表和数据跟没有问题再执行下面的操作。
|
||||
|
||||
```SQL
|
||||
use `school`;
|
||||
|
||||
-- 查询所有学生的所有信息
|
||||
-- 说明:实际工作中不建议使用 select * 的方式进行查询
|
||||
select *
|
||||
from tb_student;
|
||||
|
||||
-- 查询学生的学号、姓名和籍贯
|
||||
select stu_id,
|
||||
stu_name,
|
||||
stu_addr
|
||||
from tb_student;
|
||||
|
||||
select stu_id as 学号,
|
||||
stu_name as 姓名,
|
||||
stu_addr as 籍贯
|
||||
from tb_student;
|
||||
|
||||
-- 查询所有课程的名称及学分
|
||||
select cou_name as 课程名称,
|
||||
cou_credit as 学分
|
||||
from tb_course;
|
||||
|
||||
-- 查询所有女学生的姓名和出生日期
|
||||
select stu_name,
|
||||
stu_birth
|
||||
from tb_student
|
||||
where stu_sex=0;
|
||||
|
||||
-- 查询籍贯为“四川成都”的女学生的姓名和出生日期
|
||||
select stu_name,
|
||||
stu_birth
|
||||
from tb_student
|
||||
where stu_sex=0
|
||||
and stu_addr='四川成都';
|
||||
|
||||
-- 查询籍贯为“四川成都”或者性别是女的学生
|
||||
select stu_name,
|
||||
stu_birth
|
||||
from tb_student
|
||||
where stu_sex=0
|
||||
or stu_addr='四川成都';
|
||||
|
||||
-- 查询所有80后学生的姓名、性别和出生日期
|
||||
-- 方法一:
|
||||
select stu_name,
|
||||
stu_sex,
|
||||
stu_birth
|
||||
from tb_student
|
||||
where stu_birth >= '1980-1-1'
|
||||
and stu_birth <= '1989-12-31';
|
||||
|
||||
-- 方法二:
|
||||
select stu_name,
|
||||
stu_sex,
|
||||
stu_birth
|
||||
from tb_student
|
||||
where stu_birth between '1980-1-1' and '1989-12-31';
|
||||
|
||||
-- 查询学分大于2的课程的名称和学分
|
||||
select cou_name,
|
||||
cou_credit
|
||||
from tb_course
|
||||
where cou_credit > 2;
|
||||
|
||||
-- 查询学分是奇数的课程的名称和学分
|
||||
select cou_name,
|
||||
cou_credit
|
||||
from tb_course
|
||||
where cou_credit % 2 <> 0;
|
||||
|
||||
-- 查询选择选了1111的课程考试成绩在90分以上的学生学号
|
||||
select stu_id
|
||||
from tb_record
|
||||
where cou_id = 1111
|
||||
and score > 90;
|
||||
|
||||
-- 查询姓“杨”的学生姓名和性别(模糊)
|
||||
-- % 可以匹配零个或任意多个字符
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name like '杨%';
|
||||
|
||||
-- 查询姓“杨”名字两个字的学生姓名和性别(模糊)
|
||||
-- _ 可以匹配一个字符
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name like '杨_';
|
||||
|
||||
-- 查询姓“杨”名字三个字的学生姓名和性别(模糊)
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name like '杨__';
|
||||
|
||||
-- 查询名字中有“不”字或“嫣”字的学生的姓名(模糊)
|
||||
-- 方法一:
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name like '%不%'
|
||||
or stu_name like '%嫣%';
|
||||
|
||||
-- 方法二:
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name like '%不%'
|
||||
union
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name like '%嫣%';
|
||||
|
||||
-- 查询姓“杨”或姓“林”名字三个字的学生的姓名
|
||||
select stu_name,
|
||||
stu_sex
|
||||
from tb_student
|
||||
where stu_name regexp '^[杨林][\\u4e00-\\u9fa5]{2}$';
|
||||
|
||||
-- 查询没有录入籍贯的学生姓名
|
||||
select stu_name
|
||||
from tb_student
|
||||
where stu_addr = ''
|
||||
or stu_addr is null;
|
||||
|
||||
-- 查询录入了籍贯的学生姓名
|
||||
select stu_name
|
||||
from tb_student
|
||||
where stu_addr <> ''
|
||||
and stu_addr is not null;
|
||||
|
||||
-- 查询学生选课的所有日期
|
||||
select distinct sel_date
|
||||
from tb_record;
|
||||
|
||||
-- 查询学生的籍贯
|
||||
select distinct stu_addr
|
||||
from tb_student
|
||||
where stu_addr <> ''
|
||||
and stu_addr is not null;
|
||||
|
||||
-- 查询学院编号为1的学生姓名、性别和生日按年龄从大到小排列
|
||||
-- asc - 升序(从小到大,默认),desc - 降序(从大到小)
|
||||
select stu_name,
|
||||
stu_sex,
|
||||
stu_birth
|
||||
from tb_student
|
||||
where col_id = 1
|
||||
order by stu_sex asc,
|
||||
stu_birth asc;
|
||||
|
||||
-- 补充:将上面的性别处理成“男”或“女”,将生日换算成年龄
|
||||
select stu_name as 姓名,
|
||||
if(stu_sex, '男', '女') as 性别,
|
||||
floor(datediff(curdate(), stu_birth) / 365) as 年龄
|
||||
from tb_student
|
||||
where col_id = 1
|
||||
order by stu_sex asc,
|
||||
年龄 desc;
|
||||
|
||||
-- 查询年龄最大的学生的出生日期
|
||||
select min(stu_birth)
|
||||
from tb_student;
|
||||
|
||||
-- 查询年龄最小的学生的出生日期
|
||||
select max(stu_birth)
|
||||
from tb_student;
|
||||
|
||||
-- 查询学号为1001的学生一共选了几门课
|
||||
select count(*)
|
||||
from tb_record
|
||||
where stu_id = 1001;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的平均分
|
||||
select round(avg(score), 1) as 平均分
|
||||
from tb_record
|
||||
where stu_id = 1001;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的平均分,如果有null值,null值算0分
|
||||
-- 方法一:
|
||||
select sum(score) / count(*)
|
||||
from tb_record
|
||||
where stu_id = 1001;
|
||||
|
||||
-- 方法二:
|
||||
select avg(coalesce(score, 0))
|
||||
from tb_record
|
||||
where stu_id = 1001;
|
||||
|
||||
-- 查询学号为1001的学生考试成绩的标准差(聚合函数)
|
||||
select stddev_samp(score)
|
||||
from tb_record
|
||||
where stu_id = 1001;
|
||||
|
||||
-- 查询男女学生的人数
|
||||
select case stu_sex when 1 then '男' else '女' end as 性别,
|
||||
count(*) as 人数
|
||||
from tb_student
|
||||
group by stu_sex;
|
||||
|
||||
-- 查询每个学院学生人数
|
||||
select col_id as 学院编号,
|
||||
count(*) as 人数
|
||||
from tb_student
|
||||
group by col_id;
|
||||
|
||||
-- 查询每个学院男女学生人数
|
||||
select col_id as 学院编号,
|
||||
case stu_sex when 1 then '男' else '女' end as 性别,
|
||||
count(*) as 人数
|
||||
from tb_student
|
||||
group by col_id, stu_sex;
|
||||
|
||||
-- 查询每个学生的学号和平均成绩
|
||||
select stu_id as 学号,
|
||||
round(avg(score), 2) as 平均成绩
|
||||
from tb_record
|
||||
group by stu_id;
|
||||
|
||||
-- 查询平均成绩大于等于90分的学生的学号和平均成绩
|
||||
-- 方法一:
|
||||
select stu_id as 学号,
|
||||
round(avg(score), 2) as 平均成绩
|
||||
from tb_record
|
||||
group by stu_id
|
||||
having 平均成绩 >= 90;
|
||||
|
||||
-- 方法二:
|
||||
select *
|
||||
from ( select stu_id as 学号,
|
||||
round(avg(score), 2) as 平均成绩
|
||||
from tb_record
|
||||
group by stu_id) as t
|
||||
where 平均成绩 >= 90;
|
||||
|
||||
-- 查询1111、2222、3333三门课程平均成绩大于等于90分的学生的学号和平均成绩
|
||||
select stu_id as 学号,
|
||||
round(avg(score), 2) as 平均成绩
|
||||
from tb_record
|
||||
where cou_id in (1111, 2222, 3333)
|
||||
group by stu_id
|
||||
having avg(score) >= 90;
|
||||
|
||||
-- 嵌套查询:把一个查询的结果作为另外一个查询的一部分来使用
|
||||
select stu_name
|
||||
from tb_student
|
||||
where stu_birth = (select min(stu_birth)
|
||||
from tb_student);
|
||||
|
||||
-- 查询选了两门以上的课程的学生姓名
|
||||
select stu_name
|
||||
from tb_student
|
||||
where stu_id in ( select stu_id
|
||||
from tb_record
|
||||
group by stu_id
|
||||
having count(*) > 2);
|
||||
|
||||
-- 查询学生的姓名、生日和所在学院名称
|
||||
-- 方法一:
|
||||
select stu_name,
|
||||
stu_birth,
|
||||
col_name
|
||||
from tb_student, tb_college
|
||||
where tb_student.col_id = tb_college.col_id;
|
||||
|
||||
-- 方法二:
|
||||
select stu_name,
|
||||
stu_birth,
|
||||
col_name
|
||||
from tb_student inner join tb_college
|
||||
on tb_student.col_id = tb_college.col_id;
|
||||
|
||||
-- 方法三:
|
||||
select stu_name,
|
||||
stu_birth,
|
||||
col_name
|
||||
from tb_student natural join tb_college;
|
||||
|
||||
-- 查询学生姓名、课程名称以及成绩
|
||||
-- 方法一:
|
||||
select stu_name,
|
||||
cou_name,
|
||||
score
|
||||
from tb_student, tb_course, tb_record
|
||||
where tb_student.stu_id = tb_record.stu_id
|
||||
and tb_course.cou_id = tb_record.cou_id
|
||||
and score is not null;
|
||||
|
||||
-- 方法二:
|
||||
select stu_name,
|
||||
cou_name,
|
||||
score
|
||||
from tb_student inner join tb_record inner join tb_course
|
||||
on tb_student.stu_id = tb_record.stu_id
|
||||
and tb_course.cou_id = tb_record.cou_id
|
||||
where score is not null;
|
||||
|
||||
-- 方法三:
|
||||
select stu_name,
|
||||
cou_name,
|
||||
score
|
||||
from tb_student natural join tb_record natural join tb_course
|
||||
where score is not null;
|
||||
|
||||
-- 补充:上面的查询结果取前5条数据
|
||||
select stu_name,
|
||||
cou_name,
|
||||
score
|
||||
from tb_student natural join tb_record natural join tb_course
|
||||
where score is not null
|
||||
order by stu_id asc, score desc
|
||||
limit 5;
|
||||
|
||||
-- 补充:上面的查询结果取第6-10条数据
|
||||
select stu_name,
|
||||
cou_name,
|
||||
score
|
||||
from tb_student inner join tb_record inner join tb_course
|
||||
on tb_student.stu_id = tb_record.stu_id
|
||||
and tb_course.cou_id = tb_record.cou_id
|
||||
order by stu_id asc, score desc
|
||||
limit 5
|
||||
offset 5;
|
||||
|
||||
-- 补充:上面的查询结果取第11-15条数据
|
||||
select stu_name,
|
||||
cou_name,
|
||||
score
|
||||
from tb_student natural join tb_record natural join tb_course
|
||||
where score is not null
|
||||
order by stu_id asc, score desc
|
||||
limit 5
|
||||
offset 10;
|
||||
|
||||
-- 查询选课学生的姓名和平均成绩
|
||||
select stu_name,
|
||||
avg_score
|
||||
from tb_student t1 inner join ( select stu_id,
|
||||
round(avg(score), 2) as avg_score
|
||||
from tb_record
|
||||
group by stu_id) t2
|
||||
on t1.stu_id = t2.stu_id;
|
||||
|
||||
-- 查询学生的姓名和选课的数量
|
||||
select stu_name,
|
||||
total
|
||||
from tb_student t1 natural join ( select stu_id,
|
||||
count(*) as total
|
||||
from tb_record
|
||||
group by stu_id) t2;
|
||||
|
||||
-- 查询每个学生的姓名和选课数量(左外连接和子查询)
|
||||
select stu_name as 姓名,
|
||||
coalesce (total, 0) as 选课数量
|
||||
from tb_student t1 left join ( select stu_id,
|
||||
count(*) as total
|
||||
from tb_record
|
||||
group by stu_id) t2
|
||||
on t1.stu_id = t2.stu_id;
|
||||
```
|
||||
|
||||
上面的 DQL 有几个地方需要加以说明:
|
||||
|
||||
1. MySQL目前的版本不支持全外连接,上面我们通过`union`操作,将左外连接和右外连接的结果求并集实现全外连接的效果。大家可以通过下面的图来加深对连表操作的认识。
|
||||
|
||||
<img src="http://localhost/mypic/20211121135117.png" style="zoom:50%">
|
||||
|
||||
2. MySQL 中支持多种类型的运算符,包括:算术运算符(`+`、`-`、`*`、`/`、`%`)、比较运算符(`=`、`<>`、`<=>`、`<`、`<=`、`>`、`>=`、`BETWEEN...AND..`.、`IN`、`IS NULL`、`IS NOT NULL`、`LIKE`、`RLIKE`、`REGEXP`)、逻辑运算符(`NOT`、`AND`、`OR`、`XOR`)和位运算符(`&`、`|`、`^`、`~`、`>>`、`<<`),我们可以在 DML 中使用这些运算符处理数据。
|
||||
|
||||
3. 在查询数据时,可以在`SELECT`语句及其子句(如`WHERE`子句、`ORDER BY`子句、`HAVING`子句等)中使用函数,这些函数包括字符串函数、数值函数、时间日期函数、流程函数等,如下面的表格所示。
|
||||
|
||||
常用字符串函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| --------------------------- | ----------------------------------------------------- |
|
||||
| `CONCAT` | 将多个字符串连接成一个字符串 |
|
||||
| `FORMAT` | 将数值格式化成字符串并指定保留几位小数 |
|
||||
| `FROM_BASE64` / `TO_BASE64` | BASE64解码/编码 |
|
||||
| `BIN` / `OCT` / `HEX` | 将数值转换成二进制/八进制/十六进制字符串 |
|
||||
| `LOCATE` | 在字符串中查找一个子串的位置 |
|
||||
| `LEFT` / `RIGHT` | 返回一个字符串左边/右边指定长度的字符 |
|
||||
| `LENGTH` / `CHAR_LENGTH` | 返回字符串的长度以字节/字符为单位 |
|
||||
| `LOWER` / `UPPER` | 返回字符串的小写/大写形式 |
|
||||
| `LPAD` / `RPAD` | 如果字符串的长度不足,在字符串左边/右边填充指定的字符 |
|
||||
| `LTRIM` / `RTRIM` | 去掉字符串前面/后面的空格 |
|
||||
| `ORD` / `CHAR` | 返回字符对应的编码/返回编码对应的字符 |
|
||||
| `STRCMP` | 比较字符串,返回-1、0、1分别表示小于、等于、大于 |
|
||||
| `SUBSTRING` | 返回字符串指定范围的子串 |
|
||||
|
||||
常用数值函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| -------------------------------------------------------- | ---------------------------------- |
|
||||
| `ABS` | 返回一个数的绝度值 |
|
||||
| `CEILING` / `FLOOR` | 返回一个数上取整/下取整的结果 |
|
||||
| `CONV` | 将一个数从一种进制转换成另一种进制 |
|
||||
| `CRC32` | 计算循环冗余校验码 |
|
||||
| `EXP` / `LOG` / `LOG2` / `LOG10` | 计算指数/对数 |
|
||||
| `POW` | 求幂 |
|
||||
| `RAND` | 返回[0,1)范围的随机数 |
|
||||
| `ROUND` | 返回一个数四舍五入后的结果 |
|
||||
| `SQRT` | 返回一个数的平方根 |
|
||||
| `TRUNCATE` | 截断一个数到指定的精度 |
|
||||
| `SIN` / `COS` / `TAN` / `COT` / `ASIN` / `ACOS` / `ATAN` | 三角函数 |
|
||||
|
||||
常用时间日期函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| ----------------------------- | ------------------------------------- |
|
||||
| `CURDATE` / `CURTIME` / `NOW` | 获取当前日期/时间/日期和时间 |
|
||||
| `ADDDATE` / `SUBDATE` | 将两个日期表达式相加/相减并返回结果 |
|
||||
| `DATE` / `TIME` | 从字符串中获取日期/时间 |
|
||||
| `YEAR` / `MONTH` / `DAY` | 从日期中获取年/月/日 |
|
||||
| `HOUR` / `MINUTE` / `SECOND` | 从时间中获取时/分/秒 |
|
||||
| `DATEDIFF` / `TIMEDIFF` | 返回两个时间日期表达式相差多少天/小时 |
|
||||
| `MAKEDATE` / `MAKETIME` | 制造一个日期/时间 |
|
||||
|
||||
常用流程函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| -------- | ------------------------------------------------ |
|
||||
| `IF` | 根据条件是否成立返回不同的值 |
|
||||
| `IFNULL` | 如果为NULL则返回指定的值否则就返回本身 |
|
||||
| `NULLIF` | 两个表达式相等就返回NULL否则返回第一个表达式的值 |
|
||||
|
||||
其他常用函数。
|
||||
|
||||
| 函数 | 功能 |
|
||||
| -------------------------- | ----------------------------- |
|
||||
| `MD5` / `SHA1` / `SHA2` | 返回字符串对应的哈希摘要 |
|
||||
| `CHARSET` / `COLLATION` | 返回字符集/校对规则 |
|
||||
| `USER` / `CURRENT_USER` | 返回当前用户 |
|
||||
| `DATABASE` | 返回当前数据库名 |
|
||||
| `VERSION` | 返回当前数据库版本 |
|
||||
| `FOUND_ROWS` / `ROW_COUNT` | 返回查询到的行数/受影响的行数 |
|
||||
| `LAST_INSERT_ID` | 返回最后一个自增主键的值 |
|
||||
| `UUID` / `UUID_SHORT` | 返回全局唯一标识符 |
|
|
@ -0,0 +1,74 @@
|
|||
## SQL详解之DCL
|
||||
|
||||
数据库服务器通常包含了非常重要的数据,可以通过访问控制来确保这些数据的安全,而 DCL 就是解决这一问题的,它可以为指定的用户授予访问权限或者从指定用户处召回指定的权限。DCL 对数据库管理员来说非常重要,因为用户权限的管理关系到数据库的安全。简单的说,我们可以通过 DCL 允许受信任的用户访问数据库,阻止不受信任的用户访问数据库,同时还可以通过 DCL 将每个访问者的的权限最小化(让访问者的权限刚刚够用)。
|
||||
|
||||
### 创建用户
|
||||
|
||||
我们可以使用下面的 SQL 来创建一个用户并为其指定访问口令。
|
||||
|
||||
```SQL
|
||||
create user 'wangdachui'@'%' identified by 'Wang.618';
|
||||
```
|
||||
|
||||
上面的 SQL 创建了名为 wangdachui 的用户,它的访问口令是 Wang.618,该用户可以从任意主机访问数据库服务器,因为 @ 后面使用了可以表示任意多个字符的通配符 %。如果要限制 wangdachui 这个用户只能从 192.168.0.x 这个网段的主机访问数据库服务器,可以按照下面的方式来修改 SQL 语句。
|
||||
|
||||
```SQL
|
||||
drop user if exists 'wangdachui'@'%';
|
||||
|
||||
create user 'wangdachui'@'192.168.0.%' identified by 'Wang.618';
|
||||
```
|
||||
|
||||
此时,如果我们使用 wangdachui 这个账号访问数据库服务器,我们几乎不能做任何操作,因为该账号没有任何操作权限。
|
||||
|
||||
### 授予权限
|
||||
|
||||
我们用下面的语句为 wangdachui 授予查询 school 数据库学院表(`tb_college`)的权限。
|
||||
|
||||
```SQL
|
||||
grant select on `school`.`tb_college` to 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
我们也可以让 wangdachui 对 school 数据库的所有对象都具有查询权限,代码如下所示。
|
||||
|
||||
```SQL
|
||||
grant select on `school`.* to 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
如果我们希望 wangdachui 还有 insert、delete 和 update 权限,可以使用下面的方式进行操作。
|
||||
|
||||
```SQL
|
||||
grant insert, delete, update on `school`.* to 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
如果我们还想授予 wangdachui 执行 DDL 的权限,可以使用如下所示的 SQL。
|
||||
|
||||
```SQL
|
||||
grant create, drop, alter on `school`.* to 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
如果我们希望 wangdachui 账号对所有数据库的所有对象都具备所有的操作权限,可以执行如下所示的操作,但是一般情况下,我们不会这样做,因为我们之前说过,权限刚刚够用就行,一个普通的账号不应该拥有这么大的权限。
|
||||
|
||||
```SQL
|
||||
grant all privileges on *.* to 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
### 召回权限
|
||||
|
||||
如果要召回 wangdachui 对 school 数据库的 insert、delete 和 update 权限,可以使用下面的操作。
|
||||
|
||||
```SQL
|
||||
revoke insert, delete, update on `school`.* from 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
如果要召回所有的权限,可以按照如下所示的方式进行操作。
|
||||
|
||||
```SQL
|
||||
revoke all privileges on *.* from 'wangdachui'@'192.168.0.%';
|
||||
```
|
||||
|
||||
需要说明的是,由于数据库可能会缓存用户的权限,可以在授予或召回权限后执行下面的语句使新的权限即时生效。
|
||||
|
||||
```SQL
|
||||
flush privileges;
|
||||
```
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
## 索引
|
||||
|
||||
索引是关系型数据库中用来提升查询性能最为重要的手段。关系型数据库中的索引就像一本书的目录,我们可以想象一下,如果要从一本书中找出某个知识点,但是这本书没有目录,这将是意见多么可怕的事情!我们估计得一篇一篇的翻下去,才能确定这个知识点到底在什么位置。创建索引虽然会带来存储空间上的开销,就像一本书的目录会占用一部分篇幅一样,但是在牺牲空间后换来的查询时间的减少也是非常显著的。
|
||||
|
||||
MySQL 数据库中所有数据类型的列都可以被索引。对于MySQL 8.0 版本的 InnoDB 存储引擎来说,它支持三种类型的索引,分别是 B+ 树索引、全文索引和 R 树索引。这里,我们只介绍使用得最为广泛的 B+ 树索引。使用 B+ 树的原因非常简单,因为它是目前在基于磁盘进行海量数据存储和排序上最有效率的数据结构。B+ 树是一棵[平衡树](https://zh.wikipedia.org/zh-cn/%E5%B9%B3%E8%A1%A1%E6%A0%91),树的高度通常为3或4,但是却可以保存从百万级到十亿级的数据,而从这些数据里面查询一条数据,只需要3次或4次 I/O 操作。
|
||||
|
||||
B+ 树由根节点、中间节点和叶子节点构成,其中叶子节点用来保存排序后的数据。由于记录在索引上是排序过的,因此在一个叶子节点内查找数据时可以使用二分查找,这种查找方式效率非常的高。当数据很少的时候,B+ 树只有一个根节点,数据也就保存在根节点上。随着记录越来越多,B+ 树会发生分裂,根节点不再保存数据,而是提供了访问下一层节点的指针,帮助快速确定数据在哪个叶子节点上。
|
||||
|
||||
在创建二维表时,我们通常都会为表指定主键列,主键列上默认会创建索引,而对于 MySQL InnoDB 存储引擎来说,因为它使用的是索引组织表这种数据存储结构,所以主键上的索引就是整张表的数据,而这种索引我们也将其称之为**聚集索引**(clustered index)。很显然,一张表只能有一个聚集索引,否则表的数据岂不是要保存多次。我们自己创建的索引都是二级索引(secondary index),更常见的叫法是**非聚集索引**(non-clustered index)。通过我们自定义的非聚集索引只能定位记录的主键,在获取数据时可能需要再通过主键上的聚集索引进行查询,这种现象称为“回表”,因此通过非聚集索引检索数据通常比使用聚集索引检索数据要慢。
|
||||
|
||||
接下来我们通过一个简单的例子来说明索引的意义,比如我们要根据学生的姓名来查找学生,这个场景在实际开发中应该经常遇到,就跟通过商品名称查找商品是一个道理。我们可以使用 MySQL 的`explain`关键字来查看 SQL 的执行计划(数据库执行 SQL 语句的具体步骤)。
|
||||
|
||||
```SQL
|
||||
explain select * from tb_student where stuname='林震南'\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: tb_student
|
||||
partitions: NULL
|
||||
type: ALL
|
||||
possible_keys: NULL
|
||||
key: NULL
|
||||
key_len: NULL
|
||||
ref: NULL
|
||||
rows: 11
|
||||
filtered: 10.00
|
||||
Extra: Using where
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
```
|
||||
|
||||
在上面的 SQL 执行计划中,有几项值得我们关注:
|
||||
|
||||
1. `select_type`:查询的类型。
|
||||
- `SIMPLE`:简单 SELECT,不需要使用 UNION 操作或子查询。
|
||||
- `PRIMARY`:如果查询包含子查询,最外层的 SELECT 被标记为 PRIMARY。
|
||||
- `UNION`:UNION 操作中第二个或后面的 SELECT 语句。
|
||||
- `SUBQUERY`:子查询中的第一个 SELECT。
|
||||
- `DERIVED`:派生表的 SELECT 子查询。
|
||||
2. `table`:查询对应的表。
|
||||
3. `type`:MySQL 在表中找到满足条件的行的方式,也称为访问类型,包括:`ALL`(全表扫描)、`index`(索引全扫描,只遍历索引树)、`range`(索引范围扫描)、`ref`(非唯一索引扫描)、`eq_ref`(唯一索引扫描)、`const` / `system`(常量级查询)、`NULL`(不需要访问表或索引)。在所有的访问类型中,很显然 ALL 是性能最差的,它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。
|
||||
4. `possible_keys`:MySQL 可以选择的索引,但是**有可能不会使用**。
|
||||
5. `key`:MySQL 真正使用的索引,如果为`NULL`就表示没有使用索引。
|
||||
6. `key_len`:使用的索引的长度,在不影响查询的情况下肯定是长度越短越好。
|
||||
7. `rows`:执行查询需要扫描的行数,这是一个**预估值**。
|
||||
8. `extra`:关于查询额外的信息。
|
||||
- `Using filesort`:MySQL 无法利用索引完成排序操作。
|
||||
- `Using index`:只使用索引的信息而不需要进一步查表来获取更多的信息。
|
||||
- `Using temporary`:MySQL 需要使用临时表来存储结果集,常用于分组和排序。
|
||||
- `Impossible where`:`where`子句会导致没有符合条件的行。
|
||||
- `Distinct`:MySQL 发现第一个匹配行后,停止为当前的行组合搜索更多的行。
|
||||
- `Using where`:查询的列未被索引覆盖,筛选条件并不是索引的前导列。
|
||||
|
||||
从上面的执行计划可以看出,当我们通过学生名字查询学生时实际上是进行了全表扫描,不言而喻这个查询性能肯定是非常糟糕的,尤其是在表中的行很多的时候。如果我们需要经常通过学生姓名来查询学生,那么就应该在学生姓名对应的列上创建索引,通过索引来加速查询。
|
||||
|
||||
```SQL
|
||||
create index idx_student_name on tb_student(stuname);
|
||||
```
|
||||
|
||||
再次查看刚才的 SQL 对应的执行计划。
|
||||
|
||||
```SQL
|
||||
explain select * from tb_student where stuname='林震南'\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: tb_student
|
||||
partitions: NULL
|
||||
type: ref
|
||||
possible_keys: idx_student_name
|
||||
key: idx_student_name
|
||||
key_len: 62
|
||||
ref: const
|
||||
rows: 1
|
||||
filtered: 100.00
|
||||
Extra: NULL
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
```
|
||||
|
||||
可以注意到,在对学生姓名创建索引后,刚才的查询已经不是全表扫描而是基于索引的查询,而且扫描的行只有唯一的一行,这显然大大的提升了查询的性能。MySQL 中还允许创建前缀索引,即对索引字段的前N个字符创建索引,这样的话可以减少索引占用的空间(但节省了空间很有可能会浪费时间,**时间和空间是不可调和的矛盾**),如下所示。
|
||||
|
||||
```SQL
|
||||
create index idx_student_name_1 on tb_student(stuname(1));
|
||||
```
|
||||
|
||||
上面的索引相当于是根据学生姓名的第一个字来创建的索引,我们再看看 SQL 执行计划。
|
||||
|
||||
```SQL
|
||||
explain select * from tb_student where stuname='林震南'\G
|
||||
```
|
||||
|
||||
```
|
||||
*************************** 1. row ***************************
|
||||
id: 1
|
||||
select_type: SIMPLE
|
||||
table: tb_student
|
||||
partitions: NULL
|
||||
type: ref
|
||||
possible_keys: idx_student_name
|
||||
key: idx_student_name
|
||||
key_len: 5
|
||||
ref: const
|
||||
rows: 2
|
||||
filtered: 100.00
|
||||
Extra: Using where
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
```
|
||||
|
||||
不知道大家是否注意到,这一次扫描的行变成了2行,因为学生表中有两个姓“林”的学生,我们只用姓名的第一个字作为索引的话,在查询时通过索引就会找到这两行。
|
||||
|
||||
如果要删除索引,可以使用下面的SQL。
|
||||
|
||||
```SQL
|
||||
alter table tb_student drop index idx_student_name;
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```SQL
|
||||
drop index idx_student_name on tb_student;
|
||||
```
|
||||
|
||||
在创建索引时,我们还可以使用复合索引、函数索引(MySQL 5.7 开始支持),用好复合索引实现**索引覆盖**可以减少不必要的排序和回表操作,这样就会让查询的性能成倍的提升,有兴趣的读者可以自行研究。
|
||||
|
||||
我们简单的为大家总结一下索引的设计原则:
|
||||
|
||||
1. **最适合**索引的列是出现在**WHERE子句**和连接子句中的列。
|
||||
2. 索引列的基数越大(取值多、重复值少),索引的效果就越好。
|
||||
3. 使用**前缀索引**可以减少索引占用的空间,内存中可以缓存更多的索引。
|
||||
4. **索引不是越多越好**,虽然索引加速了读操作(查询),但是写操作(增、删、改)都会变得更慢,因为数据的变化会导致索引的更新,就如同书籍章节的增删需要更新目录一样。
|
||||
5. 使用 InnoDB 存储引擎时,表的普通索引都会保存主键的值,所以**主键要尽可能选择较短的数据类型**,这样可以有效的减少索引占用的空间,提升索引的缓存效果。
|
||||
|
||||
最后,还有一点需要说明,InnoDB 使用的 B-tree 索引,数值类型的列除了等值判断时索引会生效之外,使用`>`、`<`、`>=`、`<=`、`BETWEEN...AND... `、`<>`时,索引仍然生效;对于字符串类型的列,如果使用不以通配符开头的模糊查询,索引也是起作用的,但是其他的情况会导致索引失效,这就意味着很有可能会做全表查询。
|
|
@ -0,0 +1,336 @@
|
|||
## 视图、函数和过程
|
||||
|
||||
为了讲解视图、函数和过程,我们首先用下面的 DDL 和 DML 创建名为 hrs 的数据库并为其二维表添加如下所示的数据。
|
||||
|
||||
```SQL
|
||||
-- 创建名为hrs的数据库并指定默认的字符集
|
||||
create database `hrs` default charset utf8mb4;
|
||||
|
||||
-- 切换到hrs数据库
|
||||
use `hrs`;
|
||||
|
||||
-- 创建部门表
|
||||
create table `tb_dept`
|
||||
(
|
||||
`dno` int not null comment '编号',
|
||||
`dname` varchar(10) not null comment '名称',
|
||||
`dloc` varchar(20) not null comment '所在地',
|
||||
primary key (`dno`)
|
||||
);
|
||||
|
||||
-- 插入4个部门
|
||||
insert into `tb_dept` values
|
||||
(10, '会计部', '北京'),
|
||||
(20, '研发部', '成都'),
|
||||
(30, '销售部', '重庆'),
|
||||
(40, '运维部', '深圳');
|
||||
|
||||
-- 创建员工表
|
||||
create table `tb_emp`
|
||||
(
|
||||
`eno` int not null comment '员工编号',
|
||||
`ename` varchar(20) not null comment '员工姓名',
|
||||
`job` varchar(20) not null comment '员工职位',
|
||||
`mgr` int comment '主管编号',
|
||||
`sal` int not null comment '员工月薪',
|
||||
`comm` int comment '每月补贴',
|
||||
`dno` int not null comment '所在部门编号',
|
||||
primary key (`eno`),
|
||||
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
|
||||
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
|
||||
);
|
||||
|
||||
-- 插入14个员工
|
||||
insert into `tb_emp` values
|
||||
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
|
||||
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
|
||||
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
|
||||
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
|
||||
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
|
||||
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
|
||||
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
|
||||
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
|
||||
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
|
||||
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
|
||||
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
|
||||
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
|
||||
(3577, '杨过', '会计', 5566, 2200, null, 10),
|
||||
(3588, '朱九真', '会计', 5566, 2500, null, 10);
|
||||
```
|
||||
|
||||
### 视图
|
||||
|
||||
视图是关系型数据库中将一组查询指令构成的结果集组合成可查询的数据表的对象。简单的说,视图就是虚拟的表,但与数据表不同的是,数据表是一种实体结构,而视图是一种虚拟结构,你也可以将视图理解为保存在数据库中被赋予名字的 SQL 语句。
|
||||
|
||||
使用视图可以获得以下好处:
|
||||
|
||||
1. 可以将实体数据表隐藏起来,让外部程序无法得知实际的数据结构,让访问者可以使用表的组成部分而不是整个表,降低数据库被攻击的风险。
|
||||
2. 在大多数的情况下视图是只读的(更新视图的操作通常都有诸多的限制),外部程序无法直接透过视图修改数据。
|
||||
3. 重用 SQL 语句,将高度复杂的查询包装在视图表中,直接访问该视图即可取出需要的数据;也可以将视图视为数据表进行连接查询。
|
||||
4. 视图可以返回与实体数据表不同格式的数据,在创建视图的时候可以对数据进行格式化处理。
|
||||
|
||||
创建视图。
|
||||
|
||||
```SQL
|
||||
create view `vw_emp_simple`
|
||||
as
|
||||
select `eno`,
|
||||
`ename`,
|
||||
`job`,
|
||||
`dno`
|
||||
from `tb_emp`;
|
||||
```
|
||||
|
||||
> **提示**:因为视图不包含数据,所以每次使用视图时,都必须执行查询以获得数据,如果你使用了连接查询、嵌套查询创建了较为复杂的视图,你可能会发现查询性能下降得很厉害。因此,在使用复杂的视图前,应该进行测试以确保其性能能够满足应用的需求。
|
||||
|
||||
有了上面的视图,我们就可以使用之前讲过的 DCL, 限制某些用户只能从视图中获取员工信息,这样员工表中的工资(`sal`)、补贴(`comm`)等敏感字段便不会暴露给用户。下面的代码演示了如何从视图中获取数据。
|
||||
|
||||
```SQL
|
||||
select * from `vw_emp_simple`;
|
||||
```
|
||||
|
||||
查询结果:
|
||||
|
||||
```
|
||||
+------+-----------+--------------+-----+
|
||||
| eno | ename | job | dno |
|
||||
+------+-----------+--------------+-----+
|
||||
| 1359 | 胡二刀 | 销售员 | 30 |
|
||||
| 2056 | 乔峰 | 分析师 | 20 |
|
||||
| 3088 | 李莫愁 | 设计师 | 20 |
|
||||
| 3211 | 张无忌 | 程序员 | 20 |
|
||||
| 3233 | 丘处机 | 程序员 | 20 |
|
||||
| 3244 | 欧阳锋 | 程序员 | 20 |
|
||||
| 3251 | 张翠山 | 程序员 | 20 |
|
||||
| 3344 | 黄蓉 | 销售主管 | 30 |
|
||||
| 3577 | 杨过 | 会计 | 10 |
|
||||
| 3588 | 朱九真 | 会计 | 10 |
|
||||
| 4466 | 苗人凤 | 销售员 | 30 |
|
||||
| 5234 | 郭靖 | 出纳 | 10 |
|
||||
| 5566 | 宋远桥 | 会计师 | 10 |
|
||||
| 7800 | 张三丰 | 总裁 | 20 |
|
||||
+------+-----------+--------------+-----+
|
||||
```
|
||||
|
||||
既然视图是一张虚拟的表,那么视图的中的数据可以更新吗?视图的可更新性要视具体情况而定,以下类型的视图是不能更新的:
|
||||
|
||||
1. 使用了聚合函数(`SUM`、`MIN`、`MAX`、`AVG`、`COUNT`等)、`DISTINCT`、`GROUP BY`、`HAVING`、`UNION`或者`UNION ALL`的视图。
|
||||
2. `SELECT`中包含了子查询的视图。
|
||||
3. `FROM`子句中包含了一个不能更新的视图的视图。
|
||||
4. `WHERE`子句的子查询引用了`FROM`子句中的表的视图。
|
||||
|
||||
删除视图。
|
||||
|
||||
```SQL
|
||||
drop view if exists `vw_emp_simple`;
|
||||
```
|
||||
|
||||
> **说明**:如果希望更新视图,可以先用上面的命令删除视图,也可以通过`create or replace view`来更新视图。
|
||||
|
||||
视图的规则和限制。
|
||||
|
||||
1. 视图可以嵌套,可以利用从其他视图中检索的数据来构造一个新的视图。视图也可以和表一起使用。
|
||||
2. 创建视图时可以使用`order by`子句,但如果从视图中检索数据时也使用了`order by`,那么该视图中原先的`order by`会被覆盖。
|
||||
3. 视图无法使用索引,也不会激发触发器(实际开发中因为性能等各方面的考虑,通常不建议使用触发器,所以我们也不对这个概念进行介绍)的执行。
|
||||
|
||||
### 函数
|
||||
|
||||
MySQL 中的函数跟 Python 中的函数大同小异,因为函数都是用来封装功能上相对独立且会被重复使用的代码的。如果非要找出一些差别来,那么 MySQL 中的函数是可以执行 SQL 语句的。下面的例子,我们通过自定义函数实现了截断超长字符串的功能。
|
||||
|
||||
```SQL
|
||||
delimiter $$
|
||||
|
||||
create function fn_truncate_string(
|
||||
content varchar(10000),
|
||||
max_length int unsigned
|
||||
) returns varchar(10000) no sql
|
||||
begin
|
||||
declare result varchar(10000) default content;
|
||||
if char_length(content) > max_length then
|
||||
set result = left(content, max_length);
|
||||
set result = concat(result, '……');
|
||||
end if;
|
||||
return result;
|
||||
end $$
|
||||
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
> **说明1**:函数声明后面的`no sql`是声明函数体并没有使用 SQL 语句;如果函数体中需要通过 SQL 读取数据,需要声明为`reads sql data`。
|
||||
>
|
||||
> **说明2**:定义函数前后的`delimiter`命令是为了修改终止符(定界符),因为函数体中的语句都是用`;`表示结束,如果不重新定义定界符,那么遇到的`;`的时候代码就会被截断执行,显然这不是我们想要的效果。
|
||||
|
||||
在查询中调用自定义函数。
|
||||
|
||||
```SQL
|
||||
select fn_truncate_string('和我在成都的街头走一走,直到所有的灯都熄灭了也不停留', 10) as short_string;
|
||||
```
|
||||
|
||||
```
|
||||
+--------------------------------------+
|
||||
| short_string |
|
||||
+--------------------------------------+
|
||||
| 和我在成都的街头走一…… |
|
||||
+--------------------------------------+
|
||||
```
|
||||
|
||||
### 过程
|
||||
|
||||
过程(又称存储过程)是事先编译好存储在数据库中的一组 SQL 的集合,调用过程可以简化应用程序开发人员的工作,减少与数据库服务器之间的通信,对于提升数据操作的性能也是有帮助的。其实迄今为止,我们使用的 SQL 语句都是针对一个或多个表的单条语句,但在实际开发中经常会遇到某个操作需要多条 SQL 语句才能完成的情况。例如,电商网站在受理用户订单时,需要做以下一系列的处理。
|
||||
|
||||
1. 通过查询来核对库存中是否有对应的物品以及库存是否充足。
|
||||
2. 如果库存有物品,需要锁定库存以确保这些物品不再卖给别人, 并且要减少可用的物品数量以反映正确的库存量。
|
||||
3. 如果库存不足,可能需要进一步与供应商进行交互或者至少产生一条系统提示消息。
|
||||
4. 不管受理订单是否成功,都需要产生流水记录,而且需要给对应的用户产生一条通知信息。
|
||||
|
||||
我们可以通过过程将复杂的操作封装起来,这样不仅有助于保证数据的一致性,而且将来如果业务发生了变动,只需要调整和修改过程即可。对于调用过程的用户来说,过程并没有暴露数据表的细节,而且执行过程比一条条的执行一组 SQL 要快得多。
|
||||
|
||||
下面的过程实现 hrs 数据库中员工工资的普调,具体的规则是:`10`部门的员工薪资上浮`300`, `20`部门的员工薪资上浮`800`,`30`部门的员工薪资上浮`500`。
|
||||
|
||||
```SQL
|
||||
delimiter $$
|
||||
|
||||
create procedure sp_upgrade_salary()
|
||||
begin
|
||||
declare flag boolean default 1;
|
||||
-- 定义一个异常处理器
|
||||
declare continue handler for sqlexception set flag=0;
|
||||
|
||||
-- 开启事务环境
|
||||
start transaction;
|
||||
|
||||
update tb_emp set sal=sal+300 where dno=10;
|
||||
update tb_emp set sal=sal+800 where dno=20;
|
||||
update tb_emp set sal=sal+500 where dno=30;
|
||||
|
||||
-- 提交或回滚事务
|
||||
if flag then
|
||||
commit;
|
||||
else
|
||||
rollback;
|
||||
end if;
|
||||
end $$
|
||||
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
> **说明**:上面的过程代码中使用了`start transaction`来开启事务环境,关于事务,在本课的最后有一个简单的介绍。为了确定代码中是否发生异常,从而提交或回滚事务,上面的过程中定义了一个名为`flag`的变量和一个异常处理器,如果发生了异常,`flag`将会被赋值为`0`,后面的分支结构会根据`flag`的值来决定是执行`commit`,还是执行`rollback`。
|
||||
|
||||
调用过程。
|
||||
|
||||
```SQL
|
||||
call sp_upgrade_salary();
|
||||
```
|
||||
|
||||
删除过程。
|
||||
|
||||
```SQL
|
||||
drop procedure if exists sp_upgrade_salary;
|
||||
```
|
||||
|
||||
在过程中,我们可以定义变量、条件,可以使用分支和循环语句,可以通过游标操作查询结果,还可以使用事件调度器,这些内容我们暂时不在此处进行介绍。虽然我们说了很多过程的好处,但是在实际开发中,如果频繁的使用过程并将大量复杂的运算放到过程中,会给据库服务器造成巨大的压力,而数据库往往都是性能瓶颈所在,使用过程无疑是雪上加霜的操作。所以,对于互联网产品开发,我们一般建议让数据库只做好存储,复杂的运算和处理交给应用服务器上的程序去完成,如果应用服务器变得不堪重负了,我们可以比较容易的部署多台应用服务器来分摊这些压力。
|
||||
|
||||
如果大家对上面讲到的视图、函数、过程包括我们没有讲到的触发器这些知识有兴趣,建议大家阅读 MySQL 的入门读物[《MySQL必知必会》](https://item.jd.com/12818982.html)进行一般性了解即可,因为这些知识点在大家将来的工作中未必用得上,学了也可能仅仅是为了应付面试而已。
|
||||
|
||||
### 其他内容
|
||||
|
||||
#### 范式理论
|
||||
|
||||
范式理论是设计关系型数据库中二维表的指导思想。
|
||||
|
||||
1. 第一范式:数据表的每个列的值域都是由原子值组成的,不能够再分割。
|
||||
2. 第二范式:数据表里的所有数据都要和该数据表的键(主键与候选键)有完全依赖关系。
|
||||
3. 第三范式:所有非键属性都只和候选键有相关性,也就是说非键属性之间应该是独立无关的。
|
||||
|
||||
> **说明**:实际工作中,出于效率的考虑,我们在设计表时很有可能做出反范式设计,即故意降低方式级别,增加冗余数据来获得更好的操作性能。
|
||||
|
||||
#### 数据完整性
|
||||
|
||||
1. 实体完整性 - 每个实体都是独一无二的
|
||||
|
||||
- 主键(`primary key`) / 唯一约束(`unique`)
|
||||
2. 引用完整性(参照完整性)- 关系中不允许引用不存在的实体
|
||||
|
||||
- 外键(`foreign key`)
|
||||
3. 域(domain)完整性 - 数据是有效的
|
||||
- 数据类型及长度
|
||||
|
||||
- 非空约束(`not null`)
|
||||
|
||||
- 默认值约束(`default`)
|
||||
|
||||
- 检查约束(`check`)
|
||||
|
||||
> **说明**:在 MySQL 8.x 以前,检查约束并不起作用。
|
||||
|
||||
#### 数据一致性
|
||||
|
||||
1. 事务:一系列对数据库进行读/写的操作,这些操作要么全都成功,要么全都失败。
|
||||
|
||||
2. 事务的 ACID 特性
|
||||
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
|
||||
- 一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态
|
||||
- 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
|
||||
- 持久性:已被提交的事务对数据库的修改应该永久保存在数据库中
|
||||
|
||||
3. MySQL 中的事务操作
|
||||
|
||||
- 开启事务环境
|
||||
|
||||
```SQL
|
||||
start transaction
|
||||
```
|
||||
|
||||
- 提交事务
|
||||
|
||||
```SQL
|
||||
commit
|
||||
```
|
||||
|
||||
- 回滚事务
|
||||
|
||||
```SQL
|
||||
rollback
|
||||
```
|
||||
|
||||
4. 查看事务隔离级别
|
||||
|
||||
```SQL
|
||||
show variables like 'transaction_isolation';
|
||||
```
|
||||
|
||||
```
|
||||
+-----------------------+-----------------+
|
||||
| Variable_name | Value |
|
||||
+-----------------------+-----------------+
|
||||
| transaction_isolation | REPEATABLE-READ |
|
||||
+-----------------------+-----------------+
|
||||
```
|
||||
|
||||
可以看出,MySQL 默认的事务隔离级别是`REPEATABLE-READ`。
|
||||
|
||||
5. 修改(当前会话)事务隔离级别
|
||||
|
||||
```SQL
|
||||
set session transaction isolation level read committed;
|
||||
```
|
||||
|
||||
重新查看事务隔离级别,结果如下所示。
|
||||
|
||||
```
|
||||
+-----------------------+----------------+
|
||||
| Variable_name | Value |
|
||||
+-----------------------+----------------+
|
||||
| transaction_isolation | READ-COMMITTED |
|
||||
+-----------------------+----------------+
|
||||
```
|
||||
|
||||
关系型数据库的事务是一个很大的话题,因为当存在多个并发事务访问数据时,就有可能出现三类读数据的问题(脏读、不可重复读、幻读)和两类更新数据的问题(第一类丢失更新、第二类丢失更新)。想了解这五类问题的,可以阅读我发布在 CSDN 网站上的[《Java面试题全集(上)》](https://blog.csdn.net/jackfrued/article/details/44921941)一文的第80题。为了避免这些问题,关系型数据库底层是有对应的锁机制的,按锁定对象不同可以分为表级锁和行级锁,按并发事务锁定关系可以分为共享锁和独占锁。然而直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定适当的事务隔离级别,数据库就会通过分析 SQL 语句,然后为事务访问的资源加上合适的锁。此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的。想了解 MySQL 事务和锁的细节知识,推荐大家阅读进阶读物[《高性能MySQL》](https://item.jd.com/11220393.html),这也是数据库方面的经典书籍。
|
||||
|
||||
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示。需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定到底使用哪种事务隔离级别,这个地方没有万能的原则。
|
||||
|
||||
<img src="http://localhost/mypic/20211121225327.png" style="zoom:50%;">
|
||||
|
||||
### 总结
|
||||
|
||||
关于 MySQL 的知识肯定远远不止上面列出的这些,比如 MySQL 性能调优、MySQL 运维相关工具、MySQL 数据的备份和恢复、监控 MySQL 服务、部署高可用架构等,这一系列的问题在这里都没有办法逐一展开来讨论,那就留到有需要的时候再进行讲解吧,各位读者也可以自行探索。
|
|
@ -0,0 +1,192 @@
|
|||
## MySQL 新特性
|
||||
|
||||
#### JSON类型
|
||||
|
||||
很多开发者在使用关系型数据库做数据持久化的时候,常常感到结构化的存储缺乏灵活性,因为必须事先设计好所有的列以及对应的数据类型。在业务发展和变化的过程中,如果需要修改表结构,这绝对是比较麻烦和难受的事情。从 MySQL 5.7 版本开始,MySQL引入了对 JSON 数据类型的支持(MySQL 8.0 解决了 JSON 的日志性能瓶颈问题),用好 JSON 类型,其实就是打破了关系型数据库和非关系型数据库之间的界限,为数据持久化操作带来了更多的便捷。
|
||||
|
||||
JSON 类型主要分为 JSON 对象和 JSON数组两种,如下所示。
|
||||
|
||||
1. JSON 对象
|
||||
|
||||
```JSON
|
||||
{"name": "骆昊", "tel": "13122335566", "QQ": "957658"}
|
||||
```
|
||||
|
||||
2. JSON 数组
|
||||
|
||||
```JSON
|
||||
[1, 2, 3]
|
||||
```
|
||||
|
||||
```JSON
|
||||
[{"name": "骆昊", "tel": "13122335566"}, {"name": "王大锤", "QQ": "123456"}]
|
||||
```
|
||||
|
||||
哪些地方需要用到JSON类型呢?举一个简单的例子,现在很多产品的用户登录都支持多种方式,例如手机号、微信、QQ、新浪微博等,但是一般情况下我们又不会要求用户提供所有的这些信息,那么用传统的设计方式,就需要设计多个列来对应多种登录方式,可能还需要允许这些列存在空值,这显然不是很好的选择;另一方面,如果产品又增加了一种登录方式,那么就必然要修改之前的表结构,这就更让人痛苦了。但是,有了 JSON 类型,刚才的问题就迎刃而解了,我们可以做出如下所示的设计。
|
||||
|
||||
```SQL
|
||||
create table `tb_test`
|
||||
(
|
||||
`user_id` bigint unsigned,
|
||||
`login_info` json,
|
||||
primary key (`user_id`)
|
||||
) engine=innodb;
|
||||
|
||||
insert into `tb_test` values
|
||||
(1, '{"tel": "13122335566", "QQ": "654321", "wechat": "jackfrued"}'),
|
||||
(2, '{"tel": "13599876543", "weibo": "wangdachui123"}');
|
||||
```
|
||||
|
||||
如果要查询用户的手机和微信号,可以用如下所示的 SQL 语句。
|
||||
|
||||
```SQL
|
||||
select
|
||||
`user_id`,
|
||||
json_unquote(json_extract(`login_info`, '$.tel')) as 手机号,
|
||||
json_unquote(json_extract(`login_info`, '$.wechat')) as 微信
|
||||
from `tb_test`;
|
||||
```
|
||||
|
||||
```
|
||||
+---------+-------------+-----------+
|
||||
| user_id | 手机号 | 微信 |
|
||||
+---------+-------------+-----------+
|
||||
| 1 | 13122335566 | jackfrued |
|
||||
| 2 | 13599876543 | NULL |
|
||||
+---------+-------------+-----------+
|
||||
```
|
||||
|
||||
因为支持 JSON 类型,MySQL 也提供了配套的处理 JSON 数据的函数,就像上面用到的`json_extract`和`json_unquote`。当然,上面的 SQL 还有更为便捷的写法,如下所示。
|
||||
|
||||
```SQL
|
||||
select
|
||||
`user_id`,
|
||||
`login_info` ->> '$.tel' as 手机号,
|
||||
`login_info` ->> '$.wechat' as 微信
|
||||
from `tb_test`;
|
||||
```
|
||||
|
||||
再举个例子,如果我们的产品要实现用户画像功能(给用户打标签),然后基于用户画像给用户推荐平台的服务或消费品之类的东西,我们也可以使用 JSON 类型来保存用户画像数据,示意代码如下所示。
|
||||
|
||||
创建画像标签表。
|
||||
|
||||
```SQL
|
||||
create table `tb_tags`
|
||||
(
|
||||
`tag_id` int unsigned not null comment '标签ID',
|
||||
`tag_name` varchar(20) not null comment '标签名',
|
||||
primary key (`tag_id`)
|
||||
) engine=innodb;
|
||||
|
||||
insert into `tb_tags` (`tag_id`, `tag_name`)
|
||||
values
|
||||
(1, '70后'),
|
||||
(2, '80后'),
|
||||
(3, '90后'),
|
||||
(4, '00后'),
|
||||
(5, '爱运动'),
|
||||
(6, '高学历'),
|
||||
(7, '小资'),
|
||||
(8, '有房'),
|
||||
(9, '有车'),
|
||||
(10, '爱看电影'),
|
||||
(11, '爱网购'),
|
||||
(12, '常点外卖');
|
||||
```
|
||||
|
||||
为用户打标签。
|
||||
|
||||
```SQL
|
||||
create table `tb_users_tags`
|
||||
(
|
||||
`user_id` bigint unsigned not null comment '用户ID',
|
||||
`user_tags` json not null comment '用户标签'
|
||||
) engine=innodb;
|
||||
|
||||
insert into `tb_users_tags` values
|
||||
(1, '[2, 6, 8, 10]'),
|
||||
(2, '[3, 10, 12]'),
|
||||
(3, '[3, 8, 9, 11]');
|
||||
```
|
||||
|
||||
接下来,我们通过一组查询来了解 JSON 类型的巧妙之处。
|
||||
|
||||
1. 查询爱看电影(有`10`这个标签)的用户ID。
|
||||
|
||||
```SQL
|
||||
select `user_id` from `tb_users_tags` where 10 member of (`user_tags`->'$');
|
||||
```
|
||||
|
||||
2. 查询爱看电影(有`10`这个标签)的80后(有`2`这个标签)用户ID。
|
||||
|
||||
```SQL
|
||||
select `user_id` from `tb_users_tags` where json_contains(`user_tags`->'$', '[2, 10]');
|
||||
```
|
||||
|
||||
3. 查询爱看电影或80后或90后的用户ID。
|
||||
|
||||
```SQL
|
||||
select `user_id` from `tb_users_tags` where json_overlaps(user_tags->'$', '[2, 3, 10]');
|
||||
```
|
||||
|
||||
> **说明**:上面的查询用到了`member of`谓词和两个 JSON 函数,`json_contains`可以检查 JSON 数组是否包含了指定的元素,而`json_overlaps`可以检查 JSON 数组是否与指定的数组有重叠部分。
|
||||
|
||||
#### 窗口函数
|
||||
|
||||
MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源数据库早已提供了对窗口函数的支持,有的也将其称之为 OLAP(联机分析和处理)函数,听名字就知道跟统计和分析相关。为了帮助大家理解窗口函数,我们先说说窗口的概念。
|
||||
|
||||
窗口可以理解为记录的集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数。窗口函数和我们上面讲到的聚合函数比较容易混淆,二者的区别主要在于聚合函数是将多条记录聚合为一条记录,窗口函数是每条记录都会执行,执行后记录条数不会变。窗口函数不仅仅是几个函数,它是一套完整的语法,函数只是该语法的一部分,基本语法如下所示:
|
||||
|
||||
```SQL
|
||||
<窗口函数> over (partition by <用于分组的列名> order by <用户排序的列名>)
|
||||
```
|
||||
|
||||
上面语法中,窗口函数的位置可以放以下两种函数:
|
||||
|
||||
1. 专用窗口函数,包括:`lead`、`lag`、`first_value`、`last_value`、`rank`、`dense_rank`和`row_number`等。
|
||||
2. 聚合函数,包括:`sum`、`avg`、`max`、`min`和`count`等。
|
||||
|
||||
下面为大家举几个使用窗口函数的简单例子,我们直接使用上一课创建的 hrs 数据库。
|
||||
|
||||
例子1:查询按月薪从高到低排在第4到第6名的员工的姓名和月薪。
|
||||
|
||||
```SQL
|
||||
select * from (
|
||||
select
|
||||
`ename`, `sal`,
|
||||
row_number() over (order by `sal` desc) as `rank`
|
||||
from `tb_emp`
|
||||
) `temp` where `rank` between 4 and 6;
|
||||
```
|
||||
|
||||
> **说明**:上面使用的函数`row_number()`可以为每条记录生成一个行号,在实际工作中可以根据需要将其替换为`rank()`或`dense_rank()`函数,三者的区别可以参考官方文档或阅读[《通俗易懂的学会:SQL窗口函数》](https://zhuanlan.zhihu.com/p/92654574)进行了解。在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。
|
||||
>
|
||||
> ```SQL
|
||||
> select `rank`, `ename`, `sal` from (
|
||||
> select @a:=@a+1 as `rank`, `ename`, `sal`
|
||||
> from `tb_emp`, (select @a:=0) as t1 order by `sal` desc
|
||||
> ) as `temp` where `rank` between 4 and 6;
|
||||
> ```
|
||||
|
||||
例子2:查询每个部门月薪最高的两名的员工的姓名和部门名称。
|
||||
|
||||
```SQL
|
||||
select `ename`, `sal`, `dname`
|
||||
from (
|
||||
select
|
||||
`ename`, `sal`, `dno`,
|
||||
rank() over (partition by `dno` order by `sal` desc) as `rank`
|
||||
from `tb_emp`
|
||||
) as `temp` natural join `tb_dept` where `rank`<=2;
|
||||
```
|
||||
|
||||
> 说明:在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。
|
||||
>
|
||||
> ```SQL
|
||||
> select `ename`, `sal`, `dname` from `tb_emp` as `t1`
|
||||
natural join `tb_dept`
|
||||
where (
|
||||
select count(*) from `tb_emp` as `t2`
|
||||
where `t1`.`dno`=`t2`.`dno` and `t2`.`sal`>`t1`.`sal`
|
||||
)<2 order by `dno` asc, `sal` desc;
|
||||
> ```
|
|
@ -1,65 +1,7 @@
|
|||
## 第43课:Python程序接入MySQL数据库
|
||||
## Python程序接入MySQL数据库
|
||||
|
||||
在 Python3 中,我们可以使用`mysqlclient`或者`pymysql`三方库来接入 MySQL 数据库并实现数据持久化操作。二者的用法完全相同,只是导入的模块名不一样。我们推荐大家使用纯 Python 的三方库`pymysql`,因为它更容易安装成功。下面我们仍然以之前创建的名为`hrs`的数据库为例,为大家演示如何通过 Python 程序操作 MySQL 数据库实现数据持久化操作。
|
||||
|
||||
### 建库建表
|
||||
|
||||
```SQL
|
||||
-- 创建名为hrs的数据库并指定默认的字符集
|
||||
create database `hrs` default character set utf8mb4;
|
||||
|
||||
-- 切换到hrs数据库
|
||||
use `hrs`;
|
||||
|
||||
-- 创建部门表
|
||||
create table `tb_dept`
|
||||
(
|
||||
`dno` int not null comment '编号',
|
||||
`dname` varchar(10) not null comment '名称',
|
||||
`dloc` varchar(20) not null comment '所在地',
|
||||
primary key (`dno`)
|
||||
);
|
||||
|
||||
-- 插入4个部门
|
||||
insert into `tb_dept` values
|
||||
(10, '会计部', '北京'),
|
||||
(20, '研发部', '成都'),
|
||||
(30, '销售部', '重庆'),
|
||||
(40, '运维部', '深圳');
|
||||
|
||||
-- 创建员工表
|
||||
create table `tb_emp`
|
||||
(
|
||||
`eno` int not null comment '员工编号',
|
||||
`ename` varchar(20) not null comment '员工姓名',
|
||||
`job` varchar(20) not null comment '员工职位',
|
||||
`mgr` int comment '主管编号',
|
||||
`sal` int not null comment '员工月薪',
|
||||
`comm` int comment '每月补贴',
|
||||
`dno` int not null comment '所在部门编号',
|
||||
primary key (`eno`),
|
||||
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
|
||||
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
|
||||
);
|
||||
|
||||
-- 插入14个员工
|
||||
insert into `tb_emp` values
|
||||
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
|
||||
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
|
||||
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
|
||||
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
|
||||
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
|
||||
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
|
||||
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
|
||||
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
|
||||
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
|
||||
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
|
||||
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
|
||||
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
|
||||
(3577, '杨过', '会计', 5566, 2200, null, 10),
|
||||
(3588, '朱九真', '会计', 5566, 2500, null, 10);
|
||||
```
|
||||
|
||||
### 接入MySQL
|
||||
|
||||
首先,我们可以在命令行或者 PyCharm 的终端中通过下面的命令安装`pymysql`,如果需要接入 MySQL 8,还需要安装一个名为`cryptography`的三方库来支持 MySQL 8 的密码认证方式。
|
Loading…
Reference in New Issue