MySQL子查询、分页查询与联合查询详解(附示例与运行结果)
本文系统讲解 MySQL 查询进阶三大核心技能------子查询、分页查询与联合查询。先梳理子查询的分类与特点,再结合 HR 示例库数据,逐一演示
WHERE/HAVING后的标量、列、行子查询,SELECT后的相关子查询,FROM后的派生表及EXISTS用法;接着介绍LIMIT分页语法与分页公式,最后讲解UNION/UNION ALL合并结果集。全文示例均附运行结果,适合初学者对照练习,也是面试高频考点速查。
目录
-
-
[1.1 按出现位置分类](#1.1 按出现位置分类)
-
[1.2 按结果集行数分类](#1.2 按结果集行数分类)
-
[1.3 子查询特点总结](#1.3 子查询特点总结)
-
-
-
[2.1 标量子查询(一行一列)](#2.1 标量子查询(一行一列))
-
[2.2 列子查询(多行一列)](#2.2 列子查询(多行一列))
-
[2.3 行子查询(一行多列)](#2.3 行子查询(一行多列))
-
前言
在前两天的学习中,我们已经掌握了 SELECT、WHERE、GROUP BY、ORDER BY 以及多表连接(JOIN)等基础查询语法。但在实际开发中,我们经常遇到这样的需求:
-
"谁的工资比Lex高?"
-
"查询最低工资大于50号部门最低工资的部门?"
-
"查询每个部门的平均工资等级?"
这些需求都有一个共同特征:查询条件依赖于另一条查询的结果 。此时,就需要使用子查询。
子查询(Subquery) ,又称内查询(Inner Query) ,是指出现在其他SQL语句中的 SELECT 语句。包含子查询的语句则称为**主查询(Outer Query)**或外查询。
子查询可以嵌套在 SELECT、FROM、WHERE、HAVING 等多个位置,灵活强大,是SQL查询能力进阶的关键,也是面试中的高频考点。
本文将系统讲解以下内容:
- 子查询的分类与特点
- WHERE/HAVING 后面的子查询(标量子查询、列子查询、行子查询)
- SELECT 后面的子查询
- FROM 后面的子查询
- EXISTS 相关子查询
- 分页查询(LIMIT)
- 联合查询(UNION)
下面我们逐一展开。
数据准备
本文所有示例基于以下四张表,数据来自经典 HR 示例库的精简版本(employees 共 29 行)。完整建表与数据脚本见配套文件 00_数据准备_建表与数据.sql,在 Navicat 中全选运行即可。
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.00 | NULL | NULL | 90 | 1987-06-17 |
| 101 | Neena | Kochhar | AD_VP | 17000.00 | NULL | 100 | 90 | 1989-09-21 |
| 102 | Lex | De Haan | AD_VP | 17000.00 | NULL | 100 | 90 | 1993-01-13 |
| 103 | Alexander | Hunold | IT_PROG | 9000.00 | NULL | 102 | 60 | 1990-01-03 |
| 104 | Bruce | Ernst | IT_PROG | 6000.00 | NULL | 103 | 60 | 1991-05-21 |
| 105 | David | Austin | IT_PROG | 4800.00 | NULL | 103 | 60 | 1997-06-25 |
| 106 | Valli | Pataballa | IT_PROG | 4800.00 | NULL | 103 | 60 | 1998-02-05 |
| 107 | Diana | Lorentz | IT_PROG | 4200.00 | NULL | 103 | 60 | 1999-02-07 |
| 108 | Nancy | Greenberg | FI_MGR | 12000.00 | NULL | 101 | 100 | 1994-08-17 |
| 109 | Daniel | Faviet | FI_ACCOUNT | 9000.00 | NULL | 108 | 100 | 1994-08-16 |
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 |
另有
locations(位置表)用于部门与城市的关联,完整字段与数据见配套 SQL 文件。后文示例中每个查询块后均给出基于上述数据的预测运行结果。
一、子查询分类概述
在动手写示例之前,先从两个维度对子查询做一个整体梳理,便于后续理解每种写法的适用场景。
1.1 按出现位置分类
子查询可以出现在SQL语句的不同位置,每个位置支持的结果集类型也各不相同:
| 出现位置 | 支持的子查询类型 | 常用操作符 |
|---|---|---|
SELECT 后面 |
仅支持标量子查询 | =、>、< 等 |
WHERE 或 HAVING 后面 |
标量子查询、列子查询、行子查询 | 单行:> < >= <= = != 多行:IN、ANY/SOME、ALL |
EXISTS 后面 |
表子查询(相关子查询) | EXISTS、NOT EXISTS |
FROM 后面 |
表子查询(结果充当一张表) | 必须取别名 |
1.2 按结果集行数分类
根据子查询返回结果集的形状,可以划分为四类:
| 类型 | 结果集形状 | 通俗理解 |
|---|---|---|
| 标量子查询 | 一行一列(一个值) | 一个数 |
| 列子查询 | 一列多行 | 一列数据 |
| 行子查询 | 一行多列 | 一行数据 |
| 表子查询 | 多行多列 | 一张表 |
1.3 子查询特点总结
不论子查询出现在哪个位置,都有以下通用特点:
- 子查询必须放在小括号
()内,这是语法强制要求; - 子查询一般放在条件的右侧,便于阅读;
- 子查询的执行优先于主查询,主查询的条件会用到子查询的结果(一张"虚表");
- 标量子查询一般搭配单行操作符 (
><>=<==!=<><=>)使用; - 列子查询一般搭配多行操作符 (
IN、ANY/SOME、ALL)使用。
理解了以上骨架,后面的示例就是把这些"零件"组装起来。
二、WHERE或HAVING后面的子查询
这是最常见、最常用的子查询场景。根据子查询返回结果的不同,又分为三种:标量子查询、列子查询、行子查询。
2.1 标量子查询(一行一列)
标量子查询返回一个值,所以搭配单行操作符使用。
示例1:谁的工资比Lex高
需求分析:要找出工资高于Lex的员工,必须先知道Lex的工资,再以该工资为条件筛选员工。
分步拆解:
第一步,查询Lex的工资:
sql
-- (1) 查询Lex的工资
SELECT salary
FROM employees
WHERE first_name = 'Lex';
运行结果:
| salary |
|---|
| 17000.00 |
第二步,把上面查出来的 17000 当作条件,查询工资大于该值的员工:
sql
-- (2) 查询员工信息,要求 salary 大于(1)的结果
SELECT *
FROM employees
WHERE salary > ( SELECT salary FROM employees WHERE first_name = 'Lex' );
运行结果:
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 100 | Steven | King | SKING | AD_PRES | 24000.00 | 90 |
可以看到,只有King(CEO)的工资24000高于Lex的17000。
小结 :这就是子查询最经典的"两步走"思路------先写子查询拿到那个值,再套到主查询里。
示例2:job_id与141号员工相同且salary比143号多的员工
需求 :返回 job_id 与141号员工相同,且 salary 比143号员工多的员工名、job_id 和工资。
分步拆解:
第一步,查询141号员工的 job_id:
sql
-- (1) 查询141号员工的 job_id
SELECT job_id
FROM employees
WHERE employee_id = 141;
运行结果:
| job_id |
|---|
| ST_CLERK |
第二步,查询143号员工的工资:
sql
-- (2) 查询143号员工的 salary
SELECT salary
FROM employees
WHERE employee_id = 143;
运行结果:
| salary |
|---|
| 2600.00 |
第三步,把两个子查询分别嵌入到主查询的条件中:
sql
-- 查询员工名、job_id 和工资,要求:job_id = (1) 并且 salary > (2)
SELECT
first_name,
job_id,
salary
FROM employees
WHERE job_id = ( SELECT job_id FROM employees WHERE employee_id = 141 )
AND salary > ( SELECT salary FROM employees WHERE employee_id = 143 );
运行结果:
| first_name | job_id | salary |
|---|---|---|
| Kevin | ST_CLERK | 5800.00 |
| Trenna | ST_CLERK | 3500.00 |
| Curtis | ST_CLERK | 3100.00 |
小结 :一条 WHERE 中可以同时使用多个标量子查询,每个子查询独立执行并返回单值,分别参与条件判断。
示例3:公司最低工资的员工
需求 :返回公司工资最少的员工的 last_name、job_id、salary。
分步拆解:
第一步,查询公司最低工资:
sql
-- (1) 查询公司最低工资
SELECT MIN(salary)
FROM employees;
运行结果:
| MIN(salary) |
|---|
| 2500.00 |
第二步,套到主查询:
sql
-- (2) 查询员工的 last_name、job_id、salary,要求 salary = (1) 的结果
SELECT
last_name,
job_id,
salary
FROM employees
WHERE salary = ( SELECT MIN(salary) FROM employees );
运行结果:
| last_name | job_id | salary |
|---|---|---|
| Vargas | ST_CLERK | 2500.00 |
小结 :聚合函数 MIN/MAX/SUM/AVG 经常与子查询配合使用,因为它们天然返回单值,正好是标量子查询。
示例4:最低工资大于50号部门最低工资的部门(HAVING中使用子查询)
需求:查询最低工资大于50号部门最低工资的部门id和其最低工资。
分步拆解:
第一步,查询50号部门的最低工资:
sql
-- (1) 查询50号部门最低工资
SELECT MIN(salary)
FROM employees
WHERE department_id = 50;
运行结果:
| MIN(salary) |
|---|
| 2500.00 |
第二步,查询每个部门的最低工资:
sql
-- (2) 查询每个部门的最低工资
SELECT
MIN(salary),
department_id
FROM employees
GROUP BY department_id;
运行结果:
| MIN(salary) | department_id |
|---|---|
| 2900.00 | 30 |
| 2500.00 | 50 |
| 4200.00 | 60 |
| 9000.00 | 80 |
| 17000.00 | 90 |
| 9000.00 | 100 |
第三步,在第二步基础上用 HAVING 过滤,条件中嵌入第一步的子查询:
sql
-- (3) 在(2)基础上,满足 MIN(salary) > (1) 的结果
SELECT
MIN(salary),
department_id
FROM employees
GROUP BY department_id
HAVING MIN(salary) > ( SELECT MIN(salary) FROM employees WHERE department_id = 50 );
运行结果:
| MIN(salary) | department_id |
|---|---|
| 2900.00 | 30 |
| 4200.00 | 60 |
| 9000.00 | 80 |
| 17000.00 | 90 |
| 9000.00 | 100 |
小结 :HAVING 是对分组结果再筛选,子查询同样可以用在 HAVING 后面,思路与 WHERE 完全一致。
⚠️ 非法使用标量子查询的陷阱
标量子查询必须返回一行一列。如果子查询返回了多行多列,再用单行操作符比较,就会报错。
错误示例:
sql
-- 非法使用:子查询返回多行结果,再与单值比较 → 报错!
SELECT
MIN(salary),
department_id
FROM employees
GROUP BY department_id
HAVING MIN(salary) > ( SELECT salary FROM employees WHERE department_id = 50 );
为什么错 :SELECT salary FROM employees WHERE department_id = 50 返回50号部门所有人的工资(多行),而 MIN(salary) > ? 期望的是一个确定的值,用多行结果去比较一个单值,MySQL会直接抛出错误:
ERROR 1242: Subquery returns more than 1 row
记住 :用单行操作符(> < = 等)比较,子查询必须只返回一行一列。若子查询返回多行,必须改用多行操作符(IN、ANY、ALL)。
2.2 列子查询(多行一列)
当子查询返回一列多行(一组值)时,就需要使用多行操作符。
| 操作符 | 含义 |
|---|---|
IN / NOT IN |
等于列表中的任意一个 |
ANY / SOME |
和子查询返回的某一个值进行比较 |
ALL |
和子查询返回的所有值进行比较 |
ANY和SOME完全等价,只是不同写法。
示例1:location_id为1400或1700的部门中的员工(IN)
需求 :返回 location_id 是 1400 或 1700 的部门中的所有员工名。
分步拆解:
第一步,查询 location_id 是1400或1700的部门编号:
sql
-- (1) 查询 location_id 是 1400 或 1700 的部门编号
SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700);
运行结果:
| department_id |
|---|
| 10 |
| 30 |
| 60 |
| 90 |
| 100 |
| 110 |
第二步,查询员工名,要求部门号是上述结果中的某一个:
sql
-- (2) 查询员工名,要求部门号是(1)结果中的某一个
SELECT first_name
FROM employees
WHERE department_id IN (
SELECT DISTINCT department_id FROM departments WHERE location_id IN (1400, 1700)
);
运行结果:
| first_name |
|---|
| Steven |
| Neena |
| Lex |
| Alexander |
| Bruce |
| David |
| Valli |
| Diana |
| ... |
上述部门中 10、110 号无员工,实际命中 30、60、90、100 四个部门,共 13 名员工,因超过 8 行仅展示前 8 行。
小结 :IN 子查询的本质是------主查询的字段值只要等于子查询返回列表中的任意一个即可。
示例2:比IT_PROG工种任一工资低的员工(ANY)
需求 :返回其他工种中比 job_id 为 IT_PROG 工种任一工资低 的员工的工号、名、job_id 以及 salary。
分步拆解:
第一步,查询 job_id 为 IT_PROG 的所有工资:
sql
-- (1) 查询 job_id 为 IT_PROG 的工资
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG';
运行结果:
| salary |
|---|
| 4200.00 |
| 4800.00 |
| 6000.00 |
| 9000.00 |
第二步,查询工资比上述任意一个工资都低的员工:
sql
-- (2) 查询员工的工号、名、job_id 和 salary,要求 salary < (1) 任意一个
SELECT
employee_id,
first_name,
job_id,
salary
FROM employees
WHERE salary < ANY ( SELECT DISTINCT salary FROM employees WHERE job_id = 'IT_PROG' );
运行结果:
| employee_id | first_name | job_id | salary |
|---|---|---|---|
| 104 | Bruce | IT_PROG | 6000.00 |
| 105 | David | IT_PROG | 4800.00 |
| 106 | Valli | IT_PROG | 4800.00 |
| 107 | Diana | IT_PROG | 4200.00 |
| 115 | Alexander | PU_CLERK | 3100.00 |
| 116 | Shelli | PU_CLERK | 2900.00 |
| 120 | Matthew | ST_MAN | 8000.00 |
| 121 | Adam | ST_MAN | 8200.00 |
| ... |
< ANY等价于小于子查询中的最大值 9000,共 15 行满足(含 IT_PROG 自身员工,因 SQL 未排除该工种),超过 8 行仅展示前 8 行。
关键理解 :< ANY(...) 的意思是"小于子查询结果中的任意一个",即小于最大的那个值。本例中IT_PROG最高工资是9000,所以工资小于9000的所有员工都会被选出。
等价写法(更直观):
sql
-- 等价写法:salary < IT_PROG工种的最高工资
SELECT
employee_id,
first_name,
job_id,
salary
FROM employees
WHERE salary < ( SELECT MAX(salary) FROM employees WHERE job_id = 'IT_PROG' );
运行结果: 与上方 < ANY 写法完全一致(共 15 行),不再重复展示。
示例3:比IT_PROG工种所有工资低的员工(ALL)
需求 :返回其他工种中比 job_id 为 IT_PROG 工种所有工资都低的员工。
分步拆解:
第一步,同上,查询 IT_PROG 工种的所有工资。
第二步,查询工资比上述所有工资都低的员工:
sql
-- (2) 查询员工的工号、名、job_id 和 salary,要求 salary < (1) 的所有
SELECT
employee_id,
first_name,
job_id,
salary
FROM employees
WHERE salary < ALL ( SELECT DISTINCT salary FROM employees WHERE job_id = 'IT_PROG' );
运行结果:
| employee_id | first_name | job_id | salary |
|---|---|---|---|
| 115 | Alexander | PU_CLERK | 3100.00 |
| 116 | Shelli | PU_CLERK | 2900.00 |
| 141 | Trenna | ST_CLERK | 3500.00 |
| 142 | Curtis | ST_CLERK | 3100.00 |
| 143 | Randall | ST_CLERK | 2600.00 |
| 144 | Peter | ST_CLERK | 2500.00 |
关键理解 :< ALL(...) 的意思是"小于子查询结果中的所有值",即小于最小的那个值。本例中IT_PROG最低工资是4200,所以只有工资小于4200的员工会被选出。
等价写法(更直观):
sql
-- 等价写法:salary < IT_PROG工种的最低工资
SELECT
employee_id,
first_name,
job_id,
salary
FROM employees
WHERE salary < ( SELECT MIN(salary) FROM employees WHERE job_id = 'IT_PROG' );
运行结果: 与上方 < ALL 写法完全一致(共 6 行),不再重复展示。
ANY与ALL对比总结
| 操作符 | < 时的含义 |
> 时的含义 |
等价写法 |
|---|---|---|---|
ANY / SOME |
小于最大值(< max) | 大于最小值(> min) | < MAX(...) / > MIN(...) |
ALL |
小于最小值(< min) | 大于最大值(> max) | < MIN(...) / > MAX(...) |
记忆口诀:ANY比一个,ALL比全部;ANY看极值对侧,ALL看极值同侧。
2.3 行子查询(一行多列)
行子查询返回一行多列的结果,可以一次性与主查询的多列组合比较。
示例:员工编号最小且工资最高的员工
需求:查询员工编号最小并且工资最高的员工信息。
分步拆解:
第一步,查询最小的员工编号:
sql
-- (1) 查询最小的员工编号
SELECT MIN(employee_id)
FROM employees;
运行结果:
| MIN(employee_id) |
|---|
| 100 |
第二步,查询最高工资:
sql
-- (2) 查询最高工资
SELECT MAX(salary)
FROM employees;
运行结果:
| MAX(salary) |
|---|
| 24000.00 |
第三步,查询员工信息,要求 employee_id 等于最小值且 salary 等于最大值:
sql
-- (3) 查询员工信息
SELECT *
FROM employees
WHERE employee_id = ( SELECT MIN(employee_id) FROM employees )
AND salary = ( SELECT MAX(salary) FROM employees );
运行结果:
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 100 | Steven | King | SKING | AD_PRES | 24000.00 | 90 |
小结:示例中员工编号最小的是100,工资最高的也是100号King,所以结果只有一条。实际开发中若两个条件不指向同一行,结果可能为空。
进阶写法:行子查询可以用
(col1, col2) = (SELECT ...)这种成对比较的形式,但本例拆成两个标量子查询更直观。
三、SELECT后面的子查询
SELECT 后面的子查询仅支持标量子查询 ,因为 SELECT 列表中每一列对应一行结果,必须是一个确定的值。
示例1:查询每个部门的员工个数(相关子查询)
需求:查询每个部门的基本信息,并附带该部门的员工个数。
sql
-- 查询每个部门的员工个数(相关子查询)
SELECT
d.*,
( SELECT COUNT(*) FROM employees e
WHERE e.department_id = d.department_id ) AS 个数
FROM departments d;
运行结果:
| department_id | department_name | manager_id | location_id | 个数 |
|---|---|---|---|---|
| 10 | Administration | 200 | 1700 | 0 |
| 20 | Marketing | 201 | 1800 | 0 |
| 30 | Purchasing | 114 | 1700 | 3 |
| 40 | Human Resources | 203 | 2400 | 0 |
| 50 | Shipping | 121 | 1500 | 9 |
| 60 | IT | 103 | 1400 | 5 |
| 80 | Sales | 145 | 2500 | 7 |
| 90 | Executive | 100 | 1700 | 3 |
| ... | ... | ... | ... | ... |
共 10 个部门,超过 8 行仅展示前 8 行;无员工的部门"个数"列为 0。
关键概念------相关子查询 :子查询中引用了主查询的表 d.department_id,这种"子查询依赖主查询字段"的写法叫相关子查询。主查询每扫一行,子查询都会被重新执行一次。
示例2:查询员工工号等于102的部门名
需求 :通过子查询的方式,直接在 SELECT 中输出102号员工所在部门名。
sql
-- 查询员工工号等于102的部门名
SELECT
(
SELECT department_name
FROM departments d
INNER JOIN employees e ON d.department_id = e.department_id
WHERE e.employee_id = 102
) AS 部门名;
运行结果:
| 部门名 |
|---|
| Executive |
小结:102号员工Lex属于Executive部门。这种把子查询直接作为一列输出的写法,本质是"把一个查询结果当作字段使用",必须保证子查询只返回一行一列。
四、FROM后面的子查询
FROM 后面可以跟一个子查询,把子查询的结果集当作一张"虚拟表"来使用。注意:必须给这张虚拟表取别名,否则会报错。
示例:查询每个部门的平均工资等级
需求 :查询每个部门的平均工资,并对应到 job_grades 表中的工资等级。
分步拆解:
第一步,查询每个部门的平均工资:
sql
-- (1) 查询每个部门的平均工资
SELECT
ROUND(AVG(salary)),
department_id
FROM employees
GROUP BY department_id;
运行结果:
| ROUND(AVG(salary)) | department_id |
|---|---|
| 5667 | 30 |
| 5344 | 50 |
| 5760 | 60 |
| 11214 | 80 |
| 19333 | 90 |
| 10500 | 100 |
第二步,把上述结果当作一张表,连接 job_grades 表,按平均工资落在 lowest_sal ~ highest_sal 区间匹配等级:
sql
-- (2) 连接(1)结果和 job_grades 表,筛选条件:平均工资 BETWEEN lowest_sal AND highest_sal
SELECT
*
FROM
( SELECT ROUND( AVG(salary) ) AS ag, department_id
FROM employees
GROUP BY department_id
) AS ag_dep -- 子查询结果必须取别名,这里叫 ag_dep
INNER JOIN job_grades g
ON ag_dep.ag BETWEEN g.lowest_sal AND g.highest_sal;
运行结果:
| ag | department_id | grade_level | lowest_sal | highest_sal |
|---|---|---|---|---|
| 5667 | 30 | D | 4000 | 5999 |
| 5344 | 50 | D | 4000 | 5999 |
| 5760 | 60 | D | 4000 | 5999 |
| 11214 | 80 | A | 10000 | 99999 |
| 19333 | 90 | A | 10000 | 99999 |
| 10500 | 100 | A | 10000 | 99999 |
小结 :FROM 后面的子查询相当于"用一张查询结果充当临时表",配合 JOIN 使用能够解决"先聚合再关联"这类复杂场景。别名是必须的------MySQL把子查询结果当作派生表(Derived Table),派生表必须有一个名字。
五、EXISTS相关子查询
语法说明
sql
EXISTS ( 完整的SQL语句 )
-
EXISTS后面跟一个完整的子查询; -
返回结果是布尔值:
1(真,子查询有结果)或0(假,子查询无结果); -
EXISTS不关心子查询返回的字段和行数,只关心是否有数据返回。
先看一个最简单的例子:
sql
-- 测试 EXISTS 返回 1 或 0
SELECT EXISTS( SELECT employee_id FROM employees WHERE salary = 80000 );
运行结果:
| EXISTS(...) |
|---|
| 0 |
工资80000的员工不存在,所以返回0。
示例:查询有员工的部门名(EXISTS vs IN 两种写法对比)
需求:查询有员工的部门名。
写法一:EXISTS(相关子查询)
sql
-- 用 EXISTS 查询有员工的部门名
SELECT department_name
FROM departments d
WHERE EXISTS (
SELECT * FROM employees e
WHERE d.department_id = e.department_id
);
运行结果: 见下方 IN 写法结果(两种写法一致)。
写法二:IN(子查询)
sql
-- 用 IN 查询有员工的部门名
SELECT department_name
FROM departments d
WHERE d.department_id IN ( SELECT department_id FROM employees );
运行结果:(两种写法结果一致)
| department_name |
|---|
| Purchasing |
| Shipping |
| IT |
| Sales |
| Executive |
| Finance |
共 6 个部门有员工,分别为 30、50、60、80、90、100 号部门。
两种写法对比:
| 对比项 | EXISTS | IN |
|---|---|---|
| 执行逻辑 | 主查询每行触发一次子查询(相关子查询) | 先执行子查询,再用于主查询(非相关子查询) |
| 性能倾向 | 子查询结果集大、主查询表小时更快 | 子查询结果集小、主查询表大时更快 |
| 使用习惯 | 用于判断"是否存在" | 用于判断"是否在集合中" |
实际开发中,MySQL优化器会自动改写
IN子查询为半连接(semijoin),二者性能差异已大幅缩小,按业务可读性选择即可。
六、分页查询(LIMIT)
应用场景说明
当一张表的数据量很大,前端页面不可能一次显示全部,就需要"翻页"------每页显示固定条数。此时就要用到 LIMIT 分页查询。
语法格式总结
完整的 SELECT 语法(MySQL官方文档):
sql
SELECT
[ALL | DISTINCT | DISTINCTROW]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr] ...
[into_option]
[FROM table_references
[PARTITION partition_list]]
[WHERE where_condition]
[GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]]
[HAVING where_condition]
[WINDOW window_name AS (window_spec)]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
其中,子句的执行顺序如下(这是面试常考点):
| 步骤 | 子句 | 说明 |
|---|---|---|
| 1 | FROM |
确定数据源 |
| 2 | JOIN |
多表连接 |
| 3 | ON |
连接条件 |
| 4 | WHERE |
行级筛选 |
| 5 | GROUP BY |
分组 |
| 6 | HAVING |
分组后筛选 |
| 7 | SELECT |
选取列、计算 |
| 8 | ORDER BY |
排序 |
| 9 | LIMIT |
分页截取 |
简化后的分页语法:
sql
SELECT 查询列表 -- (7)
FROM 表名 -- (1)
[
JOIN 类型 JOIN 表2 -- (2)
ON 连接条件 -- (3)
WHERE 筛选条件 -- (4)
GROUP BY 分组字段 -- (5)
HAVING 分组后筛选 -- (6)
ORDER BY 排序字段 -- (8)
]
LIMIT [offset,] size; -- (9)
offset和size参数说明
-
offset:要显示条目数的起始索引,默认从0开始; -
size:要显示的条目个数; -
LIMIT语句必须放在查询语句的最后。
语法变体:
sql
-- 写法一:LIMIT offset, size
LIMIT 0, 5 -- 从第0条开始,取5条
-- 写法二:LIMIT size(省略offset,默认从0开始)
LIMIT 5 -- 等价于 LIMIT 0, 5
分页公式
假设每页显示 size 条,要显示第 page 页的数据,则:
sql
LIMIT (page - 1) * size, size;
| 页数 page | offset | 含义 |
|---|---|---|
| 1 | 0 | 从第1条开始 |
| 2 | 25 | 从第26条开始(假设size=25) |
| 3 | 50 | 从第51条开始 |
示例1:查询前5条员工信息
sql
-- 查询前5条员工信息
SELECT *
FROM employees
LIMIT 5;
运行结果:
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 100 | Steven | King | SKING | AD_PRES | 24000.00 | 90 |
| 101 | Neena | Kochhar | NKOCHHAR | AD_VP | 17000.00 | 90 |
| 102 | Lex | De Haan | LDEHAAN | AD_VP | 17000.00 | 90 |
| 103 | Alexander | Hunold | AHUNOLD | IT_PROG | 9000.00 | 60 |
| 104 | Bruce | Ernst | BERNST | IT_PROG | 6000.00 | 60 |
等价写法:
sql
-- 等价写法,显式指定起始索引
SELECT *
FROM employees
LIMIT 0, 5;
运行结果: 与上方 LIMIT 5 完全一致(共 5 行),不再重复展示。
sql
-- 跳过前2条,取接下来的5条
SELECT *
FROM employees
LIMIT 2, 5;
运行结果:
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 102 | Lex | De Haan | LDEHAAN | AD_VP | 17000.00 | 90 |
| 103 | Alexander | Hunold | AHUNOLD | IT_PROG | 9000.00 | 60 |
| 104 | Bruce | Ernst | BERNST | IT_PROG | 6000.00 | 60 |
| 105 | David | Austin | DAUSTIN | IT_PROG | 4800.00 | 60 |
| 106 | Valli | Pataballa | VPATABAL | IT_PROG | 4800.00 | 60 |
示例2:查询员工表第11条到第25条数据
需求是"第11到第25条",共15条,起始索引是10(从0开始计数):
sql
-- 查询第11条到第25条数据(即跳过前10条,取15条)
SELECT *
FROM employees
LIMIT 10, 15;
运行结果:
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 114 | Den | Raphaely | DRAPHEAL | PU_MAN | 11000.00 | 30 |
| 115 | Alexander | Khoo | AKHOO | PU_CLERK | 3100.00 | 30 |
| 116 | Shelli | Baida | SBAIDA | PU_CLERK | 2900.00 | 30 |
| 120 | Matthew | Weiss | MWEISS | ST_MAN | 8000.00 | 50 |
| 121 | Adam | Fripp | AFRIPP | ST_MAN | 8200.00 | 50 |
| 122 | Payam | Kaufling | PKAUFLIN | ST_MAN | 7900.00 | 50 |
| 123 | Shanta | Vollman | SVOLLMAN | ST_MAN | 6500.00 | 50 |
| 124 | Kevin | Mourgos | KMOURGOS | ST_CLERK | 5800.00 | 50 |
| ... |
共 15 条,超过 8 行仅展示前 8 行。
实战:有奖金率的前10名员工(ORDER BY + LIMIT组合)
需求:查询有奖金率的员工信息,并按工资由高到低排序,取前10名。
sql
-- 有奖金率且工资较高的前10名员工
SELECT *
FROM employees
WHERE commission_pct IS NOT NULL
ORDER BY salary DESC
LIMIT 10;
运行结果:
| employee_id | first_name | last_name | job_id | salary | commission_pct | department_id |
|---|---|---|---|---|---|---|
| 145 | John | Russell | SA_MAN | 14000.00 | 0.40 | 80 |
| 146 | Karen | Partners | SA_MAN | 13500.00 | 0.30 | 80 |
| 147 | Alberto | Errazuriz | SA_MAN | 12000.00 | 0.30 | 80 |
| 149 | Eleni | Zlotkey | SA_MAN | 10500.00 | 0.20 | 80 |
| 150 | Peter | Tucker | SA_REP | 10000.00 | 0.30 | 80 |
| 151 | David | Bernstein | SA_REP | 9500.00 | 0.25 | 80 |
| 152 | Peter | Hall | SA_REP | 9000.00 | 0.25 | 80 |
有奖金率的员工仅 7 人,不足 10 条,故全部返回。
小结 :ORDER BY + LIMIT 是开发中最常见的组合------先排序,再截取前N条,常用于"Top N"查询。
七、联合查询(UNION)
语法格式
sql
查询语句1
UNION [ALL]
查询语句2
[UNION [ALL]
查询语句3
...];
应用场景
要查询的结果来自多个表 ,且多个表之间没有直接的连接关系 ,但查询的信息(列的语义)一致时,就可以用 UNION 把多条查询的结果合并成一个结果集。
三条特点
- 要求多条查询语句的查询列数是一致的;
- 要求多条查询语句查询的每一列的类型和顺序最好一致(不一致时MySQL会做隐式转换);
UNION关键字默认去重 ;如果使用UNION ALL,则保留重复项,不去重。
示例:部门编号大于90或邮箱包含e的员工(OR vs UNION对比)
需求 :查询部门编号大于90或邮箱包含字母 e 的员工信息。
写法一:用 OR
sql
-- 用 OR 实现
SELECT *
FROM employees
WHERE department_id > 90 OR email LIKE '%e%';
运行结果: 见下方 UNION 写法结果(两种写法一致)。
写法二:用 UNION
sql
-- 用 UNION 实现
SELECT * FROM employees WHERE department_id > 90
UNION
SELECT * FROM employees WHERE email LIKE '%e%';
运行结果:(两种写法结果一致)
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 102 | Lex | De Haan | LDEHAAN | AD_VP | 17000.00 | 90 |
| 104 | Bruce | Ernst | BERNST | IT_PROG | 6000.00 | 60 |
| 107 | Diana | Lorentz | DLORENTZ | IT_PROG | 4200.00 | 60 |
| 108 | Nancy | Greenberg | NGREENBE | FI_MGR | 12000.00 | 100 |
| 109 | Daniel | Faviet | DFAVIET | FI_ACCOUNT | 9000.00 | 100 |
| 114 | Den | Raphaely | DRAPHEAL | PU_MAN | 11000.00 | 30 |
| 120 | Matthew | Weiss | MWEISS | ST_MAN | 8000.00 | 50 |
| 142 | Curtis | Davies | CDAVIES | ST_CLERK | 3100.00 | 50 |
| ... |
部门 > 90 命中 100 号部门(108、109),邮箱含 e 共 14 人,去重后合计 14 条,超过 8 行仅展示前 8 行。
两种写法对比:
| 对比项 | OR | UNION |
|---|---|---|
| 适用场景 | 同一张表内多条件 | 不同表查询合并,或同表但条件差异较大 |
| 去重行为 | 默认不去重(除非 DISTINCT) | 默认去重 |
| 性能倾向 | 单表内条件筛选更快 | 跨表合并更直观 |
| 可读性 | 条件多时易混乱 | 多条查询清晰独立 |
当多个查询来自不同的表 时,只能用
UNION,不能用OR。例如:查询中国的用户和美国的用户,两张表结构相同,但数据分布在不同表里。
UNION 与 UNION ALL 的区别
sql
-- UNION:默认去重(性能略低,需要去重排序)
SELECT * FROM employees WHERE department_id = 90
UNION
SELECT * FROM employees WHERE department_id = 90;
-- 结果只有3条(去重后)
-- UNION ALL:保留重复(性能更高,不去重)
SELECT * FROM employees WHERE department_id = 90
UNION ALL
SELECT * FROM employees WHERE department_id = 90;
-- 结果有6条(保留重复)
运行结果(UNION,默认去重,共 3 条):
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 100 | Steven | King | SKING | AD_PRES | 24000.00 | 90 |
| 101 | Neena | Kochhar | NKOCHHAR | AD_VP | 17000.00 | 90 |
| 102 | Lex | De Haan | LDEHAAN | AD_VP | 17000.00 | 90 |
运行结果(UNION ALL,保留重复,共 6 条):
| employee_id | first_name | last_name | job_id | salary | department_id | |
|---|---|---|---|---|---|---|
| 100 | Steven | King | SKING | AD_PRES | 24000.00 | 90 |
| 101 | Neena | Kochhar | NKOCHHAR | AD_VP | 17000.00 | 90 |
| 102 | Lex | De Haan | LDEHAAN | AD_VP | 17000.00 | 90 |
| 100 | Steven | King | SKING | AD_PRES | 24000.00 | 90 |
| 101 | Neena | Kochhar | NKOCHHAR | AD_VP | 17000.00 | 90 |
| 102 | Lex | De Haan | LDEHAAN | AD_VP | 17000.00 | 90 |
实战建议:如果确定没有重复数据,或业务上需要保留重复,优先用
UNION ALL,性能更好。
总结
到这里,关于子查询、分页查询、联合查询的常用知识点就梳理完了。回顾一下:
- 子查询 是SQL查询能力进阶的核心,按位置分为
SELECT/WHERE/FROM/EXISTS四种,按结果集分为标量/列/行/表四种; - WHERE/HAVING 后面 最常用,要分清单行操作符(
><=)和多行操作符(INANYALL),不能混用; - FROM 后面的子查询必须取别名,是"先聚合再关联"的关键写法;
- EXISTS 用于判断"是否存在",与
IN在多数场景可互换; - 分页查询 LIMIT 是Web开发必备技能,记住公式
LIMIT (page-1)*size, size; - UNION 用于跨表合并结果集,注意列数一致、
UNION去重而UNION ALL不去重。
子查询是面试高频考点 (尤其标量子查询、ANY/ALL、相关子查询),分页查询是开发必备技能(几乎每个后台管理系统都会用到)。建议把本文的示例都动手敲一遍,加深理解。