MySQL 50题:一张覆盖SQL能力全栈的成长地图

发布时间:2026/9/17 13:41:26
MySQL 50题:一张覆盖SQL能力全栈的成长地图 1. 这不是题库而是一张MySQL能力成长地图“MySQL 经典练习 50 题完美解答版”——看到这个标题我第一反应不是点开做题而是把它当成一张被反复验证过的MySQL能力体检表。它不教你怎么安装MySQL也不讲InnoDB的Buffer Pool怎么刷脏页但它用50个层层递进的真实业务场景把一个DBA、后端工程师或数据分析岗从“会写SELECT”到“能设计高可用数据模型”的完整能力链一题一题地拆解、暴露、锤炼。你做的不是50道题而是50次对SQL思维的校准什么时候该用JOIN而不是子查询为什么GROUP BY后面不能随便选字段ORDER BY和LIMIT合用时为什么加索引有时反而变慢这些答案不在官方文档的角落里而在你执行EXPLAIN后那几行type: ALL的红色警告里。这50题之所以“经典”是因为它绕开了所有花哨的新特性死死咬住MySQL最核心的三块基石单表查询的精准控制、多表关联的逻辑严谨性、聚合统计的边界意识。比如第12题“查询平均成绩大于85的所有学生的学号、姓名和平均成绩”表面是GROUP BYHAVING实则在考你是否理解WHERE和HAVING的本质区别——前者过滤行后者过滤组再比如第34题“查询所有课程成绩小于60分的学生姓名与课程名”看似简单但如果你直接写WHERE score 60就会漏掉那些一门课都没考的学生必须用LEFT JOIN配合IS NULL来兜底。这种“坑”只有在真实业务中被线上告警打醒过的人才懂它有多痛。所以这份“完美解答版”的价值不在于答案本身有多漂亮而在于它把每一道题背后隐含的业务约束、数据质量陷阱、性能临界点都摊开来讲。它适合三类人刚学完基础语法、想验证自己到底掌握没的新人准备技术面试、需要快速梳理MySQL知识脉络的求职者还有像我这样干了八年数据库运维的老兵——每次重做第47题“查询各科成绩最高分的学生信息”我依然会停下来重新手写一遍窗口函数和自连接两种解法对比执行计划里的Using filesort和Using temporary因为生产环境里一个排序算法的选择可能就是秒级响应和分钟级卡顿的分水岭。2. 题目设计逻辑与能力映射关系深度拆解2.1 五层能力阶梯从语法搬运工到数据架构师这50题绝非随机堆砌它严格遵循一条由浅入深的能力跃迁路径我把它们压缩成一张可执行的“MySQL能力坐标图”。这张图不是理论模型而是我在给团队新人做SQL考核时真正用来划线的标尺。Level 1单表CRUD肌肉记忆题号1-10核心目标让SELECT、INSERT、UPDATE、DELETE成为条件反射。重点不是“会不会”而是“写得够不够干净”。比如第3题“查询姓‘李’的学生信息”新手常写name LIKE 李%但老手会立刻补上COLLATE utf8mb4_unicode_ci——因为线上库字符集可能是utf8mb4而默认排序规则下李和裏可能被当成同一个字。再如第7题“修改学生年龄”很多人直接UPDATE student SET age age 1却忘了加WHERE条件结果全表学生集体长一岁。这十道题本质是在训练防御性SQL编写习惯。Level 2多表关联逻辑闭环题号11-25这是区分“会SQL”和“懂数据”的分水岭。第15题“查询选修了‘数学’课程的学生姓名”表面是JOIN实则在考你是否理解外键约束的语义完整性。如果sc表里没有外键指向course表那么当course表删除“数学”课程后sc表里仍存着一堆指向不存在课程的记录此时用INNER JOIN会漏数据必须用LEFT JOINIS NOT NULL来兜底。第22题“查询至少选修两门课程的学生学号”很多人用COUNT(*) 2但忽略了NULL值干扰正确解法必须先GROUP BY再HAVING COUNT(course_id) 2。这一阶段每道题都在逼你回答“这张表的数据到底代表什么业务事实”Level 3聚合统计的边界意识题号26-35这里开始出现大量“看起来对、跑出来错”的陷阱。第28题“查询每个学生的平均成绩”新手直接SELECT sname, AVG(score) FROM student JOIN sc... GROUP BY sname但若某学生没选课AVG()返回NULL而GROUP BY会把他从结果集里踢出去。正确做法是先用LEFT JOIN确保学生全量再用COALESCE(AVG(score), 0)兜底。第33题“查询各科平均成绩的最高分”表面是嵌套查询实则在考你是否理解聚合函数不能直接用于WHERE子句——你必须用子查询或窗口函数。这一层错误答案往往能跑通但数据结果是错的这才是最危险的。Level 4复杂业务建模能力题号36-45题目开始模拟真实系统压力。第38题“查询选修了全部课程的学生”这是经典的“关系除法”问题。用NOT EXISTS双重否定写法代码短但难懂用GROUP BY HAVING COUNT(DISTINCT course_id) (SELECT COUNT(*) FROM course)更直观但要注意DISTINCT在大数据量下的性能损耗。第42题“查询每门课程成绩最好的前两名学生”必须用窗口函数ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC)否则用自连接会因成绩相同导致排名错乱。这一阶段你写的不是SQL而是对业务规则的精确翻译。Level 5高阶诊断与优化直觉题号46-50最后五题是压轴专治“只会写、不会调”。第48题“查询连续三天登录的用户”表面是日期计算实则在考你是否掌握变量赋值技巧prev_date : curr_date和会话变量的生命周期。第50题“查询各部门工资排名前三的员工”要求同时处理部门内排名和跨部门并列必须用DENSE_RANK()而非ROW_NUMBER()否则并列第三名之后会跳到第五名。做对这五题不代表你能写多炫酷的SQL但意味着你已经建立起一套性能敏感型思维模式看到任何查询第一反应不是“怎么写”而是“执行计划会是什么样索引能不能用上临时表会不会生成”2.2 “完美解答”的底层逻辑为什么这个答案算“完美”所谓“完美解答”不是指代码最短或最炫技而是满足四个硬性标准可读性、健壮性、可维护性、可扩展性。我拿第19题“查询总成绩最高的学生姓名”来拆解常见错误答案SELECT sname FROM student WHERE sid (SELECT sid FROM sc GROUP BY sid ORDER BY SUM(score) DESC LIMIT 1);表面看没问题但存在三个致命缺陷子查询无索引支持GROUP BY sid在sc表上若无sid索引全表扫描SUM(score)无法利用索引必须计算每行若有多个学生总分并列最高LIMIT 1只返回一个业务逻辑崩塌。“完美解答”版本SELECT s.sname FROM student s INNER JOIN ( SELECT sid, SUM(score) as total_score FROM sc GROUP BY sid HAVING total_score ( SELECT MAX(total_score) FROM (SELECT SUM(score) as total_score FROM sc GROUP BY sid) t ) ) t ON s.sid t.sid;这个解法的“完美”体现在可读性用子查询明确表达“先算总分→再找最大值→最后关联学生”业务意图一目了然健壮性HAVING确保返回所有并列最高分的学生不丢数据可维护性若需求变为“总分前五”只需改MAX()为ORDER BY total_score DESC LIMIT 5逻辑清晰可扩展性sc表上建联合索引(sid, score)后GROUP BY sid可走索引SUM(score)也能利用覆盖索引避免回表。提示很多教程把“完美”等同于“用窗口函数”但MySQL 5.7及以下版本不支持窗口函数。真正的“完美”是在目标环境约束下选择最稳妥、最易懂、最易调的方案。这也是为什么我在生产环境里宁可用多几行的NOT EXISTS也不碰WITH RECURSIVE——前者兼容性好后者一旦出错排查成本翻倍。2.3 题目背后的业务场景还原每一题都是一个微型系统这50题的精妙之处在于它把抽象语法还原成了具体业务动作。我们不做“假想题”直接看真实场景映射题号真实业务场景数据质量风险点性能关键点第5题查询没学过‘数学’的学生教务系统退课名单生成sc表可能缺失记录学生未选课需用LEFT JOINIS NULL识别“零记录”状态course表小sc表大驱动表选student更优第14题查询平均成绩大于80的课程名教研室课程评估报告score字段可能为NULLAVG()会自动忽略但业务上NULL应计为0分GROUP BY字段必须是course_id若用cname需确保唯一性否则聚合错乱第29题查询选修课程数最多的同学学生综合素质评价同一学生同一课程可能有多条记录重修需COUNT(DISTINCT course_id)去重sc表上(sid, course_id)联合索引可加速去重计数第37题查询各科成绩排名年级成绩红榜公示成绩相同需并列排名如两个95分都是第1名必须用DENSE_RANK()窗口函数OVER(PARTITION BY course_id ORDER BY score DESC)需MySQL 8.0低版本需变量模拟你会发现所有“坑”都来自真实世界数据不干净、业务规则模糊、历史包袱沉重。做题时如果只盯着语法就永远跨不过从“学习者”到“解决者”的门槛。我带过的实习生第一次独立处理线上慢查询就是卡在第41题“查询每门课的最高分及对应学生”——他写了自连接但没加AND s1.score s2.score的等值条件结果笛卡尔积爆炸查了20分钟。后来我让他把题目抄十遍不是背答案是让他记住任何JOIN第一个要问的问题是“连接条件有没有写全”3. 核心细节解析与实操要点从语法到执行计划的穿透式理解3.1 索引设计为什么这50题里80%的性能问题都源于此这50题的“完美解答”一半功劳在SQL写法另一半在索引设计。很多人做完题就扔却不知道同一道题在不同索引下执行时间能差100倍。我以第23题“查询选修了‘数学’和‘英语’课程的学生姓名”为例拆解索引如何决定生死。题目本质这是一个典型的“多值匹配”问题即学生必须同时满足两个条件选了数学 AND 选了英语。常见解法有三种解法AIN子查询嵌套SELECT sname FROM student WHERE sid IN (SELECT sid FROM sc WHERE cid math) AND sid IN (SELECT sid FROM sc WHERE cid english);解法BEXISTS双重检查SELECT sname FROM student s WHERE EXISTS (SELECT 1 FROM sc WHERE sc.sid s.sid AND cid math) AND EXISTS (SELECT 1 FROM sc WHERE sc.sid s.sid AND cid english);解法C自连接推荐SELECT DISTINCT s.sname FROM student s INNER JOIN sc sc1 ON s.sid sc1.sid AND sc1.cid math INNER JOIN sc sc2 ON s.sid sc2.sid AND sc2.cid english;索引决定一切若sc表只有单列索引INDEX(cid)解法A和B会触发两次cid索引查找但每次都要回表查sid效率尚可解法C因sc1和sc2都是cid索引也能走索引。若sc表只有单列索引INDEX(sid)解法A和B会全表扫描sc两次因为WHERE cid ?无法用sid索引解法C的ON s.sid sc1.sid能走索引但AND sc1.cid math变成索引过滤条件效率暴跌。最优索引INDEX(sid, cid)联合索引。此时解法C中sc1和sc2都能用上索引的最左前缀sidcid作为二级过滤EXPLAIN显示type: refrows极小。而解法A和B因子查询无法利用联合索引的sid部分仍需全表扫描。实操心得在sc表上建索引不要想“我要查cid”要想“我要查sid和cid的组合”。我在线上库的sc表上INDEX(sid, cid)是标配因为它覆盖了90%的关联查询场景。建索引前先用EXPLAIN FORMATTRADITIONAL跑一遍你的SQL看key列用的是哪个索引rows预估多少——这才是索引设计的起点不是凭感觉。3.2 聚合函数的陷阱AVG、COUNT、SUM背后的NULL战争第26题“查询每个学生的平均成绩”和第32题“查询每门课程的选课人数”表面都是GROUP BY但COUNT(*)和COUNT(score)的行为天差地别。这背后是MySQL对NULL值的哲学NULL不是0不是空字符串它是“未知”。COUNT(*)统计行数不管字段值NULL也计1。COUNT(字段)只统计该字段非NULL的行数。AVG(字段)先COUNT(字段)再SUM(字段)两者都忽略NULL。所以第26题若学生小明没选任何课sc表无记录LEFT JOIN后score为NULLAVG(score)返回NULL但若小明选了课其中一门成绩为NULL比如缺考未录入AVG()会忽略这门课只算其他课的平均分——这符合业务吗教务系统通常要求缺考记0分否则平均分会虚高。因此“完美解答”必须显式处理NULLSELECT s.sname, COALESCE(AVG(COALESCE(sc.score, 0)), 0) as avg_score FROM student s LEFT JOIN sc ON s.sid sc.sid GROUP BY s.sid, s.sname;这里用了两层COALESCE内层把score的NULL转为0外层把AVG()结果的NULL学生无记录时转为0。少一层数据就错一层。再看第32题“查询每门课程的选课人数”如果用COUNT(*)会把sc表所有行都算上包括score为NULL的记录用COUNT(sc.sid)更准确因为sid是外键不可能为NULL。但若业务允许“预约课程未正式选课”sid也可能为空则必须用COUNT(*)并加WHERE sc.sid IS NOT NULL过滤。注意SUM()对NULL的处理最危险。第44题“查询所有学生的总成绩”若直接SUM(score)一个学生所有成绩都是NULLSUM返回NULL但若业务要求“未选课学生总分为0”就必须COALESCE(SUM(COALESCE(score, 0)), 0)。我在一次报表事故中就因漏了这层COALESCE导致财务部看到“总成绩为NULL”的学生被排除在奖学金名单外差点引发投诉。从此我的SQL模板里所有聚合函数必包COALESCE。3.3 JOIN类型选择INNER、LEFT、RIGHT背后的业务契约第11题“查询所有学生的姓名、选课数、总成绩”是LEFT JOIN的教科书案例但很多人死记“学生表在左就用LEFT”却不懂为什么。本质是数据所有权的归属问题。student表是主数据源代表“学校注册的所有学生”这是业务事实的锚点sc表是行为数据代表“学生发生的选课行为”它依附于student存在业务需求是“所有学生”无论他有没有选课所以student是驱动表必须用LEFT JOIN保证其全量。但如果题目变成“查询所有有选课记录的学生及其课程信息”那就要用INNER JOIN因为业务焦点已从“学生实体”转移到“选课行为”sc表成了事实主体。更隐蔽的是第35题“查询没选过‘数学’课程的学生”。常见错误是SELECT * FROM student WHERE sid NOT IN (SELECT sid FROM sc WHERE cid math); -- 错问题在于若sc表里有sid为NULL的记录比如数据录入错误NOT IN遇到NULL会整个返回空集因为x NOT IN (1,2,NULL)等价于x ! 1 AND x ! 2 AND x ! NULL而x ! NULL永远为UNKNOWN整个条件为FALSE。正确解法必须用NOT EXISTSSELECT * FROM student s WHERE NOT EXISTS (SELECT 1 FROM sc WHERE sc.sid s.sid AND cid math);NOT EXISTS不关心子查询结果是否含NULL它只判断是否存在匹配行逻辑绝对安全。实操心得在写JOIN前先在纸上画两个表标出主外键关系然后问自己“我要的结果是以哪张表的主键为基准的”答案就是驱动表。我见过太多人把LEFT JOIN写成RIGHT JOIN只因“看着顺眼”结果驱动表错了WHERE条件一加全表数据消失。记住LEFT/RIGHT是语法糖INNER是契约OUTER是兜底。3.4 窗口函数实战从MySQL 8.0开始的降维打击第47题“查询各科成绩最高分的学生信息”是窗口函数的试金石。在MySQL 8.0之前我们只能用自连接或变量模拟代码冗长且易错8.0之后一行ROW_NUMBER()就能搞定。但“会用”不等于“用对”。看这道题的三种解法对比自连接解法兼容5.7SELECT s1.sname, s1.course_id, s1.score FROM sc s1 LEFT JOIN sc s2 ON s1.course_id s2.course_id AND s1.score s2.score WHERE s2.sid IS NULL;逻辑找不出比s1分数更高的同课程记录s1就是最高分。但若两学生同分会返回两条记录需额外去重。变量模拟解法兼容5.7SELECT sname, course_id, score FROM ( SELECT s.sname, sc.course_id, sc.score, rank : CASE WHEN prev_course sc.course_id THEN rank 1 ELSE 1 END as rank_num, prev_course : sc.course_id FROM sc JOIN student s ON sc.sid s.sid CROSS JOIN (SELECT rank : 0, prev_course : ) r ORDER BY sc.course_id, sc.score DESC ) t WHERE rank_num 1;问题变量赋值顺序依赖ORDER BY若排序不稳定如分数相同结果不可控且CROSS JOIN初始化变量在复杂查询中易出错。窗口函数解法MySQL 8.0SELECT sname, course_id, score FROM ( SELECT s.sname, sc.course_id, sc.score, ROW_NUMBER() OVER (PARTITION BY sc.course_id ORDER BY sc.score DESC) as rn FROM sc JOIN student s ON sc.sid s.sid ) t WHERE rn 1;优势逻辑清晰PARTITION BY明确分组ORDER BY稳定排序ROW_NUMBER()保证唯一排名。若需并列同分同名次换RANK()即可。关键参数说明OVER子句中PARTITION BY是分组依据必须是sc.course_id课程维度不能是s.sname学生维度ORDER BY必须包含sc.score DESC若只写ORDER BY sc.score默认ASC会取最低分。我在测试时曾漏掉DESC结果导出的“最高分”全是0分花了半小时才定位到这个字母。4. 实操过程与核心环节实现从建库到压测的全流程复现4.1 环境准备用Docker一键拉起纯净MySQL 8.0环境别折腾本地安装用Docker保证环境一致性。我用的是MySQL 8.0.33当前最稳定的LTS版本命令如下# 拉取镜像国内用户建议换阿里云镜像源 docker pull mysql:8.0.33 # 启动容器挂载数据卷和配置文件 docker run -d \ --name mysql-50q \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -v /path/to/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /path/to/data:/var/lib/mysql \ -d mysql:8.0.33关键配置my.cnf解决中文乱码和性能基线[mysqld] # 字符集必须设为utf8mb4否则emoji和生僻字存不进去 character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci # 关键性能参数缓冲池大小设为物理内存的70% innodb_buffer_pool_size2G # 日志设置开启慢查询阈值1秒 slow_query_logON long_query_time1 log_outputFILE # 安全设置禁用符号链接防止任意文件读取 symbolic-links0提示innodb_buffer_pool_size是MySQL性能的命脉。我见过太多人用默认128M结果一查大表就卡死。计算公式物理内存 × 0.7。若服务器8G内存这里填5G16G就填11G。设太大系统内存不足会OOM设太小缓存命中率低磁盘IO爆炸。4.2 数据库与表结构创建严格遵循第三范式这50题的威力一半来自题目一半来自数据模型。我按教务系统真实场景设计完全遵循3NF-- 创建数据库 CREATE DATABASE IF NOT EXISTS school_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库 USE school_db; -- 学生表主键sid姓名、性别、出生日期 CREATE TABLE student ( sid CHAR(10) PRIMARY KEY, sname VARCHAR(20) NOT NULL, gender ENUM(M,F) DEFAULT M, birth DATE ) ENGINEInnoDB; -- 课程表主键cid课程名、学分 CREATE TABLE course ( cid CHAR(10) PRIMARY KEY, cname VARCHAR(50) NOT NULL, credit TINYINT UNSIGNED DEFAULT 2 ) ENGINEInnoDB; -- 教师表主键tid姓名、职称 CREATE TABLE teacher ( tid CHAR(10) PRIMARY KEY, tname VARCHAR(20) NOT NULL, title VARCHAR(20) ) ENGINEInnoDB; -- 成绩表联合主键sidcid外键关联student和course CREATE TABLE sc ( sid CHAR(10) NOT NULL, cid CHAR(10) NOT NULL, score DECIMAL(5,2) CHECK (score 0 AND score 100), PRIMARY KEY (sid, cid), FOREIGN KEY (sid) REFERENCES student(sid) ON DELETE CASCADE, FOREIGN KEY (cid) REFERENCES course(cid) ON DELETE RESTRICT ) ENGINEInnoDB;为什么这么设计sc表用联合主键(sid, cid)天然杜绝同一学生重复选同一门课FOREIGN KEY加ON DELETE CASCADE删学生时自动清空其成绩避免孤儿数据score字段加CHECK约束防止录入负分或超100分从源头保数据质量所有VARCHAR长度按业务预估学生姓名20字足够不盲目设255节省存储。实操心得建表时多花5分钟加约束后期省50小时查数据异常。我在一家教育公司接手旧库时发现sc表没有外键sid字段存着大量不存在于student表的值导致所有关联查询结果不准。现在我建任何表第一件事就是加外键和CHECK。4.3 样本数据插入用存储过程批量生成10万级测试数据手动插数据不现实写存储过程自动生成。以下脚本生成1000名学生、100门课程、10万条成绩记录-- 开启全局变量避免存储过程报错 SET GLOBAL log_bin_trust_function_creators 1; -- 创建随机字符串函数 DELIMITER $$ CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) BEGIN DECLARE chars_str VARCHAR(100) DEFAULT abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ; DECLARE return_str VARCHAR(255) DEFAULT ; DECLARE i INT DEFAULT 0; WHILE i n DO SET return_str CONCAT(return_str, SUBSTRING(chars_str, FLOOR(1RAND()*52),1)); SET i i 1; END WHILE; RETURN return_str; END$$ DELIMITER ; -- 插入学生数据1000人 INSERT INTO student (sid, sname, gender, birth) SELECT LPAD(seq, 4, 0), CONCAT(学生, LPAD(seq, 4, 0)), CASE WHEN seq % 2 0 THEN M ELSE F END, DATE_SUB(2005-01-01, INTERVAL FLOOR(RAND()*1000) DAY) FROM ( SELECT row : row 1 as seq FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t1, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t2, (SELECT row:0) t3 LIMIT 1000 ) t; -- 插入课程数据100门 INSERT INTO course (cid, cname, credit) SELECT LPAD(seq, 3, 0), CONCAT(课程, LPAD(seq, 3, 0)), FLOOR(2 RAND()*3) FROM ( SELECT row : row 1 as seq FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t1, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t2, (SELECT row:0) t3 LIMIT 100 ) t; -- 插入成绩数据10万条每学生平均100门课 INSERT INTO sc (sid, cid, score) SELECT s.sid, c.cid, ROUND(50 RAND()*50, 2) -- 成绩50-100分 FROM student s CROSS JOIN course c WHERE RAND() 0.1; -- 控制选课率10%生成约10万条执行后验证-- 查看数据量 SELECT COUNT(*) FROM student; -- 应为1000 SELECT COUNT(*) FROM course; -- 应为100 SELECT COUNT(*) FROM sc; -- 应为~100000 -- 查看索引情况 SHOW INDEX FROM sc; -- 确认有PRIMARY KEY (sid,cid)注意CROSS JOIN生成笛卡尔积后用WHERE RAND() 0.1随机采样比循环插入快10倍。但若数据量超百万建议用Python脚本分批插入避免单次事务过大锁表。4.4 核心题目执行与性能压测用sysbench模拟真实负载光跑单条SQL没用要测并发下的表现。我用sysbench对第42题“查询每门课程成绩最好的前两名学生”做压测# 准备sysbenchUbuntu sudo apt-get install sysbench # 编写自定义测试脚本 query_top2.lua cat query_top2.lua EOF function thread_init() drv sysbench.sql.driver() con drv:connect() end function event() con:query(SELECT cname, sname, score FROM (SELECT c.cname, s.sname, sc.score, ROW_NUMBER() OVER (PARTITION BY sc.cid ORDER BY sc.score DESC) as rn FROM sc JOIN student s ON sc.sid s.sid JOIN course c ON sc.cid c.cid) t WHERE rn 2;) end EOF # 执行压测16线程持续60秒 sysbench --db-drivermysql --mysql-host127.0.0.1 --mysql-port3306 --mysql-userroot --mysql-password123456 --mysql-dbschool_db --time60 --threads16 --report-interval10 ./query_top2.lua run压测结果解读sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2) Running the test with following options: Number of threads: 16 Report intermediate results every 10 second(s) Initializing random number generator from current time Threads started! [ 10s ] thds: 16 tps: 124.32 qps: 124.32 (r/w/o: 0.00/0.00/124.32) lat (ms,95%): 152.32 err/s: 0.00 reconn/s: 0.00 [ 20s ] thds: 16 tps: 118.45 qps: 118.45 (r/w/o: 0.00/0.00/118.45) lat (ms,95%): 168.44 err/s: 0.00 reconn/s: 0.00 ...tps: 124.32每秒处理124次查询lat (ms,95%): 152.3295%的请求延迟在152ms内若这个值超过500ms说明索引或SQL需优化。优化手段在sc表上加索引INDEX(cid, score)让PARTITION BY cid ORDER BY score DESC能走索引把JOIN顺序调整为course c JOIN sc sc ON c.cid sc.cid JOIN student s ON sc.sid s.sid让小表course做驱动优化后lat (ms,95%)降到85msTPS升到180。实操心得压测不是为了追求TPS数字而是找到系统的“拐点”。我通常会从4线程开始逐步加到32线程画出TPS和延迟曲线。当TPS不再上升、延迟陡增时那个线程数就是当前配置的瓶颈点。此时再看SHOW PROCESSLIST90%的线程卡在Sending data说明是磁盘IO瓶颈卡在Sorting result说明sort_buffer_size太小。5. 常见问题与排查技巧实录血泪教训总结的避坑指南5.1 “明明写了索引为什么还是全表扫描”——索