MySQL进阶-SQL高级语法全解析

一、复杂查询语法:搞定多表与子查询的核心技巧

实际开发中,我们很少只查询单张表,多表关联、子查询是高频需求,这部分也是SQL进阶的基础,掌握这些,能解决80%的业务查询场景。

1.1 多表连接:不同场景选对连接方式,效率翻倍

多表连接的核心是"通过关联字段,将多张表的数据整合为一张虚拟表",常用的连接方式有4种:INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN,很多人容易混淆它们的区别,这里用"学生表+成绩表"的示例,一次性讲清楚。

先准备测试表和测试数据(后续所有示例均基于这两张表):

sql 复制代码
-- 学生表(student)
CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    student_no VARCHAR(20) NOT NULL COMMENT '学号',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    grade VARCHAR(10) NOT NULL COMMENT '年级',
    class VARCHAR(20) NOT NULL COMMENT '班级'
);

-- 成绩表(score)
CREATE TABLE score (
    id INT PRIMARY KEY AUTO_INCREMENT,
    student_no VARCHAR(20) NOT NULL COMMENT '学号(关联student表)',
    subject VARCHAR(30) NOT NULL COMMENT '科目',
    score INT NOT NULL COMMENT '分数',
    exam_date DATE NOT NULL COMMENT '考试日期'
);

-- 插入测试数据
INSERT INTO student (student_no, name, grade, class) 
VALUES 
('2024001', '张三', '高一', '1班'),
('2024002', '李四', '高一', '1班'),
('2024003', '王五', '高一', '2班'),
('2024004', '赵六', '高二', '3班');

INSERT INTO score (student_no, subject, score, exam_date) 
VALUES 
('2024001', '数学', 92, '2024-06-10'),
('2024001', '语文', 88, '2024-06-10'),
('2024002', '数学', 75, '2024-06-10'),
('2024003', '语文', 90, '2024-06-10'),
('2024005', '数学', 80, '2024-06-10'); -- 该学号无对应学生信息
1.1.1 INNER JOIN(内连接):只取两张表的交集

核心逻辑:只返回"两张表中关联字段匹配成功"的数据,不匹配的会被过滤掉。

示例:查询有成绩记录的学生姓名、班级及对应科目和分数(只显示既有学生信息、又有成绩的记录):

sql 复制代码
SELECT s.name, s.class, sc.subject, sc.score
FROM student s
INNER JOIN score sc ON s.student_no = sc.student_no;

-- 执行结果:不会出现赵六(无成绩)和学号2024005(无学生信息)的记录
-- 姓名  班级  科目  分数
-- 张三  1班  数学  92
-- 张三  1班  语文  88
-- 李四  1班  数学  75
-- 王五  2班  语文  90
1.1.2 LEFT JOIN(左连接):左表全保留,右表匹配补充

核心逻辑:保留"左表(LEFT JOIN左边的表)"的所有数据,右表中匹配成功的显示对应数据,匹配失败的显示NULL。

示例:查询所有学生的姓名、班级,以及他们的成绩(无成绩的学生也显示,成绩字段为NULL):

sql 复制代码
SELECT s.name, s.class, sc.subject, sc.score
FROM student s
LEFT JOIN score sc ON s.student_no = sc.student_no;

-- 执行结果:赵六(2024004)无成绩,subject和score显示NULL
-- 姓名  班级  科目    分数
-- 张三  1班  数学    92
-- 张三  1班  语文    88
-- 李四  1班  数学    75
-- 王五  2班  语文    90
-- 赵六  3班  NULL   NULL
1.1.3 RIGHT JOIN(右连接):右表全保留,左表匹配补充

核心逻辑:和LEFT JOIN相反,保留"右表(RIGHT JOIN右边的表)"的所有数据,左表匹配成功显示对应数据,匹配失败显示NULL。

示例:查询所有成绩记录,以及对应学生的姓名、班级(无对应学生的成绩也显示):

sql 复制代码
SELECT s.name, s.class, sc.subject, sc.score
FROM student s
RIGHT JOIN score sc ON s.student_no = sc.student_no;

-- 执行结果:学号2024005无学生信息,name和class显示NULL
-- 姓名  班级  科目  分数
-- 张三  1班  数学  92
-- 张三  1班  语文  88
-- 李四  1班  数学  75
-- 王五  2班  语文  90
-- NULL  NULL 数学  80
1.1.4 FULL JOIN(全连接):保留两张表的所有数据

核心逻辑:保留左表和右表的所有数据,匹配成功的显示对应数据,匹配失败的一侧显示NULL。 注意:MySQL本身不直接支持FULL JOIN,但可以通过"LEFT JOIN + UNION ALL + RIGHT JOIN"模拟实现。

示例:查询所有学生和所有成绩记录,无论是否匹配:

sql 复制代码
-- 模拟FULL JOIN
SELECT s.name, s.class, sc.subject, sc.score
FROM student s
LEFT JOIN score sc ON s.student_no = sc.student_no
UNION ALL
SELECT s.name, s.class, sc.subject, sc.score
FROM student s
RIGHT JOIN score sc ON s.student_no = sc.student_no
WHERE s.id IS NULL; -- 过滤掉重复的匹配数据

