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(简洁易维护);需要跨会话复用时,用临时表。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui