MySQL基础

目录

1、数据库概述

1.1、为什么使用数据库

  • 持久化(persistence)把数据保存到可掉电式存储设备中以供之后使用,大多数情况下, 数据持久化意味着将内存中的数据保存到硬盘上加以固化,而持久化的实现过程大多通过各种关系数据库来完成。
  • 持久化的主要作用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。

1.2、数据库与数据库管理系统

1.2.1、数据库的相关概念

DB:数据库(Database)
即存储数据的“仓库”,其本质是一个文件系统。它保存了一系列有组织的数据
DBMS:数据库管理系统(Database Management System)
是一种操纵和管理数据库的大型软件,用于建立、使用和维护数据库,对数据库进行统一管理和控制。用户通过数据库管理系统访问数据库表内的数据
SQL:结构化查询语言(Structured Query Language)
专门用来与数据库通信的语言

1.2.2、DB与DBMS的关系

数据库管理系统(DBMS)可以管理多个数据库,一般开发人员会针对每一个应用创建一个数据库。为保存应用中实体的数据,一般会在数据库创建多个表,以保存程序中实体用户的数据。

DB和DBMS的关系图

DB、DBMS和表的关系

1.2.3、常见的数据库管理系统(DCMS)

目前互联网上常见的数据库管理软件有OracleMySQLMS SQL ServerDB2PostgreSQLAccessSybaseInformix这几种。以下是2021年DB-Engines Ranking对各种数据库受欢迎程序进行调查统计后的统计结果,查看数据库最新排名:DB-Engines Ranking

常见的DBMS排名

1.2.4、常见的DBMS介绍

Oracle

1979年,Oracle 2诞生,它是第一个商用的RDBMS(关系型数据库管理系统)。随着Oracle软件的名气越来越大,公司也改名叫Oracle公司。

2007年,总计85亿美金收购BEA Systems。

2009年,总计74亿美金收购SUN。此前的2008年,SUN以10亿美金收购MySQL。意味着Oracle同时拥有了MySQL的管理权,至此Oracle在数据库领域中成为绝对的领导者。

2013年,甲骨文超越IBM,成为继Microsoft后全球第二大软件公司。

如今Oracle的年收入达到了400亿美金,足以证明商用(收费)数据库软件的价值。

SQL Server

SQL Server是微软开发的大型商业数据库,诞生于1989年。C#.Net等语言常使用,与WinNT完全集成,也可以很好的与Microsoft BackOffice产品集成。

DB2

IBM公司的数据库产品(收费),常应用在银行系统中。

PostgreSQL

PostgreSQL的稳定性极强,最符合SQL标准,开放源码,具备商业级DBMS质量。PG对数据量大的文本以及SQL处理较快。

SyBase

已经淡出历史舞台。提供了有一个非常专业数据建模的工具PowerDesigner

SQLite

嵌入式的小型数据库,应用在手机端。零配置,SQLite3不用安装,不用配置,不用启动,关闭或配置数据库实例。当系统崩溃后不用做任何恢复操作,再下次使用数据库的时候自动恢复。

Informix

IBM公司产品,取自Information和Unix的结合,它是第一个被移植到Linux上的商业数据库产品。仅运行于Unix/Linux平台,命令行操作。性能较高,支持集群,是英语安全性要求极高的系统,尤其是银行,证券系统的应用。

1.3、MySQL介绍

1.3.1、概述

  • MySQL是一个开放源代码的关系型数据库管理系统,由瑞典MySQL AB(创始人Michael Widenius)公司1995年开发,迅速成为开源数据库的No.1。
  • 2008年被Sun收购(10亿美金),2009年Sun被Oracle收购。MariaDB应运而生。(MySQL的创造者担心MySQL有闭源的风险,因此创建了MySQL的分支项目MariaDB)
  • MySQL 6.x版本之后分为社区版和商业版。
  • MySQL是一种关联数据库管理系统,将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
  • MySQL是可以定制的,采用了GPL(GNU General Public License)协议,可以修改源码来开发自己的MySQL系统。
  • MySQL支持大型的数据库,可以处理拥有上千万条记录的大型数据库。
  • MySQL支持大型数据库,支持5000万条记录的数据仓库,32系统表文件最大可支持4GB,64位系统支持最大的表文件为8TB
  • MySQL使用标准的SQL数据语言形式。
  • MySQL可以允许运行于多个系统上,并且支持多种语言。这些编程语言包括CC++PythonJavaPerlPHPRuby等。

1.3.2、MySQL发展史重大事件

MySQL的历使就是整个互联网的发展史。互联网业务从社交领域、电商领域到金融领域的发展,推动着应用对数据库的需求提升,对传统的数据库服务能力提出了挑战。高并发、高性能、高可用、轻资源、易维护、易扩展的需求,促进了MySQL的发展。

MySQL的发展历史:

  1. 1979年,Michael Widenius用BASIC设计了一个报表工具Unireg,这是一个很底层的面向报表的存储引擎,是存储引擎算法的前身,但不支持SQL。
  2. 1985年,David Axmark、Allan Larsson和Michael Widenius成立了一家公司,曾是MySQL AB的前身,他们设计了一个利用索引顺序存取数据的方法,也就是ISAM(Indexed Sequential Access Method)存储引擎算法的前身。
  3. 1996年,Michael Widenius和David Axmark一起协作,写出了MySQL的第一个版本。此时还只是小范围使用,几个月后直接发布了3.11版本。
  4. 1998年,CX Datakonsult公司正式更名为MySQL AB公司,同年MySQL官方网站完成建立(www.mysql.com)。
  5. 1999年,MySQL与Sleepcat公司合作,MySQL提供了支持事务的Berkeley DB存储引擎。后来由于这个引擎的许多问题,MySQL5.1以后不再对该引擎提供支持。
  6. 2000年,ISAM华丽转身为MyISAM存储引擎,同年MySQL开放了自己的源代码,并且基于GPL许可协议
  7. 2001年,MySQL开始集成InnoDB存储引擎。
  8. 2003年,MySQL发布4.0版本,与InnoDB正是结合。
  9. 2005年,MySQL5.0版本发布,这是一个里程碑的版本,实现了许多功能特性。同年5月,Oracle收购了开发InnoDB存储引擎的Innobase Oy公司,预示着不久后对MySQL的收购。
  10. 2008年,MySQL AB公司被Sun公司以10亿美金收购。
  11. 2009年,Oracle公司以74亿美元收购Sun公司,自此MySQL数据库进入Oracle时代
  12. 2010年,MySQL5.5发布,InnoDB存储引擎为MySQL的默认存储引擎
  13. 2013年,MySQL 5.6GA版本发布。
  14. 2015年,MySQL 5.7GA版本发布
  15. 2016年9月,MySQL发布里程碑式的版本MySQL 8.0.0
  16. 2018年4月,MySQL 8.0.11GA版本正式发布。

1.3.3、关于MySQL 8.0

MySQL从5.7版本直接跳跃发布了8.0版本,可见这是一个令人兴奋的里程碑式的版本。MySQL 8版本在功能上做了显著的改进与增强,开发者对MySQL的源代码进行了重构,最突出的一点是对MySQL Optimizer优化器进行了改进,不仅在速度上得到了改善,还为用户带来了更好的性能和更棒的体验。

1.3.4、为什么选择MySQL?

  1. 开放源码,使用成本低。
  2. 性能卓越,服务稳定。
  3. 软件体积小,使用简单,并且易于维护。
  4. 历史悠久,社区用户非常活跃,遇到问题可以寻求帮助。
  5. 许多互联网公司都在用,经过了时间的验证。

1.3.5、Oracle vs MySQL

Oracle更适合大型跨国企业的使用,因为他们对费用不敏感,但是对性能要求以及安全性有更高的有球。

MySQL由于体积小、速度快、总体拥有成本低,可处理上千万条记录的大型数据库,尤其是开放源码这一特点,使得很多互联网公司、中小型网站选择MySQL作为网站数据库(Facebook、Twitter、YouTube、阿里巴巴/蚂蚁金服、去哪儿、美团、腾讯)。

1.4、RDBMS与非RDBMS

关系型数据库(RDBMS, Relational DBMS)是DBMS的主流,其中使用最多的DBMS分别是Oracle、MySQL和SQL Server。这些都是关系型数据库(RDBMS)。

关系型数据库(RDBMS)

实质

  • 这种类型的数据库是最古老的数据库类型,关系型数据库模型是把复杂的数据结构归为简单的二元关系(即二维表格形式)。
  • 关系型数据库以行(row)列(column)的形式存储数据,以便于用户理解。这一系列的行和列被称为表(table),一组表组成了一个库(database)
  • 表与表之间的数据记录有关系(relationship)。现实世界的各种实体以及实体之间的各种联系均用关系模型来表示。关系型数据库就是建立在关系模型基础上的数据库。
  • SQL就是关系型数据库的查询语言。

优势

  • 复杂查询:可以用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询。
  • 事务支持:使得对于安全性能很高的数据访问要求得以实现。

非关系型数据库(非RDBMS)

介绍

非关系型数据库,可以看成传统关系型数据库的功能阉割版本(轻量级版本),基于键值对存储数据,不需要经过SQL层的解析,性能非常高。同时,通过减少不常用的功能进一步提高性能。目前基本上大部分主流的非关系型数据库都是免费的。

常见的非RDBMS

相比于SQL,NoSQL泛指非关系型数据库,包括了上述榜单上的键值型数据库(Reids)、文档型数据库(MongoDB)、搜索引擎和列存储等,除此以外还包括图形数据库。也只有用NoSQL才能将这些技术囊括进来。

键值型数据库

键值型数据库通过Key-Value键值对的方式来存储,其中Key和Value可以是简单的对象,也可以是复杂的对象。Key作为唯一标识符,优点是查找速度快,在这方面明显优于RDBMS,缺点是无法像RDBMS一样使用条件过滤(如WHERE),如果不知道去哪里找数据,就要遍历所有的Key,这会消耗掉大量的计算。键值型数据库典型的使用场景是作为内存缓存Redis是最流行的键值型数据库。

文档型数据库

此类数据库可存放并获取文档,可以是XML、JSON等格式。在数据库中文档作为处理信息的基本单位,一个文档就相当于一条记录。文档型数据库所存放的文档就相当于键值型数据库所存放的Value。MongoDB是最流行的文档型数据库。此外还有CouchDB等。

搜索引擎数据库

虽然关系型数据库采用了索引提升检索效率,但是针对全文索引效率却较低。搜索引擎数据库是应用在搜索引擎领域的数据存储形式,由于搜索引擎会爬取大量的数据,并以特定的格式进行存储,这样在检索的时候才能保证性能最优。核心原理是倒排索引。典型产品:Solr、Elasticsearch、Splunk 等。

列式数据库

列式数据库是相对于行式存储的数据库,Oracle、MySQL、SQL Server 等数据库都是采用的行式存储(Row-based),而列式数据库是将数据按照列存储到数据库中,这样做的好处是可以大量降低系统的I/O适合于分布式文件系统,不足在于功能相对有限。典型产品:HBase等。

图形数据库

图形数据库顾名思义,就是一种存储图形关系的数据库。它利用了图这种数据结构存储了实体(对象)之间的关系。关系型数据用于存储明确关系的数据,但对于复杂关系的数据存储却有些力不从心。如社交网络中人物之间的关系,如果用关系型数据库则非常复杂,用图形数据库将非常简单。典型产品:Neo4J、InfoGrid等。

NoSQL的演变

由于SQL一直称霸DBMS,因此许多人在思考是否有一种数据库技术能远离SQL,于是NoSQL诞生了,但是随着发展却发现越来越离不开SQL。到目前为止NoSQL 阵营中的DBMS都会有实现类似SQL的功能。下面是“NoSQL”这个名词在不同时期的诠释,从这些释义的变化中可以看出NoSQL功能的演变:
1970:NoSQL = We have no SQL
1980:NoSQL = Know SQL
2000:NoSQL = No SQL!
2005:NoSQL = Not only SQL
2013:NoSQL = No, SQL!
NoSQL对SQL做出了很好的补充,比如实际开发中,有很多业务需求,其实并不需要完整的关系型数据库功能,非关系型数据库的功能就足够使用了。这种情况下,使用性能更高、成本更低的非关系型数据库当然是更明智的选择。比如:日志收集、排行榜、定时器等。

1.5、关系型数据库设计规则

  • 关系型数据库的典型数据结构就是数据表,这些数据表的组成都是结构化的(Structured)。
  • 一个数据库中可以有多个表,每个表都有一个表名,用来表示自己。表名具有唯一性
  • 表具有一些特性,这些特性定义了数据在表中如何存储,类似Java和Python中的设计。(表 -> 类

1.5.1、表、记录、字段

  • E-R(entity-relationship,实体-联系)模型中有三个主要概念是:实体集属性联系集
  • 一个实体集(class)对应于数据库中的一个表(table),一个实体(instance)则对应于数据库表中的一行(row),也称为一条记录(record)。一个属性(attribute)对应于数据库表中的一列(column),也称为一个字段(field)

表、记录、字段的关系图

表、记录、字段的关系图

ORM思想(Object Relational Mapping)体现:

数据库中的一个表 <-----> Java或Python中的类

表中的一条记录 <-----> 类中的一个对象(实体)

表中的一个列 <-----> 类中的一个字段/属性(Field)

1.5.2、表的关联关系

  • 表与表之间的数据记录有关系(relationship)。现实世界中的各种实体以及实体之间的各种联系均用关系模型来表示。
  • 四种:一对一关联、一对多关联、多对多关联、自我引用。

一对一关系(one-to-one)

在实际的开发中应用不多,因为一对一可以创建成一张表。

  1. 举例:设计学生表:学号、姓名、手机号码、班级、系别、身份证号码、家庭住址、籍贯、紧急联系人、…

  2. 拆为两个表:两个表的记录是一一对应关系。

基础信息表(常用信息):学号、姓名、手机号码、班级、系别

档案信息表(不常用信息):学号、身份证号码、家庭住址、籍贯、紧急联系人、…

  1. 两种建表原则:
  • 外键唯一:主表的主键和从表的外键(唯一),形成主外键关系,外键唯一。

  • 外键是主键:主表的主键和从表的主键,形成主外键关系。

一对一关系

一对多关系(one-to-many)

  • 常见实例场景:客户表和订单表,分类表和商品表,部门表和员工表。如:

    员工表:编号、姓名、…、所属部门

    部门表:编号、名称、简介

  • 一对多建表原则:在从表(多方)创建一个字段,字段作为外键指向主表(一方)的主键

一对多关系

多对多关系(many-to-many)

要表示多对多关系,必须创建第三个表,该表通常称为联接表 ,它将多对多关系划分为两个一对多关系。将这两个表的主键都插入到第三个表中。

多对多关系

举例1:学生-课程

  • 学生信息表:一行代表一个学生的信息(学号、姓名、手机号码、班级、系别...)
  • 课程信息表:一行代表一个课程的信息(课程编号、授课老师、简介...)
  • 选课信息表(联接表):一个学生可以选多门课,一门课可以被多个学生选择
学号 课程编号
1 1001
2 1001
1 1002

举例2:产品-订单

“订单”表和“产品”表有一种多对多的关系,这种关系是通过与“订单明细”表建立两个一对多关系来定义的。一个订单可以有多个产品,每个产品可以出现在多个订单中。

  • 产品表:“产品”表中的每条记录表示一个产品。

  • 订单表:“订单”表中的每条记录表示一个订单。

  • 订单明细表(联接表):每个产品可以与“订单”表中的多条记录对应,即出现在多个订单中。一个订单可以与“产品”表中的多条记录对应,即包含多个产品。

举例3:用户-角色

多对多关系建表原则:需要创建第三张表,中间表中至少两个字段,这两个字段分别作为外键指向各自一方的主键。

自我引用(Self reference)

举例:员工有对应的主管,而主管也是员工。

2、MySQL环境搭建

2.1、MySQL 8.0卸载

第一步:停止MySQL服务

第二步:卸载软件

  • 方式1:使用控制面板直接卸载以下程序

    • MySQL Server 8.0
    • MySQL Installer - Community
  • 方式2:通过360或电脑管家附带的卸载程序功能

  • 方式3:通过安装包提供的卸载功能

    1. 双击下载的mysql-installer-community-8.0.26.0.msi文件,打开安装向导,安装向导会自动检测已安装的MySQL服务器程序

    2. 选择要卸载的MySQL服务器程序,单击 Remove,进行卸载

    3. 单击 Next,确认卸载

    4. 弹出是否同时移除数据目录选择窗口

      如果想要同时删除MySQL服务器中的数据,则勾选 Remove the data directory

    5. 执行卸载,单击 Execute

    6. 完成卸载,单击 Finish

      如果想要同时卸载MySQL8.0的安装向导程序,勾选 Yes,Uninstall MySQL Installer

第三步:删除MySQL的数据存储文件夹data,由my.ini文件配置的路径决定(此步关乎MySQL的所有数据库,可以对data文件夹中的内容进行备份处理,也可以省略该步骤)

第四步:删除MySQL的安装目录

第五步:删除注册表信息(MySQL 8.0前的版本需要手动删除注册表中的信息)

  1. 打开管理员的cmd

  2. 输入regedit打开注册表

  3. 删除以下信息(注意以下)

    HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Eventlog\Application\MySQL
    HKEY_LOCAL_MACHINE\SYSTEM\ControlSet002\Services\Eventlog\Application\MySQL
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\MySQL

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControl001\Services\MYSQL
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControl002\Services\MYSQL
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MYSQL

    注:注册表中的ControlSet001、ControlSet002不一定是001和002,也可能是其他数字后缀

第六步:删除以下MySQL的环境变量(MySQL的环境变量值都是MySQL的安装路径)

  1. 管理员的用户变量下的Path变量中的关于MySQL的值
  2. 系统变量中的Path变量中的关于MySQL的值(没有则省略该步骤)
  3. 系统变量中的MYSQL_HOME变量(没有则省略该步骤)

2.2、MySQL的下载与安装

2.2.1、MySQL的4大版本

  • MySQL Community Server社区版本,开源免费,自由下载,但不提供官方技术支持

  • MySQL Enterprise Edition企业版本,需要付费,不能在线下载,可以试用30天。提供了更多的功能和更完备的技术支持,适合对数据库的功能和可靠性更高的企业客户

  • MySQL Cluster集群版,开源免费。用于架设集群服务器,可将几个MySQL Server封装成一个Server。需要在社区版或企业版基础上使用

  • MySQL Cluster CGE高级集群版,需要付费

MySQL官网

MySQL的下载链接

下载、安装、配置MySQL 5.7

第一步:打开下载地址,点击下载安装包(以解压包.zip为例)

MySQL下载步骤

下载得到.zip安装文件

第二步:解压安装包,得到文件夹

MySQL文件目录

第三步:添加环境变量:此电脑-属性-高级系统设置-环境变量。在Administrator的用户变量下的Path变量添加MySQL的安装目录\bin目录

添加MySQL环境变量1

如果mysql安装完毕并且环境变量配置正确后还是无法使用mysql指令,则将mysql变量值上移至首行

第四步:在MySQL的安装目录文件夹下,创建my.ini文件,文件内容如下:

my.ini文件

[client]
# 设置3306端口,可以更改
port=3306
# 设置mysql客户端默认字符集
default-character-set=gbk
[mysqld]
# 设置3306端口,可以更改
port=3306
# 设置mysql的安装目录(个人的)
basedir=D:/Program Files (x86)/MySQL/MySQL-5.7.39
# 设置mysql数据的存放目录(个人的)(由系统创建,不需要手动创建该文件夹)
datadir=D:/Program Files (x86)/MySQL/MySQL-5.7.39/data/
# 允许最大连接数
max_connections=20
# 服务端使用的字符集默认为8比特编码的latin1字符集
character-set-server=gbk
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
#跳过安全检查(即密码为空)
skip-grant-tables
# 创建模式
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

第五步:使用管理员身份打开cmd,并切换到mysql的安装目录\bin目录下,执行mysqld -install指令

MySQL安装服务

在cmd命令符界面中:

  1. 使用mysqld -install 指定安装mysql的服务名指令执行mysql的服务安装
  2. 可以使用sc delete 指定服务名卸载服务
  3. 出现Service successfully installed.即服务安装成功

第六步:在管理员的cmd命令行界面,初始化数据库:mysqld --initialize-insecure --user=mysql

MySQL初始化数据库

data数据目录

第七步:在管理员的cmd命令行界面下,进入mysql的bin目录,执行net start [mysql服务名]开启服务

注:

  1. 指令net start mysql服务名:开启mysql服务
  2. 指令net stop mysql服务名:关闭mysql服务

开启和关闭mysql服务

第八步:进入mysql管理终端:mysql -u root -p,在mysql管理终端使用exit命令可以退出终端

说明:

  1. -u root:-u表示用户,root为用户名,root用户是mysql的管理员用户
  2. -p:代表密码,可以在-p后紧跟密码(显式地),如:mysql -u root -p123123-p与密码之间没有空格

进入mysql管理终端

第九步:修改root用户的密码

  1. 进入mysql管理终端

  2. 执行use mysql;指令,在mysql管理终端中使用mysql数据库。注:句末有;表示一个语句

  3. update user set authentication_string=password('123456') where user='root' and Host='localhost';
    

    说明:上述语句就是修改root用户的密码为123456,并且在本地登录(localhost)

    句末有;,回车执行该指令

  4. 执行flush privileges;,刷新权限

修改mysql登录密码

第十步:修改my.ini文件,注销跳过安全检查的内容后,重启mysql服务,再次进入(重启MySQL管理终端)就会进行权限验证

#跳过安全检查(即密码为空), 注销后需要使用正确的用户名和密码才能登录
#skip-grant-tables

第十一步:输入正确的用户名和密码,登录MySQL管理终端

使用密码登录MySQL管理终端

第十二步:为保证在任意目录下访问MySQL服务,需要为MySQL配置系统变量

此电脑-属性-高级系统设置-环境变量,在系统变量添加新变量:

变量名:MYSQL_HOME

变量值:D:\Program Files (x86)\MySQL\MySQL-5.7.39

系统变量的Path变量下添加一个路径:

%MYSQL_HOME%\bin

第十三步:修改mysql的端口号,再重启mysql服务,访问MySQL管理终端

修改后的my.ini文件

[client]
port=3357
# 设置mysql客户端默认字符集
default-character-set=utf8
[mysqld]
# 设置3306端口,可以更改
port=3357
# 设置mysql的安装目录
basedir=D:/Program Files (x86)/MySQL/MySQL-5.7.39
# 设置mysql数据的存放目录(由系统创建)
datadir=D:/Program Files (x86)/MySQL/MySQL-5.7.39/data/
# 允许最大连接数
max_connections=20
# 服务端使用的字符集默认为8比特编码的latin1字符集,修改为utf8字符集,否则会出现无法添加中文记录的错误
character-set-server=utf8
collation-server=utf8_general_ci
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
#跳过安全检查(即密码为空),注销后需要使用正确的用户名和密码才能登录
#skip-grant-tables
# 创建模式
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

注意:客户端client和mysqld下的端口号port都需要修改并且保持一致

完整命令登录MySQL终端2

使用命令行窗口连接MySQL服务(MySQL数据库)的指令

mysql [-h 主机IP] [-P 端口号] -u 用户名 -p密码

说明:

  1. [-h 主机IP]中,主机IP不显式指定则默认为本机(localhost),IP也可以写成127.0.0.1(表示本机)。
  2. [-P 端口号],端口号应该与my.ini文件中指定的端口号一致,如果不显式指定端口号,则监听的端口号默认为配置文件中的端口号port。-P(P必须为大写)与端口号之间可以有空格也可没有空格。
  3. 如果my.ini文件中端口号不是3306,则使用命令行窗口连接MySQL服务时必须显式指定端口号(-P 端口号)。
  4. 在实际工作中,端口号3306一般会修改。
  5. 登录MySQL管理终端前,必须先启动MySQL服务。
  6. -u和用户名之间可以有空格也可以没有空格 -> 即:mysql -uroot -p密码

完整命令登录MySQL终端

安装MySQL总结

  1. 下载.zip安装包,解压到指定路径filePath(filePath为MySQL的安装路径),以下步骤均使用管理员的cmd命令行界面在MySQL安装目录\bin目录下执行

  2. 配置MySQL的环境变量

  3. 在MySQL安装目录下创建my.ini文件,并写入相关内容

  4. 打开管理员的cmd命令行界面:执行以下命令

  5. cd /盘符名 mysql安装路径\bin
    mysqld -install mysql服务名
    mysqld --initialize-insecure --user=mysql
    net start mysql服务名
    mysql -u root -p
    
  6. 进入MySQL管理终端,执行以下命令修改mysql的root用户的登陆密码

    use mysql;
    update user set authentication_string=password('新密码') where user='root' and Host='localhost';
    flush privileges;
    exit;
    
  7. 注销my.ini文件中的关于跳过安全检查的内容,并重启mysql服务,即可用更改后的密码登录mysql数据库

    my.ini文件的修改内容:skip-grant-tables -> #skip-grant-tables

    #skip-grant-tables
    

    在DOS窗口下,输入命令

    net start mysql服务名
    mysql -u root -p新密码
    

2.3、图形化工具操控MySQL

常用的数据库图形化管理工具:SQL-yog(免费)、Navicat(收费)

此外,官方还提供了MySQL Workbench(GUITOOL),一款专为MySQL设计的图形界面管理工具,分为社区版(MySQL Workbench OSS)、商用版(MySQL WorkbenchSE)

3、基本SELECT语句

3.1、SQL概述

3.1.1、SQL背景知识

  • 1946 年,世界上第一台电脑诞生,如今,借由这台电脑发展起来的互联网已经自成江湖。在这几十 年里,无数的技术、产业在这片江湖里沉浮,有的方兴未艾,有的已经几幕兴衰。但在这片浩荡的 波动里,有一门技术从未消失,甚至“老当益壮”,那就是 SQL。

  • 45 年前,也就是 1974 年,IBM 研究员发布了一篇揭开数据库技术的论文《SEQUEL:一门结构 化的英语查询语言》,直到今天这门结构化的查询语言并没有太大的变化,相比于其他语言, SQL 的半衰期非常长。

  • 不论是前端工程师,还是后端算法工程师,都一定会和数据打交道,都需要了解如何又快又准确地 提取自己想要的数据。更别提数据分析师了,他们的工作就是和数据打交道,整理不同的报告,以 便指导业务决策。

  • SQL(Structured Query Language,结构化查询语言)是使用关系模型的数据库应用语言,与数据直接打交道,由 IBM 上世纪70年代开发出来。后由美国国家标准局(ANSI)开始着手制定SQL标准,先后有SQL-86,SQL-89,SQL-92,SQL-99等标准。

    • SQL 有两个重要的标准,分别是 SQL92 和 SQL99,它们分别代表了 92 年和 99 年颁布的 SQL 标准,我们今天使用的 SQL 语言依然遵循这些标准。
  • 不同的数据库生产厂商都支持SQL语句,但都有特有内容。

3.1.2、TIOBE编程语言排行榜

链接地址:TIOBE编程语言排行榜

编程语言受欢迎排行榜

3.1.3、SQL分类

SQL语言在功能上主要分为如下3大类:

  • DDL(Data Definition Languages、数据定义语言),这些语句定义了不同的数据库、表、视图、索引等数据库对象,还可以用来创建、删除、修改数据库和数据表的结构。主要的语句关键字包括 CREATE、DROP、ALTER、RENAME、TRUNCATE 等。

  • DML(Data Manipulation Language、数据操作语言),用于添加、删除、更新和查询数据库记录,并检查数据完整性。主要的 语句关键字包括INSERT 、 DELETE、UPDATE、SELECT等。SELECT是SQL语言的基础,最为重要,且在实际开发中使用最频繁

  • DCL(Data Control Language、数据控制语言),用于定义数据库、表、字段、用户的访问权限和安全级别。主要的语句关键字包括GRANT、REVOKE、COMMIT、ROLLBACK、SAVEPOINT等。

因为查询语句使用的非常的频繁,所以很多人把查询语句单拎出来一类:DQL(Data Query Language、数据查询语言)。还有单独将COMMIT、ROLLBACK取出来称为TCL(Transaction Control Language,事务控制语言)。

3.2、SQL语言的规则与规范

3.2.1、SQL规则

  • SQL 可以写在一行或者多行。为了提高可读性,各子句分行写,必要时使用缩进

  • 每条命令以;\g\G结束

  • 关键字不能被缩写也不能分行

  • 关于标点符号:

    • 必须保证所有的()、单引号、双引号是成对结束的
    • 必须使用英文状态下的半角输入方式
    • 字符串型和日期时间类型的数据可以使用单引号('')表示
    • 列的别名,尽量使用双引号(""),而且不建议省略AS

3.2.2、SQL大小写规范

  • MySQL在Windows环境下是大小写不敏感的(Windows大小写不敏感)
  • MySQL在Linux环境下是大小写敏感的
    • 数据库名、表名、表的别名、变量名是严格区分大小写的
    • 关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。
  • 推荐采用统一的书写规范:
    • 数据库名、表名、表别名、字段名、字段别名等都小写
    • SQL 关键字、函数名、绑定变量等都大写

3.2.3、注释

单行注释:#注释文字(MySQL特有的方式)
单行注释:-- 注释文字(--后面必须包含一个空格。) 这是SQL通用单行注释
多行注释:
/* 
注释文字
注释文字
....
*/

3.2.4、命名规则

  • 数据库、表名不得超过30个字符,变量名限制为29个
  • 必须只能包含A–Z, a–z, 0–9, _共63个字符
  • 数据库名、表名、字段名等对象名中间不要包含空格
  • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名
  • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使用```(着重号)`引起来
  • 保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性。假如数据类型在一个表里是整数,那在另一个表里可就别变成字符型了
#以下两句是一样的,不区分大小写
show databases;
SHOW DATABASES;
#创建表格
#create table student info(...); #表名错误,因为表名有空格
CREATE TABLE student_info(...);
#其中order使用``飘号,因为order和系统关键字或系统函数名等预定义标识符重名了
CREATE TABLE `order`(
);
SELECT id AS "编号", `name` AS "姓名" FROM t_stu; #起别名时,as都可以省略
SELECT id AS 编号, `name` AS 姓名 FROM t_stu; #如果字段别名中没有空格,那么可以省略""
SELECT id AS 编 号, `name` AS 姓 名 FROM t_stu; #错误,如果字段别名中有空格,那么不能省略""

3.2.5、数据导入指令

在命令行客户端登录mysql,使用source指令导入

mysql> source d:\mysqldb.sql
mysql> desc employees;

导入现有的数据表、表的数据的方式:

  • 方式1:source 文件的全路径名

    举例:source d:\atguigudb.sql;

  • 方式2:基于具体的图形化界面的工具可以导入数据

    如:SQLyog中 选择 “工具” -- “执行sql脚本” -- 选中xxx.sql即可。

3.3、基本的SELECT语句

SELECT关键字

SELECT 1; #没有任何子句
SELECT 9/2; #没有任何子句
#5. 最基本的SELECT语句: SELECT 字段1,字段2,... FROM 表名 
SELECT 1 + 1,3 * 2;
SELECT 1 + 1,3 * 2
FROM DUAL; #dual:伪表
#字符串、日期时间类型的变量需要使用一对''表示

注意:字符串、日期时间类型的变量需要使用一对''表示,在MySQL中使用双引号表示字符串不报错是因为MySQL不够严谨,在其他DBMS中会报语法错误

3.3.1、SELECT...FROM

语法

SELECT 标识选择哪些列
FROM 标识从哪个表中选择;

选择全部列:

SELECT *
FROM departments;

一般情况下,除非需要使用表中所有的字段数据,最好不要使用通配符‘*’。使用通配符虽然可以节省输入查询语句的时间,但是获取不需要的列数据通常会降低查询和所使用的应用程序的效率。

通配符的优势是,当不知道所需要的列的名称时,可以通过它获取它们。

在生产环境下,不推荐直接使用 SELECT * 进行查询。

选择特定的列:

SELECT department_id, location_id
FROM departments;

SELECT employee_id, last_name, salary
FROM employees;

MySQL中的SQL语句是不区分大小写的,因此SELECT和select的作用是相同的,但是,开发人员习惯将关键字大写、数据列和表名小写

3.3.2、AS列的别名

  • 重命名一个列
  • 便于计算
  • 紧跟列名,也可以在列名和别名之间加入关键字AS,别名使用双引号,以便在别名中包含空格或特殊的字符并区分大小写。
  • AS 可以省略(AS, alias的缩写)
  • 建议别名简短,见名知意
SELECT last_name AS name, commission_pct comm
FROM employees;
SELECT last_name "Name", salary * 12 "Annual Salary"
FROM employees;
# as:全称:alias(别名),可以省略
# 列的别名可以使用一对""引起来,不要使用''

3.3.3、DISTINCT去重

默认情况下,查询会返回全部行,包括重复行。在SELECT语句中使用关键字DISTINCT去除重复行

SELECT DISTINCT department_id
FROM employees;

# 7. 去除重复行
#查询员工表中一共有哪些部门id呢?
#错误的:没有去重的情况
SELECT department_id
FROM employees;
#正确的:去重的情况
SELECT DISTINCT department_id
FROM employees;
#错误的:两列的查询到的行数不一致
SELECT salary,DISTINCT department_id
FROM employees;
#仅仅是没有报错,但是没有实际意义。
SELECT DISTINCT department_id,salary
FROM employees;
#最后的结果是 74 条,因为这 74 个部门id不同,都有 salary 这个属性值。

DISTINCT 其实是对后面所有列名的组合进行去重,如果你想要看都有哪些不同的部门(department_id),只需要写 DISTINCT department_id 即可,后面不需要再加其他的列名了。

3.3.4、空值null参与运算

所有运算符或列值遇到null值,运算的结果都为null

空值:nullnull不等同于0'''null'

SELECT
  employee_id,
  salary,
  commission_pct,
  12 * salary * (1 + commission_pct) "annual_sal"
FROM
  employees;

SELECT
  employee_id,
  salary "月工资",
  salary * (1 + commission_pct) * 12 "年工资",
  commission_pct
FROM
  employees;

#实际问题的解决方案:引入IFNULL
 SELECT
  employee_id,
  salary "月工资",
  salary * (1 + IFNULL(commission_pct, 0)) * 12 "年工资",
  commission_pct
FROM
  `employees`;

注意:在MySQL里面,空值不等于空字符串。一个空字符串的长度是 0,而一个空值的长度是空。而且,在MySQL里面,空值是占用空间的

3.3.5、着重号``

#错误:order是关键字,作为表名需要着重号``
mysql> SELECT * FROM ORDER;
/*
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near 'ORDER' at line 1
*/
#正确:
mysql> SELECT * FROM `ORDER`;
mysql> SELECT * FROM `order`;

需要保证表中的字段、表名等没有和保留字、数据库系统或常用方法冲突。如果真的相同,请在SQL语句中使用一对```(着重号)`引起来。

3.3.6、查询常数

SELECT 查询还可以对常数进行查询。对的,就是在 SELECT 查询结果中增加一列固定的常数列。这列的取值是我们指定的,而不是从数据表中动态取出的

为什么还要对常数进行查询?

SQL 中的 SELECT 语法的确提供了这个功能,一般来说我们只从一个表中查询数据,通常不需要增加一个固定的常数列,但如果想整合不同的数据源,用常数列作为这个表的标记,就需要查询常数

比如说,我们想对 employees 数据表中的员工姓名进行查询,同时增加一列字段 corporation ,这个字段固定值为“尚硅谷”,可以这样写

SELECT '尚硅谷' AS corporation, last_name FROM employees;

3.4、DESC显示表结构

使用DESCRIBE 或 DESC 命令,表示表结构。

DESCRIBE employees;
DESC employees;
mysql> desc employees;
/*
+----------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+-------------+------+-----+---------+-------+
| employee_id | int(6) | NO | PRI | 0 | |
| first_name | varchar(20) | YES | | NULL | |
| last_name | varchar(25) | NO | | NULL | |
| email | varchar(25) | NO | UNI | NULL | |
| phone_number | varchar(20) | YES | | NULL | |
| hire_date | date | NO | | NULL | |
| job_id | varchar(10) | NO | MUL | NULL | |
| salary | double(8,2) | YES | | NULL | |
| commission_pct | double(2,2) | YES | | NULL | |
| manager_id | int(6) | YES | MUL | NULL | |
| department_id | int(4) | YES | MUL | NULL | |
+----------------+-------------+------+-----+---------+-------+
11 rows in set (0.00 sec)
*/

其中,各个字段的含义分别解释如下:

  • Field:表示字段名称。
  • Type:表示字段类型,这里 barcode、goodsname 是文本型的,price 是整数类型的。
  • Null:表示该列是否可以存储NULL值。
  • Key:表示该列是否已编制索引。PRI表示该列是表主键的一部分UNI表示该列是UNIQUE索引的一部分MUL表示在列中某个给定值允许出现多次。
  • Default:表示该列是否有默认值,如果有,那么值是多少。
  • Extra:表示可以获取的与给定列有关的附加信息,例如AUTO_INCREMENT等。

3.5、WHERE过滤数据

WHERE关键字能够过滤查询到的数据。

语法:

SELECT 字段1,字段2
FROM 表名
WHERE 过滤条件;

使用WHERE子句,将不满足条件的行过滤掉。WHERE子句紧随FROM子句

SELECT employee_id, last_name, job_id, department_id
FROM employees
WHERE department_id = 90 ;
#练习:查询90号部门的员工信息
SELECT * 
FROM employees
#过滤条件,声明在FROM结构的后面
WHERE department_id = 90;

#练习:查询last_name为'King'的员工信息
SELECT * 
FROM EMPLOYEES
WHERE LAST_NAME = 'King';
#在SQL语言中,这里''内的内容是严格区分大小写的
# MySQL中这样写也能查询到小写king是因为MySQL不够严谨
# 'King'在其他DBMS中只能查询到King的内容,而不能查询到'king'
# 这里King只能用''表示,不能使用双引号,使用双引号表示字符串在其他DBMS中会报错

章节练习

#第03章_基本的SELECT语句的课后练习
# 1.查询员工12个月的工资总和,并起别名为ANNUAL SALARY
#理解1:计算12月的基本工资
SELECT employee_id,last_name,salary * 12 "ANNUAL SALARY"
FROM employees;

#理解2:计算12月的基本工资和奖金
SELECT employee_id,last_name,salary * 12 * (1 + IFNULL(commission_pct,0)) "ANNUAL SALARY"
FROM employees;

# 2.查询employees表中去除重复的job_id以后的数据
SELECT DISTINCT job_id
FROM employees;

# 3.查询工资大于12000的员工姓名和工资
SELECT last_name,salary
FROM employees
WHERE salary > 12000;

# 4.查询员工号为176的员工的姓名和部门号
SELECT last_name,department_id
FROM employees
WHERE employee_id = 176;

# 5.显示表 departments 的结构,并查询其中的全部数据 
DESCRIBE departments;

SELECT * FROM departments;

4、运算符

运算符 作用 举例
+ SELECT 3 + 2
- SELECT 5 - 3
* SELECT 3 * 4
/ DIV SELECT 4 / 2, SELECT 8 DIV 4
% MOD 取模 SELECT 8 % 3, SELECT 11 MOD 4
= 等于 SELECT C FROM table WHERE A = B
<=> 安全等于 SELECT C FROM table WHERE A <=> B
<>(!=) 不等于 SELECT C FROM table WHERE A <> B
< 小于 SELECT C FROM table WHERE A < B
<= 小于等于 SELECT C FROM table WHERE A <= B
> 大于 SELECT C FROM table WHERE A > B
>= 大于等于 SELECT C FROM table WHERE A >= B

4.1、算术运算符

算数运算符:+、-、*、/(DIV)、%(MOD)

注意:

  • 在SQL中,+没有连接的作用,就表示加法运算。此时,会将字符串转换为数值(隐式转换),如:3 + '10A' = 13
  • 一个数乘以浮点数1和除以浮点数1后变成浮点数,数值与原数相等;如:3 * 1.0 = 3.0
  • 一个数除以整数后,不管是否能除尽,结果都为一个浮点数;如:10 / 5 = 2.0
  • 一个数除以另一个数,除不尽时,结果为一个浮点数,并保留到小数点后4位;如:11 / 3 = 3.6667
  • 乘法和除法的优先级相同,进行先乘后除操作与先除后乘操作,得出的结果相同。如:3 * 4 / 2 = 4 / 2 * 3 = 6.0
  • 在数学运算中,0不能用作除数,在MySQL中,一个数除以0为NULL。如:10 / 0 = NULL
  • 有NULL参与的任何运算,表达式结果都为NULL。如:3 + NULL = NULL

示例代码:

# 算术运算符: +  -  *  /  div  % mod
SELECT 100, 100 + 0, 100 - 0, 100 + 50, 100 + 50 * 30, 100 + 35.5, 100 - 35.5 
FROM DUAL;

# 在SQL中,+没有连接的作用,就表示加法运算。此时,会将字符串转换为数值(隐式转换)
SELECT 100 + '1'  # 在Java语言中,结果是:1001FROM DUAL;

SELECT 100 + 'a' #此时将'a'看做0处理
FROM DUAL;

SELECT 100 + NULL  # null值参与运算,结果为null
FROM DUAL;

SELECT 100, 100 * 1, 100 * 1.0, 100 / 1.0, 100 / 2,
100 + 2 * 5 / 2,100 / 3, 100 DIV 0  # 分母如果为0,则结果为null
FROM DUAL;

# 取模运算: % mod
SELECT 12 % 3,12 % 5, 12 MOD -5,-12 % 5,-12 % -5
FROM DUAL;

#练习:查询员工id为偶数的员工信息
SELECT employee_id,last_name,salary
FROM employees
WHERE employee_id % 2 = 0;

4.2、比较运算符

比较运算符:=、<=>、<>(!=)、<、<=、>、>=

比较运算符将表达式两边的操作数进行比较,为真返回1,为假返回0

注意:

  • 等于运算符(=)判断等号两边的值、字符串或表达式是否相等,如果相等则返回1,不相等则返回0。在SQL中,赋值符号使用:=

    在使用等于运算符时,遵循如下规则:

    • 如果等号两边的值、字符串或表达式都为字符串,则MySQL会按照字符串进行比较,其比较的是每个字符串中字符的ANSI编码是否相等。

    • 如果等号两边的值都是整数,则MySQL会按照整数来比较两个值的大小。

    • 如果等号两边的值一个是整数,另一个是字符串,则MySQL会将字符串转化为数字进行比较。

    • 如果等号两边的值、字符串或表达式中有一个为NULL,则比较结果为NULL。

  • 安全等于运算符(<=>)判断两边的值、字符串或表达式是否相等,相等返回1,不相等返回0。如果两边都为NULL时,则返回1,而不会像等于运算符(=)一样有NULL参与结果一律为NULL。如筛选某个字段中值为NULL的记录可以使用<=>。

  • 不等于运算符(<>)判断两边的值、字符串或表达式是否相等。相等则返回0,不相等则返回1。不等于运算符不能用于判断NULL值。

示例代码:

#2. 比较运算符
#2.1 =  <=>  <> !=  <  <=  >  >= 

# = 的使用
SELECT 1 = 2,1 != 2,1 = '1',1 = 'a',0 = 'a' #字符串存在隐式转换。如果转换数值不成功,则看做0
FROM DUAL;

SELECT 'a' = 'a','ab' = 'ab','a' = 'b' #两边都是字符串的话,则按照ANSI的比较规则进行比较。
FROM DUAL;

SELECT 1 = NULL,NULL = NULL # 只要有null参与判断,结果就为null
FROM DUAL;

SELECT last_name,salary,commission_pct
FROM employees
#where salary = 6000;
WHERE commission_pct = NULL;  #此时执行,不会有任何的结果

# <=> :安全等于。 记忆技巧:为NULL而生。

SELECT 1 <=> 2,1 <=> '1',1 <=> 'a',0 <=> 'a'
FROM DUAL;

SELECT 1 <=> NULL, NULL <=> NULL
FROM DUAL;

#练习:查询表中commission_pct为null的数据有哪些
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct <=> NULL;

SELECT 3 <> 2,'4' <> NULL, '' != NULL,NULL != NULL
FROM DUAL;

4.3、非符号运算符

运算符 名称 作用
IS NOT NULL 不为空运算符 判断值、字符串或表达式是否不为空
LEAST 最小值运算符 在多个值中返回最小值
GREATEST 最大值运算符 在多个值中返回最大值
BETWEEN AND 两值之间的运算符 判断一个值是否在区间内[a,b]
ISNULL(IS NULL) 为空运算符 判断值、字符串或表达式是否为空
IN 属于运算符 判断一个值是否为列表中任意一个值
NOT IN 不属于运算符 判断一个值是否不是列表中任意一个值
LIKE 模糊匹配运算符 判断一个值是否符合模糊匹配规则
ESCAPE 转义符 使用其他符号代替转义符/
REGEXP 正则表达式运算符 判断一个值是否符合正则表达式的规则
RLIKE 正则表达式运算符 判断一个值是否符合正则表达式的规则

注意:

  • ISNULL(IS NULL):如果值为NULL,则返回1,否则返回0
  • IS NOT NULL:如果值不为NULL,则返回1,否则返回0
  • LEATEST(参数列表)/GREATEST(参数列表):最小/最大值,如果参数列表中是整数或浮点数,则返回最值;如果参数列表有字符串,则返回字母序最小/最大的;如果参数列表中有NULL,则返回NULL。
  • BETWEEN A AND B:判断值是否在区间[A, B]中,在区间中则返回1,否则返回0。注意双闭区间包含区间端点。适合连续的情况
  • IN/NOT IN:判断值是否在列表中。条件满足返回1,否则返回0。适合离散的情况
  • LIKE:模糊查询。LIKE通常使用通配符%_%表示匹配0个或多个字符,_只能匹配1个字符,相当于一个占位符
  • REGEXP:
    • 语法格式:expr REGEXP 匹配条件
    • 正则表达式常用通配符:
      • ^匹配以该字符后面的字符开头的字符串。
      • $匹配以该字符前面的字符结尾的字符串。
      • .匹配任何一个单字符。
      • [...]匹配在方括号内的任何字符。例如,“[abc]”匹配“a”或“b”或“c”。为了命名字符的范围,使用一个‘-’。“[a-z]”匹配任何字母,而“[0-9]”匹配任何数字。
      • *匹配零个或多个在它前面的字符。例如,x*匹配任何数量的x字符,[0-9]*匹配任何数量的数字,而*匹配任何数量的任何字符。
  • RLIKE:正则表达式的模糊查询,通配符同REGEXP。

示例代码:

#① IS NULL \ IS NOT NULL \ ISNULL
#练习:查询表中commission_pct为null的数据有哪些
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct IS NULL;
#或
SELECT last_name,salary,commission_pct
FROM employees
WHERE ISNULL(commission_pct);

#练习:查询表中commission_pct不为null的数据有哪些
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct IS NOT NULL;
#或
SELECT last_name,salary,commission_pct
FROM employees
WHERE NOT commission_pct <=> NULL;

#② LEAST() \ GREATEST 

SELECT LEAST('g','b','t','m'),GREATEST('g','b','t','m')
FROM DUAL;

SELECT LEAST(first_name,last_name),LEAST(LENGTH(first_name),LENGTH(last_name))
FROM employees;

#③ BETWEEN 条件下界1 AND 条件上界2  (查询条件1和条件2范围内的数据,包含边界)
#查询工资在60008000的员工信息
SELECT employee_id,last_name,salary
FROM employees
#where salary between 6000 and 8000;
WHERE salary >= 6000 && salary <= 8000;

#交换60008000之后,查询不到数据
SELECT employee_id,last_name,salary
FROM employees
WHERE salary BETWEEN 8000 AND 6000;

#查询工资不在60008000的员工信息
SELECT employee_id,last_name,salary
FROM employees
WHERE salary NOT BETWEEN 6000 AND 8000;
#where salary < 6000 or salary > 8000;

#④ in (set)\ not in (set)

#练习:查询部门为10,20,30部门的员工信息
SELECT last_name,salary,department_id
FROM employees
#where department_id = 10 or department_id = 20 or department_id = 30;
WHERE department_id IN (10,20,30);

#练习:查询工资不是6000,7000,8000的员工信息
SELECT last_name,salary,department_id
FROM employees
WHERE salary NOT IN (6000,7000,8000);

#⑤ LIKE :模糊查询
# % : 代表不确定个数的字符 (0个,1个,或多个)

#练习:查询last_name中包含字符'a'的员工信息
SELECT last_name
FROM employees
WHERE last_name LIKE '%a%';

#练习:查询last_name中以字符'a'开头的员工信息
SELECT last_name
FROM employees
WHERE last_name LIKE 'a%';

#练习:查询last_name中包含字符'a'且包含字符'e'的员工信息
#写法1SELECT last_name
FROM employees
WHERE last_name LIKE '%a%' AND last_name LIKE '%e%';
#写法2SELECT last_name
FROM employees
WHERE last_name LIKE '%a%e%' OR last_name LIKE '%e%a%';

# _ :代表一个不确定的字符

#练习:查询第3个字符是'a'的员工信息
SELECT last_name
FROM employees
WHERE last_name LIKE '__a%';

#练习:查询第2个字符是_且第3个字符是'a'的员工信息
#需要使用转义字符: \ 
SELECT last_name
FROM employees
WHERE last_name LIKE '_\_a%';

#或者  (了解)
SELECT last_name
FROM employees
WHERE last_name LIKE '_$_a%' ESCAPE '$';

#⑥ REGEXP \ RLIKE :正则表达式
SELECT 'shkstart' REGEXP '^shk', 'shkstart' REGEXP 't$', 'shkstart' REGEXP 'hk'
FROM DUAL;

SELECT 'atguigu' REGEXP 'gu.gu','atguigu' REGEXP '[ab]'
FROM DUAL;

4.4、逻辑运算符

运算符 作用 示例
NOT 或 ! 逻辑非 SELECT NOT A
AND 或 && 逻辑与 SELECT A AND B
OR 或 || 逻辑或 SELECT A OR B
XOR 逻辑异或 SELECT A XOR B
  • NOT(!):逻辑非(NOT或!)运算符表示当给定的值为0时返回1;当给定的值为非0值时返回0;当给定的值为NULL时,返回NULL。

  • AND(&&):运算符是当给定的所有值均为非0值,并且都不为NULL时,返回1;当给定的一个值或者多个值为0时则返回0;否则返回NULL。

  • OR(||):运算符是当给定的值都不为NULL,并且任何一个值为非0值时,则返回1,否则返回0;当一个值为NULL,并且另一个值为非0值时,返回1,否则返回NULL;当两个值都为NULL时,返回NULL。

    OR可以和AND一起使用,但是在使用时要注意两者的优先级,由于AND的优先级高于OR,因此先对AND两边的操作数进行操作,再与OR中的操作数结合。

  • XOR:运算符是当给定的值中任意一个值为NULL时,则返回NULL;如果两个非NULL的值都是0或者都不等于0时,则返回0;如果一个值为0,另一个值不为0时,则返回1。

# 逻辑运算符:OR ||, AND &&, NOT !, XOR
 # or  and 
SELECT last_name, salary, department_id
FROM employees #where department_id = 10 or department_id = 20;
#where department_id = 10 and department_id = 20;
WHERE department_id = 50 AND salary > 6000;

# not 
SELECT last_name, salary, department_id
FROM employees #where salary not between 6000 and 8000;
#where commission_pct is not null;
WHERE NOT commission_pct <=> NULL;

# XOR :追求的"异"
SELECT last_name, salary, department_id
FROM employees
WHERE department_id = 50 XOR salary > 6000;
#注意:AND的优先级高于OR

4.5、位运算符

运算符 作用 示例
& 按位与 SELECT A & B
| 按位或 SELECT A | B
^ 按位异或 SELECT A ^ B
~ 按位取反 SELECT ~A
>> 按位右移 SELECT A >> 2
<< 按位左移 SELECT A << 3

注意:

  • 按位右移(>>)运算符将给定的值的二进制数的所有位右移指定的位数。右移指定的位数后,右边低位的数值被移出并丢弃,左边高位空出的位置用0补齐。
  • 按位左移(<<)运算符将给定的值的二进制数的所有位左移指定的位数。左移指定的位数后,左边高位的数值被移出并丢弃,右边低位空出的位置用0补齐。
# 位运算符: & |  ^  ~  >>   <<
SELECT 12 & 5, 12 | 5, 12 ^ 5
FROM DUAL;

SELECT 10 & ~ 1
FROM DUAL;

#在一定范围内满足:每向左移动1位,相当于乘以2;每向右移动一位,相当于除以2SELECT 4 << 1, 8 >> 1
FROM DUAL;

4.6、运算符的优先级

优先级 运算符
(优先级最低)1 :=, =(赋值运算符)
2 OR(||), XOR
3 AND(&&)
4 NOT
5 BETWEEN AND, CASE, WHERE, THEN, ELSE
6 =(比较运算符), <=>, >=, >, <=, <, <>(!=), IS, LIKE, REGEXP, IN
7 |
8 &
9 <<, >>
10 -, +
11 *, /(DIV), %(MOD)
12 ^
13 -(负号), ~(按位取反)
14 NOT(!)
(优先级最高)15 ()

4.7、正则表达式字符

字符 说明 例子 匹配值示例
^ 匹配文本的开始字符 ‘^b’匹配以字母b开头的字符串 book, big, banana
$ 匹配文本的结束字符 ‘st$’匹配以st结尾的字符串 test, resist, persist
. 匹配任何单个字符 ‘b.r’匹配任何b和t之间有一个字符的字符串 bit, bat, but
* 匹配0个或多个在它前面的字符 ‘f*n’匹配字符n前面有任意个字符f开头的字符串 fn, fan, faan, fabcn
+ 匹配前面的字符1次或多次 ‘ba+’匹配以b开头后面紧跟至少有一个a的字符串 ba, bay, bare, battle
<字符串> 匹配包含指定的字符串的文本 ‘fa’匹配包含fa的字符串 fan, afa, faad
[字符集合] 匹配字符集合中的任何一个字符 ‘[xz]’匹配包含x或者z的字符串 dizzy, zebra, x-ray
[^] 匹配不在括号中的任何字符 ‘[^abc]’匹配任何不包含a、b或c的字符串 desk, fox, f8ke
字符串 匹配前面的字符串至少n次 ‘b{2}’匹配2个或更多的b bbb, bbbb, bbbbbbb
字符串 匹配前面的字符串至少n次,至多m次,如果n为0,此参数为可选参数 ‘b{2, 4}’匹配含最少2个,最多4个b的字符串 bb, bbb, bbbb

章节练习

# 第04章_运算符课后练习
# 1.选择工资不在500012000的员工的姓名和工资
SELECT last_name,salary
FROM employees
#where salary not between 5000 and 12000;
WHERE salary < 5000 OR salary > 12000;

# 2.选择在2050号部门工作的员工姓名和部门号
SELECT last_name,department_id
FROM employees
# where department_id in (20,50);
WHERE department_id = 20 OR department_id = 50;

# 3.选择公司中没有管理者的员工姓名及job_id
SELECT last_name,job_id,manager_id
FROM employees
WHERE manager_id IS NULL;

SELECT last_name,job_id,manager_id
FROM employees
WHERE manager_id <=> NULL;

# 4.选择公司中有奖金的员工姓名,工资和奖金级别
SELECT last_name,salary,commission_pct
FROM employees
WHERE commission_pct IS NOT NULL;

SELECT last_name,salary,commission_pct
FROM employees
WHERE NOT commission_pct <=> NULL;

# 5.选择员工姓名的第三个字母是a的员工姓名
SELECT last_name
FROM employees
WHERE last_name LIKE '__a%';

# 6.选择姓名中有字母a和k的员工姓名
SELECT last_name
FROM employees
WHERE last_name LIKE '%a%k%' OR last_name LIKE '%k%a%';
#where last_name like '%a%' and last_name LIKE '%k%';

# 7.显示出表 employees 表中 first_name 以 'e'结尾的员工信息
SELECT first_name,last_name
FROM employees
WHERE first_name LIKE '%e';

SELECT first_name,last_name
FROM employees
WHERE first_name REGEXP 'e$'; # 以e开头的写法:'^e'

# 8.显示出表 employees 部门编号在 80-100 之间的姓名、工种
SELECT last_name,job_id
FROM employees
#方式1:推荐
WHERE department_id BETWEEN 80 AND 100;
#方式2:推荐,与方式1相同
#where department_id >= 80 and department_id <= 100;
#方式3:仅适用于本题的方式。
#where department_id in (80,90,100);

SELECT * FROM departments;

# 9.显示出表 employees 的 manager_id 是 100,101,110 的员工姓名、工资、管理者id
SELECT last_name,salary,manager_id
FROM employees
WHERE manager_id IN (100,101,110);

5、排序与分页

5.1、排序

使用子句ORDER BY可以对表中的数据排序。

语法格式:ORDER BY 列名 排序方式; 注意:WHERE子句必须在FROM子句之后ORDER BY子句之前

排序方式:排序方式不显示指定则默认为ASC(升序)

  • ASC:ascend表示升序排序,如:1,2,3,...
  • DESC:descend表示降序排序,如:9,8,7,6,...

可以使用列的别名排序,但是在WHERE子句中不能使用列的别名

多列排序:可以在ORDER BY子句中对多列排序。多列排序时,第一列必须有相同的列值,才会对第二列进行排序,如果第一列的列值都是唯一的,则不在对第二列排序。

示例代码:

# 排序
# 如果没有使用排序操作,默认情况下查询返回的数据是按照添加数据的顺序显示的。
SELECT * FROM employees;

# 1.1 基本使用
# 使用 ORDER BY 对查询到的数据进行排序操作。
# 升序:ASC (ascend)
# 降序:DESC (descend)

# 练习:按照salary从高到低的顺序显示员工信息
SELECT employee_id,last_name,salary
FROM employees
ORDER BY salary DESC;

# 练习:按照salary从低到高的顺序显示员工信息
SELECT employee_id,last_name,salary
FROM employees
ORDER BY salary ASC;

SELECT employee_id,last_name,salary
FROM employees
ORDER BY salary; # 如果在ORDER BY 后没有显式指名排序的方式的话,则默认按照升序排列。

#2. 我们可以使用列的别名,进行排序
SELECT employee_id,salary,salary * 12 annual_sal
FROM employees
ORDER BY annual_sal;

#列的别名只能在 ORDER BY 中使用,不能在WHERE中使用。
#如下操作报错!
SELECT employee_id,salary,salary * 12 annual_sal
FROM employees
WHERE annual_sal > 81600;

#3. 强调格式:WHERE 需要声明在FROM后,ORDER BY之前。
SELECT employee_id,salary
FROM employees
WHERE department_id IN (50,60,70)
ORDER BY department_id DESC;

#4. 二级排序
#练习:显示员工信息,按照department_id的降序排列,salary的升序排列
SELECT employee_id,salary,department_id
FROM employees
ORDER BY department_id DESC,salary ASC;

5.2、分页

需求:1、当查询返回的结果很多时,查看不方便。2、需要指定显示某几条的记录。

实现规则:MySQL使用LIMIT子句实现分页,将结果集一段一段的显示出来。

语法格式:LIMIT [位置偏移量], 返回的行数

说明:位置偏移量指从第几个位置开始计数(从0开始),当位置偏移量为0时可以省略该参数

--前10条记录:
SELECT * FROM 表名 LIMIT 0,10;
或者
SELECT * FROM 表名 LIMIT 10;
--第11至20条记录:
SELECT * FROM 表名 LIMIT 10,10;
--第21至30条记录:
SELECT * FROM 表名 LIMIT 20,10;

MySQL8.0新特性:LIMIT 返回行数 OFFSET [位置偏移量]

#2. 分页
#2.1 mysql使用limit实现数据的分页显示
# 需求1:每页显示20条记录,此时显示第1SELECT employee_id,last_name
FROM employees
LIMIT 0,20;

# 需求2:每页显示20条记录,此时显示第2SELECT employee_id,last_name
FROM employees
LIMIT 20,20;

# 需求3:每页显示20条记录,此时显示第3SELECT employee_id,last_name
FROM employees
LIMIT 40,20;

#需求:每页显示pageSize条记录,此时显示第pageNo页:
#公式:LIMIT (pageNo-1) * pageSize,pageSize;

#2.2 WHERE ... ORDER BY ...LIMIT 声明顺序如下:
# LIMIT的格式: 严格来说:LIMIT 位置偏移量,条目数
# 结构"LIMIT 0,条目数" 等价于 "LIMIT 条目数"
SELECT employee_id,last_name,salary
FROM employees
WHERE salary > 6000
ORDER BY salary DESC
#limit 0,10;
LIMIT 10;

#练习:表里有107条数据,我们只想要显示第 3233 条数据怎么办呢?
SELECT employee_id,last_name
FROM employees
LIMIT 31,2;

#2.3 MySQL8.0新特性:LIMIT ... OFFSET ...
#练习:表里有107条数据,我们只想要显示第 3233 条数据怎么办呢?

SELECT employee_id,last_name
FROM employees
LIMIT 2 OFFSET 31;

#练习:查询员工表中工资最高的员工信息
SELECT employee_id,last_name,salary
FROM employees
ORDER BY salary DESC
#limit 0,1
LIMIT 1;

#2.4 LIMIT 可以使用在MySQL、PGSQL、MariaDB、SQLite 等数据库中使用,表示分页。
# 不能使用在SQL Server、DB2、Oracle!

分页显示公式:(当前页数 - 1) * 每页条数, 每页条数

SELECT *
FROM table
LIMIT (PageNo - 1) * PageSize, PageSize;

注意:LIMIT子句必须放在整个SELECT语句的最后

分页的优点:

约束返回结果的数量可以 减少数据表的网络传输量,也可以提升查询效率。如果我们知道返回结果只有1条,就可以使用 LIMIT 1 ,告诉 SELECT 语句只需要返回一条记录即可。这样的好处就是 SELECT 不需要扫描完整的表,只需要检索到一条符合条件的记录即可返回。可以配合排序查询列的最值。

章节练习

#1. 查询员工的姓名和部门号和年薪,按年薪降序,按姓名升序显示 
SELECT last_name,department_id,salary * 12 annual_salary
FROM employees
ORDER BY annual_salary DESC,last_name ASC;

#2. 选择工资不在 800017000 的员工的姓名和工资,按工资降序,显示第2140位置的数据 
SELECT last_name,salary
FROM employees
WHERE salary NOT BETWEEN 8000 AND 17000
ORDER BY salary DESC
LIMIT 20,20;


#3. 查询邮箱中包含 e 的员工信息,并先按邮箱的字节数降序,再按部门号升序
SELECT employee_id,last_name,email,department_id
FROM employees
#where email like '%e%'
WHERE email REGEXP '[e]'
ORDER BY LENGTH(email) DESC,department_id;

已学所有知识结合的SELECT查询结构:

SELECT table.column
FROM table
WHERE condition
ORDER BY column ASC(DESC)
LIMIT 位置偏移量, 返回行数;

6、多表查询

多表查询也称关联查询,指两个表或更多表一起完成查询操作。

前提条件:这些一起查询的表之间有关系(一对一、一对多),它们之间一定有关联字段,这个关联字段可能建立了外键,也可能没有建立外键。如员工表(employees)和部门表(departments)之间使用department_id关联。

6.1、笛卡尔积现象

示例代码:

#错误的实现方式:每个员工都与每个部门匹配了一遍。
SELECT employee_id,department_name
FROM employees,departments;  #查询出2889条记录

#错误的方式
SELECT employee_id,department_name
FROM employees CROSS JOIN departments;#查询出2889条记录

笛卡尔积(也称交叉连接)的理解:

笛卡尔乘积是一个数学运算。假设有两个集合 X 和 Y,那么 X 和 Y 的笛卡尔积就是 X 和 Y 的所有可能组合,也就是第一个对象来自于 X,第二个对象来自于 Y 的所有可能。组合的个数即为两个集合中元素个数的乘积数。

在SQL92中,笛卡尔积也称为交叉连接(CROSS JOIN)。在SQL99中也是使用CROSS JOIN实现交叉连接,它的作用是使任意表进行连接,即使表之间不相关。

如下情况会出现笛卡尔积:

#查询员工姓名和所在部门名称
SELECT last_name,department_name FROM employees,departments;
SELECT last_name,department_name FROM employees CROSS JOIN departments;
SELECT last_name,department_name FROM employees INNER JOIN departments;
SELECT last_name,department_name FROM employees JOIN departments;

笛卡尔积的错误会在下面的条件下产生:

  • 省略多个表的连接条件(关联条件)
  • 连接条件(关联条件)无效
  • 所有表中的所有行互相连接

为了避免笛卡尔积,可以在WHERE子句中加入有效的连接条件

加入连接条件后的查询语法:

SELECT table1.column, table2.column
FROM table1, table2
WHERE table1.column1 = table2.column2; # 连接条件(关联条件)

案例:查询员工的姓名及其部门名称

SELECT employees.last_name, departments.department_name #建议:加上表名前缀,防止同一个字段名在多表出现
FROM employees, departments
WHERE employees.department_id = departments.department_id;

注意:多表查询时,如果不同表之间有相同字段时,则必须在字段名之前加上表名前缀

# 如果查询语句中出现了多个表中都存在的字段,则必须指明此字段所在的表。
SELECT employees.employee_id,departments.department_name,employees.department_id
FROM employees,departments
WHERE employees.`department_id` = departments.department_id;
#建议:从sql优化的角度,建议多表查询时,每个字段前都指明其所在的表。可以避免在其他表中检索字段,优化查询速度

#如果给表起了别名,一旦在SELECTWHERE中使用表名的话,则必须使用表的别名,而不能再使用表的原名。
#如下的操作是错误的:
SELECT emp.employee_id,departments.department_name,emp.department_id
FROM employees emp,departments dept
WHERE emp.`department_id` = departments.department_id;

#6. 结论:如果有n个表实现多表的查询,则需要至少n-1个连接条件
#练习:查询员工的employee_id,last_name,department_name,city
SELECT e.employee_id,e.last_name,d.department_name,l.city,e.department_id,l.location_id
FROM employees e,departments d,locations l
WHERE e.`department_id` = d.`department_id`
AND d.`location_id` = l.`location_id`;

总结:如果有n个表实现多表的查询,则需要至少n-1个连接条件。

6.2、多表查询分类

  • 角度1:等值连接 vs 非等值连接
  • 角度2:自连接 vs 非自连接
  • 角度3:内连接 vs 外连接

6.2.1、等值连接vs非等值连接

等值连接:连接条件相等

# 案例:查询员工所在部门和城市名
SELECT e.`last_name`, d.`department_name`, l.`city`
FROM employees e, departments d, locations l
WHERE e.`department_id` = d.`department_id`
AND d.`location_id` = l.`location_id`;

非等值连接:连接条件不相等

# 案例:查询员工的姓名,薪资,薪资等级
SELECT e.`last_name`, e.`salary`, j.`grade_level`
FROM employees e, job_grades j
WHERE e.`salary` BETWEEN j.`lowest_sal` AND j.`highest_sal`;
# WHERE e.`salary` >= j.`lowest_sal` AND e.`salary` <= j.`highest_sal`;

6.2.2、自连接vs非自连接

非自连接:不与自身连接,多表之间连接

以上多表查询案例都属于非自连接 -> 不同表之间相互连接

自连接:自己与自己连接

# 案例:查询员工id和姓名,以及员工的主管id和姓名
SELECT e.`employee_id`, e.`last_name`, m.`employee_id` AS Mar_id, m.`last_name` AS Manager
FROM employees e, employees m
WHERE e.`manager_id` = m.`employee_id`;

6.2.3、内连接vs外连接

内连接:合并具有同一列的两个以上的表的行,结果集中不包含一个表与另一个表不匹配的行。即:只查询左表(FROM子句中第一张表)和右表(FROM子句中第二张表)中满足WHERE连接条件的记录,忽略其他不满足连接条件的记录。

# 案例:查询员工id,姓名,部门
SELECT employee_id, last_name, department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id; # 忽略了两种情况:1.没有部门的员工 2.没有员工的部门

以上的多表查询案例都属于内连接 -> 都忽略了不满足连接条件的记录

SQL92语法实现内连接:如上

SQL99语法实现内连接:使用INNER JOIN实现内连接,其中INNER可以省略(建议加上INNER,以示和外连接区分),当多表查询涉及两张以上的表时则使用多次JOIN ON连接。

语法格式:

SELECT table.column, table.column
FROM table1 INNER JOIN table2
ON 连接条件;
#SQL99语法实现内连接:
SELECT last_name,department_name
FROM employees e INNER JOIN departments d
ON e.`department_id` = d.`department_id`;

# 三表联合查询
SELECT last_name,department_name,city
FROM employees e JOIN departments d
ON e.`department_id` = d.`department_id`
JOIN locations l
ON d.`location_id` = l.`location_id`;

外连接:合并具有同一列的两个以上的表的行,结果集中除了包含一个表与另一个表匹配的行之外,还包含了左(右)表中不匹配的行,这种连接称为左(右)外连接,没有匹配的行时,结果表中显示为NULL。

  • 左外连接:包含左表中不匹配的行,左表称为主表右表称为从表
  • 右外连接:包含右表中不匹配的行,右表称为主表左表称为从表
  • 满外连接:也称为全外连接,即除了包含匹配的行,还包含了左表和右表中不匹配的行。

6.3、外连接(最常用)

SQL92语法实现外连接:使用(+)实现,在包含不匹配的左(右)表后加上(+)号。

注意:MySQL中不支持SQL92语法中外连接的写法。

# 案例:查询员工id和部门(左外连接)
SELECT employee_id, department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id(+);

JOIN ON关键字

SQL99语法实现外连接:使用JOIN...ON...的方式实现多表的查询,MySQL支持SQL99语法中外连接的写法。

语法格式:

SELECT table.column
FROM table1 OUTER JOIN table2
ON 连接条件

说明:

  1. OUTER JOIN前面加上LEFTRIGHT可以实现左外连接和右外连接。
  2. MySQL中不支持使用FULL OUTER JOIN的方式实现满外连接。但是可以使用LEFT JOIN UNION RIGHT JOIN代替。
#练习:查询所有的员工的last_name,department_name信息 
# 左外连接:
SELECT last_name,department_name
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`;

#右外连接:
SELECT last_name,department_name
FROM employees e RIGHT OUTER JOIN departments d
ON e.`department_id` = d.`department_id`;

#满外连接:mysql不支持FULL OUTER JOIN
SELECT last_name,department_name
FROM employees e FULL OUTER JOIN departments d
ON e.`department_id` = d.`department_id`;

UNION和UNION ALL

UNION合并查询结果:利用UNION关键字,可以给出多条SELECT语句,并将它们的结果组合成单个结果集。合并时,两个表对应的列数和数据类型必须相同,并且相互对应。各个SELECT语句之间使用UNIONUNION ALL关键字分隔。

语法格式:

SELECT column,...FROM table1
UNION [ALL]
SELECT column,...FROM table2

UNION操作符:返回两个查询的结果集的并集,并去除重复记录

# 举例:查询部门编号>90或邮箱包含a的员工信息
#方式1
SELECT * FROM employees WHERE employees.department_id > 90 OR employees.email LIKE '%a%';
#方式2
SELECT * FROM employees WHERE employees.department_id > 90
UNION
SELECT * FROM employees WHERE employees.email LIKE '%a%';

UNION ALL操作符:返回两个查询的结果集的并集。对于两个结果集的重复部分不会去重

# 举例:查询中国用户中男性的信息以及美国用户中年男性的用户信息
SELECT id,cname FROM t_chinamale WHERE csex='男'
UNION ALL
SELECT id,tname FROM t_usmale WHERE tGender='male';

注意:执行UNION ALL语句时所需要的资源比UNION语句少。如果明确知道合并数据后的结果数据不存在重复数据,或者不需要去除重复的数据,则尽量使用UNION ALL语句,以提高数据查询的效率。

7种SQL JOINS的实现

7种SQL JOINS

#中图:内连接 A∩B
SELECT employee_id, last_name, department_name
FROM employees e JOIN departments d
ON e.`department_id` = d.`department_id`;

#左上图:左外连接
SELECT employee_id,last_name,department_name
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`;

#右上图:右外连接
SELECT employee_id,last_name,department_name
FROM employees e RIGHT JOIN departments d
ON e.`department_id` = d.`department_id`;

#左中图:A - A∩B
SELECT employee_id,last_name,department_name
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE d.`department_id` IS NULL;

#右中图:B-A∩B
SELECT employee_id,last_name,department_name
FROM employees e RIGHT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE e.`department_id` IS NULL;

# 左下图:满外连接
# 左中图 + 右上图 A∪B
SELECT employee_id,last_name,department_name
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE d.`department_id` IS NULL
UNION ALL #没有去重操作,效率高
SELECT employee_id,last_name,department_name
FROM employees e RIGHT JOIN departments d
ON e.`department_id` = d.`department_id`;

#右下图
#左中图 + 右中图 A ∪B- A∩B 或者 (A - A∩B) ∪ (B - A∩B)
SELECT employee_id,last_name,department_name
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE d.`department_id` IS NULL
UNION ALL
SELECT employee_id,last_name,department_name
FROM employees e RIGHT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE e.`department_id` IS NULL;

SQL99语法新特性

自然连接NATURAL JOIN

SQL99在SQL92的基础上提供了一些特殊语法,比如NATURAL JOIN用来表示自然连接。我们可以把自然连接理解为SQL92 中的等值连接。它会帮你自动查询两张连接表中所有相同的字段,然后进行等值连接

# SQL92语法:
SELECT employee_id,last_name,department_name
FROM employees e JOIN departments d
ON e.`department_id` = d.`department_id`
AND e.`manager_id` = d.`manager_id`;

# SQL99语法:
# NATURAL JOIN : 它会帮你自动查询两张连接表中`所有相同的字段`,然后进行`等值连接`。
SELECT employee_id,last_name,department_name
FROM employees e NATURAL JOIN departments d;

USING连接

SQL99还支持使用USING指定数据表里的同名字段进行等值连接。但是只能配合JOIN一起使用。USING指定了具体的相同的字段名称,你需要在USING的括号()中填入要指定的同名字段。同时使用 JOIN...USING 可以简化 JOIN ON 的等值连接。

SELECT employee_id,last_name,department_name
FROM employees e JOIN departments d
USING (department_id);
# 等同于
SELECT employee_id,last_name,department_name
FROM employees e ,departments d
WHERE e.department_id = d.department_id;

总结

表连接的约束条件可以有三种:

  • WHERE:可以写任意的连接条件
  • ON:只能和JOIN一起使用,只能写关联条件。虽然关联条件可以并到WHERE中和其他条件一起写,但分开写可读性更好
  • USING:只能和JOIN一起使用,而且要求两个关联字段在关联表中名称一致,而且只能表示关联字段值相等
#关联条件
#把关联条件写在where后面
SELECT last_name,department_name
FROM employees,departments
WHERE employees.department_id = departments.department_id;
#把关联条件写在on后面,只能和JOIN一起使用
SELECT last_name,department_name
FROM employees INNER JOIN departments
ON employees.department_id = departments.department_id;
SELECT last_name,department_name
FROM employees CROSS JOIN departments
ON employees.department_id = departments.department_id;
SELECT last_name,department_name
FROM employees JOIN departments
ON employees.department_id = departments.department_id;
#把关联字段写在using()中,只能和JOIN一起使用
#而且两个表中的关联字段必须名称相同,而且只能表示=
#查询员工姓名与基本工资
SELECT last_name,job_title
FROM employees INNER JOIN jobs USING(job_id);
#n张表关联,需要n-1个关联条件
#查询员工姓名,基本工资,部门名称
SELECT last_name,job_title,department_name FROM employees,departments,jobs
WHERE employees.department_id = departments.department_id
AND employees.job_id = jobs.job_id;
SELECT last_name,job_title,department_name
FROM employees INNER JOIN departments INNER JOIN jobs
ON employees.department_id = departments.department_id
AND employees.job_id = jobs.job_id;

注意:控制连接表的数量。多表连接就相当于嵌套for循环一样,非常消耗资源,会让SQL查询性能下降得很严重,因此不要连接不必要的表。在许多DBMS中,也都会有最大连接表的限制。

【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。

说明:即使双表 join 也要注意表索引、SQL 性能。

-----阿里巴巴《Java开发手册》

章节练习

# 第06章_多表查询的课后练习
# 1.显示所有员工的姓名,部门号和部门名称。
SELECT e.last_name, d.department_id, d.department_name
FROM employees e LEFT OUTER JOIN departments d
ON e.`department_id` = d.`department_id`;

# 2.查询90号部门员工的job_id和90号部门的location_id
SELECT e.`job_id`, d.`location_id`
FROM employees e LEFT OUTER JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE d.`department_id` = 90;

# 3.选择所有有奖金的员工的 last_name , department_name , location_id , city
SELECT e.`last_name`, d.`department_name`, l.`location_id`, l.`city`
FROM employees e LEFT OUTER JOIN departments d
ON e.`department_id` = d.`department_id`
LEFT OUTER JOIN locations l
ON d.`location_id` = l.`location_id`
WHERE e.`commission_pct` IS NOT NULL;

# 4.选择city在Toronto工作的员工的 last_name , job_id , department_id , department_name 
SELECT e.`last_name`, e.`job_id`, e.`department_id`, d.`department_name`
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`
LEFT JOIN locations l
ON d.`location_id` = l.`location_id`
WHERE l.`city` = 'Toronto';

#sql92语法:
SELECT e.`last_name`, e.`job_id`, e.`department_id`, d.`department_name`
FROM employees e, departments d, locations l
WHERE e.`department_id` = d.`department_id`
AND d.`location_id` = l.`location_id`
AND l.`city` = 'Toronto';

#5.查询员工所在的部门名称、部门地址、姓名、工作、工资,其中员工所在部门的部门名称为Executive
SELECT d.`department_name`, l.`street_address`, e.`last_name`, e.`job_id`, e.`salary`
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`
LEFT JOIN locations l
ON d.`location_id` = l.`location_id`
WHERE d.`department_name` = 'Executive';

# 6.选择指定员工的姓名,员工号,以及他的管理者的姓名和员工号,结果类似于下面的格式
/*
employees	Emp#	manager	Mgr#
kochhar		101	king	100
*/
SELECT e.`last_name` "employees", e.`employee_id` "Emp", m.`last_name` "manager", m.`employee_id` "Mgr"
FROM employees e LEFT JOIN employees m
ON e.`manager_id` = m.`employee_id`;

# 7.查询哪些部门没有员工
SELECT d.`department_id`
FROM employees e RIGHT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE e.`department_id` IS NULL;

# 8. 查询哪个城市没有部门
SELECT l.`location_id`, l.`city`
FROM locations l LEFT JOIN departments d
ON l.`location_id` = d.`location_id`
WHERE d.`location_id` IS NULL;

# 9. 查询部门名为 Sales 或 IT 的员工信息
SELECT e.`employee_id`, e.`last_name`, e.`department_id`
FROM employees e LEFT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE d.`department_name` IN ('Sales', 'IT');

7、单行函数

7.1、MySQL内置函数及分类

MySQL提供的内置函数从实现的功能角度可以分为数值函数字符串函数日期和时间函数流程控制函数加密与解密函数获取MySQL信息函数聚合函数等。大体上也可分为两类:单行函数、多行函数(分组函数)。

  • 单行函数:操作数据对象,接收参数,只对一行进行变换,每行返回一个结果,可以嵌套,参数可以是一列或一个值
  • 多行函数:作用于一组数据,并对一组数据返回一个值

7.2、数值函数

7.2.1、基本函数

函数 用法
ABS(x) 返回x的绝对值
SIGN(x) 返回x的符号。正数返回1,负数返回-1,0返回0
PI() 返回圆周率的值
CEIL(x), CEILING(x) 返回大于或等于某个值的最小整数
FLOOR(x) 返回小于或等于某个值的最大整数
LEAST(e1,e2,e3...) 返回列表中的最小值
GREATEST(e1,e2,e3...) 返回列表中的最大值
MOD(x,y) 返回x/y的余数
RAND() 返回0~1的随机值
RAND(x) 返回0~1的随机值。其中x的值会作为种子,相同的值会产生相同的随机数
ROUND(x) 返回一个对x的值进行四舍五入后,最接近于X的整数
ROUND(x,y) 返回一个对x的值进行四舍五入后最接近X的值,并保留到小数点后面Y位
TRUNCATE(x,y) 返回数字x截断为y位小数的结果
SQRT(x) 返回x的平方根。当X的值为负数时,返回NULL
#1.数值函数
#基本的操作
SELECT ABS(-123),ABS(32),SIGN(-23),SIGN(43),PI(),CEIL(32.32),CEILING(-43.23),FLOOR(32.32),
FLOOR(-43.23),MOD(12,5),12 MOD 5,12 % 5
FROM DUAL;

#取随机数
SELECT RAND(),RAND(),RAND(10),RAND(10),RAND(-1),RAND(-1)
FROM DUAL;

#四舍五入,截断操作
SELECT ROUND(123.556),ROUND(123.456,0),ROUND(123.456,1),ROUND(123.456,2),
ROUND(123.456,-1),ROUND(153.456,-2)
FROM DUAL;

SELECT TRUNCATE(123.456,0),TRUNCATE(123.496,1),TRUNCATE(129.45,-1)
FROM DUAL;

#单行函数可以嵌套
SELECT TRUNCATE(ROUND(123.456,2),0)
FROM DUAL;

7.2.2、角度与弧度函数

函数 用法
RADIANS(x) 将角度转化为弧度,其中,参数x为角度值
DEGREES(x) 将弧度转化为角度,其中,参数x为弧度值
SELECT RADIANS(30),RADIANS(45),RADIANS(60),RADIANS(90),
DEGREES(2*PI()),DEGREES(RADIANS(60))
FROM DUAL;

7.2.3、三角函数

函数 用法
SIN(x) 返回x的正弦值,其中,参数x为弧度值
ASIN(x) 返回x的反正弦值,即获取正弦为x的值。如果x的值不在-1到1之间,则返回NULL
COS(x) 返回x的余弦值,其中,参数x为弧度值
ACOS(X) 返回x的反余弦值,即获取余弦为x的值。如果x的值不在-1到1之间,则返回NULL
TAN(x) 返回x的正切值,其中,参数x为弧度值
ATAN(x) 返回x的反正切值,即返回正切值为x的值
ATAN2(m,n) 返回两个参数的反正切值
COT(x) 返回x的余切值,其中,X为弧度值

ATAN2(M,N)函数返回两个参数的反正切值。与ATAN(X)函数相比,ATAN2(M,N)需要两个参数,例如有两个点point(x1,y1)和point(x2,y2),使用ATAN(X)函数计算反正切值为ATAN((y2-y1)/(x2-x1)),使用ATAN2(M,N)计算反正切值则为ATAN2(y2-y1,x2-x1)。由使用方式可以看出,当x2-x1等于0时,ATAN(X)函数会报错,而ATAN2(M,N)函数则仍然可以计算。

SELECT SIN(RADIANS(30)),DEGREES(ASIN(1)),TAN(RADIANS(45)),DEGREES(ATAN(1))
FROM DUAL;

7.2.4、指数与对数函数

函数 用法
POW(x,y),POWER(X,Y) 返回x的y次方
EXP(X) 返回e的X次方,其中e是一个常数,2.718281828459045
LN(X),LOG(X) 返回以e为底的X的对数,当X<=0时,返回的结果为NULL
LOG10(X) 返回以10为底的X的对数,当X<=0时,返回的结果为NULL
LOG2(X) 返回以2为底的X的对数,当X<=0时,返回NULL
#指数和对数
SELECT POW(2,5),POWER(2,4),EXP(2)
FROM DUAL;

SELECT LN(EXP(2)),LOG(EXP(2)),LOG10(10),LOG2(4)
FROM DUAL;

7.2.5、进制转换函数

函数 用法
BIN(X) 返回x的二进制数
HEX(x) 返回x的八进制数
OCT(x) 返回x的十六进制数
CONV(x,f1,f2) 将f1进制数x转换成f2进制数
SELECT BIN(10),HEX(10),OCT(10),CONV(10,10,8)
FROM DUAL;

7.3、字符串函数

注意:MySQL中字符串的索引是从 1 开始

函数 A用法
ASCII(S) 返回字符串S中的第一个字符的ASCII码值
CHAR_LENGTH(S) 返回字符串s的字符数。作用与CHARACTER_LENGTH(s)相同
LENGTH(S) 返回字符串s的字节数,和字符集有关
CONCAT(s1,s2,...,sn) 连接s1,s2,......,sn为一个字符串
CONCAT_WS(x,s1,s2,...,sn) 同CONCAT(s1,s2,...)函数,但是每个字符串之间要加上x
INSERT(str,index,length,replacestr) 将字符串str从第index位置开始,length个字符长的子串替换为字符串replacestr
REPLACE(str,a,b) 用字符串b替换字符串str中所有出现的字符串a
UPPER(s)或UCASE(s) 将字符串s的所有字母转成大写字母
LOWER(s)或LCASE(s) 将字符串s的所有字母转成小写字母
LEFT(str,n) 返回字符串str最左边的n个字符
RIGHT(str,n) 返回字符串str最右边的n个字符
LPAD(str,len,pad) 用字符串pad对str最左边进行填充,直到str的长度为len个字符
RPAD(str,len,pad) 用字符串pad对str最右边进行填充,直到str的长度为len个字符
LTRIM 去掉字符串s左侧的空格
RTRIM 去掉字符串s右侧的空格
TRIM(s) 去掉字符串s开始与结尾的空格
TRIM(s1 FROM s) 去掉字符串s开始与结尾的s1
TRIM(LEADING s1 FROM s) 去掉字符串s开始处的s1
TRIM(TRAILING s1 FROM s) 去掉字符串s结尾处的s1
REPEAT(str,n) 返回str重复n次的结果
SPACE(n) 返回n个空格
STRCMP(s1,s2) 比较字符串s1,s2的ASCII码值的大小
SUBSTR(s,index,len) 返回从字符串s的index位置其len个字符,作用与SUBSTRING(s,n,len)、MID(s,n,len)相同
LOCATE(substr,str) 返回字符串substr在字符串str中首次出现的位置,作用于POSITION(substr INstr)、INSTR(str,substr)相同。未找到,返回0
ELT(m,s1,s2,…,sn) 返回指定位置的字符串,如果m=1,则返回s1,如果m=2,则返回s2,如果m=n,则返回sn
FIELD(s,s1,s2,…,sn) 返回字符串s在字符串列表中第一次出现的位置
FIND_IN_SET(s1,s2) 返回字符串s1在字符串s2中出现的位置。其中,字符串s2是一个以逗号分隔的字符串
REVERSE(s) 返回s反转后的字符串
NULLIF(value1,value2) 比较两个字符串,如果value1与value2相等,则返回NULL,否则返回value1
#2. 字符串函数
SELECT ASCII('Abcdfsf'),CHAR_LENGTH('hello'),CHAR_LENGTH('我们'),
LENGTH('hello'),LENGTH('我们')
FROM DUAL;

# xxx worked for yyy
SELECT CONCAT(emp.last_name,' worked for ',mgr.last_name) "details"
FROM employees emp JOIN employees mgr
WHERE emp.`manager_id` = mgr.employee_id;

SELECT CONCAT_WS('-','hello','world','hello','beijing')
FROM DUAL;
#字符串的索引是从1开始的!
SELECT INSERT('helloworld',2,3,'aaaaa'),REPLACE('hello','lol','mmm')
FROM DUAL;

SELECT UPPER('HelLo'),LOWER('HelLo')
FROM DUAL;

SELECT last_name,salary
FROM employees
WHERE LOWER(last_name) = 'King';

SELECT LEFT('hello',2),RIGHT('hello',3),RIGHT('hello',13)
FROM DUAL;

# LPAD:实现右对齐效果
# RPAD:实现左对齐效果
SELECT employee_id,last_name,LPAD(salary,10,' ')
FROM employees;

SELECT CONCAT('---',LTRIM('    h  el  lo   '),'***'),TRIM('oo' FROM 'ooheollo')
FROM DUAL;

SELECT REPEAT('hello',4),LENGTH(SPACE(5)),STRCMP('abc','abe')
FROM DUAL;

SELECT SUBSTR('hello',2,2),LOCATE('lll','hello')
FROM DUAL;

SELECT ELT(2,'a','b','c','d'),FIELD('mm','gg','jj','mm','dd','mm'),
FIND_IN_SET('mm','gg,mm,jj,dd,mm,gg')
FROM DUAL;

SELECT employee_id,NULLIF(LENGTH(first_name),LENGTH(last_name)) "compare"
FROM employees;

7.4、日期和时间函数

7.4.1、获取日期、时间

函数 用法
CURDATE(),CURRENT_DATE() 返回当前日期,只包含年、月、日
CURTIME(),CURRENT_TIME() 返回当前时间,只包含时、分、秒
NOW()/SYSDATE()/CURRENT_TIMESTAMP()/LOCALTIME()/LOCALTIMESTAMP() 返回当前系统日期和时间
UTC_DATE() 返回UTC(世界标准时间)日期
UTC_TIME() 返回UTC(世界标准时间)时间
#3.1  获取日期、时间
SELECT CURDATE(),CURRENT_DATE(),CURTIME(),NOW(),SYSDATE(),
UTC_DATE(),UTC_TIME()
FROM DUAL;

SELECT CURDATE(),CURDATE() + 0,CURTIME() + 0,NOW() + 0
FROM DUAL;

7.4.2、日期与时间戳的转换

函数 用法
UNIX_TIMESTAMP() 以UNIX时间戳的形式返回当前时间。SELECTUNIX_TIMESTAMP()->1634348884
UNIX_TIMESTAMP(date) 将时间date以UNIX时间戳的形式返回。
FROM_UNIXTIME(timestamp) 将UNIX时间戳的时间转换为普通格式的时间
#3.2 日期与时间戳的转换
SELECT UNIX_TIMESTAMP(),UNIX_TIMESTAMP('2021-10-01 12:12:32'),
FROM_UNIXTIME(1635173853),FROM_UNIXTIME(1633061552)
FROM DUAL;

7.4.3、获取月份、星期、星期数、天数等函数

函数 用法
YEAR(date)/MONTH(date)/DAY(date) 返回具体的日期值
HOUR(time)/MINUTE(time)/SECOND(time) 返回具体的时间值
MONTHNAME(date) 返回英语月份:January,...
DAYNAME(date) 返回星期几:MONDAY,TUESDAY.....SUNDAY
WEEKDAY(date) 返回周几,注意,周1是0,周2是1,。。。周日是6
QUARTER(date) 返回日期对应的季度,范围为1~4
WEEK(date),WEEKOFYEAR(date) 返回一年中的第几周
DAYOFYEAR(date) 返回日期是一年中的第几天
DAYOFMONTH(date) 返回日期位于所在月份的第几天
DAYOFWEEK(date) 返回周几,注意:周日是1,周一是2,。。。周六是7
#3.3 获取月份、星期、星期数、天数等函数
SELECT YEAR(CURDATE()),MONTH(CURDATE()),DAY(CURDATE()),
HOUR(CURTIME()),MINUTE(NOW()),SECOND(SYSDATE())
FROM DUAL;

SELECT MONTHNAME('2021-10-26'),DAYNAME('2021-10-26'),WEEKDAY('2021-10-26'),
QUARTER(CURDATE()),WEEK(CURDATE()),DAYOFYEAR(NOW()),
DAYOFMONTH(NOW()),DAYOFWEEK(NOW())
FROM DUAL;

7.4.4、日期的操作函数

函数 用法
EXTRACT(type FROM date) 返回指定日期中特定的部分,type指定返回的值

注意:EXTRACT(type FROM date)中type的取值和含义

EXTRACT中type的取值和含义

#3.4 日期的操作函数
SELECT EXTRACT(SECOND FROM NOW()),EXTRACT(DAY FROM NOW()),
EXTRACT(HOUR_MINUTE FROM NOW()),EXTRACT(QUARTER FROM '2021-05-12')
FROM DUAL;

7.4.5、时间和秒钟转换的函数

函数 用法
TIME_TO_SEC(time) 将time转化为秒并返回结果值。转化的公式为:小时*3600+分钟*60+秒
SEC_TO_TIME(seconds) 将seconds描述转化为包含小时、分钟和秒的时间
SELECT TIME_TO_SEC(CURTIME()), SEC_TO_TIME(83355)
FROM DUAL;

7.4.6、计算日期和时间的函数

函数 用法
DATE_ADD(DATETIME,INTERVAL expr type),ADDDATE(DATE,INTERVAL expr type) 返回与给定日期时间相差INTERVAL时间段的日期时间
DATE_SUB(DATE,INTERVAL expr type),SUBDATE(DATE,INTERVAL expr type) 返回与date相差INTERVAL时间间隔的日期

注意:以上两种函数中type的取值如下

日期操作函数中type的取值

函数 用法
ADDTIME(time1,time2) 返回time1加上time2的时间。当time2为一个数字时,代表的是,可以为负数
SUBTIME(time1,time2) 返回time1减去time2后的时间。当time2为一个数字时,代表的是,可以为负数
DATEDIFF(date1,date2) 返回date1-date2的日期间隔天数
TIMEDIFF(time1,time2) 返回time1-time2的时间间隔
FROM_DAYS(N) 返回从0000年1月1日起,N天以后的日期
TO_DAYS(date) 返回日期date距离0000年1月1日的天数
LAST_DAY(date) 返回date所在月份的最后一天的日期
MAKEDATE(year,n) 针对给定年份与所在年份中的天数返回一个日期
MAKETIME(hour,minute,second) 将给定的小时、分钟和秒组合成时间并返回
PERIOD_ADD(time,n) 返回time加上n后的时间
#3.6 计算日期和时间的函数
SELECT NOW(),DATE_ADD(NOW(),INTERVAL 1 YEAR),
DATE_ADD(NOW(),INTERVAL -1 YEAR),
DATE_SUB(NOW(),INTERVAL 1 YEAR)
FROM DUAL;

SELECT DATE_ADD(NOW(), INTERVAL 1 DAY) AS col1,DATE_ADD('2021-10-21 23:32:12',INTERVAL 1 SECOND) AS col2,
ADDDATE('2021-10-21 23:32:12',INTERVAL 1 SECOND) AS col3,
DATE_ADD('2021-10-21 23:32:12',INTERVAL '1_1' MINUTE_SECOND) AS col4,
DATE_ADD(NOW(), INTERVAL -1 YEAR) AS col5, #可以是负数
DATE_ADD(NOW(), INTERVAL '1_1' YEAR_MONTH) AS col6 #需要单引号
FROM DUAL;

SELECT ADDTIME(NOW(),20),SUBTIME(NOW(),30),SUBTIME(NOW(),'1:1:3'),DATEDIFF(NOW(),'2021-10-01'),
TIMEDIFF(NOW(),'2021-10-25 22:10:10'),FROM_DAYS(366),TO_DAYS('0000-12-25'),
LAST_DAY(NOW()),MAKEDATE(YEAR(NOW()),32),MAKETIME(10,21,23),PERIOD_ADD(20200101010101,10)
FROM DUAL;

# 案例:查询7天内的新增用户数有多少?
SELECT COUNT(*) as num
FROM new_user
WHERE TO_DAYS(NOW()) - TO_DAYS(regist_time) <= 7;

7.4.7、日期格式化与解析函数

函数 用法
DATE_FORMAT(date,fmt) 按照字符串fmt格式化日期date值
TIME_FORMAT(time,fmt) 按照字符串fmt格式化时间time值
GET_FORMAT(date_type,format_type) 返回日期字符串的显示格式
STR_TO_DATE(str,fmt) 按照字符串fmt对str进行解析,解析为一个日期

以上函数中fmt参数常用的格式符:

格式符 说明 格式符 说明
%Y 4位数字表示年份 %y 两位数字表示年份
%M 月名表示月份(January,....) %m 两位数字表示月份(01,02,03。。。)
%b 缩写的月名(Jan.,Feb.,....) %c 数字表示月份(1,2,3,...)
%D 英文后缀表示月中的天数(1st,2nd,3rd,...) %d 两位数字表示月中的天数(01,02...)
%e 数字形式表示月中的天数(1,2,3,4,5.....)
%H 两位数字表示小数,24小时制(01,02..) %h和%I 两位数字表示小时,12小时制(01,02..)
%k 数字形式的小时,24小时制(1,2,3) %l 数字形式表示小时,12小时制(1,2,3,...)
%i 两位数字表示分钟(00,01,02) %S和%s 两位数字表示秒(00,01,02...)
%W 一周中的星期名称(Sunday...) %a 一周中的星期缩写(Sun.,Mon.,Tues.,..)
%w 以数字表示周中的天数(0=Sunday,1=Monday....)
%j 以3位数字表示年中的天数(001,002...) %U 以数字表示年中的第几周,(1,2,3。。)其中Sunday为周中第一天
%u 以数字表示年中的第几周,(1,2,3。。)其中Monday为周中第一天
%T 24小时制 %r 12小时制
%p AM或PM %% 表示%

GET_FORMAT函数中date_type和format_type参数取值如下:

GET_FORMAT函数的参数取值

#3.7 日期的格式化与解析
# 格式化:日期 ---> 字符串
# 解析:  字符串 ----> 日期
#此时我们谈的是日期的显式格式化和解析
#之前,我们接触过隐式的格式化或解析
SELECT *
FROM employees
WHERE hire_date = '1993-01-13';

#格式化:
SELECT DATE_FORMAT(CURDATE(),'%Y-%M-%D'),
DATE_FORMAT(NOW(),'%Y-%m-%d'),TIME_FORMAT(CURTIME(),'%h:%i:%S'),
DATE_FORMAT(NOW(),'%Y-%M-%D %h:%i:%S %W %w %T %r')
FROM DUAL;

#解析:格式化的逆过程
SELECT STR_TO_DATE('2021-October-25th 11:37:30 Monday 1','%Y-%M-%D %h:%i:%S %W %w')
FROM DUAL;

SELECT GET_FORMAT(DATE,'USA')
FROM DUAL;

SELECT DATE_FORMAT(CURDATE(),GET_FORMAT(DATE,'USA'))
FROM DUAL;

7.5、流程控制函数

函数 用法
IF(value,value1,value2) 如果value的值为TRUE,返回value1,否则返回value2
IFNULL(value1,value2) 如果value1不为NULL,返回value1,否则返回value2
CASE WHEN 条件1 THEN 结果1 WHEN 条件2 THEN 结果2 ....[ELSEresultn] END 相当于Java的if...elseif...else...
CASE expr WHEN 常量值1 THEN 值1 WHEN 常量值1 THEN 值1....[ELSE值n] END 相当于Java的switch...case...
#4.流程控制函数
#4.1 IF(VALUE,VALUE1,VALUE2)
 SELECT
  last_name,
  salary,
  IF(
    salary >= 6000,
    '高工资',
    '低工资'
  ) "details"
FROM
  employees;

SELECT last_name,commission_pct,IF(commission_pct IS NOT NULL,commission_pct,0) "details",
salary * 12 * (1 + IF(commission_pct IS NOT NULL,commission_pct,0)) "annual_sal"
FROM employees;

#4.2 IFNULL(VALUE1,VALUE2):看做是IF(VALUE,VALUE1,VALUE2)的特殊情况
SELECT last_name,commission_pct,IFNULL(commission_pct,0) "details"
FROM employees;

#4.3 CASE WHEN ... THEN ...WHEN ... THEN ... ELSE ... END
# 类似于java的if ... else if ... else if ... else
SELECT last_name,salary,CASE WHEN salary >= 15000 THEN '白骨精' 
			     WHEN salary >= 10000 THEN '潜力股'
			     WHEN salary >= 8000 THEN '小屌丝'
			     ELSE '草根' END "details",department_id
FROM employees;

SELECT last_name,salary,CASE WHEN salary >= 15000 THEN '白骨精' 
			     WHEN salary >= 10000 THEN '潜力股'
			     WHEN salary >= 8000 THEN '小屌丝'
			     END "details"
FROM employees;

#4.4 CASE ... WHEN ... THEN ... WHEN ... THEN ... ELSE ... END
# 类似于java的switch ... case...
/*
练习1
查询部门号为 10,20, 30 的员工信息,
若部门号为 10, 则打印其工资的 1.1 倍,
20 号部门, 则打印其工资的 1.2 倍,
30 号部门,打印其工资的 1.3 倍数,
其他部门,打印其工资的 1.4 倍数
*/
SELECT employee_id,last_name,department_id,salary,CASE department_id WHEN 10 THEN salary * 1.1
								     WHEN 20 THEN salary * 1.2
								     WHEN 30 THEN salary * 1.3
								     ELSE salary * 1.4 END "details"
FROM employees;
/*
练习2
查询部门号为 10,20, 30 的员工信息, 
若部门号为 10, 则打印其工资的 1.1 倍, 
20 号部门, 则打印其工资的 1.2 倍, 
30 号部门打印其工资的 1.3 倍
*/
SELECT employee_id,last_name,department_id,salary,CASE department_id WHEN 10 THEN salary * 1.1
								     WHEN 20 THEN salary * 1.2
								     WHEN 30 THEN salary * 1.3
								     END "details"
FROM employees
WHERE department_id IN (10,20,30);

7.6、加密与解密函数

加密与解密函数主要用于对数据库中的数据进行加密和解密处理,以防止数据被他人窃取。这些函数在保证数据库安全时非常有用。

函数 用法
PASSWORD(str) 返回字符串str的加密版本,41位长的字符串。加密结果不可逆,常用于用户的密码加密
MD5(str) 返回字符串str的md5加密后的值,也是一种加密方式。若参数为NULL,则会返回NULL
SHA(str) 从原明文密码str计算并返回加密后的密码字符串,当参数为NULL时,返回NULL。SHA加密算法比MD5更加安全
ENCODE(value,password_seed) 返回使用password_seed作为加密密码加密value
DECODE(value,password_seed) 返回使用password_seed作为加密密码解密value

可以看到,ENCODE(value,password_seed)函数与DECODE(value,password_seed)函数互为反函数。

#5. 加密与解密的函数
# PASSWORD()在mysql8.0中弃用。
SELECT MD5('mysql'),SHA('mysql'),MD5(MD5('mysql'))
FROM DUAL;

#ENCODE()\DECODE() 在mysql8.0中弃用。
/*
SELECT ENCODE('atguigu','mysql'),DECODE(ENCODE('atguigu','mysql'),'mysql')
FROM DUAL;
*/

7.7、MySQL信息函数

MySQL中内置了一些可以查询MySQL信息的函数,这些函数主要用于帮助数据库开发或运维人员更好地对数据库进行维护工作。

函数 用法
VERSION() 获取当前MySQL的版本号
CONNECTION_ID() 返回当前MySQL服务器的连接数
DATABASE(),SCHEMA() 返回MySQL命令行当前所在的数据库
USER(),CURRENT_USER()、SYSTEM_USER(),SESSION_USER() 返回当前连接MySQL的用户名,返回结果格式为“主机名@用户名”
CHARSET(value) 返回字符串value自变量的字符集
COLLATION(value) 返回字符串value的比较规则
#6. MySQL信息函数
SELECT VERSION(),CONNECTION_ID(),DATABASE(),SCHEMA(),
USER(),CURRENT_USER(),CHARSET('尚硅谷'),COLLATION('尚硅谷')
FROM DUAL;

7.8、其他函数

函数 用法
FORMAT(value,n) 返回对数字value进行格式化后的结果数据。n表示四舍五入后保留小数点后n位
CONV(value,from,to) 将value的值进行不同进制之间的转换
INET_ATON(ipvalue) 将以点分隔的IP地址转化为一个数字
INET_NTOA(value) 将数字形式的IP地址转化为以点分隔的IP地址
BENCHMARK(n,expr) 将表达式expr重复执行n次。用于测试MySQL处理expr表达式所耗费的时间
CONVERT(VALUE USING char_code) 将value所使用的字符编码修改为char_code
#7. 其他函数
#如果n的值小于或者等于0,则只保留整数部分
SELECT FORMAT(123.125,2),FORMAT(123.125,0),FORMAT(123.125,-2)
FROM DUAL;

SELECT CONV(16, 10, 2), CONV(8888,10,16), CONV(NULL, 10, 2)
FROM DUAL;
#以“192.168.1.100”为例,计算方式为192乘以2563次方,加上168乘以2562次方,加上1乘以256,再加上100SELECT INET_ATON('192.168.1.100'),INET_NTOA(3232235876)
FROM DUAL;

#BENCHMARK()用于测试表达式的执行效率
SELECT BENCHMARK(100000,MD5('mysql'))
FROM DUAL;
# CONVERT():可以实现字符集的转换
SELECT CHARSET('atguigu'),CHARSET(CONVERT('atguigu' USING 'gbk'))
FROM DUAL;

章节练习

#第07章_单行函数的课后练习
# 1.显示系统时间(注:日期+时间)
 SELECT
  NOW(),
  SYSDATE(),
  CURRENT_TIMESTAMP(),
  LOCALTIME(),
  LOCALTIMESTAMP() #只需要掌握一个函数就可以了
 FROM
  DUAL;

# 2.查询员工号,姓名,工资,以及工资提高百分之20%后的结果(new salary)
SELECT employee_id,last_name,salary,salary * 1.2 "new salary"
FROM employees;

# 3.将员工的姓名按首字母排序,并写出姓名的长度(length)
SELECT last_name,LENGTH(last_name) "name_length"
FROM employees
#order by last_name asc;
ORDER BY name_length ASC;

# 4.查询员工id,last_name,salary,并作为一个列输出,别名为OUT_PUT
SELECT CONCAT(employee_id,',',last_name,',',salary) "OUT_PUT"
FROM employees;

# 5.查询公司各员工工作的年数、工作的天数,并按工作年数的降序排序
SELECT
  employee_id,
  DATEDIFF(CURDATE(), hire_date) / 365 "worked_years",
  DATEDIFF(CURDATE(), hire_date) "worked_days",
  TO_DAYS(CURDATE()) - TO_DAYS(hire_date) "worked_days1"
FROM
  employees
ORDER BY worked_years DESC;

# 6.查询员工姓名,hire_date , department_id,满足以下条件:
#雇用时间在1997年之后,department_id 为8090110, commission_pct不为空
SELECT last_name,hire_date,department_id
FROM employees
WHERE department_id IN (80,90,110)
AND commission_pct IS NOT NULL
#and hire_date >= '1997-01-01';  #存在着隐式转换
#and  date_format(hire_date,'%Y-%m-%d') >= '1997-01-01';  # 显式转换操作,格式化:日期---> 字符串
#and  date_format(hire_date,'%Y') >= '1997';   # 显式转换操作,格式化
AND hire_date >= STR_TO_DATE('1997-01-01','%Y-%m-%d');# 显式转换操作,解析:字符串 ----> 日期

# 7.查询公司中入职超过10000天的员工姓名、入职时间
SELECT last_name,hire_date
FROM employees
WHERE DATEDIFF(CURDATE(),hire_date) >= 10000;

# 8.做一个查询,产生下面的结果
#<last_name> earns <salary> monthly but wants <salary*3> 

SELECT
  CONCAT(
    last_name,
    ' earns ',
    TRUNCATE(salary, 0),
    ' monthly but wants ',
    TRUNCATE(salary * 3, 0)
  ) "Dream Salary"
FROM
  employees;

# 9.使用case-when,按照下面的条件:
/*job                  grade
AD_PRES              	A
ST_MAN               	B
IT_PROG              	C
SA_REP               	D
ST_CLERK             	E
产生下面的结果:
*/
SELECT last_name "Last_name",job_id "Job_id",CASE job_id WHEN 'AD_PRES' THEN 'A'
							 WHEN 'ST_MAN' THEN 'B'
							 WHEN 'IT_PROG' THEN 'C'
							 WHEN 'SA_REP' THEN 'D'
							 WHEN 'ST_CLERK' THEN 'E'
							 END "Grade"
FROM employees;

SELECT last_name "Last_name",job_id "Job_id",CASE job_id WHEN 'AD_PRES' THEN 'A'
							 WHEN 'ST_MAN' THEN 'B'
							 WHEN 'IT_PROG' THEN 'C'
							 WHEN 'SA_REP' THEN 'D'
							 WHEN 'ST_CLERK' THEN 'E'
							 ELSE "undefined" END "Grade"
FROM employees;

8、聚合函数

聚合函数(也称分组函数、聚集函数),它是对一组数据进行汇总的函数,输入的是一组数据的集合,输出一个值。

常见的聚合函数:AVG()SUM()MAX()MIN()COUNT()

8.1、聚合函数的介绍

8.1.1、AVG()和SUM()

AVG()和SUM()只适用于数值类型字段。

  • AVG():对给出的列取平均值
  • SUM():求和函数
#1.1 AVG / SUM :只适用于数值类型的字段(或变量)
SELECT AVG(salary),SUM(salary),AVG(salary) * 107
FROM employees;
#如下的操作没有意义
SELECT SUM(last_name),AVG(last_name),SUM(hire_date)
FROM employees;

8.1.2、MAX()和MIN()

MAX()和MIN()适用于任意类型的字段。

  • MAX():求最大值。对于字符串类型,返回字典序最大的
  • MIN():求最小值。对于字符串类型,返回字典序最小的
#1.2 MAX / MIN :适用于数值类型、字符串类型、日期时间类型的字段(或变量)
SELECT MAX(salary),MIN(salary)
FROM employees;

SELECT MAX(last_name),MIN(last_name),MAX(hire_date),MIN(hire_date)
FROM employees;

8.1.3、COUNT()

COUNT()用于统计记录条数。COUNT()使用时要注意:

  • COUNT(*)返回表中记录总数,适用于任意数据类型
  • COUNT(expr)返回expr不为空的记录总数。如COUNT(1)
  • COUNT(column)返回字段column不为空的记录总数。如:COUNT(department_id)

问题:COUNT(*)、COUNT(1)、COUNT(column)哪个好?

答:对于MyISAM引擎的表是没有区别的。这种引擎内部有一计数器在维护着行数Innodb引擎的表用count(*),count(1)直接读行数复杂度是O(n),因为innodb真的要去数一遍。但好于具体的count(列名),不建议使用COUNT(列名)。

问题:能不能使用count(列名)替换count(*)?

答:不要使用count(列名)来替代count(*),count(*) 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

说明:count(*)统计值为 NULL 的行,而count(列名)不会统计此列为 NULL 值的行

公式:AVG() = SUM() / COUNT(*)

注意:使用COUNT(column)时,需要根据需求对包含NULL值的行进行特殊处理

#1.3 COUNT:
# ① 作用:计算指定字段在查询结构中出现的个数(不包含NULL值的)
SELECT COUNT(employee_id),COUNT(salary),COUNT(2 * salary),COUNT(1),COUNT(2),COUNT(*)
FROM employees ;

#如果计算表中有多少条记录,如何实现?
#方式1COUNT(*)
#方式2COUNT(1)
#方式3COUNT(具体字段) : 不一定对!字段可能包含NULL

#② 注意:计算指定字段出现的个数时,是不计算NULL值的。
SELECT COUNT(commission_pct)
FROM employees;

SELECT commission_pct
FROM employees
WHERE commission_pct IS NOT NULL;

#③ 公式:AVG = SUM / COUNT
SELECT AVG(salary),SUM(salary)/COUNT(salary),
AVG(commission_pct),SUM(commission_pct)/COUNT(commission_pct),
SUM(commission_pct) / 107
FROM employees;

#需求:查询公司中平均奖金率
#错误的!
SELECT AVG(commission_pct)
FROM employees;

#正确的:
SELECT SUM(commission_pct) / COUNT(IFNULL(commission_pct,0)),
AVG(IFNULL(commission_pct,0))
FROM employees;

# 如何需要统计表中的记录数,使用COUNT(*)、COUNT(1)、COUNT(具体字段) 哪个效率更高呢?
# 如果使用的是MyISAM 存储引擎,则三者效率相同,都是O(1)
# 如果使用的是InnoDB 存储引擎,则三者效率:COUNT(*) = COUNT(1)> COUNT(字段)

8.2、GROUP BY子句

GROUP BY子句可以对数据根据不同字段进行分组。

#2. GROUP BY 的使用
#需求:查询各个部门的平均工资,最高工资
SELECT department_id,AVG(salary),SUM(salary)
FROM employees
GROUP BY department_id

#需求:查询各个job_id的平均工资
SELECT job_id,AVG(salary)
FROM employees
GROUP BY job_id;

#需求:查询各个department_id,job_id的平均工资
#方式1SELECT department_id,job_id,AVG(salary)
FROM employees
GROUP BY  department_id,job_id;
#方式2SELECT job_id,department_id,AVG(salary)
FROM employees
GROUP BY job_id,department_id;

注意:

  1. WHERE子句一定紧跟FROM子句后。

  2. 在SELECT列表中所有未包含在组函数中的列都应该包含在GROUP BY子句中

    解释:对一个列分组则返回一行记录,当SELECT列表中未包含在组函数中的列数多于GROUP BY子句中的列数时,会出现分组返回一行记录中的列值不确定性

  3. 包含在GROUP BY子句中的列不必包含在SELECT列表中。

  4. 在对多个列进行分组时,在第一个列值相同时会根据第二个列进行分组,第二个列值相同时根据第三个列值分组,依此类推。。。

  5. GROUP BY 声明在FROM后面、WHERE后面,ORDER BY 前面、LIMIT前面。

  6. MySQL中GROUP BY中可以使用WITH ROLLUP关键字,在所有查询出的分组记录之后增加一条记录,该记录计算查询出的所有记录的总和,即统计记录数量,相当于AVG()的作用。

  7. 当使用ROLLUP时,不能同时使用ORDER BY子句进行结果排序,即ROLLUP和ORDER BY是互相排斥的。

#结论1SELECT中出现的非组函数的字段必须声明在GROUP BY 中。
#      反之,GROUP BY中声明的字段可以不出现在SELECT中。
#结论2GROUP BY 声明在FROM后面、WHERE后面,ORDER BY 前面、LIMIT前面
#结论3:MySQL中GROUP BY中使用WITH ROLLUP

SELECT department_id,AVG(salary)
FROM employees
GROUP BY department_id WITH ROLLUP;

#需求:查询各个部门的平均工资,按照平均工资升序排列
SELECT department_id,AVG(salary) avg_sal
FROM employees
GROUP BY department_id
ORDER BY avg_sal ASC;

#说明:当使用ROLLUP时,不能同时使用ORDER BY子句进行结果排序,即ROLLUPORDER BY是互相排斥的。
#错误的:
SELECT department_id,AVG(salary) avg_sal
FROM employees
GROUP BY department_id WITH ROLLUP
ORDER BY avg_sal ASC;

8.3、HAVING子句

语法格式:

SELECT column1,column2,group_function
FROM 表名
WHERE 普通字段的过滤条件
GROUP BY SELECT中出现的所有未包含在聚合函数中的字段
HAVING group_condition;

HAVING的使用要求

  1. 如果过滤条件中使用了聚合函数,则必须使用HAVING来替换WHERE。否则,报错。
  2. HAVING 必须声明在 GROUP BY 的后面。
  3. 开发中,我们使用HAVING的前提是SQL中使用了GROUP BY。

结论:当过滤条件中有聚合函数时,则此过滤条件必须声明在HAVING中。

当过滤条件中没有聚合函数时,则此过滤条件声明在WHERE中或HAVING中都可以。但是,建议大家声明在WHERE中。

#3. HAVING的使用 (作用:用来过滤数据的)
#练习:查询各个部门中最高工资比10000高的部门信息
#错误的写法:
SELECT department_id,MAX(salary)
FROM employees
WHERE MAX(salary) > 10000
GROUP BY department_id;

#正确的写法:
SELECT department_id,MAX(salary)
FROM employees
GROUP BY department_id
HAVING MAX(salary) > 10000;

#练习:查询部门id为10,20,30,404个部门中最高工资比10000高的部门信息
#方式1:推荐,执行效率高于方式2.
SELECT department_id,MAX(salary)
FROM employees
WHERE department_id IN (10,20,30,40)
GROUP BY department_id
HAVING MAX(salary) > 10000;

#方式2SELECT department_id,MAX(salary)
FROM employees
GROUP BY department_id
HAVING MAX(salary) > 10000 AND department_id IN (10,20,30,40);

WHERE 子句与 HAVING 子句的对比

区别1:WHERE 可以直接使用表中的字段作为筛选条件,但不能使用分组中的计算函数作为筛选条件;HAVING 必须要与 GROUP BY配合使用,可以把分组计算的函数和分组字段作为筛选条件。

这决定了,在需要对数据进行分组统计的时候,HAVING 可以完成 WHERE 不能完成的任务。这是因为,在查询语法结构中,WHERE 在 GROUP BY 之前,所以无法对分组结果进行筛选。HAVING 在 GROUP BY 之后,可以使用分组字段和分组中的计算函数,对分组的结果集进行筛选,这个功能是 WHERE 无法完成的。另外,WHERE排除的记录不再包括在分组中

区别2:如果需要通过连接从关联表中获取需要的数据,WHERE是先筛选后连接,而HAVING是先连接后筛选。这一点,就决定了在关联查询中,WHERE 比 HAVING 更高效。因为 WHERE 可以先筛选,用一个筛选后的较小数据集和关联表进行连接,这样占用的资源比较少,执行效率也比较高。HAVING 则需要先把结果集准备好,也就是用未被筛选的数据集进行关联,然后对这个大的数据集进行筛选,这样占用的资源就比较多,执行效率也较低。

优点 缺点
WHERE 先筛选数据再关联,执行效率高 不能使用分组中的计算函数进行筛选
HAVING 可以使用分组中的计算函数 最后的结果集中进行筛选,执行效率较低

开发建议

WHERE 和 HAVING 也不是互相排斥的,我们可以在一个查询里面同时使用 WHERE 和 HAVING。包含分组统计函数的条件用 HAVING,普通条件用 WHERE。这样,我们就既利用了 WHERE 条件的高效快速,又发挥了 HAVING 可以使用包含分组统计函数的查询条件的优点。当数据量特别大的时候,运行效率会有很大的差别。

8.4、SQL底层执行原理(重要)

SQL92语法

SELECT ...,...,...(存在聚合函数)
FROM ...,...,...
WHERE 多表的连接条件 AND 不包含聚合函数的过滤条件
GROUP BY ...,...
HAVING 包含聚合函数的过滤条件
ORDER BY ...,...(ASC / DESC)
LIMIT ...,....

SQL99语法

SELECT ...,...,...(存在聚合函数)
FROM ... (LEFT / RIGHT)JOIN ...ON 多表的连接条件1
(LEFT / RIGHT)JOIN ... ON 多表的连接条件2
WHERE 不包含聚合函数的过滤条件
GROUP BY ...,...
HAVING 包含聚合函数的过滤条件
ORDER BY ...,...(ASC / DESC )
LIMIT ...,...

SQL语句的执行过程(⭐⭐⭐⭐⭐⭐)

  1. FROM子句
  2. ON子句(多表连接条件筛选,第一次筛选)
  3. (LEFT/RIGHT) JOIN(多表连接)
  4. WHERE子句(不包含分组函数的过滤条件,从连接表中筛选,第二次筛选)
  5. GROUP BY子句(分组依据)
  6. HAVING子句(含有分组函数的过滤条件,从统计结果中再次筛选,第三次筛选)
  7. SELECT子句
  8. DISTINCT(在SELECT子句中对列值去重)
  9. ORDER BY子句(排序)
  10. LIMIT子句(分页)

说明:

  1. 关键字顺序
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT...
  1. SELECT语句执行顺序:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT

示例:

SELECT DISTINCT player_id, player_name, count(*) as num # 顺序 5
FROM player JOIN team ON player.team_id = team.team_id # 顺序 1
WHERE height > 1.80 # 顺序 2
GROUP BY player.team_id # 顺序 3
HAVING num > 2 # 顺序 4
ORDER BY num DESC # 顺序 6
LIMIT 2 # 顺序 7

说明:在 SELECT 语句执行这些步骤的时候,每个步骤都会产生一个 虚拟表 ,然后将这个虚拟表传入下一个步骤中作为输入。需要注意的是,这些步骤隐含在 SQL 的执行过程中,对于我们来说是不可见的。

SQL执行原理(⭐⭐⭐⭐⭐)

SELECT查询 是先执行 FROM 这一步的。在这个阶段,如果是多张表联查,还会经历下面的几个步骤:

  1. 首先先通过 CROSS JOIN 求笛卡尔积,相当于得到虚拟表 vt(virtual table)1-1;

  2. 通过 ON 进行筛选,在虚拟表 vt1-1 的基础上进行筛选,得到虚拟表 vt1-2;

  3. 添加外部行。如果我们使用的是左连接、右链接或者全连接,就会涉及到外部行,也就是在虚拟表 vt1-2 的基础上增加外部行,得到虚拟表 vt1-3。

当然如果我们操作的是两张以上的表,还会重复上面的步骤,直到所有表都被处理完为止。这个过程得到是我们的原始数据。

当我们拿到了查询数据表的原始数据,也就是最终的虚拟表 vt1 ,就可以在此基础上再进行 WHERE 阶段 。在这个阶段中,会根据 vt1 表的结果进行筛选过滤,得到虚拟表 vt2 。

然后进入第三步和第四步,也就是 GROUP 和 HAVING 阶段 。在这个阶段中,实际上是在虚拟表 vt2 的基础上进行分组和分组过滤,得到中间的虚拟表 vt3 和 vt4 。

当我们完成了条件筛选部分之后,就可以筛选表中提取的字段,也就是进入到 SELECT 和 DISTINCT阶段。

首先在 SELECT 阶段会提取想要的字段,然后在 DISTINCT 阶段过滤掉重复的行,分别得到中间的虚拟表vt5-1 和 vt5-2 。

当我们提取了想要的字段数据之后,就可以按照指定的字段进行排序,也就是 ORDER BY 阶段 ,得到虚拟表 vt6 。

最后在 vt6 的基础上,取出指定行的记录,也就是 LIMIT 阶段 ,得到最终的结果,对应的是虚拟表vt7 。

当然我们在写 SELECT 语句的时候,不一定存在所有的关键字,相应的阶段就会省略。

同时因为 SQL 是一门类似英语的结构化查询语言,所以在写 SELECT 语句的时候,还要注意相应的关键字顺序,所谓底层运行的原理,就是执行顺序。

章节练习

# 第08章_聚合函数的课后练习
#1.where子句可否使用组函数进行过滤?  No!
#2.查询公司员工工资的最大值,最小值,平均值,总和
SELECT MAX(salary),MIN(salary),AVG(salary),SUM(salary)
FROM employees;

#3.查询各job_id的员工工资的最大值,最小值,平均值,总和
SELECT job_id,MAX(salary),MIN(salary),AVG(salary),SUM(salary)
FROM employees
GROUP BY job_id;

#4.选择具有各个job_id的员工人数
SELECT job_id,COUNT(*)
FROM employees
GROUP BY job_id;

# 5.查询员工最高工资和最低工资的差距(DIFFERENCE)  #DATEDIFF
SELECT MAX(salary) - MIN(salary) "DIFFERENCE"
FROM employees;

# 6.查询各个管理者手下员工的最低工资,其中最低工资不能低于6000,没有管理者的员工不计算在内
SELECT manager_id,MIN(salary)
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id
HAVING MIN(salary) >= 6000;

# 7.查询所有部门的名字,location_id,员工数量和平均工资,并按平均工资降序 
SELECT d.`department_name`,d.`location_id`,COUNT(e.`employee_id`),AVG(e.`salary`) "avg_salary"
FROM employees e RIGHT JOIN departments d
USING (department_id)
GROUP BY d.`department_name`,d.`location_id`
ORDER BY avg_salary DESC;

# 8.查询每个部门的部门名、工种名和最低工资
SELECT d.department_name,e.job_id,MIN(salary)
FROM departments d LEFT JOIN employees e
ON d.`department_id` = e.`department_id`
GROUP BY department_name,job_id;

9、子查询

子查询指一个查询语句嵌套在另一个查询语句内部的查询,这个特性从MySQL 4.1开始引入。

SQL中子查询的使用大大增强了 SELECT 查询的能力,因为很多时候查询需要从结果集中获取数据,或者需要从同一个表中先计算得出一个数据结果,然后与这个数据结果(可能是某个标量,也可能是某个集合)进行比较。

9.1、需求分析和问题解决

问题:谁的工资比xxx高?<- xxx的工资是多少?

解决方案:

# 方案1:分步解决
SELECT salary
FROM employees
WHERE last_name = 'Abel';
SELECT last_name,salary
FROM employees
WHERE salary > 11000;
# 方案2:自连接
SELECT e2.last_name,e2.salary
FROM employees e1,employees e2
WHERE e1.last_name = 'Abel'
AND e1.`salary` < e2.`salary`;
# 方案3:子查询
SELECT last_name,salary
FROM employees
WHERE salary >
  (SELECT salary
  FROM employees
  WHERE last_name = 'Abel');

9.1.1、子查询的基本使用

语法结构:

SELECT select_list
FROM table
WHERE expr operator
				(SELECT select_list
                 FROM table);

说明:

  1. 子查询(内查询)在主查询(外查询)之前执行完成。
  2. 子查询的结果被主查询(外查询)使用。

注意:

  1. 子查询要包含在括号内。
  2. 将子查询放在比较条件右侧(避免头重脚轻)。
  3. 单行操作符对应单行子查询(单行子查询的结果返回一条记录),多行操作符对应多行子查询(多行子查询的结果返回多条记录)。

9.1.2、子查询的分类

  • 根据返回结果:
    • 单行子查询:子查询的结果返回一条记录
    • 多行子查询:子查询的结果返回多条记录
  • 根据子查询是否被执行多次:
    • 关联(相关)子查询:如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为相关子查询。
    • 非关联(不相关)子查询:子查询从数据表中查询了数据结果,如果这个数据结果只执行一次,然后这个数据结果作为主查询的条件进行执行,那么这样的子查询叫做不相关子查询。

9.2、单行子查询

单行操作符: =!=>>=<<=

#题目:查询工资大于149号员工工资的员工的信息
SELECT employee_id,last_name,salary
FROM employees
WHERE salary > (
		SELECT salary
		FROM employees
		WHERE employee_id = 149
		);

#题目:返回job_id与141号员工相同,salary比143号员工多的员工姓名,job_id和工资
SELECT last_name,job_id,salary
FROM employees
WHERE job_id = (
		SELECT job_id
		FROM employees
		WHERE employee_id = 141
		)
AND salary > (
		SELECT salary
		FROM employees
		WHERE employee_id = 143
		);

#题目:返回公司工资最少的员工的last_name,job_id和salary
SELECT last_name,job_id,salary
FROM employees
WHERE salary = (
		SELECT MIN(salary)
		FROM employees
		);

#题目:查询与141号员工的manager_id和department_id相同的其他员工的employee_id,manager_id,department_id。
#方式1SELECT employee_id,manager_id,department_id
FROM employees
WHERE manager_id = (
		    SELECT manager_id
		    FROM employees
		    WHERE employee_id = 141
		   )
AND department_id = (
		    SELECT department_id
		    FROM employees
		    WHERE employee_id = 141
		   )
AND employee_id <> 141;

#方式2:了解
SELECT employee_id,manager_id,department_id
FROM employees
WHERE (manager_id,department_id) = (
				    SELECT manager_id,department_id
			            FROM employees
				    WHERE employee_id = 141
				   )
AND employee_id <> 141;

#题目:查询最低工资大于110号部门最低工资的部门id和其最低工资
SELECT department_id,MIN(salary)
FROM employees
WHERE department_id IS NOT NULL
GROUP BY department_id
HAVING MIN(salary) > (
			SELECT MIN(salary)
			FROM employees
			WHERE department_id = 110
		     );

#题目:显示员工的employee_id,last_name和location。
#其中,若员工department_id与location_id为1800的department_id相同,
#则location为’Canada’,其余则为’USA’。
SELECT employee_id,last_name,CASE department_id WHEN (SELECT department_id FROM departments WHERE location_id = 1800) THEN 'Canada'
						ELSE 'USA' END "location"
FROM employees;

#4.2 子查询中的空值问题
SELECT last_name, job_id
FROM   employees
WHERE  job_id =
                (SELECT job_id
                 FROM   employees
                 WHERE  last_name = 'Haas'); #子查询没有返回行
                 
#4.3 非法使用子查询
#错误:Subquery returns more than 1 row
SELECT employee_id, last_name
FROM   employees
WHERE  salary =
                (SELECT   MIN(salary)
                 FROM     employees
                 GROUP BY department_id); # 单行操作符= 与返回多行的子查询进行比较

9.3、多行子查询

多行子查询(也成为集合比较子查询)的操作符:INANYALLSOME(同ANY)

操作符 含义
IN 等于列表中的任意一个
ANY 需要和单行比较操作符一起使用,和子查询返回的某一个值比较
ALL 需要和单行比较操作符一起使用,和子查询返回的所有值比较
SOME 实际上是ANY的别名,作用相同,一般常使用ANY
# IN:
SELECT employee_id, last_name
FROM   employees
WHERE  salary IN
                (SELECT   MIN(salary)
                 FROM     employees
                 GROUP BY department_id); 
                 
# ANY / ALL:
#题目:返回其它job_id中比job_id为‘IT_PROG’部门任一工资低的员工的员工号、姓名、job_id 以及salary
SELECT employee_id,last_name,job_id,salary
FROM employees
WHERE job_id <> 'IT_PROG'
AND salary < ANY (
		SELECT salary
		FROM employees
		WHERE job_id = 'IT_PROG'
		);

#题目:返回其它job_id中比job_id为‘IT_PROG’部门所有工资低的员工的员工号、姓名、job_id 以及salary
SELECT employee_id,last_name,job_id,salary
FROM employees
WHERE job_id <> 'IT_PROG'
AND salary < ALL (
		SELECT salary
		FROM employees
		WHERE job_id = 'IT_PROG'
		);
		
#题目:查询平均工资最低的部门id
#MySQL中聚合函数是不能嵌套使用的。
#方式1SELECT department_id
FROM employees
GROUP BY department_id
HAVING AVG(salary) = (
			SELECT MIN(avg_sal)
			FROM(
				SELECT AVG(salary) avg_sal
				FROM employees
				GROUP BY department_id
				) t_dept_avg_sal
			);
#方式2SELECT department_id
FROM employees
GROUP BY department_id
HAVING AVG(salary) <= ALL(	
			SELECT AVG(salary) avg_sal
			FROM employees
			GROUP BY department_id
			) 
#5.3 空值问题
SELECT last_name
FROM employees
WHERE employee_id NOT IN (
			SELECT manager_id
			FROM employees
			);

9.4、相关子查询

如果子查询的执行依赖于外部查询,通常情况下都是因为子查询中的表用到了外部的表,并进行了条件关联,因此每执行一次外部查询,子查询都要重新计算一次,这样的子查询就称之为关联子查询(也称相关子查询) 。相关子查询按照一行接一行的顺序执行,主查询的每一行都执行一次子查询。

相关子查询的语法结构:

相关子查询

FROM型的子查询:子查询是作为from的一张表,子查询要用()引起来,并且要给这个子查询取别名, 把它当成一张“临时的虚拟的表”来使用。

9.4.1、不同位置的相关子查询

#回顾:查询员工中工资大于公司平均工资的员工的last_name,salary和其department_id
SELECT last_name,salary,department_id
FROM employees
WHERE salary > (
		SELECT AVG(salary)
		FROM employees
		);

#题目:查询员工中工资大于本部门平均工资的员工的last_name,salary和其department_id
#方式1:使用相关子查询
SELECT last_name,salary,department_id
FROM employees e1
WHERE salary > (
                SELECT AVG(salary)
                FROM employees e2
                WHERE department_id = e1.`department_id`
				);

#方式2:在FROM中声明子查询
SELECT e.last_name,e.salary,e.department_id
FROM employees e,(
		SELECT department_id,AVG(salary) avg_sal #如果主查询需要用到子查询中的聚合函数的结果,则必须给子查询中的聚合函数取别名
		FROM employees
		GROUP BY department_id) t_dept_avg_sal
WHERE e.department_id = t_dept_avg_sal.department_id
AND e.salary > t_dept_avg_sal.avg_sal;

#题目:查询员工的id,salary,按照department_name 排序
SELECT employee_id,salary
FROM employees e
ORDER BY (
	 SELECT department_name
	 FROM departments d
	 WHERE e.`department_id` = d.`department_id`
	) ASC;

#题目:若employees表中employee_id与job_history表中employee_id相同的数目不小于2,
#输出这些相同id的员工的employee_id,last_name和其job_id
SELECT employee_id,last_name,job_id
FROM employees e
WHERE 2 <= (
	    SELECT COUNT(*)
	    FROM job_history j
	    WHERE e.`employee_id` = j.`employee_id`
		);

结论:在SELECT中,除了GROUP BY 和 LIMIT之外,其他位置都可以声明子查询!

9.4.2、EXISTS和NOT EXISTS

关联子查询通常也会和 EXISTS操作符一起来使用,用来检查在子查询中是否存在满足条件的行。

  • 如果在子查询中不存在满足条件的行:

    • 条件返回 FALSE

    • 继续在子查询中查找

  • 如果在子查询中存在满足条件的行:

    • 不在子查询中继续查找

    • 条件返回 TRUE

  • NOT EXISTS关键字表示如果不存在某种条件,则返回TRUE,否则返回FALSE。

# EXISTSNOT EXISTS关键字
#题目:查询公司管理者的employee_id,last_name,job_id,department_id信息
#方式1:自连接
SELECT DISTINCT mgr.employee_id,mgr.last_name,mgr.job_id,mgr.department_id
FROM employees emp JOIN employees mgr
ON emp.manager_id = mgr.employee_id;

#方式2:子查询
SELECT employee_id,last_name,job_id,department_id
FROM employees
WHERE employee_id IN (
			SELECT DISTINCT manager_id
			FROM employees
			);

#方式3:使用EXISTS
SELECT employee_id,last_name,job_id,department_id
FROM employees e1
WHERE EXISTS (
	       SELECT *
	       FROM employees e2
	       WHERE e1.`employee_id` = e2.`manager_id`
	     );

#题目:查询departments表中,不存在于employees表中的部门的department_id和department_name
#方式1SELECT d.department_id,d.department_name
FROM employees e RIGHT JOIN departments d
ON e.`department_id` = d.`department_id`
WHERE e.`department_id` IS NULL;

#方式2SELECT department_id,department_name
FROM departments d
WHERE NOT EXISTS (
		SELECT *
		FROM employees e
		WHERE d.`department_id` = e.`department_id`
		);

9.4.3、相关更新

语法结构:

UPDATE table1 alias1
SET column = (SELECT expression
FROM table2 alias2
WHERE alias1.column = alias2.column);

使用相关子查询依据一个表中的数据更新另一个表的数据。

# 案例:在employees中增加一个department_name字段,数据为员工对应的部门名称
# 1ALTER TABLE employees
ADD(department_name VARCHAR2(14));
# 2UPDATE employees e
SET department_name = (SELECT department_name
FROM departments d
WHERE e.department_id = d.department_id);

9.4.4、相关删除

语法结构:

DELETE FROM table1 alias1
WHERE column operator (
    					SELECT expression
						FROM table2 alias2
						WHERE alias1.column = alias2.column
					  );

使用相关子查询依据一个表中的数据删除另一个表的数据。

# 案例:删除表employees中,其与emp_history表皆有的数据
DELETE FROM employees e
WHERE employee_id in (
    					SELECT employee_id
                        FROM emp_history
                        WHERE employee_id = e.employee_id
					 );

自连接和子查询的区别

自连接方式好!

题目中可以使用子查询,也可以使用自连接。一般情况建议你使用自连接,因为在许多 DBMS 的处理过程中,对于自连接的处理速度要比子查询快得多。

可以这样理解:子查询实际上是通过未知表进行查询后的条件判断,而自连接是通过已知的自身数据表进行条件判断,因此在大部分 DBMS 中都对自连接处理进行了优化。

章节练习

# 第09章_子查询的课后练习
#1.查询和Zlotkey相同部门的员工姓名和工资
SELECT last_name,salary
FROM employees
WHERE department_id = (
			SELECT department_id
			FROM employees
			WHERE last_name = 'Zlotkey'
		      );

#2.查询工资比公司平均工资高的员工的员工号,姓名和工资。
SELECT employee_id,last_name,salary
FROM employees
WHERE salary > (
		SELECT AVG(salary)
		FROM employees
		);

#3.选择工资大于所有JOB_ID = 'SA_MAN'的员工的工资的员工的last_name, job_id, salary
SELECT last_name,job_id,salary
FROM employees
WHERE salary > ALL(
		   SELECT salary
		   FROM employees
		   WHERE job_id = 'SA_MAN'
		  );

#4.查询和姓名中包含字母u的员工在相同部门的员工的员工号和姓名
SELECT employee_id,last_name
FROM employees
WHERE department_id IN (
			SELECT department_id
			FROM employees
			WHERE last_name LIKE '%u%'
		       );

#5.查询在部门的location_id为1700的部门工作的员工的员工号
SELECT employee_id
FROM employees
WHERE department_id IN (
			SELECT department_id
			FROM departments
			WHERE location_id = 1700
		       );

#6.查询管理者是King的员工姓名和工资
SELECT last_name,salary
FROM employees
WHERE manager_id IN (
		     SELECT employee_id
		     FROM employees
		     WHERE last_name = 'King'
		    );

#7.查询工资最低的员工信息: last_name, salary
SELECT last_name,salary
FROM employees
WHERE salary = (
		 SELECT MIN(salary)
		 FROM employees
	       )

#8.查询平均工资最低的部门信息
#方式1SELECT *
FROM departments
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING AVG(salary) = (
						SELECT MIN(avg_sal)
						FROM (
							SELECT AVG(salary) avg_sal
							FROM employees
							GROUP BY department_id
						     ) t_dept_avg_sal
					     )
		      );
#方式2SELECT *
FROM departments
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING AVG(salary) <= ALL(
						  SELECT AVG(salary)
						  FROM employees
						  GROUP BY department_id
					         )
		      );

#方式3: LIMIT
SELECT *
FROM departments
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING AVG(salary) = (
						SELECT AVG(salary) avg_sal
						FROM employees
						GROUP BY department_id
						ORDER BY avg_sal ASC
						LIMIT 1 OFFSET 0
					     )
		      );

#方式4SELECT *
FROM departments d,(
		     SELECT department_id,AVG(salary) avg_sal
		     FROM employees
		     GROUP BY department_id
		     ORDER BY avg_sal ASC
		     LIMIT 1 OFFSET 0
		   ) t_dept_avg_sal
WHERE d.`department_id` = t_dept_avg_sal.department_id;
		
#9.查询平均工资最低的部门信息和该部门的平均工资(相关子查询)
#方式1SELECT d.*,(SELECT AVG(salary) FROM employees WHERE department_id = d.`department_id`) avg_sal
FROM departments d
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING AVG(salary) = (
					      SELECT MIN(avg_sal)
					      FROM (
						     SELECT AVG(salary) avg_sal
						     FROM employees
						     GROUP BY department_id
						   ) t_dept_avg_sal
					     )
		      );

#方式2SELECT d.*,(SELECT AVG(salary) FROM employees WHERE department_id = d.`department_id`) avg_sal
FROM departments d
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING AVG(salary) <= ALL(
						    SELECT AVG(salary)
						    FROM employees
						    GROUP BY department_id
						 )
		      );

#方式3: LIMIT
SELECT d.*,(SELECT AVG(salary) FROM employees WHERE department_id = d.`department_id`) avg_sal
FROM departments d
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING AVG(salary) = (
						SELECT AVG(salary) avg_sal
						FROM employees
						GROUP BY department_id
						ORDER BY avg_sal
						LIMIT 1 OFFSET 0
					     )
		      );

#方式4SELECT d.*,(SELECT AVG(salary) FROM employees WHERE department_id = d.`department_id`) avg_sal
FROM departments d,(
		     SELECT department_id,AVG(salary) avg_sal
		     FROM employees
		     GROUP BY department_id
		     ORDER BY avg_sal ASC
		     LIMIT 1 OFFSET 0
		   ) t_dept_avg_sal
WHERE d.`department_id` = t_dept_avg_sal.department_id;

#10.查询平均工资最高的 job 信息
#方式1SELECT *
FROM jobs
WHERE job_id = (
		SELECT job_id
		FROM employees
		GROUP BY job_id
		HAVING AVG(salary) = (
					SELECT MAX(avg_sal)
					FROM (
						SELECT AVG(salary) avg_sal
						FROM employees
						GROUP BY job_id
					     ) t_job_avg_sal
				     )
		);

#方式2SELECT *
FROM jobs
WHERE job_id = (
		SELECT job_id
		FROM employees
		GROUP BY job_id
		HAVING AVG(salary) >= ALL(
				     SELECT AVG(salary) 
				     FROM employees
				     GROUP BY job_id
				     )
		);

#方式3SELECT *
FROM jobs
WHERE job_id = (
		SELECT job_id
		FROM employees
		GROUP BY job_id
		HAVING AVG(salary) =(
				     SELECT AVG(salary) avg_sal
				     FROM employees
				     GROUP BY job_id
				     ORDER BY avg_sal DESC
				     LIMIT 0,1
				     )
		);

#方式4SELECT j.*
FROM jobs j,(
		SELECT job_id,AVG(salary) avg_sal
		FROM employees
		GROUP BY job_id
		ORDER BY avg_sal DESC
		LIMIT 1 OFFSET 0
	    ) t_job_avg_sal
WHERE j.`job_id` = t_job_avg_sal.job_id;

#11.查询平均工资高于公司平均工资的部门有哪些?
SELECT department_id
FROM employees
WHERE department_id IS NOT NULL
GROUP BY department_id
HAVING AVG(salary) > (
			SELECT AVG(salary)
			FROM employees
		     );

#12.查询出公司中所有 manager 的详细信息
#方式1:自连接  xxx worked for yyy
SELECT DISTINCT m.`employee_id`,m.`last_name`,m.`job_id`,m.`department_id`
FROM employees e JOIN employees m
WHERE e.`manager_id` = m.`employee_id`;

#方式2:子查询
SELECT employee_id,last_name,job_id,department_id
FROM employees
WHERE employee_id IN (
			SELECT DISTINCT manager_id
			FROM employees
			);

#方式3:使用EXISTS
SELECT employee_id,last_name,job_id,department_id
FROM employees e1
WHERE EXISTS (
		SELECT *
		FROM employees e2
		WHERE e1.`employee_id` = e2.`manager_id`
	     );

#13.各个部门中 最高工资中最低的那个部门的 最低工资是多少?
#方式1SELECT MIN(salary)
FROM employees
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING MAX(salary) = (
						SELECT MIN(max_sal)
						FROM (
							SELECT MAX(salary) max_sal
							FROM employees
							GROUP BY department_id
						     ) t_dept_max_sal
					     )
			);

#方式2SELECT MIN(salary)
FROM employees
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING MAX(salary) <= ALL (
						    SELECT MAX(salary)
						    FROM employees
						    GROUP BY department_id
						  )
		      );

#方式3SELECT MIN(salary)
FROM employees
WHERE department_id = (
			SELECT department_id
			FROM employees
			GROUP BY department_id
			HAVING MAX(salary) = (
						SELECT MAX(salary) max_sal
						FROM employees
						GROUP BY department_id
						ORDER BY max_sal ASC
						LIMIT 0,1
						)
			);

#方式4SELECT MIN(salary)
FROM employees e,(
		  SELECT department_id,MAX(salary)
		  FROM employees
		  GROUP BY department_id
		  ORDER BY MAX(salary) ASC
		  LIMIT 1 OFFSET 0
		 ) t_dept_max_sal
WHERE e.`department_id` = t_dept_max_sal.department_id;

#14.查询平均工资最高的部门的 manager 的详细信息: last_name, department_id, email, salary
#方式1SELECT last_name,department_id,email,salary
FROM employees
WHERE employee_id = ANY (
			SELECT DISTINCT manager_id
			FROM employees
			WHERE department_id = (
						SELECT department_id
						FROM employees
						GROUP BY department_id
						HAVING AVG(salary) = (
									SELECT MAX(avg_sal)
									FROM (
										SELECT AVG(salary) avg_sal
										FROM employees
										GROUP BY department_id
									     ) t_dept_avg_sal
								     )
					      )
		    );

#方式2:多行子查询
SELECT last_name,department_id,email,salary
FROM employees
WHERE employee_id = ANY (
			SELECT DISTINCT manager_id
			FROM employees
			WHERE department_id = (
						SELECT department_id
						FROM employees
						GROUP BY department_id
						HAVING AVG(salary) >= ALL (
									    SELECT AVG(salary) avg_sal
									    FROM employees
									    GROUP BY department_id
									  )
					      )
			);

#方式3:相关子查询
SELECT last_name,department_id,email,salary
FROM employees
WHERE employee_id IN (
			SELECT DISTINCT manager_id
			FROM employees e,(
					SELECT department_id,AVG(salary) avg_sal
					FROM employees
					GROUP BY department_id
					ORDER BY avg_sal DESC
					LIMIT 0,1
					) t_dept_avg_sal
			WHERE e.`department_id` = t_dept_avg_sal.department_id
		     );

#15. 查询部门的部门号,其中不包括job_id是"ST_CLERK"的部门号
#方式1SELECT department_id
FROM departments
WHERE department_id NOT IN (
			SELECT DISTINCT department_id
			FROM employees
			WHERE job_id = 'ST_CLERK'
			);

#方式2SELECT department_id
FROM departments d
WHERE NOT EXISTS (
		   SELECT *
		   FROM employees e
		   WHERE d.`department_id` = e.`department_id`
		   AND e.`job_id` = 'ST_CLERK'
		 );

#16. 选择所有没有管理者的员工的last_name
SELECT e.last_name
FROM employees e
WHERE NOT EXISTS (
			   SELECT DISTINCT employee_id
			   FROM employees m
			   WHERE e.`manager_id` = m.`employee_id`
			 );

#17.查询员工号、姓名、雇用时间、工资,其中员工的管理者为 'De Haan'
#方式1:多行子查询
SELECT employee_id,last_name,hire_date,salary
FROM employees
WHERE manager_id IN (
		     SELECT employee_id
		     FROM employees
		     WHERE last_name = 'De Haan'
		   )

#方式2:相关子查询
SELECT employee_id,last_name,hire_date,salary
FROM employees e
WHERE EXISTS (
		SELECT *
		FROM employees m
		WHERE e.manager_id = m.`employee_id`
		AND m.`last_name` = 'De Haan'
	     )

#18.查询各部门中工资比本部门平均工资高的员工的员工号, 姓名和工资(相关子查询)
#方式1:使用相关子查询
SELECT employee_id,last_name,salary
FROM employees e
WHERE salary > (
		 SELECT AVG(salary)
		 FROM employees e1
		 WHERE e.department_id = e1.department_id
		);

#方式2:在FROM中声明子查询
SELECT employee_id,last_name,salary
FROM employees e,(
		   SELECT department_id,AVG(salary) avg_sal
		   FROM employees e1
		   GROUP BY department_id
		 ) t_dept_avg_sal
WHERE e.salary > t_dept_avg_sal.avg_sal
AND e.department_id = t_dept_avg_sal.department_id;

#19.查询每个部门下的部门人数大于 5 的部门名称(相关子查询)
# 方式1:在FROM中声明子查询
SELECT department_name
FROM departments d,(
		   SELECT department_id,COUNT(*) count_sum
		   FROM employees
		   GROUP BY department_id
		 ) t_dept_count_sum
WHERE d.`department_id` = t_dept_count_sum.department_id
AND t_dept_count_sum.count_sum > 5;

# 方式2:相关子查询
SELECT department_name
FROM departments d
WHERE 5 < (
	   SELECT COUNT(*)
	   FROM employees e
	   WHERE d.department_id = e.`department_id`
	  );

#20.查询每个国家下的部门个数大于 2 的国家编号(相关子查询)
SELECT country_id
FROM locations l
WHERE 2 < (
	    SELECT COUNT(*)
	    FROM departments d
	    WHERE l.`location_id` = d.`location_id`
	  );

总结:

子查询的编写技巧(或步骤):1、从里往外写 2、从外往里写

  1. 如果子查询相对较简单,建议从外往里写。一旦子查询结构较复杂,则建议从里往外写
  2. 如果是相关子查询的话,通常都是从外往里写。

10、创建和管理表

10.1、基础知识

10.1.1、一条数据的存储过程

存储数据是处理数据的第一步 。只有正确地把数据存储起来,才能进行有效的处理和分析。否则,只能是一团乱麻,无从下手。
在 MySQL 中,一个完整的数据存储过程总共有 4 步:创建数据库 -> 确认字段 -> 创建数据表 -> 插入数据

系统架构的层次上看,MySQL数据库系统从大到小依次是:数据库服务器 > 数据库 > 数据表 > 数据表的行与列。

10.1.2、标识符的命名规则

  • 数据库名、表名不得超过30个字符变量名限制为29个
  • 必须只能包含 A–Z, a–z, 0–9, _共63个字符
  • 数据库名、表名、字段名等对象名中间不要包含空格
  • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名
  • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使用``(着重号)`引起来
  • 保持字段名和类型的一致性:在命名字段并为其指定数据类型的时候一定要保证一致性,假如数据类型在一个表里是整数,那在另一个表里也必须是整数类型

10.1.3、MySQL中的数据类型

类型 类型举例
整数类型 TINYINT、SMALLINT、MEDIUMINT、INT(或INTEGER)、BIGINT
浮点类型 FLOAT、DOUBLE
定点数类型 DECIMAL
位类型 BIT
日期时间类型 YEAR、TIME、DATE、DATETIME、TIMESTAMP
文本字符串类型 CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT
枚举类型 ENUM
集合类型 SET
二进制字符串类型 BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB、LONGBLOB
JSON类型 JSON对象、JSON数组
空间数据类型 单值:GEOMETRY、POINT、LINESTRING、POLYGON;集合:MULTIPOINT、MULTILINESTRING、MULTIPOLYGON、GEOMETRYCOLLECTION

其中,常用的几类类型介绍如下:

数据类型 描述
INT 从$-2{31}$到$2-1$的整型数据。存储大小为 4个字节
CHAR(size) 定长字符数据。若未指定,默认为1个字符,最大长度255
VARCHAR(size) 可变长字符数据,根据字符串实际长度保存,必须指定长度
FLOAT(M,D) 单精度,占用4个字节,M=整数位+小数位,D=小数位。 D<=M<=255,0<=D<=30,默认M+D<=6
DOUBLE(M,D) 双精度,占用8个字节,D<=M<=255,0<=D<=30,默认M+D<=15
DECIMAL(M,D) 高精度小数,占用M+2个字节,D<=M<=65,0<=D<=30,最大取值范围与DOUBLE相同。
DATE 日期型数据,格式'YYYY-MM-DD'
BLOB 二进制形式的长文本数据,最大可达4G
TEXT 长文本数据,最大可达4G

10.2、创建和管理数据库

创建和管理数据库的前提必须具备创建和管理数据库的权限,root用户权限最高

10.2.1、创建数据库

  • 方式1:创建数据库

    CREATE DATABASE 数据库名;
    
  • 方式2:创建数据库并指定字符集

    CREATE DATABASE 数据库名 CHARACTER SET 字符集;
    
  • 方式3:判断要创建的数据库是否存在,不存在则创建数据库(推荐),并建议显式指定字符集

    CREATE DATABASE IF NOT EXISTS 数据库名;
    CREATE DATABASE IF NOT EXISTS 数据库名 CHARACTER SET 字符集; # 建议使用此种方式
    # 原因:虽然MySQL8.0中字符集默认是utf8,但是开发中大部分都是MySQL5.7的版本,默认字符集latin,所以建议创建数据库时指定字符集utf8
    

    注意:如果MySQL中已经存在相关的数据库,则忽略创建语句,不再创建数据库

注意:DATABASE 不能改名。一些可视化工具可以改名,它是建新库,把所有表复制到新库,再删

旧库完成的。

10.2.2、使用数据库

  • 查看当前所有的数据库

    SHOW DATABASES;
    
  • 查看当前正在使用的数据库

    SHOW DATABASE();
    
  • 查看指定库下所有的表

    SHOW TABLES FROM 数据库名;
    
  • 查看数据库的创建信息

    SHOW CREATE DATABASE 数据库名;
    # 或
    SHOW CREATE DATABASE 数据库名\G(\g) #在dos窗口中使用
    
  • 使用/切换数据库

    USE 数据库名;
    

注意:要操作表格和数据之前必须先说明是对哪个数据库进行操作,否则就要对所有对象加上“数据库名.”。

10.2.3、修改数据库

  • 更改数据库字符集

    ALTER DATABASE 数据库名 CHARACTER SET 字符集; #比如:gbk、utf8等
    

10.2.4、删除数据库

  • 方式1:删除指定的数据库

    DROP DATABASE 数据库名;
    
  • 方式2:删除指定的数据库(推荐)

    DROP DATABASE IF EXISTS 数据库名;
    

友情提示:慎用删库操作!!!!!!

10.3、创建表

10.3.1、创建方式1

  • 必须具备条件:

    • CREATE TABLE权限
    • 存储空间
  • 语法格式:

    CREATE TABLE [IF NOT EXISTS] 表名(
    字段1, 数据类型 [约束条件] [默认值],
    字段2, 数据类型 [约束条件] [默认值],
    字段3, 数据类型 [约束条件] [默认值],
    ……
    [表约束条件]
    );
    

    加上了IF NOT EXISTS关键字,则表示:如果当前数据库中不存在要创建的数据表,则创建数据表;

    如果当前数据库中已经存在要创建的数据表,则忽略建表语句,不再创建数据表

  • 必须指定:

    • 表名
    • 列名(或字段名),数据类型,长度
  • 可选指定:

    • 约束条件
    • 默认值
    # 案例1CREATE TABLE emp (
    -- int类型
    emp_id INT,
    -- 最多保存20个中英文字符
    emp_name VARCHAR(20),
    -- 总位数不超过15位
    salary DOUBLE,
    -- 日期类型
    birthday DATE
    );
    DESC emp;
    
    # 案例2CREATE TABLE dept(
    -- int类型,自增
    deptno INT(2) AUTO_INCREMENT,
    dname VARCHAR(14),
    loc VARCHAR(13),
    -- 主键
    PRIMARY KEY (deptno)
    );
    DESCRIBE dept;
    
    #方式1:"白手起家"的方式
    CREATE TABLE IF NOT EXISTS myemp1(   #需要用户具备创建表的权限。
    id INT,
    emp_name VARCHAR(15), #使用VARCHAR来定义字符串,必须在使用VARCHAR时指明其长度。
    hire_date DATE
    );
    #查看表结构
    DESC myemp1;
    #查看创建表的语句结构
    SHOW CREATE TABLE myemp1; #如果创建表时没有指明使用的字符集,则默认使用表所在的数据库的字符集。
    #查看表数据
    SELECT * FROM myemp1;
    

    注意:MySQL在执行建表语句时,将id字段的类型设置为int(11),这里的11实际上是int类型指定的显示宽度,默认的显示宽度为11。也可以在创建数据表的时候指定数据的显示宽度。

    在MySQL 8.x版本中,不再推荐为INT类型指定显示长度,并在未来的版本中可能去掉这样的语法。

    如果创建表时没有指明使用的字符集,则默认使用表所在的数据库的字符集。

10.3.2、创建方式2

  • 使用 AS subquery 选项,将创建表和插入数据结合起来

  • 指定的列和子查询中的列要一一对应

  • 通过列名和默认值定义列

  • 语法格式:

    CREATE TABLE 表名
    AS
    一个SQL查询;
    

    注意:这种方式创建表会同时导入数据(相当于复制表),如果不想导入原表的数据,可以在WHERE子句中写永远为假的条件

    CREATE TABLE emp1 AS SELECT * FROM employees;
    CREATE TABLE emp2 AS SELECT * FROM employees WHERE 1=2; -- 创建的emp2是空表
    
    #方式2:基于现有的表,同时导入数据
    CREATE TABLE myemp2
    AS
    SELECT employee_id,last_name,salary
    FROM employees;
    
    DESC myemp2;
    DESC employees;
    
    SELECT * FROM myemp2;
    #说明1:查询语句中字段的别名,可以作为新创建的表的字段的名称。
    #说明2:此时的查询语句可以结构比较丰富,使用前面章节讲过的各种SELECT
    CREATE TABLE myemp3
    AS
    SELECT e.employee_id emp_id,e.last_name lname,d.department_name
    FROM employees e JOIN departments d
    ON e.department_id = d.department_id;
    DESC myemp3;
    

10.3.3、查看数据表结构

在MySQL中创建好数据表之后,可以查看数据表的结构。MySQL支持使用 DESCRIBE/DESC 语句查看数据表结构,也支持使用 SHOW CREATE TABLE 语句查看数据表结构。

语法格式:

DESCRIBE 表名;
DESC 表名;
SHOW CREATE TABLE 表名;

使用SHOW CREATE TABLE语句不仅可以查看表创建时的详细语句,还可以查看存储引擎和字符编码。

10.4、修改表

修改表指的是修改数据库中已经存在的数据表的结构。

使用ALTER TABLE可以实现:

  • 向已有的表中添加列
  • 修改现有表中的列
  • 删除现有表中的列
  • 重命名现有表中的列

10.4.1、添加列

语法格式:

ALTER TABLE 表名
ADD [COLUMN] 字段名 字段类型 [FIRST|AFTER 字段名];

说明:FIRST表示将字段添加在表的第一个字段的位置,AFTER 字段名表示将字段添加在指定字段之后

# 3.1 添加一个字段
ALTER TABLE myemp1
ADD salary DOUBLE(10,2); #默认添加到表中的最后一个字段的位置

ALTER TABLE myemp1
ADD phone_number VARCHAR(20) FIRST;

ALTER TABLE myemp1
ADD email VARCHAR(45) AFTER emp_name;

10.4.2、修改列

  • 可以修改列的数据类型,长度、默认值和位置
  • 修改字段数据类型、长度、默认值、位置的语法格式如下:
ALTER TABLE 表名
MODIFY [COLUMN] 字段名1 字段类型 [DEFAULT 默认值] [FIRST|AFTER 字段名2];
  • 对默认值的修改只影响今后对表的修改
  • 此外,还可以通过此种方式修改列的约束(见第13章-约束)。
# 3.2 修改一个字段:数据类型、长度、默认值(略)
ALTER TABLE myemp1
MODIFY emp_name VARCHAR(25);

ALTER TABLE myemp1
MODIFY emp_name VARCHAR(35) DEFAULT 'aaa';

10.4.3、重命名列

使用 CHANGE old_column new_column dataType子句重命名列。语法格式如下:

ALTER TABLE 表名
CHANGE [column] 列名 新列名 新数据类型;
# 3.3 重命名一个字段
ALTER TABLE myemp1
CHANGE salary monthly_salary DOUBLE(10,2);

ALTER TABLE myemp1
CHANGE email my_email VARCHAR(50);

10.4.4、删除列

删除表中某个字段的语法格式如下:

ALTER TABLE 表名
DROP [COLUMN] 字段名;
# 3.4 删除一个字段
ALTER TABLE myemp1
DROP COLUMN my_email;

10.5、重命名表

  • 方式1:使用RENAME关键字
RENAME TABLE 原表名
TO 新表名;
  • 方式2:使用ALTER RENAME
ALTER table 原表名
RENAME [TO] 新表名; -- [TO]可以省略
#方式1:
RENAME TABLE myemp1
TO myemp11;

DESC myemp11;

#方式2ALTER TABLE myemp2
RENAME TO myemp12;

DESC myemp12;

10.6、删除表

  • 在MySQL中,当一张数据表 没有与其他任何数据表形成关联关系 时,可以将当前数据表直接删除。
  • 数据和结构都被删除
  • 所有正在运行的相关事务被提交
  • 所有相关索引被删除
  • 释放表存储空间
  • 无法回滚
  • 语法格式:
DROP TABLE [IF EXISTS] 数据表1 [, 数据表2, …, 数据表n];

IF EXISTS 的含义为:如果当前数据库中存在相应的数据表,则删除数据表;如果当前数据库中不存在相应的数据表,则忽略删除语句,不再执行删除数据表的操作

DROP TABLE IF EXISTS myemp2;
DROP TABLE IF EXISTS myemp12;

10.7、清空表

清空表,表示清空表中的所有数据,但是表结构保留

  • TRUNCATE TABLE 表名语句:

    • 删除表中所有的数据
    • 释放表的存储空间
    • 数据不可以回滚
  • DELETE FROM 表名语句:

    • 删除表中所有的数据(不带WHERE子句),加上WHERE子句可以删除部分表数据
    • 数据可以回滚

对比 TRUNCATE TABLEDELETE FROM

相同点:都可以实现对表中所有数据的删除,同时保留表结构。

不同点:

TRUNCATE TABLE:一旦执行此操作,表数据全部清除。同时,数据是不可以回滚的。

DELETE FROM:一旦执行此操作,表数据可以全部清除(不带WHERE)。同时,数据是可以实现回滚的。

对比:DCL 中 COMMIT 和 ROLLBACK

COMMIT:提交数据。一旦执行COMMIT,则数据就被永久的保存在了数据库中,意味着执行COMMIT后,数据不可以回滚

ROLLBACK:回滚数据。一旦执行ROLLBACK,则可以实现数据的回滚。回滚到最近的一次COMMIT之后

DDL 和 DML 的说明

① DDL的操作一旦执行,就不可回滚。指令SET autocommit = FALSE对DDL操作失效。(因为在执行完DD操作之后,一定会执行一次COMMIT。而此COMMIT操作不受SET autocommit = FALSE影响的。)

② DML的操作默认情况,一旦执行,也是不可回滚的。但是,如果在执行DML之前,执行了SET autocommit = FALSE,则执行的DML操作就可以实现回滚

#清空表,表示清空表中的所有数据,但是表结构保留。
TRUNCATE TABLE employees_copy;
DELETE FROM emp2;

# 演示:DELETE FROM 
#1)
COMMIT;
#2)
SELECT *
FROM myemp3;
#3)
SET autocommit = FALSE;
#4)
DELETE FROM myemp3;
#5)
SELECT *
FROM myemp3;
#6)
ROLLBACK;
#7)
SELECT *
FROM myemp3;

# 演示:TRUNCATE TABLE
#1)
COMMIT;
#2)
SELECT *
FROM myemp3;
#3)
SET autocommit = FALSE;
#4)
TRUNCATE TABLE myemp3;
#5)
SELECT *
FROM myemp3;
#6)
ROLLBACK;
#7)
SELECT *
FROM myemp3;

阿里开发规范:

【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE 无事务且不触发 TRIGGER,有可能造成事故,故不建议在开发代码中使用此语句。

说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

10.8、内容拓展

拓展1:阿里巴巴《Java开发手册》

之MySQL字段命名

【 强制 】表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

正例:aliyun_admin,rdc_config,level3_name
反例:AliyunAdmin,rdcConfig,level_3_name

【 强制 】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。

【 强制 】表必备三字段:id, gmt_create, gmt_modified。

说明:其中 id 必为主键,类型为BIGINT UNSIGNED、单表时自增、步长为 1。gmt_create, gmt_modified 的类型均为 DATETIME 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新

【 推荐 】表的命名最好是遵循 “业务名称_表的作用”。

正例:alipay_task 、 force_project、 trade_config

【 推荐 】库名与应用名称尽量一致。

【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

正例:无符号值可以避免误存负数,且扩大了表示范围。

拓展2:清空、删除操作需谨慎

表删除 操作将把表的定义和表中的数据一起删除,并且MySQL在执行删除操作时,不会有任何的确认信息提示,因此执行删除操时应当慎重。在删除表前,最好对表中的数据进行 备份 ,这样当操作失误时可以对数据进行恢复,以免造成无法挽回的后果。同样的,在使用 ALTER TABLE 进行表的基本修改操作时,在执行操作过程之前,也应该确保对数据进行完整的备份 ,因为数据库的改变是 无法撤销 的,如果添加了一个不需要的字段,可以将其删除;相同的,如果删除了一个需要的列,该列下面的所有数据都将会丢失。DDL的操作无法回滚,DCL操作可以回滚

拓展3:MySQL8新特性DDL的原子化

在MySQL 8.0版本中,InnoDB表的DDL支持事务完整性,即 DDL操作要么成功要么回滚 。DDL操作回滚日志写入到data dictionary数据字典表mysql.innodb_ddl_log(该表是隐藏的表,通过show tables无法看到)中,用于回滚操作。通过设置参数,可将DDL操作日志打印输出到MySQL错误日志中。分别在MySQL 5.7版本和MySQL 8.0版本中创建数据库和数据表:

#9.测试MySQL8.0的新特性:DDL的原子化
CREATE DATABASE mytest;
USE mytest;

CREATE TABLE book1(
book_id INT,
book_name VARCHAR(255)
);

SHOW TABLES;
# 分别在MySQL5.7和MySQL8.0中执行以下操作
DROP TABLE book1,book2;

SHOW TABLES;
# MySQL5.7中显示为空
# MySQL8.0中显示有book1,因为MySQL8.0的DDL原子化,当DDL操作没有成功时就回滚

章节练习

#练习1:
#1. 创建数据库test01_office,指明字符集为utf8。并在此数据库下执行下述操作
CREATE DATABASE IF NOT EXISTS test01_office CHARACTER SET 'utf8';
USE test01_office;

#2.	创建表dept01
/*
字段      类型
id	 INT(7)
NAME	 VARCHAR(25)
*/
CREATE TABLE IF NOT EXISTS dept01(
id INT(7),
`name` VARCHAR(25)
);

#3.将表departments中的数据插入新表dept02中
CREATE TABLE dept02  
AS
SELECT *
FROM atguigudb.departments;

#4.	创建表emp01
/*
字段            类型
id		INT(7)
first_name	VARCHAR (25)
last_name	VARCHAR(25)
dept_id		INT(7)
*/

CREATE TABLE emp01(
id INT(7),
first_name VARCHAR(25),
last_name VARCHAR(25),
dept_id INT(7)
);

#5.将列last_name的长度增加到50
DESC emp01;

ALTER TABLE emp01
MODIFY last_name VARCHAR(50);

#6.根据表employees创建emp02
CREATE TABLE emp02
AS
SELECT *
FROM atguigudb.`employees`;

SHOW TABLES FROM test01_office;

#7.删除表emp01
DROP TABLE emp01;

DROP TABLE IF EXISTS emp01;

#8.将表emp02重命名为emp01
#alter table emp02 rename to emp01;
RENAME TABLE emp02 TO emp01;

#9.在表dept02和emp01中添加新列test_column,并检查所作的操作
ALTER TABLE emp01
ADD test_column INT;

ALTER TABLE dept02
ADD test_column INT FIRST;

DESC dept02;
DESC emp01;

#10.直接删除表emp01中的列 department_id
ALTER TABLE emp01
DROP COLUMN department_id;

DESC emp01;
#练习2:
# 1、创建数据库 test02_market
CREATE DATABASE IF NOT EXISTS test02_market CHARACTER SET 'utf8';

SHOW CREATE DATABASE test02_market;

# 2、创建数据表 customers
USE test02_market;
CREATE TABLE IF NOT EXISTS customers(
c_num INT,
c_name VARCHAR(50),
c_contact VARCHAR(50),
c_city VARCHAR(50),
c_birth DATE
);

SHOW TABLES;

# 3、将 c_contact 字段移动到 c_birth 字段后面
DESC customers;

ALTER TABLE customers
MODIFY c_contact VARCHAR(50) AFTER c_birth;


# 4、将 c_name 字段数据类型改为 varchar(70)
ALTER TABLE customers
MODIFY c_name VARCHAR(70);

# 5、将c_contact字段改名为c_phone
ALTER TABLE customers
CHANGE c_phonel c_phone VARCHAR(50);

DESC customers;

# 6、增加c_gender字段到c_name后面,数据类型为char(1)
ALTER TABLE customers
ADD c_gender CHAR(1) AFTER c_name;

DESC customers;

# 7、将表名改为customers_info
RENAME TABLE customers
TO customers_info;

DESC customers_info
# 8、删除字段c_city
ALTER TABLE customers_info
DROP c_city;

#练习3:
# 1、创建数据库test03_company
CREATE DATABASE IF NOT EXISTS test03_company CHARACTER SET 'utf8';

SHOW CREATE DATABASE test03_company;

USE test03_company;
# 2、创建表offices
CREATE TABLE IF NOT EXISTS offices(
officeCode INT,
city VARCHAR(30),
address VARCHAR(50),
country VARCHAR(50),
postalCode VARCHAR(25)
);

DESC offices;

# 3、创建表employees
CREATE TABLE IF NOT EXISTS employees(
empNum INT,
lastName VARCHAR(50),
firstName VARCHAR(50),
mobile VARCHAR(25),
`code` INT,
jobTitle VARCHAR(50),
birth DATE,
note VARCHAR(255),
sex VARCHAR(5)
);

DESC employees;

# 4、将表employees的mobile字段修改到code字段后面
ALTER TABLE employees
MODIFY mobile VARCHAR(25) AFTER `code`;

# 5、将表employees的birth字段改名为birthday
ALTER TABLE employees
CHANGE birth birthday DATE;

# 6、修改sex字段,数据类型为char(1)
ALTER TABLE employees
MODIFY sex CHAR(1);

# 7、删除字段note
ALTER TABLE employees
DROP COLUMN note;

# 8、增加字段名favoriate_activity,数据类型为varchar(100)
ALTER TABLE employees
ADD favoriate_activity VARCHAR(100);

# 9、将表employees的名称修改为 employees_info
RENAME TABLE employees
TO employees_info;

11、数据处理之增删改

11.1、插入数据

使用INSERT语句向表中添加数据,添加数据有两种方式:

  • 方式1:VALUES的方式添加
  • 方式2:将查询的结果插入到表中

11.1.1、VALUES方式添加

使用这种方式一次只能向表中插入一条数据。

第一种情况:为表的所有字段按默认顺序插入数据

语法格式:

INSERT INTO 表名
VALUES (value1,value2,....);

注意:值列表中需要为表的每一个字段指定值,并且值的顺序必须和数据表中字段定义时的顺序相同

#0. 储备工作
USE atguigudb;
CREATE TABLE IF NOT EXISTS emp1(
id INT,
`name` VARCHAR(15),
hire_date DATE,
salary DOUBLE(10,2)
);

DESC emp1;
SELECT * FROM emp1;
#1. 添加数据
#方式1:一条一条的添加数据
# ① 没有指明添加的字段
#正确的
INSERT INTO emp1
VALUES (1,'Tom','2000-12-21',3400); #注意:一定要按照声明的字段的先后顺序添加
#错误的
INSERT INTO emp1
VALUES (2,3400,'2000-12-21','Jerry'); # 与数据表中字段的定义顺序不一致,添加失败

第二种情况:为表的指定字段插入数据(推荐)

语法格式:

INSERT INTO 表名(column1 [, column2, …, columnn])
VALUES (value1 [,value2, …, valuen]);

为表的指定字段插入数据,就是在INSERT语句中只向部分字段中插入值,而其他字段的值为表定义时的默认值。如果字段定义时设置非空约束,则添加时必须指定值,否则添加失败。

注意:在 INSERT 子句中随意列出列名,但是一旦列出,VALUES中要插入的value1,....valuen需要与column1,...columnn列一一对应。如果类型不同,将无法插入,并且MySQL会产生错误。

# ② 指明要添加的字段(推荐)
INSERT INTO emp1(id,hire_date,salary,`name`)
VALUES(2,'1999-09-09',4000,'Jerry');
# 说明:没有进行赋值的hire_date 的值为 null
INSERT INTO emp1(id,salary,`name`)
VALUES(3,4500,'shk');

第三种情况:同时插入多条记录(推荐)

INSERT语句可以同时向数据表中插入多条记录,插入时指定多个值列表,每个值列表之间用逗号分隔开

语法格式:

INSERT INTO table_name
VALUES
(value1 [,value2, …, valuen]),
(value1 [,value2, …, valuen]),
……
(value1 [,value2, …, valuen]);
# 或者 给指定字段添加
INSERT INTO table_name(column1 [, column2, …, columnn])
VALUES
(value1 [,value2, …, valuen]),
(value1 [,value2, …, valuen]),
……
(value1 [,value2, …, valuen]);

使用INSERT同时插入多条记录时,MySQL会返回一些在执行单行插入时没有的额外信息,这些信息的含义如下:

● Records:表明插入的记录条数。

● Duplicates:表明插入时被忽略的记录,原因可能是这些记录包含了重复的主键值。

● Warnings:表明有问题的数据值,例如发生数据类型转换。

注意:一个同时插入多行记录的INSERT语句等同于多个单行插入的INSERT语句,但是多行的INSERT语句在处理过程中效率更高 。因为MySQL执行单条INSERT语句插入多行数据比使用多条INSERT语句快,所以在插入多条记录时最好选择使用单条INSERT语句的方式插入。

INSERT INTO emp1(id,NAME,salary)
VALUES
(4,'Jim',5000),
(5,'张俊杰',5500);

小结:

  • VALUES 也可以写成 VALUE ,但是VALUES是标准写法

  • 字符和日期型数据应包含在单引号中

11.1.2、查询的结果插入表中

INSERT还可以将SELECT语句查询的结果插入到表中,此时不需要把每一条记录的值一个一个输入,只需要使用一条INSERT语句和一条SELECT语句组成的组合语句即可快速地从一个或多个表中向一个表中插入多行。

语法格式:

INSERT INTO 目标表名tar_table
(tar_column1 [, tar_column2, …, tar_columnn])
SELECT
(src_column1 [, src_column2, …, src_columnn])
FROM 源表名src_table
[WHERE condition]

注意:

  • 在 INSERT 语句中加入子查询。
  • 不必书写VALUES子句。
  • 查询的字段一定要与添加到的表的字段一一对应。
  • SELECT中的字段类型必须和INSERT中对应的字段类型一致,且INSERT中的字段必须兼容SELECT中对应的字段。
#方式2:将查询结果插入到表中
SELECT * FROM emp1;

INSERT INTO emp1(id,NAME,salary,hire_date)
#查询语句
SELECT employee_id,last_name,salary,hire_date  # 查询的字段一定要与添加到的表的字段一一对应
FROM employees
WHERE department_id IN (70,60);

DESC emp1;
DESC employees;
#说明:emp1表中要添加数据的字段的长度不能低于employees表中查询的字段的长度。
# 如果emp1表中要添加数据的字段的长度低于employees表中查询的字段的长度的话,就有添加不成功的风险。

11.2、更新数据

使用UPDATE语句更新表中数据。语法格式:

UPDATE table_name
SET column1=value1, column2=value2, … , column=valuen
[WHERE condition]

说明:

  • 可以一次更新多条数据。
  • 如果需要回滚数据,需要保证在DML前,进行设置:SET AUTOCOMMIT = FALSE;
  • 使用 WHERE 子句指定需要更新的数据,否则会批量修改表中所有数据。
  • 如果 WHERE 子句指定的数据不存在则更新失败。
  • 修改数据时,是可能存在受约束影响而导致不成功的情况的。
#2. 更新数据 (或修改数据)
# UPDATE .... SET .... WHERE ...
# 可以实现批量修改数据的。

UPDATE emp1
SET hire_date = CURDATE()
WHERE id = 5;

SELECT * FROM emp1;

#同时修改一条数据的多个字段
UPDATE emp1
SET hire_date = CURDATE(),salary = 6000
WHERE id = 4;

#题目:将表中姓名中包含字符a的提薪20%
UPDATE emp1
SET salary = salary * 1.2
WHERE NAME LIKE '%a%';

#修改数据时,是可能存在不成功的情况的。(可能是由于约束的影响造成的)
UPDATE employees
SET department_id = 10000
WHERE employee_id = 102;

11.3、删除数据

使用DELETE语句删除表中数据。语法格式:

DELETE FROM table_name
[WHERE <condition>];
#table_name指定要执行删除操作的表;“[WHERE ]”为可选参数,指定删除条件,如果没有WHERE子句,DELETE语句将删除表中的所有记录。

说明:

  1. 删除操作通常搭配WHERE子句一起使用,否则将删除表中全部数据。
  2. 删除操作也会受约束影响导致删除失败。
  3. DML操作默认自动提交数据,如果希望在DML操作后不自动提交数据,需要在DML之前设置SET autocommit = FALSE;
#3. 删除数据 DELETE FROM .... WHERE....
DELETE FROM emp1
WHERE id = 1;
#在删除数据时,也有可能因为约束的影响,导致删除失败
DELETE FROM departments
WHERE department_id = 50;

11.4、MySQL8.0新特性:计算列

计算列:简单来说就是某一列的值是通过别的列计算得来的。例如,a列值为1、b列值为2,c列不需要手动插入,定义a+b的结果为c的值,那么c就是计算列,是通过别的列计算得来的。在MySQL 8.0中,CREATE TABLE 和 ALTER TABLE 中都支持增加计算列

#4. MySQL8的新特性:计算列
USE atguigudb;
CREATE TABLE IF NOT EXISTS test1(
a INT,
b INT,
c INT GENERATED ALWAYS AS (a + b) VIRTUAL  #字段c即为计算列
);

INSERT INTO test1(a,b)
VALUES(10,20);

SELECT * FROM test1;

UPDATE test1
SET a = 100;

11.5、综合案例

# 1、创建数据库test01_library
# 2、创建表 books,表结构如下:
字段名 字段说明 数据类型
id 书编号 INT
name 书名 VARCHAR(50)
authors 作者 VARCHAR(100)
price 价格 FLOAT
pubdate 出版日期 YEAR
note 说明 VARCHAR(100)
num 库存 INT
# 3、向books表中插入记录
# 1)不指定字段名称,插入第一条记录
# 2)指定所有字段名称,插入第二记录
# 3)同时插入多条记录(剩下的所有记录)
id name authors price pubdate note num
1 Tal of AAA Dickes 23 1995 novel 11
2 EmmaT Jane lura 35 1993 joke 22
3 Story of Jane Jane Tim 40 2001 novel 0
4 Lovey Day George Byron 20 2005 novel 30
5 Old land Honore Blade 30 2010 law 0
6 The Battle Upton Sara 30 1999 medicine 40
7 Rose Hood Richard haggard 28 2008 cartoon 28
# 4、将小说类型(novel)的书的价格都增加5。
# 5、将名称为EmmaT的书的价格改为40,并将说明改为drama。
# 6、删除库存为0的记录。
# 7、统计书名中包含a字母的书
# 8、统计书名中包含a字母的书的数量和库存总量
# 9、找出“novel”类型的书,按照价格降序排列
# 10、查询图书信息,按照库存量降序排列,如果库存量相同的按照note升序排列
# 11、按照note分类统计书的数量
# 12、按照note分类统计书的库存量,显示库存量超过30本的
# 13、查询所有图书,每页显示5本,显示第二页
# 14、按照note分类统计书的库存量,显示库存量最多的
# 15、查询书名达到10个字符的书,不包括里面的空格
# 16、查询书名和类型,其中note值为novel显示小说,law显示法律,medicine显示医药,cartoon显示卡通,joke显示笑话
# 17、查询书名、库存,其中num值超过30本的,显示滞销,大于0并低于10的,显示畅销,为0的显示需要无货
# 18、统计每一种note的库存量,并合计总量
# 19、统计每一种note的数量,并合计总量
# 20、统计库存量前三名的图书
# 21、找出最早出版的一本书
# 22、找出novel中价格最高的一本书
# 23、找出书名中字数最多的一本书,不含空格
#5.综合案例
# 1、创建数据库test01_library
CREATE DATABASE IF NOT EXISTS test01_library CHARACTER SET 'utf8';
USE test01_library;

# 2、创建表 books,表结构如下:
CREATE TABLE IF NOT EXISTS books(
id INT,
`name` VARCHAR(50),
`authors` VARCHAR(100),
price FLOAT,
pubdate YEAR,
note VARCHAR(100),
num INT
);

DESC books;

SELECT * FROM books;
# 3、向books表中插入记录
# 1)不指定字段名称,插入第一条记录
INSERT INTO books
VALUES(1,'Tal of AAA','Dickes',23,'1995','novel',11);
# 2)指定所有字段名称,插入第二记录
INSERT INTO books(id,NAME,AUTHORS,price,pubdate,note,num)
VALUES(2,'EmmaT','Jane lura',35,'1993','joke',22);
# 3)同时插入多条记录(剩下的所有记录)
INSERT INTO books(id,NAME,AUTHORS,price,pubdate,note,num)
VALUES
(3,'Story of Jane','Jane Tim',40,2001,'novel',0),
(4,'Lovey Day','George Byron',20,2005,'novel',30),
(5,'Old land','Honore Blade',30,2010,'Law',0),
(6,'The Battle','Upton Sara',30,1999,'medicine',40),
(7,'Rose Hood','Richard haggard',28,2008,'cartoon',28);

# 4、将小说类型(novel)的书的价格都增加5UPDATE books
SET price = price + 5
WHERE note = 'novel';

# 5、将名称为EmmaT的书的价格改为40,并将说明改为drama。
UPDATE books
SET price = 40,note = 'drama'
WHERE NAME = 'EmmaT';

# 6、删除库存为0的记录。
DELETE FROM books
WHERE num = 0;

# 7、统计书名中包含a字母的书
SELECT NAME
FROM books
WHERE NAME LIKE '%a%';

# 8、统计书名中包含a字母的书的数量和库存总量
SELECT COUNT(*),SUM(num)
FROM books
WHERE NAME LIKE '%a%';

# 9、找出“novel”类型的书,按照价格降序排列
SELECT NAME,note,price
FROM books
WHERE note = 'novel'
ORDER BY price DESC;

# 10、查询图书信息,按照库存量降序排列,如果库存量相同的按照note升序排列
SELECT *
FROM books
ORDER BY num DESC,note ASC;

# 11、按照note分类统计书的数量
SELECT note,COUNT(*)
FROM books
GROUP BY note;

# 12、按照note分类统计书的库存量,显示库存量超过30本的
SELECT note,SUM(num)
FROM books
GROUP BY note
HAVING SUM(num) > 30;

# 13、查询所有图书,每页显示5本,显示第二页
SELECT *
FROM books
LIMIT 5,5;

# 14、按照note分类统计书的库存量,显示库存量最多的
SELECT note,SUM(num) sum_num
FROM books
GROUP BY note
ORDER BY sum_num DESC
LIMIT 0,1;

# 15、查询书名达到10个字符的书,不包括里面的空格
SELECT CHAR_LENGTH(REPLACE(NAME,' ',''))
FROM books;

SELECT NAME
FROM books
WHERE CHAR_LENGTH(REPLACE(NAME,' ','')) >= 10;

# 16、查询书名和类型,其中note值为novel显示小说,law显示法律,medicine显示医药,
#cartoon显示卡通,joke显示笑话
SELECT NAME "书名",note,CASE note WHEN 'novel' THEN '小说'
				  WHEN 'law' THEN '法律'
				  WHEN 'medicine' THEN '医药'
				  WHEN 'cartoon' THEN '卡通'
				  WHEN 'joke' THEN '笑话'
				  ELSE '其他'
				  END "类型"
FROM books;

# 17、查询书名、库存,其中num值超过30本的,显示滞销,大于0并低于10的,
#显示畅销,为0的显示需要无货
SELECT NAME AS "书名",num AS "库存", CASE WHEN num > 30 THEN '滞销'
					  WHEN num > 0 AND num < 10 THEN '畅销'
					  WHEN num = 0 THEN '无货'
					  ELSE '正常'
					  END "显示状态"
FROM books;

# 18、统计每一种note的库存量,并合计总量
SELECT IFNULL(note,'合计库存总量') AS note,SUM(num)
FROM books
GROUP BY note WITH ROLLUP;

# 19、统计每一种note的数量,并合计总量
SELECT IFNULL(note,'合计总量') AS note,COUNT(*)
FROM books
GROUP BY note WITH ROLLUP;

# 20、统计库存量前三名的图书
SELECT *
FROM books
ORDER BY num DESC
LIMIT 0,3;

# 21、找出最早出版的一本书
SELECT *
FROM books
ORDER BY pubdate ASC
LIMIT 0,1;

# 22、找出novel中价格最高的一本书
SELECT *
FROM books
WHERE note = 'novel'
ORDER BY price DESC
LIMIT 0,1;

# 23、找出书名中字数最多的一本书,不含空格
SELECT *
FROM books
ORDER BY CHAR_LENGTH(REPLACE(NAME,' ','')) DESC
LIMIT 0,1;

章节练习

#第11章_数据处理之增删改的课后练习
#练习1:
#1. 创建数据库dbtest11
CREATE DATABASE IF NOT EXISTS dbtest11 CHARACTER SET 'utf8';

#2. 运行以下脚本创建表my_employees
USE dbtest11;

CREATE TABLE my_employees(
	id INT(10),
	first_name VARCHAR(10),
	last_name VARCHAR(10),
	userid VARCHAR(10),
	salary DOUBLE(10,2)
);

CREATE TABLE users(
	id INT,
	userid VARCHAR(10),
	department_id INT
);
#3.显示表my_employees的结构

DESC my_employees;

DESC users;

#4.向my_employees表中插入下列数据
ID	FIRST_NAME	LAST_NAME	USERID		SALARY
1	patel		Ralph		Rpatel		895
2	Dancs		Betty		Bdancs		860
3	Biri		Ben		Bbiri		1100
4	Newman		Chad		Cnewman		750
5	Ropeburn	Audrey		Aropebur	1550

INSERT INTO my_employees
VALUES(1,'patel','Ralph','Rpatel',895);

INSERT INTO my_employees VALUES
(2,'Dancs','Betty','Bdancs',860),
(3,'Biri','Ben','Bbiri',1100),
(4,'Newman','Chad','Cnewman',750),
(5,'Ropeburn','Audrey','Aropebur',1550);

SELECT * FROM my_employees;

DELETE FROM my_employees;

#方式2INSERT INTO my_employees
SELECT 1,'patel','Ralph','Rpatel',895 UNION ALL
SELECT 2,'Dancs','Betty','Bdancs',860 UNION ALL
SELECT 3,'Biri','Ben','Bbiri',1100 UNION ALL
SELECT 4,'Newman','Chad','Cnewman',750 UNION ALL
SELECT 5,'Ropeburn','Audrey','Aropebur',1550;
			
#5.向users表中插入数据
1	Rpatel		10
2	Bdancs		10
3	Bbiri		20
4	Cnewman		30
5	Aropebur	40
INSERT INTO users VALUES
(1,'Rpatel',10),
(2,'Bdancs',10),
(3,'Bbiri',20),
(4,'Cnewman',30),
(5,'Aropebur',40)

SELECT * FROM users;

#6.3号员工的last_name修改为“drelxer”
UPDATE my_employees
SET last_name = 'drelxer'
WHERE id = 3;

#7. 将所有工资少于900的员工的工资修改为1000
UPDATE my_employees
SET salary = 1000
WHERE salary < 900;

#8. 将userid为Bbiri的users表和my_employees表的记录全部删除
#方式1DELETE FROM my_employees
WHERE userid = 'Bbiri';

DELETE FROM users
WHERE userid = 'Bbiri';

#方式2DELETE m,u
FROM my_employees m JOIN users u
ON m.userid = u.userid
WHERE m.userid = 'Bbiri';

SELECT * FROM my_employees;
SELECT * FROM users;

#9. 删除my_employees、users表所有数据
DELETE FROM my_employees;
DELETE FROM users;

#10. 检查所作的修正
SELECT * FROM my_employees;
SELECT * FROM users;

#11. 清空表my_employees
TRUNCATE TABLE my_employees;
##########################################
#练习2:
# 1. 使用现有数据库dbtest11
USE dbtest11;

# 2. 创建表格pet
CREATE TABLE pet(
NAME VARCHAR(20),
OWNER VARCHAR(20),
species VARCHAR(20),
sex CHAR(1),
birth YEAR,
death YEAR
);

DESC pet;

# 3. 添加记录
INSERT INTO pet VALUES
('Fluffy','harold','Cat','f','2003','2010'),
('Claws','gwen','Cat','m','2004',NULL),
('Buffy',NULL,'Dog','f','2009',NULL),
('Fang','benny','Dog','m','2000',NULL),
('bowser','diane','Dog','m','2003','2009'),
('Chirpy',NULL,'Bird','f','2008',NULL);

SELECT *
FROM pet;

# 4. 添加字段:主人的生日owner_birth DATE类型。
ALTER TABLE pet
ADD owner_birth DATE;

# 5. 将名称为Claws的猫的主人改为kevin
UPDATE pet
SET OWNER = 'kevin'
WHERE NAME = 'Claws' AND species = 'Cat';

# 6. 将没有死的狗的主人改为duck
UPDATE pet
SET OWNER = 'duck'
WHERE death IS NULL AND species = 'Dog';

# 7. 查询没有主人的宠物的名字;
SELECT NAME
FROM pet
WHERE OWNER IS NULL;

# 8. 查询已经死了的cat的姓名,主人,以及去世时间;
SELECT NAME,OWNER,death
FROM pet
WHERE death IS NOT NULL;

# 9. 删除已经死亡的狗
DELETE FROM pet
WHERE death IS NOT NULL 
AND species = 'Dog';

# 10. 查询所有宠物信息
SELECT * 
FROM pet;

##################################
#练习3:
# 1. 使用已有的数据库dbtest11
USE dbtest11;
# 2. 创建表employee,并添加记录
CREATE TABLE employee(
id INT,
NAME VARCHAR(15),
sex CHAR(1),
tel VARCHAR(25),
addr VARCHAR(35),
salary DOUBLE(10,2)

);

INSERT INTO employee VALUES
(10001,'张一一','男','13456789000','山东青岛',1001.58),
(10002,'刘小红','女','13454319000','河北保定',1201.21),
(10003,'李四','男','0751-1234567','广东佛山',1004.11),
(10004,'刘小强','男','0755-5555555','广东深圳',1501.23),
(10005,'王艳','男','020-1232133','广东广州',1405.16);

SELECT * FROM employee;

# 3. 查询出薪资在1200~1300之间的员工信息。
SELECT *
FROM employee
WHERE salary BETWEEN 1200 AND 1300;

# 4. 查询出姓“刘”的员工的工号,姓名,家庭住址。
SELECT id,NAME,addr
FROM employee
WHERE NAME LIKE '刘%';

# 5. 将“李四”的家庭住址改为“广东韶关”
UPDATE employee
SET addr = '广东韶关'
WHERE NAME = '李四';

# 6. 查询出名字中带“小”的员工
SELECT *
FROM employee
WHERE NAME LIKE '%小%';

12、MySQL数据类型

12.1、MySQL中的数据类型

类型 类型举例
整数类型 TINYINT、SMALLINT、MEDIUMINT、INT(或INTEGER)、BIGINT
浮点类型 FLOAT、DOUBLE
定点数类型 DECIMAL
位类型 BIT
日期时间类型 YEAR、TIME、DATE、DATETIME、TIMESTAMP
文本字符串类型 CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT
枚举类型 ENUM
集合类型 SET
二进制字符串类型 BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB、LONGBLOB
JSON类型 JSON对象、JSON数组
空间数据类型 单值:GEOMETRY、POINT、LINESTRING、POLYGON;集合:MULTIPOINT、MULTILINESTRING、MULTIPOLYGON、GEOMETRYCOLLECTION

其中,常用的数据类型介绍如下:

数据类型 描述
INT 从$-2{31}$到$2-1$的整型数据。存储大小为 4个字节
CHAR(size) 定长字符数据。若未指定,默认为1个字符,最大长度255
VARCHAR(size) 可变长字符数据,根据字符串实际长度保存,必须指定长度
FLOAT(M,D) 单精度,占用4个字节,M=整数位+小数位,D=小数位。 D<=M<=255,0<=D<=30,默认M+D<=6
DOUBLE(M,D) 双精度,占用8个字节,D<=M<=255,0<=D<=30,默认M+D<=15
DECIMAL(M,D) 高精度小数,占用M+2个字节,D<=M<=65,0<=D<=30,最大取值范围与DOUBLE相同。
DATE 日期型数据,格式'YYYY-MM-DD'
BLOB 二进制形式的长文本数据,最大可达4G
TEXT 长文本数据,最大可达4G

常见的数据类型的属性,如下:

MySQL关键字 含义
NULL 数据列可包含NULL值
NOT NULL 数据列不允许包含NULL值
DEFAULT 默认值
PRIMARY KEY 主键
AUTO_INCREMENT 自动递增,适用于整数类型
UNSIGNED 无符号
CHARACTER SET 'name' 指定一个字符集

12.2、整数类型

12.2.1、类型介绍

整数类型一共有 5 种,包括 TINYINTSMALLINTMEDIUMINTINTINTEGER)和 BIGINT

整数类型 字节 有符号数取值范围 无符号数取值范围
TINYINT 1 -128~127 0~255
SMALLINT 2 -32768~32767 0~65535
MEDIUMINT 3 -8388608~8388607 0~16777215
INT、INTEGER 4 -2147483648~2147483647 0~4294967295
BIGINT 8 -9223372036854775808~9223372036854775807 0~18446744073709551615

12.2.2、可选属性

整数类型的可选属性有三个:

M:

M: 表示显示宽度,M的取值范围是(0, 255)。例如,int(5):当数据宽度小于5位的时候在数字前面需要用字符填满宽度。该项功能需要配合ZEROFILL使用,表示用“0”填满宽度,否则指定显示宽度无效。如果设置了显示宽度,那么插入的数据宽度超过显示宽度限制,会不会截断或插入失败?

答案:不会对插入的数据有任何影响,还是按照类型的实际宽度进行保存,即 显示宽度与类型可以存储的值范围无关 。从MySQL 8.0.17开始,整数数据类型不推荐使用显示宽度属性。整型数据类型可以在定义表结构时指定所需要的显示宽度,如果不指定,则系统为每一种类型指定默认的宽度值。

UNSIGNED:

UNSIGNED: 无符号类型(非负),所有的整数类型都有一个可选的属性UNSIGNED(无符号属性),无符号整数类型的最小取值为0。所以,如果需要在MySQL数据库中保存非负整数值时,可以将整数类型设置为无符号类型。

int类型默认显示宽度为int(11),无符号int类型默认显示宽度为int(10)

ZEROFILL

ZEROFILL: 0填充,(如果某列是ZEROFILL,那么MySQL会自动为当前列添加UNSIGNED属性),如果指定了ZEROFILL只是表示不够M位时,用0在左边填充,如果超过M位,只要不超过数据存储范围即可。

在 int(M) 中,M 的值跟 int(M) 所占多少存储空间并无任何关系。 int(3)、int(4)、int(8) 在磁盘上都是占用 4 bytes 的存储空间。也就是说int(M),必须和UNSIGNED ZEROFILL一起使用才有意义。如果整数值超过M位,就按照实际位数存储。只是无须再用字符 0 进行填充。

#2.整型数据类型
USE dbtest12;
CREATE TABLE test_int1(
f1 TINYINT,
f2 SMALLINT,
f3 MEDIUMINT,
f4 INTEGER,
f5 BIGINT
);

DESC test_int1;

INSERT INTO test_int1(f1)
VALUES(12),(-12),(-128),(127);

SELECT * FROM test_int1;

#Out of range value for column 'f1' at row 1
INSERT INTO test_int1(f1)
VALUES(128);

CREATE TABLE test_int2(
f1 INT,
f2 INT(5),
f3 INT(5) ZEROFILL  #① 显示宽度为5。当insert的值不足5位时,使用0填充。 ②当使用ZEROFILL时,自动会添加UNSIGNED
)

INSERT INTO test_int2(f1,f2)
VALUES(123,123),(123456,123456);

SELECT * FROM test_int2;

INSERT INTO test_int2(f3)
VALUES(123),(123456);

SHOW CREATE TABLE test_int2;

CREATE TABLE test_int3(
f1 INT UNSIGNED
);

DESC test_int3;

INSERT INTO test_int3
VALUES(2412321);

#Out of range value for column 'f1' at row 1
INSERT INTO test_int3
VALUES(4294967296);

12.2.3、适用场景

TINYINT:一般用于枚举数据,比如系统设定取值范围很小且固定的场景。

SMALLINT:可以用于较小范围的统计数据,比如统计工厂的固定资产库存数量等。

MEDIUMINT:用于较大整数的计算,比如车站每日的客流量等。

INT、INTEGER:取值范围足够大,一般情况下不用考虑超限问题,用得最多。比如商品编号。

BIGINT:只有当你处理特别巨大的整数时才会用到。比如双十一的交易量、大型门户网站点击量、证券公司衍生产品持仓等。

12.2.4、如何选择

在评估用哪种整数类型的时候,你需要考虑 存储空间 和 可靠性 的平衡问题:一方 面,用占用字节数少的整数类型可以节省存储空间;另一方面,要是为了节省存储空间, 使用的整数类型取值范围太小,一旦遇到超出取值范围的情况,就可能引起 系统错误 ,影响可靠性。在实际工作中,系统故障产生的成本远远超过增加几个字段存储空间所产生的成本。因此,建议首先确保数据不会超过取值范围,在这个前提之下,再去考虑如何节省存储空间

12.3、浮点类型

12.3.1、类型介绍

浮点数和定点数类型的特点是可以处理小数。 MySQL支持的浮点数类型,分别是 FLOAT、DOUBLE、REAL。

  • FLOAT 表示单精度浮点数,占用4个字节
  • DOUBLE 表示双精度浮点数,占用8个字节

浮点数的取值范围

  • REAL默认就是 DOUBLE。如果你把 SQL 模式设定为启用“ REAL_AS_FLOAT ”,那 么,MySQL 就认为REAL 是 FLOAT。如果要启用“REAL_AS_FLOAT”,可以通过以下 SQL 语句实现:

    SET sql_mode = “REAL_AS_FLOAT”;
    

为什么浮点数类型的无符号数取值范围,只相当于有符号数取值范围的一半,也就是只相当于有符号数取值范围大于等于零的部分呢?

MySQL 存储浮点数的格式为:符号(S)、尾数(M) 和 阶码(E)。因此,无论有没有符号,MySQL 的浮点数都会存储表示符号的部分。因此, 所谓的无符号数取值范围,其实就是有符号数取值范围大于等于零的部分。

12.3.2、精度说明

对于浮点类型,在MySQL中单精度值使用 4 个字节,双精度值使用 8 个字节。

  • MySQL允许使用 非标准语法(其他数据库未必支持,因此如果涉及到数据迁移,则最好不要这么用): FLOAT(M,D) 或 DOUBLE(M,D) 。这里,M称为 精度 ,D称为 标度 。(M,D)中 M=整数位+小数位,D=小数位。 D<=M<=255,0<=D<=30。

    例如,定义为FLOAT(5,2)的一个列可以显示为-999.99-999.99。如果超过这个范围会报错。

  • FLOAT和DOUBLE类型在不指定(M,D)时,默认会按照实际的精度(由实际的硬件和操作系统决定)来显示。

  • 说明:浮点类型,也可以加 UNSIGNED ,但是不会改变数据范围,例如:FLOAT(3,2) UNSIGNED仍然只能表示0-9.99的范围。

  • 不管是否显式设置了精度(M,D),这里MySQL的处理方案如下:

    • 如果存储时,整数部分超出了范围,MySQL就会报错,不允许存这样的值
    • 如果存储时,小数点部分若超出范围,就分以下情况:
      • 若四舍五入后,整数部分没有超出范围,则只警告,但能成功操作并四舍五入删除多余的小数位后保存。例如在FLOAT(5,2)列内插入999.009,近似结果是999.01。
      • 若四舍五入后,整数部分超出范围,则MySQL报错,并拒绝处理。如FLOAT(5,2)列内插入999.995和-999.995都会报错。
  • 从MySQL 8.0.17开始,FLOAT(M,D)和DOUBLE(M,D)用法在官方文档中已经明确不推荐使用,将来可能被移除。另外,关于浮点型FLOAT和DOUBLE的UNSIGNED也不推荐使用了,将来也可能被移除。

#3.浮点类型
CREATE TABLE test_double1(
f1 FLOAT,
f2 FLOAT(5,2),
f3 DOUBLE,
f4 DOUBLE(5,2)
);

DESC test_double1;

INSERT INTO test_double1(f1,f2)
VALUES(123.45,123.45);

SELECT * FROM test_double1;

INSERT INTO test_double1(f3,f4)
VALUES(123.45,123.456); #存在四舍五入

#Out of range value for column 'f4' at row 1
INSERT INTO test_double1(f3,f4)
VALUES(123.45,1234.456);

#Out of range value for column 'f4' at row 1
INSERT INTO test_double1(f3,f4)
VALUES(123.45,999.995);

12.3.3、精度误差说明

浮点数类型有个缺陷,就是不精准。

#测试FLOATDOUBLE的精度问题
CREATE TABLE test_double2(
f1 DOUBLE
);

INSERT INTO test_double2
VALUES(0.47),(0.44),(0.19);

SELECT SUM(f1)
FROM test_double2;

SELECT SUM(f1) = 1.1,1.1 = 1.1
FROM test_double2;

MySQL 用 4 个字节存储 FLOAT 类型数据,用 8 个字节来存储 DOUBLE 类型数据。无论哪个,都是采用二进制的方式来进行存储的。比如 9.625,用二进制来表达,就是 1001.101,或者表达成 1.001101×2^3。如果尾数不是 0 或 5(比如 9.624),你就无法用一个二进制数来精确表达。进而,就只好在取值允许的范围内进行四舍五入。在编程中,如果用到浮点数,要特别注意误差问题,因为浮点数是不准确的,所以我们要避免使用 = 来判断两个数是否相等。同时,在一些对精确度要求较高的项目中,千万不要使用浮点数,不然会导致结果错误,甚至是造成不可挽回的损失。那么,MySQL 有没有精准的数据类型呢?当然有,这就是定点数类型:DECIMAL 。

12.4、定点数类型

数据类型 字节数 含义
DECIMAL(M,D),DEC,NUMERIC M+2字节 有效范围由M和D决定

使用 DECIMAL(M,D) 的方式表示高精度小数。其中,M被称为精度,D被称为标度。0<=M<=65,0<=D<=30,D<M。例如,定义DECIMAL(

5,2)的类型,表示该列取值范围是-999.99~999.99。

  • DECIMAL(M,D)的最大取值范围与DOUBLE类型一样,但是有效的数据范围是由M和D决定的。DECIMAL的存储空间并不是固定的,由精度值M决定,总共占用的存储空间为M+2个字节。也就是说,在一些对精度要求不高的场景下,比起占用同样字节长度的定点数,浮点数表达的数值范围可以更大一些。
  • 定点数在MySQL内部是以 字符串 的形式进行存储,这就决定了它一定是精准的。
  • 当DECIMAL类型不指定精度和标度时,其默认为DECIMAL(10,0)当数据的精度超出了定点数类型的精度范围时,则MySQL同样会进行四舍五入处理
#4. 定点数类型
CREATE TABLE test_decimal1(
f1 DECIMAL,
f2 DECIMAL(5,2)
);

DESC test_decimal1;

INSERT INTO test_decimal1(f1)
VALUES(123),(123.45);

SELECT * FROM test_decimal1;

INSERT INTO test_decimal1(f2)
VALUES(999.99);

INSERT INTO test_decimal1(f2)
VALUES(67.567);#存在四色五入

#Out of range value for column 'f2' at row 1
INSERT INTO test_decimal1(f2)
VALUES(1267.567);

#Out of range value for column 'f2' at row 1
INSERT INTO test_decimal1(f2)
VALUES(999.995);

#演示DECIMAL替换DOUBLE,体现精度
ALTER TABLE test_double2
MODIFY f1 DECIMAL(5,2);

DESC test_double2;

SELECT SUM(f1)
FROM test_double2;

SELECT SUM(f1) = 1.1,1.1 = 1.1
FROM test_double2;

浮点数 vs 定点数

浮点数相对于定点数的优点是在长度一定的情况下,浮点类型取值范围大,但是不精准,适用于需要取值范围大,又可以容忍微小误差的科学计算场景(比如计算化学、分子建模、流体动力学等)

定点数类型取值范围相对小,但是精准,没有误差,适合于对精度要求极高的场景(比如涉及金额计算的场景)

开发经验:

“由于 DECIMAL 数据类型的精准性,在我们的项目中,除了极少数(比如商品编号)用到整数类型外,其他的数值都用的是 DECIMAL,原因就是这个项目所处的零售行业,要求精准,一分钱也不能差。 ”

12.5、位类型BIT

二进制字符串类型 长度 长度范围 占用空间
BIT(M) M 1 <= M <= 64 约为(M + 7)/8个字节

BIT类型,如果没有指定(M),默认是1位。这个1位,表示只能存1位的二进制值。这里(M)是表示二进制的位数,位数最小值为1,最大值为64。

注意:在向BIT类型的字段中插入数据时,一定要确保插入的数据在BIT类型支持的范围内。

  • 使用SELECT命令查询位字段时,可以用BIN()HEX()函数进行读取。

  • 使用b+0查询数据时,可以直接查询出存储的十进制数据的值。

#5. 位类型:BIT
CREATE TABLE test_bit1(
f1 BIT,
f2 BIT(5),
f3 BIT(64)
);

DESC test_bit1;

INSERT INTO test_bit1(f1)
VALUES(0),(1);

SELECT *
FROM test_bit1;

#Data too long for column 'f1' at row 1
INSERT INTO test_bit1(f1)
VALUES(2);

INSERT INTO test_bit1(f2)
VALUES(31);

#Data too long for column 'f2' at row 1
INSERT INTO test_bit1(f2)
VALUES(32);

SELECT BIN(f1),BIN(f2),HEX(f1),HEX(f2)
FROM test_bit1;

#此时+0以后,可以以十进制的方式显示数据
SELECT f1 + 0, f2 + 0
FROM test_bit1;

12.6、日期与时间类型

日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。原因是客户需要知道数据的时间标签,从而进行数据查询、统计和处理。

MySQL有多种表示日期和时间的数据类型,不同的版本可能有所差异,MySQL8.0版本支持的日期和时间类型主要有:YEAR类型、TIME类型、DATE类型、DATETIME类型和TIMESTAMP类型。

  • YEAR 类型通常用来表示年
  • DATE 类型通常用来表示年、月、日
  • TIME 类型通常用来表示时、分、秒
  • DATETIME 类型通常用来表示年、月、日、时、分、秒
  • TIMESTAMP 类型通常用来表示带时区的年、月、日、时、分、秒
类型 名称 字节 日期格式 最小值 最大值
YEAR 1 YYYY或YY 1901 2155
TIME 时间 3 HH:MM:SS -838:59:59 838:59:59
DATE 日期 3 YYYY-MM-DD 1000-01-01 9999-12-03
DATETIME 日期时间 8 YYYY-MM-DD,HH:MM:SS 1000-01-01 00:00:00 9999-12-31 23:59:59
TIMESTAMP 日期时间 4 YYYY-MM-DD,HH:MM:SS 1970-01-01 00:00:00 UTC 2038-01-19 03:14:07 UTC

为什么时间类型 TIME 的取值范围不是 -23:59:59~23:59:59 呢?原因是 MySQL 设计的 TIME 类型,不光表示一天之内的时间,而且可以用来表示一个时间间隔,这个时间间隔可以超过 24 小时。

12.6.1、YEAR类型

YEAR类型用来表示年份,在所有的日期时间类型中所占用的存储空间最小,只需要 1个字节 的存储空间。

在MySQL中,YEAR有以下几种存储格式:

  • 以4位字符串或数字格式表示YEAR类型,其格式为YYYY,最小值为1901,最大值为2155。

  • 以2位字符串格式表示YEAR类型,最小值为00,最大值为99。

    • 当取值为01到69时,表示2001到2069;

    • 当取值为70到99时,表示1970到1999;

    • 当取值整数的0或00添加的话,那么是0000年;

    • 当取值是日期/字符串的'0'添加的话,是2000年。

从MySQL5.5.27开始,2位格式的YEAR已经不推荐使用。YEAR默认格式就是“YYYY”,没必要写成YEAR(4),从MySQL 8.0.19开始,不推荐使用指定显示宽度的YEAR(4)数据类型。

#6.1 YEAR类型
CREATE TABLE test_year(
f1 YEAR,
f2 YEAR(4)
);

DESC test_year;

INSERT INTO test_year(f1)
VALUES('2021'),(2022);

SELECT * FROM test_year;

INSERT INTO test_year(f1)
VALUES ('2155');

#Out of range value for column 'f1' at row 1
INSERT INTO test_year(f1)
VALUES ('2156');

INSERT INTO test_year(f1)
VALUES ('69'),('70');

INSERT INTO test_year(f1)
VALUES (0),('00');

12.6.2、DATE类型

DATE类型表示日期,没有时间部分,格式为 YYYY-MM-DD ,其中,YYYY表示年份,MM表示月份,DD表示日期。需要 3个字节 的存储空间。在向DATE类型的字段插入数据时,同样需要满足一定的格式条件。

  • YYYY-MM-DD 格式或者 YYYYMMDD 格式表示的字符串日期,其最小取值为1000-01-01,最大取值为9999-12-03。YYYYMMDD格式会被转化为YYYY-MM-DD格式。
  • YY-MM-DD 格式或者 YYMMDD 格式表示的字符串日期,此格式中,年份为两位数值或字符串满足YEAR类型的格式条件为:当年份取值为00到69时,会被转化为2000到2069;当年份取值为70到99时,会被转化为1970到1999
  • 使用 CURRENT_DATE() 或者 NOW() 函数,会插入当前系统的日期。
#6.2 DATE类型
CREATE TABLE test_date1(
f1 DATE
);

DESC test_date1;

INSERT INTO test_date1
VALUES ('2020-10-01'), ('20201001'),(20201001);

INSERT INTO test_date1
VALUES ('00-01-01'), ('000101'), ('69-10-01'), ('691001'), ('70-01-01'), ('700101'), ('99-01-01'), ('990101');

INSERT INTO test_date1
VALUES (000301), (690301), (700301), (990301); #存在隐式转换

INSERT INTO test_date1
VALUES (CURDATE()),(CURRENT_DATE()),(NOW());

SELECT *
FROM test_date1;

12.6.3、TIME类型

TIME类型用来表示时间,不包含日期部分。在MySQL中,需要 3个字节 的存储空间来存储TIME类型的数据,可以使用“HH:MM:SS”格式来表示TIME类型,其中,HH表示小时,MM表示分钟,SS表示秒。

在MySQL中,向TIME类型的字段插入数据时,也可以使用几种不同的格式。

  1. 可以使用带有冒号的字符串,比如'D HH:MM:SS' 、'HH:MM:SS'、'HH:MM'、'D HH:MM'、'D HH'或'SS'格式,都能被正确地插入TIME类型的字段中。其中D表示天,其最小值为0,最大值为34。如果使用带有D格式的字符串插入TIME类型的字段时,D会被转化为小时,计算格式为D*24+HH。当使用带有冒号并且不带D的字符串表示时间时,表示当天的时间,比如12:10表示12:10:00,而不是00:12:10。
  2. 可以使用不带有冒号的字符串或者数字,格式为' HHMMSS '或者 HHMMSS 。如果插入一个不合法的字符串或者数字,MySQL在存储数据时,会将其自动转化为00:00:00进行存储。比如1210,MySQL会将最右边的两位解析成秒,表示00:12:10,而不是12:10:00。
  3. 使用 CURRENT_TIME() 或者 NOW() ,会插入当前系统的时间。
#6.3 TIME类型
CREATE TABLE test_time1(
f1 TIME
);

DESC test_time1;

INSERT INTO test_time1
VALUES('2 12:30:29'), ('12:35:29'), ('12:40'), ('2 12:40'),('1 05'), ('45');

INSERT INTO test_time1
VALUES ('123520'), (124011),(1210);

INSERT INTO test_time1
VALUES (NOW()), (CURRENT_TIME()),(CURTIME());

SELECT *
FROM test_time1;

12.6.4、DATETIME类型

DATETIME类型在所有的日期时间类型中占用的存储空间最大,总共需要 8 个字节的存储空间。在格式上为DATE类型和TIME类型的组合,可以表示为 YYYY-MM-DD HH:MM:SS ,其中YYYY表示年份,MM表示月份,DD表示日期,HH表示小时,MM表示分钟,SS表示秒。

在向DATETIME类型的字段插入数据时,同样需要满足一定的格式条件。

  • YYYY-MM-DD HH:MM:SS 格式或者 YYYYMMDDHHMMSS 格式的字符串插入DATETIME类型的字段时,最小值为1000-01-01 00:00:00,最大值为9999-12-03 23:59:59。
    • YYYYMMDDHHMMSS格式的数字插入DATETIME类型的字段时,会被转化为YYYY-MM-DD HH:MM:SS格式。
  • YY-MM-DD HH:MM:SS 格式或者 YYMMDDHHMMSS 格式的字符串插入DATETIME类型的字段时,两位数的年份规则符合YEAR类型的规则,00到69表示2000到2069;70到99表示1970到1999。
  • 使用函数 CURRENT_TIMESTAMP() 和 NOW() ,可以向DATETIME类型的字段插入系统的当前日期和时间。
#6.4 DATETIME类型
CREATE TABLE test_datetime1(
dt DATETIME
);

INSERT INTO test_datetime1
VALUES ('2021-01-01 06:50:30'), ('20210101065030');

INSERT INTO test_datetime1
VALUES ('99-01-01 00:00:00'), ('990101000000'), ('20-01-01 00:00:00'), ('200101000000');

INSERT INTO test_datetime1
VALUES (20200101000000), (200101000000), (19990101000000), (990101000000);
 
INSERT INTO test_datetime1
VALUES (CURRENT_TIMESTAMP()), (NOW()),(SYSDATE());

SELECT *
FROM test_datetime1;

12.6.5、TIMESTAMP类型

TIMESTAMP类型也可以表示日期时间,其显示格式与DATETIME类型相同,都是 YYYY-MM-DD HH:MM:SS ,需要4个字节的存储空间。但是TIMESTAMP存储的时间范围比DATETIME要小很多,只能存储“1970-01-01 00:00:01 UTC”到“2038-01-19 03:14:07 UTC”之间的时间。其中,UTC表示世界统一时间,也叫作世界标准时间。

  • 存储数据的时候需要对当前时间所在的时区进行转换,查询数据的时候再将时间转换回当前的时区。因此,使用TIMESTAMP存储的同一个时间值,在不同的时区查询时会显示不同的时间。

向TIMESTAMP类型的字段插入数据时,当插入的数据格式满足YY-MM-DD HH:MM:SSYYMMDDHHMMSS时,两位数值的年份同样符合YEAR类型的规则条件,只不过表示的时间范围要小很多。

如果向TIMESTAMP类型的字段插入的时间超出了TIMESTAMP类型的范围,则MySQL会抛出错误信息

#6.5 TIMESTAMP类型
CREATE TABLE test_timestamp1(
ts TIMESTAMP
);

INSERT INTO test_timestamp1
VALUES ('1999-01-01 03:04:50'), ('19990101030405'), ('99-01-01 03:04:05'), ('990101030405');

INSERT INTO test_timestamp1
VALUES ('2020@01@01@00@00@00'), ('20@01@01@00@00@00');

INSERT INTO test_timestamp1
VALUES (CURRENT_TIMESTAMP()), (NOW());

#Incorrect datetime value
INSERT INTO test_timestamp1
VALUES ('2038-01-20 03:14:07');

SELECT *
FROM test_timestamp1;

#对比DATETIME 和 TIMESTAMP
CREATE TABLE temp_time(
d1 DATETIME,
d2 TIMESTAMP
);

INSERT INTO temp_time VALUES('2021-9-2 14:45:52','2021-9-2 14:45:52');

INSERT INTO temp_time VALUES(NOW(),NOW());

SELECT * FROM temp_time;

#修改当前的时区
SET time_zone = '+9:00';

SELECT * FROM temp_time;

TIMESTAMP和DATETIME的区别

TIMESTAMP存储空间比较小,表示的日期时间范围也比较小

底层存储方式不同,TIMESTAMP底层存储的是毫秒值,距离1970-1-1 0:0:0 0毫秒的毫秒值。

两个日期比较大小或日期计算时,TIMESTAMP更方便、更快。

TIMESTAMP和时区有关。TIMESTAMP会根据用户的时区不同,显示不同的结果。而DATETIME则只能反映出插入时当地的时区,其他时区的人查看数据必然会有误差的。

12.6.7、开发中的经验

用得最多的日期时间类型DATETIME,就是 DATETIME 。虽然 MySQL 也支持 YEAR(年)、 TIME(时间)、DATE(日期),以及 TIMESTAMP 类型,但是在实际项目中,尽量用 DATETIME 类型。因为这个数据类型包括了完整的日期和时间信息,取值范围也最大,使用起来比较方便。毕竟,如果日期时间信息分散在好几个字段,很不容易记,而且查询的时候,SQL 语句也会更加复杂。

此外,一般存注册时间、商品发布时间等,不建议使用DATETIME存储,而是使用 时间戳TIMESTAMP ,因为DATETIME虽然直观,但不便于计算。

12.7、文本字符串类型

MySQL中,文本字符串总体上分为 CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、ENUM、SET等类型。

MySQL中文本类型

12.7.1、CHAR类型

  • CHAR(M) 类型一般需要预先定义字符串长度。如果不指定(M),则表示长度默认是1个字符
  • 如果保存时,数据的实际长度比CHAR类型声明的长度小,则会在右侧填充空格以达到指定的长度。当MySQL检索CHAR类型的数据时,CHAR类型的字段会去除尾部的空格。
  • 定义CHAR类型字段时,声明的字段长度即为CHAR类型字段所占的存储空间的字节数。
#7.1 CHAR类型
CREATE TABLE test_char1(
c1 CHAR,
c2 CHAR(5)
);

DESC test_char1;

INSERT INTO test_char1(c1)
VALUES('a');

#Data too long for column 'c1' at row 1
INSERT INTO test_char1(c1)
VALUES('ab');

INSERT INTO test_char1(c2)
VALUES('ab');

INSERT INTO test_char1(c2)
VALUES('hello');

INSERT INTO test_char1(c2)
VALUES('尚');

INSERT INTO test_char1(c2)
VALUES('硅谷');

INSERT INTO test_char1(c2)
VALUES('尚硅谷教育');

#Data too long for column 'c2' at row 1
INSERT INTO test_char1(c2)
VALUES('尚硅谷IT教育');

SELECT * FROM test_char1;

SELECT CONCAT(c2,'***')
FROM test_char1;

INSERT INTO test_char1(c2)
VALUES('ab  ');

SELECT CHAR_LENGTH(c2)
FROM test_char1;

12.7.2、VARCHAR类型

  • VARCHAR(M) 定义时,必须指定长度M,否则报错。
  • MySQL4.0版本以下,varchar(20):指的是20字节,如果存放UTF8汉字时,只能存6个(每个汉字3字节,每个字母一个字节)
  • MySQL5.0版本以上,varchar(20):指的是20字符.
  • 检索VARCHAR类型的字段数据时,会保留数据尾部的空格。VARCHAR类型的字段所占用的存储空间为字符串实际长度加1个字节。
#7.2 VARCHAR类型
CREATE TABLE test_varchar1(
NAME VARCHAR  #错误
);

#Column length too big for column 'name' (max = 21845); use BLOB or TEXT instead
CREATE TABLE test_varchar2(
NAME VARCHAR(65535)
);

CREATE TABLE test_varchar3(
NAME VARCHAR(5)
);

INSERT INTO test_varchar3
VALUES('尚硅谷'),('尚硅谷教育');

#Data too long for column 'NAME' at row 1
INSERT INTO test_varchar3
VALUES('尚硅谷IT教育');

哪些情况使用CHAR或VARCHAR更好?

类型 特点 空间上 时间上 适用场景
CHAR(M) 固定长度 浪费存储空间 效率高 存储不大,速度要求高
VARCHAR(M) 可变长度 节省存储空间 效率低 非CHAR的情况
  • 情况1:存储很短的信息。比如门牌号码101,201……这样很短的信息应该用char,因为varchar还要占个byte用于存储信息长度,本来打算节约存储的,结果得不偿失。

  • 情况2:固定长度的。比如使用uuid作为主键,那用char应该更合适。因为他固定长度,varchar动态根据长度的特性就消失了,而且还要占个长度信息。

  • 情况3:十分频繁改变的column。因为varchar每次存储都要有额外的计算,得到长度等工作,如果一个非常频繁改变的,那就要有很多的精力用于计算,而这些对于char来说是不需要的。

  • 情况4:具体存储引擎中的情况:

    • MyISAM 数据存储引擎和数据列:MyISAM数据表,最好使用固定长度(CHAR)的数据列代替可变长度(VARCHAR)的数据列。这样使得整个表静态化,从而使 数据检索更快 ,用空间换时间。
    • MEMORY 存储引擎和数据列:MEMORY数据表目前都使用固定长度的数据行存储,因此无论使用CHAR或VARCHAR列都没有关系,两者都是作为CHAR类型处理的。
    • InnoDB 存储引擎建议使用VARCHAR类型。因为对于InnoDB数据表,内部的行存储格式并没有区分固定长度和可变长度列(所有数据行都使用指向数据列值的头指针),而且主要影响性能的因素是数据行使用的存储总量,由于char平均占用的空间多于varchar,所以除了简短并且固定长度的,其他考虑varchar。这样节省空间,对磁盘I/O和数据存储总量比较好。

12.7.3、TEXT类型

在MySQL中,TEXT用来保存文本类型的字符串,总共包含4种类型,分别为TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT类型。

在向TEXT类型的字段保存和查询数据时,系统自动按照实际长度存储,不需要预先定义长度。这一点和VARCHAR类型相同。

每种TEXT类型保存的数据长度和所占用的存储空间不同,如下:

文本字符串类型 特点 长度 长度范围 占用的存储空间
TINYTEXT 小文本、可变长度 L 0 <= L <= 255 L + 2 个字节
TEXT 文本、可变长度 L 0 <= L <= 65535 L + 2 个字节
MEDIUMTEXT 中等文本、可变长度 L 0 <= L <= 16777215 L + 3 个字节
LONGTEXT 大文本、可变长度 L 0 <= L<= 4294967295(相当于4GB) L + 4 个字节

由于实际存储的长度不确定,MySQL不允许TEXT类型的字段做主键。遇到这种情况,你只能采用CHAR(M),或者 VARCHAR(M)

#7.3 TEXT类型
CREATE TABLE test_text(
tx TEXT
);

INSERT INTO test_text
VALUES('atguigu   ');

SELECT CHAR_LENGTH(tx)
FROM test_text; #10

说明在保存和查询数据时,并没有删除TEXT类型的数据尾部的空格。

开发中经验:

TEXT文本类型,可以存比较大的文本段,搜索速度稍慢,因此如果不是特别大的内容,建议使用CHAR,VARCHAR来代替。还有TEXT类型不用加默认值,加了也没用。而且text和blob类型的数据删除后容易导致“空洞”,使得文件碎片比较多,所以频繁使用的表不建议包含TEXT类型字段,建议单独分出去,单独用一个表。

12.8、ENUM类型

ENUM类型也叫作枚举类型,ENUM类型的取值范围需要在定义字段时进行指定。设置字段值时,ENUM类型只允许从成员中选取单个值,不能一次选取多个值。

其所需要的存储空间由定义ENUM类型时指定的成员个数决定。

文本字符串类型 长度 长度范围 占用的存储空间
ENUM L 1 <= L <= 65535 1或2个字节
  • 当ENUM类型包含1~255个成员时,需要1个字节的存储空间;
  • 当ENUM类型包含256~65535个成员时,需要2个字节的存储空间。
  • ENUM类型的成员个数的上限为65535个。
#8. ENUM类型
CREATE TABLE test_enum(
season ENUM('春','夏','秋','冬','unknow')
);

INSERT INTO test_enum
VALUES('春'),('秋');

SELECT * FROM test_enum;

#Data truncated for column 'season' at row 1
INSERT INTO test_enum
VALUES('春,秋');
#Data truncated for column 'season' at row 1
INSERT INTO test_enum
VALUES('人');

INSERT INTO test_enum
VALUES('unknow');

#忽略大小写的
INSERT INTO test_enum
VALUES('UNKNOW');

#可以使用索引进行枚举元素的调用
INSERT INTO test_enum
VALUES(1),('3');

# 没有限制非空的情况下,可以添加nullINSERT INTO test_enum
VALUES (NULL);

12.9、SET类型

SET表示一个字符串对象,可以包含0个或多个成员,但成员个数的上限为 64 。设置字段值时,可以取

取值范围内的 0 个或多个值。

当SET类型包含的成员个数不同时,其所占用的存储空间也是不同的,具体如下

成员个数范围(L表示实际成员个数) 占用的存储空间
1 <= L <= 8 1个字节
9 <= L <= 16 2个字节
17 <= L <= 24 3个字节
25 <= L <= 32 4个字节
33 <= L <= 64 8个字节

SET类型在存储数据时成员个数越多,其占用的存储空间越大。注意:SET类型在选取成员时,可以一次选择多个成员,这一点与ENUM类型不同。

#9. SET类型
CREATE TABLE test_set(
s SET ('A', 'B', 'C')
);

INSERT INTO test_set (s) VALUES ('A'), ('A,B');

#插入重复的SET类型成员时,MySQL会自动删除重复的成员
INSERT INTO test_set (s) VALUES ('A,B,C,A');

#向SET类型的字段插入SET成员中不存在的值时,MySQL会抛出错误。
INSERT INTO test_set (s) VALUES ('A,B,C,D');

SELECT *
FROM test_set;

CREATE TABLE temp_mul(
gender ENUM('男','女'),
hobby SET('吃饭','睡觉','打豆豆','写代码')
);

INSERT INTO temp_mul
VALUES('男','睡觉,打豆豆');

SELECT * 
FROM temp_mul;

#Data truncated for column 'gender' at row 1
INSERT INTO temp_mul
VALUES('男,女','睡觉,打豆豆');

12.10、二进制字符串类型

MySQL中的二进制字符串类型主要存储一些二进制数据,比如可以存储图片、音频和视频等二进制数据。

MySQL中支持的二进制字符串类型主要包括BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB 和LONGBLOB类型。

BINARY与VARBINARY类型

BINARY和VARBINARY类似于CHAR和VARCHAR,只是它们存储的是二进制字符串。

  • BINARY(M)为固定长度的二进制字符串,M表示最多能存储的字节数,取值范围是0~255个字符。如果未指定(M),表示只能存储 1个字节 。例如BINARY (8),表示最多能存储8个字节,如果字段值不足(M)个字节,将在右边填充'\0'以补齐指定长度。
  • VARBINARY(M)为可变长度的二进制字符串,M表示最多能存储的字节数,总字节数不能超过行的字节长度限制65535,另外还要考虑额外字节开销,VARBINARY类型的数据除了存储数据本身外,还需要1或2个字节来存储数据的字节数。VARBINARY类型必须指定(M),否则报错
二进制字符串类型 特点 值的长度 占用空间
BINARY(M) 固定长度 M(0 <= M <= 255) M个字节
VARBINARY(M) 可变长度 M(0 <= M <= 65535) M+1个字节
#10.1 BINARYVARBINARY类型
CREATE TABLE test_binary1(
f1 BINARY,
f2 BINARY(3),
#f3 VARBINARY,
f4 VARBINARY(10)
);

DESC test_binary1;

INSERT INTO test_binary1(f1,f2)
VALUES('a','abc');

SELECT * FROM test_binary1;

#Data too long for column 'f1' at row 1
INSERT INTO test_binary1(f1)
VALUES('ab');

INSERT INTO test_binary1(f2,f4)
VALUES('ab','ab');

SELECT LENGTH(f2),LENGTH(f4)
FROM test_binary1;

BLOB类型

BLOB是一个 二进制大对象 ,可以容纳可变数量的数据。

MySQL中的BLOB类型包括TINYBLOBBLOBMEDIUMBLOBLONGBLOB4种类型,它们可容纳值的最大长度不同。可以存储一个二进制的大对象,比如图片、 音频和视频等。

需要注意的是,在实际工作中,往往不会在MySQL数据库中使用BLOB类型存储大对象数据,通常会将图片、音频和视频文件存储到服务器的磁盘上,并将图片、音频和视频的访问路径存储到MySQL中。

二进制字符串类型 值的长度 长度范围 占用空间
TINYBLOB L 0 <= L <= 255 L + 1 个字节
BLOB L 0 <= L <= 65535(相当于64KB) L + 2 个字节
MEDIUMBLOB L 0 <= L <= 16777215 (相当于16MB L + 3 个字节
LONGBLOB L 0 <= L <= 4294967295(相当于4GB) L + 4 个字节
#10.2 Blob类型
CREATE TABLE test_blob1(
id INT,
img MEDIUMBLOB
);

INSERT INTO test_blob1(id)
VALUES (1001);

SELECT *
FROM test_blob1;

TEXT和BLOB的使用注意事项

在使用text和blob字段类型时要注意以下几点,以便更好的发挥数据库的性能。

  • BLOB和TEXT值也会引起自己的一些问题,特别是执行了大量的删除或更新操作的时候。删除这种值会在数据表中留下很大的"空洞",以后填入这些"空洞"的记录可能长度不同。为了提高性能,建议定期使用 OPTIMIZE TABLE 功能对这类表进行碎片整理
  • 如果需要对大文本字段进行模糊查询,MySQL 提供了 前缀索引 。但是仍然要在不必要的时候避免检索大型的BLOB或TEXT值。例如,SELECT * 查询就不是很好的想法,除非你能够确定作为约束条件的WHERE子句只会找到所需要的数据行。否则,你可能毫无目的地在网络上传输大量的值。
  • 把BLOB或TEXT列分离到单独的表中。在某些环境中,如果把这些数据列移动到第二张数据表中,可以让你把原数据表中的数据列转换为固定长度的数据行格式,那么它就是有意义的。这会减少主表中的碎片,使你得到固定长度数据行的性能优势。它还使你在主数据表上运行 SELECT * 查询的时候不会通过网络传输大量的BLOB或TEXT值。

12.11、JSON类型

JSON(JavaScript Object Notation)是一种轻量级的 数据交换格式 。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。它易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。JSON 可以将JavaScript 对象中表示的一组数据转换为字符串,然后就可以在网络或者程序之间轻松地传递这个字符串,并在需要的时候将它还原为各编程语言所支持的数据格式。

在MySQL 5.7中,就已经支持JSON数据类型。在MySQL 8.x版本中,JSON类型提供了可以进行自动验证的JSON文档和优化的存储结构,使得在MySQL中存储和读取JSON类型的数据更加方便和高效。

#11. JSON类型
CREATE TABLE test_json(
js json);

INSERT INTO test_json (js) 
VALUES ('{"name":"songhk", "age":18, "address":{"province":"beijing", "city":"beijing"}}');

SELECT * FROM test_json;

SELECT js -> '$.name' AS NAME,js -> '$.age' AS age ,js -> '$.address.province' AS province, js -> '$.address.city' AS city
FROM test_json;

12.12、空间类型

MySQL 空间类型扩展支持地理特征的生成、存储和分析。这里的地理特征表示世界上具有位置的任何东西,可以是一个实体,例如一座山;可以是空间,例如一座办公楼;也可以是一个可定义的位置,例如一个十字路口等等。MySQL中使用 Geometry(几何) 来表示所有地理特征。Geometry指一个点或点的集合,代表世界上任何具有位置的事物。

MySQL的空间数据类型(Spatial Data Type)对应于OpenGIS类,包括单值类型:GEOMETRY、POINT、LINESTRING、POLYGON以及集合类型:MULTIPOINT、MULTILINESTRING、MULTIPOLYGON、GEOMETRYCOLLECTION 。

  • Geometry是所有空间集合类型的基类,其他类型如POINT、LINESTRING、POLYGON都是Geometry的子类。
    • Point,顾名思义就是点,有一个坐标值。例如POINT(121.213342 31.234532),POINT(30 10),坐标值支持DECIMAL类型,经度(longitude)在前,维度(latitude)在后,用空格分隔。
    • LineString,线,由一系列点连接而成。如果线从头至尾没有交叉,那就是简单的(simple);如果起点和终点重叠,那就是封闭的(closed)。例如LINESTRING(30 10,10 30,4040),点与点之间用逗号分隔,一个点中的经纬度用空格分隔,与POINT格式一致。

12.13、小结及选择建议

在定义数据类型时,如果确定是整数,就用INT;如果是小数,一定用定点数类型DECIMAL(M,D);如果是日期与时间,就用DATETIME。这样做的好处是,首先确保你的系统不会因为数据类型定义出错。不过,凡事都是有两面的,可靠性好,并不意味着高效。比如,TEXT虽然使用方便,但是效率不如 CHAR(M) 和 VARCHAR(M)。

关于字符串的选择,建议参考如下阿里巴巴的《Java开发手册》规范:

阿里巴巴《Java开发手册》之MySQL数据库:

任何字段如果为非负数,必须是 UNSIGNED

  • 【 强制 】小数类型为 DECIMAL,禁止使用 FLOAT 和 DOUBLE
    • 说明:在存储的时候,FLOAT 和 DOUBLE 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 DECIMAL 的范围,建议将数据拆成整数和小数并分开存储。
  • 【 强制 】如果存储的字符串长度几乎相等,使用 CHAR 定长字符串类型。
  • 【 强制 】VARCHAR是可变长字符串,不预先分配存储空间,长度不要超过5000。如果存储长度大于此值,定义字段类型为TEXT,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

13、约束

13.1、约束(constraint)概述

13.1.1、为什么需要约束

数据完整性(Data Integrity)是指数据的精确性(Accuracy)和可靠性(Reliability)。它是防止数据库中存在不符合语义规定的数据和防止因错误信息的输入输出造成无效操作或错误信息而提出的。

为了保证数据的完整性,SQL规范以约束的方式对表数据进行额外的条件限制。从以下四个方面考虑:

  • 实体完整性(Entity Integrity):例如,同一个表中,不能存在两条完全相同无法区分的记录
  • 域完整性(Domain Integrity):例如:年龄范围0-120,性别范围“男/女”
  • 引用完整性(Referential Integrity):例如:员工所在部门,在部门表中要能找到这个部门
  • 用户自定义完整性(User-defined Integrity):例如:用户名唯一、密码不能为空等,本部门经理的工资不得高于本部门职工的平均工资的5倍

13.1.2、什么是约束

约束是表级的强制规定。

可以在创建表时规定约束(通过 CREATE TABLE 语句),或者在表创建之后通过 ALTER TABLE 语句规定约束

13.1.3、约束的分类

  • 根据约束数据列的限制,约束可分为:

    • 单列约束:每个约束只约束一列

    • 多列约束:每个约束可约束多列数据

  • 根据约束的作用范围,约束可分为:

    • 列级约束:只能作用在一个列上,跟在列的定义后面

    • 表级约束:可以作用在多个列上,不与列一起,而是单独定义

  • 根据约束起的作用,约束可分为:

    • NOT NULL 非空约束,规定某个字段不能为空

    • UNIQUE 唯一性约束,规定某个字段在整个表中是唯一的

    • PRIMARY KEY 主键(非空且唯一)约束

    • FOREIGN KEY 外键约束

    • CHECK 检查约束,注意:MySQL不支持check约束,但可以使用check约束,而没有任何效果

    • DEFAULT 默认值约束

查看各表中的约束:

#information_schema数据库名(系统库)
#table_constraints表名称(专门存储各个表的约束)
SELECT * FROM information_schema.table_constraints
WHERE table_name = '表名称';

13.1.4、添加约束

  • CREATE TABLE时添加约束
  • ALTER TABLE时增加约束、删除约束

13.2、NOT NULL非空约束

使用NOT NULL关键字限制某个字段/列值不能为空。

关于非空约束的说明:

  • 默认,所有的类型的值都可以是NULL,包括INT、FLOAT等数据类型
  • 非空约束只能出现在表对象的列上,只能某个列单独限定非空,不能组合非空
  • 一个表可以有很多列都分别限定了非空
  • 空字符串''不等于NULL,0也不等于NULL
#3. not null (非空约束)
#3.1CREATE TABLE时添加约束
CREATE TABLE test1(
id INT NOT NULL,
last_name VARCHAR(15) NOT NULL,
email VARCHAR(25),
salary DECIMAL(10,2)

);

DESC test1;

INSERT INTO test1(id,last_name,email,salary)
VALUES(1,'Tom','tom@126.com',3400);

#错误:Column 'last_name' cannot be null
INSERT INTO test1(id,last_name,email,salary)
VALUES(2,NULL,'tom1@126.com',3400);

#错误:Column 'id' cannot be null
INSERT INTO test1(id,last_name,email,salary)
VALUES(NULL,'Jerry','jerry@126.com',3400);

INSERT INTO test1(id,email)
VALUES(2,'abc@126.com');

UPDATE test1
SET last_name = NULL
WHERE id = 1;

UPDATE test1
SET email = 'tom@126.com'
WHERE id = 1;

#3.2ALTER TABLE时添加约束
SELECT * FROM test1;
DESC test1;

ALTER TABLE test1
MODIFY email VARCHAR(25) NOT NULL;

#3.3ALTER TABLE时删除约束
ALTER TABLE test1
MODIFY email VARCHAR(25) NULL;

注意:在ALTER TABLE给某个字段添加非空约束时,使用MODIFY关键字修改且必须保证该列当前没有NULL值,否则添加操作失败。

13.3、UNIQUE唯一性约束

13.3.1、作用

使用UNIQUE关键字创建唯一性约束用来限制某个字段/某列的值不能重复。

13.3.2、特点

  • 同一个表可以有多个唯一约束。
  • 唯一约束可以是某一个列的值唯一,也可以多个列组合的值唯一。
  • 唯一性约束允许列值为空。
  • 在创建唯一约束的时候,如果不给唯一约束命名,就默认和列名相同。
  • MySQL会给唯一约束的列上默认创建一个唯一索引。

13.3.3、添加唯一性约束

情况1:在建表时添加唯一性约束

语法格式:

# 在字段后声明 列级唯一性约束
create table 表名称(
字段名 数据类型,
字段名 数据类型 unique,
字段名 数据类型 unique key,
字段名 数据类型
);
# 创建 表级唯一性约束
create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
[constraint 约束名] unique key(字段名)
)

情况2:建表后在ALTER TABLE中指定唯一性约束

语法格式:

#字段列表中如果是一个字段,表示该列的值唯一。如果是两个或更多个字段,那么复合唯一,即多个字段的组合是唯一的
#方式1alter table 表名称
add [constraint 表级约束名] unique key(字段列表)
#方式2alter table 表名称 modify 字段名 字段类型 unique;
#4. unique  (唯一性约束)
#4.1CREATE TABLE时添加约束
CREATE TABLE test2(
id INT UNIQUE, #列级约束
last_name VARCHAR(15) ,
email VARCHAR(25),
salary DECIMAL(10,2),
#表级约束
CONSTRAINT uk_test2_email UNIQUE(email)
);

DESC test2;

SELECT * FROM information_schema.table_constraints 
WHERE table_name = 'test2';

#在创建唯一约束的时候,如果不给唯一约束命名,就默认和列名相同。

INSERT INTO test2(id,last_name,email,salary)
VALUES(1,'Tom','tom@126.com',4500);

#错误:Duplicate entry '1' for key 'test2.id'
INSERT INTO test2(id,last_name,email,salary)
VALUES(1,'Tom1','tom1@126.com',4600);

#错误:Duplicate entry 'tom@126.com' for key 'test2.uk_test2_email'
INSERT INTO test2(id,last_name,email,salary)
VALUES(2,'Tom1','tom@126.com',4600);

#可以向声明为unique的字段上添加null值。而且可以多次添加null
INSERT INTO test2(id,last_name,email,salary)
VALUES(2,'Tom1',NULL,4600);

INSERT INTO test2(id,last_name,email,salary)
VALUES(3,'Tom2',NULL,4600);

SELECT * FROM test2;

#4.2ALTER TABLE时添加约束
DESC test2;

UPDATE test2
SET salary = 5000
WHERE id = 3;
#方式1ALTER TABLE test2
ADD CONSTRAINT uk_test2_sal UNIQUE(salary);
#方式2ALTER TABLE test2
MODIFY last_name VARCHAR(15) UNIQUE;

13.3.4、复合唯一性约束

语法格式:

create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
unique key(字段列表) #字段列表中写的是多个字段名,多个字段名用逗号分隔,表示那么是复合唯一,即多个字段的组合是唯一的
# 如果没有显式指定唯一性约束名,则默认约束名是字段列表中第一个字段名
);
#4.3 复合的唯一性约束
CREATE TABLE USER(
id INT,
`name` VARCHAR(15),
`password` VARCHAR(25),
#表级约束
CONSTRAINT uk_user_name_pwd UNIQUE(`name`,`password`)
);

INSERT INTO USER
VALUES(1,'Tom','abc');
#可以成功的:
INSERT INTO USER
VALUES(1,'Tom1','abc');

SELECT *
FROM USER;

13.3.5、删除唯一性约束

  • 添加唯一性约束的列上也会自动创建唯一索引。
  • 删除唯一约束只能通过删除唯一索引的方式删除。
  • 删除时需要指定唯一索引名,唯一索引名就和唯一约束名一样。
  • 如果创建唯一约束时未指定名称,如果是单列,就默认和列名相同;如果是组合列,那么默认和()中排在第一个的列名相同。也可以自定义唯一性约束名。
  • 查看都有哪些约束:SELECT * FROM information_schema.table_constraints WHERE table_name = '表名';

语法格式:

ALTER TABLE USER
DROP INDEX 唯一索引名;

注意:可以通过 show index from 表名称; 查看表的索引

13.4、PRIMARY KEY主键约束

一个表中只能有一个PRIMARY KEY主键约束

可以使用PRIMARY KEY关键字为表声明主键约束,主键约束用来唯一标识表中的一行记录。

主键约束相当于唯一约束+非空约束的组合,主键约束列不允许重复,不允许出现NULL值。

  • 一个表中有且只能有一个主键约束,建立主键约束可以在列级别创建,也可以在表级别上创建。
  • 主键约束对应着表中的一列或者多列(复合主键)
  • 如果是多列组合的复合主键约束,那么这些列都不允许为空值,并且组合的值不允许重复。
  • MySQL的主键名总是PRIMARY,就算自己命名了主键约束名也没用。
  • 当创建主键约束时,系统默认会在所在的列或列组合上建立对应的主键索引(能够根据主键查询的,就根据主键查询,效率更高)。如果删除主键约束了,主键约束对应的索引就自动删除了。(实际开发中,永远不会出现删除主键约束的操作)
  • 需要注意的一点是,不要修改主键字段的值。因为主键是数据记录的唯一标识,如果修改了主键的值,就有可能会破坏数据的完整性。

13.4.1、添加主键约束

情况1:建表时指定主键约束。语法格式:

create table 表名称(
字段名 数据类型 primary key, #列级模式
字段名 数据类型,
字段名 数据类型
);
create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
[constraint 约束名] primary key(字段名) #表级模式
);

情况2:建表后增加主键约束。语法格式:

ALTER TABLE 表名称
ADD PRIMARY KEY(字段列表); #字段列表可以是一个字段,也可以是多个字段,如果是多个字段的话,是复合主键
#5. primary key (主键约束)
#5.1CREATE TABLE时添加约束
#一个表中最多只能有一个主键约束。

#错误:Multiple primary key defined
CREATE TABLE test3(
id INT PRIMARY KEY, #列级约束
last_name VARCHAR(15) PRIMARY KEY,
salary DECIMAL(10,2),
email VARCHAR(25)
);

# 主键约束特征:非空且唯一,用于唯一的标识表中的一条记录。
CREATE TABLE test4(
id INT PRIMARY KEY, #列级约束
last_name VARCHAR(15),
salary DECIMAL(10,2),
email VARCHAR(25)
);

#MySQL的主键名总是PRIMARY,就算自己命名了主键约束名也没用。
CREATE TABLE test5(
id INT , 
last_name VARCHAR(15),
salary DECIMAL(10,2),
email VARCHAR(25),
#表级约束
CONSTRAINT pk_test5_id PRIMARY KEY(id)  #没有必要起名字。
);

SELECT * FROM information_schema.table_constraints 
WHERE table_name = 'test5';

INSERT INTO test4(id,last_name,salary,email)
VALUES(1,'Tom',4500,'tom@126.com');

#错误:Duplicate entry '1' for key 'test4.PRIMARY'
INSERT INTO test4(id,last_name,salary,email)
VALUES(1,'Tom',4500,'tom@126.com');

#错误:Column 'id' cannot be null
INSERT INTO test4(id,last_name,salary,email)
VALUES(NULL,'Tom',4500,'tom@126.com');

SELECT * FROM test4;

13.4.2、复合主键约束

复合主键约束:多个列组合构成主键,语法格式:

create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
primary key(字段名1,字段名2) #表示字段1和字段2的组合是唯一的,也可以有更多个字段
);
CREATE TABLE user1(
id INT,
NAME VARCHAR(15),
PASSWORD VARCHAR(25),

PRIMARY KEY (NAME,PASSWORD)

);
#如果是多列组合的复合主键约束,那么这些列都不允许为空值,并且组合的值不允许重复。
INSERT INTO user1
VALUES(1,'Tom','abc');

INSERT INTO user1
VALUES(1,'Tom1','abc');
#错误:Column 'name' cannot be null
INSERT INTO user1
VALUES(1,NULL,'abc');

SELECT * FROM user1;

#5.2ALTER TABLE时添加约束

CREATE TABLE test6(
id INT ,
last_name VARCHAR(15),
salary DECIMAL(10,2),
email VARCHAR(25)
);

DESC test6;

ALTER TABLE test6
ADD PRIMARY KEY (id);

#5.3 如何删除主键约束 (在实际开发中,不会去删除表中的主键约束!)
ALTER TABLE test6
DROP PRIMARY KEY;

说明:删除主键约束,不需要指定主键名,因为一个表只有一个主键,删除主键约束后,非空还存在。

13.5、AUTO_INCREMENT自增列

使用AUTO_INCREMENT关键字实现某个字段的值自增。

自增列的特点和要求:

  • 一个表最多只能有一个自增长列
  • 当需要产生唯一标识符或顺序值时,可设置自增长
  • 自增长列约束的列必须是键列(主键列,唯一键列)
  • 自增约束的列的数据类型必须是整数类型
  • 如果自增列指定了 0 和 null,会在当前最大值的基础上自增;如果自增列手动指定了具体值,直接赋值为具体值。

13.5.1、添加、删除自增约束

情况1:建表时

create table 表名称(
字段名 数据类型 primary key auto_increment,
字段名 数据类型 unique key not null,
字段名 数据类型 unique key,
字段名 数据类型 not null default 默认值,
);
create table 表名称(
字段名 数据类型 default 默认值 ,
字段名 数据类型 unique key auto_increment,
字段名 数据类型 not null default 默认值,,
primary key(字段名)
);

情况2:建表后

alter table 表名称
modify 字段名 数据类型 auto_increment;

# 删除自增约束
#alter table 表名称 modify 字段名 数据类型 auto_increment;#给这个字段增加自增约束
alter table 表名称 modify 字段名 数据类型; #去掉auto_increment相当于删除
#6. 自增长列:AUTO_INCREMENT
# 6.1CREATE TABLE时添加
CREATE TABLE test7(
id INT PRIMARY KEY AUTO_INCREMENT,
last_name VARCHAR(15) 
);
#开发中,一旦主键作用的字段上声明有AUTO_INCREMENT,则我们在添加数据时,就不要给主键
#对应的字段去赋值了。
INSERT INTO test7(last_name)
VALUES('Tom');

SELECT * FROM test7;

#当我们向主键(含AUTO_INCREMENT)的字段上添加0null时,实际上会自动的往上添加指定的字段的数值
INSERT INTO test7(id,last_name)
VALUES(0,'Tom');

INSERT INTO test7(id,last_name)
VALUES(NULL,'Tom');

INSERT INTO test7(id,last_name)
VALUES(10,'Tom');

INSERT INTO test7(id,last_name)
VALUES(-10,'Tom');

#6.2ALTER TABLE 时添加
CREATE TABLE test8(
id INT PRIMARY KEY ,
last_name VARCHAR(15) 
);

DESC test8;

ALTER TABLE test8
MODIFY id INT AUTO_INCREMENT;

#6.3ALTER TABLE 时删除
ALTER TABLE test8
MODIFY id INT ;

13.5.2、MySQL8.0新特性—自增变量持久化

在MySQL8.0之前,自增主键AUTO_INCREMENT的值如果大于max(primary key)+1,在MySQL重启后,会重置AUTO_INCREMENT=max(primary key)+1,这种现象在某些情况下会导致业务主键冲突或者其他难以发现的问题。

在MySQL 5.7系统中,对于自增主键的分配规则,是由InnoDB数据字典内部一个 计数器 来决定的,而该计数器只在 内存中维护 ,并不会持久化到磁盘中。当数据库重启时,该计数器会被初始化

MySQL 8.0将自增主键的计数器持久化到 重做日志 中。每次计数器发生改变,都会将其写入重做日志中。如果数据库重启,InnoDB会根据重做日志中的信息来初始化计数器的内存值

13.6、FOREIGN KEY约束

使用FOREIGN KEY关键字创建外键约束,限定某个表的某个字段的引用完整性。

比如:员工表的员工所在部门的选择,必须在部门表能找到对应的部分。

13.6.1、主表和从表、父表和子表

主表(父表):被引用的表,被参考的表

从表(子表):引用别人的表,参考别人的表

例如:员工表的员工所在部门这个字段的值要参考部门表:部门表是主表,员工表是从表。

例如:学生表、课程表、选课表:选课表的学生和课程要分别参考学生表和课程表,学生表和课程表是主表,选课表是从表。

13.6.2、特点

  1. 从表的外键列,必须引用/参考主表的主键或唯一约束的列,因为被依赖/被参考的值必须是唯一的
  2. 在创建外键约束时,如果不给外键约束命名,默认名不是列名,而是自动产生一个外键名(例如student_ibfk_1;),也可以指定外键约束名。
  3. 创建(CREATE)表时就指定外键约束的话,先创建主表,再创建从表
  4. 删表时,先删从表(或先删除外键约束),再删除主表
  5. 当主表的记录被从表参照时,主表的记录将不允许删除,如果要删除数据,需要先删除从表中依赖
  6. 该记录的数据,然后才可以删除主表的数据
  7. 在“从表”中指定外键约束,并且一个表可以建立多个外键约束
  8. 从表的外键列与主表被参照的列名字可以不相同,但是数据类型必须一样,逻辑意义一致。如果类型不一样,创建子表时,就会出现错误“ERROR 1005 (HY000): Can't createtable'database.tablename'(errno: 150)”。例如:都是表示部门编号,都是int类型。(
  9. 当创建外键约束时,系统默认会在所在的列上建立对应的普通索引。但是索引名是外键的约束名。(根据外键查询效率很高)
  10. 删除外键约束后,必须 手动 删除对应的索引

13.6.3、添加外键约束

情况1:建表时。语法格式:

create table 主表名称(
字段1 数据类型 primary key,
字段2 数据类型
);
create table 从表名称(
字段1 数据类型 primary key,
字段2 数据类型,
[CONSTRAINT <外键约束名称>] FOREIGN KEY(从表的某个字段) references 主表名(被参考字段)
);
#(从表的某个字段)的数据类型必须与主表名(被参考字段)的数据类型一致,逻辑意义也一样
#(从表的某个字段)的字段名可以与主表名(被参考字段)的字段名一样,也可以不一样
-- FOREIGN KEY: 在表级指定子表中的列
-- REFERENCES: 标示在父表中的列

情况2:建表后。语法格式:

ALTER TABLE 从表名 ADD [CONSTRAINT 约束名] FOREIGN KEY (从表的字段) REFERENCES 主表名(被引用
字段) [on update xx][on delete xx];
#7.foreign key (外键约束)
#7.1CREATE TABLE 时添加
#主表和从表;父表和子表
#①先创建主表
CREATE TABLE dept1(
dept_id INT,
dept_name VARCHAR(15)
);
#②再创建从表
CREATE TABLE emp1(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(15),
department_id INT,

#表级约束
CONSTRAINT fk_emp1_dept_id FOREIGN KEY (department_id) REFERENCES dept1(dept_id)
);

#上述操作报错,因为主表中的dept_id上没有主键约束或唯一性约束。
#③ 添加
ALTER TABLE dept1
ADD PRIMARY KEY (dept_id);

DESC dept1;

#④ 再创建从表
CREATE TABLE emp1(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(15),
department_id INT,

#表级约束
CONSTRAINT fk_emp1_dept_id FOREIGN KEY (department_id) REFERENCES dept1(dept_id)

);

DESC emp1;

SELECT * FROM information_schema.table_constraints 
WHERE table_name = 'emp1';

#7.2 演示外键的效果
#添加失败
INSERT INTO emp1
VALUES(1001,'Tom',10);

#
INSERT INTO dept1
VALUES(10,'IT');
#在主表dept1中添加了10号部门以后,我们就可以在从表中添加10号部门的员工
INSERT INTO emp1
VALUES(1001,'Tom',10);

#删除失败
DELETE FROM dept1
WHERE dept_id = 10;

#更新失败
UPDATE dept1
SET dept_id = 20
WHERE dept_id = 10;

#7.3ALTER TABLE时添加外键约束
CREATE TABLE dept2(
dept_id INT PRIMARY KEY,
dept_name VARCHAR(15)
);

CREATE TABLE emp2(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(15),
department_id INT
);

ALTER TABLE emp2
ADD CONSTRAINT fk_emp2_dept_id FOREIGN KEY(department_id) REFERENCES dept2(dept_id);

SELECT * FROM information_schema.table_constraints 
WHERE table_name = 'emp2';

总结:约束关系是针对双方的

  • 添加了外键约束后,主表的修改和删除数据受约束
  • 添加了外键约束后,从表的添加和修改数据受约束
  • 在从表上建立外键,要求主表必须存在
  • 删除主表时,要求从表从表先删除,或将从表中外键引用该主表的关系先删除

13.6.4、约束等级

  • Cascade方式 :在父表上update/delete记录时,同步update/delete掉子表的匹配记录
  • Set null方式 :在父表上update/delete记录时,将子表上匹配记录的列设为null,但是要注意子表的外键列不能为not null
  • No action方式 :如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作
  • Restrict方式 :同no action, 都是立即检查外键约束
  • Set default方式 (在可视化工具SQLyog中可能显示空白):父表有变更时,子表将外键列设置成一个默认的值,但Innodb不能识别

如果没有指定等级,就相当于Restrict方式。

对于外键约束,最好是采用: ON UPDATE CASCADE ON DELETE RESTRICT 的方式

# on update cascade on delete set null
CREATE TABLE dept(
    did INT PRIMARY KEY,		#部门编号
    dname VARCHAR(50)			#部门名称
);

CREATE TABLE emp(
    eid INT PRIMARY KEY,  #员工编号
    ename VARCHAR(5),     #员工姓名
    deptid INT,		  #员工所在的部门
    FOREIGN KEY (deptid) REFERENCES dept(did)  ON UPDATE CASCADE ON DELETE SET NULL
    #把修改操作设置为级联修改等级,把删除操作设置为set null等级
);

INSERT INTO dept VALUES(1001,'教学部');
INSERT INTO dept VALUES(1002, '财务部');
INSERT INTO dept VALUES(1003, '咨询部');

INSERT INTO emp VALUES(1,'张三',1001); #在添加这条记录时,要求部门表有1001部门
INSERT INTO emp VALUES(2,'李四',1001);
INSERT INTO emp VALUES(3,'王五',1002);

UPDATE dept
SET did = 1004
WHERE did = 1002;

DELETE FROM dept
WHERE did = 1004;

SELECT * FROM dept;
SELECT * FROM emp;
#结论:对于外键约束,最好是采用: `ON UPDATE CASCADE ON DELETE RESTRICT` 的方式。

13.6.5、删除外键约束

删除外键约束流程:

# 1.先查看约束名和删除外键约束
SELECT * FROM information_schema.table_constraints WHERE table_name = '表名称';#查看某个表的约束名
ALTER TABLE 从表名 DROP FOREIGN KEY 外键约束名;
# 2.查看索引名和删除索引。(注意,只能手动删除)
SHOW INDEX FROM 表名称; #查看某个表的索引名
ALTER TABLE 从表名 DROP INDEX 索引名;
#7.5 删除外键约束
#一个表中可以声明有多个外键约束
USE atguigudb;
SELECT * FROM information_schema.table_constraints 
WHERE table_name = 'employees';

USE dbtest13;

SELECT * FROM information_schema.table_constraints 
WHERE table_name = 'emp1';

#删除外键约束
ALTER TABLE emp1
DROP FOREIGN KEY fk_emp1_dept_id;

#再手动的删除外键约束对应的普通索引
SHOW INDEX FROM emp1;

ALTER TABLE emp1
DROP INDEX fk_emp1_dept_id;

13.6.6、开发场景

问题1:建和不建外键约束的区别:

答:建外键约束,操作(创建表、删除表、添加、修改、删除)会受到限制,从语法层面受到限制不建外键约束要保证数据的 引用完整性 ,只能依 靠程序员的自觉,或者是 在Java程序中进行限定。建立外键约束对查询语句没有影响。

在 MySQL 里,外键约束是有成本的,需要消耗系统资源。对于大并发的 SQL 操作,有可能会不适合。比如大型网站的中央数据库,可能会 因为外键约束的系统开销而变得非常慢 。所以, MySQL 允许你不使用系统自带的外键约束,在 应用层面 完成检查数据一致性的逻辑。也就是说,即使你不用外键约束,也要想办法通过应用层面的附加逻辑,来实现外键约束的功能,确保数据的一致性。

《阿里开发规范手册》:

【 强制 】不得使用外键与级联,一切外键概念必须在应用层解决

说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于 单机低并发 ,不适合 分布式 、 高并发集群级联更新是强阻塞存在数据库更新风暴 的风险外键影响数据库的 插入速度

13.7、CHECK约束

使用CHECK关键字实现检查约束,以限制某字段的值是否符合xx要求,一般是数值范围。

注意:MySQL5.7 可以使用check约束,但check约束对数据验证没有任何作用。添加数据时,没有任何错误或警告。但是MySQL 8.0中可以使用check约束了

CREATE TABLE test10(
id INT,
last_name VARCHAR(15),
salary DECIMAL(10,2) CHECK(salary > 2000)
);

INSERT INTO test10
VALUES(1,'Tom',2500);

#添加失败
INSERT INTO test10
VALUES(2,'Tom1',1500);

SELECT * FROM test10;

13.8、DEFAULT约束

使用DEFAULT关键字给某一列指定默认值约束。一旦设置默认值,在插入数据时,如果此字段没有显式赋值,则赋值为默认值。

13.8.1、添加默认值约束

语法格式:

# 建表时指定默认值约束
create table 表名称(
    字段名 数据类型 primary key,
    字段名 数据类型 unique key not null,
    字段名 数据类型 unique key,
    字段名 数据类型 not null default 默认值,
);
create table 表名称(
    字段名 数据类型 default 默认值 ,
    字段名 数据类型 not null default 默认值,
    字段名 数据类型 not null default 默认值,
    primary key(字段名),
    unique key(字段名)
);
# 说明:默认值约束一般不在唯一键和主键列上加
# 建表后修改
alter table 表名称
modify 字段名 数据类型 default 默认值;
#9.DEFAULT约束
#9.1CREATE TABLE添加约束
CREATE TABLE test11(
id INT,
last_name VARCHAR(15),
salary DECIMAL(10,2) DEFAULT 2000
);

DESC test11;

INSERT INTO test11(id,last_name,salary)
VALUES(1,'Tom',3000);

INSERT INTO test11(id,last_name)
VALUES(2,'Tom1');

SELECT * 
FROM test11;

#9.2ALTER TABLE添加约束
CREATE TABLE test12(
id INT,
last_name VARCHAR(15),
salary DECIMAL(10,2)
);

DESC test12;

ALTER TABLE test12
MODIFY salary DECIMAL(8,2) DEFAULT 2500;

13.8.2、删除默认值约束

语法格式:

alter table 表名称 modify 字段名 数据类型 ;#删除默认值约束,也不保留非空约束
alter table 表名称 modify 字段名 数据类型 not null; #删除默认值约束,保留非空约束
#9.3ALTER TABLE删除约束
ALTER TABLE test12
MODIFY salary DECIMAL(8,2);

SHOW CREATE TABLE test12;

13.9、相关面试问题

面试1、为什么建表时,加not null default ''default 0

答:不想让表中出现null值。

面试2、为什么不想要null的值

答:(1)不好比较。null是一种特殊值,比较时只能用专门的is null 和 is not null来比较。碰到运算符,通常返回null。

(2)效率不高。影响提高索引效果。因此,我们往往在建表时 not null default ''default 0

面试3、带AUTO_INCREMENT约束的字段值是从1开始的吗?

在MySQL中,默认AUTO_INCREMENT的初始值是1,每新增一条记录,字段值自动加1。设置自增属性(AUTO_INCREMENT)的时候,还可以指定第一条插入记录的自增字段的值,这样新插入的记录的自增字段值从初始值开始递增,如在表中插入第一条记录,同时指定id值为5,则以后插入的记录的id值就会从6开始往上增加。添加主键约束时,往往需要设置字段自动增加属性。

面试4、并不是每个表都可以任意选择存储引擎?

外键约束(FOREIGN KEY)不能跨引擎使用。MySQL支持多种存储引擎,每一个表都可以指定一个不同的存储引擎,需要注意的是:外键约束是用来保证数据的参照完整性的,如果表之间需要关联外键,却指定了不同的存储引擎,那么这些表之间是不能创建外键约束的。所以说,存储引擎的选择也不完全是随意的。

章节练习

#第13章_约束的课后练习
#练习1CREATE DATABASE test04_emp;

USE test04_emp;

CREATE TABLE emp2(
id INT,
emp_name VARCHAR(15)
);

CREATE TABLE dept2(
id INT,
dept_name VARCHAR(15)
);

#1.向表emp2的id列中添加PRIMARY KEY约束
ALTER TABLE emp2
ADD CONSTRAINT pk_emp2_id PRIMARY KEY(id);

#2.向表dept2的id列中添加PRIMARY KEY约束
ALTER TABLE dept2
ADD PRIMARY KEY(id);

#3.向表emp2中添加列dept_id,并在其中定义FOREIGN KEY约束,与之相关联的列是dept2表中的id列。
ALTER TABLE emp2
ADD dept_id INT;

DESC emp2;

ALTER TABLE emp2
ADD CONSTRAINT fk_emp2_deptid FOREIGN KEY(dept_id) REFERENCES dept2(id);

#练习2:
#承接《第11章_数据处理之增删改》的综合案例。
USE test01_library;
DESC books;

#根据题目要求给books表中的字段添加约束
#方式1ALTER TABLE books
ADD PRIMARY KEY (id);

ALTER TABLE books
MODIFY id INT AUTO_INCREMENT;
#方式2ALTER TABLE books
MODIFY id INT PRIMARY KEY AUTO_INCREMENT;

#针对于非id字段的操作
ALTER TABLE books
MODIFY NAME VARCHAR(50) NOT NULL;

ALTER TABLE books
MODIFY AUTHORS VARCHAR(100) NOT NULL;

ALTER TABLE books
MODIFY price FLOAT NOT NULL;

ALTER TABLE books
MODIFY pubdate YEAR NOT NULL;

ALTER TABLE books
MODIFY num INT NOT NULL;

#练习3:
#1. 创建数据库test04_company
CREATE DATABASE IF NOT EXISTS test04_company CHARACTER SET 'utf8';

USE test04_company;

#2. 按照下表给出的表结构在test04_company数据库中创建两个数据表offices和employees
CREATE TABLE IF NOT EXISTS offices(
officeCode INT(10) PRIMARY KEY ,
city VARCHAR(50) NOT NULL,
address VARCHAR(50) ,
country VARCHAR(50) NOT NULL,
postalCode VARCHAR(15),
CONSTRAINT uk_off_poscode UNIQUE(postalCode)
);

DESC offices;

CREATE TABLE employees(
employeeNumber INT PRIMARY KEY AUTO_INCREMENT,
lastName VARCHAR(50) NOT NULL,
firstName VARCHAR(50) NOT NULL,
mobile VARCHAR(25) UNIQUE,
officeCode INT(10) NOT NULL,
jobTitle VARCHAR(50) NOT NULL,
birth DATETIME NOT NULL,
note VARCHAR(255),
sex VARCHAR(5),
CONSTRAINT fk_emp_offcode FOREIGN KEY (officeCode) REFERENCES offices(officeCode)
);

DESC employees;

#3. 将表employees的mobile字段修改到officeCode字段后面
ALTER TABLE employees
MODIFY mobile VARCHAR(25) AFTER officeCode;

#4. 将表employees的birth字段改名为employee_birth
ALTER TABLE employees
CHANGE birth employee_birth DATETIME;

#5. 修改sex字段,数据类型为CHAR(1),非空约束
ALTER TABLE employees
MODIFY sex CHAR(1) NOT NULL;

#6. 删除字段note
ALTER TABLE employees
DROP COLUMN note;

#7. 增加字段名favoriate_activity,数据类型为VARCHAR(100)
ALTER TABLE employees
ADD favoriate_activity VARCHAR(100);

#8. 将表employees名称修改为employees_info
RENAME TABLE employees
TO employees_info;

#错误:Table 'test04_company.employees' doesn't exist
DESC employees;
DESC employees_info;

14、视图

14.1、常见的数据库对象

对象 描述
表(TABLE) 表是存储数据的逻辑单元,以行和列的形式存在,列就是字段,行就是记录
数据字典 就是系统表,存放数据库相关信息的表。系统表的数据通常由数据库系统维护,程序员通常不应该修改,只可查看
约束(CONSTRAINT) 执行数据校验的规则,用于保证数据完整性的规则
视图(VIEW) 一个或者多个数据表里的数据的逻辑显示,视图并不存储数据
索引(INDEX) 用于提高查询性能,相当于书的目录
存储过程(PROCEDURE) 用于完成一次完整的业务处理,没有返回值,但可通过传出参数将多个值传给调用环境
存储函数(FUNCTION) 用于完成一次特定的计算,具有一个返回值
触发器(TRIGGER) 相当于一个事件监听器,当数据库发生特定事件后,触发器被触发,完成相应的处理

14.2、视图的概述

14.2.1、为什么使用视图?

视图一方面可以帮我们使用表的一部分而不是所有的表,另一方面也可以针对不同的用户制定不同的查询视图。比如,针对一个公司的销售人员,我们只想给他看部分数据,而某些特殊的数据,比如采购的价格,则不会提供给他。再比如,人员薪酬是个敏感的字段,那么只给某个级别以上的人员开放,其他人的查询视图中则不提供这个字段

14.2.2、视图的理解

  • 视图是一种虚拟表,本身是不具有数据的,占用很少的内存空间,它是SQL中的一个重要概念。

  • 视图建立在已有表的基础上, 视图赖以建立的这些表称为基表

  • 视图的创建和删除只影响视图本身不影响对应的基表。但是当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化,反之亦然。

  • 向视图提供数据内容的语句为 SELECT 语句, 可以将视图理解为存储起来的 SELECT 语句

    • 在数据库中,视图不会保存数据,数据真正保存在数据表中。当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化;反之亦然。
  • 视图,是向用户提供基表数据的另一种表现形式。通常情况下,小型项目的数据库可以不使用视图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我们把经常查询的结果集放到虚拟表中,提升使用效率。理解和使用起来都非常方便。

14.3、创建视图

CREATE VIEW语句中创建视图。语法格式:

CREATE [OR REPLACE]
[ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
VIEW 视图名称 [(字段列表)]
AS 查询语句
[WITH [CASCADED|LOCAL] CHECK OPTION]

精简版:

CREATE VIEW 视图名
AS 查询语句;

14.3.1、创建基于单表视图

#2. 如何创建视图
#准备工作
CREATE DATABASE dbtest14;
USE dbtest14;

CREATE TABLE emps
AS
SELECT *
FROM atguigudb.`employees`;

CREATE TABLE depts
AS
SELECT *
FROM atguigudb.`departments`;

SELECT * FROM emps;
SELECT * FROM depts;
DESC emps;
DESC atguigudb.employees;

#2.1 针对于单表
#情况1:视图中的字段与基表的字段有对应关系
CREATE VIEW vu_emp1
AS
SELECT employee_id,last_name,salary
FROM emps;

SELECT * FROM vu_emp1;

#确定视图中字段名的方式1CREATE VIEW vu_emp2
AS
SELECT employee_id emp_id,last_name lname,salary #查询语句中字段的别名会作为视图中字段的名称出现
FROM emps
WHERE salary > 8000;

#确定视图中字段名的方式2CREATE VIEW vu_emp3(emp_id,NAME,monthly_sal) #小括号内字段个数与SELECT中字段个数相同
AS
SELECT employee_id,last_name,salary 
FROM emps
WHERE salary > 8000;

SELECT * FROM vu_emp3;

#情况2:视图中的字段在基表中可能没有对应的字段
CREATE VIEW vu_emp_sal
AS
SELECT department_id,AVG(salary) avg_sal
FROM emps
WHERE department_id IS NOT NULL
GROUP BY department_id;

SELECT * FROM vu_emp_sal;

说明1:实际上就是我们在 SQL 查询语句的基础上封装了视图 VIEW,这样就会基于 SQL 语句的结果集形成一张虚拟表。

说明2:在创建视图时,没有在视图名后面指定字段列表,则视图中字段列表默认和SELECT语句中的字段列表一致。如果SELECT语句中给字段取了别名,那么视图中的字段名和别名相同。

14.3.2、创建多表联合视图

#2.2 针对于多表
CREATE VIEW vu_emp_dept
AS
SELECT e.employee_id,e.department_id,d.department_name
FROM emps e JOIN depts d
ON e.`department_id` = d.`department_id`;

SELECT * FROM vu_emp_dept;

#利用视图对数据进行格式化
CREATE VIEW vu_emp_dept1
AS
SELECT CONCAT(e.last_name,'(',d.department_name,')') emp_info
FROM emps e JOIN depts d
ON e.`department_id` = d.`department_id`;

SELECT * FROM vu_emp_dept1;

14.3.3、基于视图创建视图

当我们创建好一张视图之后,还可以在它的基础上继续创建视图。

#2.3 基于视图创建视图
CREATE VIEW vu_emp4
AS
SELECT employee_id,last_name
FROM vu_emp1;

SELECT * FROM vu_emp4;

14.4、查看视图

语法1:查看数据库的表对象、视图对象

SHOW TABLES;

语法2:查看视图的结构

DESC / DESCRIBE 视图名称;

语法3:查看视图的属性信息

# 查看视图信息(显示数据表的存储引擎、版本、数据行数和数据大小等)
SHOW TABLE STATUS LIKE '视图名称'(\G 在DOS窗口中查看);

执行结果显示,注释Comment为VIEW,说明该表为视图,其他的信息为NULL,说明这是一个虚表。

语法4:查看视图的详细定义信息

SHOW CREATE VIEW 视图名称;
#3. 查看视图
# 语法1:查看数据库的表对象、视图对象
SHOW TABLES;

#语法2:查看视图的结构
DESCRIBE vu_emp1;

#语法3:查看视图的属性信息
SHOW TABLE STATUS LIKE 'vu_emp1';

#语法4:查看视图的详细定义信息
SHOW CREATE VIEW vu_emp1;

14.5、更新视图的数据

14.5.1、一般情况

MySQL支持使用INSERT、UPDATE和DELETE语句对视图中的数据进行插入、更新和删除操作。当视图中的数据发生变化时,数据表中的数据也会发生变化,反之亦然。

#4."更新"视图中的数据
#4.1 一般情况,可以更新视图的数据
SELECT * FROM vu_emp1;

SELECT employee_id,last_name,salary
FROM emps;
#更新视图的数据,会导致基表中数据的修改
UPDATE vu_emp1
SET salary = 20000
WHERE employee_id = 101;

#同理,更新表中的数据,也会导致视图中的数据的修改
UPDATE emps
SET salary = 10000
WHERE employee_id = 101;

#删除视图中的数据,也会导致表中的数据的删除
DELETE FROM vu_emp1
WHERE employee_id = 101;

SELECT employee_id,last_name,salary
FROM emps
WHERE employee_id = 101;

14.5.2、不可更新的视图

要使视图可更新,视图中的行和底层基本表中的行之间必须存在 一对一 的关系。另外当视图定义出现如下情况时,视图不支持更新操作:

  • 在定义视图的时候指定了“ALGORITHM = TEMPTABLE”,视图将不支持INSERT和DELETE操作
  • 视图中不包含基表中所有被定义为非空又未指定默认值的列,视图将不支持INSERT操作;
  • 在定义视图的SELECT语句中使用了JOIN联合查询视图将不支持INSERT和DELETE操作
  • 在定义视图的SELECT语句后的字段列表中使用了数学表达式或子查询视图将不支持INSERT,也不支持UPDATE使用了数学表达式、子查询的字段值;
  • 在定义视图的SELECT语句后的字段列表中使用DISTINCT、聚合函数、GROUP BY、HAVING、UNION等,视图将不支持INSERT、UPDATE、DELETE;
  • 在定义视图的SELECT语句中包含了子查询,而子查询中引用了FROM后面的表,视图将不支持INSERT、UPDATE、DELETE;
  • 视图定义基于一个不可更新视图
  • 常量视图
#4.2 不能更新视图中的数据
SELECT * FROM vu_emp_sal;

#更新失败
UPDATE vu_emp_sal
SET avg_sal = 5000 # 对使用了聚合函数的列更新失败
WHERE department_id = 30;

#删除失败
DELETE FROM vu_emp_sal
WHERE department_id = 30;

虽然可以更新视图数据,但总的来说,视图作为 虚拟表 ,主要用于 方便查询 ,不建议更新视图的数据。对视图数据的更改,都是通过对实际数据表里数据的操作来完成的。

14.6、修改、删除视图

14.6.1、修改视图

方式1:使用CREATE OR REPLACE VIEW子句修改视图。语法格式:

CREATE OR REPLACE VIEW 视图名
AS
查询语句;

说明:CREATE VIEW子句中各列的别名应和查询语句中各列相对应。

方式2:ALTER VIEW。语法格式:

ALTER VIEW 视图名称
AS
查询语句;
#5. 修改视图
DESC vu_emp1;

#方式1
CREATE OR REPLACE VIEW vu_emp1
AS
SELECT employee_id,last_name,salary,email
FROM emps
WHERE salary > 7000;

#方式2
ALTER VIEW vu_emp1
AS 
SELECT employee_id,last_name,salary,email,hire_date
FROM emps;

14.6.2、删除视图

  • 删除视图只是删除视图的定义,并不会删除基表的数据。
  • 删除视图的语法是:
DROP VIEW IF EXISTS 视图名称;
DROP VIEW IF EXISTS 视图名称1,视图名称2,视图名称3,...;

说明:基于视图a、b创建了新的视图c,如果将视图a或者视图b删除,会导致视图c的查询失败。这样的视图c需要手动删除或修改,否则影响使用

14.7、总结

14.7.1、视图优点

1. 操作简单

将经常使用的查询操作定义为视图,可以使开发人员不需要关心视图对应的数据表的结构、表与表之间的关联关系,也不需要关心数据表之间的业务逻辑和查询条件,而只需要简单地操作视图即可,极大简化了开发人员对数据库的操作。

2. 减少数据冗余

视图跟实际数据表不一样,它存储的是查询语句。所以,在使用的时候,我们要通过定义视图的查询语句来获取结果集。而视图本身不存储数据,不占用数据存储的资源,减少了数据冗余

3. 数据安全

MySQL将用户对数据的访问限制在某些数据的结果集上,而这些数据的结果集可以使用视图来实现。用户不必直接查询或操作数据表。这也可以理解为视图具有 隔离性 。视图相当于在用户和实际的数据表之间加了一层虚拟表。

同时,MySQL可以根据权限将用户对数据的访问限制在某些视图上,用户不需要查询数据表,可以直接通过视图获取数据表中的信息。这在一定程度上保障了数据表中数据的安全性。

4. 适应灵活多变的需求

当业务系统的需求发生变化后,如果需要改动数据表的结构,则工作量相对较大,可以使用视图来减少改动的工作量。这种方式在实际工作中使用得比较多。

5. 能够分解复杂的查询逻辑

数据库中如果存在复杂的查询逻辑,则可以将问题进行分解,创建多个视图获取数据,再将创建的多个视图结合起来,完成复杂的查询逻辑。

14.7.2、视图缺点

如果我们在实际数据表的基础上创建了视图,那么,如果实际数据表的结构变更了,我们就需要及时对相关的视图进行相应的维护。特别是嵌套的视图(就是在视图的基础上创建视图),维护会变得比较复杂,可读性不好,容易变成系统的潜在隐患。因为创建视图的 SQL 查询可能会对字段重命名,也可能包含复杂的逻辑,这些都会增加维护的成本。实际项目中,如果视图过多,会导致数据库维护成本的问题。所以,在创建视图的时候,你要结合实际项目需求,综合考虑视图的优点和不足,这样才能正确使用视图,使系统整体达到最优。

章节练习

#第14章_视图的课后练习
USE dbtest14;
#练习1:
#1. 使用表emps创建视图employee_vu,
#其中包括姓名(LAST_NAME),员工号(EMPLOYEE_ID),部门号(DEPARTMENT_ID)
CREATE VIEW employee_vu
AS
SELECT last_name,employee_id,department_id
FROM emps;

SHOW TABLES;

#2. 显示视图的结构
DESCRIBE employee_vu;

#3. 查询视图中的全部内容
SELECT * FROM employee_vu;

#4. 将视图中的数据限定在部门号是80的范围内
CREATE OR REPLACE VIEW employee_vu(NAME,emp_id,dept_id)
AS
SELECT last_name,employee_id,department_id
FROM emps
WHERE department_id <= 80;

#练习2DESC emps;

#1. 创建视图emp_v1,要求查询电话号码以‘011’开头的员工姓名和工资、邮箱
CREATE OR REPLACE VIEW emp_v1
AS
SELECT last_name,salary,email
FROM emps
WHERE phone_number LIKE '011%';

SELECT * FROM emp_v1;

#2. 要求将视图 emp_v1 修改为查询电话号码以‘011’开头的并且邮箱中包含 e 字符
#的员工姓名和邮箱、电话号码
CREATE OR REPLACE VIEW emp_v1
AS
SELECT last_name,email,phone_number
FROM emps
WHERE phone_number LIKE '011%' AND email LIKE '%e%';

SELECT * FROM emp_v1;

#3. 向 emp_v1 插入一条记录,是否可以?
DESC emps;

# 实测:失败了
INSERT INTO emp_v1
VALUES('Tom','tom@126.com','01012345');

#4. 修改emp_v1中员工的工资,每人涨薪1000
SELECT *
FROM emp_v1;

UPDATE emp_v1
SET salary = salary + 1000; # 更新失败,原因:不能更改视图中不存在的字段

#5. 删除emp_v1中姓名为Olsen的员工
DELETE FROM emp_v1
WHERE last_name = 'Olsen';

#6. 创建视图emp_v2,要求查询部门的最高工资高于 12000 的部门id和其最高工资
CREATE OR REPLACE VIEW emp_v2
AS
SELECT department_id,MAX(salary)
FROM emps
GROUP BY department_id
HAVING MAX(salary) > 12000;

SELECT * FROM emp_v2;

#7. 向 emp_v2 中插入一条记录,是否可以?
#不可以!
#错误:The target table emp_v2 of the INSERT is not insertable-into
INSERT INTO emp_v2(dept_id,max_sal)
VALUES(4000,20000);

#8. 删除刚才的emp_v2 和 emp_v1
DROP VIEW IF EXISTS emp_v2,emp_v1;

15、存储过程与函数

MySQL从5.0版本开始支持存储过程和函数。存储过程和函数能够将复杂的SQL逻辑封装在一起,应用程序无须关注存储过程和函数内部复杂的SQL逻辑,而只需要简单地调用存储过程和函数即可。

15.1、存储过程概述

15.1.1、理解

含义存储过程(Stored Procedure)。它的思想很简单,就是一组经过预先编译的SQL语句的封装

执行过程:存储过程预先存储在MySQL服务器上,需要执行的时候,客户端只需要向服务器端发出调用存储过程的命令,服务器端就可以把预先存储好的这一系列SQL语句全部执行。

好处

  1. 简化操作,提高了sql语句的重用性,减少了开发程序员的压力
  2. 减少操作过程中的失误,提高效率
  3. 减少网络传输量(客户端不需要把所有的 SQL 语句通过网络发给服务器)
  4. 减少了 SQL 语句暴露在网上的风险,也提高了数据查询的安全性

和视图、函数的对比

它和视图有着同样的优点,清晰、安全,还可以减少网络传输量。不过它和视图不同,视图是 虚拟表 ,通常不对底层数据表直接操作,而存储过程是程序化的 SQL,可以直接操作底层数据表 ,相比于面向集合的操作方式,能够实现一些更复杂的数据处理。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。相较于函数,存储过程是没有返回值的。

15.1.2、分类

存储过程的参数类型可以是IN、OUT和INOUT。根据这点分类如下:

  1. 没有参数(无参数无返回)
  2. 仅仅带 IN 类型(有参数无返回)
  3. 仅仅带 OUT 类型(无参数有返回)
  4. 既带 IN 又带 OUT(有参数有返回)
  5. 带 INOUT(有参数有返回)

注意:IN、OUT、INOUT 都可以在一个存储过程中带多个。

15.2、创建存储过程

语法格式:

CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名 参数类型,...)
[characteristics ...]
BEGIN
存储过程体
END

说明:

  1. 参数前面的符号的意思

IN:当前参数为输入参数,也就是表示入参;存储过程只是读取这个参数的值。如果没有定义参数种类,默认就是IN,表示输入参数。

OUT:当前参数为输出参数,也就是表示出参;执行完成之后,调用这个存储过程的客户端或者应用程序就可以读取这个参数返回的值了。

INOUT:当前参数既可以为输入参数,也可以为输出参数。

  1. 形参类型可以是MySQL数据库中的任意类型。

  2. characteristics 表示创建存储过程时指定的对存储过程的约束条件,其取值信息如下:

    LANGUAGE SQL
    | [NOT] DETERMINISTIC
    | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
    | SQL SECURITY { DEFINER | INVOKER }
    | COMMENT 'string'
    
  • LANGUAGE SQL:说明存储过程执行体是由SQL语句组成的,当前系统支持的语言为SQL。
  • [NOT] DETERMINISTIC:指明存储过程执行的结果是否确定。DETERMINISTIC表示结果是确定的。每次执行存储过程时,相同的输入会得到相同的输出。NOT DETERMINISTIC表示结果是不确定的,相同的输入可能得到不同的输出。如果没有指定任意一个值,默认为NOT DETERMINISTIC。
  • {CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA}:指明子程序使用SQL语句的限制。
    • CONTAINS SQL表示当前存储过程的子程序包含SQL语句,但是并不包含读写数据的SQL语句;
    • NO SQL表示当前存储过程的子程序中不包含任何SQL语句;
    • READS SQL DATA表示当前存储过程的子程序中包含读数据的SQL语句;
    • MODIFIES SQL DATA表示当前存储过程的子程序中包含写数据的SQL语句。
    • 默认情况下,系统会指定为CONTAINS SQL
  • SQL SECURITY { DEFINER | INVOKER }:执行当前存储过程的权限,即指明哪些用户能够执行当前存储过程。
    • DEFINER 表示只有当前存储过程的创建者或者定义者才能执行当前存储过程;
    • INVOKER 表示拥有当前存储过程的访问权限的用户能够执行当前存储过程。
    • 如果没有设置相关的值,则MySQL默认指定值为DEFINER。
  • COMMENT 'string':注释信息,可以用来描述存储过程。
  1. 存储过程体中可以有多条 SQL 语句,如果仅仅一条SQL 语句,则可以省略 BEGIN 和 END。

编写存储过程并不是一件简单的事情,可能存储过程中需要复杂的 SQL 语句。

1. BEGINENDBEGINEND 中间包含了多个语句,每个语句都以(;)号为结束符。
2. DECLAREDECLARE 用来声明变量,使用的位置在于 BEGINEND 语句中间,而且需要在其他语句使用之前进
行变量的声明。
3. SET:赋值语句,用于对变量进行赋值。
4. SELECTINTO:把从数据表中查询的结果存放到变量中,也就是为变量赋值。
  1. 需要设置新的结束标记
DELIMITER 新的结束标记

因为MySQL默认的语句结束符号为分号‘;’。为了避免与存储过程中SQL语句结束符相冲突,需要使用DELIMITER改变存储过程的结束符。

比如:“DELIMITER //”语句的作用是将MySQL的结束符设置为//,并以“END //”结束存储过程。存储过程定义完毕之后再使用“DELIMITER ;”恢复默认结束符。DELIMITER也可以指定其他符号作为结束符。

当使用DELIMITER命令时,应该避免使用反斜杠(‘\’)字符,因为反斜线是MySQL的转义字符。

所以,创建存储过程的完整语法格式:

DELIMITER $
CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名 参数类型,...)
[characteristics ...]
BEGIN
sql语句1;
sql语句2;
END $
DELIMITER ;
#第15章_存储过程与存储函数
#0.准备工作
CREATE DATABASE dbtest15;
USE dbtest15;

CREATE TABLE employees
AS
SELECT * 
FROM atguigudb.`employees`;

CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

SELECT * FROM employees;
SELECT * FROM departments;

#1. 创建存储过程
#类型1:无参数无返回值
#举例1:创建存储过程select_all_data(),查看 employees 表的所有数据
DELIMITER $
CREATE PROCEDURE select_all_data()
    BEGIN
        SELECT * FROM employees;
    END $
DELIMITER ;

#2. 存储过程的调用
CALL select_all_data();

#举例2:创建存储过程avg_employee_salary(),返回所有员工的平均工资
DELIMITER //
CREATE PROCEDURE avg_employee_salary()
    BEGIN 
        SELECT AVG(salary) FROM employees;
    END //
DELIMITER ;
#调用
CALL avg_employee_salary();

#举例3:创建存储过程show_max_salary(),用来查看“emps”表的最高薪资值。
DELIMITER //
CREATE PROCEDURE show_max_salary()
    BEGIN
        SELECT MAX(salary)
        FROM employees;
    END //
DELIMITER ;
#调用
CALL show_max_salary();

#类型2:带 OUT
#举例4:创建存储过程show_min_salary(),查看“emps”表的最低薪资值。并将最低薪资
#通过OUT参数“ms”输出
DESC employees;

DELIMITER //
CREATE PROCEDURE show_min_salary(OUT ms DOUBLE)
    BEGIN
        SELECT MIN(salary) INTO ms
        FROM employees;
    END //
DELIMITER ;

#调用
CALL show_min_salary(@ms);
#查看变量值
SELECT @ms;

#类型3:带 IN
#举例5:创建存储过程show_someone_salary(),查看“emps”表的某个员工的薪资,
#并用IN参数empname输入员工姓名。
DELIMITER //
CREATE PROCEDURE show_someone_salary(IN empname VARCHAR(20))
BEGIN
	SELECT salary FROM employees
	WHERE last_name = empname;
END //
DELIMITER ;

#调用方式1
CALL show_someone_salary('Abel');
#调用方式2
SET @empname := 'Abel';
CALL show_someone_salary(@empname);
SELECT * FROM employees WHERE last_name = 'Abel';

#类型4:带 INOUT
#举例6:创建存储过程show_someone_salary2(),查看“emps”表的某个员工的薪资,
#并用IN参数empname输入员工姓名,用OUT参数empsalary输出员工薪资。
DELIMITER //
CREATE PROCEDURE show_someone_salary2(IN empname VARCHAR(20),OUT empsalary DECIMAL(10,2))
BEGIN
	SELECT salary INTO empsalary
	FROM employees
	WHERE last_name = empname;
END //
DELIMITER ;

#调用
SET @empname = 'Abel'; # 创建用户变量
CALL show_someone_salary2(@empname,@empsalary);
SELECT @empsalary; # 查看变量值

#类型5:带 INOUT
#举例7:创建存储过程show_mgr_name(),查询某个员工领导的姓名,并用INOUT参数“empname”输入员工姓名,
#输出领导的姓名。
DESC employees;

DELIMITER $
CREATE PROCEDURE show_mgr_name(INOUT empname VARCHAR(25))
    BEGIN
        SELECT last_name INTO empname
        FROM employees
        WHERE employee_id = (
                                SELECT manager_id
                                FROM employees
                                WHERE last_name = empname
                    		);

    END $
DELIMITER ;

#调用
SET @empname := 'Abel';
CALL show_mgr_name(@empname);

SELECT @empname;

15.3、调用存储过程

存储过程有多种调用方法。存储过程必须使用CALL语句调用,并且存储过程和数据库相关,如果要执行其他数据库中的存储过程,需要指定数据库名称,例如CALL dbname.procname。

CALL 存储过程名(实参列表)

15.3.1、语法格式

  1. 调用IN模式的参数:
CALL sp1('值');
  1. 调用out模式的参数
SET @name;
CALL sp1(@name);
SELECT @name;
  1. 调用inout模式的参数:
SET @name=值;
CALL sp1(@name);
SELECT @name;

注意:如果用的是Navicat工具,那么在编写存储过程的时候,Navicat会自动设置DELIMITER为其他符号,不需要再进行DELIMITER的操作。

15.3.2、如何调试

在MySQL中,存储过程不像普通的编程语言(比如 VC++、Java 等)那样有专门的集成开发环境。因此,你可以通过SELECT语句,把程序执行的中间结果查询出来,来调试一个SQL语句的正确性。调试成功之后,把SELECT语句后移到下一个SQL语句之后,再调试下一个SQL语句。这样逐步推进 ,就可以完成对存储过程中所有操作的调试了。当然,你也可以把存储过程中的SQL语句复制出来,逐段单独调试。

15.4、函数的使用

15.4.1、语法格式

CREATE FUNCTION 函数名(参数名 参数类型,...)
RETURNS 返回值类型
[characteristics ...]
    BEGIN
    函数体 #函数体中肯定有 RETURN 语句
    END

说明:

  1. 参数列表:指定参数为IN、OUT或INOUT只对PROCEDURE是合法的,FUNCTION中总是默认为IN参数。
  2. RETURNS type 语句表示函数返回数据的类型;RETURNS子句只能对FUNCTION做指定,对函数而言这是 强制 的。它用来指定函数的返回类型,而且函数体必须包含一个 RETURN value 语句。
  3. characteristic 创建函数时指定的对函数的约束。取值与创建存储过程时相同,这里不再赘述。
  4. 函数体也可以用BEGIN…END来表示SQL代码的开始和结束。如果函数体只有一条语句,也可以省略BEGIN…END。

15.4.2、调用函数

在MySQL中,存储函数的使用方法与MySQL内部函数的使用方法是一样的。换言之,用户自己定义的存储函数与MySQL内部函数是一个性质的。区别在于,存储函数是用户自己定义的,而内部函数是MySQL的开发者定义的。调用存储函数的语法格式:

SELECT 函数名(实参列表)

注意:若在创建存储函数中报错“you might want to use the less safelog_bin_trust_function_creators variable”,有两种处理方法:

  1. 方式1:加上必要的函数特性[NOT] DETERMINISTIC”和“{CONTAINS SQL | NO SQL | READS SQL DATA |MODIFIES SQL DATA}”。
  2. 方式2:在创建存储函数前,执行语句SET GLOBAL log_bin_trust_function_creators = 1;确保能够成功创建存储函数
#2.存储函数
# 举例1:创建存储函数,名称为email_by_name(),参数定义为空,
#该函数查询Abel的email,并返回,数据类型为字符串型。
DELIMITER //
CREATE FUNCTION email_by_name()
RETURNS VARCHAR(25)
	DETERMINISTIC
	CONTAINS SQL
	READS SQL DATA
    BEGIN
        RETURN (SELECT email FROM employees WHERE last_name = 'Abel');
    END //
DELIMITER ;

#调用
SELECT email_by_name();
SELECT email,last_name FROM employees WHERE last_name = 'Abel';

#举例2:创建存储函数,名称为email_by_id(),参数传入emp_id,该函数查询emp_id的email,
#并返回,数据类型为字符串型。
#创建函数前执行此语句,保证函数的创建会成功
SET GLOBAL log_bin_trust_function_creators = 1;

#声明函数
DELIMITER //
CREATE FUNCTION email_by_id(emp_id INT)
RETURNS VARCHAR(25)
    BEGIN
        RETURN (SELECT email FROM employees WHERE employee_id = emp_id);
    END //
DELIMITER ;

#调用
SELECT email_by_id(101);
SET @emp_id := 102;
SELECT email_by_id(@emp_id);

#举例3:创建存储函数count_by_id(),参数传入dept_id,该函数查询dept_id部门的
#员工人数,并返回,数据类型为整型。
DELIMITER //
CREATE FUNCTION count_by_id(dept_id INT)
RETURNS INT
    BEGIN
        RETURN (SELECT COUNT(*) FROM employees WHERE department_id = dept_id);
    END //
DELIMITER ;

#调用
SET @dept_id := 50;
SELECT count_by_id(@dept_id);

15.4.3、对比函数和存储过程

关键字 调用语法 返回值 应用场景
存储过程 PROCEDURE CALL 存储过程名(实参列表) 理解为有0个或多个(OUT/INOUT可以看作返回值) 一般用于更新
存储函数 FUNCTION SELECT 存储函数名(实参列表) 有且仅有一个 一般用于查询结果为一个值并返回时

此外,存储函数可以放在查询语句中使用,存储过程不行。反之,存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。

15.5、存储过程和函数的查看、修改、删除

15.5.1、查看

创建完之后,怎么知道我们创建的存储过程、存储函数是否成功了呢?

MySQL存储了存储过程和函数的状态信息,用户可以使用SHOW STATUS语句或SHOW CREATE语句来查看,也可直接从系统的information_schema数据库中查询。这里介绍3种方法。

1. 使用SHOW CREATE语句查看存储过程和函数的创建信息

基本语法结构如下:

SHOW CREATE {PROCEDURE | FUNCTION} 存储过程名或函数名;

2. 使用SHOW STATUS语句查看存储过程和函数的状态信息

基本语法结构如下:

SHOW {PROCEDURE | FUNCTION} STATUS [LIKE 'pattern']

这个语句返回子程序的特征,如数据库、名字、类型、创建者及创建和修改日期。

[LIKE 'pattern']:匹配存储过程或函数的名称,可以省略。当省略不写时,会列出MySQL数据库中存在的所有存储过程或函数的信息。

mysql> SHOW PROCEDURE STATUS LIKE 'SELECT%' \G
*************************** 1. row ***************************
Db: test_db
Name: SelectAllData
Type: PROCEDURE
Definer: root@localhost
Modified: 2021-10-16 15:55:07
Created: 2021-10-16 15:55:07
Security_type: DEFINER
Comment:
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)

3. 从information_schema.Routines表中查看存储过程和函数的信息

MySQL中存储过程和函数的信息存储在information_schema数据库下的Routines表中。可以通过查询该表的记录来查询存储过程和函数的信息。其基本语法形式如下:

SELECT * FROM information_schema.Routines
WHERE ROUTINE_NAME='存储过程或函数的名' [AND ROUTINE_TYPE = {'PROCEDURE|FUNCTION'}];

说明:如果在MySQL数据库中存在存储过程和函数名称相同的情况,最好指定ROUTINE_TYPE查询条件来指明查询的是存储过程还是函数。

15.5.2、修改

修改存储过程或函数,不影响存储过程或函数功能,只是修改相关特性。使用ALTER语句实现。

ALTER {PROCEDURE | FUNCTION} 存储过程或函数的名 [characteristic ...]

其中,characteristic指定存储过程或函数的特性,其取值信息与创建存储过程、函数时的取值信息略有不同。

{ CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string'
  • CONTAINS SQL,表示子程序包含SQL语句,但不包含读或写数据的语句。
  • NO SQL,表示子程序中不包含SQL语句。
  • READS SQL DATA,表示子程序中包含读数据的语句。
  • MODIFIES SQL DATA,表示子程序中包含写数据的语句。
  • SQL SECURITY { DEFINER | INVOKER },指明谁有权限来执行。
  • DEFINER,表示只有定义者自己才能够执行。
  • INVOKER,表示调用者可以执行。
  • COMMENT 'string',表示注释信息。

修改存储过程使用ALTER PROCEDURE语句,修改存储函数使用ALTER FUNCTION语句。但是,这两个语句的结构是一样的,语句中的所有参数也是一样的。

15.5.3、删除

删除存储过程和函数,可以使用DROP语句,其语法结构如下:

DROP {PROCEDURE | FUNCTION} [IF EXISTS] 存储过程或函数的名

IF EXISTS:如果程序或函数不存储,它可以防止发生错误,产生一个用SHOW WARNINGS查看的警告。

#3. 存储过程、存储函数的查看
#方式1. 使用SHOW CREATE语句查看存储过程和函数的创建信息
SHOW CREATE PROCEDURE show_mgr_name;
SHOW CREATE FUNCTION count_by_id;

#方式2. 使用SHOW STATUS语句查看存储过程和函数的状态信息
SHOW PROCEDURE STATUS;
SHOW PROCEDURE STATUS LIKE 'show_max_salary';
SHOW FUNCTION STATUS LIKE 'email_by_id';

#方式3.从information_schema.Routines表中查看存储过程和函数的信息
SELECT * FROM information_schema.Routines
WHERE ROUTINE_NAME='email_by_id' AND ROUTINE_TYPE = 'FUNCTION';

SELECT * FROM information_schema.Routines
WHERE ROUTINE_NAME='show_min_salary' AND ROUTINE_TYPE = 'PROCEDURE';
#4.存储过程、函数的修改
ALTER PROCEDURE show_max_salary
SQL SECURITY INVOKER
COMMENT '查询最高工资';

#5. 存储过程、函数的删除
DROP FUNCTION IF EXISTS count_by_id;
DROP PROCEDURE IF EXISTS show_min_salary;

15.6、存储过程使用的争议

15.6.1、优点

  1. 存储过程可以一次编译多次使用存储过程只在创建时进行编译,之后的使用都不需要重新编译,这就提升了SQL的执行效率。
  2. 可以减少开发工作量。将代码 封装 成模块,实际上是编程的核心思想之一,这样可以把复杂的问题拆解成不同的模块,然后模块之间可以 重复使用 ,在减少开发工作量的同时,还能保证代码的结构清晰。
  3. 存储过程的安全性强。我们在设定存储过程的时候可以 设置对用户的使用权限 ,这样就和视图一样具有较强的安全性。
  4. 可以减少网络传输量。因为代码封装到存储过程中,每次使用只需要调用存储过程即可,这样就减少了网络传输量。
  5. 良好的封装性。在进行相对复杂的数据库操作时,原本需要使用一条一条的 SQL 语句,可能要连接多次数据库才能完成的操作,现在变成了一次存储过程,只需要连接一次即可 。

15.6.2、缺点

阿里开发规范

强制禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

  1. 可移植性差。存储过程不能跨数据库移植,比如在 MySQL、Oracle和 SQL Server里编写的存储过程,在换成其他数据库时都需要重新编写。
  2. 调试困难。只有少数 DBMS 支持存储过程的调试。对于复杂的存储过程来说,开发和维护都不容易。虽然也有一些第三方工具可以对存储过程进行调试,但要收费。
  3. 存储过程的版本管理很困难。比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。
  4. 不适合高并发的场景。高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。

章节练习

#第15章_存储过程与存储函数的课后练习
#0.准备工作
CREATE DATABASE test15_pro_func;
USE test15_pro_func;

#1. 创建存储过程insert_user(),实现传入用户名和密码,插入到admin表中
CREATE TABLE admin(
	id INT PRIMARY KEY AUTO_INCREMENT,
	user_name VARCHAR(25) NOT NULL,
	pwd VARCHAR(25) NOT NULL
);

DELIMITER //
CREATE PROCEDURE insert_user(IN user_name VARCHAR(25),IN pwd VARCHAR(25))
BEGIN
  INSERT INTO admin(user_name,pwd)
  VALUES(user_name,pwd);
END //
DELIMITER ;

#调用
CALL insert_user('Tom','abc123');
SELECT * FROM admin;

#2. 创建存储过程get_phone(),实现传入女神编号,返回女神姓名和女神电话
CREATE TABLE beauty(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(15) NOT NULL,
phone VARCHAR(15) UNIQUE,
birth DATE
);

INSERT INTO beauty(NAME,phone,birth)
VALUES
('朱茵','13201233453','1982-02-12'),
('孙燕姿','13501233653','1980-12-09'),
('田馥甄','13651238755','1983-08-21'),
('邓紫棋','17843283452','1991-11-12'),
('刘若英','18635575464','1989-05-18'),
('杨超越','13761238755','1994-05-11');

SELECT * FROM beauty;

DELIMITER //
CREATE PROCEDURE get_phone(IN id INT,OUT NAME VARCHAR(15),OUT phone VARCHAR(15))
BEGIN
  SELECT b.name,b.phone INTO NAME,phone
  FROM beauty b
  WHERE b.id = id;
END //
DELIMITER ;

#调用
CALL get_phone(3,@name,@phone);
SELECT @name,@phone;

#3. 创建存储过程date_diff(),实现传入两个女神生日,返回日期间隔大小
DELIMITER //
CREATE PROCEDURE date_diff(IN birth1 DATE,IN birth2 DATE,OUT sub_date INT)
BEGIN
	SELECT DATEDIFF(birth1,birth2) INTO sub_date;
END //
DELIMITER ;

#调用
SET @birth1 := '1992-01-01';
SET @birth2 := '1996-10-24';
CALL date_diff(@birth1,@birth2,@result);
SELECT @result;

#4. 创建存储过程format_date(),实现传入一个日期,格式化成xx年xx月xx日并返回
DELIMITER //
CREATE PROCEDURE format_date(IN mydate DATE,OUT fmt_date VARCHAR(25))
BEGIN
	SELECT DATE_FORMAT(mydate,'%y年%m月%d日') INTO fmt_date;
END //
DELIMITER ;

#调用
CALL format_date('2000-06-20',@get_date);
SELECT @get_date;

#5. 创建存储过程beauty_limit(),根据传入的起始索引和条目数,查询女神表的记录
DELIMITER $
CREATE PROCEDURE beauty_limit(IN start_index INT,IN records INT)
BEGIN
	SELECT * FROM beauty
	LIMIT records OFFSET start_index;
END $
DELIMITER ;

#调用
CALL beauty_limit(3,2);

#创建带inout模式参数的存储过程
#6. 传入a和b两个值,最终a和b都翻倍并返回
DELIMITER //
CREATE PROCEDURE multiply(INOUT a INT,INOUT b INT)
BEGIN
	SET a = a * 2;
	SET b = b * 2;
END //
DELIMITER ;

#调用
SET @a := 2;
SET @b := 6;
CALL multiply(@a,@b);
SELECT @a,@b;

#7. 删除题目5的存储过程
DROP PROCEDURE IF EXISTS beauty_limit;

#8. 查看题目6中存储过程的信息
SHOW CREATE PROCEDURE multiply;

#存储函数的练习
#0. 准备工作
USE test15_pro_func;

CREATE TABLE employees
AS
SELECT * FROM atguigudb.`employees`;

CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

SET GLOBAL log_bin_trust_function_creators = 1;

#无参有返回
#1. 创建函数get_count(),返回公司的员工个数
DELIMITER //
CREATE FUNCTION get_count() RETURNS INT
BEGIN
  RETURN (SELECT COUNT(*) FROM employees);
END //
DELIMITER ;

#调用
SELECT get_count();

#有参有返回
#2. 创建函数ename_salary(),根据员工姓名,返回它的工资
DESC employees;
DELIMITER //
CREATE FUNCTION ename_salary(emp_name VARCHAR(25)) RETURNS DOUBLE(8,2)
BEGIN
	RETURN(SELECT salary FROM employees WHERE last_name = emp_name);
END //
DELIMITER ;

#调用
SELECT ename_salary('Abel');
SET @ename := 'Gee';
SELECT ename_salary(@ename);

#3. 创建函数dept_sal() ,根据部门名,返回该部门的平均工资
DELIMITER //
CREATE FUNCTION dept_sal(dept_name VARCHAR(30)) RETURNS DOUBLE(8,2)
BEGIN
	RETURN (
		SELECT AVG(salary)
		FROM employees e JOIN departments d
		ON e.department_id = d.department_id
		WHERE d.department_name = dept_name
	);
END //
DELIMITER ;

#调用
SELECT dept_sal('Marketing');

#4. 创建函数add_float(),实现传入两个float,返回二者之和
DELIMITER //
CREATE FUNCTION add_float(value1 FLOAT,value2 FLOAT) RETURNS FLOAT
BEGIN
	RETURN (SELECT value1 + value2);
END //
DELIMITER ;

# 调用
SET @v1 := 12.2;
SET @v2 = 2.3;
SELECT add_float(@v1,@v2);

16、变量、流程控制与游标

在MySQL数据库的存储过程和函数中,可以使用变量来存储查询或计算的中间结果数据,或者输出最终的结果数据。

在MySQL数据库中,变量分为系统变量以及用户自定义变量

16.1、变量

16.1.1、系统变量

系统变量的分类

变量由系统定义,不是用户定义,属于服务器层面启动MySQL服务,生成MySQL服务实例期间,MySQL将为MySQL服务器内存中的系统变量赋值,这些系统变量定义了当前MySQL服务实例的属性、特征。这些系统变量的值要么是 编译MySQL时参数 的默认值,要么是 配置文件 (例如my.ini等)中的参数值。可以通过网址 https://dev.mysql.com/doc/refman/8.0/en/server-systemvariables.html 查看MySQL文档的系统变量。

系统变量分为全局系统变量(需要添加 global 关键字)以及会话系统变量(需要添加 session 关键字),有时也把全局系统变量简称为全局变量,有时也把会话系统变量称为local变量。如果不写,默认会话级别。静态变量(在MySQL服务实例运行期间它们的值不能使用set动态修改)属于特殊的全局系统变量。

每一个MySQL客户机成功连接MySQL服务器后,都会产生与之对应的会话。会话期间,MySQL服务实例会在MySQL服务器内存中生成与该会话对应的会话系统变量,这些会话系统变量的初始值是全局系统变量值的复制。

  • 全局系统变量针对于所有会话(连接)有效,但不能跨重启
  • 会话系统变量仅针对于当前会话(连接)有效。会话期间,当前会话对某个会话系统变量值的修改,不会影响其他会话同一个会话系统变量的值。
  • 会话1对某个全局系统变量值的修改会导致会话2中同一个全局系统变量值的修改。

在MySQL中有些系统变量只能是全局的,例如 max_connections 用于限制服务器的最大连接数;有些系统变量作用域既可以是全局又可以是会话,例如 character_set_client 用于设置客户端的字符集;有些系统变量的作用域只能是当前会话,例如 pseudo_thread_id 用于标记当前会话的 MySQL 连接 ID。

查看系统变量

作为 MySQL 编码规范,MySQL中的系统变量以 两个“@” 开头,其中“@@global”仅用于标记全局系统变量,“@@session”仅用于标记会话系统变量。“@@首先标记会话系统变量,如果会话系统变量不存在,则标记全局系统变量

查看系统变量的语法格式:

#查看所有全局变量
SHOW GLOBAL VARIABLES;
#查看所有会话变量
SHOW SESSION VARIABLES;
# 或
SHOW VARIABLES;

#查看满足条件的部分系统变量。
SHOW GLOBAL VARIABLES LIKE '%标识符%';
#查看满足条件的部分会话变量
SHOW SESSION VARIABLES LIKE '%标识符%';

#查看指定的系统变量的值
SELECT @@global.变量名;
#查看指定的会话变量的值
SELECT @@session.变量名;
#或者
SELECT @@变量名;
#1.3 查看指定系统变量
SELECT @@global.max_connections;
SELECT @@global.character_set_client;
#错误: SELECT @@global.pseudo_thread_id;
#错误: SELECT @@session.max_connections;
SELECT @@session.character_set_client; # 查看客户端的字符集
SELECT @@session.pseudo_thread_id;
SELECT @@character_set_client; #先查询会话系统变量,再查询全局系统变量

修改系统变量的值

有些时候,数据库管理员需要修改系统变量的默认值,以便修改当前会话或者MySQL服务实例的属性、特征。具体方法:

方式1:修改MySQL配置文件 ,继而修改MySQL系统变量的值(该方法需要重启MySQL服务

方式2:在MySQL服务运行期间使用“set”命令重新设置系统变量的值

#为某个系统变量赋值
#方式1SET @@global.变量名=变量值;
#方式2SET GLOBAL 变量名=变量值;
#为某个会话变量赋值
#方式1SET @@session.变量名=变量值;
#方式2SET SESSION 变量名=变量值;
#1.4 修改系统变量的值
#全局系统变量:
#方式1SET @@global.max_connections = 161;
#方式2SET GLOBAL max_connections = 171;
#针对于当前的数据库实例是有效的,一旦重启mysql服务,就失效了。

#会话系统变量:
#方式1SET @@session.character_set_client = 'gbk';
#方式2SET SESSION character_set_client = 'gbk';
#针对于当前会话是有效的,一旦结束会话,重新建立起新的会话,就失效了。

16.1.2、用户变量

用户变量是用户自己定义的,作为MySQL编码规范,MySQL中的用户变量以 一个“@”开头。根据作用范围不同,又分为会话用户变量局部变量

  • 会话用户变量:作用域和会话变量一样,只对当前连接会话有效
  • 局部变量:只在 BEGIN 和 END 语句块中有效。局部变量只能在存储过程和函数中使用

16.1.2.1、会话用户变量

  • 变量的定义
#方式1:“=”或“:=SET @用户变量 = 值;
SET @用户变量 := 值;
#方式2:“:=” 或 INTO关键字
SELECT @用户变量 := 表达式 [FROM 等子句];
SELECT 表达式 INTO @用户变量 [FROM 等子句];
  • 查看用户变量的值(查看、比较、运算等)
SELECT @用户变量
#准备工作
CREATE DATABASE dbtest16;
USE dbtest16;

CREATE TABLE employees
AS
SELECT * FROM atguigudb.`employees`;

CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

SELECT * FROM employees;
SELECT * FROM departments;

#测试:
#方式1SET @m1 = 1;
SET @m2 := 2;
SET @sum := @m1 + @m2;
SELECT @sum;

#方式2SELECT @count := COUNT(*) FROM employees;
SELECT @count;
SELECT AVG(salary) INTO @avg_sal FROM employees;
SELECT @avg_sal;
SELECT @big; #查看某个未声明的变量时,将得到NULL

16.1.2.2、局部变量

  • 定义:可以使用DECLARE语句定义一个局部变量
  • 作用域:仅仅在定义它的 BEGIN ... END 中有效
  • 位置:只能放在 BEGIN ... END 中,而且只能放在第一句
BEGIN
    #声明局部变量
    DECLARE 变量名1 变量数据类型 [DEFAULT 变量默认值];
    DECLARE 变量名2,变量名3,... 变量数据类型 [DEFAULT 变量默认值];
    #为局部变量赋值
    SET 变量名1 = 值;
    SELECTINTO 变量名2 [FROM 子句];
    #查看局部变量的值
    SELECT 变量1,变量2,变量3;
END

1. 定义变量

DECLARE 变量名 类型 [default 值]; # 如果没有DEFAULT子句,初始值为NULL

2. 变量赋值

方式1:一般用于赋简单的值

SET 变量名=值;
SET 变量名:=值;

方式2:一般用于赋表中的字段值

SELECT 字段名或表达式 INTO 变量名 FROM 表;

3. 使用变量

SELECT 局部变量名;
作用域 定义位置 语法
会话用户变量 当前会话 会话的任何地方 加@符,不用指定变量类型
局部变量 定义它的存储过程或函数中 BEGIN END中第一句话 一般不加@,需要指定变量类型
#举例:
DELIMITER //
CREATE PROCEDURE test_var()
    BEGIN
        #1、声明局部变量
        DECLARE a INT DEFAULT 0;
        DECLARE b INT ;
        #DECLARE a,b INT DEFAULT 0;
        DECLARE emp_name VARCHAR(25);
        #2、赋值
        SET a = 1;
        SET b := 2;
        SELECT last_name INTO emp_name FROM employees WHERE employee_id = 101;
        #3、使用
        SELECT a,b,emp_name;
    END //
DELIMITER ;

#调用存储过程
CALL test_var();

#举例1:声明局部变量,并分别赋值为employees表中employee_id为102的last_name和salary
DELIMITER //
CREATE PROCEDURE test_pro()
    BEGIN
        #声明
        DECLARE emp_name VARCHAR(25);
        DECLARE sal DOUBLE(10,2) DEFAULT 0.0;
        #赋值
        SELECT last_name,salary INTO emp_name,sal
        FROM employees
        WHERE employee_id = 102;
        #使用
        SELECT emp_name,sal;
    END //
DELIMITER ;

#调用存储过程
CALL test_pro();

SELECT last_name,salary FROM employees
WHERE employee_id = 102;

#举例2:声明两个变量,求和并打印 (分别使用会话用户变量、局部变量的方式实现)
#方式1:使用会话用户变量
SET @v1 = 10;
SET @v2 := 20;
SET @result := @v1 + @v2;

#查看
SELECT @result;

#方式2:使用局部变量
DELIMITER //
CREATE PROCEDURE add_value()
    BEGIN
        #声明
        DECLARE value1,value2,sum_val INT;
        #赋值
        SET value1 = 10;
        SET value2 := 100;
        SET sum_val = value1 + value2;
        #使用
        SELECT sum_val;
    END //
DELIMITER ;

#调用存储过程
CALL add_value();

#举例3:创建存储过程“different_salary”查询某员工和他领导的薪资差距,并用IN参数emp_id接收员工id,用OUT参数dif_salary输出薪资差距结果。
DELIMITER //
CREATE PROCEDURE different_salary(IN emp_id INT,OUT dif_salary DOUBLE)
    BEGIN
        #分析:查询出emp_id员工的工资;查询出emp_id员工的管理者的id;查询管理者id的工资;计算两个工资的差值
        #声明变量
        DECLARE emp_sal DOUBLE DEFAULT 0.0; #记录员工的工资
        DECLARE mgr_sal DOUBLE DEFAULT 0.0; #记录管理者的工资
        DECLARE mgr_id INT DEFAULT 0; #记录管理者的id
        #赋值
        SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
        SELECT manager_id INTO mgr_id FROM employees WHERE employee_id = emp_id;
        SELECT salary INTO mgr_sal FROM employees WHERE employee_id = mgr_id;
        SET dif_salary = mgr_sal - emp_sal;
    END //
DELIMITER ;

#调用存储过程
SET @emp_id := 103;
SET @dif_sal := 0;
CALL different_salary(@emp_id,@dif_sal);

SELECT @dif_sal;
SELECT * FROM employees;

16.2、定义条件与处理程序

定义条件是事先定义程序执行过程中可能遇到的问题,处理程序定义了在遇到问题时应当采取的处理方式,并且保证存储过程或函数在遇到警告或错误时能继续执行。这样可以增强存储程序处理问题的能力,避免程序异常停止运行。

说明:定义条件和处理程序在存储过程、存储函数中都是支持的。

16.2.1、定义条件

定义条件就是给MySQL中的错误码命名,这有助于存储的程序代码更清晰。它将一个 错误名字 和指定的错误条件 关联起来。这个名字可以随后被用在定义处理程序的DECLARE HANDLER语句中。

定义条件使用DECLARE语句,语法格式如下:

DECLARE 错误名称 CONDITION FOR 错误码(或错误条件)

错误码的说明:

  • MySQL_error_code 和 sqlstate_value 都可以表示MySQL的错误。

    • MySQL_error_code是数值类型错误代码

    • sqlstate_value是长度为5的字符串类型错误代码

      如:在ERROR 1418 (HY000)中,1418是MySQL_error_code,'HY000'是sqlstate_value。

#举例1:定义“Field_Not_Be_NULL”错误名与MySQL中违反非空约束的错误类型
#是“ERROR 1048 (23000)”对应。
#方式1:使用MySQL_error_code
DECLARE Field_Not_Be_NULL CONDITION FOR 1048;

#方式2:使用sqlstate_value
DECLARE Field_Not_Be_NULL CONDITION FOR SQLSTATE '23000';

#举例2:定义"ERROR 1148(42000)"错误,名称为command_not_allowed。
#方式1:使用MySQL_error_code
DECLARE command_not_allowed CONDITION FOR 1148;

#方式2:使用sqlstate_value
DECLARE command_not_allowed CONDITION FOR SQLSTATE '42000';

16.2.2、定义处理程序

可以为SQL执行过程中发生的某种类型的错误定义特殊的处理程序。定义处理程序时,使用DECLARE语句的语法如下:

DECLARE 处理方式 HANDLER FOR 错误类型 处理语句;

说明:

  • 处理方式:处理方式有3个取值:CONTINUE、EXIT、UNDO。
    • CONTINUE:表示遇到错误不处理,继续执行。
    • EXIT:表示遇到错误马上退出。
    • UNDO:表示遇到错误后撤回之前的操作。MySQL中暂时不支持这样的操作。
  • 错误类型(即条件)可以有如下取值:
    • SQLSTATE '字符串错误码':表示长度为5的sqlstate_value类型的错误代码;
    • MySQL_error_code:匹配数值类型错误代码;
    • 错误名称:表示DECLARE ... CONDITION定义的错误条件名称。
    • SQLWARNING:匹配所有以01开头的SQLSTATE错误代码;
    • NOT FOUND:匹配所有以02开头的SQLSTATE错误代码;
    • SQLEXCEPTION:匹配所有没有被SQLWARNING或NOT FOUND捕获的SQLSTATE错误代码。
  • 处理语句:如果出现上述条件之一,则采用对应的处理方式,并执行指定的处理语句。语句可以是像SET 变量 = 值这样的简单语句,也可以是使用 BEGIN ... END 编写的复合语句。
#举例:
#方法1:捕获sqlstate_value
DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @info = 'NO_SUCH_TABLE';

#方法2:捕获mysql_error_value
DECLARE CONTINUE HANDLER FOR 1146 SET @info = 'NO_SUCH_TABLE';

#方法3:先定义条件,再调用
DECLARE no_such_table CONDITION FOR 1146;
DECLARE CONTINUE HANDLER FOR no_such_table SET @info = 'NO_SUCH_TABLE';

#方法4:使用SQLWARNING
DECLARE EXIT HANDLER FOR SQLWARNING SET @info = 'ERROR';

#方法5:使用NOT FOUND
DECLARE EXIT HANDLER FOR NOT FOUND SET @info = 'NO_SUCH_TABLE';

#方法6:使用SQLEXCEPTION
DECLARE EXIT HANDLER FOR SQLEXCEPTION SET @info = 'ERROR';

16.2.3、案例分析

#重新定义存储过程,体现错误的处理程序
DELIMITER //
CREATE PROCEDURE UpdateDataNoCondition()
	BEGIN
		#声明处理程序
		#处理方式1DECLARE CONTINUE HANDLER FOR 1048 SET @prc_value = -1;
		#处理方式2:
		#DECLARE CONTINUE HANDLER FOR sqlstate '23000' SET @prc_value = -1;
		
		SET @x = 1;
		UPDATE employees SET email = NULL WHERE last_name = 'Abel';
		SET @x = 2;
		UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel';
		SET @x = 3;
	END //
DELIMITER ;

#调用存储过程:
CALL UpdateDataNoCondition();

#查看变量:
SELECT @x,@prc_value;

#2.5 再举一个例子:
#创建一个名称为“InsertDataWithCondition”的存储过程
#① 准备工作
CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

DESC departments;

ALTER TABLE departments
ADD CONSTRAINT uk_dept_name UNIQUE(department_id);

#② 定义存储过程:
DELIMITER //
CREATE PROCEDURE InsertDataWithCondition()
	BEGIN
		SET @x = 1;
		INSERT INTO departments(department_name) VALUES('测试');
		SET @x = 2;
		INSERT INTO departments(department_name) VALUES('测试');
		SET @x = 3;
	END //
DELIMITER ;

#③ 调用
CALL InsertDataWithCondition();
SELECT @x;  #2

#④ 删除此存储过程
DROP PROCEDURE IF EXISTS InsertDataWithCondition;

#⑤ 重新定义存储过程(考虑到错误的处理程序)
DELIMITER //
CREATE PROCEDURE InsertDataWithCondition()
	BEGIN
		#处理程序
		#方式1:
		#declare exit handler for 1062 set @pro_value = -1;
		#方式2:
		#declare exit handler for sqlstate '23000' set @pro_value = -1;
		#方式3:
		#定义条件
		DECLARE duplicate_entry CONDITION FOR 1062;
		DECLARE EXIT HANDLER FOR duplicate_entry SET @pro_value = -1;
		SET @x = 1;
		INSERT INTO departments(department_name) VALUES('测试');
		SET @x = 2;
		INSERT INTO departments(department_name) VALUES('测试');
		SET @x = 3;
	END //
DELIMITER ;
#调用
CALL InsertDataWithCondition();
SELECT @x,@pro_value;

16.3、流程控制

解决复杂问题不可能通过一个 SQL 语句完成,我们需要执行多个 SQL 操作。流程控制语句的作用就是控制存储过程中 SQL 语句的执行顺序,是我们完成复杂操作必不可少的一部分。只要是执行的程序,流程就分为三大类:

  • 顺序结构 :程序从上往下依次执行
  • 分支结构 :程序按条件进行选择执行,从两条或多条路径中选择一条执行
  • 循环结构 :程序满足一定条件下,重复执行一组语句

针对于MySQL的流程控制语句主要有3类。注意:只能用于存储程序

  • 条件判断语句:IF 语句和 CASE 语句
  • 循环语句:LOOP、WHILE 和 REPEAT 语句
  • 跳转语句:ITERATE 和 LEAVE 语句

16.3.1、分支结构IF

IF语句的语法结构:

IF 表达式1 THEN 操作1
[ELSEIF 表达式2 THEN 操作2]……
[ELSE 操作N]
END IF

根据表达式的结果为TRUE或FALSE执行相应的语句。这里“[]”中的内容是可选的。

特点:

  1. 不同的表达式对应不同的操作
  2. 使用在begin end中
#3.1 分支结构之 IF
#举例1
DELIMITER //
CREATE PROCEDURE test_if()
    BEGIN	
        #情况1:
        #声明局部变量
        #declare stu_name varchar(15);

        #if stu_name is null 
        #	then select 'stu_name is null';
        #end if;

        #情况2:二选一
        #declare email varchar(25) default 'aaa';

        #if email is null
        #	then select 'email is null';
        #else
        #	select 'email is not null';
        #end if;

        #情况3:多选一
        DECLARE age INT DEFAULT 20;

        IF age > 40
            THEN SELECT '中老年';
        ELSEIF age > 18
            THEN SELECT '青壮年';
        ELSEIF age > 8
            THEN SELECT '青少年';
        ELSE
            SELECT '婴幼儿';
        END IF;
    END //
DELIMITER ;
#调用
CALL test_if();
DROP PROCEDURE test_if;
#举例2:声明存储过程“update_salary_by_eid1”,定义IN参数emp_id,输入员工编号。
#判断该员工薪资如果低于8000元并且入职时间超过5年,就涨薪500元;否则就不变。
DELIMITER //
CREATE PROCEDURE update_salary_by_eid1(IN emp_id INT)
    BEGIN
        #声明局部变量
        DECLARE emp_sal DOUBLE; #记录员工的工资
        DECLARE hire_year DOUBLE; #记录员工入职公司的年头
        #赋值
        SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
        SELECT DATEDIFF(CURDATE(),hire_date)/365 INTO hire_year FROM employees WHERE employee_id = emp_id;
        #判断
        IF emp_sal < 8000 AND hire_year >= 5
            THEN UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
        END IF;
    END //
DELIMITER ;

#调用存储过程
CALL update_salary_by_eid1(104);
SELECT DATEDIFF(CURDATE(),hire_date)/365, employee_id,salary
FROM employees
WHERE salary < 8000 AND DATEDIFF(CURDATE(),hire_date)/365 >= 5;

DROP PROCEDURE update_salary_by_eid1;
#举例3:声明存储过程“update_salary_by_eid2”,定义IN参数emp_id,输入员工编号。
#判断该员工薪资如果低于9000元并且入职时间超过5年,就涨薪500元;否则就涨薪100元。
DELIMITER //
CREATE PROCEDURE update_salary_by_eid2(IN emp_id INT)
    BEGIN
        #声明局部变量
        DECLARE emp_sal DOUBLE; #记录员工的工资
        DECLARE hire_year DOUBLE; #记录员工入职公司的年头
        #赋值
        SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
        SELECT DATEDIFF(CURDATE(),hire_date)/365 INTO hire_year FROM employees WHERE employee_id = emp_id;
        #判断
        IF emp_sal < 9000 AND hire_year >= 5
            THEN UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
        ELSE
            UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
        END IF;
    END //
DELIMITER ;

#调用
CALL update_salary_by_eid2(103);
CALL update_salary_by_eid2(104);
SELECT * FROM employees
WHERE employee_id IN (103,104);

#举例4:声明存储过程“update_salary_by_eid3”,定义IN参数emp_id,输入员工编号。
#判断该员工薪资如果低于9000元,就更新薪资为9000元;薪资如果大于等于9000元且
#低于10000的,但是奖金比例为NULL的,就更新奖金比例为0.01;其他的涨薪100元。
DELIMITER //
CREATE PROCEDURE update_salary_by_eid3(IN emp_id INT)
    BEGIN
        #声明变量
        DECLARE emp_sal DOUBLE; #记录员工工资
        DECLARE bonus DOUBLE; #记录员工的奖金率
        #赋值
        SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
        SELECT commission_pct INTO bonus FROM employees WHERE employee_id = emp_id;
        #判断
        IF emp_sal < 9000 
            THEN UPDATE employees SET salary = 9000 WHERE employee_id = emp_id;
        ELSEIF emp_sal < 10000 AND bonus IS NULL
            THEN UPDATE employees SET commission_pct = 0.01 WHERE employee_id = emp_id;
        ELSE 
            UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
        END IF;
    END //
DELIMITER ;
#调用
CALL update_salary_by_eid3(102);
CALL update_salary_by_eid3(103);
CALL update_salary_by_eid3(104);

SELECT *
FROM employees
WHERE employee_id IN (102,103,104);

16.3.2、分支结构CASE

CASE语句的语法结构1:

#情况一:类似于switch
CASE 表达式
WHEN1 THEN 结果1或语句1(如果是语句,需要加分号)
WHEN2 THEN 结果2或语句2(如果是语句,需要加分号)
...
ELSE 结果n或语句n(如果是语句,需要加分号)
END [case](如果是放在begin end中需要加上case,如果放在select后面不需要)

CASE语句的语法结构2:

#情况二:类似于多重if
CASE
WHEN 条件1 THEN 结果1或语句1(如果是语句,需要加分号)
WHEN 条件2 THEN 结果2或语句2(如果是语句,需要加分号)
...
ELSE 结果n或语句n(如果是语句,需要加分号)
END [case](如果是放在begin end中需要加上case,如果放在select后面不需要)
##3.2 分支结构之case
#举例1:基本使用
DELIMITER //
CREATE PROCEDURE test_case()
    BEGIN
        #演示1case ... when ...then ...
        /*
        declare var int default 2;
        case var
            when 1 then select 'var = 1';
            when 2 then select 'var = 2';
            when 3 then select 'var = 3';
            else select 'other value';
        end case;
        */
        #演示2case when ... then ....
        DECLARE var1 INT DEFAULT 10;
        CASE 
        WHEN var1 >= 100 THEN SELECT '三位数';
        WHEN var1 >= 10 THEN SELECT '两位数';
        ELSE SELECT '个数位';
        END CASE;
    END //
DELIMITER ;
#调用
CALL test_case();
DROP PROCEDURE test_case;
#举例2:声明存储过程“update_salary_by_eid4”,定义IN参数emp_id,输入员工编号。
#判断该员工薪资如果低于9000元,就更新薪资为9000元;薪资大于等于9000元且低于10000的,
#但是奖金比例为NULL的,就更新奖金比例为0.01;其他的涨薪100元。
DELIMITER //
CREATE PROCEDURE update_salary_by_eid4(IN emp_id INT)
    BEGIN
        #局部变量的声明
        DECLARE emp_sal DOUBLE; #记录员工的工资
        DECLARE bonus DOUBLE; #记录员工的奖金率
        #局部变量的赋值
        SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
        SELECT commission_pct INTO bonus FROM employees WHERE employee_id = emp_id;

        CASE
        WHEN emp_sal < 9000 THEN UPDATE employees SET salary = 9000 WHERE employee_id = emp_id;
        WHEN emp_sal < 10000 AND bonus IS NULL THEN UPDATE employees SET commission_pct = 0.01 
                                WHERE employee_id = emp_id;
        ELSE UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
        END CASE;
    END //
DELIMITER ;
#调用
CALL update_salary_by_eid4(103);
CALL update_salary_by_eid4(104);
CALL update_salary_by_eid4(105);
SELECT * FROM employees WHERE employee_id IN (103,104,105);

#举例3:声明存储过程update_salary_by_eid5,定义IN参数emp_id,输入员工编号。
#判断该员工的入职年限,如果是0年,薪资涨50;如果是1年,薪资涨100;
#如果是2年,薪资涨200;如果是3年,薪资涨300;如果是4年,薪资涨400;其他的涨薪500。
DELIMITER //
CREATE PROCEDURE update_salary_by_eid5(IN emp_id INT)
    BEGIN
        #声明局部变量
        DECLARE hire_year INT; #记录员工入职公司的总时间(单位:年)
        #赋值
        SELECT ROUND(DATEDIFF(CURDATE(),hire_date) / 365) INTO hire_year 
        FROM employees WHERE employee_id = emp_id;
        #判断
        CASE hire_year
            WHEN 0 THEN UPDATE employees SET salary = salary + 50 WHERE employee_id = emp_id;
            WHEN 1 THEN UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
            WHEN 2 THEN UPDATE employees SET salary = salary + 200 WHERE employee_id = emp_id;
            WHEN 3 THEN UPDATE employees SET salary = salary + 300 WHERE employee_id = emp_id;
            WHEN 4 THEN UPDATE employees SET salary = salary + 400 WHERE employee_id = emp_id;
            ELSE UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
        END CASE;
    END //
DELIMITER ;
#调用
CALL update_salary_by_eid5(101);
SELECT * FROM employees;
DROP PROCEDURE update_salary_by_eid5;

16.3.3、循环结构LOOP

LOOP循环语句用来重复执行某些语句。LOOP内的语句一直重复执行直到循环被退出(使用LEAVE子句),跳出循环过程。

LOOP语句的基本格式如下:

[loop_label:] LOOP
	循环执行的语句
END LOOP [loop_label]

其中,loop_label表示LOOP语句的标注名称,该参数可以省略。

#4.1 循环结构之LOOP
/*
[loop_label:] LOOP
	循环执行的语句
END LOOP [loop_label]
*/
#举例1:
DELIMITER //
CREATE PROCEDURE test_loop()
    BEGIN
        #声明局部变量
        DECLARE num INT DEFAULT 1;

        loop_label:LOOP
            #重新赋值
            SET num = num + 1;
            #可以考虑某个代码程序反复执行。(略)
            IF num >= 10 THEN LEAVE loop_label;
            END IF;
        END LOOP loop_label;
        #查看num
        SELECT num;
    END //
DELIMITER ;
#调用
CALL test_loop();

#举例2:当市场环境变好时,公司为了奖励大家,决定给大家涨工资。
#声明存储过程“update_salary_loop()”,声明OUT参数num,输出循环次数。
#存储过程中实现循环给大家涨薪,薪资涨为原来的1.1倍。直到全公司的平
#均薪资达到12000结束。并统计循环次数。
DELIMITER //
CREATE PROCEDURE update_salary_loop(OUT num INT)
    BEGIN
        #声明变量
        DECLARE avg_sal DOUBLE ; #记录员工的平均工资
        DECLARE loop_count INT DEFAULT 0;#记录循环的次数
        #① 初始化条件
        #获取员工的平均工资
        SELECT AVG(salary) INTO avg_sal FROM employees;

        loop_lab:LOOP
            #② 循环条件
            #结束循环的条件
            IF avg_sal >= 12000
                THEN LEAVE loop_lab;
            END IF;
            #③ 循环体
            #如果低于12000,更新员工的工资
            UPDATE employees SET salary = salary * 1.1;
            #④ 迭代条件
            #更新avg_sal变量的值
            SELECT AVG(salary) INTO avg_sal FROM employees;
            #记录循环次数
            SET loop_count = loop_count + 1;
        END LOOP loop_lab;
        #给num赋值
        SET num = loop_count;
    END //
DELIMITER ;

SELECT AVG(salary) FROM employees;
CALL update_salary_loop(@num);
SELECT @num;

16.3.4、循环结构WHILE

WHILE语句创建一个带条件判断的循环过程。WHILE在执行语句执行时,先对指定的表达式进行判断,如果为真,就执行循环内的语句,否则退出循环。WHILE语句的基本格式如下:

[while_label:] WHILE 循环条件 DO
	循环体
END WHILE [while_label];

while_label为WHILE语句的标注名称;如果循环条件结果为真,WHILE语句内的语句或语句群被执行,直至循环条件为假,退出循环。

#4.2 循环结构之WHILE
/*
[while_label:] WHILE 循环条件  DO
	循环体
END WHILE [while_label];
*/
#举例1:
DELIMITER //
CREATE PROCEDURE test_while()
    BEGIN	
        #初始化条件
        DECLARE num INT DEFAULT 1;
        #循环条件
        WHILE num <= 10 DO
            #循环体(略)
            #迭代条件
            SET num = num + 1;
        END WHILE;
        #查询
        SELECT num;
    END //
DELIMITER ;
#调用
CALL test_while();

#举例2:市场环境不好时,公司为了渡过难关,决定暂时降低大家的薪资。
#声明存储过程“update_salary_while()”,声明OUT参数num,输出循环次数。
#存储过程中实现循环给大家降薪,薪资降为原来的90%。直到全公司的平均薪资
#达到5000结束。并统计循环次数。
DELIMITER //
CREATE PROCEDURE update_salary_while(OUT num INT)
    BEGIN
        #声明变量
        DECLARE avg_sal DOUBLE ; #记录平均工资
        DECLARE while_count INT DEFAULT 0; #记录循环次数
        #赋值
        SELECT AVG(salary) INTO avg_sal FROM employees;

        WHILE avg_sal > 5000 DO
        	# 循环体
            UPDATE employees SET salary = salary * 0.9 ;
            # 循环次数+1
            SET while_count = while_count + 1;
			# 迭代条件
            SELECT AVG(salary) INTO avg_sal FROM employees;
        END WHILE;
        #给num赋值
        SET num = while_count;		
    END //
DELIMITER ;
#调用
CALL update_salary_while(@num);
SELECT @num;
SELECT AVG(salary) FROM employees;

16.3.5、循环结构REPEAT

REPEAT语句创建一个带条件判断的循环过程。与WHILE循环不同的是,REPEAT 循环首先会执行一次循环(相当于Java中的do while),然后在 UNTIL中进行表达式的判断,如果满足条件就退出,即END REPEAT;如果条件不满足,则会就继续执行循环,直到满足退出条件为止。

REPEAT语句的基本格式如下:

[repeat_label:] REPEAT
    循环体的语句
    UNTIL 结束循环的条件表达式
END REPEAT [repeat_label]

repeat_label为REPEAT语句的标注名称,该参数可以省略;REPEAT语句内的语句或语句群被重复,直至expr_condition为真

注意:UNTIL语句必须在REPEAT中的最后一行

#4.3 循环结构之REPEAT
/*
[repeat_label:] REPEAT
    循环体的语句
UNTIL 结束循环的条件表达式
END REPEAT [repeat_label]
*/
#举例1:
DELIMITER //
CREATE PROCEDURE test_repeat()
    BEGIN
        #声明变量
        DECLARE num INT DEFAULT 1;

        REPEAT
            SET num = num + 1;
            UNTIL num >= 10
        END REPEAT;
        #查看
        SELECT num;
    END //
DELIMITER ;
#调用
CALL test_repeat();

#举例2:当市场环境变好时,公司为了奖励大家,决定给大家涨工资。
#声明存储过程“update_salary_repeat()”,声明OUT参数num,输出循环次数。
#存储过程中实现循环给大家涨薪,薪资涨为原来的1.15倍。直到全公司的平均
#薪资达到13000结束。并统计循环次数。
DELIMITER //
CREATE PROCEDURE update_salary_repeat(OUT num INT)
    BEGIN
        #声明变量
        DECLARE avg_sal DOUBLE ; #记录平均工资
        DECLARE repeat_count INT DEFAULT 0; #记录循环次数
        #赋值
        SELECT AVG(salary) INTO avg_sal FROM employees;

        REPEAT
            UPDATE employees SET salary = salary * 1.15;
            SET repeat_count = repeat_count + 1;
            SELECT AVG(salary) INTO avg_sal FROM employees;
            UNTIL avg_sal >= 13000
        END REPEAT;
        #给num赋值
        SET num = repeat_count;
    END //
    DELIMITER ;
#调用
CALL update_salary_repeat(@num);
SELECT @num;
SELECT AVG(salary) FROM employees;

总结:

这三种循环都可以省略名称,但如果循环中添加了循环控制语句(LEAVE或ITERATE)则必须添加名称。

LOOP:一般用于实现简单的"死"循环

WHILE:先判断后执行

REPEAT:先执行后判断,无条件至少执行一次

凡是循环结构,一定具备4个要素:

  1. 初始化条件
  2. 循环条件
  3. 循环体
  4. 迭代条件

16.3.6、跳转语句LEAVE

LEAVE语句:可以用在循环语句内,或者以 BEGIN 和 END 包裹起来的程序体内,表示跳出循环或者跳出程序体的操作。可以把 LEAVE 理解为 break。意思为“退出循环”基本格式如下:

LEAVE 标记名

其中,label参数表示循环的标志。LEAVE和BEGIN ... END或循环一起被使用。

#5.1 LEAVE的使用
/*
举例1:创建存储过程 “leave_begin()”,声明INT类型的IN参数num。给BEGIN...END加标记名,
并在BEGIN...END中使用IF语句判断num参数的值。
- 如果num<=0,则使用LEAVE语句退出BEGIN...END;
- 如果num=1,则查询“employees”表的平均薪资;
- 如果num=2,则查询“employees”表的最低薪资;
- 如果num>2,则查询“employees”表的最高薪资。
IF语句结束后查询“employees”表的总人数。
*/
DELIMITER //
CREATE PROCEDURE leave_begin(IN num INT)
    begin_label:BEGIN
        IF num <= 0
            THEN LEAVE begin_label;
        ELSEIF num = 1
            THEN SELECT AVG(salary) FROM employees;
        ELSEIF num = 2
            THEN SELECT MIN(salary) FROM employees;
        ELSE 
            SELECT MAX(salary) FROM employees;
        END IF;
        #查询总人数
        SELECT COUNT(*) FROM employees;
    END //
DELIMITER ;
#调用
CALL leave_begin(1);

#举例2:当市场环境不好时,公司为了渡过难关,决定暂时降低大家的薪资。
#声明存储过程“leave_while()”,声明OUT参数num,输出循环次数,存储过程中使用WHILE
#循环给大家降低薪资为原来薪资的90%,直到全公司的平均薪资小于等于10000,并统计循环次数。
DELIMITER //
CREATE PROCEDURE leave_while(OUT num INT)
    BEGIN 
        DECLARE avg_sal DOUBLE;#记录平均工资
        DECLARE while_count INT DEFAULT 0; #记录循环次数
        SELECT AVG(salary) INTO avg_sal FROM employees; #① 初始化条件

        while_label:WHILE TRUE DO  #② 循环条件
            #③ 循环体
            IF avg_sal <= 10000 THEN
                LEAVE while_label;
            END IF;

            UPDATE employees SET salary  = salary * 0.9;
            SET while_count = while_count + 1;
            #④ 迭代条件
            SELECT AVG(salary) INTO avg_sal FROM employees;
        END WHILE;
        #赋值
        SET num = while_count;
    END //
DELIMITER ;

#调用
CALL leave_while(@num);
SELECT @num;
SELECT AVG(salary) FROM employees;

16.3.7、跳转语句ITERATE

ITERATE语句:只能用在循环语句(LOOP、REPEAT和WHILE语句)内,表示重新开始循环,将执行顺序转到语句段开头处。可以把 ITERATE 理解为 continue,意思为“再次循环”。

语句基本格式如下:

ITERATE label

label参数表示循环的标志。ITERATE语句必须跟在循环标志前面。

#5.2 ITERATE的使用
/*
举例: 定义局部变量num,初始值为0。循环结构中执行num + 1操作。
- 如果num < 10,则继续执行循环;
- 如果num > 15,则退出循环结构;
*/
DELIMITER //
CREATE PROCEDURE test_iterate()
    BEGIN
        DECLARE num INT DEFAULT 0;
        loop_label:LOOP
            #赋值
            SET num = num + 1;
            IF num  < 10
                THEN ITERATE loop_label;
            ELSEIF num > 15
                THEN LEAVE loop_label;
            END IF;
            SELECT '尚硅谷:让天下没有难学的技术';
        END LOOP;
    END //
DELIMITER ;
CALL test_iterate();
SELECT * FROM employees;

16.4、游标

16.4.1、什么是游标

虽然可以通过筛选条件 WHERE 和 HAVING,或者是限定返回记录的关键字 LIMIT 返回一条记录,但是,却无法在结果集中像指针一样,向前定位一条记录、向后定位一条记录,或者是 随意定位到某一条记录 ,并对记录的数据进行处理。

这个时候,就可以用到游标。游标,提供了一种灵活的操作方式,能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。游标让 SQL 这种面向集合的语言有了面向过程开发的能力。

在 SQL 中,游标是一种临时的数据库对象,可以指向存储在数据库表中的数据行指针。这里游标充当了指针的作用 ,可以通过操作游标来对数据行进行操作。MySQL中游标可以在存储过程和函数中使用

16.4.2、使用游标的步骤

游标必须在声明处理程序之前被声明,并且变量和条件还必须在声明游标或处理程序之前被声明。如果想要使用游标,一般需要经历四个步骤。不同的DBMS中,使用游标的语法可能略有不同。

第一步:声明游标

在MySQL中,使用DECLARE关键字来声明游标,其语法的基本形式如下:

DECLARE cursor_name CURSOR FOR select_statement;

这个语法适用于 MySQL,SQL Server,DB2 和 MariaDB。如果是用 Oracle 或者 PostgreSQL,需要写成:

DECLARE cursor_name CURSOR IS select_statement;

要使用 SELECT 语句来获取数据结果集,而此时还没有开始遍历数据,这里 select_statement 代表的是SELECT 语句,返回一个用于创建游标的结果集。

第二步:打开游标

OPEN cursor_name

当我们定义好游标之后,如果想要使用游标,必须先打开游标。打开游标的时候 SELECT 语句的查询结果集就会送到游标工作区,为后面游标的逐条读取结果集中的记录做准备。

第三步:使用游标(从游标中取得数据)

FETCH cursor_name INTO var_name [, var_name] ...

这句的作用是使用 cursor_name 这个游标来读取当前行,并且将数据保存到 var_name 这个变量中,游标指针指到下一行。如果游标读取的数据行有多个列名,则在 INTO 关键字后面赋值给多个变量名即可。

注意:

  1. var_name必须在声明游标之前就定义好。
  2. 游标的查询结果集中的字段数,必须跟 INTO 后面的变量数一致,否则,在存储过程执行的时候,MySQL会提示错误。

第四步:关闭游标

CLOSE cursor_name

OPEN就会有CLOSE,也就是打开和关闭游标。当我们使用完游标后需要关闭掉该游标。因为游标会占用系统资源 ,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放游标占用的系统资源。关闭游标之后,我们就不能再检索查询结果中的数据行,如果需要检索只能再次打开游标

#举例:创建存储过程“get_count_by_limit_total_salary()”,声明IN参数 limit_total_salary,
#DOUBLE类型;声明OUT参数total_count,INT类型。函数的功能可以实现累加薪资最高的几个员工的薪资值,
#直到薪资总和达到limit_total_salary参数的值,返回累加的人数给total_count。
DELIMITER //
CREATE PROCEDURE get_count_by_limit_total_salary(IN limit_total_salary DOUBLE,OUT total_count INT)
    BEGIN
        #声明局部变量
        DECLARE sum_sal DOUBLE DEFAULT 0.0; #记录累加的工资总额
        DECLARE emp_sal DOUBLE; #记录每一个员工的工资
        DECLARE emp_count INT DEFAULT 0;#记录累加的人数
        #1.声明游标
        DECLARE emp_cursor CURSOR FOR SELECT salary FROM employees ORDER BY salary DESC;
        #2.打开游标
        OPEN emp_cursor;
        
        REPEAT
            #3.使用游标
            FETCH emp_cursor INTO emp_sal;
            
            SET sum_sal = sum_sal + emp_sal;
            SET emp_count = emp_count + 1;
            UNTIL sum_sal >= limit_total_salary
        END REPEAT;
        SET total_count = emp_count;
        #4.关闭游标
        CLOSE emp_cursor;
    END //
DELIMITER ;
#调用
CALL get_count_by_limit_total_salary(200000,@total_count);
SELECT @total_count;

16.4.3、小结

游标是 MySQL 的一个重要的功能,为逐条读取结果集中的数据,提供了完美的解决方案。跟在应用层面实现相同的功能相比,游标可以在存储程序中使用,效率高,程序也更加简洁。但同时也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行加锁 ,这样在业务并发量大的时候,不仅会影响业务之间的效率,还会消耗系统资源,造成内存不足,这是因为游标是在内存中进行的处理

章节练习

#第16章_变量、流程控制与游标的课后练习

#练习1:测试变量的使用
#存储函数的练习
#0. 准备工作
CREATE DATABASE test16_var_cursor;
USE test16_var_cursor;

CREATE TABLE employees
AS
SELECT * FROM atguigudb.`employees`;

CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

SET GLOBAL log_bin_trust_function_creators = 1;

#无参有返回
#1. 创建函数get_count(),返回公司的员工个数
DELIMITER //
CREATE FUNCTION get_count() RETURNS INT
BEGIN
	DECLARE emp_count INT DEFAULT 0;
	SELECT COUNT(*) INTO emp_count FROM employees;
	RETURN emp_count;
END //
DELIMITER ;
#调用
SELECT get_count();

#有参有返回
#2. 创建函数ename_salary(),根据员工姓名,返回它的工资
DELIMITER //
CREATE FUNCTION ename_salary(ename VARCHAR(25)) RETURNS DOUBLE
BEGIN
	SET @sal = 0; #定义一个会话用户变量
	SELECT salary INTO @sal FROM employees WHERE last_name = ename;
	RETURN @sal;
END //
DELIMITER ;
#调用
SELECT ename_salary('Abel');

#3. 创建函数dept_sal() ,根据部门名,返回该部门的平均工资
DELIMITER //
CREATE FUNCTION dept_sal(dept_name VARCHAR(25)) RETURNS DOUBLE
BEGIN
	DECLARE avg_sal DOUBLE DEFAULT 0.0;
	SELECT AVG(salary) INTO avg_sal
	FROM employees e JOIN departments d
	ON e.department_id = d.department_id
	WHERE d.department_name = dept_name;
	
	RETURN avg_sal;
END //
DELIMITER ;
#调用
SELECT dept_sal('IT');

#4. 创建函数add_float(),实现传入两个float,返回二者之和
DELIMITER //
CREATE FUNCTION add_float(value1 FLOAT,value2 FLOAT) RETURNS FLOAT
BEGIN
	DECLARE result FLOAT;
	SET result = value1 + value2;
	RETURN result;
END //
DELIMITER ;
# 调用
SELECT add_float(13.14,5.20);

#2. 流程控制
#创建函数test_if_case(),实现传入成绩,如果成绩>90,返回A,如果成绩>80,返回B,如果成绩>60,返回C,否则返回D
#要求:分别使用if结构和case结构实现
#方式1:if
DELIMITER //
CREATE FUNCTION test_if_case1(score DOUBLE) RETURNS CHAR
BEGIN
	DECLARE score_level CHAR;
	IF score > 90 THEN SET score_level = 'A';
	ELSEIF score > 80 THEN SET score_level = 'B';
	ELSEIF score > 60 THEN SET score_level = 'C';
	ELSE SET score_level = 'D';
	END IF;
	# 返回
	RETURN score_level;
END //
DELIMITER ;
#调用
SELECT test_if_case1(99);

#方式2case when ...
DELIMITER //
CREATE FUNCTION test_if_case2(score DOUBLE) RETURNS CHAR
BEGIN
	DECLARE score_level CHAR;
	
	CASE 
	WHEN score > 90 THEN SET score_level = 'A';
	WHEN score > 80 THEN SET score_level = 'B';
	WHEN score > 60 THEN SET score_level = 'C';
	ELSE SET score_level = 'D';
	END CASE;
	# 返回
	RETURN score_level;
END //
DELIMITER ;
#调用
SELECT test_if_case2(66);

#2. 创建存储过程test_if_pro(),传入工资值,如果工资值<3000,则删除工资为此值的员工,
# 如果3000 <= 工资值 <= 5000,则修改此工资值的员工薪资涨1000,否则涨工资500
DELIMITER //
CREATE PROCEDURE test_if_pro(IN sal DOUBLE)
BEGIN
	CASE 
	WHEN sal < 3000 THEN DELETE FROM employees WHERE salary = sal;
	WHEN sal <= 5000 THEN UPDATE employees SET salary = salary + 1000 WHERE salary = sal;
	ELSE UPDATE employees SET salary = salary + 500 WHERE salary = sal;
	END CASE;
END //
DELIMITER ;
#调用
CALL test_if_pro(24000);
SELECT * FROM employees;

#3. 创建存储过程insert_data(),传入参数为 ININT 类型变量 insert_count,实现向admin表中
#批量插入insert_count条记录
CREATE TABLE admin(
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(25) NOT NULL,
user_pwd VARCHAR(35) NOT NULL
);

DELIMITER //
CREATE PROCEDURE insert_data(IN insert_count INT)
BEGIN
	DECLARE init_count INT DEFAULT 1;
	WHILE init_count <= insert_count DO
		#循环体
		INSERT INTO admin(user_name,user_pwd)
		VALUES(CONCAT('user',init_count),ROUND(RAND()*100000));
		#迭代条件
		SET init_count = init_count + 1;
	END WHILE;
END //
DELIMITER ;
# 调用
CALL insert_data(5);
SELECT * FROM admin;

#3. 游标的使用
#创建存储过程update_salary(),参数1ININT型变量dept_id,表示部门id;
#参数2ININT型变量change_sal_count,表示要调整薪资的员工个数。查询指定id部门的员工信息,
#按照salary升序排列,根据hire_date的情况,调整前change_sal_count个员工的薪资,详情如下。
DELIMITER //
CREATE PROCEDURE update_salary(IN dept_id INT,IN change_sal_count INT)
BEGIN
	# 声明变量
	DECLARE emp_id INT;	#记录员工id
	DECLARE emp_hire_date DATE; #记录员工入职时间
	DECLARE init_count INT DEFAULT 1; #用于表示循环结构的初始化条件
	DECLARE add_sal_rate DOUBLE ; #记录涨薪的比例
	
	#1.声明游标
	DECLARE emp_cursor CURSOR FOR SELECT employee_id,hire_date FROM employees 
	WHERE department_id = dept_id ORDER BY salary;
	#2.打开游标
	OPEN emp_cursor;
	# 循环
	WHILE init_count <= change_sal_count DO
		#3.使用游标
		FETCH emp_cursor INTO emp_id,emp_hire_date;
		#获取涨薪的比例
		IF (YEAR(emp_hire_date) < 1995)
			THEN SET add_sal_rate = 1.2;
		ELSEIF(YEAR(emp_hire_date) <= 1998)
			THEN SET add_sal_rate = 1.15;
		ELSEIF(YEAR(emp_hire_date) <= 2001)
			THEN SET add_sal_rate = 1.10;
		ELSE SET add_sal_rate = 1.05;
		END IF;
		# 涨薪
		UPDATE employees SET salary = salary * add_sal_rate WHERE employee_id = emp_id;
		
		#迭代条件更新
		SET init_count = init_count + 1;
	END WHILE;
	#4.关闭游标
	CLOSE emp_cursor;
END //
DELIMITER ;
#调用
CALL update_salary(50,3);

SELECT employee_id,hire_date,salary
FROM employees
WHERE department_id = 50
ORDER BY salary ASC;

17、触发器

在实际开发中,我们经常会遇到这样的情况:有 2 个或者多个相互关联的表,如 商品信息 和 库存信息 分别存放在 2 个不同的数据表中,我们在添加一条新商品记录的时候,为了保证数据的完整性,必须同时在库存表中添加一条库存记录。这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用事务包裹起来,确保这两个操作成为一个原子操作要么全部执行,要么全部不执行。要是遇到特殊情况,可能还需要对数据进行手动维护,这样就很容易忘记其中的一步,导致数据缺失。

解决方法:可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。这样一来,就不用担心因为忘记添加库存数据而导致的数据缺失了。

17.1、触发器概述

MySQL从5.0.2版本开始支持触发器。MySQL的触发器和存储过程一样,都是嵌入到MySQL服务器的一段程序。

触发器是由事件来触发某个操作,这些事件包括INSERT、UPDATE、DELETE事件。所谓事件就是指用户的动作或者触发某项行为。如果定义了触发程序,当数据库执行这些语句时候,就相当于事件发生了,就会自动激发触发器执行相应的操作。

当对数据表中的数据执行插入、更新和删除操作,需要自动执行一些数据库逻辑时,可以使用触发器来实现。

17.2、创建触发器

CREATE TRIGGER 触发器名称
{BEFORE|AFTER} {INSERT|UPDATE|DELETE} ON 表名
FOR EACH ROW
触发器执行的语句块;

说明:

  • 表名:表示触发器监控的对象
  • BEFORE|AFTER:表示触发的时间。BEFORE表示在事件之前触发;AFTER表示在事件之后触发。
  • INSERT|UPDATE|DELETE:表示触发的事件。
    • INSERT 表示插入记录时触发;
    • UPDATE 表示更新记录时触发;
    • DELETE 表示删除记录时触发。
  • 触发器执行的语句块:可以是单条SQL语句,也可以是由BEGIN…END结构组成的复合语句块
#第17章_触发器
#0.准备工作
CREATE DATABASE dbtest17;
USE dbtest17;
#1. 创建触发器
#举例1:
#① 创建数据表
CREATE TABLE test_trigger (
id INT PRIMARY KEY AUTO_INCREMENT,
t_note VARCHAR(30)
);
CREATE TABLE test_trigger_log (
id INT PRIMARY KEY AUTO_INCREMENT,
t_log VARCHAR(30)
);
#② 查看表数据
SELECT * FROM test_trigger;
SELECT * FROM test_trigger_log;

#③ 创建触发器
#创建名称为before_insert_test_tri的触发器,向test_trigger数据表插入数据之前,
#向test_trigger_log数据表中插入before_insert的日志信息。
DELIMITER //
CREATE TRIGGER before_insert_test_tri
BEFORE INSERT ON test_trigger
FOR EACH ROW
    BEGIN
        INSERT INTO test_trigger_log(t_log)
        VALUES('before insert...');
    END //
DELIMITER ;

#④ 测试
INSERT INTO test_trigger(t_note)
VALUES('Tom...');

SELECT * FROM test_trigger;
SELECT * FROM test_trigger_log;

#举例2:
#创建名称为after_insert_test_tri的触发器,向test_trigger数据表插入数据之后,
#向test_trigger_log数据表中插入after_insert的日志信息。
DELIMITER $
CREATE TRIGGER after_insert_test_tri
AFTER INSERT ON test_trigger
FOR EACH ROW
BEGIN
	INSERT INTO test_trigger_log(t_log)
	VALUES('after insert...');
END $
DELIMITER ;
#测试
INSERT INTO test_trigger(t_note)
VALUES('Jerry2...');

SELECT * FROM test_trigger;
SELECT * FROM test_trigger_log;

#举例3:
#定义触发器“salary_check_trigger”,基于员工表“employees”的INSERT事件,
#在INSERT之前检查将要添加的新员工薪资是否大于他领导的薪资,如果大于领导薪资,
#则报sqlstate_value为'HY000'的错误,从而使得添加失败。
#准备工作
CREATE TABLE employees
AS
SELECT * FROM atguigudb.`employees`;
CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

#创建触发器
DELIMITER //
CREATE TRIGGER salary_check_trigger
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
	#查询到要添加的数据的manager的薪资
	DECLARE mgr_sal DOUBLE;
	
	SELECT salary INTO mgr_sal FROM employees 
	WHERE employee_id = NEW.manager_id;
	
	IF NEW.salary > mgr_sal
		THEN SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = '薪资高于领导薪资错误';
	END IF;
END //
DELIMITER ;

#测试:
#添加成功:依然触发了触发器salary_check_trigger的执行
INSERT INTO employees(employee_id,last_name,email,hire_date,job_id,salary,manager_id)
VALUES(300,'Tom','tom@126.com',CURDATE(),'AD_VP',8000,103);
#添加失败:触发了触发器salary_check_trigger的执行
INSERT INTO employees(employee_id,last_name,email,hire_date,job_id,salary,manager_id)
VALUES(301,'Tom1','tom1@126.com',CURDATE(),'AD_VP',10000,103);

SELECT * FROM employees;

17.3、查看和删除触发器

17.3.1、查看触发器

方式1:查看当前数据库的所有触发器的定义

SHOW TRIGGERS;

方式2:查看当前数据库中某个触发器的定义

SHOW CREATE TRIGGER 触发器名;

方式3:从系统库information_schemaTRIGGERS表中查询“salary_check_trigger”触发器的信息。

SELECT * FROM information_schema.TRIGGERS;

17.3.2、删除触发器

触发器也是数据库对象,删除触发器也用DROP语句,语法格式如下:

DROP TRIGGER IF EXISTS 触发器名称;
#2. 查看触发器
#① 查看当前数据库的所有触发器的定义
SHOW TRIGGERS;
#② 方式2:查看当前数据库中某个触发器的定义
SHOW CREATE TRIGGER salary_check_trigger;
#③ 方式3:从系统库information_schema的TRIGGERS表中查询“salary_check_trigger”触发器的信息。
SELECT * FROM information_schema.TRIGGERS;
#3. 删除触发器
DROP TRIGGER IF EXISTS after_insert_test_tri;

17.4、触发器的优缺点

17.4.1、优点

  1. 触发器可以确保数据的完整性

  2. 触发器可以帮助我们记录操作日志。

  3. 触发器还可以用在操作数据前,对数据进行合法性检查。

17.4.2、缺点

  1. 触发器的可读性差。

    因为触发器存储在数据库中,并且由事件驱动,这就意味着触发器有可能不受应用层的控制。这对系统维护是非常有挑战的。

  2. 相关数据的变更,可能导致触发器出错。

17.4.3、注意事项

注意,如果在子表中定义了外键约束,并且外键指定了ON UPDATE/DELETE CASCADE/SET NULL子句,此时修改父表被引用的键值或删除父表被引用的记录行时,也会引起子表的修改和删除操作,此时基于子表的UPDATE和DELETE语句定义的触发器并不会被激活。

例如:基于子表员工表(t_employee)的DELETE语句定义了触发器t1,而子表的部门编号(did)字段定义了外键约束引用了父表部门表(t_department)的主键列部门编号(did),并且该外键加了“ON DELETE SET NULL”子句,那么如果此时删除父表部门表(t_department)在子表员工表(t_employee)有匹配记录的部门记录时,会引起子表员工表(t_employee)匹配记录的部门编号(did)修改为NULL,但是此时不会激活触发器t1。只有直接对子表员工表(t_employee)执行DELETE语句时才会激活触发器t1

章节练习

#第17章_触发器的课后练习
#练习1:
#0. 准备工作
CREATE DATABASE test17_trigger;
USE test17_trigger;

CREATE TABLE emps
AS
SELECT employee_id,last_name,salary
FROM atguigudb.`employees`;

SELECT * FROM emps;

#1. 复制一张emps表的空表emps_back,只有表结构,不包含任何数据
CREATE TABLE emps_back
AS
SELECT * FROM emps
WHERE 1 = 2;

#2. 查询emps_back表中的数据
SELECT * FROM emps_back;

#3. 创建触发器emps_insert_trigger,每当向emps表中添加一条记录时,同步将这条记录
#添加到emps_back表中
DELIMITER //
CREATE TRIGGER emps_insert_trigger
AFTER INSERT ON emps
FOR EACH ROW
BEGIN
	#将新添加到emps表中的记录添加到emps_back表中
	INSERT INTO emps_back(employee_id,last_name,salary)
	VALUES(NEW.employee_id,NEW.last_name,NEW.salary);
END //
DELIMITER ;

#show triggers;

#4. 验证触发器是否起作用
SELECT * FROM emps;
SELECT * FROM emps_back;

INSERT INTO emps(employee_id,last_name,salary)
VALUES(301,'Tom1',3600);

#练习2:
#0. 准备工作:使用练习1中的emps表
#1. 复制一张emps表的空表emps_back1,只有表结构,不包含任何数据
CREATE TABLE emps_back1 AS
SELECT * FROM emps WHERE 1 = 2;

#2. 查询emps_back1表中的数据
SELECT * FROM emps_back1;

#3.创建触发器emps_del_trigger,每当向emps表中删除一条记录时,同步将删除的这条记录添加到emps_back1表中
DELIMITER //
CREATE TRIGGER emps_del_trigger
BEFORE DELETE ON emps
FOR EACH ROW
	BEGIN
		#将emps表中删除的记录,添加到emps_back1表中。
		INSERT INTO emps_back1(employee_id,last_name,salary)
		VALUES(OLD.employee_id,OLD.last_name,OLD.salary);
	END //
DELIMITER ;

#4. 验证触发器是否起作用
DELETE FROM emps
WHERE employee_id = 101;

DELETE FROM emps;
SELECT * FROM emps;
SELECT * FROM emps_back1;

18、MySQL8其他新特性

18.1、MySQL8.0新特性概述

18.1.1、MySQL8.0新增特性

  1. 更简便的NoSQL支持 NoSQL泛指非关系型数据库和数据存储。随着互联网平台的规模飞速发展,传统的关系型数据库已经越来越不能满足需求。从5.6版本开始,MySQL就开始支持简单的NoSQL存储功能。MySQL 8对这一功能做了优化,以更灵活的方式实现NoSQL功能,不再依赖模式(schema)。

  2. 更好的索引 在查询中,正确地使用索引可以提高查询的效率。MySQL 8中新增了 隐藏索引 和 降序索引 。隐藏索引可以用来测试去掉索引对查询性能的影响。在查询中混合存在多列索引时,使用降序索引可以提高查询的性能。

  3. 更完善的JSON支持 MySQL从5.7开始支持原生JSON数据的存储,MySQL 8对这一功能做了优化,增加了聚合函数 JSON_ARRAYAGG() 和 JSON_OBJECTAGG() ,将参数聚合为JSON数组或对象,新增了行内操作符 ->>,是列路径运算符 ->的增强,对JSON排序做了提升,并优化了JSON的更新操作。

  4. 安全和账户管理 MySQL 8中新增了 caching_sha2_password 授权插件、角色、密码历史记录和FIPS模式支持,这些特性提高了数据库的安全性和性能,使数据库管理员能够更灵活地进行账户管理工作。

  5. InnoDB的变化 InnoDB是MySQL默认的存储引擎 ,是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键。在MySQL 8 版本中,InnoDB在自增、索引、加密、死锁、共享锁等方面做了大量的 改进和优化 ,并且支持原子数据定义语言(DDL),提高了数据安全性,对事务提供更好的支持。

  6. 数据字典 在之前的MySQL版本中,字典数据都存储在元数据文件和非事务表中。从MySQL 8开始新增了事务数据字典,在这个字典里存储着数据库对象信息,这些数据字典存储在内部事务表中。

  7. 原子数据定义语句 MySQL 8开始支持原子数据定义语句(Automic DDL),即 原子DDL 。目前,只有InnoDB存储引擎支持原子DDL。原子数据定义语句(DDL)将与DDL操作相关的数据字典更新、存储引擎操作、二进制日志写入结合到一个单独的原子事务中,这使得即使服务器崩溃,事务也会提交或回滚。使用支持原子操作的存储引擎所创建的表,在执行DROP TABLE、CREATE TABLE、ALTER TABLE、RENAME TABLE、TRUNCATE TABLE、CREATE TABLESPACE、DROP TABLESPACE等操作时,都支持原子操作,即事务要么完全操作成功,要么失败后回滚,不再进行部分提交。 对于从MySQL 5.7复制到MySQL 8版本中的语句,可以添加 IF EXISTS 或 IF NOT EXISTS 语句来避免发生错误。

  8. · MySQL 8开始支持创建和管理资源组,允许将服务器内运行的线程分配给特定的分组,以便线程根据组内可用资源执行。组属性能够控制组内资源,启用或限制组内资源消耗。数据库管理员能够根据不同的工作负载适当地更改这些属性。 目前,CPU时间是可控资源,由“虚拟CPU”这个概念来表示,此术语包含CPU的核心数,超线程,硬件线程等等。服务器在启动时确定可用的虚拟CPU数量。拥有对应权限的数据库管理员可以将这些CPU与资源组关联,并为资源组分配线程。 资源组组件为MySQL中的资源组管理提供了SQL接口。资源组的属性用于定义资源组。MySQL中存在两个默认组,系统组和用户组,默认的组不能被删除,其属性也不能被更改。对于用户自定义的组,资源组创建时可初始化所有的属性,除去名字和类型,其他属性都可在创建之后进行更改。 在一些平台下,或进行了某些MySQL的配置时,资源管理的功能将受到限制,甚至不可用。例如,如果安装了线程池插件,或者使用的是macOS系统,资源管理将处于不可用状态。在FreeBSD和Solaris系统中,资源线程优先级将失效。在Linux系统中,只有配置了CAP_SYS_NICE属性,资源管理优先级才能发挥作用。

  9. 字符集支持 MySQL 8中默认的字符集由 latin1 更改为 utf8mb4 ,并首次增加了日语所特定使用的集合,utf8mb4_ja_0900_as_cs。

  10. 优化器增强 MySQL优化器开始支持隐藏索引和降序索引。隐藏索引不会被优化器使用,验证索引的必要性时不需要删除索引,先将索引隐藏,如果优化器性能无影响就可以真正地删除索引。降序索引允许优化器对多个列进行排序,并且允许排序顺序不一致。

  11. 公用表表达式 公用表表达式(Common Table Expressions)简称为CTE,MySQL现在支持递归和非递归两种形式的CTE。CTE通过在SELECT语句或其他特定语句前 使用WITH语句对临时结果集 进行命名。基础语法如下:

    WITH cte_name (col_name1,col_name2 ...) AS (Subquery)
    SELECT * FROM cte_name;
    

    Subquery代表子查询,子查询前使用WITH语句将结果集命名为cte_name,在后续的查询中即可使用cte_name进行查询。

  12. 窗口函数 MySQL 8开始支持窗口函数。在之前的版本中已存在的大部分 聚合函数 在MySQL 8中也可以作为窗口函数来使用。

  13. 正则表达式支持 MySQL在8.0.4以后的版本中采用支持Unicode的国际化组件库实现正则表达式操作,这种方式不仅能提供完全的Unicode支持,而且是多字节安全编码。MySQL增加了REGEXP_LIKE()、EGEXP_INSTR()、REGEXP_REPLACE()和 REGEXP_SUBSTR()等函数来提升性能。另外,regexp_stack_limit和regexp_time_limit 系统变量能够通过匹配引擎来控制资源消耗。

  14. 内部临时表 TempTable存储引擎取代MEMORY存储引擎成为内部临时表的默认存储引擎 。TempTable存储引擎为VARCHAR和VARBINARY列提供高效存储。internal_tmp_mem_storage_engine会话变量定义了内部临时表的存储引擎,可选的值有两个,TempTable和MEMORY,其中TempTable为默认的存储引擎。temptable_max_ram系统配置项定义了TempTable存储引擎可使用的最大内存数量。

  15. 日志记录 在MySQL 8中错误日志子系统由一系列MySQL组件构成。这些组件的构成由系统变量log_error_services来配置,能够实现日志事件的过滤和写入。WITH cte_name (col_name1,col_name2 ...) AS (Subquery)SELECT * FROM cte_name;

  16. 备份锁 新的备份锁允许在线备份期间执行数据操作语句,同时阻止可能造成快照不一致的操作。新备份锁由 LOCK INSTANCE FOR BACKUP 和 UNLOCK INSTANCE 语法提供支持,执行这些操作需要备份管理员特权。

  17. 增强的MySQL复制 MySQL 8复制支持对 JSON文档 进行部分更新的 二进制日志记录 ,该记录 使用紧凑

    的二进制格式 ,从而节省记录完整JSON文档的空间。当使用基于语句的日志记录时,这种紧凑的日志记

    录会自动完成,并且可以通过将新的binlog_row_value_options系统变量值设置为PARTIAL_JSON来启用。

18.1.2、MySQL8.0移除的特性

在MySQL 5.7版本上开发的应用程序如果使用了MySQL8.0 移除的特性,语句可能会失败,或者产生不同

的执行结果。为了避免这些问题,对于使用了移除特性的应用,应当尽力修正避免使用这些特性,并尽

可能使用替代方法。

  1. 查询缓存 查询缓存已被移除 ,删除的项有:
    • 语句:FLUSH QUERY CACHE和RESET QUERYCACHE。
    • 系统变量:query_cache_limit、query_cache_min_res_unit、query_cache_size、query_cache_type、query_cache_wlock_invalidate。
    • 状态变量:Qcache_free_blocks、Qcache_free_memory、Qcache_hits、Qcache_inserts、Qcache_lowmem_prunes、Qcache_not_cached、Qcache_queries_in_cache、Qcache_total_blocks。
    • 线程状态:checking privileges on cachedquery、checking query cache for query、invalidating query cache entries、sending cached result to client、storing result in query cache、waiting for query cache lock。
  2. 加密相关 删除的加密相关的内容有:ENCODE()、DECODE()、ENCRYPT()、DES_ENCRYPT()和DES_DECRYPT()函数,配置项des-key-file,系统变量have_crypt,FLUSH语句的DES_KEY_FILE选项,HAVE_CRYPT CMake选项。 对于移除的ENCRYPT()函数,考虑使用SHA2()替代,对于其他移除的函数,使用AES_ENCRYPT()和AES_DECRYPT()替代。
  3. 空间函数相关 在MySQL 5.7版本中,多个空间函数已被标记为过时。这些过时函数在MySQL 8中都已被移除,只保留了对应的ST_和MBR函数。
  4. \N和NULL 在SQL语句中,解析器不再将\N视为NULL,所以在SQL语句中应使用NULL代替\N。这项变化不会影响使用LOAD DATA INFILE或者SELECT...INTO OUTFILE操作文件的导入和导出。在这类操作中,NULL仍等同于\N。
  5. mysql_install_db 在MySQL分布中,已移除了mysql_install_db程序,数据字典初始化需要调用带着--initialize或者--initialize-insecure选项的mysqld来代替实现。另外,--bootstrap和INSTALL_SCRIPTDIR CMake也已被删除。
  6. 通用分区处理程序 通用分区处理程序已从MySQL服务中被移除。为了实现给定表分区,表所使用的存储引擎需要自有的分区处理程序。 提供本地分区支持的MySQL存储引擎有两个,即InnoDB和NDB,而在MySQL 8中只支持InnoDB。
  7. 系统和状态变量信息 在INFORMATION_SCHEMA数据库中,对系统和状态变量信息不再进行维护。GLOBAL_VARIABLES、SESSION_VARIABLES、GLOBAL_STATUS、SESSION_STATUS表都已被删除。另外,系统变量show_compatibility_56也已被删除。被删除的状态变量有Slave_heartbeat_period、Slave_last_heartbeat,Slave_received_heartbeats、Slave_retried_transactions、Slave_running。以上被删除的内容都可使用性能模式中对应的内容进行替代。
  8. mysql_plugin工具 mysql_plugin工具用来配置MySQL服务器插件,现已被删除,可使用--plugin-load或--plugin-load-add选项在服务器启动时加载插件或者在运行时使用INSTALL PLUGIN语句加载插件来替代该工具。

18.2、新特性1:窗口函数

MySQL从8.0版本开始支持窗口函数。窗口函数的作用类似于在查询中对数据进行分组,不同的是,分组操作会把分组的结果聚合成一条记录,而窗口函数是将结果置于每一条数据记录中。窗口函数可以分为静态窗口函数动态窗口函数

  • 静态窗口函数的窗口大小是固定的,不会因为记录的不同而不同;
  • 动态窗口函数的窗口大小会随着记录的不同而变化。

MySQL官网窗口函数的说明:MySQL :: MySQL 8.0 Reference Manual :: 12.21.1 Window Function Descriptions

18.2.1、窗口函数的分类

窗口函数总体上可以分为序号函数、分布函数、前后函数、首尾函数和其他函数,如下表:

窗口函数

18.2.2、语法结构

函数 OVER([PARTITION BY 字段名 ORDER BY 字段名 ASC|DESC])
# 或者
函数 OVER 窗口名 … WINDOW 窗口名 AS ([PARTITION BY 字段名 ORDER BY 字段名 ASC|DESC])
  • OVER 关键字指定函数窗口的范围。
    • 如果省略后面括号中的内容,则窗口会包含满足WHERE条件的所有记录,窗口函数会基于所有满足WHERE条件的记录进行计算。
    • 如果OVER关键字后面的括号不为空,则可以使用如下语法设置窗口。
  • 窗口名:为窗口设置一个别名,用来标识窗口。
  • PARTITION BY子句:指定窗口函数按照哪些字段进行分组。分组后,窗口函数可以在每个分组中分别执行。
  • ORDER BY子句:指定窗口函数按照哪些字段进行排序。执行排序操作使窗口函数按照排序后的数据记录的顺序进行编号。
  • FRAME子句:为分区中的某个子集定义规则,可以用来作为滑动窗口使用。

18.2.3、分类使用

1、序号函数

  • ROW_NUMBER():能够对数据中的序号进行顺序显示。
  • RANK():能够对序号进行并列排序,并且会跳过重复的序号,比如序号为1、1、3。
  • DENSE_RANK():对序号进行并列排序,并且不会跳过重复的序号,比如序号为1、1、2。

2、分布函数

  • PERCENT_RANK():PERCENT_RANK()函数是等级值百分比函数。计算公式(rank - 1) / (rows - 1),其中,rank的值为使用RANK()函数产生的序号,rows的值为当前窗口的总记录数。
  • CUME_DIST():主要用于查询小于或等于某个值的比例。

3、前后函数

  • LAG(expr,n):返回当前行的前n行的expr的值。
  • LEAD(expr,n):返回当前行的后n行的expr的值。

4、首尾函数

  • FIRST_VALUE(expr):返回第一个expr的值
  • LAST_VALUE(expr):返回最后一个expr的值。

5、其他函数

  • NTH_VALUE(expr,n):返回第n个expr的值。
  • NTILE(n):将分区中的有序数据分为n个桶,记录桶编号。

窗口函数的特点是可以分组,而且可以在分组内排序。另外,窗口函数不会因为分组而减少原表中的行数,这对我们在原表数据的基础上进行统计和排序非常有用。

#第18章_MySQL8.0的其它新特性的课后练习
#1. 创建students数据表,如下
CREATE DATABASE test18_mysql8;
USE test18_mysql8;
CREATE TABLE students(
id INT PRIMARY KEY AUTO_INCREMENT,
student VARCHAR(15),
points TINYINT
);

#2. 向表中添加数据如下
INSERT INTO students(student,points)
VALUES
('张三',89),
('李四',77),
('王五',88),
('赵六',90),
('孙七',90),
('周八',88);
SELECT * FROM students;

#3. 分别使用RANK()、DENSE_RANK() 和 ROW_NUMBER()函数对学生成绩降序排列情况进行显示
#方式1SELECT 
ROW_NUMBER() OVER (ORDER BY points DESC) AS "排序1",
RANK() OVER (ORDER BY points DESC) AS "排序2",
DENSE_RANK() OVER (ORDER BY points DESC) AS "排序3",
student,points
FROM students;

#方式2SELECT 
ROW_NUMBER() OVER w AS "排序1",
RANK() OVER w AS "排序2",
DENSE_RANK() OVER w AS "排序3",
student,points
FROM students WINDOW w AS (ORDER BY points DESC);

18.3、新特性2:公用表表达式

公用表表达式(或通用表表达式)简称为CTE(Common Table Expressions)。CTE是一个命名的临时结果集,作用范围是当前语句。CTE可以理解成一个可以复用的子查询,当然跟子查询还是有点区别的,CTE可以引用其他CTE,但子查询不能引用其他子查询。所以,可以考虑代替子查询。依据语法结构和执行方式的不同,公用表表达式分为 普通公用表表达式递归公用表表达式 2 种。

18.3.1、普通公用表表达式

普通公用表表达式的语法结构是:

WITH CTE名称
AS (子查询)
SELECT|DELETE|UPDATE 语句;

普通公用表表达式类似于子查询,不过,跟子查询不同的是,它可以被多次引用,而且可以被其他的普通公用表表达式所引用。

# 举例:查询员工所在的部门的详细信息。
# 方式1:子查询
mysql> SELECT * FROM departments
    -> WHERE department_id IN (
    -> SELECT DISTINCT department_id
    -> FROM employees
    -> );
# 方式2:
mysql> WITH emp_dept_id
    -> AS (SELECT DISTINCT department_id FROM employees)
    -> SELECT *
    -> FROM departments d JOIN emp_dept_id e
    -> ON d.department_id = e.department_id;

注意:跟子查询相比,公用表表达式有一个优点,就是定义过公用表表达式之后的查询,可以像一个表一样多次引用公用表表达式,而子查询则不能。

18.3.2、递归公用表表达式

递归公用表表达式也是一种公用表表达式,只不过,除了普通公用表表达式的特点以外,它还有自己的特点,就是可以调用自己。它的语法结构是:

WITH RECURSIVE
CTE名称 AS (子查询)
SELECT|DELETE|UPDATE 语句;

递归公用表表达式由 2 部分组成,分别是种子查询和递归查询,中间通过关键字 UNION [ALL]进行连接。这里的种子查询,意思就是获得递归的初始值。这个查询只会运行一次,以创建初始数据集,之后递归查询会一直执行,直到没有任何新的查询数据产生,递归返回。

# 案例:针对于我们常用的employees表,包含employee_id,last_name和manager_id三个字段。如果a是b的管理者,那么,我们可以把b叫做a的下属,如果同时b又是c的管理者,那么c就是b的下属,是a的下下属
WITH RECURSIVE cte
AS (
SELECT employee_id,last_name,manager_id,1 AS n FROM employees WHERE employee_id = 100
-- 种子查询,找到第一代领导
UNION ALL
SELECT a.employee_id,a.last_name,a.manager_id,n+1 FROM employees AS a JOIN cte
ON (a.manager_id = cte.employee_id) -- 递归查询,找出以递归公用表表达式的人为领导的人
)
SELECT employee_id,last_name FROM cte WHERE n >= 3;

递归公用表表达式对于查询一个有共同的根节点的树形结构数据,非常有用。它可以不受层级的限制,轻松查出所有节点的数据。如果用其他的查询方式,就比较复杂。

posted @   Yxz-smile  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示