-- 执行结果:包含所有学生和所有成绩,无匹配的字段显示NULL
-- 姓名  班级  科目    分数
-- 张三  1班  数学    92
-- 张三  1班  语文    88
-- 李四  1班  数学    75
-- 王五  2班  语文    90
-- 赵六  3班  NULL   NULL
-- NULL  NULL 数学    80
1.1.5 自连接与不等值连接

除了上述4种常规连接,还有两种特殊连接场景,在复杂业务中会用到:

  1. 自连接:一张表自己和自己连接,本质是"将一张表当作两张表使用",常用于查询"同一表中具有关联关系的数据"(如员工表中查询员工及其上级)。

示例(新增员工表测试数据):

sql 复制代码
-- 员工表(含上级ID)
CREATE TABLE emp (
    id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(50) NOT NULL,
    dept VARCHAR(30) NOT NULL,
    manager_id INT COMMENT '上级ID(关联自身id)'
);

INSERT INTO emp (emp_name, dept, manager_id)
VALUES 
('张三', '研发部', NULL), -- 无上级(部门经理)
('李四', '研发部', 1),    -- 上级是张三
('王五', '研发部', 1),    -- 上级是张三
('赵六', '测试部', NULL); -- 无上级(部门经理)

-- 自连接查询:查询每个员工的姓名及其上级姓名
SELECT e.emp_name AS 员工姓名, m.emp_name AS 上级姓名
FROM emp e
LEFT JOIN emp m ON e.manager_id = m.id;

-- 执行结果
-- 员工姓名  上级姓名
-- 张三      NULL
-- 李四      张三
-- 王五      张三
-- 赵六      NULL
  1. 不等值连接:连接条件不是"=",而是">、<、>=、<=、BETWEEN AND"等不等值条件,常用于"范围匹配"场景。

示例(新增分数等级表):

sql 复制代码
-- 分数等级表
CREATE TABLE score_level (
    level VARCHAR(10) PRIMARY KEY,
    min_score INT NOT NULL,
    max_score INT NOT NULL
);

INSERT INTO score_level (level, min_score, max_score)
VALUES ('优秀', 90, 100), ('良好', 80, 89), ('及格', 60, 79), ('不及格', 0, 59);

-- 不等值连接:查询每个学生的成绩及对应等级
SELECT sc.student_no, sc.subject, sc.score, sl.level
FROM score sc
LEFT JOIN score_level sl 
ON sc.score BETWEEN sl.min_score AND sl.max_score;

-- 执行结果
-- student_no  subject  score  level
-- 2024001     数学     92     优秀
-- 2024001     语文     88     良好
-- 2024002     数学     75     及格
-- 2024003     语文     90     优秀
-- 2024005     数学     80     良好

1.2 子查询:嵌套查询的正确用法与避坑技巧

子查询(又称嵌套查询):将一个查询语句的结果,作为另一个查询语句的条件或数据源,分为"标量子查询、列子查询、行子查询、表子查询、关联子查询"5种,核心是"先执行内层查询,再执行外层查询"。

重点:子查询虽然灵活,但嵌套过深会影响性能,实际开发中需合理使用,优先考虑用JOIN替代复杂子查询。

1.2.1 标量子查询:返回单个值(一行一列)

核心特点:内层查询结果只有"一个值",常用于WHERE条件中,搭配=、>、<等比较运算符。

示例1:查询数学成绩大于平均分的学生学号、科目、分数:

sql 复制代码
-- 先查询数学平均分(内层子查询,返回单个值),再查询大于该平均分的记录
SELECT student_no, subject, score
FROM score
WHERE subject = '数学' 
  AND score > (SELECT AVG(score) FROM score WHERE subject = '数学');

-- 执行逻辑:先执行内层查询,得到数学平均分((92+75+80)/3 ≈ 82.33),再筛选出分数>82.33的记录
-- 执行结果
-- student_no  subject  score
-- 2024001     数学     92

示例2:查询成绩最高的学生姓名、科目、分数(假设最高成绩唯一):

sql 复制代码
SELECT s.name, sc.subject, sc.score
FROM student s
JOIN score sc ON s.student_no = sc.student_no
WHERE sc.score = (SELECT MAX(score) FROM score);

-- 执行结果:成绩最高为92(张三,数学)
-- name  subject  score
-- 张三  数学     92
1.2.2 列子查询:返回一列多行

核心特点:内层查询结果是"一列多个值",常用于WHERE条件中,搭配IN、NOT IN、ANY、ALL等运算符。

示例1:查询高一学生的所有成绩记录(先查高一学生的学号,再查对应成绩):

sql 复制代码
SELECT * FROM score
WHERE student_no IN (SELECT student_no FROM student WHERE grade = '高一');

-- 内层查询返回高一学生的学号(2024001、2024002、2024003),外层查询筛选出这些学号的成绩
-- 执行结果:包含张三、李四、王五的成绩,不包含赵六(高二)和2024005(无年级信息)
-- id  student_no  subject  score  exam_date
-- 1   2024001     数学     92     2024-06-10
-- 2   2024001     语文     88     2024-06-10
-- 3   2024002     数学     75     2024-06-10
-- 4   2024003     语文     90     2024-06-10

