MySQL:高级查询 - 上
查询功能是MySQL最重要的功能之一,除了查询单表之外,MySQL还有将多张表甚至于多个数据库的信息进行聚合、统计等操作的能力。本文除了简要、完整地介绍单表查询之外,还将介绍多表连接查询、聚合查询、分组统计查询和子查询等重要技能。
单表查询复习
在本节内容中,我们将对单表查询作一些复习巩固,不了解单表查询的请阅读专栏中的前几篇文章。传送门
查询全部或指定字段
我们前面已经介绍过,在MySQL中,查询表中全部字段要将SELECT
语句后面的查询字段列表改为通配符*
,如果需要指定查询部分字段,只需要将查询字段列表改为自己需要的字段即可,多个字段之间用英文标点“,
”分隔。
下面演示查询army_region
表中的全部数据和army_info
表中的支队编号、人数、查阅权限等级三字段数据(由于数据量原因均限制到8行):
/* 复习 - 查询表中全部和部分字段 */
SELECT * FROM `army_region`; # 查询army_region表中全部数据
SELECT armyid,numOfPol,queryPri FROM `army_info`; # 查询army_info表中部分字段数据
去除重复数据
在MySQL中,如果我们在进行数据查询时需要排除重复结果对应用程序的干扰(比如登录、修改用户信息等),可以使用DISTINCT
关键字去除结果集中重复的记录,下面进行演示(首先向表中插入重复数据):查询region_dirtbl
表中部分字段数据(除id
列的所有数据)并去除重复记录。
1.先向表中写入重复数据(使用Navicat)
2.使用DISTINCT关键字去除重复行
/* 复习 - 查询时数据去重 */
SELECT DISTINCT region_L1,region_L2,regionDir,isOverSea FROM `region_dirtbl`;
可以看到,使用DISTINCT
关键字后,结果集中的重复数据已经被去除。
范围查询
在前一篇文章超详细的MySQL入门教程(五)中,我们已经介绍了大量MySQL运算符的使用。然而,还有一种 BETWEEN
运算符可以筛选出指定字段数据在规定数值范围之间的记录。语法如下:
/* BETWEEN运算符语法介绍 */
# SELECT ... WHERE <字段名> BETWEEN <起始数值> AND <结束数值>;
比如,在army_info
表中筛选出人数在3000到6000之间的千岩军支队信息:
/* BETWEEN运算符实战 */
SELECT * FROM `army_info` WHERE numOfPol BETWEEN 3000 AND 6000;
可以看到,人数在3000-6000之间的记录信息已经被查询出来。
正则查询
在MySQL中,还有一种 REGEXP
运算符,可以用于筛选符合指定正则表达式的记录。语法如下:
/* REGEXP运算符语法介绍 */
# SELECT ... WHERE <字段名> REGEXP '<正则表达式>';
例如,筛选出army_region
表中包含’Per’三字符的记录(正则表达式:Per
):
/* REGEXP运算符实战 */
SELECT * FROM `army_region` WHERE region_L2 REGEXP 'Per';
如图,已经成功筛选出了二级地址符合正则表达式'Per'
的记录。
结果集排序
在MySQL中,我们可以使用ORDER BY
子句根据某一字段对结果集进行排序(可以指定升序ASC
和降序DESC
,默认升序),例如:
/* ORDER BY子句演示 */
SELECT * FROM `army_info` ORDER BY armyID DESC LIMIT 10; # 查询army_info表中倒数10行
在这个例子中,我们使用ORDER BY
子句对armyID
字段降序排序,配合LIMIT
子句限制输出行数,成功查出了army_info
表中倒数10行数据。
单表多条件查询实战
关于
AND
、OR
和NOT
三种运算符的作用和使用它们进行简单条件查询的方法,作者已经在超详细的MySQL入门教程(四)中进行了详细的介绍。本节主要通过一个复杂程度相对微大的例子来介绍多个条件的混合使用,让读者对多条件查询有更加深刻的认识和理解。ps:单表,其实真的不难ヽ( ̄▽ ̄)و
题目
临近海灯节前,千岩军和三十人团要在层岩巨渊采樵谷及其西北地区执行联合行动,璃月方参加行动的部队须符合以下条件:
- 建立日期在
2019-06-01
之后, - 部队人数不在以下四个数字之内:
0、200、300、360
, - 如果部队条件不符合第一条和第二条 ,但其查询权限等级为3,仍可以参加联合行动。
- 璃月方部队总数为5支。
月海亭现委托你编写SQL查询语句,在数据库(army_info
数据表)中查找出符合条件的部队信息记录,并将结果集按armyid
字段升序排序,限制显示条数为5条。
分析
本题除了查询出符合联合行动的部队信息以外,还对结果集的显示做了限制打印条数和按字段排序的要求。我们先逐条分析所有条件:
- 首先是建立日期。建立日期要求在2019-06-01之后,可以通过
DATE()
函数提取字段birthday
的日期数据,再使用>
运算符筛选出在目标日期之后的日期即可。表达式:DATE(birthday) > '2019-06-01'
。 - 其次是部队人数。题目给出了一个集合,要求部队人数(
numOfPol
字段)不在这四个集合元素之内:0、200、300和360,则我们可以使用IN
运算符设定一个集合,再使用NOT
运算符传达逻辑否定即可。表达式:numOfPol NOT IN(0,200,300,360)
。 - 然后是查询权限等级。如果一支部队不符合条件1和条件2,但它的查询权限等级为3,那么这支部队也符合参加条件。我们可以使用
=
运算符设置queryPri
字段为3,同时这里应该用OR
运算符来连接前面的条件。表达式:queryPri = 3
。 - 下一步是确定优先级。根据题目要求,条件1和条件2应该在同一优先级(看作一个整体),而且部队信息应该同时符合两个条件,所以条件1和条件2之间使用
AND
运算符连接;当部队信息不符合条件1和2这个整体时,才会判断条件3,如果符合条件3则会被显示出来,所以条件3和条件1、2这个整体之间要用OR
连接。由于AND
运算符的优先级高于OR
运算符,所以条件1和条件2不需要加()
。 - 最后是结果集过滤。使用
ORDER BY
子句依据指定字段进行结果集排序,使用LIMIT
子句限制结果集行数。
答案
/* 单表查询实战 */
SELECT * FROM `army_info` WHERE
(
DATE(birthday) > '2019-06-01'
AND
numOfPol NOT IN(0,200,300,360)
) OR queryPri = 3
ORDER BY armyID LIMIT 5;
ps:这里为了美观加了括号,实际上不加也一样。
聚合函数
基本语法介绍
聚合函数,又名组函数,它通过对表中一组数据进行一系列计算操作,返回这组数据的数量、平均值、方差等信息,是对数据进行统计的函数。聚合函数一般配合分组语句使用,以对数据进行分组统计。
在MySQL中,有以下常用的聚合函数:
COUNT()
,统计数据总数。SUM()
,求数据总和。MAX()
,求一组数据最大值。MIN()
,求一组数据最小值。AVG()
,求一组数据平均值。- 其它函数,如
STD()
标准差,VARIANCE()
方差。
注意:
- 聚合函数会忽略
NULL
值。 - 聚合函数一般在
SELECT
、HAVING
和ORDER BY
之后,无法在WHERE
后面使用,因为WHERE
子句是对表中的值进行逐条操作,不是对一组值进行操作。 - 如果在SQL语句中没有分组子句的情况下使用聚合函数,那么必须对所有选择的列使用聚合函数。
聚合函数的基本使用语法如下:
/* 聚合函数的基本语法 */
SELECT FUNCTION(column1),FUNCTION(column2) FROM `table`; # 在没有分组子句的情况下必须对所有选中列使用聚合函数
SELECT column1,FUNCTION(column2) FROM `table` GROUP BY column1; # 配合分组子句使用聚合函数
数据总量统计
在MySQL中,可以使用 COUNT()
函数来统计指定字段的数据总量,不会统计NULL
值。
/* 聚合函数 - COUNT() */
# 语法:
# SELECT COUNT([DISTINCT]<*|COLUMN_NAME>) FROM `table_name`;
SELECT COUNT(*) FROM `army_info`; # 统计表army_info中的数据总量,包括重复数据
SELECT COUNT(birthday) FROM `army_info`; # 统计表army_info中birthday字段的数据总量,包括重复数据
SELECT COUNT(DISTINCT *) FROM `army_info`; # 统计表army_info中的数据总量,不包括重复数据
下面演示通过COUNT()
函数统计表army_info
中armyID
和birthday
两个字段的数据总量:
/* 聚合函数应用 - COUNT() */
SELECT COUNT(armyID),COUNT(birthday) FROM `army_info`;
可以看到,我们现在成功统计出了两个字段的数据总量,因为birthday
字段中有一个值为NULL
,所以该值没有被统计。
在生产环境中,我们统计数据总量时,使用最多的应该是下面的这条SQL语句(统计表中全部数据总量):
SELECT COUNT(*) FROM `army_info`;
字段求和
在MySQL中,可以使用 SUM()
函数对数字字段进行求和操作,不会统计NULL
值。下面介绍语法:
/* 聚合函数 - SUM() */
# 语法:
# SELECT SUM([DISTINCT] column_name) FROM `table_name`;
SELECT SUM(numOfPol) FROM `army_info`; # 统计army_info表中numOfPol字段所有数据的总和
SELECT SUM(DISTINCT numOfPol) FROM `army_info`; # 统计army_info表中numOfPol字段所有数据的总和,并忽略重复值
下面演示统计army_info
表中已经登记的千岩军部队总人数:
求字段中最大值
在MySQL中,使用 MAX()
函数可以求出数字字段中最大的数值,不会统计NULL
值。下面介绍语法:
/* 聚合函数 - MAX() */
# 语法:
# SELECT MAX([DISTINCT] column_name) FROM `table`;
SELECT MAX(numOfPol) FROM `army_info`; # 求army_info表中numOfPol字段最大值
SELECT MAX(DISTINCT numOfPol) FROM `army_info`; # 求army_info表中numOfPol字段最大值,忽略重复值
下面演示求出登记的千岩军部队的最大人数值:
求字段中最小值
在MySQL中,使用 MIN()
函数可以求出数字字段中最小的数值,不会统计NULL
值。下面介绍语法:
/* 聚合函数 - MIN() */
# 语法:
# SELECT MIN([DISTINCT] column_name) FROM `table`;
SELECT MIN(numOfPol) FROM `army_info`; # 求army_info表中numOfPol字段最小值
SELECT MIN(DISTINCT numOfPol) FROM `army_info`; # 求army_info表中numOfPol字段最小值,忽略重复值
下面演示求出登记的千岩军部队的最小人数值:
求字段平均值
在MySQL中,我们可以使用 AVG()
函数求数字字段中的平均值,不统计NULL
值。
下面演示求出表中千岩军部队的人数平均值:
/* 聚合函数 - AVG() */
SELECT AVG(numOfPol) FROM `army_info`;
其它函数
在MySQL中,除了以上5个最常用的聚合函数,还有非常多的聚合函数可供使用,根据MySQL官方文档的记载,可供用户使用的聚合函数多达19个。如图:
更多的聚合函数,作者将会在以后的文章中讲解。
注:
GROUP_CONCAT
函数稍后将在本章章节:数据分组统计中讲解。
数据分组统计
分组子句介绍
为什么要用分组子句?
在实际的开发环境中,我们通常都会遇到要对表中某字段的数据进行分类统计的情况。拿我们的练习用数据库来举个例子:
- 请分别统计不同查询权限等级的千岩军部队的人数。
- 请分别统计驻扎在璃月不同地区的千岩军部队的数量。
- 请分别统计璃月不同机构管理的地区,统计数量并将它们在一行中列出。
- 类似的问题还有更多……
如果只允许你使用现在已经所学的技能,很难解决此类问题。于是,GROUP BY
子句应运而生。
分组子句的作用
GROUP BY
子句:根据一定的规则将一大个数据集划分为一组组的形式(分组),以便于以组为单位对数据进行数据处理。
简而言之,GROUP BY
子句的作用就是根据指定字段数据的异同(规则)将相对应的字段数据划分成一块块相对较小的“数据块”。如图所示:
语法和使用规则
GROUP BY
子句有着严格的语法和使用规则,下面进行介绍:
/* GROUP BY子句语法 */
# 语法:
# SELECT 列名1,列名2,...列名N,聚合函数(列名N+1) FROM `表名` [WHERE子句] GROUP BY 列名1,...,列名N;
SELECT col1,FUNCTION(col2) FROM `table_name` GROUP BY col1;
GROUP BY
子句的使用规则及注意事项如下:
- 不能对以下三种字段类型及其变长类型进行分组:
TEXT
、BIT
、BLOB
。 - 如果语句中使用了分组子句,那么所有被
SELECT
选中的字段都必须同时被GROUP BY
选中,除非该字段使用了聚合函数。 - 如果需要对分组后返回的结果进行排序,需要使用
ORDER BY
子句,GROUP BY
子句不会对结果排序。 - 如果需要进行条件筛选,必须在分组开始前执行,使用
HAVING
子句可对结果集进行二次过滤。(人话:不能把WHERE
子句写在GROUP BY
子句后面) - MySQL 8.0支持在
GROUP BY
语句中使用别名。 - 不能对使用聚合函数的字段进行分组!
- 被
GROUP BY
选中的字段不一定要被SELECT
选中。
例如,这些SQL语句是错误的:
/* 达咩!不能这样使用GROUP BY */
SELECT * FROM `army_info` GROUP BY queryPri;
SELECT queryPri,armyID,SUM(numOfPol) FROM `army_info` GROUP BY queryPri;
SELECT queryPri,armyID,SUM(numOfPol) FROM `army_info` GROUP BY queryPri,armyID WHERE id < 15;
SELECT queryPri,SUM(numOfPol) AS total FROM `army_info` GROUP BY total;
# 还有更多五花八门的错误用法......
分组统计
我们通常使用聚合函数配合分组子句进行数据统计,本节介绍使用GROUP_CONCAT()
函数和五个常用聚合函数进行分组统计的方法。
GROUP_CONCAT
在介绍使用该函数进行分组统计前,我们首先介绍一下它的作用和语法。
GROUP_CONCAT
函数:将分组中所有非NULL
的字符串连接到一个字符串中,如果没有非NULL
的字符串,那么它就会返回NULL
。
简而言之,该函数的作用就是把一个分组中所有的字段值合并成一个字符串记录值。如图所示:
该函数的语法如下:
/* 分组统计 - GROUP_CONCAT()函数语法 */
# 语法:
# GROUP_CONCAT([DISTINCT] col_name [ORDER BY col_name {ASC|DESC}] [SEPARATOR symbol])
SELECT col1,GROUP_CONCAT(DISTINCT col2 ORDER BY col1 DESC SEPARATOR '|') FROM `table_name` GROUP BY col1;
语法解释:
col_name
---- 必选,指明函数要操作的字段名。
DISTINCT
---- 可选,数据去重。
ORDER BY
子句 ---- 可选,在合并字段值操作前先对其进行排序。
SEPARATOR
---- 可选,指定分隔字段值用的字符。
下面进行举例演示。
题目
分组统计璃月各个一级地区内的所有负责单位,要求同行结果不能重复、用中文点号、
分隔,操作的表为 region_dirtbl
。
分析
- 根据题意可知,题目要求统计璃月各个一级地区内有多少单位在进行管理。
region_dirtbl
的表大致结构:id
主键记录编号、region_L1
一级地区、region_L2
二级地区、regionDir
区域负责单位。- 统计各个一级地区内的负责单位,就要对
region_L1
字段进行分组统计,并将regionDir
字段的对应数据合并成一行,因此可以确定分组子句:GROUP BY region_L1
和在SELECT
后面使用GROUP_CONCAT()
函数。 - 题目中对输出的结果格式进行了要求:一是同一行不能重复,在聚合函数中使用
DISTINCT
即可过滤重复记录;二是用中文点号进行分隔,同样地,在聚合函数中使用SEPARATOR
关键字即可解决。
答案
/* 分组查询 - GROUP_CONCAT()函数实战 */
SELECT region_L1,
GROUP_CONCAT(
DISTINCT regionDir # 数据去重
ORDER BY region_L1 # 排序
SEPARATOR '、') # 设置分隔符
AS resp_dept
FROM `region_dirtbl` GROUP BY region_L1;
如图,我们成功查询出了璃月各个一级地区内的所有负责单位列表。
使用其它聚合函数
除了GROUP_CONCAT()
函数,我们还可以使用其它MySQL提供的聚合函数来实现各种各样的需求,例如用MIN()
函数分组统计最小值,用SUM()
函数分组统计数据相加的和等等。本节演示使用前面介绍的聚合函数进行分组统计。
本节介绍的聚合函数:
COUNT()
、SUM()
。
分组统计数据总量
如果在分组查询中使用COUNT()
函数,可以分别统计出不同数据组别中的具体数据条数。它的使用方法如下:
/* 分组查询 - COUNT()使用方法介绍 */
SELECT col1,COUNT(col2) FROM `table_name` [WHERE ...] GROUP BY col1 [ORDER BY ...] [LIMIT ...];
# 解释:在表table_name中根据col1字段对col2字段进行数据总量分组统计。
注:在
[]
内的为可选参数。
下面通过一个例子来讲解使用COUNT()
函数进行分组统计的具体方法:
题目
分别统计璃月不同机构负责的二级地区总数量,按机构名称倒序排列。使用数据表:region_dirtbl
。
分析
- 分析数据表结构:二级地区 -
region_L2
,负责单位名称 -regionDir
。 - 既然要根据
regionDir
字段对region_L2
字段进行数据总量分组统计,就要对region_L2
字段使用COUNT()
聚合函数。 - 使用
ORDER BY
子句对结果集进行排序,倒序使用DESC
参数。
答案
/* 分组统计 - COUNT()函数实战 */
SELECT regionDir,COUNT(region_L2) AS region_totalNum FROM `region_dirtbl`
GROUP BY regionDir ORDER BY regionDir DESC;
如图,璃月各个机构管辖的地区总数量已经被统计出来。
分组统计数据总和
如果在分组查询中使用SUM()
函数,可以分别统计出不同数据组别中所有数值数据相加的和,它的使用语法我们不再重复阐述。
下面演示分别查询不同权限级别的千岩军总人数:
/* 分组查询 - SUM()函数实战 */
SELECT queryPri,SUM(numOfPol) AS total_polNum FROM `army_info`
GROUP BY queryPri ORDER BY queryPri ASC;
# 注:结果已经按queryPri字段进行正序排列。
关于其它聚合函数
在MySQL中,还有非常多内置的聚合函数。读者可以根据自己的需求,灵活地使用恰当的聚合函数。它们的使用语法基本一致。
小计子句
在MySQL的分组统计操作中,可以使用
WITH ROLLUP
子句对结果集中被分组统计的列生成小计。
例如:
如图所示,已经对结果集生成了小计。
工作原理
按照ANSI SQL-92
标准,SQL语言分组子句的小计功能应该包括以下三种:ROLLUP
、CUBE
和GROUPING SETS
;而MySQL 8.0中仅支持ROLLUP
。
ROLLUP
子句会根据输入列对每个分组集的数据值进行统计,并在分组集的最后插入一行包含NULL
值和小计值的超级聚合行。例如GROUP BY queryPri,region_L1 WITH ROLLUP
子句,会先对region_L1
列生成小计,再对整个表中的值生成小计。
如图所示,标红的为对region_L1
列生成的小计,标黄的为对整张表生成的小计。(ps:这里为了便于观察将结果导出为Excel文件)CUBE
子句会根据输入列进行排列组合,生成交叉报表,也会在每个分组集的最后插入包含NULL
值和小计值的超级聚合行。例如,如果输入列为c1
、c2
、c3
、c4
,那么会生成如下列的超级聚合行:c1+c2
、c1+c3
、c1+c4
、c2+c3
、c2+c4
、c3+c4
、全表小计。(由于MySQL不支持CUBE
,不做演示)GROUPING SETS
子句等价于将多个分组结果集结合起来。(同样的,由于MySQL不支持GROUPING SETS
子句,不进行演示)
基本语法
在MySQL中,ROLLUP
的使用语法非常简单。如果用户在查询中使用了GROUP BY
子句,将WITH ROLLUP
子句写到GROUP BY
子句输入列的后面即可。
下面举例介绍ROLLUP
的使用语法(分组统计璃月各机构负责管理的地区数量,并生成小计):
/* MySQL小计功能演示 */
SELECT regionDir,COUNT(region_L2) AS region_totalNum # 选择输入列
FROM `region_dirtbl` # 选择数据表
WHERE regionDir IS NOT NULL # 过滤regionDir字段中的NULL值
GROUP BY regionDir WITH ROLLUP # 选择进行分组的依据字段,指定生成小计
ORDER BY regionDir DESC; # 按照regionDir字段进行倒序排序
如图所示,已经生成了结果集的小计。
结果集二次过滤
HAVING
子句的功能:对结果集中的数据进行筛选。
在实际工作环境中,我们有时需要对结果集里的数据进行二次过滤,筛选掉一些不符合要求的结果行。这时,HAVING
子句就可以满足我们的需求。注意:HAVING
子句之于GROUP BY
子句的关系等同于WHERE
子句之于SELECT
的关系。
在使用HAVING
子句时应当注意如下规则:
HAVING
子句必须写在WHERE
子句后面。HAVING
子句通常和GROUP BY
子句联合使用。HAVING
后面可以跟表达式,但该表达式运算的结果只能是布尔值(0或非0值)。
HAVING子句的基本语法如下所示:
/* HAVING语法介绍 */
# SELECT ... [WHERE子句] [GROUP BY子句] HAVING <表达式>;
SELECT col1,FUNC(col2) AS cname
FROM `table_name`
GROUP BY col1 HAVING cname > 1;
例如,分组统计出不同创建年份的千岩军部队的人数,并在结果集中过滤人数小于6000的行,操作表army_info
:
注:可以通过
YEAR()
函数根据具体日期获取年份信息。
/* HAVING子句实战 */
SELECT
YEAR(birthday) AS createYear, # 获取年份信息用于分组
SUM(numOfPol) AS totalNum
FROM `army_info`
GROUP BY createYear HAVING totalNum > 4000 # 根据人数进行过滤
ORDER BY createYear;
如图,我们已经统计出了不同年份建立的千岩军部队人数,并过滤了人数在4000及以下的部队。
分组子句和排序子句的区别
众所周知,在SQL语言中,GROUP BY
子句用于分组,ORDER BY
子句用于排序,但部分初学者可能对它们之间的区别比较模糊,下面进行区分:
- 分组子句是根据输入列数据的异同将对应列的数据划分为一个个分组集,然后使用聚合函数对分组集中的数据进行分析统计后返回一个值。
- 排序子句只是根据输入列对结果集各行的显示顺序进行调整。