MySQL操作
- 认识 MySQL
- 什么是 MySQL
- 关系型数据库的组成结构
- "客户端-服务器"结构
- 数据库的基本操作
- 创建数据库
- 查看数据库
- 删除数据库
- 使用数据库
- 数据类型
- 整型
- 浮点类型
- 字符串类型
- 日期类型
- 总结
- 表的操作
- 创建表
- 查看表
- 查看表的信息
- 删除表
- 数据的基础操作
- 插入数据
- 指定列插入
- 全列插入
- 插入查询集
- 查找数据
- 指定列查询
- 全列查询
- 条件查询
- 分页查询
- 表达式查询
- 别名查询
- 排序查询
- 去重查询
- 总结
- 删除数据
- 修改数据
- 数据库约束
- 约束的概念
- NOT NULL
- UNIQUE
- DEFAULT
- PRIMARY KEY
- FOREIGN KEY
- 外键的创建
- 涉及到外键的删除
- 表的设计
- 聚合查询
- count()
- sum()
- avg()
- max()与min()
- group by 与 having
- 多表查询
- 笛卡尔积
- 内连接
- 外连接
- 自连接
- 子查询
- 合并查询
认识 MySQL
什么是 MySQL
MySQL 是一个用于存储大量数据的数据库管理软件, 一般我们也会直接叫这种软件为数据库. 它主要的功能就是能够将大量的数据有组织的存储起来, 便于我们增删查改, 从而实现一些涉及到大量数据的业务逻辑. 目前, 我们就只需要知道, MySQL 他就是用于把数据有组织的存储起来的, 能够支持我们进行对数据进行增删查改, 那么就足够了.
关系型数据库的组成结构
我们接下来学习的MySQL数据库, 就是一种关系型数据库, 为了能够在后续的使用中更好的理解和记忆各种操作, 我们优先介绍一下关系型数据库的组成结构.
关系型数据库从逻辑结构上来看就是由一张张表格组成的, 其中这种表格由两个部分组成: 抽象的列, 和指向实际数据的行
其中列就可以看作是Java中的类, 是一个类型, 其中列里面也有各种抽象的属性(字段), 用于描述现实中的数据. 行就可以看作是实例化的对象, 讲列里面的抽象属性进行了实例化, 是实际存在的数据.
例如我有一个数据库用于存储商场里面的商品的信息, 其中肯定要一张商品的表用于存储商品信息, 而这个商品表的列就可以包含各种字段来描述商品, 例如: 商品名称, 商品价格, 商品描述等等. 然后其中的行就是各个实际存储的信息
以下面这个表为例子(价格随手定的)
商品名称 | 商品价格 | 商品描述 |
---|---|---|
苹果 | 10 | 水果 |
白菜 | 5 | 蔬菜 |
矿泉水 | 2 | 饮品 |
"客户端-服务器"结构
接下来我们再来看一个结构, 叫做"客户端-服务器"结构, 为什么要了解这个结构, 因为我们即将要学习的MySQL数据库就是一个"客户端-服务器"结构的软件, 了解这个结构能够帮助我们更好的理解这个数据库的一些运行方式
该结构顾名思义, 由客户端和服务器两个部分组成, 那么客户端和服务器有什么区别呢?其中, 客户端(Client)是指发出请求的一方, 此时客户端会对服务器发送一些数据, 这些数据被称作为请求(Request). 而服务器(Server)则是接收请求的那一方, 接收请求后会返回一些数据, 这些数据被称作为响应(Response). 其中两个部分的数据传输是通过网络通信来进行的
如果用更简单的话描述一下客户端和服务器: 客户端是主动发信的一方, 而服务器是被动接收的一方. 举一个很简单的例子, 服务器就是类似于餐馆这样的公共设施, 用于接待客人, 也就是接收客人人要吃饭的请求(接收请求)并且做出相应的饭菜(做出响应), 是被动接收请求的一方. 而客人就是客户端, 会发出自己要吃饭的请求(发出请求), 是主动发出请求的一方.
服务器作为接收请求的一方, 大部分情况下有两个特点:
- 一般情况下服务器是要一直开启的, 因为服务器并不知道什么时候会有客户端对其发送请求, 因此需要一直开启来随时等待着客户端的请求. 拿上例中的餐馆也是一样的意思, 餐馆在它的工作时间内是一直开启的, 因为不知道什么时候会有客人来吃饭.
- 一般的服务器都是要接收来自很多客户端的请求的, 但是也有特殊情况的存在, 也会有服务器给少数的一两个客户端提供服务.
数据库的基本操作
创建数据库
理清了关系型数据库的组成逻辑, 那么接下来我们来看数据库的创建, 语法如下
create database 数据库名称;
注意:
- MySQL 中并不区分大小写, 你无论输入大写还是小写甚至大小写夹着写也不会有任何问题
- 分号是命令的结束标志, 如果不输入分号按下了回车并不会执行命令. 同时可以运用这个特点进行多行输入
- 如果显示报错优先检查自己是否拼写错误
另外在创建数据库的时候还有一些可选的语法, 我们依次来看
create database if not exists 数据库名称;
这个多加的一段直接通过英文翻译过来实际上也很好懂, 如果当前数据库列表中已经有了同名的数据库那就不执行任何操作, 如果没有同名数据库则创建. 但有些人可能就会产生一个问题: 创建一个数据库的时候如果有重名情况那它会报错, 那我看到它报错了我再换个名字不就行了, 那这个语法有什么用呢?
实际上在后期我们执行 SQL 语句的时候, 一般是写好一系列指令然后一起执行的, 但是如果中间有某一句指令报错的话, 那么后面所有的指令都不会再执行了. 这个语法就是为了防止这种情况发生
接下来来看下一个
create database 数据库名称 charset 字符集;
这个语句主要是用来指定创建的数据库的字符集的, 由于 MySQL 默认的字符集是不支持中文的, 所以我们在创建数据库的时候一般都需要自己手动指定字符集, 一般指定为 utf8 或者 utf8mb4. 其中 utf8 和 utf8mb4 的区别在于, 前者不包含 emoji 表情, 而后者包含 emoji 表情
另外上面提到的两个语法我们是可以混合一起用的, 举一个简单的例子
create database if not exists test charset utf8mb4;
上面这条语句我们创建了一个名字为test的数据库, 字符集设定为了utf8mb4, 并且在前面加上了IF NOT EXISTS防止重名报错
查看数据库
我们可以用下面的命令来查看数据库的列表
show databases;
输入上述命令后就会显示出数据库列表, 输入后我们会发现里面除了我们创建的数据库外, 还有一些自带的数据库, 例如mysql, information_schema, performance_schema, sys. 这些数据库存储着MySQL的服务器运行和管理所需的信息, 非必要不要去修改这些数据库内部的信息
删除数据库
我们可以用下面的命令来删除一个数据库
drop database 数据库名称;
当然也可以加一个IF EXISTS关键字
drop databast if exists 数据库名称;
意思和创建的那个差不多, 只不过是反过来的, 如果有这个名称的数据库存在就删库, 没有就不执行任何操作
但是注意: 删库是一个非常非常危险的操作, 因为删库就是直接将整个数据库清除, 包括里面的表, 表里面包含的数据. 并且数据库没有像你的电脑一样有回收站, 一旦删除无法撤销, 若要进行删库操作一定要经过再三确认
使用数据库
如果我们要对某一个数据库进行操作, 则一定要先选择要对哪一个数据库进行操作, 那么就可以用下面的命令
use 数据库名称;
选择成功后, 我们就可以执行往里面添加表, 删除里面的表, 或者是修改里面表内的数据等等操作了
数据类型
知道了如何操作数据库, 我们接下来就要开始学习有关表的操作, 但是由于创建表的过程中涉及到一些数据类型的知识, 所以我们这里先看以下MySQL中的数据类型
整型
数据类型 | 大小 | 说明 |
---|---|---|
BIT[ (M) ] | M指定占用的二进制位数, 默认M为1 | 二进制数, M范围从1到64, 存储数值范围从0到2^M-1 |
TINYINT | 1字节 | 对应Java中的Byte |
SMALLINT | 2字节 | 对应Java中的Short |
INT | 4字节 | 对应Java中的Int |
BIGINT | 8字节 | 对应Java中的Long |
其中常用的类型为INT, 有时如果长度不够就会使用 BIGINT
浮点类型
数据类型 | 大小 | 说明 |
---|---|---|
FLOAT(M, D) | 4字节 | 对应Java中的Float, 会发生精度丢失 |
DOUBLE(M, D) | 8字节 | 对应Java中的Double, 会发生精度丢失 |
DECIMAL(M, D) | M/D的最大值+2 | 对应Java中的BigDecimal, 精度高 |
NUMERIC(M, D) | M/D的最大值+2 | 对应Java中的BigDecimal, 精度高 |
(M, D) 的 M 用于指定数字的长度(有几个数字数字的长度就是几), D 用来指定小数点后的位数
其中, 如果精度要求不高则常用DOUBLE类型, 如果精度要求高常用 DECIMAL(M, D) 类型. 虽然 DECIMAL 类型精度更高, 但是其占用的内存也更多, 运算效率也更慢
字符串类型
数据类型 | 大小 | 说明 |
---|---|---|
VARCHAR (SIZE) | 0-65,535字节 | 可变长度字符串 |
TEXT | 0-65,535字节 | 长文本数据 |
MEDIUMTEXT | 0-16777215字节 | 中等长度文本数据 |
BLOB | 0-65,535字节 | 二进制形式的长文本数据 |
其中常用的类型为 VARCHAR (SIZE), SIZE 为指定的长度
日期类型
数据类型 | 大小 | 说明 |
---|---|---|
DATETIME | 8字节 | 范围从1000到9999年, 不会进行时区的检索及转换 |
TIMESTAMP | 4字节 | 范围从1970到2038年, 自动检索当前时区并进行转换. |
一般现在常用 DATETIME, 不用担心超过存储范围
总结
上面说了那么多类型, 没记住几个也没有问题, 主要用的就下面额外提出常用的那几个:INT, DOUBLE, DECIMAL, VARCHAR, DATETIME. 我们在使用的时候多用用自然就记住了, 其他的类型如果有情况用到了再查就行
表的操作
注意, 在执行下列表的操作之前, 我们要先选择要使用哪个数据库, 也就是执行下面语句
use 数据库名称;
创建表
创建表的语法如下
create table 表名(变量名1 变量类型1, 变量名2 变量类型2, .....);
我们在创建表的时候必须提前指定表中的各个列, 也就是提前规定好这个表要存储的数据的各项属性, 此时就会用到上面的数据类型
例如我们要创建一个学生表, 里面要有学生的学号信息, 名字信息, 性别信息, 此时就可以用下面的语句创建
create table student(id int, name varchar(10), gender varchar(5));
此时数据库是我们自己学习用的, 所以这里指定的字符串长度并没有什么限制, 感觉是多少就多少, 在工作中具体多少看安排
那接下来再看一个实例
设计一张老师表, 包含以下字段: 姓名, 年龄, 身高, 体重, 性别, 生日, 身份证号
这里大部分并没有什么好注意的, 主要要提的就是这里有一个生日, 我们可以使用字符串类型来表示, 但也可以用上面提到过的日期类型来表示
create table teacher
(name varchar(10),age int,height int,weight int,gender varchar(5),birthday datetime,id_num varchar(20)
);
这里还利用了MySQL可以分行输入的特性, 使得我们能够更加直观的看出有什么样的属性
另外创建表的时候也可以利用IF NOT EXISTS语法, 如下
create table if not exists student
(id int,name varchar(10),gender varchar(5)
);
查看表
假如我们不知道当前数据库里有什么表, 我们可以使用下面的语句来查看当前数据库有什么表
show tables;
比如我们刚刚创建了两个表, 那么里面就会显示有一个 teacher表 和 student 表
查看表的信息
有的时候我们可能也不清楚一张表里面有什么列, 那么此时我们就可以用下面的命令来看
desc 表名;
我们以上面的学生表为例
使用了上述指令后, 就会展示如上这样的表格, 其中展示了这个表的3个列, 这个属性表格里面的每一行对应数据库存储的表里的每一列. 这张属性表格里面除了描述当前列的数据类型以外, 也描述了一些列的其他属性, 比如: 是否能为空值, 索引类型, 默认类型等等. 这些其他的属性暂时不用管, 在后续会讲到
删除表
能创建表那当然也就能删除表, 使用如下的语法就可以删除一张表
drop table 表名;
此时删除也可以用if exists
, 如下
drop table if exists student;
依旧是要注意, 删除表也代表删除这张表下面的所有数据, 并且删除操作无法撤销, 在删除前一定要慎重考虑
数据的基础操作
插入数据
指定列插入
虽然一般来说, 增删查改我们对应的英文缩写是 CRUD, 其中 C 是 create 代表增, 但是实际上用的关键字并不是create, 我们来看增加数据的语法
insert into 表名 (列名1,列名2....) values(对应数据1), (对应数据2).....;
此时有人如果是第一次学习 SQL 语言, 可能看完这一段语法, 发现似乎有一点点抽象, 怎么和我学过的那些编程语言不是一个画风? 因此为了帮助这些初学 SQL 的人理解这里到底在说什么, 我们直接来看一个例子
首先我们创建一个学生表, 表中包含两个属性: 学生代号和学生名字
create table if not exists student(id int, name varchar(20));
此时我想要给里面加几个数据: 1号张三, 2号李四, 3号王五
那么按照上面的指示, 我们就可以这样写
insert into student (id, name) values(1, '张三'), (2, '李四'), (3, '王五');
注意: MySQL中的字符串既可以用双引号括起来也可以用单引号括起来, 都表示字符串
现在我们结合一下这个例子, 来看一下我们那个语法是什么意思. 实际上列名1, 列名2, 就是你要填入数据的列. 后面的对应数据1, 对应数据2, 就是对应着你前面指定的列输入的数据.
比如我现在要添加一个学生, 他只有代号4, 但是没有名字, 那么就应该这样写(此时没有给予数据的这个位置会自动置空)
insert into student (id) values(4);
那现在此时可能就有人有问题了: 我上面添加三条数据的时候能不能一条一条添加呢?答案是: 当然可以. 但是这样效率会比我们一次性插入三条数据更低, 这是为什么呢?
我们回忆一下上节我们说过, MySQL是一个客户端-服务器结构的软件, 我们操作的客户端每进行一个操作就要发给服务器一个请求, 随后服务器再返回一个响应, 那么如果我们分别3次进行请求, 那么服务器也就要返回3次响应. 但是如果我们一次性直接插入3个数据, 那么我们就只用请求1次, 服务器也只用响应1次, 这样效率就会有明显提升. 依旧是以餐馆为例, 假如我是一个客人, 我要点三个菜, 那么假如我每一次都只说一个菜, 然后等餐馆炒完一个菜我再给它讲我下一个菜要什么, 这肯定没有我一次性给餐馆说: “我就要这三个菜” 来的快
全列插入
另外, 我们也可以在插入数据的时候不写我们要给什么列输入数据, 但这样就是默认你要给所有列插入数据, 并且如果此时你给的数据类型和列对不上就会直接报错(列的顺序和你创建的时候给的顺序对应)
比如我们把上面插入3个人的数据的语句修改一下
insert into student values(1, '张三'), (2, '李四'), (3, '王五');
但是如果此时我调转一下顺序就会报错
insert into student values('张三', 1);
包括如果我想插入一个只带id的数据也是不行的
插入查询集
查找数据
指定列查询
上面我们学会了怎么插入数据, 接下来如果想要看看我们插入的数据那就可以用到查找数据的语句, 语法如下
select 列名1, 列名2.... from 表名;
比如我想看看我刚刚插入的学生代号和学生名字, 我就可以输入下面的语句(此时列名不要括号)
select id, name from student;
全列查询
同样的我们这里也可以进行全列查询, 但是并不是什么都不写, 而是要写一个*号
select * from 表名;
但是注意, 在实际使用数据库的时候, 这是一个危险操作. 为什么?因为在实际的应用场景中, 数据库中存储的数据量是很大的, 一次性对所有的数据进行查询, 可能会导致数据库服务器的卡顿, 导致数据库服务器无法被访问, 从而造成不可估量的损失. 因此我们在查询数据的时候, 一般不会查询全列, 或者是会加一些条件再进行查找, 从而对数据进行筛选, 只要找到我们要的数据就行, 不要全部都查找出来
条件查询
条件查询, 顾名思义就是能够通过一定的条件筛选出一些数据, 这样就能够更加迅速的获取需要的数据
select 列... from 表名 where 条件表达式;
要进行条件查询, 首先我们要借用 where 关键字, 然后再通过条件表达式来写出筛选条件, 那么如何写一个条件表达式呢?
我们先来看一些用于书写条件表达式的运算符
条件运算符
运算符 | 意义 |
---|---|
>, <, >=, <= | 大于, 小于, 大于等于, 小于等于 |
= | 等于(无法进行NULL运算) |
<=> | 等于(可以进行NULL运算) |
!=, <> | 不等于(不能进行NULL运算) |
IS NULL | 是NULL |
IS NOT NULL | 不是NULL |
BETWEEN…AND… | 范围匹配, 两边都是闭区间, 如果数据在范围内为TRUE(1) |
IN(…) | 如果数据是括号中的任意一个数据则结果为TRUE(1) |
LIKE | 模糊匹配, 详情看案例 |
逻辑运算符
运算符 | 意义 |
---|---|
AND | 与 |
OR | 或 |
! | 非 |
这里的运算符也是有优先级的, 但是我们没有任何必要去记忆, 如果不想因为运算符优先级导致出现不符合需求的查询结果则可以多加一些括号
接下来我们来通过一些实例来演示上列的所有运算符的效果
首先创建一张成绩表, 并且插入一些数据
create table exam_result(id int, name varchar(20),chinese decimal(3, 1), math decimal(3, 1), english decimal(3, 1));
insert into exam_result values(1, '张三', 98.0, 85.5, 87.5),(2, '李四', 87.0, 60.5, 75.5),(3, '王五', 64.5, 70.0, 90.0),(4, '赵六', 77.0, 95.5, 69.0),(5, '田七', 88.0, 74.5, 86.0),(6, '孙行者', 50.0, 60.0, 70.0),(7, '行孙者', 83.0, 75.0, 60.0),(8, '者行孙', 99.0, 93.5, 83.0),(9, '孙权', 87.5, 83.5, 88.0);
首先我们看前面几个< > <= >=, 非常简单直接上案例
-- 查询语文大于85分的人的所有信息
select * from exam_result where chinese > 85;
其他的几个运算符也是同理于是不再展示, 接下来看=, 也非常简单直接上案例
-- 查询数学成绩为75分的人的所有信息
select * from exam_result where math = 75;
但是注意, 这个=如果在比较过程中遇到了NULL, 则结果直接为NULL, 也就是FALSE
例如我们这里再插入一个成绩带有NULL的数据
insert into exam_result values(10, '唐三藏', NULL, NULL, NULL);
此时我们如果尝试用=去查找这个带有NULL的数据, 则会发现什么都找不到
select * from exam_result where chinese = NULL;
所以如果要筛选带有NULL的数据, 则要用<=>或者IS NULL
select * from exam_result where chinese <=> NULL;
-- 或者用下面的写法
select * from exam_result where chinese IS NULL;
这部分!= <>也是同理, 也不能和NULL进行比较, 如果要比较不为NULL则应该使用IS NOT NULL, 或者将逻辑运算符!和<=>配合使用, 两个正确的比较案例如下
select * from exam_result where chinese IS NOT NULL;
-- 或者可以用下面的写法 当前还没有讲解!的使用 看一下即可
select * from exam_result where !(chinese <=> NULL);
接下来看3个比较特殊一点的条件运算符, 首先是BETWEEN…AND…, 直接先看一个案例
-- 搜索数学成绩在 60 到 70 的人
select * from exam_result where math BETWEEN 60 AND 70;
此时我们可以发现数学成绩等于 60 和等于 70 的 人也被查询进来了, 因为BETWEEN…AND…的两边边界都是闭区间(也就是会包括边界值)
IN虽然看起来有点抽象, 但是我们只要一看例子立马就懂, 直接上例子
-- 搜索数学成绩等于60, 70, 80, 90的人
select * from exam_result where math IN (60, 70, 80, 90);
这个实际上就是只会筛选出等于我们所列出的情况的数据, 相当于将多个等于号浓缩在了一起
LIKE 一般用于执行字符串查询, 比如我可以用它实现: 查询一个姓李的人的成绩, 查询一个名字中含有孙的人的成绩
在使用 LIKE 进行查询的时候, 我们用两个特殊符号和字符串组合从而达到模糊匹配的效果: % 用于替代任意个字符, 包括零个字符, _ 只能用于替代一个字符
我们直接看例子, 结合每一句查询语句和上面的注释来理解模糊匹配的规则
-- 查询名字里面带孙字的人
select * from exam_result where name LIKE '%孙%';-- 查询名字为两个字且姓王的人
select * from exam_result where name LIKE '王_';-- 查询名字第二个字为孙字的人
select * from exam_result where name LIKE '_孙%';-- 查询名字里面最后一个字为孙字的人
select * from exam_result where name LIKE '%孙';-- 查询姓孙的人
select * from exam_result where name LIKE '孙%';
逻辑运算符其实也非常好理解, 逻辑运算符就是对已有的条件表达式进行操作, 直接看几个例子
-- 搜索数学大于80分, 并且英语小于90分的人
select * from exam_result where math > 80 AND english < 90;-- 搜索语文小于80分, 或者英语大于80分的人
select * from exam_result where chinese < 80 OR english > 80;
或者是我要对条件进行反转: 搜索数学在90分以上的人, 此时我可以直接写math > 90, 但也可以利用!逻辑取反来达到相同的结果
-- 搜索数学在90分以上的人
select * from exam_result where !(math <= 90);
指的一提的是, AND 和 OR 同时使用的时候可能就会发生一些运算符优先级的问题, 因此在多次同时使用两者的时候建议加上括号
分页查询
有些时候我们即使运用了条件查询, 一次性展示的数据还是有点多, 我们此时希望能够通过其他的方式来进一步减少数据的展示量, 那么就可以使用分页查询来做到这一点, 语法如下
select * from exam_result LIMIT 单次查询的数据量 OFFSET 偏移量
单次查询的数据量很好理解, 就是我们一次要查的数据的数量, 偏移量则是指的要从哪个位置开始查询(第一个位置的偏移量为 0, 类似于数组的下标)
这里依旧借用上一部分的成绩表中的数据来进行举例, 直接看例子
-- 查询前三条数据
select * from exam_result LIMIT 3 OFFSET 0;
如果要从第4条数据开始查询, 那么我们就将OFFSET修改为3即可, 其他的以此类推
-- 从第四条数据开始查询
select * from exam_result LIMIT 3 OFFSET 3;
另外还有一些写法, 我们简单看一下即可, 我们使用分页查询时主要使用上面的那种方式即可
-- 查询x条数据, 默认OFFSET为0
select * from exam_result LIMIT x;-- 查询x条数据, OFFSET为y
select * from exam_result LIMIT y, x;
表达式查询
表达式查询支持我们在查询的时候直接对列进行运算, 例如我们想要查询同学们成绩的总和, 此时并不需要我们创建一张新的表并且新建一列自己算, 我们可以直接使用表达式查询, 语法如下
select 列名/表达式 from exam_result;
这里就以查找同学们的总成绩为例子
-- 查询语文+数学+英语的总和
select id, name, chinese + math + english from exam_result;
包括我们也可以查询语文减去10分, 英语加上10分等等类似的表达式
-- 查询数学+10分后的结果
select id, name, math + 10 from exam_result;
此时可能有人产生了一些问题: 我的这个表达式操作, 会不会影响我们数据库中存储的数据呢?
答案是并不会, 首先我们执行的本身就不是修改的操作, 而是查询的操作. 并且我们此时看到的表实际上并不是数据库中实际的表, 而是一张服务器返回给我们看的临时表, 因此即使我们再怎么修改这张临时表上面的数据, 数据库存储的数据并不会改变
别名查询
使用了表达式查询后, 我们会发现有时候一些表达式过于累赘, 一大串在那里非常难看并且不直观, 那么此时我们就可以使用别名查询, 给我们的表达式取一个别名, 语法如下
select 列名/表达式名 AS 别名 from 表名;
比如我们将上面查询总分的 chinese + math + english 取一个别名为 total
-- 查询语文+数学+英语的总和, 并且将表达式命名为total
select id, name, chinese + math + english AS total from exam_result;
并且我们还可以对列名进行别名查询, 也可以同时取多个别名
-- 分别赋予别名
select id AS student_id, name AS student_name, chinese + math + english AS total from exam_result;
排序查询
我们还可以对查询的数据进行排序, 语法如下
select 列名 from 表名 ORDER BY 列名 排序方式;
其中排序方式有两种: asc和desc, 分别指代了升序和降序(也可以不指定排序方式, 此时默认为升序)
例如, 我要查询以数学成绩降序排序的数据
-- 根据数学成绩降序排序
select id, name, math from exam_result ORDER BY math DESC;
另外我们也可以利用别名来进行排序, 但是前提是一定要在前面设置过这个别名
例如, 给求总分的表达式设置一个别名 total, 并且使用 total 给查询出的数据进行排序
-- 根据总成绩降序排序
select id, name, chinese + math + english AS total from exam_result ORDER BY total DESC;
我们还可以利用我们没查询的列来进行排序
例如, 我们查询语文成绩, 但是是以英语成绩来排序的
-- 根据英语成绩升序排序, 但是没有查询英语列
select id, name, chinese from exam_result ORDER BY english ASC;
去重查询
去重查询, 顾名思义就是能够帮我们把内容相同的数据进行去重, 只展示一次, 语法如下
select DISTINCT 列名 from 表名;
为了能够测试效果, 我们先往这个成绩表里面加两个数学成绩相同的数据
insert into exam_result (id, name, math) values(11, '孙悟空', 90.0), (12, '沙悟净', 90.0);
然后我们先不用去重直接查询一波数学成绩, 并且为了能够直观观察我们使用一个排序
select math from exam_result ORDER BY math;
此时我们会发现数学成绩为90.0的那个数据出现了两次
接下来我们加上去重关键字再查询一次
select DISTINCT math from exam_result ORDER BY math;
那么这个时候我们就会发现, 数学成绩为90.0的那个数据就只会出现一次了
但是注意, 我们这个时候如果再查询一个id或者name这样的数据, 那么此时这两个数学成绩为90.0的数据就不会再被认为是重复的了, 也就不会被去重
总结
上面讲的很多不同的查询模式, 上面讲述的时候为了方便理解没有混合使用, 但是实际上也是可以混合进行使用的, 并不是说我每一次只能用一个查询方法, 例如我可以排序后进行分页查询等等类似操作. 但是这个就交给各位自行研究了
删除数据
删除数据的语法一般情况下是要借用条件查询的条件语法的, 语法如下
delete from exam_result where 条件表达式;
直接看例子
-- 删除全部成绩都为空的数据
delete from exam_result where chinese IS NULL AND math IS NULL AND english IS NULL
删除并没有什么好多讲的, 主要核心就是条件语法的书写
实际上我们也可以不使用任何条件语法, 但是这样就是一个危险操作, 是直接删除整个表的数据, 不推荐使用, 使用前务必确认自己要执行这样的操作
修改数据
修改数据一般也需要借用条件查询的条件语法, 否则后果同上, 修改数据的语法如下
update 表名 set 要修改的列 = 要设定的数据 where 条件表达式;
直接看一个例子
-- 将11号同学的英语成绩修改为85.5分
update exam_result set english = 85.5 where id = 11;
我们还可以直接给要要修改的列直接以自身为标准进行修改
-- 将11号同学的英语成绩加10分
update exam_result set english = english + 10 where id = 11;
但是此时假如我们尝试对这里面的列乘二那就可能会报错
-- 给11号同学的英语成绩乘2
update exam_result set english = english * 2 where id = 11;
因为此时我们的结果数据超出了DECIMAL(3, 1)的表达范围, 因此在执行修改操作的时候要注意不要超出数据类型的表达范围
如果不指定条件, 这里也会直接修改该列中所有行的数据, 务必在执行这样的操作的时候反复确认, 不要因为误操作导致不可挽回的后果
数据库约束
约束的概念
约束, 简单的说就是一种限制条件, 我们在创建数据库的时候, 可以给一些列加上一些限制条件, 从而让这些列遵循一些规则.
例如我们管理用户的时候, 我们往往会让用户名或者用户id不能重复, 但是如果要我们自己检查每一列的具体值就会很麻烦. 此时我们就可以通过约束来实现这一点, 下面是就是Mysql的一些常用约束
NOT NULL
这个顾名思义, 就是指定这一列不能为空的意思
drop table if exists student;
create table student(-- 指定学生id和学生名不能为空id int not null,student_name varchar(20) not null
)
UNIQUE
unique, 翻译过来就是唯一, 顾名思义就是指定这一列的值必须唯一, 不能重复
drop table if exists student;
create table student(-- 指定学生id不能为空且唯一id int not null unique,student_name varchar(20) not null
)
DEFAULT
默认值约束, 当我们插入数据的时候不给这一列赋值, 那么就会自动赋予默认值
drop table if exists student;
create table student(id int not null unique,student_name varchar(20) not null,-- 指定学生性别默认为 未选择gender varchar(10) default '未选择'
)
此时可能有人要问了, 那假如我的表里面所有的列都有默认值, 且我希望插入一条数据里面全都是这个默认值, 那么应该怎么写呢? 一列都不写吗?
在这种情况, 我们可以借助DEFAULT
本身作为一个默认值的代表值来进行插入操作, 下面是一个例子
drop table if exists student;
create table student(-- 指定默认值student_name varchar(20) default '未填写',gender varchar(10) default '未选择'
);-- 插入一行, 两个属性都是默认值
insert into student(student_name, gender) values(default, default);
PRIMARY KEY
主键约束, 相当于NOT NULL和UNIQUE的组合, 用于给每一列作为一个身份标识. 假如有一个用户表如下
用户ID | 用户名 | 密码 |
---|---|---|
1 | zhangsan | 123 |
2 | lisi | 123 |
3 | wangwu | 123 |
假如此时我希望这个用户ID, 可以作为每一个用户的独立身份标识, 不要重复, 此时我就可以令其为一个主键. 如下所示
drop table if exists user;
create table user(-- 指定user_id为主键user_id int primary key,userame varchar(10) not null,password varchar(10) not null
);
同时, 对于这种类型为整数类型的主键, 我们还有一个常用的配合约束就是自增约束 auto_increment. 这个约束又是用来干嘛的呢? 我们先创建一个自增主键的表, 然后插入数据观察效果
drop table if exists user;
create table user
(-- 指定user_id为自增主键user_id int primary key auto_increment,username varchar(10) not null,password varchar(10) not null
);-- 插入三条数据
insert into user (username, password)
values('zhangsan', '123456');
insert into user (username, password)
values('lisi', '123456');
insert into user (username, password)
values('wangwu', '123456');-- 查看
此时我们可以看到, 我们并没有手动的去指定 user_id 这个属性的内容, 但是会发现一个神奇的事情, 就是它自动的从 1 开始生成了这个 user_id.
这就是自增约束的作用, 他会自动的生成这个列的上一个值 + 1, 配合主键实现自增主键是其非常常用的有一种用法
FOREIGN KEY
外键的创建
外键约束, 这个约束就涉及到多张表了, 因此我们先创建两张表
-- 创建一个用户表
drop table if exists user;
create table user
(user_id int primary key auto_increment,username varchar(10) not null,password varchar(10) not null
);-- 插入三条数据
insert into user (username, password)
values('zhangsan', '123456');
insert into user (username, password)
values('lisi', '123456');
insert into user (username, password)
values('wangwu', '123456');-- 创建一个评论表
drop table if exists user_comment;
create table user_comment(user_id int,comment varchar(100) not null
);
这里可以看到, 我们创建了两张表, 一张是用户表, 一张是评论表, 同时评论表中的 user_id 应该是和 user表中的 user_id 对应的, 从而标识出这个评论是来自于哪个用户的.
那么此时如果出现下面的这种情况, 那肯定是不科学的
insert into user_comment values(4, '第一条评论');
可以看到, 我们插入了一个 user_id 为 4 的评论. 但是问题在于, 我们的 user 表中根本就没有 user_id 为 4 的用户, 那么此时很明显是不科学的.
那么此时我们如何去限制这个评论表中的 user_id 必须是来自 user 表中的 user_id 呢? 此时就可以用到这个外键约束. 我们下面就将这个评论表中的 user_id 和用户表中的 user_id 建立一个外键约束. 然后再尝试插入一个 user_id 为 4 的评论
-- 创建一个带有外键约束的评论表
drop table if exists user_comment;
create table user_comment(user_id int,comment varchar(100) not null,-- 建立外键约束, 第一个 (user_id) 自身的 user_id 列-- 而第二个 user(user_id) 对应 user 表的 (user_id)foreign key (user_id) references user(user_id)
);
-- 尝试插入 user_id 为 4 的评论
insert into user_comment values(4, '第一条评论');
可以发现, 直接报错了.
当然, 对于外键约束具体是否应该使用, 这里还是有一些议论在的, 因为外键约束对于性能的影响较大, 在一些追求性能的场景下是不适用的. 此时可能有人要问了: 那我怎么保证这个 user_id 一定在 user 表里面呢?
实际上, 我们现在学习数据库操作的目的, 就是为了能够在后续通过代码操作数据库的时候, 能够写出对应的数据库语句. 并不是说我们以后都要自己手动一条一条的写, 这是不现实的.
那么当我们后面使用代码去操作数据库的时候, 我们就可以在代码层面上去自己实现一些检查的逻辑, 此时就可以实现即使不用外键约束, 也可以保证这个 user_id 一定存在的效果.
如果我们尝试使用下面的代码来创建外键约束
-- 创建一个用户表
drop table if exists user;
create table user
(user_id int,username varchar(10) not null,password varchar(10) not null
);-- 创建一个带有外键约束的评论表
drop table if exists user_comment;
create table user_comment
(user_id int,comment varchar(100) not null,foreign key (user_id) references user (user_id)
);
会发现无法创建外键约束, 这是因为我们创建外键约束的时候, 要求我们这里父表中被关联的这一列, 必须是一个主键或者是UNIQUE的, 这样主要是保证关联的数据一定是唯一的
涉及到外键的删除
此时可能有人问了: 前面的这个 user 表是被 user_comment 表参考的一个表, 那此时如果我们尝试删除这个 user 表会发生什么呢?
我们可以先进行一个尝试
-- 创建一个用户表
drop table if exists user;
create table user
(user_id int primary key auto_increment,username varchar(10) not null,password varchar(10) not null
);-- 创建一个带有外键约束的评论表
drop table if exists user_comment;
create table user_comment(user_id int,comment varchar(100) not null,-- 建立外键约束, 第一个 (user_id) 自身的 user_id 列-- 而第二个 user(user_id) 对应 user 表的 (user_id)foreign key (user_id) references user(user_id)
);-- 尝试删除 user 表
drop table user;
可以看到, 我们这里是无法删除父表, 实际上就是无法删除被参考的表, 在这个例子中就是 user表.
同理如果这里 user 表的数据如果被 user_comment 表中的某个数据参考了, 那么此时 user 表中的那条被参考的数据也是无法被删除的.
-- 创建一个用户表
drop table if exists user;
create table user
(user_id int primary key auto_increment,username varchar(10) not null,password varchar(10) not null
);-- 创建一个带有外键约束的评论表
drop table if exists user_comment;
create table user_comment
(user_id int,comment varchar(100) not null,foreign key (user_id) references user (user_id)
);-- 插入三条数据到user表中
insert into user (username, password)
values('zhangsan', '123456');
insert into user (username, password)
values('lisi', '123456');
insert into user (username, password)
values('wangwu', '123456');-- 插入一条数据到 user_comment 表中
insert into user_comment(user_id, comment) values(1, '今天天气不错');-- 尝试删除 user 表中被参考的数据
delete from user where user_id = 1;
可以发现, 同样是无法删除的
此时可能有人要说了: 你这个外键, 也太鸡肋了, 删个数据还这么麻烦, 这还有什么使用的必要吗?
那我们这里就先考虑一个情景: 在一个交流网站中, 同样也有上面的用户表和评论表, 此时假设有用户注销账户. 那么此时他的评论并不会消失, 在评论展示的地方会直接显示用户已注销
. 那么这里是如何实现的不删除子表中的评论, 而父表中的用户实现注销(删除)的呢?
实际上, 这里就可以通过给用户表创建一个新的列, 专门用于表示一个用户的状态. 如下所示
drop table if exists user;
create table user
(user_id int primary key auto_increment,username varchar(10) not null,password varchar(10) not null,-- 新建一个状态列, 用于表示用户状态status int default 1 comment '1-正常 0-注销'
);
可以看到, 我们这里就使用了一个 status 列来表示用户的状态, 一旦用户注销后, 就只需要更新这个列即可, 不需要真正的从数据库中删除这一条数据, 这也是一种非常常见的用于数据删除的方式, 这种删除又叫做逻辑删除.
此时可能有人问了: 那这样不占硬盘空间吗? 为什么不能直接删掉呢?
实际上对于企业来说, 数据相较于一些物理上的硬盘空间是更加有价值的, 因此绝大部分的公司都会去选择耗费一些物理空间来存储数据也不会去直接删除掉所有数据.
表的设计
表的设计, 我们一般就分为两个流程: 1. 找出需求中的实体 2. 处理实体之间的关系 3. 根据实体的关系, 设定表
例如我们要设计一个图书管理系统, 那么此时我们就可以找出两个实体: 1. 书籍 2. 用户
随后就是根据需求来查看他们之间的关系, 那假如我的需求就只有一个: 用户可以借书. 那么此时我们就可以考虑在这个需求中, 用户和书籍的关系是什么.
一般在考虑关系的时候, 我们主要就考虑三个情况: 1. 一对一 2. 一对多 3. 多对多
那么我们如何去看实体之间的关系呢? 其实非常简单, 直接带入到一个模板中看看是否合理即可. 这个模板如下所示
一对一:
- 一个 A 只能对应一个 B
- 一个 B 只能对应一个 A
一对多:
- 一个 A 只能对应一个 B
- 一个 B 可以对应多个 A
多对多:
- 一个 A 能够对应多个 B
- 一个 B 能够对应多个 A
那么此时我们就可以根据我们的需求, 去把实体套到上面的这个东西里, 如果符合我们的需求, 那他就是我们要的关系. 例如还是我们上面的例子, 我们现在要求用户和书籍的关系, 那么我们就开始依次带入, 首先是一对一
一个用户只能借一本书
一本书只能被一个用户借
很明显, 这个一对一似乎并不是很合理, 因为一个用户只能借一个书太局限了, 当然不排除有些场景要求这种情况, 我们就以日常生活中的情景来看. 那么一对一不行, 接下来就看一对多
一个用户能借多本书
一本书只能被一个用户借
很明显, 这个时候就是比较合理的了. 那么此时我们就可以确认用户和书的关系是一对多的, 其中用户是一, 书籍是多. 这里需要注意的是, 一对多不是双向的, 和一对一多对多不同, A -> B 和 B -> A 是不等价的, 例如我们也可以看看用户是多, 书籍是一的情况
一本书能被多个用户借
一个用户只能借一本书
很明显, 反过来之后这个就很不对劲了. 并不是一个合理的关系, 因此对于一对多, 我们需要正反都带入一下看看.
那么搞明白了如何确认关系, 接下来就是根据关系来去设定表了.
首先就是一对一的关系, 我们考虑一个学生姓名和一个学号, 很明显他们应该是一个一对一的关系, 那么我们如何去设定这个表呢? 其实很简单, 将他们放入同一个表中即可, 那么此时每一行都是一个一对一的关系
create table student(id int primary key,name varchar(20)
)
例如学号 1 是张三, 那么此时学号 1 就只能对应张三, 此时就符合我们一对一的关系.
此时可能有人要说了, 你这个例子我没看明白, 名字本来就是学生的, 我放在一个表里怎么了? 假如我有其他的实体在另一个表里面怎么办?
那此时我们来看另一个例子, 假设我们的书籍和学生是一对一的. 此时数据库里面有两个表, 学生表和书籍表, 如何表示他们的关系?
create table student(id int primary key,name varchar(20)
)create table book(id int primary key,title varchar(50)
)
此时思路也比较明显, 我们给其中任意一个表, 加一个属性放另外一个表的 id 不就行了? 例如我们的book 表中, 就可以存储对应借走的学生的 id. 那么此时表的设计如下
create table student(id int primary key,name varchar(20)
);create table book(id int primary key,title varchar(50),student_id int,-- 我们甚至可以创建一个外键约束foreign key (student_id) references student(id)
)
此时就可以看到, 我们的书籍就可以看作是和学生产生了关系, 如果想要知道哪个学生借了什么书, 就直接通过 student_id 来找即可. 同理, 如果我们想要看看这个书被谁借走了, 那么就看看 student_id 是多少就行.
当然, 这里也是通过在 student 表中设置一个 book_id 来做的, 原理相同, 不再阐述.
总而言之, 我们的一对一关系, 是可以通过在两个表中的任意一个表添加一列来实现的.
接下来就是一对多, 我们考虑一个用户对多个书籍. 实际上我们此时依旧是可以通过在 book 创建一列 student_id 来实现的.
因为对于每一本书, 最多都只有一个学生会与其对应, 此时直接看 student_id 就知道是哪一个学生了.
此时可能有人就要问了: 那我此时能否在 student 表里面创建 book_id 呢?
实际上对于 MySQL 来说, 是不可以的, 因为一个学生可以借多本书, 也就是说一个学生可能会对应多个book_id, 但是 MySQL 并不支持数组类型, 因此无法对应多个值, 所以并不能这样做.
总而言之, 对于一对多的关系, 我们依旧是可以通过在一个表中添加一列来进行的, 只不过这一列必须添加在一对多中多的那一方
接下来就是最后一个关系, 多对多. 这里我们假设是多个课程对应多个学生的关系, 那么此时很明显, 我们使用两个表大概率是无法表示的了. 那此时我们就可以通过创建一张额外的关系表来进行关系的记录.
那么此时我们先来看 SQL 的写法
create table student(id int primary key,name varchar(20)
);create table course(id int primary key,name varchar(20)
);create table student_course(student_id int,course_id int,-- 主键和外键, 可选primary key (student_id, course_id),foreign key (student_id) references student(id),foreign key (course_id) references course(id)
);
此时我们通过一个额外的表来表示多对多的关系, 其中每一行就代表一个关系. 此时可能稍微有点难理解, 我们通过一个图来看, 如下图所示
可以看到, 关联表通过一个一个对应的 id 信息, 表示了对应学生的选课信息
总而言之, 对于多对多的关系, 我们设计表的时候就可以创建一个关系表来表示它们的关系.
此时可能有人问了: 那如果不符合上面说的三个关系呢? 怎么设计呢?
如果不符合上面的三个关系, 那么此时就证明, 这两个实体没有关系, 此时就可以无需关心互相之间的关系, 直接根据自己的属性来设置即可.
这里可能有人想, 为什么你这里讲表的设计没有讲到什么范式之类的? 我听说表的设计不都是根据什么第一第二第三范式来的吗?
实际上按照范式去设计数据库表, 个人认为还是非常传统的, 这个主要是早期开发的约定, 因为早期的时候确实没有什么规定, 很多人想怎么写就怎么写, 因此需要这种约定来优化数据库的结构.
而当今在正常情况下, 当前我们设计表并不会说和以前很早期的一些开发人员一样, 按照自己的想法写代码, 甚至于所有东西塞到一个表里面. 而是更加趋向于选择对于每一个实体都对应一张表. 实际上这也算是前人总结的经验规律, 使得我们能够不去了解那些复杂的概念, 也能够创建出一系列不错的数据库设计.
其次就是范式的概念对于初学者来说比较抽象, 需要解释非常非常多的数据库理论基础知识, 例如什么传递依赖, 部分依赖, 候选码等这样的东西.
那此时按照上面所述的这个相对来说更加简单的思路去设计数据库表, 一般来说都不会特别差. 并且实际上设计数据库表的时候, 甚至还会因为特殊需求而去反范式, 因此个人认为范式的介绍并不是非常的必要.
下面我们就通过简单的描述一下三大范式, 没有做细致的介绍, 简单了解即可
第一范式(1NF): 只要数据库表的列不嵌套, 那么就是符合第一范式的. 由于 MySQL 不会出现违反第一范式的情况, 所以我们就看一个我手动画的一个违反第一范式的表
第二范式(2NF): 当我们的表符合第一范式的时候, 如果所有的非主属性都完全依赖于候选码, 那么符合第二范式.
候选码: 实际上就是主键, 但是它指的是整个主键. 假如说我的一个表, 由 student_id 和 book_id 共同组成主键, 那么此时 (student_id, book_id) 是候选码
非主属性: 不是任何键(包括主键和外键)的属性
完全依赖: 假如 A 完全依赖于 B, 那么指的是只有 A 能推出 B, 其他属性得不出B. 比如我有一个学生表(学号, 年龄, 姓名), 此时只有 学号 -> 姓名, 而年龄无法 -> 姓名, 那么姓名完全依赖于年龄.
但是假如主键有多个值, 那么此时如果是整个主键才能推出, 那么才是完全依赖, 否则就是部分依赖. 例如我有一个关系表(student_id, course_id, score), 其中(student_id, course_id)为主键. 此时很明显, 对于每一个分数(score), 必须要整个主键才可以确定, 那么此时 score 完全依赖于主键.
可以看到, 这里为了解释一个第二范式, 我们就引入了非常多的概念, 这也是为什么我个人不喜欢讲解范式的原因之一. 下面两个范式即使举例也是基本不会遇到的例子, 我们简单了解即可
第三范式(3NF): 如果我们的表符合第二范式, 此时还不存在传递依赖, 那么就符合第三范式.
传递依赖: 假如一个表有属性(A, B, C), 如果存在 A -> B, 然后 B -> C的情况, 那么此时就是有传递依赖.
实际上此时还有一个常见的范式, 我们一并带过
BC范式(BCNF): 第三范式的改良版本, 但是没有到达第四范式的程度. 如果我们的表符合第三范式, 并且其中的决定属性集都有主键, 那么就是BC范式.
决定属性集: 在一个表中, 一定会有各种各样的 A -> B 出现, 例如在学生表(id, name, age)中, 就会有 id -> name, id -> age. 此时 id 就是一个决定属性集, 只不过此时这个集合里面只有一个值就是 id. 名字可能会重名, 因此无法确定一个值. 如果约定不会重名, 那么 name 的低位等同于 id. 此时决定属性集中有 {id, name}
聚合查询
聚合查询, 主要就是通过 MySQL 提供的一些内部函数来实现的查询, 例如实现查询平均值, 最大值, 最小值这样的操作
函数 | 说明 |
---|---|
COUNT([DISTINCT] expr) | 返回查询到的数据的数量 |
SUM([DISTINCT] expr) | 返回查询到的数据的总和 |
AVG([DISTINCT] expr) | 返回查询到的数据的平均值 |
MAX([DISTINCT] expr) | 返回查询到的数据的最大值 |
MIN([DISTINCT] expr) | 返回查询到的数据的最小值 |
count()
首先来看 count, 我们直接看例子
-- 查看有多少行
select count(*) from student;
select count(0) from student;
执行后, 可以发现他就是统计了行数
实际上他就相当于是, 执行了一次 select * from student
然后再看看有几行. 下面这个count(常数)
也是同样的效果, 一般来说两者没有区别, 根据个人喜好选择即可. 当然一般推荐 count(*)
, 看起来更加直观.
但是如果是对于查询一个具体的列, 那么此时就会有所差异了. 假如我们的表数据如下
可以看到, 邮箱有很多空的, 此时我们执行一下select count(qq_mail) from student
看看效果
可以发现, 结果为 4, 并不是行数, 而是非空的行数, 此时也就说明, 如果我们采用具体的列来进行行数的统计, 它就会把 null 给去掉, 这个是需要注意的.
同时, 这个也是支持去重操作的, 我们给内部加上distinct
就可以去重了
-- 从成绩表中查询数学成绩的行数, 去重
select count(distinct math) from score;
sum()
实际上根据这个函数的名字, 我们也可以看出它就是用于求和的. 我们就可以使用它来达到对行去求和操作
-- 查询学生的总分
select sum(score) from score where student_id = 1;
这个求和的操作, 只能使用在数字类型, 如果启用在其他类型的上面, 那么就不会生效
select sum(name) from course;
此时我们可以通过日志来看, 发现求和前他会先将这个值转换为 double, 那么我们此时给的是字符串类型, 当然就不会转成功了.
当然, 这个 sum 不仅仅可以对一列求和, 也可以对多列求和
-- 对语文数学英语的成绩进行求和
select sum(chinese + math + english) from score;
avg()
avg()
主要就是用于求平均值的, 就相当于是将sum()
和count()
混合起来用了, 然后相除一下.
-- 查询学生的平均分
select avg(score) from score where student_id = 1;
max()与min()
max()
顾名思义, 就是用于求最大值的, 我们同求最小值的一起来简单看一下使用即可
-- 查询学生的最高分
select max(score) from score;
-- 查询学生的最低分
select min(score) from score ;
group by 与 having
group by 主要用于对行进行分组查询, 同时一般都会搭配聚合函数使用. 这个听起来比较难理解, 所以我们先来看一个例子, 假如我现在有下面这张表及数据
create table emp
(id int primary key,name varchar(20),role varchar(20),salary int
);insert into emp
values(1, '张三', 'java工程师', 5000),(2, '李四', '大数据工程师', 8000),(3, '王五', 'java工程师', 6000),(4, '赵六', 'java工程师', 5000),(5, '钱七', 'php工程师', 5000);
那么此时, 我如果想要对行进行分组, 就可以使用 group by. 例如我根据这些人的职业分为 java工程师, 大数据工程师以及 php工程师.
-- 根据 role 进行分组
select * from emp group by role;
但是此时很明显有问题, 因为它这样查询, 会抛弃掉一些数据. 甚至于在某些配置项下, 上面的这个SQL是无法执行的.
因此, 一般来说我们使用 group by 的时候, 都会搭配聚合查询使用. 此时我们搭配聚合查询, 他就会将分组后的数据进行聚合. 例如我使用一下 sum(salary), 此时求的就是各个职位的总工资
-- 查询各个职位的总工资
select sum(salary) from emp group by role;
此时很明显不够直观, 所以我们还可以再查询一个 role
select role,sum(salary) from emp group by role;
之前我们进行一般查询的时候, 使用到的条件语句是 where, 我们这里当然也可以用, 只不过是在分组前用于排除元素, 例如我想要排除张三, 不让张三被统计, 那么就可以这样写
-- 统计薪资, 但是排除张三
select role,sum(salary) from emp where name != '张三' group by role;
但是假如说我想要排除 sum(salary) 大于 7000 的结果, 此时我们会发现我们的 sum() 如果写在了 where 的后面, 则会直接报错. 因为我们的 where 是在分组前执行的, 你 sum 是在分组后进行的, 两个一个在前一个在后, 那 where 肯定管不着 sum 了.
此时如果我们想要对分组后的结果进行筛选, 我们就需要通过 having 来进行, 使用的方式和 where 类似. 如下所示
-- 找薪资总和大于 7000 的数据
select role,sum(salary) from emp group by role having sum(salary) > 7000;
多表查询
在之前我们谈到过, 如果一个数据的关系是一对多或者多对多, 此时数据就会被分到两张表上面. 那此时自然就无法避免一个问题: 如何将这两个表的数据联合起来查询呢? 这就是接下来我们要介绍的多表查询
笛卡尔积
笛卡尔积, 本质上就是对两张表的数据进行排列组合. 例如有下面的两个表, 我们对其进行排列组合
那么此时我们得到的下面的这个排列组合的结果, 就是笛卡尔积. 但是此时我们可以发现, 在这个笛卡尔积里面有非常多无效的数据, 例如明明 book 表里面的 user_id 是 1, 但是却和 user 表里面 id 为 2 和 3 的数据对上了.
因此此时为了得到有效数据, 我们就可以加上限制条件, 即where user.id = book.user_id
. 可以发现, 我们采用了表名.列名
的方式去访问列, 因为两个表都有 id 列, 如果不加表名就不知道是哪个表了.
我们刚刚笛卡尔积的 SQL, 实际上也非常简单, 就是在 from 的后面添加上你想要笛卡尔积的表即可
select * from user,book;
随后我们就可以加上条件筛选, 得出合理的数据
select * from user,book where user.id = book.user_id;
此时我们就可以得到一个只包含正常数据的查询结果, 随后我们就可以对行和列进行进一步的筛选, 得出想要的数据
例如我只想要知道张三借了什么书, 那么我们就这样查询
select user.username, book.title from user,book where user.id = book.user_id and username = '张三';
当然, 这个操作也是可以结合聚合查询使用的, 这里我们就不详细介绍了, 感兴趣可以自行尝试.
笛卡尔积, 就是数据库进行多表查询的基础, 但是同时我们也可以看出, 如果数据量一大, 那么此时一旦进行了排列组合, 这个组合出来的数据量就会比较恐怖. 例如我的表 1 有 10000 条数据, 表 2 也有 10000 条数据, 此时对他们进行笛卡尔积, 得到的行数就是 10000 * 10000. 可以看到这个数据量还是非常恐怖的.
因此一般来说, 虽然多表查询用起来非常的爽, 但是在实际场景中, 我们更加倾向于多次分批查询.
内连接
实际上, 我们刚刚介绍简化笛卡尔积的过程中, 就是基于内连接来进行的. 这个内连接还有另外一种写法, 我们简单的通过对比的方式来看一下
-- 上面介绍的写法
select * from user,book where user.id = book.user_id;
-- 另一种写法, inner 可以省略
select * from user inner join book on user.id = book.user_id;
-- 省略 inner
select * from user join book on user.id = book.user_id;
下面两个的写法, 与接下来要介绍的两个外连接的格式还是比较像的, 而上面的那个如果不仔细看可能还会以为是单表查询, 不过具体使用哪个就看个人喜好了, 使用效果都是一致的
外连接
在连接的过程中, 可能会出现一些数据对不上的情况, 例如有没有可能有人没有借书呢? 当然是有可能的, 那此时假如说我还想要筛选出这个人来, 那如何让这个人不要被我的连接条件给筛掉呢? 此时就可以用到外连接
我们先来看一个效果, 假如我把刚刚那个内连接的inner join
改为left join
select * from user left join book on user.id = book.user_id;
可以看到, 此时我们的王五, 虽然没有借书, 但是他还是显示出来了.
这个就是左连接, 它会将左边的表的数据全部展示出来, 但是如果没有对应的数据就会直接给其他列置空. 当然这个左右指的就是你这两个表怎么放的, 左边的就是左表, 右边就是右表
既然有左外连接, 那么自然也就有右外连接, 只需要把left
再改成right
就行. 但是在我们这个例子中是看不出区别的, 我们就不演示了.
下面是一个图能够反映这三种连接的关系
那此时可能有人就要问了: 那有没有全部都连接的呢?
实际上, 这个全连接在 MySQL 中是没有的, 在其他的一些关系数据库中有, 例如 Oracle 上就是有的.
自连接
自连接, 实际上是一种特殊的技巧, 它能够把行之间的关系转换为列之间的关系. 那么什么时候用的到这样的关系呢? 例如我们有如下一张表
此时我想要得到大数据工程师比 Java 工程师工资更高的数据, 此时我们就可以用自连接.
因为我们现在相当于是要在一行一行中的数据进行比较, 但是很明显, MySQL 的 where 只能让我们进行列的筛选, 而不是去进行行的比较, 所以我们就需要转换一下行和列的关系.
当然, 此时由于我们是自连接, 因此需要给表取别名, 否则 MySQL就分不清楚了
select * from emp e1,emp e2;
可以看到, 如果我们想要得到大数据工程师比 Java 工程师工资更高的数据, 此时就可以直接用列去比较了
最后我们通过条件来去除无效数据, 筛选出想要的数据即可. 最后得到的 SQL 如下
select * from emp e1,emp e2 where e1.role = 'java工程师' and e2.role = '大数据工程师' and e1.salary < e2.salary;
子查询
子查询, 实际上就是将一个 SQL 的查询条件, 在另一个 SQL 里面复用. 例如我想要查询借了书的用户的信息, 那么此时假如我要分两个步骤:
- 在 book 表中找到对应的 user_id
- 通过 user_id 在 user 表里面查询信息
那么此时我们就可以通过如下这样的 SQL 一次执行完
-- 查询信息
select * from user where id in (
-- 查询 user_id
select user_id from book
);
此时可能有人要问了: 我这样的操作可不可以通过联合查询呢? 实际上也可以, 只不过子查询的这种方法, 就相当于是多次执行 SQL 但是放在了一次, 相对于联合查询, 效率上可能会更高一些.
但是无论是哪个查询, 都违背了一个原则就是简洁原则, 一旦套娃多了, 就会使得代码可读性非常的差. 因此子查询虽然是多个简单的 SQL 组合, 不会影响特别多的效率, 但是也要适可而止.
合并查询
合并查询, 实际上就是将两个 SQL 的执行结果合并在了一起, 使用的是 union 关键字. 例如我们对同一个表进行 or 查询的时候, 就可以使用这个方法
-- 使用 or
select * from user where id = 1 or id = 2;
-- 使用 union
select * from user where id = 1 union select * from user where id = 2;
当然, 这个只是为了演示效果, 并不是只能这样用, 那不然也太没用了.
这个当然并不仅仅能合并本身, 也可以和其他的表进行合并, 但是要求它们的列数和列的类型需要一致, 名字可以不一样. 并且它还会自动进行去重, 如果不想要去重就可以使用union all