示例2:查询分数大于"高一所有学生数学成绩"的记录(用ALL):

sql 复制代码
SELECT * FROM score
WHERE score > ALL (SELECT score FROM score WHERE student_no IN (SELECT student_no FROM student WHERE grade = '高一') AND subject = '数学');

-- 内层查询返回高一学生的数学成绩(92、75),ALL表示"大于所有值",即>92
-- 执行结果:无符合条件的记录(最高分为92)
1.2.3 行子查询:返回一行多列

核心特点:内层查询结果是"一行多个值",常用于WHERE条件中,搭配=、IN等运算符,需保证内层和外层的列数、数据类型一致。

示例:查询和"张三(2024001)数学成绩"相同的所有记录(学号、科目、分数都匹配):

sql 复制代码
SELECT * FROM score
WHERE (student_no, subject, score) = (SELECT student_no, subject, score FROM score WHERE student_no = '2024001' AND subject = '数学');

-- 内层查询返回一行三列(2024001、数学、92),外层查询匹配相同的记录
-- 执行结果
-- id  student_no  subject  score  exam_date
-- 1   2024001     数学     92     2024-06-10
1.2.4 表子查询:返回多行多列(当作临时表)

核心特点:内层查询结果是"多行多列",相当于一张临时表,常用于FROM子句中,给临时表起别名后使用。

示例:查询每个学生的平均成绩,筛选出平均成绩大于85分的学生(先查每个学生的平均成绩,再筛选):

sql 复制代码
-- 表子查询:将每个学生的平均成绩作为临时表
SELECT t.student_no, s.name, t.avg_score
FROM (SELECT student_no, AVG(score) AS avg_score FROM score GROUP BY student_no) t
JOIN student s ON t.student_no = s.student_no
WHERE t.avg_score > 85;

-- 执行逻辑:内层子查询生成临时表t(包含student_no和avg_score),再和student表连接,筛选平均成绩>85的记录
-- 执行结果
-- student_no  name  avg_score
-- 2024001     张三  90.0000
-- 2024003     王五  90.0000
1.2.5 关联子查询:内层查询依赖外层查询(核心难点)

核心特点:和普通子查询不同,关联子查询的内层查询会用到外层查询的字段,执行顺序是"外层查询每执行一行,内层查询就执行一次",灵活性高,但性能相对较差(避免大量数据场景使用)。

示例1:查询每个学生的最高成绩及对应科目(按学生分组,每个学生取最高成绩的记录):

sql 复制代码
SELECT s.name, sc.subject, sc.score
FROM student s
JOIN score sc ON s.student_no = sc.student_no
WHERE sc.score = (SELECT MAX(score) FROM score WHERE student_no = sc.student_no);

-- 执行逻辑:外层查询每取一条score记录,内层查询就根据该记录的student_no,查询该学生的最高成绩,判断当前score是否等于最高成绩
-- 执行结果
-- name  subject  score
-- 张三  数学     92
-- 李四  数学     75
-- 王五  语文     90
-- (2024005无学生信息,不显示)

示例2:查询每个科目中,分数大于该科目平均分的记录:

sql 复制代码
SELECT subject, student_no, score
FROM score sc1
WHERE score > (SELECT AVG(score) FROM score sc2 WHERE sc2.subject = sc1.subject);

-- 执行逻辑:外层查询每取一条sc1记录,内层查询就根据该记录的subject,查询该科目的平均分,判断当前score是否大于平均分
-- 执行结果(数学平均分≈82.33,语文平均分≈89)
-- subject  student_no  score
-- 数学     2024001     92
-- 语文     2024003     90

1.3 EXISTS / IN 优化与选择

EXISTS和IN都是用于"判断是否存在满足条件的记录",但用法和性能有差异,很多人不知道该选哪个,这里结合场景给出明确结论。

1.3.1 语法区别
sql 复制代码
-- IN:判断字段是否在子查询返回的列表中
SELECT * FROM 表名 WHERE 字段 IN (子查询);

-- EXISTS:判断子查询是否返回数据(只要有一条,就返回true)
SELECT * FROM 表名 WHERE EXISTS (子查询);
1.3.2 性能差异与选择原则

核心结论:外层表小,用IN;内层表小,用EXISTS(原因:IN是先执行内层子查询,将结果存入临时表,再和外层表匹配;EXISTS是先执行外层表,再执行内层子查询,匹配到就终止,无需全部执行)。

示例1:查询有成绩记录的学生(外层表student小,用IN):

sql 复制代码
SELECT * FROM student WHERE student_no IN (SELECT DISTINCT student_no FROM score);

-- 等价于用EXISTS:
SELECT * FROM student s WHERE EXISTS (SELECT 1 FROM score sc WHERE sc.student_no = s.student_no);

-- 两种方式结果一致,但student表数据量小时,IN效率更高

示例2:查询有学生信息的成绩记录(内层表student小,用EXISTS):

sql 复制代码
SELECT * FROM score sc WHERE EXISTS (SELECT 1 FROM student s WHERE s.student_no = sc.student_no);

