JOIN
连接、子查询、集合操作、聚合函数及复杂查询,帮助掌握数据库操作的核心技能。UNION
、INTERSECT
、MINUS
) 及 MySQL 替代方案Subqueries
):IN
、EXISTS
、ANY
、ALL
的应用Multi-Table Queries
):INNER JOIN
、LEFT JOIN
、RIGHT JOIN
、CROSS JOIN
Aggregations
):结合 GROUP BY
和 HAVING
进行数据统计集合操作用于 合并 或 对比 不同 SELECT
语句的结果集。常见的集合操作有:
UNION
:合并两个 SELECT
查询的结果,并去重。UNION ALL
:合并两个 SELECT
查询的结果,但不去重。INTERSECT
(MySQL 不支持):返回两个 SELECT
查询的交集(共同存在的行)。MINUS
/ EXCEPT
(MySQL 不支持):返回仅在第一个 SELECT
查询中存在,但不在第二个查询中的行。注意:MySQL 不支持
INTERSECT
和MINUS
,但可以用JOIN
或NOT IN
来实现类似功能。
UNION
和 UNION ALL
UNION
语法SELECT column1, column2 FROM table1
UNION
SELECT column1, column2 FROM table2;
✅ 作用:
UNION
示例SELECT fName AS Name FROM Customers
UNION
SELECT fName FROM Employees;
✅ 查询结果
Name |
---|
Alice |
Bob |
Charlie |
David |
✅ 去除了重复的姓名(即使 Customers
和 Employees
表里可能有相同的 fName
)。
UNION ALL
语法SELECT column1, column2 FROM table1
UNION ALL
SELECT column1, column2 FROM table2;
✅ 作用:
UNION
更快)。UNION ALL
示例SELECT fName AS Name FROM Customers
UNION ALL
SELECT fName FROM Employees;
✅ 查询结果
Name |
---|
Alice |
Bob |
Bob |
Charlie |
David |
区别
UNION
去重 → 只会出现一个 Bob
。UNION ALL
不去重 → Bob
出现了两次。UNION
vs UNION ALL
对比操作 | 是否去重 | 性能 | 适用场景 |
---|---|---|---|
UNION |
✅ 去重 | ❌ 较慢(需去重计算) | 需要唯一值时,如去重后的客户名单 |
UNION ALL |
❌ 不去重 | ✅ 较快(仅合并结果) | 允许重复值,提高查询性能 |
INTERSECT
(交集)INTERSECT
概念INTERSECT
返回 两个 SELECT
查询共同存在的记录(即交集)。INTERSECT
,可以使用 INNER JOIN
或 IN
代替。INTERSECT
语法(适用于 SQL Server, PostgreSQL, Oracle)SELECT column1, column2 FROM table1
INTERSECT
SELECT column1, column2 FROM table2;
INTERSECT
MySQL 替代方案Customers
和 Employees
都有的 fName
SELECT fName FROM Customers
WHERE fName IN (SELECT fName FROM Employees);
✅ 查询结果
fName |
---|
Bob |
等价于 INTERSECT
Customers
和 Employees
都存在 的 fName
。IN
的性能不如 INNER JOIN
,大数据集时推荐 INNER JOIN
。INTERSECT
MySQL 替代方案:INNER JOIN
SELECT c.fName FROM Customers c
INNER JOIN Employees e ON c.fName = e.fName;
✅ 查询结果
fName |
---|
Bob |
INNER JOIN
更高效,避免 IN
的子查询开销。
MINUS
/ EXCEPT
(差集)MINUS
/ EXCEPT
概念MINUS
(Oracle) / EXCEPT
(SQL Server、PostgreSQL)返回 第一个 SELECT
查询有,而第二个 SELECT
没有的记录。MINUS
/ EXCEPT
,但可以用 NOT IN
或 LEFT JOIN
代替。MINUS
语法(适用于 Oracle)SELECT column1 FROM table1
MINUS
SELECT column1 FROM table2;
✅ 效果:
table1
中有,而 table2
没有 的数据。MINUS
MySQL 替代方案Customers
里有,而 Employees
里没有的 fName
SELECT fName FROM Customers
WHERE fName NOT IN (SELECT fName FROM Employees);
✅ 查询结果
fName |
---|
Alice |
Charlie |
等价于 MINUS
fName
。MINUS
MySQL 替代方案:LEFT JOIN
SELECT c.fName FROM Customers c
LEFT JOIN Employees e ON c.fName = e.fName
WHERE e.fName IS NULL;
✅ 查询结果
fName |
---|
Alice |
Charlie |
LEFT JOIN + IS NULL
更高效,避免 NOT IN
的子查询开销。
操作 | 作用 | MySQL 支持? | 替代方案 |
---|---|---|---|
UNION |
合并两个查询结果,去重 | ✅ 支持 | - |
UNION ALL |
合并两个查询结果,不去重 | ✅ 支持 | - |
INTERSECT |
取两个查询的交集 | ❌ 不支持 | INNER JOIN / IN |
MINUS / EXCEPT |
取 table1 有但 table2 没有的行 |
❌ 不支持 | NOT IN / LEFT JOIN |
子查询(Subquery)是一个嵌套在 SELECT
、INSERT
、UPDATE
或 DELETE
语句中的查询。它用于:
IN
、EXISTS
、ANY
、ALL
语法中。子查询必须返回一个或多个值,但不能在
WHERE
直接使用多列返回。
SELECT column1, column2
FROM table1
WHERE column3 OPERATOR (SELECT column4 FROM table2 WHERE condition);
说明:
OPERATOR
可以是=, >, <, >=, <=, !=, IN, EXISTS, ANY, ALL
等。
WHERE
中的子查询子查询用于 WHERE
语句,用于 条件匹配。
SELECT staffNo, fName, salary
FROM Staff
WHERE salary > (SELECT AVG(salary) FROM Staff);
解析:
AVG(salary)
。salary
高于该值的员工。HAVING
中的子查询用于 HAVING
过滤分组结果。
SELECT branchNo, COUNT(*) AS num_employees
FROM Staff
GROUP BY branchNo
HAVING COUNT(*) >= (SELECT AVG(num_employees) FROM (
SELECT COUNT(*) AS num_employees FROM Staff GROUP BY branchNo
) AS branch_counts);
解析:
IN
和 NOT IN
子查询IN
子查询用于查找 符合子查询返回的任意值 的行。
SELECT fName, lName
FROM Staff
WHERE branchNo IN (SELECT branchNo FROM Branch WHERE city = 'London');
解析:
branchNo
。branchNo
在这些值中的员工。NOT IN
子查询用于查找 不符合子查询返回值 的记录。
SELECT customerNo, fName
FROM Customers
WHERE customerNo NOT IN (SELECT customerNo FROM PropertyRentals);
解析:
customerNo
。customerNo
中的客户。EXISTS
和 NOT EXISTS
子查询EXISTS
EXISTS
返回 TRUE
,否则返回 FALSE
。SELECT customerNo, fName
FROM Customers c
WHERE EXISTS (SELECT * FROM PropertyRentals p WHERE c.customerNo = p.customerNo);
解析:
customerNo
是否存在于 PropertyRentals
。NOT EXISTS
NOT EXISTS
才返回 TRUE
。SELECT customerNo, fName
FROM Customers c
WHERE NOT EXISTS (SELECT * FROM PropertyRentals p WHERE c.customerNo = p.customerNo);
解析:
customerNo
是否存在 PropertyRentals
表。ANY
和 ALL
子查询ANY
TRUE
。>
,<
,>=
,<=
,!=
这些运算符。branchNo='B003'
任意一个员工的员工SELECT staffNo, fName, salary
FROM Staff
WHERE salary > ANY (SELECT salary FROM Staff WHERE branchNo = 'B003');
解析:
B003
的所有薪资。ALL
TRUE
。>
,<
,>=
,<=
,!=
这些运算符。branchNo='B003'
所有员工的员工SELECT staffNo, fName, salary
FROM Staff
WHERE salary > ALL (SELECT salary FROM Staff WHERE branchNo = 'B003');
解析:
B003
所有员工的薪资。B003
最高薪资的员工。相关子查询是指子查询依赖于主查询的值,即子查询在执行时,每一行都要重新计算一次。
SELECT staffNo, fName, salary, branchNo
FROM Staff s1
WHERE salary > (SELECT AVG(salary) FROM Staff s2 WHERE s1.branchNo = s2.branchNo);
解析:
s1
代表主查询中的员工。s2
在子查询中计算 s1.branchNo
对应的平均薪资。子查询类型 | 作用 | 示例 |
---|---|---|
IN |
匹配子查询返回的值 | WHERE branchNo IN (SELECT branchNo FROM Branch WHERE city='London') |
NOT IN |
过滤子查询返回的值 | WHERE customerNo NOT IN (SELECT customerNo FROM PropertyRentals) |
EXISTS |
检查子查询是否有数据 | WHERE EXISTS (SELECT * FROM PropertyRentals p WHERE c.customerNo = p.customerNo) |
NOT EXISTS |
检查子查询是否返回空 | WHERE NOT EXISTS (SELECT * FROM PropertyRentals p WHERE c.customerNo = p.customerNo) |
ANY |
比较子查询返回的某个值 | WHERE salary > ANY (SELECT salary FROM Staff WHERE branchNo='B003') |
ALL |
比较子查询返回的所有值 | WHERE salary > ALL (SELECT salary FROM Staff WHERE branchNo='B003') |
在 SQL 中,多表查询(Multi-Table Queries)是数据库操作的关键技能。JOIN
连接多个表,可以整合数据、实现复杂查询,并提高查询效率。本节将详细讲解 SQL 中的 JOIN
连接、笛卡尔积(Cartesian Product)、JOIN
结合聚合查询(Aggregations)等核心知识。
JOIN
连接JOIN
用于 将两个或多个表按关联字段连接起来,从而合并数据,主要类型有:
JOIN 类型 | 作用 |
---|---|
INNER JOIN |
只返回两张表都匹配的行 |
LEFT JOIN (或 LEFT OUTER JOIN ) |
返回左表所有行,右表没有匹配时返回 NULL |
RIGHT JOIN (或 RIGHT OUTER JOIN ) |
返回右表所有行,左表没有匹配时返回 NULL |
FULL JOIN (或 FULL OUTER JOIN ) |
返回左右表的所有匹配和不匹配数据(MySQL 不支持) |
CROSS JOIN |
生成笛卡尔积(所有可能组合) |
MySQL 不支持
FULL JOIN
,可以用LEFT JOIN
和RIGHT JOIN
结合UNION
实现。
INNER JOIN
(内连接) INNER JOIN
作用
NULL
。JOIN
类型,适用于数据完全匹配的查询。SELECT s.staffNo, s.fName, b.city
FROM Staff s
INNER JOIN Branch b ON s.branchNo = b.branchNo;
✅ 查询结果
staffNo | fName | city |
---|---|---|
SA1 | John | London |
SA2 | Alice | Glasgow |
SA3 | Bob | Bristol |
解析
ON s.branchNo = b.branchNo
确保 Staff
表的 branchNo
和 Branch
表的 branchNo
匹配。Staff
没有对应 Branch
,该员工不会出现在结果中!LEFT JOIN
(左连接) LEFT JOIN
作用
NULL
。SELECT b.branchNo, b.city, s.staffNo, s.fName
FROM Branch b
LEFT JOIN Staff s ON b.branchNo = s.branchNo;
✅ 查询结果
branchNo | city | staffNo | fName |
---|---|---|---|
B001 | London | SA1 | John |
B002 | Glasgow | SA2 | Alice |
B003 | Bristol | NULL | NULL |
解析
B003
没有员工,但仍然显示 NULL
,因为 LEFT JOIN
保留了 Branch
表的所有行。RIGHT JOIN
(右连接) RIGHT JOIN
作用
NULL
。SELECT s.staffNo, s.fName, b.city
FROM Staff s
RIGHT JOIN Branch b ON s.branchNo = b.branchNo;
✅ 查询结果
staffNo | fName | city |
---|---|---|
SA1 | John | London |
SA2 | Alice | Glasgow |
NULL | NULL | Bristol |
解析
B003
没有员工,但仍然在 Branch
表中,因此 staffNo
和 fName
为 NULL
。CROSS JOIN
(笛卡尔积) CROSS JOIN
作用
ON
条件,会返回两个表的所有可能组合(m × n
行)。SELECT s.staffNo, s.fName, b.branchNo, b.city
FROM Staff s
CROSS JOIN Branch b;
✅ 如果 Staff
有 3 行,Branch
有 3 行,则结果有 3 × 3 = 9
行。
避免使用 CROSS JOIN
,除非明确需要计算所有可能组合!
JOIN
结合 GROUP BY
和 HAVING
JOIN
结合 GROUP BY
和 HAVING
用于 统计数据、进行聚合分析。
SELECT b.branchNo, COUNT(s.staffNo) AS num_employees
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo;
✅ 查询结果
branchNo | num_employees |
---|---|
B001 | 2 |
B002 | 1 |
解析
GROUP BY b.branchNo
按 branchNo
分组。COUNT(s.staffNo)
统计每个分支的员工数。SELECT b.branchNo, COUNT(s.staffNo) AS num_employees
FROM Branch b
LEFT JOIN Staff s ON b.branchNo = s.branchNo
GROUP BY b.branchNo
HAVING COUNT(s.staffNo) = 0;
✅ 查询结果
branchNo | num_employees |
---|---|
B003 | 0 |
解析
LEFT JOIN
保留 Branch
表的所有记录。HAVING COUNT(s.staffNo) = 0
过滤出没有员工的分支。JOIN 类型 | 作用 | 是否支持 NULL |
---|---|---|
INNER JOIN |
只返回两表都匹配的行 | ❌ 不支持 |
LEFT JOIN |
左表所有行,右表匹配不到填 NULL |
✅ 支持 |
RIGHT JOIN |
右表所有行,左表匹配不到填 NULL |
✅ 支持 |
CROSS JOIN |
笛卡尔积(所有可能组合) | ❌ 不支持 |
JOIN
查询实战INNER JOIN
)。LEFT JOIN + GROUP BY
)。LEFT JOIN + HAVING
)。CROSS JOIN
)。JOIN
结合聚合查询(Aggregations)详细解析在 SQL 中,JOIN
结合聚合函数(Aggregation Functions),可以在多表查询的基础上进行统计、汇总、分组分析,从而实现更复杂的数据查询需求。
SQL 聚合函数用于计算一组值的统计信息,常见的聚合函数包括:
聚合函数 | 作用 |
---|---|
COUNT() |
计算行数 |
SUM() |
计算总和 |
AVG() |
计算平均值 |
MAX() |
计算最大值 |
MIN() |
计算最小值 |
JOIN
结合 GROUP BY
在 JOIN
多表查询的基础上,可以使用 GROUP BY
进行分组统计。
SELECT column1, AGGREGATE_FUNCTION(column2)
FROM table1
JOIN table2 ON table1.common_column = table2.common_column
GROUP BY column1;
SELECT b.branchNo, COUNT(s.staffNo) AS num_employees
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo;
✅ 查询结果
branchNo | num_employees |
---|---|
B001 | 2 |
B002 | 1 |
解析
GROUP BY b.branchNo
→ 按分支机构 branchNo
分组。COUNT(s.staffNo)
→ 统计每个分支的员工数。HAVING
过滤分组结果WHERE
不能用于聚合函数的筛选,因为 WHERE
在 GROUP BY
之前执行。
要过滤分组后的聚合结果,需要用 HAVING
。
SELECT b.branchNo, COUNT(s.staffNo) AS num_employees
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo
HAVING COUNT(s.staffNo) >= 2;
✅ 查询结果
branchNo | num_employees |
---|---|
B001 | 2 |
解析
HAVING COUNT(s.staffNo) >= 2
→ 只返回员工数量大于等于 2 的分支。JOIN
结合 SUM()
SELECT b.branchNo, SUM(s.salary) AS total_salary
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo;
✅ 查询结果
branchNo | total_salary |
---|---|
B001 | 60000 |
B002 | 45000 |
解析
SUM(s.salary)
→ 计算该分支所有员工的工资总和。JOIN
结合 AVG()
SELECT b.branchNo, AVG(s.salary) AS avg_salary
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo;
✅ 查询结果
branchNo | avg_salary |
---|---|
B001 | 30000 |
B002 | 45000 |
解析
AVG(s.salary)
→ 计算该分支所有员工的平均工资。JOIN
结合 MAX()
和 MIN()
SELECT b.branchNo, MAX(s.salary) AS max_salary, MIN(s.salary) AS min_salary
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo;
✅ 查询结果
branchNo | max_salary | min_salary |
---|---|---|
B001 | 50000 | 25000 |
B002 | 45000 | 45000 |
解析
MAX(s.salary)
→ 查询最高薪资。MIN(s.salary)
→ 查询最低薪资。LEFT JOIN
处理无匹配数据当某些分支机构没有员工时,INNER JOIN
会丢失这些分支。
可以用 LEFT JOIN
保留所有分支,并将 NULL
替换为 0
。
SELECT b.branchNo, COALESCE(COUNT(s.staffNo), 0) AS num_employees
FROM Branch b
LEFT JOIN Staff s ON b.branchNo = s.branchNo
GROUP BY b.branchNo;
✅ 查询结果
branchNo | num_employees |
---|---|
B001 | 2 |
B002 | 1 |
B003 | 0 |
解析
B003
没有员工,但 LEFT JOIN
仍然保留该分支,并返回 0
而不是 NULL
(COALESCE()
处理 NULL
)。JOIN
结合 ORDER BY
可以结合 ORDER BY
进行排序,如按员工总数降序排序。
SELECT b.branchNo, COUNT(s.staffNo) AS num_employees
FROM Staff s
JOIN Branch b ON s.branchNo = b.branchNo
GROUP BY b.branchNo
ORDER BY num_employees DESC;
✅ 查询结果
branchNo | num_employees |
---|---|
B001 | 3 |
B002 | 2 |
B003 | 1 |
解析
ORDER BY num_employees DESC
→ 按员工数量降序排列。查询类型 | SQL 语法 | 作用 |
---|---|---|
统计员工数量 | COUNT(s.staffNo) |
统计每个分支的员工数量 |
计算工资总额 | SUM(s.salary) |
计算每个分支的工资总额 |
计算平均工资 | AVG(s.salary) |
计算每个分支的平均工资 |
查询最高工资 | MAX(s.salary) |
找出每个分支的最高工资 |
查询最低工资 | MIN(s.salary) |
找出每个分支的最低工资 |
按分支分组 | GROUP BY b.branchNo |
按 branchNo 统计数据 |
过滤聚合结果 | HAVING COUNT(*) > 2 |
只显示符合条件的分支 |
保留所有分支 | LEFT JOIN |
确保无员工的分支也被显示 |
按员工数排序 | ORDER BY num_employees DESC |
让数据更易读 |
JOIN
、GROUP BY
、子查询等)在这一部分,我们将深入分析 SQL 练习题,重点是使用 JOIN
结合 GROUP BY
、HAVING
、子查询、LEFT JOIN
、ORDER BY
等 SQL 技巧来解决常见的数据查询问题。
查询 从未在 Orders
表中下过单的顾客,即 Customers
表中的 customerNo
在 Orders
表中不存在。
NOT IN
SELECT customerNo, fName
FROM Customers
WHERE customerNo NOT IN (SELECT customerNo FROM Orders);
解析
SELECT customerNo FROM Orders
获取所有下过单的 customerNo
。NOT IN
过滤掉这些客户,只保留从未下单的顾客。LEFT JOIN + IS NULL
(推荐,效率更高)SELECT c.customerNo, c.fName
FROM Customers c
LEFT JOIN Orders o ON c.customerNo = o.customerNo
WHERE o.customerNo IS NULL;
解析
LEFT JOIN
保留所有 Customers
数据,即使 Orders
里没有匹配项。WHERE o.customerNo IS NULL
过滤掉有订单的顾客,只保留未下单的顾客。✅ 推荐用 LEFT JOIN
,避免 NOT IN
在大数据集上的性能问题。
查找 每个部门薪资最高的员工,返回 departmentNo, fName, salary
。
IN
子查询SELECT departmentNo, fName, salary
FROM Employees
WHERE (departmentNo, salary) IN (
SELECT departmentNo, MAX(salary)
FROM Employees
GROUP BY departmentNo
);
解析
SELECT departmentNo, MAX(salary) FROM Employees GROUP BY departmentNo
获取每个部门的最高薪资。departmentNo
和 salary
符合该最大值的员工。JOIN
SELECT e.departmentNo, e.fName, e.salary
FROM Employees e
JOIN (
SELECT departmentNo, MAX(salary) AS max_salary
FROM Employees
GROUP BY departmentNo
) sub ON e.departmentNo = sub.departmentNo AND e.salary = sub.max_salary;
解析
sub
获取每个 departmentNo
的 MAX(salary)
。JOIN
结合 sub
和 Employees
,选出符合该最大薪资的员工。✅ 推荐用 JOIN
,在大数据集上比 IN
子查询更快。
假设 Weather
表有以下列:
CREATE TABLE Weather (
id INT PRIMARY KEY,
recordDate DATE,
temperature INT
);
recordDate, temperature
。JOIN
进行自关联SELECT w1.recordDate, w1.temperature
FROM Weather w1
JOIN Weather w2
ON DATEDIFF(w1.recordDate, w2.recordDate) = 1
WHERE w1.temperature > w2.temperature;
解析
JOIN
让 Weather
自连接,w1
和 w2
分别代表不同日期的记录。DATEDIFF(w1.recordDate, w2.recordDate) = 1
确保 w1
的日期比 w2
晚 1 天。w1.temperature > w2.temperature
过滤掉温度未上升的日期。✅ JOIN
方式是处理「比较当前行与上一行」的常见方法。
查找 拥有至少 2 套出租房产 的分支机构 branchNo
。
SELECT branchNo, COUNT(*) AS num_properties
FROM PropertyRentals
GROUP BY branchNo
HAVING COUNT(*) >= 2;
解析
GROUP BY branchNo
→ 按分支机构分组。COUNT(*)
→ 计算每个分支机构的房产数量。HAVING COUNT(*) >= 2
→ 过滤出拥有至少 2 套房产的分支。✅ 使用 HAVING
进行分组后筛选。
查找 不同职位(position
)的员工数量。
SELECT position, COUNT(*) AS num_employees
FROM Employees
GROUP BY position;
✅ 查询结果
position | num_employees |
---|---|
Manager | 3 |
Assistant | 5 |
Clerk | 2 |
解析
GROUP BY position
→ 按 position
进行分组。COUNT(*)
→ 统计每个职位的员工人数。✅ 常用于 HR 数据分析,如统计每个职位的招聘需求。
查询 Property
表中所有从未被出租的房产信息。
SELECT p.propertyNo, p.address
FROM Property p
LEFT JOIN PropertyRentals r ON p.propertyNo = r.propertyNo
WHERE r.propertyNo IS NULL;
解析
LEFT JOIN
保留所有 Property
数据。WHERE r.propertyNo IS NULL
→ 只保留没有匹配租赁记录的房产。✅ 适用于查找“没有关联记录”的情况。
查询目标 | SQL 解决方案 |
---|---|
找出从未下单的顾客 | LEFT JOIN + IS NULL 或 NOT IN |
查询部门最高薪资 | JOIN 结合 MAX(salary) |
查询温度上升的日期 | JOIN 让 Weather 自连接 |
查询至少 2 套房产的分支 | GROUP BY + HAVING COUNT(*) >= 2 |
统计不同职位的员工人数 | GROUP BY position + COUNT(*) |
查询从未出租的房产 | LEFT JOIN + IS NULL |