目录
《SQL必知必会》查询部分学习笔记
1 前言
《SQL必知必会》是一本200多页的小册子,很适合没有SQL基础快速上手。整理一下这本书的内容,力求sql快速上手。对一个未知的语言,技术能够很快的掌握是一件很幸福的事情,毕竟大多数时间学习一个未知的东西的时候都会绕远路,还不得要领,最后萌生退意。所以这个阶段的目标是快速上手,知道是怎么回事,能操作起来。但是以后有机会也会好好补充一些原理上的知识来继续深入。这一部分是主要关于查询的。
2 认识SQL
我们都知道SQL是用来操作数据库的,来对数据库进行增删改查(curd)操作的。那么首先什么是数据库。下面的关于数据库的概念的知识如果看起来不好理解也没有关系,但是后面一旦开始用SQL操作数据库了就十分容易理解了。
2.1 数据库 database
数据库是以某种有组织的方式存储的数据集合,是通过数据库管理系统(DBMS)创建和操纵的容器。再通俗的理解就是存储数据的容器,但是是数据以某种形式组合在一起的集合,我们学习SQL就是学习用SQL如何来达到我们想要的对数据库的操作。
2.2 表 table
我们把数据放入数据库中,肯定不能是把什么数据都放一起,为了把不同特征的数据分开存放就出现了表。表是一种结构化的文件,是用来存储某种特定类型的数据。值得注意的是数据库中的表的名字应该是唯一的,数据库中没有其他表具有相同的名字。这个前提是再用一个数据库中,对于不同数据库来说就可以这个数据库中的一个表的表名和其他数据库中的一个表的表名相同。
2.3 列 column 和行 row
表是由列和行组成的,可以想象一个excle表格,用来存储学生信息,那么每一列就是学生的一个特征,比如:姓名、年龄、性别等,而每一行就可以表示一个学生。每一列的数据类型应该是相同的,比如存储姓名的一列就应该是字符串类型的,而存储年龄的就应该是数字类型。
2.4 主键 primary key
主键就是用来唯一标识一行的一列或者多列,听起来很拗口,但是有个例子就很好理解,比如:上面提到的一个学生表,我们用学生的学号来作为这个表的主键就很符合,每一行也就是每个学生都有唯一学号,这个学号不会相同。通过一个唯一的学号就可以找到对应的这个学生。
2.5 SQL
SQL是Structured Query Language 结构化查询语言的缩写,是用来于数据库沟通交流的语言。
3 检索数据
对数据库的增删改查来说,相对重要的就是查询了,也就是检索数据,所以我们首先来看这个重要的知识点,在这本书中,也是这样把查询放到一开始来讲解。现在在我的数据库中有这样一个表student,下面的查询数据就用这个表来演示。
+----+----------+------+------------+------+------------+------+---------+---------+
| id | name | age | birthday | sex | address | math | english | remark1 |
+----+----------+------+------------+------+------------+------+---------+---------+
| 1 | 柳岩 | 24 | 1990-03-23 | 女 | 上海 | 70 | 60 | B |
| 2 | 李若彤 | 22 | 1993-08-11 | 女 | 上海 | 40 | 50 | a |
| 3 | 迪丽热巴 | 22 | 1990-09-01 | 女 | 乌鲁木齐 | 40 | 50 | A |
| 4 | 高圆圆 | 22 | 1994-04-03 | 女 | 北京 | 90 | 40 | b |
| 5 | 丫丫 | 20 | 1996-08-11 | 女 | 乌鲁木齐 | 99 | 80 | c |
| 6 | 李金铭 | 25 | 1995-11-21 | 女 | 济南 | 70 | 50 | a |
+----+----------+------+------------+------+------------+------+---------+---------+
3.1 SELECT语句
在检索数据中最重要的就是SELECT
语句了,我们在控制台输入
SELECT name FROM student;
可以得到输出结果:
name
李若彤
迪丽热巴
柳岩
高圆圆
丫丫
这个语句的意思就是从表student中检索出名为name的列。注意这个输出的数据没有排序的。多条语句中每条语句结尾必须加上分号 ; ,SQL是大小写不敏感的,但是一般关键字大写,列名和表名小写。上面的语句中SELECT
和FROM
大写,但是列名name和表名student小写。
3.2 SELECT检索多列
上面是检索单个列的,我们想检索多列可以用,逗号分隔列名:
SELECT name,age,adress FROM student;
得到结果:
name age address
李若彤 22 上海
迪丽热巴 22 乌鲁木齐
柳岩 24 上海
高圆圆 22 北京
丫丫 20 乌鲁木齐
李金铭 25 济南
检索所有列:
SELECT * FROM student;
得到的结果就是所有数据都会出现,就像上面的整体表一样。
3.3 DISTINCT子句
当我们想看某一列的数据但是有可能会出现重复的现象,但是我们又不想看到重复的数据的时候就可以用到DISTINCT
子句:
SELECT DISTINCT age FROM student;
得到结果:
age
22
24
20
25
而不会得到重复的年龄数据。
3.4 LIMIT 子句
如果想要得到数据是一列的某几个数据就要用到LIMIT
,例如:
SELECT age FROM student LIMIT 2;
那么就会得到age这列的前两个数据:
age
22
22
这在不同的DBMS中语句有所不同,这里只给出MYSQL的语句。
3.5 OFFSET子句
有时我们并不是想从头开始获取指定的几个数据,而是从某个数据开始,那么这里就要用到OFFSET
,例如:
SELECT name FROM student LIMIT 2 OFFSET 2;
OFFSET
指定从哪开始,第一个被检索的行是第0行所以我们上述语句其实是从第三行开始的,这和数组的索引类似,是从0开始计数的。
name
柳岩
高圆圆
这条语句可以简化为
SELECT name FROM student LIMIT 2,2;
输出结果一致。
4 排序检索数据
4.1 SELECT的子句ORDER BY
上面讲到用SELECT
输出单列的时候数据是没有排序,想要输出的数据有序,就要用到ORDER BY
子句,例如:
SELECT age FROM student ORDER BY age;
输出为:
age
20
22
22
22
24
25
需要注意的是ORDER BY
一定要是SELECT
的最后一个子句,不然会报错。还有我们不是一定要按照我们检索的列来排序,我们完全可以检索name列,但是按照age排序例如:
SELECT name FROM student ORDER BY age;
输出结果为:
name
丫丫
李若彤
迪丽热巴
高圆圆
柳岩
4.2 ORDER BY中文排序问题
这里还需要注意的是关于中文排序的问题,例如我们想要以中文来排序,但是mysql中如果不做特殊处理的花排序出来时乱的,这里我们用CONVERT
函数来解决这个问题:
select name from student order by convert(name using gbk);
输出的名字结果如下:
name
迪丽热巴
高圆圆
李若彤
柳岩
丫丫
李金铭
4.3 ORDER BY按照多列排序
上面用到的是只按照单列排序,我们想按照多个列排序也是可可以的在ORDER BY
后多个列,用逗号隔开:
SELECT name,age,math FROM student ORDER BY age,math;
得到如下结果,可以看到先按照age排序,在age相同情况下再按照math排序:
name age math
丫丫 20 99
李若彤 22 40
迪丽热巴 22 40
高圆圆 22 90
柳岩 24 70
李金铭 25 70
还可以简写为:
SELECT name,age,math FROM student ORDER BY 2,3;
输出效果和上面相同,2,3 的意思是按照所选择的清单的第2个和第三个排序,所以这个方式不能用在没有选择的列上。
4.4 ORDER BY 降序排列
上面的情况是按照默认的升序排列的,我们可以指定排序是升序还是降序排列,升序是默认的ASC
,降序是DESC
,ASC
是默认的所以上面的排序都没有写,但是降序需要指定:
SELECT name,age FROM student ORDER BY age DESC;
4.5 ORDER BY 大小写字母排序问题
在MYSQL中排序是不区分大小写的,也就是说a和A都排在B或者b之前,并且没有先后顺序。这里我们可以用collate utf8_bin
来解决:
SELECT name,age FROM student ORDER BY remark1 COLLATE utf8_bin;
可以得到:
name remark1
迪丽热巴 A
柳岩 B
李若彤 a
高圆圆 b
丫丫 c
李金铭 a
5 过滤数据
5.1 WHERE子句
有时候我们想要得到的数据是我们有特殊需要的那些,满足一些我们对数据的特殊要求的数据,比如我们想要找到所有age等于22的数据,这时我们需要用到SELECT
的WHERE
子句:
SELECT name,age FROM student WHERE age = 20;
可以得到如下结果,这里我们虽然检索了name和age两列数据但是并没有将这两列的说有数据取出,而是仅仅取出了age=20的数据,当然我们不仅可以取出等于某个条件的数据。
name age
丫丫 20
5.2 WHERE子句操作符
我们可以想要取出age满足我们各种条件的数据,比如大于、小于、大于等于、小于等于、不等于各种条件。
MYSQL中支持:大于>、小于<、大于等于>=,小于等于<=,不等于<>或者!=。
例如我们现在想要找出age大于等于22的数据我们可以:
SELECT name,age FROM student WHERE age >= 22;
得到结果为:
name age
李若彤 22
迪丽热巴 22
柳岩 24
高圆圆 22
李金铭 25
需要注意的是如果我们想要对这个结果排序,我们一定要保证ORDER BY
子句在WHERE
这个子句的后面,因为我们之前提到排序子句一定是是最后的子句。例如:
SELECT name,age FROM student WHERE age >= 22 ORDER BY age;
得到结果为:
name age
李若彤 22
迪丽热巴 22
高圆圆 22
柳岩 24
李金铭 25
5.3 不匹配检查以及字符串匹配
不匹配检查,也就是不等于 != 或者 <> 情况,需要注意的是当我们去匹配字符串的时候必须要用 '' 单引号将需要匹配的内容括起来,例如:
SELECT name,age FROM student WHERE id = '高圆圆';
可以得到除了name为'高圆圆'的数据:
name age
李若彤 22
迪丽热巴 22
柳岩 24
丫丫 20
李金铭 25
5.4 BETWEEN子句
有时我们想要找出的数据是在某个范围之间的,而不是单纯的大于,小于或者不等于某个值,这时我们要用到BETWEEN
子句,例如我们要找出age在22到25之间的数据,我们可以:
SELECT name,age FROM student WHERE age BETWEEN 22 AND 25;
我们可以得到:
name age
李若彤 22
迪丽热巴 22
柳岩 24
高圆圆 22
李金铭 25
6 高级数据过滤
6.1 组合WHERE子句
前面提到的WHERE
子句都是只有单条的,有时我们需要满足多个条件的数据,需要用到多个WHERE
子句组合使用,我们一般用AND
和OR
来连接多个WHERE
子句。
6.2 AND操作符
现在我们想要的数据是同时满足age=22并且address='上海'这两个条件,我们可以:
SELECT name,age,address FROM student WHERE age = 22 AND address = '上海';
得到的结果如下:
name age address
李若彤 22 上海
这里我们只用到了一个AND
子句,我们想要满足不仅仅是两个条件,比如三个,四个多个条件的时候,我们就要在每两个条件之间都要用AND
来隔开。
6.3 OR操作符
如果我们要想的数据是满足一个条件或者满足另一个条件,就可以用OR
操作符来分隔两个条件,例如我们现在想要得到age=22或者address='上海'这两个条件满足任意一个的数据就额可以:
SELECT name,age,address FROM student WHERE age = 20 OR address='上海';
我们得到结果为:
name age address
李若彤 22 上海
柳岩 24 上海
丫丫 20 乌鲁木齐
可以看到上面列出的数据要么是age=20,要么address='上海',这两个条件是其中一个满足就会被检索出来。
6.4 求值顺序问题
现在我们想要找出age=22或者address=‘上海’的数据然后这个数据中满足math=40的数据,我们这么写:
SELECT name,age,address,math FROM student WHERE age=22 OR address=‘上海’ AND math=40;
按照我们想象的逻辑,最后得到的数据不管怎么样一定是math=40的数据,但是我们看下下结果:
name age address math
李若彤 22 上海 40
迪丽热巴 22 乌鲁木齐 40
高圆圆 22 北京 90
很明显这里面有我们并不想要的数据,会出现这个结果的原因是AND
在这条语句中的求值优先级更高,AND
被先执行了,然后才执行的OR
,为了解决这个问题我们给OR
操作符加上(),所以:
SELECT name,age,address,math FROM student WHERE (age=22 OR address=‘上海’) AND math=40;
就可以得到我们想要的结果了。
6.5 IN操作符
IN
操作符用来指定一个范围,在这个范围内的数据都可以取到,用一组括号里面用逗号隔开,例如:
SELECT name,address FROM student WHERE address IN ('上海','北京');
得到结果:
name address
李若彤 上海
柳岩 上海
高圆圆 北京
可以看到得到的数据是address为上海或者北京的数据。看起来这个语句也完全可以用OR
来实现,但是我们建议用IN
,这是因为以下几点原因:
- 在有多个合法选项的时候,
IN
语法更加清晰,直观; - 在和
AND OR
操作符连接使用的时候,IN
没有求值顺序更好管理; IN
比OR
操作符执行更快;IN
能包含其他的SELECT
语句,能更动态的建立SELECT
语句(这点我们在后面会谈到);
6.6 NOT操作符
NOT
操作符用来否定后面跟的条件,NOT
总是和其他操作符一起使用,不会单独使用。现在我们想找出所有的数据中除了age=22的数据,我们可以这样:
SELECT name,age FROM student WHERE NOT age=22;
可以得到结果:
name age
柳岩 24
丫丫 20
这条语句看起来用<>
或者说!=
也可以很容易实现,但是在一些复杂的语句中才能NOT
的优势,比如和IN
、BETWEEN
、EXISTS
等连用的时候可以更好的否定一些条件。
7 通配符
前面我们提到的检索都是在已经知道了值的情况下去检索这个值的,有的情况是我们对一个要检索的值并不是很确定的,例如我想找出名字的姓是李的同学,但是我并不完全确定名字,这个时候就要用到通配符(wildcard),用来匹配值的一部分的特殊字符。使用通配符我们必须使用LIKE
操作符。需要注意的是通配符只能用于匹配文本字段,非文本字段不能用通配符搜索。
7.1 %通配符
我们最常使用的就是%
通配符,在搜索串中,%
表示任意字符出现任意次数。例如,我们想找出所有名字的姓是 李 的同学。我们可以:
SELECT name,age FROM student WHERE name LIKE '李%';
我们可以得到所有name是以李开头的数据:
name age
李若彤 22
李金铭 25
现在我们想要得到名字中有'圆'这个字的数据,这个字可以出现在名字的任意位置:开头、中间、结尾,只要有这个字就好,上面的方法只能找到姓,所以我们现在这样%圆%
,%
表示的是任意字符出现任意次数,任意次数包括了0次、1次和若干次。所以我们这样做可以知道到名字中包含圆着各自的,无论是在名字的任意位置:
SELECT name,age FROM student WHERE name LIKE '%圆%';
我们可以得到:
name age
高圆圆 22
7.2 _通配符
下划线_
通配符用于匹配单个字符,现在我们想找出数据中name是两个字的数据,我们用%
就没办法去匹配,我们用两个_
去匹配名字是两个字的数据:
SELECT name,age FROM student WHERE name LIKE '__';
注意上面的下划线是两个用来匹配两个字符。我们可以得到:
name age
柳岩 24
丫丫 20
8 创建计算字段
字段(field)和列的概念相同,可以互换。我们想要从数据库中拿到的数据是我们理想的格式的,但是实际上从数据库中直接检索出来的数据是不符合的,比如一个订单表格存储了货物的数量和单价但是没有总价,我们现在想要得到所有的总价,只能将数量和单价检索出来然后在客户端应用程序中进行处理,然后这样的方式比起直接在数据库中处理来说很慢,效率不高,于是我们就需要在数据库服务器中对数据进行计算得到我们想要的形式。
8.1 拼接字段
现在我们想要得到输出的数据格式是 姓名(地址),就是姓名后有个括号,括号里面是地址,这样的格式输出来。在有的DBMS中可以直接使用+
来拼接字段,例如:
SELECT name + '(' + address +')' FROM student;
但是这个语句在MySQL中执行时不正确的,在MySQL我们使用Concat()
函数来拼接字段:
SELECT Concat(name , '(' , address ,')') FROM student;
可以得到输出的结果:
concat(name,'(' , address ,')')
李若彤(上海)
迪丽热巴(乌鲁木齐)
柳岩(上海)
高圆圆(北京)
丫丫(乌鲁木齐)
李金铭(济南)
8.2 使用别名
上面的结果可以看出来输出了我们想要,的格式的一列,但是这列数据的列名concat(name,'(' , address ,')')
,这个列名没有意义,我们的客户端程序无法引用它,这个时候我们可以用给它一个别名,用AS
关键字赋予。例如上面的语句可以写成:
SELECT Concat(name , '(' , address ,')') AS name_addr FROM student;
这样执行结果为:
name_addr
李若彤(上海)
迪丽热巴(乌鲁木齐)
柳岩(上海)
高圆圆(北京)
丫丫(乌鲁木齐)
李金铭(济南)
现在客户端引用就可以根据这个表名来引用这个字段,就好像这个字段是真实存在的一样。AS
是可选的,但是我们建议使用。别名还可以用于给非法的表列名时重新命名它,以防止非法字符带来的混淆和误解。建议列名最好重命名为多个单词拼接为一个单词,当然能用一个单词表示那最好。
8.3 算数计算
计算字段的另一个很常用的用途是对检索出来的数据进行数学计算,例如我们有一个有商品数量和商品单价的数据库,但是我们现在想输出总价,于是我们就要用商品数量 * 商品单价然后将总价输出。现在我们要对我们的表格计算math分数+english分数得到一个总分并且重命名别名为score,我们可以这样:
SELECT name,math,english,math + english AS score FROM student;
于是我们可以得到:
name math english score
李若彤 40 50 90
迪丽热巴 40 50 90
柳岩 70 60 130
高圆圆 90 40 130
丫丫 99 80 179
李金铭 70 50 120
在复杂的运算中可以用()
来提升优先级运算,除了上面的+
之外还有- * /
减、乘和除。
9 使用函数处理数据
在上一节中我们用到了Concat()
这个函数来拼接字段,Concat()
就是一个函数,SQL中提供了一些函数来处理数据,函数一般是在数据上执行的,为数据的转换和处理带来了方便,但是不同的DBMS支持的函数的函数名和语法不尽相同,这导致了程序的可移植性变得很差,但是如果不使用函数,编写某些应用程序会徒增困难,我们就必须要用其他的方法来实现本来DBMS可以很有效的就可以完成的工作。
9.1 文本处理函数
这里我们列出一些常用的文本处理函数:
函数 | 说明 |
---|---|
LEFT() | 返回字符串左边指定长度的字符 |
RIGHT() | 返回字符串右边指定长度的字符 |
LOWER() | 将字符串转换为小写 |
UPPER() | 将字符串转换为大写 |
LTRIM() | 去掉字符串左边的空格 |
TRRIM() | 去掉字符串右边的空格 |
SOUNDEX() | 返回字符串的SOUNDEX值 |
LENGTH() | 返回字符串的长度 |
分别给出这些函数的使用实例和结果:
9.1.1 LEFT()和RIGHT()函数
LEFT()
返回字符串左边指定长度的字符:
SELECT LEFT(name, 1) FROM student;
输出结果为:
name
李
迪
柳
高
丫
李
RIGHT()
返回字符串左边指定长度的字符:
SELECT RIGHT(name, 2) FROM student;
输出结果为:
name
若彤
热巴
柳岩
圆圆
丫丫
金铭
9.1.2 LOWER()和UPPER()函数
LOWER()
将字符串转换为小写:
SELECT LOWER(remark1) AS lower_remark FROM student;
输出结果为:
lower_remar
a
a
b
b
c
a
UPPER
将字符串转换为大写:
SELECT UPPER(remark1) AS upper_remark FROM student;
输出结果为:
upper_remark
A
A
B
B
C
A
9.1.3 LTRIM()、TRRIM()和LENGTH()函数
为验证这两个函数我们在所有数据的address字段的两边都加了空格,我们先看下在函数处理之前加了空格的数据:
SELECT * FROM student;
+----+----------+------+------------+------+------------+------+---------+---------+
| id | name | age | birthday | sex | address | math | english | remark1 |
+----+----------+------+------------+------+------------+------+---------+---------+
| 1 | 柳岩 | 24 | 1990-03-23 | 女 | 上海 | 70 | 60 | B |
| 2 | 李若彤 | 22 | 1993-08-11 | 女 | 上海 | 40 | 50 | a |
| 3 | 迪丽热巴 | 22 | 1990-09-01 | 女 | 乌鲁木齐 | 40 | 50 | A |
| 4 | 高圆圆 | 22 | 1994-04-03 | 女 | 北京 | 90 40 | b |
| 5 | 丫丫 | 20 | 1996-08-11 | 女 | 乌鲁木齐 | 99 | 80 | c |
| 6 | 李金铭 | 25 | 1995-11-21 | 女 | 济南 | 7 | 50 | a |
+----+----------+------+------------+------+------------+------+---------+---------+
我们可以用LENGTH()
函数先来看下address字段的数据长度:
SELECT address,LENGTH(address) AS addr FROM student;
得到输出的结果:
address addr
上海 8
乌鲁木齐 14
上海 8
北京 8
乌鲁木齐 14
济南 8
这里需要说明的是:在默认的utf8编码中一个中文字符占3个字节,所以上面的上海字符串的长度为8是因为在字符串的前面和后面各自还有一个空格。
然后我们分别用LTRIM()
和TRRIM()
方法对地址字段处理,再用LENGTH()
查看address字段的长度:
SELECT address, LENGTH(address) AS len, LENGTH(LTRIM(address)) AS ltrim_len FROM student;
上面这个sql语句中同时取得了address字段的长度和LTRIM()
函数处理后的长度:
address len ltrim_len
上海 8 7
乌鲁木齐 14 13
上海 8 7
北京 8 7
乌鲁木齐 14 13
济南 8 7
可以看到在LTRIM()
函数处理后的address字段长度减少了1,去除了address字段左边的空格,RTRIM()
也是同样的效果只是是处理了address字段右边的空格。
9.1.4 SOUNDEX()方法
这个方法是用来返回四个字符的soundex值,用以评估两个字符串的相似性。例如我们在数据有个名字'Jemas',这是一个错误的名字,本应该是'James',但是由于某种原因输入错误,那我们现在用'James'去检索肯定不能得到我们想要的结果,于是借助SOUNDEX()
方法我们可以这样:
SELECT name FROM student WHERE SOUNDEX(name) = SOUNDEX('james');
这样我们就可以找到名字这一字段中和'James'发音相似的名字了。
这个方法似乎对于中文并没有很好的效果,我们测试一下'李若彤'这个字符串的SOUNDEX值是多少:
SELECT SOUNDEX('李若彤');
我们得到输出结果:
soundex('李若彤')
李000
这样根本没有办法去判断中文读音是否相似,只能找到姓相同的名字。
9.2 日期和时间处理函数
时间和日期采用了相应的数据类型存储在表中,每种DBMS都有其特殊形式,时间日期函数可移植性最差,几乎每种DBMS都有不同的方法,这里我们介绍几个MySQL中的时间和日期相关的函数。MySQL中和时间日期有关的函数有很多,这里只是列举几个,更多的大家可以去参考MySQL的使用手册。
9.2.1 获取当前日期和时间NOW()、SYSDATE()、CURRENT_TIMESTAMP()
NOW()、SYSDATE()、CURRENT_TIMESTAMP()这三个函数都可以获取当前的日期和函数,我们一起执行一下看下她们三个的不同:
SELECT SLEEP(2),NOW(),SLEEP(2),SYSDATE(),SLEEP(2),CURRENT_TIMESTAMP();
SLEEP(2) NOW() SLEEP(2) CURRENT_TIMESTAMP SLEEP(2) SYSDATE()
0 2019-05-09 09:10:11 0 2019-05-09 09:10:11 0 2019-05-09 09:10:17
可以看到NOW()
和CURRENT_TIMESTAMP()
都是获取程序在执行之前的时间,而SYSDATE()
获取的是当前的系统动态时间。
9.2.2 日期转换函数DATE_FORMATE()和STR_TO_DATE()
依靠DATE_FORMATE()
和STR_TO_DATE()
我们可以把DATE类型的数据转化成我们想要的格式的字符串以及把一个时间字符串转换成日期类型的数据:
DATE_FORMATE()
函数:
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d:%H%i%s');
把当前时间转换成了我们想要的格式的字符串:
DATE_FORMAT(NOW(), '%Y-%m-%d:%H%i%s')
2019-05-09:092755
STR_TO_DATE()
函数
SELECT STR_TO_DATE('2020/09/26', '%Y/%m/%d');
把字符串转换成了日期数据:
STR_TO_DATE('2020/09/26', '%Y/%m/%d')
2020-09-26
9.2.3 时区转换函数TIMEZONE()
将时间从当前时区转换到目标时区:CONVERT_TZ(dt,from_tz,to_tz)
SELECT CONVERT_TZ('2008-08-08 12:00:00', '+08:00', '+00:00');
CONVERT_TZ('2008-08-08 12:00:00', '+08:00', '+00:00')
2008-08-08 04:00:00
9.3 数值处理函数
数值处理函数不像前两种函数,字符串处理函数和日期时间函数那样用的很多,但是众多DBMS却在这类函数最一致和统一,这里我们列举出一些常用的数值处理函数:
函数 | 说明 |
---|---|
ABS() | 返回一个数的绝对值 |
SIN() | 返回一个角度的正弦 |
COS() | 返回一个角度的余弦 |
TAN() | 返回一个角度的正切 |
EXP() | 返回一个数的指数值 |
PI() | 返回圆周率 |
SQRT() | 返回一个数的平方根 |
### 10 汇总函数
有时候我们需要一列的数据可能并不是想要获取这些数据本身,而是想要得到它们的一些汇总数据,例如:这些数据的个数、它们的和、它们中的最大值最小值等等。如果我们是把这些原始的数据取到,然后我们再自己去处理得到我们想要的一些汇总数据这无疑是一种对资源的浪费,我们希望在数据库服务器中就能等到一些我们想要的处理后的数据而不是原始数据,所以我们就需要这些聚集函数来帮我们得到我们想要的数据。
10.1 AVG()函数
AVG()
函数用于求一列的平均值:
SELECT AVG(math) AS math_avg FROM student;
得到结果:
math_avg
68.1667
需要注意的是这里AVG()
会忽略值为NULL的行。
如果需要我们可以用TRUNCATE()
函数来指定需要保留的小数位数,例如:
SELECT TRUNCATE(AVG(math),2) AS math_avg FROM student;
得到结果:
math_avg
68.16
可以看到TRUNCATE()
函数并没有四舍五入。
如果我们只想计算不同值的平均数我们可以:
SELECT AVG(DISTINCT math) FROM student;
可以得到:
AVG(DISTINCT math)
74.7500
10.2 COUNT()函数
COUNT()
函数用于确定表中行的数目或者符合指定条件的行的数目,例如:
SELECT COUNT(name) AS name_count FROM student;
可以得到name的行数:
name_count
6
需要注意的是如果我们写这样:
SELECT COUNT(*) FROM student;
COUNT()
函数是会将值为null的行也计入总数的,但是如果像上面的指定了name行那么如果name行有值为null那么COUNT()
函数不会将值为null的行计入总数。
10.3 MAX()和MIN()函数
MAX()
和MIN()
函数用于返回指定行的最大值和最小值:
SELECT MAX(math) AS math_max, MIN(math) AS math_min from student;
可以得到math字段的最大值和最小值:
math_max math_min
99 40
MAX()
和MIN()
都会忽略值为null的行。
9.4 SUM()函数
SUM()
函数用于返回指定行的总和,例如:
SELECT SUM(math) AS math_sum FROM student;
可以得到math个这字段的总和:
sum(math)
409
10.5 组合聚集函数
SELECT
语句可以根据需要组合多个聚集函数,例如:
SELECT COUNT(*) AS count,
AVG(math) AS math_avg,
MIN(math) AS math_min,
MAX(math) AS math_max,
SUM(math) AS math_sum
FROM student;
这样可以的到:
count math_avg math_min math_max math_sum
6 68.1667 40 99 409
11 分组数据
之前我们的查询都是依靠WHERE
子句匹配特定的数据完成的,但是如果现在我们想要的到的数据是:男生、女生的个数,来自不同城市的学生的个数,考不同分数的学生的个数亦或者是不同的remark的学生的个数,要得到这样的数据这个时候我们就要用的到分组了,使用分组将数据分为不同的逻辑组,然后对分组后的每个组进行一些聚集计算。
在开始之前我们对数据进行一些添加好方便演示分组,我们又添加了6个学生的数据,现在来看下所有的数据:
SELECT * FROM student;
现在的数据是:
+------+----------+------+------------+------+------------+------+---------+---------+
| id | name | age | birthday | sex | address | math | english | remark1 |
+------+----------+------+------------+------+------------+------+---------+---------+
| 2 | 李若彤 | 22 | 1993-08-11 | 女 | 上海 | 40 | 50 | a |
| 3 | 迪丽热巴 | 22 | 1990-09-01 | 女 | 乌鲁木齐 | 40 | 50 | A |
| 1 | 柳岩 | 24 | 1990-03-23 | 女 | 上海 | 70 | 60 | B |
| 4 | 高圆圆 | 22 | 1994-04-03 | 女 | 北京 | 90 | 40 | b |
| 5 | 丫丫 | 20 | 1996-08-11 | 女 | 乌鲁木齐 | 99 | 80 | c |
| 6 | 李金铭 | 25 | 1995-11-21 | 女 | 济南 | 70 | 50 | a |
| 7 | 张三 | 22 | 1990-06-14 | 男 | 西安 | 60 | 70 | c |
| 8 | 李四 | 23 | 1996-07-11 | 男 | 成都 | 80 | 30 | b |
| 9 | 王五 | 20 | 1998-06-10 | 男 | 上海 | 70 | 80 | b |
| 10 | 赵一 | 23 | 1992-06-11 | 男 | 北京 | 60 | 60 | a |
| 11 | 钱二 | 25 | 1994-06-18 | 男 | 成都 | 60 | 70 | c |
| 12 | 周六 | 22 | 1992-02-27 | 男 | 北京 | 80 | 80 | a |
+------+----------+------+------------+------+------------+------+---------+---------+
11.1 创建分组
创建分组是用的SELECT
的子句GROUP BY
来完成的,例如我们现在依据性别来分组,并且用COUNT()
函数得到每组的学生数:
SELECT sex,COUNT(*) AS count FROM student GROUP BY sex;
可以得到:
+------+-------+
| sex | count |
+------+-------+
| 女 | 6 |
| 男 | 6 |
+------+-------+
现在我们再依据address字段来分组:
SELECT address,COUNT(address) AS count FROM student GROUP BY address;
可以得到根据城市分组的数据:
+------------+-------+
| address | count |
+------------+-------+
| 上海 | 3 |
| 乌鲁木齐 | 2 |
| 北京 | 3 |
| 成都 | 2 |
| 济南 | 1 |
| 西安 | 1 |
+------------+-------+
11.2 过滤分组
有的时候得到的分组可能有的组并不是我们需要的,比如现在我还是按照address来分组,但是我只是想得到至少有两个学生的address组,这个时候需要对分好的组进行过滤,我们会第一时间想到WHERE
子句来完成过滤,但是对于GROUP BY
子句来说有特殊的要求,GROUP BY
子句必须出现在WHERE
子句之后,在ORDER BY
子句之前。所以WHERE
子句并不能用来过滤分组,对于分组进行过滤我们使用HAVING
子句,例如:
SELECT address,COUNT(*) AS count
FROM student
GROUP BY address
HAVING COUNT(*) >= 2;
我们就可以得到address组中学生数量大于等于2的address:
+------------+----------+
| address | count |
+------------+----------+
| 上海 | 3 |
| 乌鲁木齐 | 2 |
| 北京 | 3 |
| 成都 | 2 |
+------------+----------+
现在我们加上其他的条件,我们还是用GROUP BY
子句来依据address来对学生分组,并且用COUNT(*)
统计不同address的学生个数,但是现在我只想统计男生中的address的情况,这个时候我们就需要用WHERE
在分组之前对数据过滤,过滤之后对只有男生的数据进行分组,然后再对分好的组用HAVING
进行过滤,找到address的学生数量大于等于2的组。
简单来说,分组前用WHERE
过滤,分组后用HAVING
过滤:
SELECT address,COUNT(*)
FROM student
WHERE sex = '男'
GROUP BY address
HAVING COUNT(*) >= 2;
这样我们就得到:
+---------+----------+
| address | COUNT(*) |
+---------+----------+
| 北京 | 2 |
| 成都 | 2 |
+---------+----------+
我们再举个例子来理解WHERE
和HAVING
,我们现在想要找出相同年龄的女生的个数,首先我们需要的是女生的数据,所以用WHERE
过滤出女生的数据,然后按照女生的年龄分组,再过滤出每个组中大于等于2的组,就是我们想要的组:
SELECT age,COUNT(*) AS count
FROM student
WHERE sex = '女'
GROUP BY age
HAVING COUNT(*) >= 2;
我们可以得到:
+------+----------+
| age | count |
+------+----------+
| 22 | 3 |
+------+----------+
我们可以知道女生中有相同的年龄是22岁,并且相同年龄的女生有3个。
需要注意的是在MySQL中,过滤分组建议用HAVING
,过滤分组外的其他数据用WHERE
。
11.3 分组和排序
最开始的分组的例子中我们得到了来自每个不同城市的学生的个数,执行如下语句:
SELECT address,COUNT(*) AS count
FROM student
GROUP BY address
HAVING COUNT(*) >= 2;
可以得到:
+------------+----------+
| address | count |
+------------+----------+
| 上海 | 3 |
| 乌鲁木齐 | 2 |
| 北京 | 3 |
| 成都 | 2 |
+------------+----------+
可以看到这组数据是没有顺序的,我们现在希望按照每个城市的学生数量的多少来递增排序,我们就需要用到ORDER BY
来排序,注意ORDER BY
这个子句必须出现在SELECT
的最后,也就是它要在GROUP BY
以及过滤分组的HAVING
之后:
SELECT address,COUNT(*) AS count
FROM student
GROUP BY address
HAVING COUNT(*) >= 2
ORDER BY COUNT(*);
这样我们就得到了我们想要的并且有序的数据:
+------------+----------+
| address | count |
+------------+----------+
| 成都 | 2 |
| 乌鲁木齐 | 2 |
| 北京 | 3 |
| 上海 | 3 |
+------------+----------+
现在我们来总结一下SELECT
的子句:
首先,FROM
子句并不似必须的,它是在需要从表中选择数据时才用的。然后对行级元素进行过滤我们用WHERE
,对数据进行分组我们用GROUP BY
,对分组进行过滤固定用HAVING
,最后GROUP BY
虽然可以对数据进行分组,但是还是需要用到ORDER BY
来对数据进行排序,ORDER BY
子句必须在SELECT
语句的最后。
12 子查询
前面我们用SELECT
去查询我们想要的数据都是简单查询,也就是说都是从单个数据库中检索数据的单条语句。接下来我们来学习一些复杂的查询,也就是子查询,就是嵌套在查询中的查询,我们第一次查询的结果会被其他的查询语句再用到,组成了多条语句嵌套的查询,简单的说就是:一条查询语句结果作为另一条查询语法一部分。
为了能更好的演示子查询我们不能只用一个表来查询,所以下面我们需要再创建几个表,以及对应的数据,我们还没有讲到创建表,插入数据等等的操作,但是没有关系我们在这里主要是为了去练习子查询,关于创建表以及插入数据等等的一系列操作我们后面会再讲到,这里可以直接看后面的子查询的内容就好。现在来创建了另外的几个表:
-- 创建部门表,插入数据
CREATE TABLE dept (
id INT PRIMARY KEY AUTO_INCREMENT, -- 部门id
NAME VARCHAR(20) -- 部门名称
);
INSERT INTO dept (NAME) VALUES ('开发部'),('市场部'),('财务部');
上面的语句分变创建了一个部门表和向部门表中插入了三个数据。
-- 创建员工表
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT, -- 员工id
NAME VARCHAR(10), -- 姓名
gender CHAR(1), -- 性别
salary DOUBLE, -- 工资
join_date DATE, -- 入职日期
dept_id INT -- 部门id
);
上面我们创建了一个员工表。
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('高圆圆','女',7200,'2013-02-24',1);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('丫丫','女',3600,'2010-12-02',2);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('柳岩','女',9000,'2008-08-08',2);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('张三','男',5000,'2015-10-07',3);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('李四','男',4500,'2011-03-14',1);
上面我们向员工表中插入了五个数据。
现在我们有两个表:
- 部门表:保存了部门名称和部门id
- 员工表:保存了员工的id、姓名、性别、工资、入职时间和所属部门id。
接下来我们从子查询的结果不同来分别讨论不同的情况:
12.1 子查询的结果是单行单列
SELECT 查询字段 FROM 表 WHERE 字段 = (子查询);
子查询的结果是单行单列的时候也就是上面的语句括号中的子查询的结果是单行单列,就是只有一个数据返回。我们用两个例子来说明:
查询工资最高的员工信息
首先我们要在员工表中找到最高的工资
SELECT MAX(salary) FROM emp;
可以得到:
+-------------+ | MAX(salary) | +-------------+ | 9000 | +-------------+
根据得到的最高工资找打最高工资的员工
SELECT * FROM emp WHERE salary = 9000;
可以得到最高工资的员工信息:
+----+------+--------+--------+------------+---------+ | id | NAME | gender | salary | join_date | dept_id | +----+------+--------+--------+------------+---------+ | 3 | 柳岩 | 女 | 9000 | 2008-08-08 | 2 | +----+------+--------+--------+------------+---------+
现在我们把第一步和第二步结合到一起得到:
SELECT * FROM emp WHERE salary = (SELECT MAX(salary) FROM emp);
太长的SQL语句建议应该合理的缩进。可以看到我们第一步首先找到了员工的最高的工资,然后在第二步中
WHERE
条件就直接用=
去判断salary等于最高工资的员工。因为最高工资只会是一个值,所以我们在这里条件就直接用的`=``。
查询工资小于平均工资的员工有哪些?
首先用聚合函数得到员工的平均工资:
SELECT AVG(salary) FROM emp;
这里用到了聚合函数AVG()用于得到平均工资。
找出员工工资小于平均工资的员工:
SELECT * FROM emp WHERE salary < (SELECT AVG(salary) FROM emp);
可以得到:
+----+------+--------+--------+------------+---------+ | id | NAME | gender | salary | join_date | dept_id | +----+------+--------+--------+------------+---------+ | 2 | 丫丫 | 女 | 3600 | 2010-12-02 | 2 | | 4 | 张三 | 男 | 5000 | 2015-10-07 | 3 | | 5 | 李四 | 男 | 4500 | 2011-03-14 | 1 | +----+------+--------+--------+------------+---------+
上面个的两个例子的子查询的结果都是只是单行单列的数据,条件判断的时候都是用的
=
或者<
。
12.2 子查询的结果是单列多行
现在子查询的结果不再是单行单列的一个数了,而是单列多行的数据,像是得到一个数组一样,我们这个时候不能再用上面的=
或者是>
了,我们在这种情况的时候用IN
。我们来看列子:
查询工资大于5000的员工的部门的名字
最终要得到的是部门名字,那么一定要在部门表里去查询,但是部门表中只保存部门id和部门名称,所以我们需要在员工表中先得到工资大于5000的员工所属的部门id,然后再用这个部门id去部门表中得到对应的部门名称。
先查询工资大于5000的员工的部门id
SELECT dept_id FROM emp WHERE salary > 5000;
根据第一步得到的部门id在部门表中查询部门名称:
SELECT name FROM dept WHERE id IN(SELECT dept_id FROM emp WHERE salary > 5000);
在这里我们在第一步中得到的数据不再是单行单列的一个数据了,我们得到的是一个类似数组的数据所以我们在条件判断的时候需要用
IN
。
查询开发部与财务部所有的员工信息
首先我们要从部门表中的到开发部和财务部的部门id,然后根据这个部门id去员工共表中查询部门id是上一步得到的部门id的员工,查询这些员工的信息。
查询开发部与财务部的部门id
SELECT id FROM dept WHERE name IN ('开发部','财务部');
我们可以得到:
+----+ | id | +----+ | 1 | | 3 | +----+
上面的语句也可以考虑用:
SELECT id FROM dept WHERE name = '开发部' OR name = '财务部';
也可以得到一样的结果。
根据第一步中得到的部门id查找部门id是这些id的员工:
SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE name IN ('开发部','财务部'););
这一步中我们要找的员工信息在员工表中,所以我们在
FROM
后面的表写的是emp,部门iddept_id
在emp表中名字是dept_id
,但是在部门表中部门id是id
,一定要区分清楚。第一步中查询的结果是单列多行的类似数组的表格,所以我们的条件要用IN
。
12.3 子查询的结果是多行多列
子查询的结果现在是一个多行多列的表,我们子查询的到一个临时表,对于自查询查询到的临时表我们必须要给这个临时表起一个别名,不然在父查询的时候我们没法用到子查询的到的数据,来看下面的例子来深刻理解:
查询出2011年以后入职的员工信息,包括部门名称
首先在员工表中查询到入职时间小于2011-01-01的员工的所有信息:
SELECT * FROM emp WHERE join_date > '2011-01-01';
可以得到结果:
+----+--------+--------+--------+------------+---------+ | id | NAME | gender | salary | join_date | dept_id | +----+--------+--------+--------+------------+---------+ | 1 | 高圆圆 | 女 | 7200 | 2013-02-24 | 1 | | 4 | 张三 | 男 | 5000 | 2015-10-07 | 3 | | 5 | 李四 | 男 | 4500 | 2011-03-14 | 1 | +----+--------+--------+--------+------------+---------+
我们还需要得到上一步得到的员工的部门名称,从上一步的临时表中取出部门id去部门表中查询即可,同时还要注意我们还要把上一步的临时表中的所有元素也查询出来:
SELECT temp.*,dept,name FROM dept,(SELECT * FROM emp WHERE join_date > '2011-01-01') AS temp WHERE temp.dept_id = dept.id;
可以得到:
+----+--------+--------+--------+------------+---------+--------+ | id | NAME | gender | salary | join_date | dept_id | name | +----+--------+--------+--------+------------+---------+--------+ | 1 | 高圆圆 | 女 | 7200 | 2013-02-24 | 1 | 开发部 | | 4 | 张三 | 男 | 5000 | 2015-10-07 | 3 | 财务部 | | 5 | 李四 | 男 | 4500 | 2011-03-14 | 1 | 开发部 | +----+--------+--------+--------+------------+---------+--------+
通过这个例子我们可以看到,我们在第一步中查询结果是一个临时表,这个表的所有信息也是我们最后要查询的,并且这个表中的部门id也是我们在部门表中查询部门名称的依据。对于临时表我们一定要给临时表一个别名,方便我们后面查询这个表中的某些数据。
### 13 联结表
为了说清楚联结表我们用例子来说明:现在我们需要记录老师和课程,老师有老师的id和姓名,课程有课程id和姓名,设想一下我们把数据都放到一个表中,会产生大量的冗余信息,因为一个老师可能教授多个课程。所以我们会这样做:我们创建两个表:老师表和课程表,老师表中记录了老师的编号id和老师姓名,在课程表中记录了课程id和课程姓名以及老师的id。这样老师表和课程表就通过课程表里的老师id将两个表连接到了一起。在关系型数据库中我们要把信息分解成多个表,一类数据一个表。各个表通过某些公共值相互关联,就像我们说的老师表和课程表通过老师的id关联到了一起,我们要看一个老师教授了什么课程,只需从老师表中的到老师id,然后在课程表中去找老师id对应的课程,反过来我们想看一个课程是哪位老师上课,可以从课程表中找到课程对应的老师id,然后通过老师id在老师表中找到对应的老师。
下面我们就来创建老师表和课程表这两个表,创建表和插入数据并不是我们这里的重点。
-- 创建老师表 老师编号为主键
create table teacher (
id int PRIMARY key auto_increment,
name varchar(50)
);
-- 创建课程表 老师编号外键约束
CREATE table course (
id int primary key auto_increment,
name varchar(50),
teacher_id int,
foreign key(teacher_id) references teacher(id)
);
-- 插入老师表数据
insert into teacher values(null,'关羽');
insert into teacher values(null,'张飞');
insert into teacher values(null,'赵云');
-- 插入课程表数据
insert into course values(null,'语文',1);
insert into course values(null,'数学',1);
insert into course values(null,'生物',2);
insert into course values(null,'化学',2);
insert into course values(null,'物理',2);
insert into course values(null,'英语',3);
13.1 创建联结
像上面一样我们把数据分解成多个表存储能更有效的存储,更方便处理数据,伸缩性也更好。但是问题是数据存储在多个表中,我们如果才能用一条SELECT
语句将数据查询出来呢?答案就是使用联结。那么如何使用联结呢?我们只需要指定要联结的所有表和关联它们的方式就可以。例如:我们现在要把老师表和课程表的信息都查询出来,我们可以这样:
SELECT teacher.id,teacher.name,course.id,course.name
FROM teacher,course
WHERE teacher.id = course.teacher_id;
由于在老师表和课程表中都有id,name字段如果我们不使用完全限制列名的话,DBMS无法知道我们要查询的id和name是哪个表中的字段,所以在联结中可能会出现歧义的地方我们一定要使用完全限制列名来避免歧义,现在我们就通过WHERE teacher.id = course.teacher_id
两个表都有的老师的id联结到了一起,我们可以得到结果:
+----+------+----+------+
| id | name | id | name |
+----+------+----+------+
| 1 | 关羽 | 1 | 语文 |
| 1 | 关羽 | 2 | 数学 |
| 2 | 张飞 | 3 | 生物 |
| 2 | 张飞 | 4 | 化学 |
| 2 | 张飞 | 5 | 物理 |
| 3 | 赵云 | 6 | 英语 |
+----+------+----+------+
WHERE
子句在建立联结中十分重要,我们必须要用WHERE
子句作为过滤条件,过滤出匹配条件的数据。如果没有WHERE
子句,那么第一个表中的每一行都会于第二表的每一行进行配对,而不管得到的数据对于我们有没有实际的意义。如果我们把WHERE
子句删除将会得到下面的结果:
SELECT teacher.id,teacher.name,course.id,course.name
FROM teacher,course;
+----+------+----+------+
| id | name | id | name |
+----+------+----+------+
| 1 | 关羽 | 1 | 语文 |
| 2 | 张飞 | 1 | 语文 |
| 3 | 赵云 | 1 | 语文 |
| 1 | 关羽 | 2 | 数学 |
| 2 | 张飞 | 2 | 数学 |
| 3 | 赵云 | 2 | 数学 |
| 1 | 关羽 | 3 | 生物 |
| 2 | 张飞 | 3 | 生物 |
| 3 | 赵云 | 3 | 生物 |
| 1 | 关羽 | 4 | 化学 |
| 2 | 张飞 | 4 | 化学 |
| 3 | 赵云 | 4 | 化学 |
| 1 | 关羽 | 5 | 物理 |
| 2 | 张飞 | 5 | 物理 |
| 3 | 赵云 | 5 | 物理 |
| 1 | 关羽 | 6 | 英语 |
| 2 | 张飞 | 6 | 英语 |
| 3 | 赵云 | 6 | 英语 |
+----+------+----+------+
我们称这样的结果为笛卡尔积也叫叉联结,这里面的许多数据并不是我们想要的,为了避免这样的结果我们必须WHERE
子句来作为过滤条件。
13.2 内连接
上面一个例子中的联结类型我们称为内联结,即:用一个表中的记录去匹配另一个表,如果符合条件则返回为我们的查询结果。上面的使用WHERE
语句的联结我们称为隐式内联结,用WHERE
语句指定条件。而对应的显式内联结语法有所不同:
SELECT teacher.id,teacher.name,course.id,course.name
FROM teacher INNER JOIN course ON teacher.id = course.teacher_id;
同样可以得到:
+----+------+----+------+
| id | name | id | name |
+----+------+----+------+
| 1 | 关羽 | 1 | 语文 |
| 1 | 关羽 | 2 | 数学 |
| 2 | 张飞 | 3 | 生物 |
| 2 | 张飞 | 4 | 化学 |
| 2 | 张飞 | 5 | 物理 |
| 3 | 赵云 | 6 | 英语 |
+----+------+----+------+
显示内联结的语法格式为:
SELECT 列名1,列名2... FROM 表1 INNER JOIN 表2 ON 条件;
可以看到不管是显示内联结还是隐式内联结都解决了笛卡尔积问题,显示内联结,一般称为标准的内联结,有INNER JOIN
,查询到的数据为两个表经过on条件过滤后的笛卡尔积。
13.3 自联结
前面我们有说到使用表别名可是使一个表在SELECT
语句中多次方便的使用。还记得我们之前一直使用的student学生表吗?
+------+----------+------+------------+------+------------+------+---------+---------+
| id | name | age | birthday | sex | address | math | english | remark1 |
+------+----------+------+------------+------+------------+------+---------+---------+
| 2 | 李若彤 | 22 | 1993-08-11 | 女 | 上海 | 40 | 50 | a |
| 3 | 迪丽热巴 | 22 | 1990-09-01 | 女 | 乌鲁木齐 | 40 | 50 | A |
| 1 | 柳岩 | 24 | 1990-03-23 | 女 | 上海 | 70 | 60 | B |
| 4 | 高圆圆 | 22 | 1994-04-03 | 女 | 北京 | 90 | 40 | b |
| 5 | 丫丫 | 20 | 1996-08-11 | 女 | 乌鲁木齐 | 99 | 80 | c |
| 6 | 李金铭 | 25 | 1995-11-21 | 女 | 济南 | 70 | 50 | a |
| 7 | 张三 | 22 | 1990-06-14 | 男 | 西安 | 60 | 70 | c |
| 8 | 李四 | 23 | 1996-07-11 | 男 | 成都 | 80 | 30 | b |
| 9 | 王五 | 20 | 1998-06-10 | 男 | 上海 | 70 | 80 | b |
| 10 | 赵一 | 23 | 1992-06-11 | 男 | 北京 | 60 | 60 | a |
| 11 | 钱二 | 25 | 1994-06-18 | 男 | 成都 | 60 | 70 | c |
| 12 | 周六 | 22 | 1992-02-27 | 男 | 北京 | 80 | 80 | a |
+------+----------+------+------------+------+------------+------+---------+---------+
有这样一个学生表,现在我们要得到和张三年龄一样的的学生信息,考虑这个问题我们用之前的子查询:
SELECT *
FROM student
WHERE age = (SELECT age
FROM student
WHERE name = '张三');
我们首先用子查询得到张三的年龄,然后再从student表中找到这个年龄的学生的信息。
下面我们用自联结可以得到同样的结果:
SELECT s1.*
FROM student AS s1,student AS s2
WHERE s1.age = s2.age AND s2.name = '张三';
这条SQL语句中我们使用WHERE
将这个student表和自己连接了起来,将符合条件的数据滤了出来。两次出现了相同的一个表为了避免歧义我们用到的别名来区分两个同样的表。
13.4 自然联结
上面一节的查询语句中:
SELECT s1.*
FROM student AS s1,student AS s2
WHERE s1.age = s2.age AND s2.name = '张三';
我们使用SELECT s1.*
只留下了s1表中的内容,但是如果我们用如下的语句:
SELECT *
FROM student AS s1,student AS s2
WHERE s1.age = s2.age AND s2.name = '张三';
这时出现的结果就不是我们想要的,因为会出现重复的列。自然连接是排除多次出现,一列只会出现一次。非自然连接那样冗余、重复的数据并不是我们想要的。
13.5 外联结
我们使用联结是将一个表中的行与另一个表中的行进行关联,从而得到的数据是两个表都有的行的数据,但是有时候我们可能需要的是将一个表中没有关联的行也检索出来,这个时候我们需要用到外联结。外联结我们分为左外连接和右外联结。
为了能方便演示外联结,我们向老师表和课程表中添加一些和另一个表并没有对应联结的数据:
-- 插入id为4的老师诸葛亮但是在课程表中没有一门课程对应老师表中id为4的老师
insert into teacher values(4,'诸葛亮');
-- 插入历史课程但是没有对应老师的id
insert into course values(null,'历史',null);
13.5.1 左外联结
现在我们有个需求是需要将老师和对应的课程查询出来,我们先用之前的内联结的方式:
SELECT teacher.id,teacher.name,course.id,course.name
FROM teacher INNER JOIN course ON teacher.id = course.teacher_id;
可以得到查询结果:
+----+------+----+------+
| id | name | id | name |
+----+------+----+------+
| 1 | 关羽 | 1 | 语文 |
| 1 | 关羽 | 2 | 数学 |
| 2 | 张飞 | 3 | 生物 |
| 2 | 张飞 | 4 | 化学 |
| 2 | 张飞 | 5 | 物理 |
| 3 | 赵云 | 6 | 英语 |
+----+------+----+------+
可以看到尽管我们已经插入了新的一些数据,但是在这里我们使用内联结查询的结果和之前并没有变化。但是如果我们现在需要得到所有的老师和课程的对应数据呢?这里我们就需要使用到外联结,我们先在这里使用左外联结:
SELECT teacher.id,teacher.name,course.id,course.name
FROM teacher LEFT JOIN course
ON teacher.id = course.teacher_id;
这时我们可以得到:
+----+--------+------+------+
| id | name | id | name |
+----+--------+------+------+
| 1 | 关羽 | 1 | 语文 |
| 1 | 关羽 | 2 | 数学 |
| 2 | 张飞 | 3 | 生物 |
| 2 | 张飞 | 4 | 化学 |
| 2 | 张飞 | 5 | 物理 |
| 3 | 赵云 | 6 | 英语 |
| 4 | 诸葛亮 | NULL | NULL |
+----+--------+------+------+
可以看到尽管id为4的老师没有对应的课程表中的数据但是它还是被查询了出来,这里的左外联结就以LEFT JOIN
语句左边的表为主表右边的表为从表去查询数据。所以老师表中的数据不论是否和课程表中的数据右对应关系都会被查询出来。
13.5.2 右外联结
右外联结和左外联结十分类似,现在我们还是要查询老师表和课程表的对应的数据,但是我们现在想要无论是否右对应关系,课程表中的数据都要显示出来,用我们上面的左外联结当然就可以完成,只需要把LEFT JOIN
这个子句的左边换成现在的主表课程表而右边是老师表就可以得到:
SELECT teacher.id,teacher.name,course.id,course.name
FROM course LEFT JOIN course
ON teacher.id = course.teacher_id;
可以得到:
+------+------+----+------+
| id | name | id | name |
+------+------+----+------+
| 1 | 关羽 | 1 | 语文 |
| 1 | 关羽 | 2 | 数学 |
| 2 | 张飞 | 3 | 生物 |
| 2 | 张飞 | 4 | 化学 |
| 2 | 张飞 | 5 | 物理 |
| 3 | 赵云 | 6 | 英语 |
| NULL | NULL | 9 | 历史 |
+------+------+----+------+
可以看到所欲的课程数据都被查询了出来尽管它没有和老师表中的老师对应,这个方法是左外联结的方式,而右外联结我们可以将查询的主表放在RIGHT JOIN
语句的右边就可以得到我们要的结果:
SELECT teacher.id,teacher.name,course.id,course.name
FROM teacher RIGHT JOIN course
ON teacher.id = course.teacher_id;
可以得到和上面一样的结果。所以我们得到其实左外连接和右外连接其实可以相互转换,只要注意主表和从表在语句中的位置就行,还有就是你可能发现,只用左外联结或者只用右外连接就可以完成不同需求。
可以看到无论是使用哪种联结我们都需要ON
语句后加上条件,这是必须的,如果没有条件就会得到笛卡尔积。
13.6 组合查询
组合查询就是使用UNION
操作符将多个SELECT
语句的查询结果组合成一个结果集。组合查询可以用在多个表的查询或是一个表中的多次查询中。下面来看这样一个例子:
现在我们要在老师表中和课程表中查询出关羽老师和张飞老师的id和姓名以及它们教授的课程,看到这样的需求我们不用组合查询也可以实现,但是我们先用组合查询来实现:
SELECT teacher.id,teacher.name,course.name
FROM teacher,course
WHERE teacher.id = course.teacher_id AND teacher.name = '关羽'
UNION
SELECT teacher.id,teacher.name,course.name
FROM teacher,course
WHERE teacher.id = course.teacher_id AND teacher.name = '张飞';
可以得到:
+----+------+------+
| id | name | name |
+----+------+------+
| 1 | 关羽 | 语文 |
| 1 | 关羽 | 数学 |
| 2 | 张飞 | 生物 |
| 2 | 张飞 | 化学 |
| 2 | 张飞 | 物理 |
+----+------+------+
我们用UNION
联结了两个SELECT
语句,将两个SELECT
语句的查询结果组合到了一起。
我们可以用一个SELECT
语句完成这个需求:
SELECT teacher.id,teacher.name,course.name
FROM teacher,course
WHERE teacher.id = course.teacher_id AND (teacher.name = '关羽' OR teacher.name = '张飞');
看起来似乎用UNION
要更繁琐一些,但是在查询语句非常复杂的情况使用UNION
可能会更简单。同样的我们也可以使用ORDER BY
对查询结果进行排序。
SELECT teacher.id,teacher.name,course.name
FROM teacher,course
WHERE teacher.id = course.teacher_id AND (teacher.name = '关羽' OR teacher.name = '张飞')
ORDER BY teacher.id;
14 一些练习
这里有一些关于多表查询的练习,大多都是用子查询解答的。
14.1准备数据
-- 创建老师表 老师编号为主键
create table teacher (
id int PRIMARY key auto_increment,
name varchar(50)
);
-- 创建课程表 老师编号外键约束
CREATE table course (
id int primary key auto_increment,
name varchar(50),
teacher_id int,
foreign key(teacher_id) references teacher(id)
);
-- 创建学生课程表 中间表
create table studentcourse (
student_id int,
course_id int,
score double,
foreign key(student_id) references student(id),
foreign key(course_id) references course(id)
);
-- 给已经存在的学生表的id设为主键
alter table student add primary key (id);
以及给老师表、课程表、学生课程中间表添加数据:
-- 添加老师数据
insert into teacher values(null,'关羽');
insert into teacher values(null,'张飞');
insert into teacher values(null,'赵云');
-- 添加课程数据
insert into course values(null,'语文',1);
insert into course values(null,'数学',1);
insert into course values(null,'生物',2);
insert into course values(null,'化学',2);
insert into course values(null,'物理',2);
insert into course values(null,'英语',3);
-- 添加学生课程表中间表数据
insert into studentcourse values(1,1,80);
insert into studentcourse values(1,2,90);
insert into studentcourse values(1,3,85);
insert into studentcourse values(1,4,78);
insert into studentcourse values(2,2,53);
insert into studentcourse values(2,3,77);
insert into studentcourse values(2,5,80);
insert into studentcourse values(3,1,71);
insert into studentcourse values(3,2,70);
insert into studentcourse values(3,4,80);
insert into studentcourse values(3,5,65);
insert into studentcourse values(3,6,75);
insert into studentcourse values(4,2,90);
insert into studentcourse values(4,3,80);
insert into studentcourse values(4,4,70);
insert into studentcourse values(4,6,95);
insert into studentcourse values(5,1,60);
insert into studentcourse values(5,2,70);
insert into studentcourse values(5,5,80);
insert into studentcourse values(5,6,69);
insert into studentcourse values(6,1,76);
insert into studentcourse values(6,2,88);
insert into studentcourse values(6,3,87);
insert into studentcourse values(7,4,80);
insert into studentcourse values(8,2,71);
insert into studentcourse values(8,3,58);
insert into studentcourse values(8,5,68);
insert into studentcourse values(9,2,88);
insert into studentcourse values(10,1,77);
insert into studentcourse values(10,2,76);
insert into studentcourse values(10,3,80);
insert into studentcourse values(10,4,85);
insert into studentcourse values(10,5,83);
insert into studentcourse values(11,1,67);
insert into studentcourse values(11,7,88);
insert into studentcourse values(11,3,56);
insert into studentcourse values(11,4,66);
insert into studentcourse values(12,1,88);
insert into studentcourse values(12,2,55);
insert into studentcourse values(12,3,67);
insert into studentcourse values(12,6,77);
insert into studentcourse values(12,5,78);
现在可以看到创建的表的结构大概是这样:
-- 学生表:
+----------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(20) | YES | | NULL | |
| age | int(11) | YES | | NULL | |
| birthday | date | YES | | NULL | |
| sex | char(2) | YES | | NULL | |
| address | varchar(50) | YES | | NULL | |
| math | int(11) | YES | | NULL | |
| english | int(11) | YES | | NULL | |
| remark1 | varchar(10) | YES | | NULL | |
+----------+-------------+------+-----+---------+-------+
-- 老师表
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(50) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
-- 课程表
+------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(50) | YES | | NULL | |
| teacher_id | int(11) | YES | MUL | NULL | |
+------------+-------------+------+-----+---------+----------------+
-- 学生课程表
+------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------+------+-----+---------+-------+
| student_id | int(11) | YES | MUL | NULL | |
| course_id | int(11) | YES | MUL | NULL | |
| score | double | YES | | NULL | |
+------------+---------+------+-----+---------+-------+
并且这些表里也有了一些可以用来查询的数据,这些不是这里的重点,我们的重点是下面的子查询。
我们现在创建的表都是关系表:
- 学生表student中保存了学生的基本数据以及重要的学生id,
- 在老师表中保存了老师的姓名和id,
- 在课程表中保存了课程的id、课程名以及和老师表构成关系的老师id,
- 学生课程表中存储了课程id、学生id和学生对应课程的成绩。
现在我们来完成一些查询。
14.2 查询最高分学生的信息
要查到最高分的学生的信息,我们肯定需要查询学生表student,但是学生的成绩保存在课程和学生的中间表中,所以我们也必须查询这个中间表,其他的表就不需要了。我们的思路是首先从中间表中找到最高分的成绩,从最高分成绩可以得到对应的课程id和学生id,这个学生id就是我们要的,最后依靠这个学生id在学生表中找到对应的学生,输出这个学生的信息。
在中间表中找到最高分:
SELECT MAX(score) FROM studentcourse;
我们得到最高分:
max(score) 95
根据这个最高分在中间表中找到对应的学生id:
我们从上一步得到最高分是95,所以:
SELECT student_id FROM studentcourse WHERE score = 95;
这个最高分95是从上一步得到的,我们现在把两步写到一起,这就是子查询:
SELECT student_id FROM studentcourse WHERE score = (SELECT MAX(score) FROM studentcourse);
这样我们就可以得到最高分对应的学生的id:
student_id 4
最后我们根据得到的最高分的学生id在学生表中查询这个学生的信息:
SELECT * FROM student WHERE id = 4;
我们再把4替换成上面的查询语句就有了:
SELECT * FROM student WHERE id = (SELECT student_id FROM studentcourse WHERE score = (SELECT MAX(score) FROM studentcourse));
这样就得到了最后要的最高分的学生的信息:
+----+--------+------+------------+------+---------+------+---------+---------+ | id | name | age | birthday | sex | address | math | english | remark1 | +----+--------+------+------------+------+---------+------+---------+---------+ | 4 | 高圆圆 | 22 | 1994-04-03 | 女 | 北京 | 90 | 40 | b | +----+--------+------+------------+------+---------+------+---------+---------+
这样我们就一步一步的得到最终的结果,需要注意的是我们三次的查询每次都是检索出来的结果都是只有一个,所以我们每次都是用的WHERE
来匹配,在查询到数据不仅仅只有一个等情况,我们就用IN
。
14.3 查询课程编号是2的课程比编号是1的课程最高成绩高的学生信息
查询课程编号是2的课程比编号是1的课程最高成绩高的学生信息,要完成这个练习我们来把这个查询拆分:
查询出课程编号为1的最高分:
SELECT MAX(score) FROM studentcourse WHERE course_id = 1;
可以的到:
+------------+ | max(score) | +------------+ | 88 | +------------+
查询课程编号是2并且分数比88高的学生的id:
SELECT student_id FROM studentcourse WHERE course_id = 2 AND score > 88;
这里用
AND
连接了course_id = 2
和score > 88
,两个条件都要满足的学生的id,所以SELECT
后面跟的是student_id
。这样我们就得到了:+------------+ | student_id | +------------+ | 1 | | 4 | +------------+
利用上一步得到学生id查询学生的信息:
SELECT * FROM student WHERE id IN (1,4);
这样我们就得到了我们要的最终的结果,课程编号是2的课程比编号是1的课程最高成绩高的学生信息,现在我们来把上面三个语句合并成一个:
SELECT * FROM student WHERE id IN (SELECT student_id FROM studentcourse WHERE course_id = 2 AND score > (SELECT MAX(score) FROM studentcourse WHERE course_id = 1));
注意:从第二步中的得到的学生id我们并不知道到底有多少个符合条件的,可能有1个也可能有很多,所以我们只能在最后一步用
IN
。我们得到最终查询的结果:+----+--------+------+------------+------+---------+------+---------+---------+ | id | name | age | birthday | sex | address | math | english | remark1 | +----+--------+------+------------+------+---------+------+---------+---------+ | 1 | 柳岩 | 24 | 1990-03-23 | 女 | 上海 | 70 | 60 | B | | 4 | 高圆圆 | 22 | 1994-04-03 | 女 | 北京 | 90 | 40 | b | +----+--------+------+------------+------+---------+------+---------+---------+
14.4 查询编号是2的课程比编号是1的课程最高成绩高的学生姓名和成绩
查询编号是2的课程比编号是1的课程最高成绩高的学生姓名和成绩,和上面个的方法一样我们把步骤拆分成三个部分:
查询出课程编号为1的最高分:
SELECT MAX(score) FROM studentcourse WHERE course_id = 1;
可以得到和上面的一样的结果是最高分是88.
查询课程编号为2并且分数比88高的学生的id和成绩:
SELECT student_id,score FROM studentcourse WHERE course_id = 2 AND score > 88;
我们注意到要求是要获取符合条件的学生的姓名和成绩,学生的姓名只能通过学生id然后在学生表student中获得,但是这里还需要获取成绩,成绩只能在我们目前操作的表studentcourse这个表中获得,所以在这里第二步操作中我们就需要在这个表中获取到学生的成绩。我们可以得到:
+------------+-------+ | student_id | score | +------------+-------+ | 1 | 90 | | 4 | 90 | +------------+-------+
可以注意到的是我们既在这步取得了符合条件的学生的id还有它们的成绩,这个表被当作为一个临时表用作下一步查询。这里有个很重要的概念临时表。我们在下一步来看它的具体作用。
从student中查询学生姓名,从第二步中得到的临时表中查询学生的成绩:
SELECT student.name,temp.score FROM student,(SELECT student_id,score FROM studentcourse WHERE course_id = 2 AND score > 88) AS temp WHERE student.id = temp.student_id;
这样就把第第二步和第三步结合到了一起,现在我们再把88这个分数替换为步骤1中的语句:
SELECT student.name,temp.score FROM student,(SELECT student_id,score FROM studentcourse WHERE course_id = 2 AND score > (SELECT MAX(score) FROM studentcourse WHERE course_id = 1)) AS temp WHERE student.id = temp.student_id;
现在这个语句就是整个这个练习的结果,我们来再看下这个问题:
我们的问题是查询编号是2的课程比编号是1的课程最高成绩高的学生姓名和成绩,所以毋庸置疑的是最终的结果是学生的姓名和成绩数据,但是学生的姓名和成绩分别保存在不同的表中,所以我们肯定不能像上面两个练习一样仅仅在学生student这个表中检索,我们需要从学生和课程的中间表studentcourse这个表中取得符合条件的学生的成绩,还有要从这个表中的到符合条件的学生的id,所用我们在第二步的时候从这个中间表中取得了student_id,score这个两个数据来放到临时表中。这里第一步和第二步中关于过滤出我们想要的数据我们就不在赘述了,和第一、二个练习一样。我们在这里需要重点说明的是这个在第三步中起到重要作用的中间表。从第二步中过滤出了符合条件的学生的id和成绩,在第三步中我们把这两个数据放到了临时表中,需要注意的是这里一定要给临时表一个别名,我们用
AS temp
给临时表起了别名temp,看到FROM
语句后面STUDENT
和这个临时表,临时表很长的语句其实就相当于一个普通的表,我们从这两个表中来查询我们想要的数据,这个临时表的名字就是temp。
14.5 查询每个同学的学号、姓名、选课数、总成绩。
现在我们需要查询每个同学的学号、姓名、选课数、总成绩,从前面的题目我们可以知道我们需要的学号、选课数、总成绩都都是在学生和课程studentcourse这个中间表中得到,姓名需要在学生表student中获得。但是这个练习和前面不同之处在于我们要得到选课数和总成绩,这两个数据不可能简单的从中间表中获得,这里必须要用到我们之前学到的聚合函数COUNT()
、SUM()
去获得选课数和总成绩。还有一个重要的问题是如何才能获得一个同学的这些数据呢?我们知道一个学生对应一个学号,相同的两个、三个或者多个学号也是对应一个学生,我们在这里要用到前面学到的分组GROUP BY
来对中间表的学号进行分组,分组后我们就可以用聚合函数容易的获得我们想要的数据了。我们一样的分步来做:
对中间表进行分组并且得到学号再利用聚合函数得到每个学号的选课数和总成绩:
SELECT student_id AS id,COUNT(*) AS count,SUM(score) AS score FROM studentcourse GROUP BY student_id;
别忘记了这里我们用到了
AS
来给三个数据起了别名,我们得到一个临时表:+------+-------+-------+ | id | count | score | +------+-------+-------+ | 1 | 4 | 333 | | 2 | 3 | 210 | | 3 | 5 | 361 | | 4 | 4 | 335 | | 5 | 4 | 279 | | 6 | 3 | 251 | | 7 | 1 | 80 | | 8 | 3 | 197 | | 9 | 1 | 88 | | 10 | 5 | 401 | | 11 | 4 | 277 | | 12 | 5 | 365 | +------+-------+-------+
根据上一步的得到的id来查找学生姓名,并且和上一步得到的数据一起输出:
SELECT student.name,temp.*
FROM student,(SELECT student_id AS id,COUNT(*) AS count,SUM(score) AS score
FROM studentcourse
GROUP BY student_id) AS temp
WHERE student.id = temp.id;
这一步我们就的到了我们要的结果:
+----------+------+-------+-------+
| name | id | count | score |
+----------+------+-------+-------+
| 柳岩 | 1 | 4 | 333 |
| 李若彤 | 2 | 3 | 210 |
| 迪丽热巴 | 3 | 5 | 361 |
| 高圆圆 | 4 | 4 | 335 |
| 丫丫 | 5 | 4 | 279 |
| 李金铭 | 6 | 3 | 251 |
| 张三 | 7 | 1 | 80 |
| 李四 | 8 | 3 | 197 |
| 王五 | 9 | 1 | 88 |
| 赵一 | 10 | 5 | 401 |
| 钱二 | 11 | 4 | 277 |
| 周六 | 12 | 5 | 365 |
+----------+------+-------+-------+
SELECT
这个语句中我们将temp这个临时表的所有数据全都取出,因为我们在第二步中得到的数据就是最终要输出的数据,最后WHERE
语句来通过我们得到的id来找到对应的学生表中的姓名name,注意WHERE
后我们用的是student.id = temp.id
这是因为我们给中间表中的student_id已经取了别名id,所以这里取的是temp.id
。这里完全限定列名就是为了防止DBMS误解我们的意思,我们想要取的id来自不同的两个表中。