-- 等价于用IN:
SELECT * FROM score WHERE student_no IN (SELECT student_no FROM student);

-- 两种方式结果一致,但student表数据量小时,EXISTS效率更高

注意:NOT IN 和 NOT EXISTS 差异更大------NOT IN 会受到NULL值影响(如果子查询返回的结果包含NULL,NOT IN会返回空集),而NOT EXISTS 不受NULL值影响,优先用NOT EXISTS替代NOT IN。

反例(NOT IN 踩坑):

sql 复制代码
-- 子查询返回的student_no包含NULL(假设新增一条student_no为NULL的成绩)
INSERT INTO score (student_no, subject, score, exam_date) VALUES (NULL, '英语', 85, '2024-06-10');

-- 用NOT IN 查询无学生信息的成绩记录,会返回空集(因为NULL无法比较)
SELECT * FROM score WHERE student_no NOT IN (SELECT student_no FROM student);

-- 用NOT EXISTS 查询,正常返回结果
SELECT * FROM score sc WHERE NOT EXISTS (SELECT 1 FROM student s WHERE s.student_no = sc.student_no);

1.4 UNION / UNION ALL 区别与性能

UNION和UNION ALL 用于"合并两个或多个查询结果集",要求两个查询的列数、数据类型一致,核心区别在于"是否去重"。

1.4.1 语法与示例
sql 复制代码
-- UNION:合并结果集,去重(会扫描所有结果,删除重复记录,性能较差)
SELECT student_no, subject, score FROM score WHERE subject = '数学'
UNION
SELECT student_no, subject, score FROM score WHERE score > 90;

-- UNION ALL:合并结果集,不去重(直接合并,不处理重复,性能较好)
SELECT student_no, subject, score FROM score WHERE subject = '数学'
UNION ALL
SELECT student_no, subject, score FROM score WHERE score > 90;

执行结果对比:

  1. UNION 结果:合并后去重,张三的数学92分只出现一次;

  2. UNION ALL 结果:合并后不去重,张三的数学92分出现两次(既满足subject=数学,又满足score>90)。

1.4.2 选择原则
  1. 如果确定两个结果集没有重复记录,优先用UNION ALL(性能更优,避免不必要的去重操作);

  2. 如果可能有重复记录,且需要去重,再用UNION;

  3. 注意:UNION 和 UNION ALL 都要求两个查询的"列数、数据类型、列顺序"完全一致,否则会报错。

二、分组与聚合高级用法:摆脱GROUP BY的常见坑

分组(GROUP BY)和聚合函数(COUNT、SUM、AVG等)是统计类查询的核心,很多人在使用时会遇到"ONLY_FULL_GROUP_BY"报错、HAVING与WHERE混淆等问题,这里逐一拆解,结合示例避坑。

2.1 GROUP BY 原理与常见坑(ONLY_FULL_GROUP_BY)

核心原理:GROUP BY 用于"按指定字段分组",分组后,SELECT 后面的字段只能是"分组字段"或"聚合函数",否则会出现逻辑混乱(MySQL 5.7+ 开启了ONLY_FULL_GROUP_BY模式,会直接报错)。

常见坑:SELECT 字段中包含非分组、非聚合的字段,导致报错。

示例(正确 vs 错误):
sql 复制代码
-- 错误示例:SELECT 包含非分组、非聚合字段(name),ONLY_FULL_GROUP_BY模式下报错
SELECT student_no, name, AVG(score) AS avg_score
FROM student s
JOIN score sc ON s.student_no = sc.student_no
GROUP BY student_no;

-- 报错原因:name不是分组字段,也不是聚合函数,分组后无法确定返回哪个name(虽然一个student_no对应一个name,但MySQL不允许)

-- 正确示例1:SELECT 只包含分组字段和聚合函数
SELECT student_no, AVG(score) AS avg_score
FROM score
GROUP BY student_no;

-- 正确示例2:如果需要显示name,将name也加入分组(因为student_no是主键,name和student_no一一对应,分组后不影响)
SELECT s.student_no, s.name, AVG(sc.score) AS avg_score
FROM student s
JOIN score sc ON s.student_no = sc.student_no
GROUP BY s.student_no, s.name;

补充:如果确实需要在SELECT中显示非分组字段,可通过"聚合函数(如MAX、MIN)"包裹(适用于非分组字段和分组字段一一对应的场景):

sql 复制代码
SELECT student_no, MAX(name) AS name, AVG(score) AS avg_score
FROM student s
JOIN score sc ON s.student_no = sc.student_no
GROUP BY student_no;

2.2 聚合函数:COUNT/SUM/AVG/MAX/MIN 进阶

基础用法大家都熟悉,这里重点讲进阶用法和避坑点,尤其是COUNT和AVG的细节。

2.2.1 COUNT:统计行数的3种用法与区别
sql 复制代码
-- 1. COUNT(*):统计所有行数,包括NULL值(最常用,效率最高)
SELECT COUNT(*) FROM score; -- 统计所有成绩记录数(含student_no为NULL的记录)

-- 2. COUNT(字段名):统计该字段非NULL的行数
SELECT COUNT(student_no) FROM score; -- 统计student_no非NULL的成绩记录数(不含student_no为NULL的记录)

