资讯动态

MySQL SQL练习题详解:从基础查询到窗口函数与面试真题

发布时间:2026/10/5 3:29:14 来源:尧图企业网站定制
MySQL学得好不好不看教材翻了几遍只看你对着一个数据库窗口能不能把SQL写出来。这句话我反复跟身边学数据库的朋友念叨过。很多人问怎么练SQL我的回答永远只有一个字刷。但刷题不是瞎刷更不是背答案。这篇MySQL SQL练习题详解把我从入门、面试到日常开发里反复踩过的SQL知识点重新按难度整理了一遍每道题拆了思路、报了坑也补上了答案解析背后的为什么。零基础的人可以按顺序一道一道啃工作几年想查漏补缺的可以跳过基础直接看你薄弱的那章准备数据库面试的第五部分的真题和报错速查表强烈建议重点过一遍。1. 这套SQL练习题的设计思路与刷题路径1.1 为什么我不建议只用眼睛学SQLSQL是一门典型的手上功夫跟学骑自行车一样看再多教程不跨上车永远学不会。我见过太多人教材翻得滚瓜烂熟一到命令行就抓瞎不是忘了加WHERE就是搞不清LEFT JOIN和INNER JOIN的区别。原因很简单SQL的语法只是表层真正的难点在于把业务问题翻译成查询逻辑这种翻译能力只能靠大量练习喂出来。这套练习题按五层递进设计基础查询、聚合分组、多表连接、窗口函数与事务、面试真题。每一层都挂在业务场景下而不是拿表A、表B这种抽象例子糊弄人。为什么这么做因为实际工作中没有人会问你用SELECT查一下表而是帮我查一下最近30天哪个城市的复购率最高。脑子里能把SQL和业务问题画上等号你才算真的会了。1.2 刷题环境的搭建Docker和本地安装选哪个刷题之前环境必须准备好。我自己最推荐Docker干净且切换版本方便docker run --name mysql-study -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8.0这条命令会拉取MySQL 8.0镜像并启动容器。如果拉镜像总是失败多半是网络原因配置一下镜像加速源再重试。不想用Docker的Windows用户直接下载MySQL安装包选Developer Default一路Next注意设置root密码那一步别跳过Linux用户可以用发行版的包管理器安装软件源里的版本通常够用。提示练习题默认在MySQL 8.0上运行因为窗口函数要8.0才完整支持。用5.7的话4.1的小节会跑不起来建议统一用8.0。1.3 练习方式先手写再验证最后改场景刷题有个笨但极有效的顺序先别急着查答案拿起纸笔写出你脑子里的SQL再去终端里执行验证跑通了再试着改改条件比如把查询北京用户换成查询上海用户看看你的SQL是不是还成立。这一套下来一道简单题能榨出三四种写法比闷头做二十道题都强。另外建议早期别依赖图形化工具就在命令行里敲SQL报错信息看得多了你对语法的敏感度会明显上一个台阶。2. 基础查询练习题精解CRUD、去重、排序与条件筛选2.1 建表与基础CRUD练习先建一张学生表后面几道题都在它上面做CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT, city VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );练习题1往表里插入三条学生数据分别来自上海、北京、广州。这道题考的是INSERT基础INSERT INTO student (name, age, city) VALUES (张三, 22, 上海), (李四, 23, 北京), (王五, 21, 广州);练习题2把id等于3的学生年龄改成20。练习题3删除年龄小于21的学生。这两道题看着不值一提但我要多说一句UPDATE和DELETE之前先SELECT一遍确认WHERE条件。谁都干过失手删全表的蠢事练习环境无所谓养成这个习惯以后能救你一命。2.2 条件查询WHERE组合条件的经典坑练习题查询城市为上海且年龄大于20的学生。SELECT * FROM student WHERE city 上海 AND age 20;这题本身不难但它背后藏着初学者必踩的三个坑。第一字符串必须加引号不加引号MySQL会把它当字段名解析直接报Unknown column。第二AND和OR混用时不加括号逻辑就乱了。比如WHERE city上海 OR city北京 AND age20因为AND优先级高于OR实际执行的是上海的所有人加上北京且大于20的人和你想的完全不是一回事。第三字段名不要加引号WHERE age 20会把age当成字符串常量而不是列结果全是0。2.3 去重查询DISTINCT和GROUP BY怎么选练习题查出学生都来自哪些城市去掉重复值。SELECT DISTINCT city FROM student;DISTINCT是最直接的去重方式适合只看有哪些值的场景。但两个细节要注意一是DISTINCT对NULL不去重多个NULL会被合并成一个这在统计上可能有问题二是当你想要按某个字段去重同时取出其他字段的时候DISTINCT经常无能为力它只能对整行组合去重。这时候就得靠GROUP BY或者窗口函数。面试里有个高频追问就是DISTINCT和GROUP BY去重有什么区别最根本的差异是DISTINCT把去重结果当成一个整体返回而GROUP BY是先把数据分组再对每组做聚合表达能力完全不同。2.4 排序与分页NULL排在哪你可能从没注意过练习题学生按年龄从大到小排序年龄相同的按id从小到大排。SELECT name, age FROM student ORDER BY age DESC, id ASC;排序有三件小事写错一个结果就歪了。一是多字段排序要分别写方向ORDER BY age DESC, id ASC很多人习惯一个DESC管所有字段结果id也倒序了。二是NULL的顺序MySQL默认升序时NULL排在最前面如果你想让NULL排最后得写成ORDER BY age IS NULL, age。三是分页的写法LIMIT 10 OFFSET 20表示跳过20行取10行简写LIMIT 10, 20里第一个是数量、第二个是偏移量这个顺序全世界都在搞混我自己也写反过。3. 进阶查询练习题精解聚合、分组、多表连接与子查询3.1 聚合函数加GROUP BY统计类题目的核心骨架现在换一套业务表订单表ordersid, customer_id, amount, order_date。练习题统计每个客户的订单数量和订单总金额。SELECT customer_id, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM orders GROUP BY customer_id;这道题奠定了所有统计类SQL的基本骨架先限定范围WHERE再分组GROUP BY再聚合COUNT/SUM最后用HAVING过滤组。很多人会写WHERE COUNT(*) 5MySQL直接报错因为聚合函数只能在HAVING或者SELECT里用不能进WHERE。WHERE是对分组前的原始行过滤HAVING是对分组后的每组过滤两者执行顺序完全不同。顺带说一个高频细节COUNT(*)统计的是行数COUNT(amount)统计的是amount非NULL的行数。如果某行amount是NULL这两个结果就不一样。统计有多少个客户下过单可以用COUNT(DISTINCT customer_id)这个写法面试里出现频率极高。3.2 JOIN系列INNER JOIN和LEFT JOIN怎么选练习题查出所有客户的下单总额没下过单的客户也要出现在结果里金额显示为0。SELECT c.id, c.name, IFNULL(SUM(o.amount), 0) AS total_amount FROM customers c LEFT JOIN orders o ON c.id o.customer_id GROUP BY c.id, c.name;这题的关键是选LEFT JOIN而不是INNER JOIN。INNER JOIN只保留两边都匹配的行没下过单的客户会被直接过滤掉LEFT JOIN保留左表全部行右边没有匹配就补NULL配合IFNULL把NULL转成0才算完整回答所有客户这个要求。初学者最常犯的错就是分不清查所有客户和查有订单的客户多写几次JOIN自然就懂了。JOIN还有一个隐藏坑关联字段类型不一致。一边是INT一边是VARCHARMySQL会做隐式转换转换后索引大概率失效数据量一大查询立刻变慢。建表时外键字段的类型一定要对齐这个习惯从练习题阶段就值得养起来。3.3 经典子查询为什么我更推荐NOT EXISTS练习题查出从未下过单的客户。这题子查询写法很多我推荐用NOT EXISTSSELECT c.* FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id c.id );为什么不推荐NOT IN因为如果子查询结果里包含一个NULL整个NOT IN (1, 2, NULL)的判断就会变成既不是1也不是2还不是NULL结果为空一行都查不出来。这是SQL里最阴的坑没有之一。EXISTS走的是存在性判断碰到NULL也不受影响所以凡是查没有关联记录的这类需求我默认用NOT EXISTS。面试官如果追问IN和EXISTS哪个快一句数据量小时差别不大但EXISTS对NULL更安全就能体现你踩过坑。3.4 每个部门工资最高的员工真题的两种解法员工表empemp_id, name, dept_id, salary。求每个部门工资最高的员工这题是面试常客。先看最直观的子查询解法SELECT e.name, e.dept_id, e.salary FROM emp e WHERE (e.dept_id, e.salary) IN ( SELECT dept_id, MAX(salary) FROM emp GROUP BY dept_id );这种写法好懂但如果同一个部门有两个并列最高工资两行都会查出来。如果业务上只想取一个人就得换窗口函数SELECT name, dept_id, salary FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM emp ) t WHERE t.rn 1;子查询写法讲的是哪个工资最高窗口函数写法的逻辑是每个部门按工资排序后取第一名表达能力更强。能在面试里给出两种解法并说清差异是明显的加分项。4. 高级SQL练习题精解窗口函数、事务与存储过程4.1 窗口函数RANK系列三兄弟怎么区分MySQL 8.0引入窗口函数之后很多以前要绕半天自连接的场景都被简化了。排名三大函数的区别先用一张表说清楚函数行为1、2、2、3四个分数排名结果ROW_NUMBER()按顺序编号没有并列1,2,3,4RANK()并列跳过下一个名次1,2,2,4DENSE_RANK()并列不跳过名次1,2,2,3练习题给成绩表scorestudent_id, subject, score里的学生按分数排名分数相同名次一样且下一名不跳号这时应该用DENSE_RANKSELECT student_id, score, DENSE_RANK() OVER (ORDER BY score DESC) AS rk FROM score;窗口函数最大的坑是它不能直接写在WHERE里。想筛排名前3必须先包一层子查询SELECT * FROM ( SELECT student_id, score, DENSE_RANK() OVER (ORDER BY score DESC) AS rk FROM score ) t WHERE t.rk 3;这个窗口函数子查询的组合拳在分组取TopN类题目里几乎天天用值得多练几遍形成肌肉记忆。4.2 连续登录3天一个用到DATE_SUB的经典套路假设有用户登录表loginuser_id, login_date求连续登录3天及以上的用户。这是面试题里的老演员解法核心是日期减行号WITH t AS ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp FROM login ) SELECT user_id FROM t GROUP BY user_id, grp HAVING COUNT(*) 3;原理不复杂同一个用户连续登录的日期减去它对应的行号后会得到一个固定不变的基准日期一旦日期断了减去行号后的值就跳走了。把连续的日子归到同一个grp里再统计每组的天数就能筛出连续3天以上的用户。这个思路面试官很爱考但光看答案记不住建议自己造几天数据跑一遍体会一下断档是怎么体现出来的。4.3 事务练习题转账为什么不能只成功一半事务处理是SQL里最容易自以为懂了的知识点。练习题场景账户表accountid, user_name, balance模拟转账100元。START TRANSACTION; UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; COMMIT;如果在两条UPDATE之间故意写错一条然后执行ROLLBACK你会发现两条更新都被回滚两边余额不变。这就是事务的原子性。我建议你亲手做个小实验开两个数据库会话在一个会话里执行UPDATE但不COMMIT然后去另一个会话查同一个账户你会看到的还是旧余额——这就是隔离级别在起作用。这种事情不亲自跑一遍光背ACID四个字母永远理解不到实处。4.4 存储过程练习题批量生成测试数据存储过程在面试题里出现频率不算高但日常开发里很有用特别是批量造数据。比如要生成1000条测试订单DELIMITER // CREATE PROCEDURE generate_orders() BEGIN DECLARE i INT DEFAULT 1; WHILE i 1000 DO INSERT INTO orders (customer_id, amount, order_date) VALUES (i % 100 1, RAND() * 1000, DATE_SUB(NOW(), INTERVAL i DAY)); SET i i 1; END WHILE; END// DELIMITER ;写存储过程有两个高频坑。第一DELIMITER没改导致客户端把分号当作语句结束整个CREATE PROCEDURE被拆成好几段直接语法报错。第二循环里忘了给i加增量直接死循环。我当年练习时真跑过一整晚没停的存储过程第二天才发现表里多了几万行。建议练习时先加个COUNT统计或者LIMIT限制次数。5. 面试级SQL练习题经典真题与高频易错点5.1 去重取最新一条GROUP BY和窗口函数的高下之争场景订单表中有重复的订单号每个订单号可能有多条状态变更记录现在要取每个订单最新的一条。这种去重取最新是面试高频题。先看GROUP BY解法SELECT * FROM orders WHERE id IN (SELECT MAX(id) FROM orders GROUP BY order_no);再看窗口函数解法SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY order_no ORDER BY id DESC) AS rn FROM orders ) t WHERE t.rn 1;两种写法都能实现但窗口函数方案更灵活——如果最新不是按id最大而是按某个状态顺序或者时间字段窗口函数只要改ORDER BY即可GROUP BY方案就得重新设计聚合条件。面试时能主动讲出这个对比说明你真的理解两种方案的适用场景而不是背了一个答案。5.2 行转列CASE WHEN加分组聚合的经典套路成绩表scorestudent_id, subject, score每个学生有三科成绩现在要把三科转成一行三列。SELECT student_id, MAX(CASE WHEN subject math THEN score END) AS math_score, MAX(CASE WHEN subject english THEN score END) AS english_score, MAX(CASE WHEN subject chinese THEN score END) AS chinese_score FROM score GROUP BY student_id;这道题的核心是为什么外面要套MAX。GROUP BY之后每个学生只剩一行但某个学生的math成绩只存在于math那行数据里在english那行里CASE表达式得到的就是NULL。MAX的作用是把这一组里的非NULL值提取出来。不加MAX结果里全是NULL我第一次自己写的时候就在这卡了半天。理解分组加条件聚合这个套路行转列、条件计数这类题目都能通吃。5.3 第二高薪水LIMIT偏移和IFNULL缺一不可员工表employeesalary。求第二高薪水如果不存在就返回NULL。SELECT IFNULL( (SELECT DISTINCT salary FROM employee ORDER BY salary DESC LIMIT 1 OFFSET 1), NULL );这题有两个易错点。第一不加DISTINCT当两个人薪资相同且恰好排在第二三名时取到的第二高可能不对。第二不加IFNULL当表里只有一条记录时子查询返回空集合整个结果就是空行而不是NULL面试官常在这个边界条件上埋伏笔。补充一句OFFSET的数字是跳过多少行不是取多少行用错的人不在少数。5.4 面试高频报错速查表报错信息出错原因解决办法this is incompatible with sql_modeonly_full_group_bySELECT的列不在GROUP BY中把列加入GROUP BY或用聚合函数包裹Column xxx in field list is ambiguous多表连接中列名两边都有写表别名前缀如o.xxxUnknown column xxx in where clauseWHERE里用了SELECT别名WHERE不能识别别名改用原列名或包子查询Lock wait timeout exceeded事务未提交导致锁等待超时找到并提交/回滚未完成事务或调大锁等待时间Expression #1 of SELECT list is not in GROUP BY clauseGROUP BY写得不全把SELECT中非聚合列补进GROUP BY这份速查表基本覆盖了面试和日常开发中九成的报错场景。我的建议是把表格存下来每次报错对照着查一遍慢慢就能形成条件反射。5.5 面试现场写出高质量SQL的习惯面试让你手写SQL的时候先别急着落笔。在脑里或纸上理出结构顺序先想清楚要过滤哪些行WHERE要不要分组GROUP BY要不要在分组后过滤HAVING最后排序分页ORDER BY、LIMIT。这个顺序就是SQL实际的执行顺序按着它写你的思路会非常清晰面试官也更容易看懂你的逻辑。写完可以补一句这里如果给查询条件加上索引扫描行数会明显下降这句话比把SQL写得再漂亮都更能体现实战经验。6. 从刷题到实战SQL优化与疑难排查6.1 养成先看EXPLAIN的习惯刷题别只满足于跑出结果写完顺手在SQL前面加个EXPLAIN你会看到MySQL是怎么执行这条查询的。重点看三列type访问类型从好到差通常是system、const、eq_ref、ref、range、index、ALL。出现ALL就是全表扫描数据量一大就危险。key实际命中的索引NULL代表没用上索引。rows预估扫描行数。对比不同写法的rows就能直观看出哪个写法更高效。我就是靠这个习惯避开了无数次线上慢查询。很多题目用哪种写法更好EXPLAIN一眼就能给出答案比争论哪种写法性能好靠谱得多。6.2 最常见的慢SQL原因慢SQL的原因翻来覆去就那几个。一是SELECT *把用不上的大字段全捞回来白白增加网络和内存开销我只会在练习时用星号线上永远显式列出字段。二是对索引列做函数运算比如WHERE YEAR(create_time)2024MySQL没法直接利用create_time上的索引改成create_time 2024-01-01 AND create_time 2025-01-01索引就生效了。三是模糊查询前置通配符LIKE %张三同样让索引失效后置通配符张三%则没问题。这三类问题是最常见的优化切入点。6.3 刷题环境的疑难排查实录刷题过程中卡你的往往不是SQL本身而是环境问题。我自己碰过太多次随手整理几条高频的第一Docker容器起不来。先查端口占用比如3306端口被旧安装的MySQL占着把容器端口映射改成-p 3307:3306再启动。第二连接报SSL相关错误。测试环境可以在连接参数里加useSSLfalseallowPublicKeyRetrievaltrue绕开但生产环境不要这么干该配证书就配证书。第三MySQL 8.0换了默认认证插件caching_sha2_password老客户端连不上时会报认证方式不兼容升级客户端或用配套驱动即可。第四改了配置文件后服务起不来先别瞎猜去看错误日志。用SHOW VARIABLES LIKE log_error找到日志位置日志里写的错误原因比任何排查教程都准确。6.4 刷完这些题之后的提升方向如果你已经能把前面五章独立做出来说明基础已经扎实了。下一步我建议把手头的练习题改造成真实压力场景给表加上索引重新跑一遍同样的查询对比EXPLAIN的变化或者用存储过程把数据灌到几十万行亲眼看一看哪些写法在数据量变大后开始变慢。SQL的学习没有终点刷题只是让肌肉记忆成型真正的功力是在一次次慢查询优化和线上故障排查里长出来的。我把这套题自己重新刷了一遍最大的体会是SQL这玩意儿看答案永远觉得简单合上书自己写就漏洞百出。每个报错速查表里的坑都是我或身边同事在实际操作中踩过的写出来是希望你别再花同样的时间。你拿题先自己写写不出来再看解析看完一定亲手跑一遍再想一想如果换一个业务场景这条SQL还成立吗。把这套题刷扎实应付日常工作开发和绝大多数面试已经够用了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