【MySQL SQL语句实战】设计表,建表语句,数据插入,实战案例包括:简单查询、汇总统计、分组分析、多表关联、窗口函数
文章目录
- 前言
- SQL实战学习教程(MySQL版)
- 项目介绍
- 一、数据模型说明
- 二、环境准备
- 三、建表语句(DDL)
- 四、数据插入(DML)
- 五、SQL实战案例
- 5.1 简单查询(单表模糊/精准查询)
- 案例1:查询姓“曹”的学生名单
- 案例2:查询姓名最后一个字是“玉”的学生名单
- 案例3:查询姓名中包含“星”的学生名单
- 案例4:查询1996年出生的学生名单
- 案例5:查询各学生的年龄(按当前日期计算)
- 5.2 汇总查询(聚合函数)
- 案例1:查询课程编号为“0002”(数学)的总成绩
- 案例2:查询选了课程的学生人数(去重)
- 案例3:查询所有课程成绩小于85分的学生的学号、姓名
- 案例4:查询没有学全所有课程的学生的学号、姓名
- 5.3 分组查询(GROUP BY)
- 案例1:查询各科成绩最高和最低得分
- 案例2:查询每门课程被选修的学生数
- 案例3:查询学生中男、女人数
- 案例4:查询同名同姓学生名单并统计同名人数
- 5.4 带条件的分组查询(HAVING)
- 案例1:查询平均成绩大于60分学生的学号和平均成绩
- 案例2:查询至少选修两门课程的学生学号以及课程数目
- 案例3:查询不及格的课程并按照课程号从大到小排序
- 案例4:查询两门以上成绩不满85分的同学的学号及其平均成绩
- 5.5 多表查询(JOIN关联)
- 案例1:查询所有学生的学号、姓名、选课数、总成绩
- 案例2:查询平均成绩大于85分的所有学生的学号、姓名、平均成绩
- 案例3:查询学生的选课情况:学号、姓名、课程号、课程名称
- 案例4:查询出每门课程的大于80分人数和不大于80分的人数
- 案例5:按分数段统计各科成绩([90,100]、[80,90)、[70,80)、[60,70)、<60)
- 案例6:数据的行列互换(将课程名转为列,显示学生各科成绩)
- 5.6 多表连接查询(复杂关联)
- 案例1:查询课程号为“0001”(语文)且分数小于90的学生信息,按分数降序排列
- 案例2:查询不同老师所教的不同课程的平均分,从高到低显示
- 案例3:查询课程名称为“数学”且分数低于90的学生姓名和分数
- 案例4:查询学过“陈独秀”老师所教的所有课的同学的学号、姓名
- 案例5:查询至少有一门课与学号为“0001”的学生所学课程相同的学生的学号和姓名
- 5.7 窗口函数(MySQL 8.0+)
- 案例1:查询学生平均成绩及其名次
- 案例2:按照各科成绩进行排序,并显示排名(不跳号)
- 案例3:查询每门成绩最好的前两名学生的姓名
- 案例4:按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩
- 六、经典面试题:查找A表存在而B表不存在的ID
- 方法1:LEFT JOIN + IS NULL(兼容性最好)
- 方法2:NOT IN(简洁,但需注意NULL)
- 方法3:NOT EXISTS(性能最优,推荐)
- 七、总结
前言
- 若对您有帮助的话,请点赞收藏加关注哦,您的关注是我持续创作的动力!有问题请私信或联系邮箱:funian.gm@gmail.com
SQL实战学习教程(MySQL版)
项目介绍
本项目专注于MySQL SQL语句的实战练习,涵盖简单查询、汇总统计、分组分析、多表关联、窗口函数等几乎所有主流使用场景。所有代码均可直接复制到MySQL客户端(如Navicat、MySQL Workbench)执行,适合SQL初学者夯实基础,也适合开发/测试人员复习巩固。
项目包含4个核心数据表(学生表、成绩表、课程表、教师表),通过“数据准备→实战案例→面试题”的结构,帮助读者循序渐进掌握SQL技能。
一、数据模型说明
首先明确数据表结构及关联关系,为后续查询打下基础。4个表的核心关联如下:
- 学生表(
student
)与成绩表(score
):通过学号(id
/stu_id
) 关联 - 课程表(
course
)与成绩表(score
):通过课程号(id
/course_id
) 关联 - 教师表(
teacher
)与课程表(course
):通过教师号(id
/teacher_id
) 关联
各表字段详情如下:
表名 | 字段名 | 数据类型 | 说明 | 约束 |
---|---|---|---|---|
student | id | VARCHAR(20) | 学号 | 主键(非空唯一) |
student | name | VARCHAR(20) | 学生姓名 | 非空 |
student | birth | DATE | 出生日期 | 非空 |
student | sex | VARCHAR(5) | 性别(男/女) | 非空 |
score | stu_id | VARCHAR(20) | 学号(关联student.id ) | 联合主键(非空) |
score | course_id | VARCHAR(20) | 课程号(关联course.id ) | 联合主键(非空) |
score | grade | FLOAT(3) | 成绩(0-100) | 非空 |
course | id | VARCHAR(20) | 课程号 | 主键(非空唯一) |
course | name | VARCHAR(20) | 课程名称 | 非空 |
course | teacher_id | VARCHAR(20) | 教师号(关联teacher.id ) | 非空 |
teacher | id | VARCHAR(20) | 教师号 | 主键(非空唯一) |
teacher | name | VARCHAR(20) | 教师姓名 | 非空 |
二、环境准备
- MySQL版本:推荐5.7+或8.0+(窗口函数需8.0+支持)
- 字符集:默认
utf8mb4
(支持中文及特殊字符) - 引擎:
InnoDB
(支持事务和外键,确保数据完整性)
三、建表语句(DDL)
执行以下SQL创建4个核心数据表,语句包含字段注释和索引约束,可直接复制执行:
-- 1. 学生表(student):存储学生基本信息
DROP TABLE IF EXISTS student;
CREATE TABLE student (`id` VARCHAR(20) NOT NULL COMMENT '学号(主键)',`name` VARCHAR(20) NOT NULL COMMENT '学生姓名',`birth` DATE NOT NULL COMMENT '出生日期',`sex` VARCHAR(5) NOT NULL COMMENT '性别(男/女)',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生基本信息表';-- 2. 成绩表(score):存储学生课程成绩(学号+课程号联合主键,避免重复成绩)
DROP TABLE IF EXISTS score;
CREATE TABLE score (`stu_id` VARCHAR(20) NOT NULL COMMENT '学号(关联student.id)',`course_id` VARCHAR(20) NOT NULL COMMENT '课程号(关联course.id)',`grade` FLOAT(3) NOT NULL COMMENT '成绩(0-100)',PRIMARY KEY (`stu_id`, `course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生课程成绩表';-- 3. 课程表(course):存储课程信息及授课教师
DROP TABLE IF EXISTS course;
CREATE TABLE course (`id` VARCHAR(20) NOT NULL COMMENT '课程号(主键)',`name` VARCHAR(20) NOT NULL COMMENT '课程名称',`teacher_id` VARCHAR(20) NOT NULL COMMENT '教师号(关联teacher.id)',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程信息表';-- 4. 教师表(teacher):存储教师基本信息
DROP TABLE IF EXISTS teacher;
CREATE TABLE teacher (`id` VARCHAR(20) NOT NULL COMMENT '教师号(主键)',`name` VARCHAR(20) NOT NULL COMMENT '教师姓名',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师基本信息表';
四、数据插入(DML)
向数据表中插入测试数据,确保后续查询案例可正常运行:
-- 1. 插入学生数据(6名学生,包含不同姓名、出生日期和性别)
INSERT INTO student (`id`, `name`, `birth`, `sex`)
VALUES
('0001', '曹喜嗯', '1996-01-01', '男'),
('0002', '曹喜嗯', '1994-02-01', '女'),
('0003', '张曼玉', '1964-09-20', '女'),
('0004', '周星驰', '1962-06-22', '男'),
('0005', '刘德华', '1961-09-27', '男'),
('0006', '林志玲', '1974-11-29', '女');-- 2. 插入成绩数据(每个学生选修2-3门课程,成绩分布在60-99分)
INSERT INTO score (`stu_id`, `course_id`, `grade`)
VALUES
('0001', '0001', 80), -- 曹喜嗯(0001)-语文(0001)-80分
('0001', '0002', 90), -- 曹喜嗯(0001)-数学(0002)-90分
('0001', '0003', 99), -- 曹喜嗯(0001)-英语(0003)-99分
('0002', '0001', 60), -- 曹喜嗯(0002)-语文(0001)-60分
('0002', '0002', 80), -- 曹喜嗯(0002)-数学(0002)-80分
('0002', '0003', 88), -- 曹喜嗯(0002)-英语(0003)-88分
('0003', '0001', 85), -- 张曼玉(0003)-语文(0001)-85分
('0003', '0002', 78), -- 张曼玉(0003)-数学(0002)-78分
('0003', '0004', 92), -- 张曼玉(0003)-物理(0004)-92分
('0004', '0002', 65), -- 周星驰(0004)-数学(0002)-65分
('0004', '0003', 70), -- 周星驰(0004)-英语(0003)-70分
('0004', '0004', 88), -- 周星驰(0004)-物理(0004)-88分
('0005', '0001', 95), -- 刘德华(0005)-语文(0001)-95分
('0005', '0003', 82), -- 刘德华(0005)-英语(0003)-82分
('0006', '0002', 93), -- 林志玲(0006)-数学(0002)-93分
('0006', '0004', 76); -- 林志玲(0006)-物理(0004)-76分-- 3. 插入课程数据(4门课程,关联对应教师)
INSERT INTO course (`id`, `name`, `teacher_id`)
VALUES
('0001', '语文', '0002'), -- 语文-王后雄(0002)
('0002', '数学', '0001'), -- 数学-陈独秀(0001)
('0003', '英语', '0003'), -- 英语-李大钊(0003)
('0004', '物理', '0004'); -- 物理-鲁迅(0004)-- 4. 插入教师数据(4名教师,对应课程授课)
INSERT INTO teacher (`id`, `name`)
VALUES
('0001', '陈独秀'), -- 数学教师
('0002', '王后雄'), -- 语文教师
('0003', '李大钊'), -- 英语教师
('0004', '鲁迅'); -- 物理教师
五、SQL实战案例
以下案例按“基础→进阶”排序,覆盖日常开发中90%以上的SQL场景,每个案例包含需求描述和可执行SQL,部分案例附加逻辑说明。
5.1 简单查询(单表模糊/精准查询)
案例1:查询姓“曹”的学生名单
需求:筛选所有姓名以“曹”开头的学生,显示学号、姓名、出生日期。
SELECT id AS 学号, name AS 姓名, birth AS 出生日期
FROM student
WHERE name LIKE '曹%'; -- %:通配符,匹配任意长度字符(包括0个)
案例2:查询姓名最后一个字是“玉”的学生名单
需求:筛选姓名末尾为“玉”的学生,显示完整信息。
SELECT id AS 学号, name AS 姓名, birth AS 出生日期, sex AS 性别
FROM student
WHERE name LIKE '%玉'; -- %匹配姓名前的任意字符
案例3:查询姓名中包含“星”的学生名单
需求:筛选姓名中包含“星”字的学生(无论位置)。
SELECT id AS 学号, name AS 姓名, sex AS 性别
FROM student
WHERE name LIKE '%星%'; -- 前后均加%,匹配中间包含指定字符的情况
案例4:查询1996年出生的学生名单
需求:筛选出生日期在1996年的学生,显示学号、姓名。
-- 方法1:使用YEAR()函数提取年份
SELECT id AS 学号, name AS 姓名
FROM student
WHERE YEAR(birth) = 1996;-- 方法2:使用日期范围(性能更优,支持索引)
SELECT id AS 学号, name AS 姓名
FROM student
WHERE birth BETWEEN '1996-01-01' AND '1996-12-31';
案例5:查询各学生的年龄(按当前日期计算)
需求:根据出生日期计算学生当前年龄,显示学号、姓名、年龄。
SELECT id AS 学号,name AS 姓名,TIMESTAMPDIFF(YEAR, birth, CURDATE()) AS 年龄 -- TIMESTAMPDIFF:计算两个日期的差值(年/月/日)
FROM student;
5.2 汇总查询(聚合函数)
常用聚合函数:SUM()
(求和)、COUNT()
(计数)、AVG()
(平均值)、MAX()
(最大值)、MIN()
(最小值)。
案例1:查询课程编号为“0002”(数学)的总成绩
需求:计算所有学生数学课程的分数总和。
SELECT course_id AS 课程号,'数学' AS 课程名称, -- 固定值显示,也可关联course表获取SUM(grade) AS 总成绩
FROM score
WHERE course_id = '0002';
案例2:查询选了课程的学生人数(去重)
需求:统计至少选修1门课程的学生数量(避免重复计数)。
SELECT COUNT(DISTINCT stu_id) AS 选课学生人数 -- DISTINCT:去重,确保1个学生只计数1次
FROM score;
案例3:查询所有课程成绩小于85分的学生的学号、姓名
需求:筛选所有课程成绩均低于85分的学生(无任何一门课≥85)。
SELECT s.id AS 学号,s.name AS 姓名
FROM student s
JOIN score sc ON s.id = sc.stu_id
GROUP BY s.id, s.name
HAVING MAX(sc.grade) < 85; -- HAVING:筛选分组后的结果,MAX(grade) <85表示所有成绩均<85
案例4:查询没有学全所有课程的学生的学号、姓名
需求:所有课程共4门(0001-0004),筛选选修课程数<4的学生。
SELECT s.id AS 学号,s.name AS 姓名,COUNT(sc.course_id) AS 已选课程数
FROM student s
LEFT JOIN score sc ON s.id = sc.stu_id -- LEFT JOIN:保留所有学生,包括没选课的(已选课程数为0)
GROUP BY s.id, s.name
HAVING COUNT(sc.course_id) < (SELECT COUNT(*) FROM course); -- 子查询获取总课程数
5.3 分组查询(GROUP BY)
分组查询用于按指定字段(如课程号、性别)对数据分组,再对每组进行聚合计算。
案例1:查询各科成绩最高和最低得分
需求:按课程分组,显示每门课程的最高分、最低分。
SELECT course_id AS 课程号,c.name AS 课程名称,MAX(grade) AS 最高分,MIN(grade) AS 最低分
FROM score sc
JOIN course c ON sc.course_id = c.id -- 关联course表获取课程名称
GROUP BY course_id, c.name -- GROUP BY需包含非聚合字段(course_id、c.name)
ORDER BY course_id; -- 按课程号升序排列结果
案例2:查询每门课程被选修的学生数
需求:统计每门课程的选课人数,按人数降序排列。
SELECT c.id AS 课程号,c.name AS 课程名称,COUNT(sc.stu_id) AS 选课人数
FROM course c
LEFT JOIN score sc ON c.id = sc.course_id -- LEFT JOIN:保留无学生选修的课程(选课人数为0)
GROUP BY c.id, c.name
ORDER BY 选课人数 DESC; -- 按人数降序,人数相同则按课程号升序(可加:, c.id ASC)
案例3:查询学生中男、女人数
需求:按性别分组,统计男生和女生的数量。
SELECT sex AS 性别,COUNT(id) AS 人数
FROM student
GROUP BY sex;
案例4:查询同名同姓学生名单并统计同名人数
需求:筛选存在重名的学生,显示姓名、同名人数。
-- 步骤1:先统计每个姓名的人数,筛选人数≥2的姓名
WITH name_count AS (SELECT name,COUNT(id) AS 同名人数FROM studentGROUP BY nameHAVING COUNT(id) ≥ 2
)
-- 步骤2:关联学生表,显示重名学生的完整信息
SELECT s.id AS 学号,s.name AS 姓名,s.sex AS 性别,nc.同名人数
FROM student s
JOIN name_count nc ON s.name = nc.name
ORDER BY s.name, s.id;
5.4 带条件的分组查询(HAVING)
HAVING
用于筛选分组后的结果(区别于WHERE
:筛选分组前的行),需配合GROUP BY
使用。
案例1:查询平均成绩大于60分学生的学号和平均成绩
需求:筛选所有课程平均分数超过60的学生,按平均成绩降序排列。
SELECT stu_id AS 学号,AVG(grade) AS 平均成绩 -- AVG(grade):计算该学生所有课程的平均分
FROM score
GROUP BY stu_id
HAVING AVG(grade) > 60 -- 筛选平均分>60的学生
ORDER BY 平均成绩 DESC;
案例2:查询至少选修两门课程的学生学号以及课程数目
需求:统计选修课程数≥2的学生,显示学号和课程数。
SELECT stu_id AS 学号,COUNT(course_id) AS 课程数目
FROM score
GROUP BY stu_id
HAVING COUNT(course_id) ≥ 2 -- 筛选课程数≥2的学生
ORDER BY 课程数目 DESC;
案例3:查询不及格的课程并按照课程号从大到小排序
需求:筛选成绩<60的课程记录,按课程号降序排列。
SELECT stu_id AS 学号,course_id AS 课程号,grade AS 成绩
FROM score
WHERE grade < 60 -- WHERE:先筛选不及格的行,再排序
ORDER BY course_id DESC;
案例4:查询两门以上成绩不满85分的同学的学号及其平均成绩
需求:筛选有≥3门课程成绩<85的学生,显示学号和平均成绩。
SELECT stu_id AS 学号,AVG(grade) AS 平均成绩,SUM(CASE WHEN grade < 85 THEN 1 ELSE 0 END) AS 不满85分的课程数 -- 统计<85分的课程数量
FROM score
GROUP BY stu_id
HAVING SUM(CASE WHEN grade < 85 THEN 1 ELSE 0 END) ≥ 3 -- 筛选课程数≥3的学生
ORDER BY 平均成绩 DESC;
5.5 多表查询(JOIN关联)
多表查询通过JOIN
关联多个表,获取跨表的组合数据,常用INNER JOIN
(内连接)、LEFT JOIN
(左连接)。
案例1:查询所有学生的学号、姓名、选课数、总成绩
需求:显示所有学生(包括没选课的)的选课数量和总成绩,没选课则为0。
SELECT s.id AS 学号,s.name AS 姓名,COUNT(sc.course_id) AS 选课数,IFNULL(SUM(sc.grade), 0) AS 总成绩 -- IFNULL:将NULL替换为0(没选课的学生总成绩为0)
FROM student s
LEFT JOIN score sc ON s.id = sc.stu_id
GROUP BY s.id, s.name
ORDER BY 总成绩 DESC;
案例2:查询平均成绩大于85分的所有学生的学号、姓名、平均成绩
需求:关联学生表和成绩表,筛选平均分>85的学生。
SELECT s.id AS 学号,s.name AS 姓名,ROUND(AVG(sc.grade), 2) AS 平均成绩 -- ROUND:保留2位小数,避免小数过长
FROM student s
JOIN score sc ON s.id = sc.stu_id -- INNER JOIN:只保留有成绩的学生
GROUP BY s.id, s.name
HAVING AVG(sc.grade) > 85
ORDER BY 平均成绩 DESC;
案例3:查询学生的选课情况:学号、姓名、课程号、课程名称
需求:显示学生的完整选课信息(跨4表:student、score、course)。
SELECT s.id AS 学号,s.name AS 姓名,c.id AS 课程号,c.name AS 课程名称,sc.grade AS 成绩
FROM student s
LEFT JOIN score sc ON s.id = sc.stu_id
LEFT JOIN course c ON sc.course_id = c.id
ORDER BY s.id, c.id; -- 按学号、课程号排序,结果更清晰
案例4:查询出每门课程的大于80分人数和不大于80分的人数
需求:按课程分组,统计每门课程“≥80分”和“<80分”的人数。
SELECT c.id AS 课程号,c.name AS 课程名称,SUM(CASE WHEN sc.grade > 80 THEN 1 ELSE 0 END) AS 大于80分人数,SUM(CASE WHEN sc.grade ≤ 80 THEN 1 ELSE 0 END) AS 不大于80分人数
FROM course c
LEFT JOIN score sc ON c.id = sc.course_id
GROUP BY c.id, c.name
ORDER BY c.id;
案例5:按分数段统计各科成绩([90,100]、[80,90)、[70,80)、[60,70)、<60)
需求:按课程和分数段分组,统计各分段的人数。
SELECT c.id AS 课程号,c.name AS 课程名称,-- CASE WHEN:将分数映射到对应分段CASE WHEN sc.grade BETWEEN 90 AND 100 THEN '[90,100]'WHEN sc.grade BETWEEN 80 AND 89 THEN '[80,90)'WHEN sc.grade BETWEEN 70 AND 79 THEN '[70,80)'WHEN sc.grade BETWEEN 60 AND 69 THEN '[60,70)'ELSE '<60'END AS 分数段,COUNT(sc.stu_id) AS 人数
FROM course c
LEFT JOIN score sc ON c.id = sc.course_id
GROUP BY c.id, c.name, 分数段 -- 按课程和分数段分组
ORDER BY c.id, 分数段 DESC; -- 分数段从高到低排序
案例6:数据的行列互换(将课程名转为列,显示学生各科成绩)
需求:将“学生-课程-成绩”的行数据,转为“学生-语文-数学-英语-物理”的列数据(适合报表展示)。
SELECT s.id AS 学号,s.name AS 姓名,-- 按课程名匹配成绩,MAX()消除NULL(每个学生1门课只有1个成绩)MAX(CASE WHEN c.name = '语文' THEN sc.grade END) AS 语文,MAX(CASE WHEN c.name = '数学' THEN sc.grade END) AS 数学,MAX(CASE WHEN c.name = '英语' THEN sc.grade END) AS 英语,MAX(CASE WHEN c.name = '物理' THEN sc.grade END) AS 物理
FROM student s
LEFT JOIN score sc ON s.id = sc.stu_id
LEFT JOIN course c ON sc.course_id = c.id
GROUP BY s.id, s.name
ORDER BY s.id;-- 补充:MySQL 8.0+支持PIVOT函数(更简洁)
SELECT *
FROM (SELECT s.id AS 学号, s.name AS 姓名, c.name AS 课程名, sc.grade AS 成绩FROM student sLEFT JOIN score sc ON s.id = sc.stu_idLEFT JOIN course c ON sc.course_id = c.id
) AS source_table
PIVOT (MAX(成绩) FOR 课程名 IN ('语文', '数学', '英语', '物理') -- 课程名转为列
) AS pivot_table
ORDER BY 学号;
5.6 多表连接查询(复杂关联)
案例1:查询课程号为“0001”(语文)且分数小于90的学生信息,按分数降序排列
需求:显示语文成绩<90的学生完整信息(关联student和score表)。
SELECT s.id AS 学号,s.name AS 姓名,s.sex AS 性别,sc.grade AS 语文成绩
FROM student s
JOIN score sc ON s.id = sc.stu_id
WHERE sc.course_id = '0001' AND sc.grade < 90
ORDER BY sc.grade DESC;
案例2:查询不同老师所教的不同课程的平均分,从高到低显示
需求:按教师和课程分组,计算每门课程的平均分(关联4表)。
SELECT t.id AS 教师号,t.name AS 教师姓名,c.id AS 课程号,c.name AS 课程名称,ROUND(AVG(sc.grade), 2) AS 平均成绩
FROM teacher t
JOIN course c ON t.id = c.teacher_id
LEFT JOIN score sc ON c.id = sc.course_id
GROUP BY t.id, t.name, c.id, c.name
ORDER BY 平均成绩 DESC;
案例3:查询课程名称为“数学”且分数低于90的学生姓名和分数
需求:筛选数学成绩<90的学生(关联3表)。
SELECT s.name AS 学生姓名,sc.grade AS 数学成绩
FROM student s
JOIN score sc ON s.id = sc.stu_id
JOIN course c ON sc.course_id = c.id
WHERE c.name = '数学' AND sc.grade < 90
ORDER BY sc.grade DESC;
案例4:查询学过“陈独秀”老师所教的所有课的同学的学号、姓名
需求:陈独秀教数学(0002),筛选选修了数学的学生。
-- 方法1:子查询获取陈独秀的课程号
SELECT s.id AS 学号,s.name AS 姓名
FROM student s
JOIN score sc ON s.id = sc.stu_id
WHERE sc.course_id = (SELECT c.id FROM course cJOIN teacher t ON c.teacher_id = t.idWHERE t.name = '陈独秀'
)
GROUP BY s.id, s.name;-- 方法2:多表直接关联
SELECT DISTINCT s.id AS 学号, -- DISTINCT:避免学生重复(若1门课多次选课)s.name AS 姓名
FROM student s
JOIN score sc ON s.id = sc.stu_id
JOIN course c ON sc.course_id = c.id
JOIN teacher t ON c.teacher_id = t.id
WHERE t.name = '陈独秀';
案例5:查询至少有一门课与学号为“0001”的学生所学课程相同的学生的学号和姓名
需求:筛选与“曹喜嗯(0001)”有共同课程的学生(排除0001本人)。
SELECT DISTINCT s.id AS 学号,s.name AS 姓名
FROM student s
JOIN score sc ON s.id = sc.stu_id
-- 子查询获取0001学生的所有课程号
WHERE sc.course_id IN (SELECT course_id FROM score WHERE stu_id = '0001')AND s.id != '0001' -- 排除0001本人
ORDER BY s.id;
5.7 窗口函数(MySQL 8.0+)
窗口函数用于“分组内排序”或“分组内聚合”,无需压缩行(区别于GROUP BY
),常用函数:RANK()
(排名,跳号)、DENSE_RANK()
(排名,不跳号)、ROW_NUMBER()
(行号)、AVG() OVER()
(分组内平均值)。
案例1:查询学生平均成绩及其名次
需求:计算每个学生的平均成绩,并按平均分降序排名(名次跳号)。
SELECT s.id AS 学号,s.name AS 姓名,ROUND(AVG(sc.grade), 2) AS 平均成绩,-- RANK():按平均成绩降序排名,相同分数名次相同,后续名次跳号RANK() OVER (ORDER BY AVG(sc.grade) DESC) AS 名次
FROM student s
JOIN score sc ON s.id = sc.stu_id
GROUP BY s.id, s.name
ORDER BY 名次;
案例2:按照各科成绩进行排序,并显示排名(不跳号)
需求:按课程分组,对每门课程的成绩降序排名(相同分数名次相同,后续名次不跳号)。
SELECT c.name AS 课程名称,s.name AS 学生姓名,sc.grade AS 成绩,-- DENSE_RANK():分组内排名,不跳号;PARTITION BY:按课程分组DENSE_RANK() OVER (PARTITION BY sc.course_id ORDER BY sc.grade DESC) AS 课程内排名
FROM score sc
JOIN student s ON sc.stu_id = s.id
JOIN course c ON sc.course_id = c.id
ORDER BY sc.course_id, 课程内排名;
案例3:查询每门成绩最好的前两名学生的姓名
需求:按课程分组,筛选每门课程成绩排名前2的学生。
-- 步骤1:先对每门课程的成绩排名
WITH course_rank AS (SELECT sc.course_id,s.id AS stu_id,s.name AS stu_name,sc.grade,ROW_NUMBER() OVER (PARTITION BY sc.course_id ORDER BY sc.grade DESC) AS rnFROM score scJOIN student s ON sc.stu_id = s.id
)
-- 步骤2:筛选排名≤2的记录
SELECT c.name AS 课程名称,cr.stu_name AS 学生姓名,cr.grade AS 成绩,cr.rn AS 排名
FROM course_rank cr
JOIN course c ON cr.course_id = c.id
WHERE cr.rn ≤ 2
ORDER BY c.id, cr.rn;
案例4:按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩
需求:显示每个学生的每门课程成绩,并附带该学生的平均成绩(无需分组压缩行)。
SELECT s.id AS 学号,s.name AS 姓名,c.name AS 课程名称,sc.grade AS 课程成绩,-- AVG() OVER():按学生分组,计算该学生的平均成绩,不压缩行ROUND(AVG(sc.grade) OVER (PARTITION BY s.id), 2) AS 学生平均成绩
FROM student s
JOIN score sc ON s.id = sc.stu_id
JOIN course c ON sc.course_id = c.id
-- 按学生平均成绩降序、课程成绩降序排序
ORDER BY 学生平均成绩 DESC, sc.grade DESC;
六、经典面试题:查找A表存在而B表不存在的ID
面试高频题:给定A表(如student
)和B表(如score
),查找“在A表中存在,但在B表中不存在”的ID(如“没选课的学生ID”)。以下提供3种常用方法,附优缺点分析。
方法1:LEFT JOIN + IS NULL(兼容性最好)
-- 需求:查找没选课的学生ID和姓名(student存在,score不存在)
SELECT s.id AS 学号,s.name AS 姓名
FROM student s
-- LEFT JOIN:保留student所有记录,score匹配不到的记录字段为NULL
LEFT JOIN score sc ON s.id = sc.stu_id
WHERE sc.stu_id IS NULL; -- 筛选score中stu_id为NULL的记录(没选课)
优点:兼容性好(支持所有MySQL版本),性能稳定;
缺点:若B表数据量大,需确保关联字段(stu_id
)有索引。
方法2:NOT IN(简洁,但需注意NULL)
SELECT id AS 学号,name AS 姓名
FROM student
-- NOT IN:筛选ID不在score.stu_id中的学生
WHERE id NOT IN (SELECT DISTINCT stu_id FROM score);
优点:语法简洁,易理解;
缺点:若子查询(SELECT stu_id FROM score
)结果包含NULL
,则整个查询返回空(NOT IN
对NULL
不兼容),需确保子查询无NULL
。
方法3:NOT EXISTS(性能最优,推荐)
SELECT s.id AS 学号,s.name AS 姓名
FROM student s
-- NOT EXISTS:判断“不存在匹配的score记录”,即没选课
WHERE NOT EXISTS (SELECT 1 -- 用1代替*,减少字段读取,性能更优FROM score scWHERE sc.stu_id = s.id
);
优点:性能最优(MySQL对EXISTS
优化较好,无需去重),对NULL
兼容;
缺点:语法相对复杂,初学者需理解子查询逻辑。
七、总结
本教程通过“数据准备→实战案例→面试题”的结构,覆盖了MySQL SQL的核心场景:
- 基础操作:建表(DDL)、插入数据(DML)、单表查询(DQL);
- 核心技能:聚合函数、分组(
GROUP BY
)、筛选(HAVING
)、多表关联(JOIN
); - 进阶功能:窗口函数(排名、分组聚合)、行列互换、子查询;
- 面试高频:A表存在而B表不存在的ID查询(3种方法)。
建议读者:
- 复制代码到MySQL客户端实际执行,观察结果;
- 修改案例中的条件(如分数段、课程号),举一反三;
- 尝试添加外键约束、索引,优化查询性能(进阶学习)。
通过反复练习,可快速掌握SQL实战技能,应对日常开发和面试需求!