-- 3. COUNT(DISTINCT 字段名):统计该字段非NULL且去重后的行数
SELECT COUNT(DISTINCT student_no) FROM score; -- 统计有成绩的不同学生数

避坑点:COUNT(1) 和 COUNT(*) 效率基本一致(MySQL优化后),无需刻意替换;COUNT(字段名) 效率低于COUNT(*),因为需要判断字段是否为NULL。

2.2.2 SUM/AVG:处理NULL值与异常数据
  1. SUM:自动忽略NULL值,若所有值都是NULL,返回NULL(可搭配IFNULL处理);

  2. AVG:自动忽略NULL值,计算的是"非NULL值的平均值"(不是所有行的平均值)。

sql 复制代码
-- 示例:给score表新增一条score为NULL的记录
INSERT INTO score (student_no, subject, score, exam_date) VALUES ('2024001', '英语', NULL, '2024-06-10');

-- SUM:忽略NULL值,返回92+88+75+90+80 = 425
SELECT SUM(score) FROM score;

-- AVG:忽略NULL值,计算5个非NULL值的平均(425/5=85),不是6条记录的平均
SELECT AVG(score) FROM score;

-- 处理NULL值:将NULL视为0计算平均
SELECT AVG(IFNULL(score, 0)) FROM score; -- (425 + 0)/6 ≈ 70.83
2.2.3 MAX/MIN:忽略NULL值,支持非数值类型

MAX和MIN不仅支持数值类型,还支持字符串、日期类型,自动忽略NULL值。

sql 复制代码
-- 数值类型:查询最高、最低分数
SELECT MAX(score) AS max_score, MIN(score) AS min_score FROM score;

-- 字符串类型:查询姓名排序最前、最后的学生(按字典序)
SELECT MAX(name) AS max_name, MIN(name) AS min_name FROM student;

-- 日期类型:查询最早、最晚的考试日期
SELECT MAX(exam_date) AS latest_date, MIN(exam_date) AS earliest_date FROM score;

2.3 HAVING 与 WHERE 区别(核心重点)

很多人混淆HAVING和WHERE,核心区别只有一个:WHERE 过滤行,HAVING 过滤分组,具体差异如下:

对比项 WHERE HAVING
作用对象 单个行数据 分组后的结果集
使用时机 分组(GROUP BY)之前 分组(GROUP BY)之后
支持的条件 普通字段、表达式,不支持聚合函数 聚合函数、分组字段,支持普通字段(不推荐)
示例(对比使用):
sql 复制代码
-- 需求1:查询数学科目中,分数大于80分的学生,按学号分组,统计平均成绩(先过滤行,再分组)
SELECT student_no, AVG(score) AS avg_score
FROM score
WHERE subject = '数学' AND score > 80 -- WHERE:过滤数学科目、分数>80的行
GROUP BY student_no;

-- 需求2:查询每个学生的平均成绩,筛选出平均成绩大于85分的学生(先分组,再过滤分组)
SELECT student_no, AVG(score) AS avg_score
FROM score
GROUP BY student_no
HAVING avg_score > 85; -- HAVING:过滤平均成绩>85的分组

-- 错误示例:WHERE中使用聚合函数(会报错)
SELECT student_no, AVG(score) AS avg_score
FROM score
WHERE AVG(score) > 85 -- 报错:Invalid use of group function
GROUP BY student_no;

2.4 分组后排序、分页

实际开发中,分组后常需要对分组结果排序(如按平均成绩降序)、分页(如只显示前2个分组),核心是"GROUP BY 之后,用ORDER BY 排序,LIMIT 分页"。

sql 复制代码
-- 示例:查询每个学生的平均成绩,按平均成绩降序排序,分页显示前2条
SELECT s.name, s.class, AVG(sc.score) AS avg_score
FROM student s
JOIN score sc ON s.student_no = sc.student_no
GROUP BY s.student_no, s.name, s.class
ORDER BY avg_score DESC -- 分组后排序
LIMIT 0, 2; -- 分页(从第0条开始,取2条)

-- 执行结果:显示平均成绩最高的2个学生
-- name  class  avg_score
-- 张三  1班    90.0000
-- 王五  2班    90.0000

2.5 去重统计:COUNT(DISTINCT xx)

用于"统计某字段不重复的行数",是业务中高频需求(如统计有多少个不同的学生参加了考试、有多少个不同的科目)。

sql 复制代码
-- 示例1:统计有多少个不同的学生参加了考试(去重)
SELECT COUNT(DISTINCT student_no) AS student_count FROM score;

-- 示例2:统计有多少个不同的科目(去重)
SELECT COUNT(DISTINCT subject) AS subject_count FROM score;

-- 示例3:统计每个班级有多少个不同的学生参加了考试
SELECT s.class, COUNT(DISTINCT s.student_no) AS student_count
FROM student s
JOIN score sc ON s.student_no = sc.student_no
GROUP BY s.class;

避坑点:COUNT(DISTINCT 字段1, 字段2) 表示"两个字段同时去重",只有两个字段都相同才视为重复。

