MySQL_子查询_分页查询与联合查询详解

MySQL子查询、分页查询与联合查询详解(附示例与运行结果)

本文系统讲解 MySQL 查询进阶三大核心技能------子查询、分页查询与联合查询。先梳理子查询的分类与特点,再结合 HR 示例库数据,逐一演示 WHERE/HAVING 后的标量、列、行子查询,SELECT 后的相关子查询,FROM 后的派生表及 EXISTS 用法;接着介绍 LIMIT 分页语法与分页公式,最后讲解 UNION/UNION ALL 合并结果集。全文示例均附运行结果,适合初学者对照练习,也是面试高频考点速查。


目录


前言

在前两天的学习中,我们已经掌握了 SELECTWHEREGROUP BYORDER BY 以及多表连接(JOIN)等基础查询语法。但在实际开发中,我们经常遇到这样的需求:

  • "谁的工资比Lex高?"

  • "查询最低工资大于50号部门最低工资的部门?"

  • "查询每个部门的平均工资等级?"

这些需求都有一个共同特征:查询条件依赖于另一条查询的结果 。此时,就需要使用子查询

子查询(Subquery) ,又称内查询(Inner Query) ,是指出现在其他SQL语句中的 SELECT 语句。包含子查询的语句则称为**主查询(Outer Query)**或外查询。

子查询可以嵌套在 SELECTFROMWHEREHAVING 等多个位置,灵活强大,是SQL查询能力进阶的关键,也是面试中的高频考点。

本文将系统讲解以下内容:

  1. 子查询的分类与特点
  2. WHERE/HAVING 后面的子查询(标量子查询、列子查询、行子查询)
  3. SELECT 后面的子查询
  4. FROM 后面的子查询
  5. EXISTS 相关子查询
  6. 分页查询(LIMIT)
  7. 联合查询(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 后面 仅支持标量子查询 =、>、< 等
WHEREHAVING 后面 标量子查询、列子查询、行子查询 单行:> < >= <= = != 多行:IN、ANY/SOME、ALL
EXISTS 后面 表子查询(相关子查询) EXISTS、NOT EXISTS
FROM 后面 表子查询(结果充当一张表) 必须取别名

1.2 按结果集行数分类

根据子查询返回结果集的形状,可以划分为四类:

类型 结果集形状 通俗理解
标量子查询 一行一列(一个值) 一个数
列子查询 一列多行 一列数据
行子查询 一行多列 一行数据
表子查询 多行多列 一张表

1.3 子查询特点总结

不论子查询出现在哪个位置,都有以下通用特点:

  1. 子查询必须放在小括号 () ,这是语法强制要求;
  2. 子查询一般放在条件的右侧,便于阅读;
  3. 子查询的执行优先于主查询,主查询的条件会用到子查询的结果(一张"虚表");
  4. 标量子查询一般搭配单行操作符> < >= <= = != <> <=>)使用;
  5. 列子查询一般搭配多行操作符INANY/SOMEALL)使用。

理解了以上骨架,后面的示例就是把这些"零件"组装起来。


二、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 email 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_namejob_idsalary

分步拆解

第一步,查询公司最低工资:

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

记住 :用单行操作符(> < = 等)比较,子查询必须只返回一行一列。若子查询返回多行,必须改用多行操作符(INANYALL)。


2.2 列子查询(多行一列)

当子查询返回一列多行(一组值)时,就需要使用多行操作符。

操作符 含义
IN / NOT IN 等于列表中的任意一个
ANY / SOME 和子查询返回的某一个值进行比较
ALL 和子查询返回的所有值进行比较

ANYSOME 完全等价,只是不同写法。

示例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_idIT_PROG 工种任一工资低 的员工的工号、名、job_id 以及 salary

分步拆解

第一步,查询 job_idIT_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_idIT_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 email 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 email 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 email 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 email 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 把多条查询的结果合并成一个结果集。

三条特点

  1. 要求多条查询语句的查询列数是一致的
  2. 要求多条查询语句查询的每一列的类型和顺序最好一致(不一致时MySQL会做隐式转换);
  3. 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 email 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 email 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 email 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,性能更好。


总结

到这里,关于子查询、分页查询、联合查询的常用知识点就梳理完了。回顾一下:

  1. 子查询 是SQL查询能力进阶的核心,按位置分为 SELECT/WHERE/FROM/EXISTS 四种,按结果集分为标量/列/行/表四种;
  2. WHERE/HAVING 后面 最常用,要分清单行操作符(> < =)和多行操作符(IN ANY ALL),不能混用;
  3. FROM 后面的子查询必须取别名,是"先聚合再关联"的关键写法;
  4. EXISTS 用于判断"是否存在",与 IN 在多数场景可互换;
  5. 分页查询 LIMIT 是Web开发必备技能,记住公式 LIMIT (page-1)*size, size
  6. UNION 用于跨表合并结果集,注意列数一致、UNION 去重而 UNION ALL 不去重。

子查询是面试高频考点 (尤其标量子查询、ANY/ALL、相关子查询),分页查询是开发必备技能(几乎每个后台管理系统都会用到)。建议把本文的示例都动手敲一遍,加深理解。


相关推荐
devpotato2 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
天若有情6732 小时前
Node+MySQL小型全栈笔记项目实战课程分享
数据库·笔记·mysql
wxwx_bscxy3223 小时前
基于springboot宠物领养系统的设计与实现
数据库·spring boot·后端·spring·宠物
九月要晚安3 小时前
达梦DW与AFC对比
数据库
实战派K8S&DB3 小时前
如何在内网配置 TiDB 数据库 Agent
数据库·人工智能·分布式·tidb
实战派K8S&DB3 小时前
《基于 Dify + FastAPI + PyTiDB 搭建大模型驱动的 TiDB 智能运维 Agent》
运维·数据库·分布式·云原生·tidb·fastapi
SelectDB4 小时前
Apache Doris 5.0 年度版本前瞻(一):构建统一的多模湖仓实时分析平台
大数据·数据库·数据分析
小白学大数据4 小时前
超简单:用 Python 让 Excel 飞起来:用 openpyxl 把重复报表整理交给脚本
开发语言·数据库·python·excel
西索斯coding4 小时前
doubao-seed-2.1-turbo 调用一直 401 怎么办?pro 版同样的 Key 却正常——5 分钟排查定位指南
java·服务器·数据库·ai