MySQL流程控制函数、分组查询与连接查询详解(附示例与运行结果)
- 前言
- 数据准备
- 一、流程控制函数
- [1.1 IF(expr1,expr2,expr3)](#1.1 IF(expr1,expr2,expr3))
- [1.2 CASE函数](#1.2 CASE函数)
- [1.3 实战案例](#1.3 实战案例)
- 二、分组函数(聚合函数)
- [2.1 五大聚合函数](#2.1 五大聚合函数)
- [2.2 各函数适用数据类型与NULL处理总结](#2.2 各函数适用数据类型与NULL处理总结)
- [2.3 聚合函数都忽略NULL值的验证](#2.3 聚合函数都忽略NULL值的验证)
- [2.4 搭配DISTINCT去重](#2.4 搭配DISTINCT去重)
- [2.5 COUNT(*) vs COUNT(字段) vs COUNT(1) 的区别](#2.5 COUNT(*) vs COUNT(字段) vs COUNT(1) 的区别)
- [三、分组查询(GROUP BY)](#三、分组查询(GROUP BY))
- [3.1 语法格式总结(含执行顺序标注)](#3.1 语法格式总结(含执行顺序标注))
- [3.2 分组前筛选(WHERE)vs 分组后筛选(HAVING)](#3.2 分组前筛选(WHERE)vs 分组后筛选(HAVING))
- [3.3 基础示例:按部门求平均工资、按领导求最高工资](#3.3 基础示例:按部门求平均工资、按领导求最高工资)
- [3.4 复杂示例:查询员工个数大于2的部门](#3.4 复杂示例:查询员工个数大于2的部门)
- [3.5 按表达式/函数分组(按姓名长度分组)](#3.5 按表达式/函数分组(按姓名长度分组))
- [3.6 按多个字段分组(部门+工种)](#3.6 按多个字段分组(部门+工种))
- [3.7 分组查询+排序组合](#3.7 分组查询+排序组合)
- 四、连接查询
- [4.1 连接查询概念与笛卡尔积现象](#4.1 连接查询概念与笛卡尔积现象)
- [4.2 连接查询分类](#4.2 连接查询分类)
- [4.3 SQL99语法格式总结](#4.3 SQL99语法格式总结)
- [4.4 内连接(INNER JOIN)](#4.4 内连接(INNER JOIN))
- [4.5 外连接](#4.5 外连接)
- [4.6 交叉连接(CROSS JOIN)](#4.6 交叉连接(CROSS JOIN))
- 总结
前言
在上一篇基础查询文章中,我们学习了 SELECT、WHERE、ORDER BY 等基础查询语法,能够完成简单的单表数据检索。然而,真实的业务场景往往更加复杂:比如根据不同部门动态调整显示的工资、统计每个部门的平均工资、把员工表和部门表关联起来查询员工所在的部门名等。这些需求单靠基础查询是无法完成的,需要借助流程控制函数 、分组函数 、分组查询 以及连接查询来实现。
本文将围绕以下四大核心模块展开讲解:
- 流程控制函数 :
IF和CASE,用于在查询中做条件判断。 - 分组函数(聚合函数) :
SUM、AVG、MAX、MIN、COUNT,用于统计计算。 - 分组查询(GROUP BY) :配合聚合函数实现按组统计,并讲解
WHERE与HAVING的区别。 - 连接查询:内连接、外连接、交叉连接,把多个表的数据关联起来,是本文的重点也是实际开发中最常用的部分。
本文所有示例均基于 employees、departments、jobs、job_grades、beauty、boys 等表,建议读者边看边在本地环境执行验证,加深理解。
数据准备
本文示例基于以下数据表(完整建表 SQL 见配套文件 00_数据准备_建表与数据.sql)。
employees 表(共 29 行,下表展示前 10 行代表性数据):
| employee_id | first_name | last_name | job_id | salary | commission_pct | manager_id | department_id | hiredate |
|---|---|---|---|---|---|---|---|---|
| 100 | Steven | King | AD_PRES | 24000 | NULL | NULL | 90 | 1987-06-17 |
| 101 | Neena | Kochhar | AD_VP | 17000 | NULL | 100 | 90 | 1989-09-21 |
| 102 | Lex | De Haan | AD_VP | 17000 | NULL | 100 | 90 | 1993-01-13 |
| 103 | Alexander | Hunold | IT_PROG | 9000 | NULL | 102 | 60 | 1990-01-03 |
| 104 | Bruce | Ernst | IT_PROG | 6000 | NULL | 103 | 60 | 1991-05-21 |
| 105 | David | Austin | IT_PROG | 4800 | NULL | 103 | 60 | 1997-06-25 |
| 108 | Nancy | Greenberg | FI_MGR | 12000 | NULL | 101 | 100 | 1994-08-17 |
| 114 | Den | Raphaely | PU_MAN | 11000 | NULL | 100 | 30 | 1994-12-07 |
| 120 | Matthew | Weiss | ST_MAN | 8000 | NULL | 100 | 50 | 1996-07-01 |
| 145 | John | Russell | SA_MAN | 14000 | 0.40 | 100 | 80 | 1996-10-01 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
提示:其中 7 人有奖金率(employee_id 145-152),其余 22 人 commission_pct 为 NULL。工资范围 2500~24000。
departments 表(共 10 行):
| department_id | department_name | manager_id | location_id |
|---|---|---|---|
| 10 | Administration | 200 | 1700 |
| 20 | Marketing | 201 | 1800 |
| 30 | Purchasing | 114 | 1700 |
| 40 | Human Resources | 203 | 2400 |
| 50 | Shipping | 121 | 1500 |
| 60 | IT | 103 | 1400 |
| 80 | Sales | 145 | 2500 |
| 90 | Executive | 100 | 1700 |
| 100 | Finance | 108 | 1700 |
| 110 | Accounting | 205 | 1700 |
job_grades 表(共 5 行):
| grade_level | lowest_sal | highest_sal |
|---|---|---|
| A | 10000 | 99999 |
| B | 8000 | 9999 |
| C | 6000 | 7999 |
| D | 4000 | 5999 |
| E | 0 | 3999 |
beauty 表(共 7 行):
| id | name | sex | borndate | phone | boyfriend_id |
|---|---|---|---|---|---|
| 1 | 迪丽热巴 | 女 | 1992-06-03 | 17000000000 | 1 |
| 2 | 赵丽颖 | 女 | 1987-10-16 | 17000000001 | 2 |
| 3 | 杨幂 | 女 | 1986-08-12 | 17000000002 | 3 |
| 4 | 刘亦菲 | 女 | 1987-08-25 | 17000000003 | NULL |
| 5 | Anglelababy | 女 | 1989-02-28 | 17000000004 | 4 |
| 6 | 古力娜扎 | 女 | 1992-05-02 | 17000000005 | NULL |
| 7 | 景甜 | 女 | 1988-07-21 | 17000000006 | 5 |
boys 表(共 5 行):
| id | boyName | userCP |
|---|---|---|
| 1 | 鹿晗 | 800 |
| 2 | 冯绍峰 | 700 |
| 3 | 刘恺威 | 600 |
| 4 | 黄晓明 | 900 |
| 5 | 张继科 | 850 |
一、流程控制函数
流程控制函数允许我们在 SELECT 查询过程中根据条件动态地返回不同的值,类似于编程语言中的 if-else 和 switch-case。MySQL 中常用的流程控制函数主要有两个:IF 和 CASE。
1.1 IF(expr1,expr2,expr3)
语法说明:
sql
IF(expr1, expr2, expr3)
expr1:要判断的条件表达式,如果条件为真(非零或非空),则返回expr2,否则返回expr3。expr2:条件为真时返回的值。expr3:条件为假时返回的值。
简单示例:
sql
-- 判断2是否小于3,是则返回"大",否则返回"小"
SELECT IF(2 < 3, '大', '小') AS out_put;
运行结果:
| out_put |
|---|
| 大 |
业务示例: 查询员工的姓和名以及奖金率,如果有奖金率则返回"有",没有则返回"无",并以"备注"为列名。
sql
/*
分析:
查询的表:employees
查询的字段:last_name、first_name、commission_pct、备注
查询的条件:无
排序的条件:无
*/
SELECT
last_name,
first_name,
commission_pct,
IF(commission_pct IS NULL, '无', '有') AS 备注
FROM
employees;
运行结果(部分):
| last_name | first_name | commission_pct | 备注 |
|---|---|---|---|
| King | Steven | NULL | 无 |
| Kochhar | Neena | NULL | 无 |
| De Haan | Lex | NULL | 无 |
| Russell | John | 0.40 | 有 |
| Partners | Karen | 0.30 | 有 |
| ... | ... | ... | ... |
说明:
commission_pct为NULL的员工(22人),备注列显示"无";有奖金率的员工(7人)显示"有"。IF函数非常适合这种二选一的判断场景。
1.2 CASE函数
CASE 是一种流程控制函数,类似编程语言中的 switch 语法。它支持两种语法格式,分别适用于"等值匹配"和"条件判断"两种场景。
语法格式1:CASE value WHEN compare_value THEN result ...
sql
CASE value
WHEN compare_value1 THEN result1
WHEN compare_value2 THEN result2
...
[ELSE result]
END
value:需要进行比较的表达式或者列。compare_value:进行比较的表达式或值。result:当value等于compare_value时返回的结果。[ELSE result]:如果没有任何WHEN条件匹配,则返回的默认结果。
这种格式适合"某列等于某个值就返回某个结果"的等值匹配场景。
语法格式2:CASE WHEN condition THEN result ...
sql
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
...
[ELSE result]
END
condition:条件表达式,可以是任何布尔表达式。result:当条件表达式为真时返回的结果。ELSE result:如果没有条件表达式为真时,返回的默认结果。
这种格式适合"满足某个条件就返回某个结果"的区间判断场景,更灵活,可以写复杂的逻辑判断。
1.3 实战案例
案例1:按部门号调整工资倍数(使用语法格式1)
需求: 查询员工的工资,要求如下:
- 部门号 = 30,显示的工资为 1.1 倍
- 部门号 = 40,显示的工资为 1.2 倍
- 部门号 = 50,显示的工资为 1.3 倍
- 其他部门,显示的工资为原工资
sql
/*
分析:
查询的表:employees
查询的字段:salary、department_id、新工资
查询的条件:无
排序的条件:无
*/
-- 使用语法格式1:CASE department_id WHEN 30 THEN ...
SELECT
salary,
department_id,
CASE department_id
WHEN 30 THEN salary * 1.1
WHEN 40 THEN salary * 1.2
WHEN 50 THEN salary * 1.3
ELSE salary
END AS 新工资
FROM
employees;
运行结果(部分):
| salary | department_id | 新工资 |
|---|---|---|
| 24000 | 90 | 24000 |
| 17000 | 90 | 17000 |
| 3100 | 30 | 3410 |
| 2900 | 30 | 3190 |
| 8000 | 50 | 10400 |
| ... | ... | ... |
这个案例用的是"语法格式1",因为判断条件是
department_id等于某个值,属于等值匹配,用格式1更简洁。
案例1补充:使用语法格式2实现同样的效果
读者可以自行尝试使用语法格式2来改写上面的查询:
sql
-- 使用语法格式2:CASE WHEN department_id = 30 THEN ...
SELECT
salary,
department_id,
CASE
WHEN department_id = 30 THEN salary * 1.1
WHEN department_id = 40 THEN salary * 1.2
WHEN department_id = 50 THEN salary * 1.3
ELSE salary
END AS 新工资
FROM
employees;
两种写法效果完全一致。等值匹配场景推荐格式1,更简洁;区间判断场景只能用格式2。
案例2:按工资划分级别A/B/C/D(使用语法格式2)
需求: 查询员工的工资情况:
- 如果工资大于 20000,显示 A 级别
- 如果工资大于 15000,显示 B 级别
- 如果工资大于 10000,显示 C 级别
- 否则,显示 D 级别
sql
SELECT
salary,
CASE
WHEN salary > 20000 THEN 'A'
WHEN salary > 15000 THEN 'B'
WHEN salary > 10000 THEN 'C'
ELSE 'D'
END AS 工资级别
FROM
employees;
运行结果(部分):
| salary | 工资级别 |
|---|---|
| 24000 | A |
| 17000 | B |
| 17000 | B |
| 9000 | D |
| 14000 | C |
| ... | ... |
注意:这里必须用"语法格式2",因为判断条件是
salary > 20000这样的区间判断,而不是等于某个具体值。CASE WHEN会从上到下依次判断,一旦某个WHEN条件为真就返回对应结果,不再往下匹配,所以要把更严格的条件(更大的数值)写在前面。
至此,流程控制函数就讲解完毕。它解决了"查询时根据条件动态返回不同值"的问题。接下来我们进入分组函数的学习,它是统计计算的基础。
二、分组函数(聚合函数)
分组函数,又称为聚合函数或统计函数或组函数,主要用于对一组数据进行统计计算,比如求总和、平均值、最大值、最小值、个数等。
2.1 五大聚合函数
MySQL 提供了五个常用的聚合函数:
| 函数 | 功能说明 |
|---|---|
SUM() |
返回某列的总和 |
AVG() |
返回某列的平均值 |
MAX() |
返回某列的最大值 |
MIN() |
返回某列的最小值 |
COUNT() |
返回查询的行数 |
基础示例:
sql
-- 1、SUM():查询所有员工的工资总和
SELECT SUM(salary) AS sum_sal FROM employees;
-- 2、AVG():查询所有员工的平均工资(保留2位小数)
SELECT ROUND(AVG(salary), 2) AS avg_sal FROM employees;
-- 3、MAX():查询最高工资
SELECT MAX(salary) AS max_sal FROM employees;
-- 4、MIN():查询最低工资
SELECT MIN(salary) AS min_sal FROM employees;
-- 5、COUNT():查询有工资的员工个数
SELECT COUNT(salary) AS count_sal FROM employees;
运行结果:
| sum_sal | avg_sal | max_sal | min_sal | count_sal |
|---|---|---|---|---|
| 251400 | 8668.97 | 24000 | 2500 | 29 |
也可以在一条语句中同时查询多个统计值:
sql
-- 查询所有员工的工资总和、平均值、最高值、最低值以及总个数
SELECT
SUM(salary),
AVG(salary),
MAX(salary),
MIN(salary),
COUNT(salary)
FROM
employees;
运行结果:
| SUM(salary) | AVG(salary) | MAX(salary) | MIN(salary) | COUNT(salary) |
|---|---|---|---|---|
| 251400 | 8668.965517 | 24000 | 2500 | 29 |
2.2 各函数适用数据类型与NULL处理总结
不同聚合函数适用的数据类型不同,对 NULL 值的处理方式也不同,下面通过表格总结:
| 函数 | 适用数据类型 | 是否忽略NULL | 说明 |
|---|---|---|---|
SUM() |
数值型 | 是 | 不适合处理字符型和日期型 |
AVG() |
数值型 | 是 | 不适合处理字符型和日期型 |
MAX() |
数值型、字符型、日期型 | 是 | 适合处理任何类型 |
MIN() |
数值型、字符型、日期型 | 是 | 适合处理任何类型 |
COUNT() |
任何类型 | 是 | 适合处理任何类型,统计非NULL值的行数 |
核心结论:
SUM、AVG一般用于处理数值型,不建议用处理字符型和日期型。MAX、MIN、COUNT可以处理任何类型。
验证示例:
sql
-- ***************** 处理数值型 *****************
-- sum、avg不建议用处理字符型(结果无意义)
SELECT SUM(last_name), AVG(last_name) FROM employees;
-- sum、avg不建议用处理日期型(结果无意义)
SELECT SUM(hiredate), AVG(hiredate) FROM employees;
-- max、min适合处理任何类型(字符型按字典序比较)
SELECT MAX(last_name), MIN(last_name) FROM employees;
-- max、min适合处理任何类型(日期型按时间先后比较)
SELECT MAX(hiredate), MIN(hiredate) FROM employees;
-- count忽略null值,只统计非null的行
SELECT COUNT(commission_pct) FROM employees;
-- count适合处理任何类型
SELECT COUNT(last_name) FROM employees;
运行结果说明:
SUM(last_name)和AVG(last_name)虽然能执行,但结果是无意义的数字,不推荐使用。MAX(last_name)返回字典序最大的姓氏,MIN(last_name)返回字典序最小的姓氏,结果有意义。MAX(hiredate)返回最晚入职日期,MIN(hiredate)返回最早入职日期。COUNT(commission_pct)返回的是非NULL的奖金率行数(7),说明COUNT忽略了NULL。
2.3 聚合函数都忽略NULL值的验证
上面提到所有聚合函数都忽略 NULL 值,这里专门做一次验证。我们以 commission_pct(奖金率)字段为例,该字段有部分员工为 NULL。
sql
-- 验证sum、avg忽略null
-- employees表共29条记录,其中commission_pct不为null的有7条
-- SUM(commission_pct)/7 是实际平均值
-- SUM(commission_pct)/29 是错误平均值(除以了总行数)
SELECT
SUM(commission_pct),
AVG(commission_pct),
SUM(commission_pct) / 7,
SUM(commission_pct) / 29
FROM
employees;
运行结果:
| SUM(commission_pct) | AVG(commission_pct) | SUM/7 | SUM/29 |
|---|---|---|---|
| 2.00 | 0.285714 | 0.2857 | 0.0690 |
说明:
AVG(commission_pct)的值等于SUM(commission_pct) / 7,而不是SUM(commission_pct) / 29,证明AVG和SUM在计算时自动忽略了NULL值,分母用的是非NULL行数(7),而不是总行数(29)。
sql
-- 验证max、min忽略null
SELECT MAX(commission_pct), MIN(commission_pct) FROM employees;
运行结果:
| MAX(commission_pct) | MIN(commission_pct) |
|---|---|
| 0.40 | 0.20 |
MAX和MIN只在非NULL值中找最大最小,忽略NULL。
sql
-- 验证count忽略null
SELECT COUNT(commission_pct) FROM employees; -- 结果为7,而非29,说明忽略null
SELECT COUNT(last_name) FROM employees; -- last_name无null,结果为29
运行结果:
| COUNT(commission_pct) | COUNT(last_name) |
|---|---|
| 7 | 29 |
COUNT(commission_pct)返回 7 而非 29,证明COUNT(字段)会忽略NULL值。
2.4 搭配DISTINCT去重
聚合函数可以和 DISTINCT 搭配使用,实现去重后再统计。
sql
-- max、min搭配distinct去重
SELECT MAX(DISTINCT salary), MIN(salary) FROM employees;
-- sum搭配distinct去重,对比去重前后的总和
SELECT SUM(DISTINCT salary), SUM(salary) FROM employees;
运行结果:
| MAX(DISTINCT salary) | MIN(salary) |
|---|---|
| 24000 | 2500 |
| SUM(DISTINCT salary) | SUM(salary) |
|---|---|
| 196500 | 251400 |
说明:
SUM(DISTINCT salary)会先对salary去重再求和(23种不同工资),结果小于SUM(salary)(含重复工资)。
sql
-- count搭配distinct去重,统计有多少种不同的工资
SELECT COUNT(DISTINCT salary), COUNT(salary) FROM employees;
运行结果:
| COUNT(DISTINCT salary) | COUNT(salary) |
|---|---|
| 23 | 29 |
COUNT(DISTINCT salary)返回不同工资的种数(23),小于COUNT(salary)的 29。
2.5 COUNT(*) vs COUNT(字段) vs COUNT(1) 的区别
这是一个经典面试题,三者之间有细微但重要的区别:
sql
-- 三种count写法
SELECT COUNT(salary) FROM employees; -- 统计salary字段非null的行数
SELECT COUNT(*) FROM employees; -- 统计总行数(包含null)
SELECT COUNT(1) FROM employees; -- 统计总行数(包含null)
运行结果:
| COUNT(salary) | COUNT(*) | COUNT(1) |
|---|---|---|
| 29 | 29 | 29 |
本例中
salary字段没有NULL值,所以三者结果相同。如果字段有NULL,COUNT(字段)会小于COUNT(*)。
三者区别总结:
| 写法 | 含义 | 是否忽略NULL | 适用场景 |
|---|---|---|---|
COUNT(字段) |
统计该字段非 NULL 的行数 |
是 | 需要排除 NULL 值时使用 |
COUNT(*) |
统计表中总行数(包含 NULL) |
否 | 一般做统计时推荐使用 |
COUNT(1) |
统计表中总行数(包含 NULL) |
否 | 效果等同 COUNT(*) |
核心结论:
- 一般使用
COUNT(*)做统计函数,它统计的是总行数,不会忽略NULL。 COUNT(字段)会忽略该字段为NULL的行。COUNT(1)和COUNT(*)效果完全一致,在 MySQL 中COUNT(*)已经过优化,性能不低于COUNT(1),推荐使用COUNT(*)。- 和聚合函数一同查询的字段,要求是
GROUP BY关键字后面的字段,否则结果无意义(这一点在下一节分组查询中会详细说明)。
聚合函数本身只能对整个表或某个分组做统计。如果我们想"按部门分别统计平均工资",就需要把数据先分组,再对每一组做统计,这就是下一节"分组查询"要解决的问题。
三、分组查询(GROUP BY)
分组查询的作用是:将表中的数据按照某个或某些字段分成若干组,然后对每一组使用聚合函数进行统计。比如"查询每个部门的平均工资",就是先按 department_id 分组,再对每组求 AVG(salary)。
3.1 语法格式总结(含执行顺序标注)
分组查询的完整语法格式如下,括号中的数字标注了各子句的执行顺序:
sql
SELECT -- (5) 最后执行,选择要查询的列
分组函数,
列名(要求出现在GROUP BY后面)
FROM -- (1) 先确定数据来源表
表名
[WHERE 筛选条件] -- (2) 分组前筛选
GROUP BY -- (3) 执行分组
分组的列表
[HAVING 子句] -- (4) 分组后筛选
[ORDER BY 子句]; -- (6) 最后排序
执行顺序解读:
- FROM(1):首先确定从哪个表查询数据。
- WHERE(2):对原始表的数据进行分组前筛选,只保留满足条件的行。
- GROUP BY(3):按指定字段对筛选后的数据进行分组。
- HAVING(4):对分组后的结果集(虚表)进行筛选,只保留满足条件的分组。
- SELECT(5):选择要查询的列,包括分组函数和分组字段。
- ORDER BY(6):对最终结果进行排序。
注意:
SELECT在GROUP BY之后执行,所以SELECT中出现的非聚合字段必须出现在GROUP BY后面,否则结果无意义。
3.2 分组前筛选(WHERE)vs 分组后筛选(HAVING)
这是分组查询中最重要的一个知识点。筛选条件分为两类:
| 对比维度 | 分组前筛选 | 分组后筛选 |
|---|---|---|
| 筛选的数据源 | 原始表 | 分组后的结果集(虚表) |
| 子句位置 | GROUP BY 子句的前面 |
GROUP BY 子句的后面 |
| 关键字 | WHERE |
HAVING |
| 能否用聚合函数 | 不能(此时还没分组) | 能(此时已经分组完毕) |
选择原则:
- 分组函数做条件,肯定是放在
HAVING子句中。 因为分组函数必须等分组完成后才能计算,而WHERE在分组前执行,此时还没有分组结果,无法使用聚合函数。 - 能用分组前筛选(WHERE)的,优先考虑使用分组前筛选。 因为
WHERE先过滤数据再分组,可以减少参与分组的数据量,提高性能。
简单记忆:
WHERE过滤的是行(分组前),HAVING过滤的是组(分组后)。能用WHERE就别用HAVING。
另外,GROUP BY 子句还支持以下用法:
- 单字段分组:按一个字段分组。
- 多字段分组:多个字段之间使用逗号隔开,没有先后顺序(相当于多级分组)。
- 按表达式或函数分组 :如按
LENGTH(last_name)分组。 - 添加排序 :排序放在整个分组查询的最后(
ORDER BY)。
3.3 基础示例:按部门求平均工资、按领导求最高工资
示例1:查询邮箱中包含a字符的每个部门的平均工资
sql
/*
分析:
查询的表:employees
查询的字段:salary、department_id
查询的条件:email like '%a%'(分组前筛选,用WHERE)
分组的字段:department_id
排序的条件:无
*/
SELECT
AVG(salary) AS avg_sal,
department_id
FROM
employees
WHERE
email LIKE '%a%'
GROUP BY
department_id;
运行结果(部分):
| avg_sal | department_id |
|---|---|
| 5666.67 | 30 |
| 4900.00 | 50 |
| 6200.00 | 60 |
| 11500.00 | 80 |
| 17000.00 | 90 |
| 9000.00 | 100 |
分析:先用
WHERE email LIKE '%a%'从原始表中筛选出邮箱包含a的员工(MySQL 中LIKE默认不区分大小写),再按department_id分组,对每组求AVG(salary)。这里"邮箱包含a"是对原始行的筛选,所以用WHERE。
示例2:查询有奖金率的每个领导手下员工的最高工资
sql
/*
分析:
查询的表:employees
查询的字段:salary、manager_id
查询的条件:commission_pct is not null(分组前筛选,用WHERE)
分组的字段:manager_id
排序的条件:无
*/
SELECT
MAX(salary) AS max_sal,
manager_id
FROM
employees
WHERE
commission_pct IS NOT NULL
GROUP BY
manager_id;
运行结果:
| max_sal | manager_id |
|---|---|
| 14000 | 100 |
| 10000 | 145 |
分析:先用
WHERE commission_pct IS NOT NULL筛选出有奖金率的员工(7人),再按manager_id分组。manager_id=100 有 4 人(145,146,147,149),最高工资 14000;manager_id=145 有 3 人(150,151,152),最高工资 10000。
3.4 复杂示例:查询员工个数大于2的部门
这个示例同时用到了分组前筛选和分组后筛选,是理解 WHERE 和 HAVING 区别的经典案例。我们分步拆解:
第一步:查询每个部门的员工个数
sql
-- 分析(1):查询每个部门的员工个数
SELECT
COUNT(*),
department_id
FROM
employees
GROUP BY
department_id;
运行结果:
| COUNT(*) | department_id |
|---|---|
| 3 | 30 |
| 9 | 50 |
| 5 | 60 |
| 7 | 80 |
| 3 | 90 |
| 2 | 100 |
此时得到了每个部门的员工个数,但还没办法直接筛选"个数大于2"的部门,因为
COUNT(*)是分组后才计算出来的。
第二步:根据第一步的结果进行筛选,员工个数大于2
sql
-- 分析(2):根据(1)的查询结果进行筛选,员工个数大于2
SELECT
COUNT(*),
department_id
FROM
employees
GROUP BY
department_id
HAVING
COUNT(*) > 2;
运行结果:
| COUNT(*) | department_id |
|---|---|
| 3 | 30 |
| 9 | 50 |
| 5 | 60 |
| 7 | 80 |
| 3 | 90 |
关键点:
COUNT(*) > 2这个条件是对分组后的结果做筛选,因为COUNT(*)是聚合函数,必须放在HAVING中,不能用WHERE。部门 100 只有 2 人,被过滤掉了。
更多复杂筛选示例
示例:查询每个工种有奖金率的员工的最高工资大于12000的工种编号和最高工资
分步拆解:
sql
-- 分析(1):查询每个工种有奖金率的员工的最高工资
SELECT
MAX(salary) AS max_sal,
job_id
FROM
employees
WHERE
commission_pct IS NOT NULL -- 分组前筛选:有奖金率
GROUP BY
job_id;
sql
-- 分析(2):根据(1)的结果继续筛选,最高工资大于12000
SELECT
MAX(salary) AS max_sal,
job_id
FROM
employees
WHERE
commission_pct IS NOT NULL -- 分组前筛选:用WHERE
GROUP BY
job_id
HAVING
max_sal > 12000; -- 分组后筛选:用HAVING(因为max_sal是聚合结果)
运行结果:
| max_sal | job_id |
|---|---|
| 14000 | SA_MAN |
同时用到了
WHERE(分组前筛选有奖金率的员工)和HAVING(分组后筛选最高工资大于12000的工种),两者各司其职。SA_REP 工种最高工资 10000 不满足 >12000,被过滤。
示例:查询领导编号大于102的每个领导手下的最低工资大于5000的领导编号以及最低工资
分步拆解:
sql
-- 分析(1):查询每个领导手下的员工最低工资
SELECT
MIN(salary),
manager_id
FROM
employees
GROUP BY
manager_id;
sql
-- 分析(2):根据(1)的结果继续添加筛选条件:领导编号大于102
SELECT
MIN(salary),
manager_id
FROM
employees
WHERE
manager_id > 102 -- 分组前筛选:用WHERE
GROUP BY
manager_id;
sql
-- 分析(3):根据(2)的结果继续筛选,最低工资大于5000
SELECT
MIN(salary),
manager_id
FROM
employees
WHERE
manager_id > 102 -- 分组前筛选:用WHERE
GROUP BY
manager_id
HAVING
MIN(salary) > 5000; -- 分组后筛选:用HAVING
运行结果:
| MIN(salary) | manager_id |
|---|---|
| 9000 | 108 |
| 5800 | 120 |
| 9000 | 145 |
这个示例完美展示了
WHERE和HAVING的配合使用:manager_id > 102是对原始行的筛选用WHERE,MIN(salary) > 5000是对分组结果的筛选用HAVING。
3.5 按表达式/函数分组(按姓名长度分组)
GROUP BY 不仅支持按字段分组,还支持按表达式或函数的结果分组。
需求: 按员工姓名的长度分组,查询每一组的员工个数,筛选员工个数大于5的有哪些。
分步拆解:
sql
-- 分析(1):查询每个姓名长度的员工个数
SELECT
COUNT(*),
LENGTH(CONCAT(last_name, first_name)) AS len_name
FROM
employees
GROUP BY
LENGTH(CONCAT(last_name, first_name));
sql
-- 分析(2):添加筛选条件,员工个数大于5
SELECT
COUNT(*),
LENGTH(CONCAT(last_name, first_name)) AS len_name
FROM
employees
GROUP BY
LENGTH(CONCAT(last_name, first_name))
HAVING
COUNT(*) > 5;
运行结果:
| COUNT(*) | len_name |
|---|---|
| 6 | 12 |
| 8 | 13 |
分析:先用
CONCAT(last_name, first_name)把姓和名拼接,再用LENGTH()计算姓名长度,按这个长度值分组,统计每组人数,最后用HAVING筛选人数大于5的组。姓名长度为12的有6人,为13的有8人。
3.6 按多个字段分组(部门+工种)
GROUP BY 支持按多个字段分组,字段之间用逗号隔开,没有先后顺序之分。多字段分组相当于"多级分组",先按第一个字段分大组,再在大组内按第二个字段分小组。
需求: 查询每个部门每个工种的员工的平均工资
sql
SELECT
AVG(salary),
department_id,
job_id
FROM
employees
GROUP BY
department_id, job_id;
运行结果(部分):
| AVG(salary) | department_id | job_id |
|---|---|---|
| 11000 | 30 | PU_MAN |
| 3000 | 30 | PU_CLERK |
| 7650 | 50 | ST_MAN |
| 3500 | 50 | ST_CLERK |
| 5760 | 60 | IT_PROG |
| 12500 | 80 | SA_MAN |
| 9500 | 80 | SA_REP |
| 24000 | 90 | AD_PRES |
| 17000 | 90 | AD_VP |
| 12000 | 100 | FI_MGR |
| 9000 | 100 | FI_ACCOUNT |
| ... | ... | ... |
分析:先按
department_id分部门,再在每个部门内按job_id分工种,最后对每个"部门+工种"小组求平均工资。
3.7 分组查询+排序组合
排序(ORDER BY)放在整个分组查询的最后,对最终结果进行排序。
需求: 查询每个部门每个工种的平均工资,筛选平均工资大于10000的,并按平均工资降序显示。
sql
SELECT
AVG(salary),
department_id,
job_id
FROM
employees
WHERE
department_id IS NOT NULL -- 分组前筛选:排除没有部门的员工
GROUP BY
department_id, job_id -- 按部门+工种分组
HAVING
AVG(salary) > 10000 -- 分组后筛选:平均工资大于10000
ORDER BY
AVG(salary) DESC; -- 按平均工资降序排序
运行结果:
| AVG(salary) | department_id | job_id |
|---|---|---|
| 24000 | 90 | AD_PRES |
| 17000 | 90 | AD_VP |
| 12500 | 80 | SA_MAN |
| 12000 | 100 | FI_MGR |
这个示例完整地展示了分组查询的全部子句:
FROM→WHERE(分组前筛选)→GROUP BY(分组)→HAVING(分组后筛选)→SELECT(选择列)→ORDER BY(排序)。是分组查询的综合应用。
到目前为止,我们学习的所有查询都是单表查询。但在实际开发中,数据往往分散在多个表中,需要把多个表关联起来查询,这就是下一节"连接查询"的内容,也是本文的核心重点。
四、连接查询
4.1 连接查询概念与笛卡尔积现象
连接查询 ,又称为多表查询,当查询的字段来自多个表时,就会用到连接查询。比如查询"员工名和部门名",员工名在 employees 表,部门名在 departments 表,需要把两张表关联起来。
但是,如果直接这样写:
sql
-- 需求:查询所有女明星对应的男朋友(错误写法,会产生笛卡尔积)
SELECT `name`, boyName
FROM beauty, boys; -- 没有连接条件,产生笛卡尔积!
就会产生笛卡尔积现象。
什么是笛卡尔积?
笛卡尔积是两个或多个表之间的连接操作。当表1有 m 行,表2有 n 行,如果没有有效的连接条件,结果会变成 m × n 行。比如 beauty 表有 7 行,boys 表有 5 行,不加连接条件直接查,结果就是 7 × 5 = 35 行,其中大部分是无意义的组合。
笛卡尔积产生的条件:
- 省略连接条件。
- 连接条件无效。
- 所有表中的所有行互相连接。
避免笛卡尔积的方法:
在 WHERE 子句后面加入有效的连接条件:
sql
-- 正确写法:添加连接条件
SELECT `name`, boyName
FROM beauty, boys
WHERE beauty.boyfriend_id = boys.id;
运行结果:
| name | boyName |
|---|---|
| 迪丽热巴 | 鹿晗 |
| 赵丽颖 | 冯绍峰 |
| 杨幂 | 刘恺威 |
| Anglelababy | 黄晓明 |
| 景甜 | 张继科 |
此时只返回满足
beauty.boyfriend_id = boys.id的记录(5条),避免了无意义的笛卡尔积。刘亦菲和古力娜扎的 boyfriend_id 为 NULL,不匹配。
4.2 连接查询分类
连接查询可以从两个维度进行分类:
按年代分类:
| 分类 | 说明 |
|---|---|
| SQL92标准 | 老标准,已被淘汰,了解即可。连接条件写在 WHERE 后面 |
| SQL99标准 | 推荐使用。支持内连接 + 外连接(左外、右外)+ 交叉连接。连接条件写在 ON 后面 |
按功能分类:
(1)内连接
a、等值连接
b、非等值连接
c、自连接
(2)外连接
a、左外连接
b、右外连接
c、全外连接
(3)交叉连接
本文重点讲解 SQL99 标准(推荐使用),连接条件放在 ON 后面,筛选条件放在 WHERE 后面,分离性好,便于阅读。
4.3 SQL99语法格式总结
SQL99 标准的连接查询语法格式如下,括号中的数字标注了执行顺序:
sql
SELECT -- (7) 选择要查询的列
查询列表
FROM -- (1) 确定主表
表1 别名
[连接类型] JOIN -- (3) 关联从表
表2 别名
ON -- (2) 连接条件
连接条件
[WHERE 筛选条件] -- (4) 分组前筛选
[GROUP BY 子句] -- (5) 分组
[HAVING 筛选条件] -- (6) 分组后筛选
[ORDER BY 子句]; -- (8) 排序
连接类型关键字:
| 连接类型 | 关键字 |
|---|---|
| 内连接 | INNER(可省略) |
| 左外连接 | LEFT [OUTER] |
| 右外连接 | RIGHT [OUTER] |
| 全外连接 | FULL [OUTER](MySQL不支持) |
| 交叉连接 | CROSS |
4.4 内连接(INNER JOIN)
内连接查询的是两个表的交集,即只返回满足连接条件的记录。
语法格式:
sql
SELECT 查询列表
FROM 表1 别名
INNER JOIN 表2 别名
ON 连接条件;
内连接特点:
- 可以添加排序、分组、筛选。
INNER关键字可以省略(省略后默认就是内连接)。- 筛选条件放在
WHERE后面,连接条件放在ON后面,提高分离性,便于阅读。
内连接分为三种:等值连接 、非等值连接 、自连接。
4.4.1 等值连接(员工名+部门名)
等值连接是指连接条件使用 = 进行判断的连接。
示例1:查询员工名、部门名
sql
/*
分析:
查询的表:employees、departments
查询的字段:first_name、department_name
查询条件:无
*/
SELECT
e.first_name,
d.department_name
FROM employees AS e
INNER JOIN departments AS d
ON e.department_id = d.department_id;
运行结果(部分):
| first_name | department_name |
|---|---|
| Steven | Executive |
| Neena | Executive |
| Lex | Executive |
| Alexander | IT |
| Bruce | IT |
| ... | ... |
分析:连接条件是
e.department_id = d.department_id,用=判断两个表的部门编号是否相等,属于等值连接。使用表别名e和d简化书写。
示例2:查询名字中包含a的员工名和工种名(添加筛选条件)
sql
SELECT
first_name,
job_title
FROM employees e
INNER JOIN jobs j
ON e.job_id = j.job_id
WHERE
e.first_name LIKE '%a%';
运行结果(部分):
| first_name | job_title |
|---|---|
| Neena | Administration Vice President |
| Alexander | Programmer |
| David | Programmer |
| Valli | Programmer |
| ... | ... |
分析:连接条件
e.job_id = j.job_id放在ON后面,筛选条件e.first_name LIKE '%a%'放在WHERE后面,分离清晰。
4.4.2 非等值连接(工资级别匹配,BETWEEN AND)
非等值连接是指连接条件不使用 =,而是使用 BETWEEN AND、>、< 等区间判断的连接。
示例1:查询员工的工资级别
sql
/*
分析:
查询的表:employees、job_grades
查询思路:查询employees表中的salary字段在job_grades的最低工资和最高工资区间内,确定等级
*/
SELECT
salary,
grade_level
FROM
employees e
INNER JOIN job_grades g
ON e.salary BETWEEN g.lowest_sal AND g.highest_sal;
运行结果(部分):
| salary | grade_level |
|---|---|
| 24000 | A |
| 17000 | A |
| 9000 | B |
| 6000 | C |
| 4800 | D |
| 4200 | D |
| 14000 | A |
| 3500 | E |
| 2500 | E |
| ... | ... |
分析:连接条件是
e.salary BETWEEN g.lowest_sal AND g.highest_sal,用BETWEEN AND判断员工工资落在哪个工资级别区间,属于非等值连接。
示例2:查询工资级别的个数大于20的级别,并按工资级别降序(内连接+分组+筛选+排序)
sql
SELECT
COUNT(*),
grade_level
FROM
employees e
INNER JOIN job_grades g
ON e.salary BETWEEN g.lowest_sal AND g.highest_sal
GROUP BY
grade_level
HAVING
COUNT(*) > 20
ORDER BY
grade_level DESC;
运行结果:
(无结果。各等级人数:A=10, B=6, C=3, D=4, E=6,均不超过20。)
分析:先内连接确定每个员工的工资级别,再按
grade_level分组统计人数,用HAVING筛选人数大于20的级别。本数据集中各等级人数均不超过20,故返回空结果。读者可将条件改为COUNT(*) > 5来测试。
4.4.3 自连接(员工-上级关系)
自连接是指一张表自己和自己连接,通过给同一张表起不同的别名来区分。常用于树形结构或层级关系查询。
示例1:查询员工的名字、上级的名字
sql
SELECT
e.first_name AS 员工,
m.first_name AS 领导
FROM
employees e
INNER JOIN employees m
ON e.manager_id = m.employee_id;
运行结果(部分):
| 员工 | 领导 |
|---|---|
| Neena | Steven |
| Lex | Steven |
| Alexander | Lex |
| Bruce | Alexander |
| David | Alexander |
| Nancy | Neena |
| ... | ... |
分析:
employees表既当员工表(别名e)又当领导表(别名m),连接条件是e.manager_id = m.employee_id(员工的领导编号等于领导的员工编号)。注意 King 是最高领导没有上级(manager_id 为 NULL),所以不会出现在结果中(内连接只返回有匹配的记录)。共返回 28 条记录。
示例2:查询名字中包含字符a的员工名字和上级名字
sql
SELECT
e.first_name AS 员工,
m.first_name AS 领导
FROM
employees e
INNER JOIN employees m
ON e.manager_id = m.employee_id
WHERE
e.first_name LIKE '%a%';
运行结果(部分):
| 员工 | 领导 |
|---|---|
| Neena | Steven |
| Alexander | Lex |
| David | Alexander |
| Valli | Alexander |
| Den | Steven |
| Adam | Steven |
| ... | ... |
分析:在自连接的基础上增加
WHERE筛选,只查名字中包含a的员工。
4.4.4 多表内连接(三表JOIN)
内连接可以连续关联多张表,语法上就是连续使用多个 INNER JOIN ... ON。
示例:查询员工名、部门名、工种名,并按部门名降序
sql
/*
分析:
查询的表:employees、departments、jobs
查询的字段:first_name、department_name、job_title
连接条件:
e.department_id = d.department_id(员工-部门)
e.job_id = j.job_id(员工-工种)
排序:department_name DESC
*/
SELECT
first_name,
department_name,
job_title
FROM employees e
INNER JOIN departments d
ON e.department_id = d.department_id
INNER JOIN jobs j
ON e.job_id = j.job_id
ORDER BY
department_name DESC;
运行结果(部分):
| first_name | department_name | job_title |
|---|---|---|
| John | Sales | Sales Manager |
| Karen | Sales | Sales Manager |
| Steven | Shipping | Stock Manager |
| ... | ... | ... |
分析:三表内连接,先用
e.department_id = d.department_id关联员工和部门,再用e.job_id = j.job_id关联员工和工种,最后按部门名降序排序。
4.4.5 内连接+分组+筛选+排序综合示例
示例1:查询部门个数大于3的城市名和部门个数(内连接+分组+筛选)
分步拆解:
sql
-- 分析(1):查询每个城市的部门个数
SELECT
COUNT(*) AS num,
city
FROM departments d
INNER JOIN locations l
ON d.location_id = l.location_id
GROUP BY
city;
sql
-- 分析(2):根据(1)的结果筛选部门个数大于3的城市
SELECT
COUNT(*) AS num,
city
FROM departments d
INNER JOIN locations l
ON d.location_id = l.location_id
GROUP BY
city
HAVING
num > 3;
运行结果:
| num | city |
|---|---|
| 5 | Seattle |
Seattle(location_id=1700)有 5 个部门(10, 30, 90, 100, 110),是唯一满足个数大于 3 的城市。
示例2:查询哪个部门的员工个数大于3的部门名和员工个数,并按个数降序(内连接+分组+筛选+排序)
分步拆解:
sql
-- 分析(1):查询每个部门的员工个数
SELECT
COUNT(*) AS num,
department_name
FROM employees e
INNER JOIN departments d
ON e.department_id = d.department_id
GROUP BY
department_name;
sql
-- 分析(2):根据(1)的结果筛选员工个数大于3的并排序
SELECT
COUNT(*) AS num,
department_name
FROM employees e
INNER JOIN departments d
ON e.department_id = d.department_id
GROUP BY
department_name
HAVING
num > 3
ORDER BY
COUNT(*) DESC;
运行结果:
| num | department_name |
|---|---|
| 9 | Shipping |
| 7 | Sales |
| 5 | IT |
分析:先内连接员工表和部门表,按部门名分组统计人数,用
HAVING筛选人数大于3,最后按人数降序排序。Shipping(50)有9人,Sales(80)有7人,IT(60)有5人。
4.5 外连接
外连接用于查询一个表中有,但另一个表中没有的记录,以及两表匹配的记录。
应用场景与主表/从表概念
外连接的应用场景是:用于查询一个表中有、另一个表中没有的记录。
主表和从表:
- 左外连接(LEFT JOIN) :
LEFT JOIN左边是主表,右边是从表。 - 右外连接(RIGHT JOIN) :
RIGHT JOIN右边是主表,左边是从表。
外连接特点总结
外连接的核心特点:
-
外连接的查询结果为主表中的所有记录。 如果从表中有和它匹配的,则显示匹配的值;如果从表中没有和它匹配的,则显示
NULL。- 外连接查询结果 = 内连接结果 + 主表中有但从表中没有的记录
-
左外连接 ,
LEFT JOIN左边是主表,右边是从表;右外连接 ,RIGHT JOIN右边是主表,左边是从表。 -
左外和右外交换两个表的顺序,可以实现相同的结果。
-
全外连接 = 内连接结果 + 表1中有但表2中没有的 + 表2中有但表1中没有的。
维恩图概念理解(文字描述)
如果把表A和表B想象成两个相交的圆(维恩图):
- 左外连接(LEFT JOIN):左圆(A)的全部 ------ 包括A独有的部分 + A和B的交集。
- 右外连接(RIGHT JOIN):右圆(B)的全部 ------ 包括B独有的部分 + A和B的交集。
- 内连接(INNER JOIN):中间交集部分 ------ 只有A和B共有的部分。
- 全外连接(FULL JOIN):两个圆的并集 ------ A独有 + 交集 + B独有。
- LEFT JOIN + WHERE B IS NULL:左圆独有部分(A - B)------ A有但B没有的记录。
- RIGHT JOIN + WHERE A IS NULL:右圆独有部分(B - A)------ B有但A没有的记录。
用表格对比LEFT/RIGHT/FULL JOIN
| 连接类型 | 关键字 | 保留的记录 | 未匹配时的处理 |
|---|---|---|---|
| 左外连接 | LEFT JOIN 或 LEFT OUTER JOIN |
左表全部记录 | 右表字段填充 NULL |
| 右外连接 | RIGHT JOIN 或 RIGHT OUTER JOIN |
右表全部记录 | 左表字段填充 NULL |
| 全外连接 | FULL JOIN 或 FULL OUTER JOIN |
两表全部记录 | 缺失方字段填充 NULL |
| 对比维度 | LEFT JOIN | RIGHT JOIN | FULL JOIN |
|---|---|---|---|
| 保留表 | 左表全部 | 右表全部 | 两表全部 |
| 匹配失败填充 | 右表字段为 NULL |
左表字段为 NULL |
对应方字段为 NULL |
| 是否可互换 | 可以(调换表顺序) | 可以(调换表顺序) | 不可以(需 UNION 模拟) |
| MySQL原生支持 | 支持 | 支持 | 不支持 |
用表格对比各JOIN的结果集范围
| 查询 | 包含A独有 | 包含交集 | 包含B独有 | 结果集范围 |
|---|---|---|---|---|
LEFT JOIN |
是 | 是 | 否 | A全部 |
RIGHT JOIN |
否 | 是 | 是 | B全部 |
INNER JOIN |
否 | 是 | 否 | 交集 |
LEFT JOIN + WHERE B IS NULL |
是 | 否 | 否 | A独有(A-B) |
RIGHT JOIN + WHERE A IS NULL |
否 | 否 | 是 | B独有(B-A) |
FULL JOIN |
是 | 是 | 是 | 并集 |
MySQL不支持全外连接的说明
MySQL 原生不支持 FULL JOIN(全外连接),如果执行 SELECT * FROM A FULL JOIN B ON ... 会报语法错误。如果需要实现全外连接的效果,可以使用 LEFT JOIN 和 RIGHT JOIN 的结果通过 UNION 合并来模拟:
sql
-- MySQL模拟全外连接
SELECT * FROM A LEFT JOIN B ON A.id = B.id
UNION
SELECT * FROM A RIGHT JOIN B ON A.id = B.id;
外连接示例
引入示例:查询没有男朋友的女神(男朋友不在男神表中的女神名)
左外连接写法:
sql
-- 左外连接:beauty是主表,boys是从表
SELECT
b.name, bo.boyName
FROM
beauty b
LEFT JOIN boys bo
ON b.boyfriend_id = bo.id
WHERE
bo.id IS NULL;
运行结果:
| name | boyName |
|---|---|
| 刘亦菲 | NULL |
| 古力娜扎 | NULL |
分析:先用左外连接保留
beauty表的全部记录(包括没有匹配男朋友的女神),再用WHERE bo.id IS NULL筛选出"从表没有匹配"的记录,即没有男朋友的女神。刘亦菲和古力娜扎的 boyfriend_id 为 NULL,所以没有匹配。
示例:查询哪个部门没有员工
sql
-- 左外连接:部门表是主表,员工表是从表
SELECT
d.department_id,
d.department_name,
e.employee_id
FROM
departments d
LEFT JOIN employees e
ON d.department_id = e.department_id
WHERE
e.employee_id IS NULL;
运行结果:
| department_id | department_name | employee_id |
|---|---|---|
| 10 | Administration | NULL |
| 20 | Marketing | NULL |
| 40 | Human Resources | NULL |
| 110 | Accounting | NULL |
分析:先用左外连接保留
departments表的全部记录(包括没有员工的部门),再用WHERE e.employee_id IS NULL筛选出"员工表没有匹配"的记录,即没有员工的部门。部门 10、20、40、110 在 employees 表中没有对应记录。
4.6 交叉连接(CROSS JOIN)
交叉连接就是返回两个表的笛卡尔积,即表1有 m 行、表2有 n 行,结果为 m × n 行。实际开发中很少使用,但需要了解。
sql
-- 交叉连接(笛卡尔积)
SELECT
*
FROM
beauty b
CROSS JOIN boys bo;
运行结果:
(返回 7 × 5 = 35 条记录,即 beauty 表的每一行都与 boys 表的每一行组合。)
说明:
CROSS JOIN不需要ON连接条件,结果就是两表的笛卡尔积。在实际开发中应尽量避免无意义的交叉连接,除非确实需要生成所有组合。
总结
本文系统讲解了 MySQL 中四大核心查询知识,回顾如下:
-
流程控制函数:
IF(expr1, expr2, expr3):简单的二选一条件判断,条件为真返回expr2,为假返回expr3。CASE函数:支持两种语法格式。格式1适合等值匹配(CASE value WHEN ...),格式2适合区间判断(CASE WHEN condition THEN ...)。注意CASE WHEN从上到下匹配,更严格的条件要写在前面。
-
分组函数(聚合函数):
- 五大函数:
SUM、AVG、MAX、MIN、COUNT。 SUM、AVG适合数值型;MAX、MIN、COUNT适合任何类型。- 所有聚合函数都忽略
NULL值。 - 可搭配
DISTINCT去重。 COUNT(*)统计总行数(推荐),COUNT(字段)忽略NULL,COUNT(1)等效COUNT(*)。
- 五大函数:
-
分组查询(GROUP BY):
- 语法执行顺序:
FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY。 WHERE是分组前筛选(对原始行),HAVING是分组后筛选(对分组结果)。能用WHERE优先用WHERE。- 支持按单字段、多字段、表达式/函数分组,支持与排序组合。
- 语法执行顺序:
-
连接查询(核心重点):
- 笛卡尔积现象:多表查询不加有效连接条件会产生 m×n 的无意义结果。
- SQL99 标准:连接条件放
ON,筛选条件放WHERE,分离性好。 - 内连接 (
INNER JOIN):查交集,分为等值连接、非等值连接(BETWEEN AND)、自连接、多表连接。 - 外连接 :左外(
LEFT JOIN,左表全部)、右外(RIGHT JOIN,右表全部)。结果 = 内连接 + 主表有但从表没有的记录。MySQL 不支持全外连接(可用UNION模拟)。 - 交叉连接 (
CROSS JOIN):笛卡尔积。
其中,连接查询是本文的核心重点,也是实际开发中最常用的查询技术。特别是内连接的等值连接、非等值连接、自连接三种形式,以及左外连接和右外连接的区别与互换,需要重点掌握。建议读者结合本文示例,在自己的 MySQL 环境中反复练习,才能真正融会贯通。