sql 复制代码
-- 统计不同的"学生+科目"组合数(一个学生选多个科目,视为不同组合)
SELECT COUNT(DISTINCT student_no, subject) AS combo_count FROM score;

三、窗口函数:进阶必备,面试高频(重点难点)

窗口函数是MySQL 8.0+ 新增的核心功能,也是进阶阶段的重点,能轻松解决"TopN、连续登录、环比同比、排名"等复杂业务场景,替代复杂的子查询和自连接,代码更简洁、可读性更高。

核心定义:窗口函数 = 聚合函数 / 排序函数 + OVER(窗口子句),其中"窗口子句"用于定义"分析的范围"(如按哪个字段分组、排序)。

窗口子句常用语法:OVER(PARTITION BY 分组字段 ORDER BY 排序字段 [ROWS/RANGE 范围])

说明:PARTITION BY 相当于"分组但不聚合"(分组后每个组内的每行都保留),ORDER BY 用于组内排序,ROWS/RANGE 用于指定窗口范围(默认无需手动指定)。

3.1 排序类窗口函数:ROW_NUMBER() / RANK() / DENSE_RANK()

三者都是用于"组内排序、排名",核心区别在于"处理并列排名的方式",用示例一次性讲清楚(基于score表,按科目分组,对分数排序)。

sql 复制代码
-- 示例:按科目分组,对每个科目的分数进行排名,对比三种函数
SELECT 
    subject,
    student_no,
    score,
    ROW_NUMBER() OVER(PARTITION BY subject ORDER BY score DESC) AS row_num,
    RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS rnk,
    DENSE_RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS dense_rnk
FROM score;

-- 执行结果(以数学科目为例,分数92、80、75):
-- subject  student_no  score  row_num  rnk  dense_rnk
-- 数学     2024001     92     1        1    1
-- 数学     2024005     80     2        2    2
-- 数学     2024002     75     3        3    3

-- 若新增一条数学分数92的记录(student_no=2024006),结果变化:
-- subject  student_no  score  row_num  rnk  dense_rnk
-- 数学     2024001     92     1        1    1
-- 数学     2024006     92     2        1    1
-- 数学     2024005     80     3        3    2
-- 数学     2024002     75     4        4    3

三者区别总结:

  1. ROW_NUMBER():不处理并列,即使分数相同,排名也不重复(1、2、3、4);

  2. RANK():处理并列,并列的排名相同,后续排名跳过(1、1、3、4);

  3. DENSE_RANK():处理并列,并列的排名相同,后续排名不跳过(1、1、2、3)。

实战场景:TopN问题(每个科目取分数前2名)
sql 复制代码
-- 方法1:用ROW_NUMBER()(分数相同,只取前2个,不管并列)
SELECT * FROM (
    SELECT 
        subject,
        student_no,
        score,
        ROW_NUMBER() OVER(PARTITION BY subject ORDER BY score DESC) AS row_num
    FROM score
) t
WHERE t.row_num <= 2;

-- 方法2:用RANK()(分数相同,并列排名,可能超过2条)
SELECT * FROM (
    SELECT 
        subject,
        student_no,
        score,
        RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS rnk
    FROM score
) t
WHERE t.rnk <= 2;

3.2 分布类窗口函数:NTILE() / PERCENT_RANK()

用于"将数据分组、计算百分比排名",适用于"分档次、占比分析"场景。

3.2.1 NTILE(n):将组内数据分为n个档次

核心:将每个分组内的行,平均分为n个部分,返回每个行所在的档次(若无法平均分,前面的档次会多1行)。

sql 复制代码
-- 示例:按科目分组,将每个科目的分数分为3个档次(优秀、良好、及格)
SELECT 
    subject,
    student_no,
    score,
    NTILE(3) OVER(PARTITION BY subject ORDER BY score DESC) AS level
FROM score;

-- 执行结果(数学科目4条记录,分为3档:第1档1行,第2档1行,第3档2行):
-- subject  student_no  score  level
-- 数学     2024001     92     1
-- 数学     2024006     92     2
-- 数学     2024005     80     3
-- 数学     2024002     75     3
3.2.2 PERCENT_RANK():计算百分比排名

核心:返回当前行在组内的百分比排名,公式:(当前排名 - 1) / (组内总行数 - 1),结果范围0~1(0表示最低,1表示最高)。

sql 复制代码
-- 示例:按科目分组,计算每个学生分数的百分比排名
SELECT 
    subject,
    student_no,
    score,
    RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS rnk,
    PERCENT_RANK() OVER(PARTITION BY subject ORDER BY score DESC) AS pct_rank
FROM score;

-- 执行结果(数学科目4条记录,组内总行数=4):
-- subject  student_no  score  rnk  pct_rank
-- 数学     2024001     92     1    0.0000  -- (1-1)/(4-1)=0
-- 数学     2024006     92     1    0.0000  -- (1-1)/(4-1)=0
-- 数学     2024005     80     3    0.6667  -- (3-1)/(4-1)≈0.6667
-- 数学     2024002     75     4    1.0000  -- (4-1)/(4-1)=1

3.3 偏移类窗口函数:LAG() / LEAD()

用于"获取当前行的前n行(LAG)或后n行(LEAD)的数据",无需自连接,就能实现"连续数据对比"(如环比、连续登录判断)。

语法:LAG(字段名, n, 默认值) / LEAD(字段名, n, 默认值),n表示"偏移n行",默认值表示"无数据时返回的值"(默认NULL)。

实战场景1:查询每个学生的当前科目分数,以及上一个科目、下一个科目的分数
sql 复制代码
SELECT 
    student_no,
    subject,
    score,
    LAG(score, 1, 0) OVER(PARTITION BY student_no ORDER BY subject) AS prev_score, -- 上一个科目分数
    LEAD(score, 1, 0) OVER(PARTITION BY student_no ORDER BY subject) AS next_score -- 下一个科目分数
FROM score
WHERE student_no = '2024001'; -- 只看张三的记录

-- 执行结果:
-- student_no  subject  score  prev_score  next_score
-- 2024001     数学     92     0          88
-- 2024001     语文     88     92         0
-- 2024001     英语     NULL   88         0
实战场景2:判断学生是否连续两天参加考试(假设exam_date为考试日期)
sql 复制代码
-- 示例:查询每个学生的考试日期,判断是否连续
SELECT 
    student_no,
    exam_date,
    LAG(exam_date, 1) OVER(PARTITION BY student_no ORDER BY exam_date) AS prev_date,
    DATEDIFF(exam_date, LAG(exam_date, 1) OVER(PARTITION BY student_no ORDER BY exam_date)) AS date_diff,
    CASE WHEN DATEDIFF(exam_date, LAG(exam_date, 1) OVER(PARTITION BY student_no ORDER BY exam_date)) = 1 THEN '连续' ELSE '不连续' END AS is_continuous
FROM score;

-- 执行逻辑:用LAG获取上一次考试日期,计算两次日期差,若差为1则连续
-- 执行结果(假设张三两天连续考试):
-- student_no  exam_date  prev_date  date_diff  is_continuous
-- 2024001     2024-06-10  NULL       NULL       不连续
-- 2024001     2024-06-11  2024-06-10 1          连续

3.4 聚合窗口:SUM() OVER() / AVG() OVER()

将聚合函数(SUM、AVG等)与窗口函数结合,用于"计算累计值、移动平均值",无需分组,保留每行数据,同时显示聚合结果。

实战场景1:计算每个科目的分数累计和
sql 复制代码
SELECT 
    subject,
    student_no,
    score,
    SUM(score) OVER(PARTITION BY subject ORDER BY score DESC) AS cumulative_sum
FROM score;

-- 执行结果(数学科目):
-- subject  student_no  score  cumulative_sum
-- 数学     2024001     92     92
-- 数学     2024006     92     184  -- 92+92
-- 数学     2024005     80     264  -- 184+80
-- 数学     2024002     75     339  -- 264+75
实战场景2:计算每个学生的分数移动平均值(前1行+当前行+后1行的平均)
sql 复制代码
SELECT 
    student_no,
    subject,
    score,
    AVG(score) OVER(PARTITION BY student_no ORDER BY subject ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg
FROM score
WHERE student_no = '2024001';

-- 说明:ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 表示"窗口范围为前1行、当前行、后1行"
-- 执行结果:
-- student_no  subject  score  moving_avg
-- 2024001     数学     92     90.0000  -- (92+88)/2(无前1行,只取当前和后1行)
-- 2024001     语文     88     88.0000  -- (92+88+NULL)/2(NULL自动忽略,取前1行和当前行)
-- 2024001     英语     NULL   88.0000  -- (88+NULL)/2(无后1行,只取前1行)
3.4.1 聚合窗口与普通聚合的区别

很多人会混淆"聚合窗口函数"和"普通聚合函数",核心区别在于:

  1. 普通聚合函数(SUM、AVG等)+ GROUP BY:会将分组后的数据合并为一行,只返回聚合结果;

  2. 聚合窗口函数 + OVER():不会合并行,保留每行原始数据,同时在每行后追加聚合结果(按窗口范围计算)。

sql 复制代码
-- 普通聚合(GROUP BY):按学生分组,只返回每个学生的平均成绩(一行一个学生)
SELECT student_no, AVG(score) AS avg_score
FROM score
WHERE student_no = '2024001'
GROUP BY student_no;

-- 聚合窗口:保留每行成绩,同时追加该学生的平均成绩(一行一个科目)
SELECT 
    student_no,
    subject,
    score,
    AVG(score) OVER(PARTITION BY student_no) AS avg_score
FROM score
WHERE student_no = '2024001';

3.5 窗口函数的注意事项(避坑重点)

窗口函数虽好用,但使用时容易踩坑,以下4个注意事项必须牢记:

  1. 窗口函数只能用于 SELECT 和 ORDER BY 子句,不能用于 WHERE、GROUP BY、HAVING 子句(因为窗口函数是在这些子句执行之后才执行的);
sql 复制代码
-- 错误示例:WHERE中使用窗口函数(会报错)
SELECT student_no, subject, score, ROW_NUMBER() OVER(PARTITION BY subject ORDER BY score DESC) AS row_num
FROM score
WHERE row_num <= 2;

-- 正确示例:先子查询获取窗口函数结果,再在WHERE中过滤
SELECT * FROM (
    SELECT student_no, subject, score, ROW_NUMBER() OVER(PARTITION BY subject ORDER BY score DESC) AS row_num
    FROM score
) t
WHERE t.row_num <= 2;
  1. 窗口函数的执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT(窗口函数)→ ORDER BY → LIMIT;

  2. PARTITION BY 可省略,省略后表示"整个结果集作为一个窗口"(不分组,对所有数据进行分析);

sql 复制代码
-- 省略PARTITION BY,对所有成绩进行排名
SELECT 
    student_no,
    subject,
    score,
    ROW_NUMBER() OVER(ORDER BY score DESC) AS global_row_num
FROM score;
  1. MySQL 8.0+ 才支持窗口函数,若使用5.7及以下版本,需用子查询、自连接替代(如TopN问题用子查询+LIMIT)。

四、CTE(公共表表达式):简化复杂查询,提升可读性

CTE(Common Table Expression,公共表表达式)是MySQL 8.0+ 新增的功能,用于"定义临时结果集",可以替代复杂的子查询嵌套,让SQL代码更简洁、可读性更高,尤其适合多次复用同一子查询结果的场景。

核心语法:WITH 临时表名 AS (子查询),然后在后续查询中使用该临时表。

4.1 基本用法:单CTE

示例:用CTE查询每个学生的平均成绩,筛选出平均成绩大于85分的学生(替代表子查询):

sql 复制代码
-- 定义CTE(临时表t,存储每个学生的平均成绩)
WITH t AS (
    SELECT student_no, AVG(score) AS avg_score 
    FROM score 
    GROUP BY student_no
)
-- 使用CTE,和student表连接筛选
SELECT t.student_no, s.name, t.avg_score
FROM t
JOIN student s ON t.student_no = s.student_no
WHERE t.avg_score > 85;

-- 等价于之前的表子查询,但代码更简洁,可读性更高

4.2 进阶用法:多CTE、嵌套CTE

  1. 多CTE:用逗号分隔多个CTE,可相互引用(前一个CTE可被后一个CTE使用);

  2. 嵌套CTE:在CTE内部再定义CTE,适用于更复杂的查询场景。

sql 复制代码
-- 多CTE示例:查询每个科目分数前2名的学生信息(结合CTE和窗口函数)
WITH 
-- 第一个CTE:给每个科目的分数排名
score_rank AS (
    SELECT 
        subject,
        student_no,
        score,
        ROW_NUMBER() OVER(PARTITION BY subject ORDER BY score DESC) AS row_num
    FROM score
),
-- 第二个CTE:筛选出每个科目前2名的记录
top2_score AS (
    SELECT * FROM score_rank WHERE row_num <= 2
)
-- 使用第二个CTE,关联student表获取学生信息
SELECT t.subject, t.student_no, s.name, s.class, t.score
FROM top2_score t
LEFT JOIN student s ON t.student_no = s.student_no;

-- 嵌套CTE示例:在CTE内部嵌套CTE
WITH parent_cte AS (
    -- 内部嵌套CTE,查询高一学生的学号
    WITH child_cte AS (
        SELECT student_no FROM student WHERE grade = '高一'
    )
    -- 父CTE使用子CTE的结果,查询高一学生的成绩
    SELECT sc.student_no, sc.subject, sc.score
    FROM score sc
    JOIN child_cte c ON sc.student_no = c.student_no
)
-- 使用父CTE,统计高一学生各科目平均分
SELECT subject, AVG(score) AS avg_score
FROM parent_cte
GROUP BY subject;

4.3 CTE与子查询、临时表的区别

对比项 CTE 子查询 临时表
可读性 高,代码结构化,可复用 低,嵌套过深易混乱 中等,需手动创建和删除
复用性 高,可在同一查询中多次引用 低,每次使用需重复编写 高,可在整个会话中复用
性能 和子查询基本一致(MySQL优化后) 嵌套过深会影响性能 性能较好,但创建和删除有开销

选择原则:简单查询用子查询,复杂查询、需要复用临时结果集时,优先用CTE(简洁易维护);需要跨会话复用时,用临时表。

相关推荐
Kapaseker2 小时前
lazy 与 lateinit 到底有什么区别?
android·kotlin
黄林晴2 小时前
慌了!Android 17 取消图标文字,你的 App 可能要找不到了
android
空中海2 小时前
3.4 状态同步与生命周期管理
android·网络
砖厂小工2 小时前
Android 开发的 AI coding 与 AI debugging
android·ai编程
peakmain92 小时前
CmComposeUI —— 基于 Kotlin Multiplatform Compose 的 UI 组件库
android
studyForMokey2 小时前
【Android面试】Glide专题
android·面试·glide
m0_738120722 小时前
渗透知识ctfshow——Web应用安全与防护(三)
android·前端·安全
y = xⁿ2 小时前
【保姆级 :图解MySQL 执行全链路讲解】主键索引扫描,全局扫描,索引下推还是分不清楚?这一篇就够啦
android·mysql
薿夜11 小时前
SpringSecurity(三)
android