news 2026/9/28 13:21:02

MySQL子查询全解:从语法实战到性能优化避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL子查询全解:从语法实战到性能优化避坑指南

前几天有个朋友发来一条SQL,三层子查询嵌套,跑了小半天都不出结果,问我怎么优化。我一看,他是用相关子查询在百万行表里做逐行扫描,关联字段上还没有索引,不慢才怪。MySQL里的子查询(subquery)就是这么个东西:用好了是利器,用歪了是灾难,而且不管是面试还是日常开发,几乎绕不开它。

这篇文章我不想堆空洞理论,就从实际开发者的角度,把MySQL子查询从头到尾盘一遍:先讲清楚子查询到底是什么、哪些场景必须用它,再把标量子查询、IN、EXISTS、派生表逐个拆开,接着拿一个学生课程成绩库做完整实战,最后重点聊聊性能优化和几个容易踩的坑。适合刚接触SQL的新人,也适合写了几年SQL但一遇到子查询优化就头疼的老同学。

1. 先搞清楚子查询是什么:一个查询套在另一个查询里

1.1 子查询的定义与三种基础形态

子查询,说白了就是一个SELECT语句嵌套在另一个SQL语句里,被嵌套的那条SELECT通常用括号包起来。它内部的执行结果会作为外部查询的输入,可以是一个值、一列数据、一行记录,也可以是一整张临时表。

根据返回值,我习惯把子查询分成三种形态来看:

  • 标量子查询:返回一行一列,比如(SELECT MAX(score) FROM score),就是一个数字或一个字符串,可以像普通值一样放在比较表达式中。
  • 列子查询/行子查询:返回一列多行或者一行多列,最典型的就是WHERE student_id IN (SELECT student_id FROM ...),把子查询的结果当作一个集合。
  • 表子查询(派生表):返回一个多行多列的结果集,出现在FROM子句后面,相当于临时造了一张表,例如FROM (SELECT ...) AS t。

子查询可以出现的位置也很多:WHERE子句、HAVING子句、FROM子句、SELECT的字段列表,甚至在UPDATE和DELETE语句里也照样能用。很多人以为子查询只是“查询时用的”,其实更新和删除操作中它更常用,后面我会专门演示。

不少初学者会把子查询和JOIN搞混。简单理解,JOIN是把两张表横向拼接,子查询是先算出一部分结果再交给外层逻辑,更像“分步计算”。这两种方式在很多场景下能互相替代,但语义和性能可能完全不同,这个是第4章的重点。

1.2 为什么需要子查询:有些过滤条件必须先“算”出来

很多场景用JOIN也能做,但子查询有它不可替代的位置。最典型的是“WHERE条件里拿聚合结果做比较”。

举个例子,你想查“成绩高于全校平均分的学生”。SQL的语法规定WHERE子句里不能直接写聚合函数,你不能写成:

SELECT * FROM score WHERE score > AVG(score);

这句话在MySQL里直接报错。正确的思路是先用子查询把平均分算出来,再放到WHERE里去比较:

SELECT * FROM score WHERE score > (SELECT AVG(score) FROM score);

在这里子查询就相当于先算出“全校平均分”这个常量,然后外部查询拿这个常量去逐行过滤。这种“先算一步,再比一步”的逻辑,用JOIN也能写,但通常要绕一大圈,远不如子查询直观。

还有一种场景,是希望筛选“主表中满足某个集合条件的记录”。比如“找出所有选过课的学生”“找出所有没有选课的学生”,这类需求如果用JOIN,会因为一对多关系让主表记录重复,还得加DISTINCT;而用子查询,语义上更贴合人的思考习惯。尤其是“没有选课”这种否定条件,NOT EXISTS和NOT IN的子查询写法几乎成了标准答案。

再说一个实战中很常见的场景:UPDATE和DELETE操作里需要引用另一张表的数据。比如“把所有低于课程平均分的成绩标记为待补考”,你必须先算出每门课的平均分,再去更新成绩表。这种场景子查询直接嵌入UPDATE语句里,写起来非常顺手。

所以我的判断是:子查询不是“能用但尽量少用”的东西,而是一个独立的思考层级。它在聚合比较、集合判断、派生表加工这三个方向上尤其有优势。

2. 核心语法拆解:从标量子查询到相关子查询

2.1 标量子查询:返回一行一列,当普通值用

标量子查询是入门最简单、也最容易踩坑的一种。它返回且只能返回一行一列,比如:

SELECT student_id, name, (SELECT MAX(score) FROM score) AS max_score FROM student;

这条SQL会在每个学生后面都带上一列全校最高分,虽然这个例子业务意义不大,但它很好展示了标量子查询的用法:子查询的结果被当成一个字段值带在结果集里。

更实际一点的用法是在WHERE里做比较。比如查“成绩高于全校平均分的学生记录”:

SELECT * FROM score WHERE score > (SELECT AVG(score) FROM score);

这里有个非常经典的报错:如果子查询返回了多行,MySQL会直接抛错,错误码是1242,提示信息是“Subquery returns more than 1 row”。很多人第一次遇到都懵了,明明逻辑没问题,为什么报错?因为标量子查询的使用场景是“代替一个值”,一个值只能有一个,多行就违反了基本语义。

还有一种情况是子查询结果为空。这个时候标量子查询不会报错,而是返回NULL。于是会出现一个隐蔽问题:外部用不等于比较时,NULL参与比较会导致结果过滤不干净,这个在第5章里会详细说。

2.2 IN与行子查询:把子查询结果当作集合

当子查询返回一列多行时,最常见的使用方式就是配合IN操作符。比如:

SELECT name FROM student WHERE student_id IN ( SELECT student_id FROM score WHERE course_id = 1 );

这条SQL先找出选了课程1的学生ID集合,再从student表里把这些人查出来。它的执行逻辑很符合人的直觉:“先找到条件集合,再根据集合过滤”。

MySQL还支持一种行子查询的写法,把多个字段当作一个“组合值”来比较。比如我想查“每门课程的最高分记录”,可以用:

SELECT * FROM score WHERE (course_id, score) IN ( SELECT course_id, MAX(score) FROM score GROUP BY course_id );

这里(course_id, score)是一个行构造器,子查询返回每一门课的课程号和最高分,外部用两列组合去匹配。这个方法可以解决“每组最大值”这类经典问题,代码比手动写相关子查询简洁很多,也不容易漏掉并列最高分的记录。

用IN的时候要注意,如果子查询结果集中包含NULL值,IN本身还是能正常工作的——只要匹配到任意一个非NULL的值就会返回TRUE。但如果你用的是NOT IN,一旦子查询结果里有NULL,结果集大概率会变成“空集”。这个坑放到第5章集中讲。

2.3 EXISTS与相关子查询:内外联动的关键

EXISTS是子查询体系里最难理解、也最重要的部分。它通常配合“相关子查询”一起出现,所谓相关,是指内层子查询引用了外层查询的字段,内外两层产生联动。

比如查询“至少选过一门课的学生”:

SELECT * FROM student s WHERE EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = s.student_id );

这里的sc.student_id = s.student_id就是关联条件。每取一个学生,MySQL就会去score表里查一下这个学生有没有成绩记录。只要存在至少一条记录,EXISTS就返回真,这个学生就会被选中。

EXISTS只关心“内层有没有记录返回”,并不是真的要把记录取出来,所以内层写SELECT 1、SELECT *,甚至SELECT NULL都一样。它不参与NULL值的比较逻辑,所以NOT EXISTS在处理“不存在”类需求时,比NOT IN可靠得多。

不过相关子查询是有性能代价的。如果外层表有10万行,内层查询理论上就可能执行10万次,这在MySQL里叫“逐行执行”。一旦内层查询少了索引,性能会迅速恶化。很多“子查询慢”的传闻,其实都来自相关子查询被滥用。这章先记住它的特点和风险,第4章我专门讲优化办法。

2.4 FROM子句里的派生表:查询套查询的“临时表”

子查询放在FROM子句后面,就是派生表。它相当于在SQL执行过程中临时生成一张表,供外层继续查询。派生表必须起别名,否则MySQL会直接报错。

SELECT course_id, avg_score FROM ( SELECT course_id, AVG(score) AS avg_score FROM score GROUP BY course_id ) AS t WHERE avg_score > 80;

这个例子里,内层先算出每门课的平均分,外层再筛出平均分大于80的课程。虽然这个特定需求用HAVING也能写,但一旦你要对聚合结果做二次计算,比如“过滤出平均分高于所有课程平均分平均值”的课程,派生表几乎就是唯一直观的解法。

MySQL 8.0对派生表做了不少优化,比如derived_merge,即优化器尝试把派生表和外部查询合并起来执行,避免真正物化成一张临时表。但能不能合并成功,要看外层查询的用法,如果派生表里用了聚合函数、DISTINCT、GROUP BY等操作,还是可能会物化成临时表。所以写派生表的时候,不要理所当然认为它是“零成本的”,也要看执行计划。

3. 实战:学生课程成绩库,把子查询用到极致

3.1 建表与造数:一个可直接复现的成绩库

光讲语法记不住,我自己学SQL的时候最有效的办法就是建一套最简单的学生课程成绩库,反复折腾。下面这个结构很基础,但足够覆盖80%的子查询场景。

CREATE TABLE student ( student_id INT PRIMARY KEY, name VARCHAR(20) NOT NULL, class_name VARCHAR(20) ); CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(40) NOT NULL, credit DECIMAL(2,1) ); CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1), remark VARCHAR(20), KEY idx_student (student_id), KEY idx_course (course_id), KEY idx_score (score) );

注意我给score表加了三个索引。这不是为了示范而示范,而是因为后续几乎所有子查询案例都会在student_id、course_id、score这三个字段上做过滤和关联,没有索引的话,再好的查询写法也白搭。

插入几条简单数据:

INSERT INTO student (student_id, name, class_name) VALUES (1, '张三', '一班'), (2, '李四', '一班'), (3, '王五', '二班'), (4, '赵六', '二班'); INSERT INTO course (course_id, course_name, credit) VALUES (1, '数据库', 3.0), (2, '操作系统', 2.5), (3, '计算机网络', 2.0); INSERT INTO score (student_id, course_id, score) VALUES (1, 1, 92.0), (1, 2, 85.0), (2, 1, 86.0), (2, 3, 78.0), (3, 2, 95.0), (3, 3, 88.0);

这里有意让赵六没有任何成绩,后面查“没有选课的学生”时正好派上用场。

3.2 查询每门课的最高分记录:IN和=的差别

先看一个非常经典的需求:找出每门课程的最高分记录,并把学生姓名等详情一起带出来。

最直观的关联子查询写法是这样的:

SELECT * FROM score s WHERE score = ( SELECT MAX(s2.score) FROM score s2 WHERE s2.course_id = s.course_id );

如果每门课只有一个最高分,这种写法是对的。但实际数据里,很可能两个学生正好考了一样的最高分,这时候内层子查询就会返回两行,外层用等于号比较直接报1242错误。

更稳妥的写法是用行构造器配合IN:

SELECT s.*, st.name FROM score s JOIN student st ON s.student_id = st.student_id WHERE (s.course_id, s.score) IN ( SELECT course_id, MAX(score) FROM score GROUP BY course_id );

这条SQL先把每个课程的最高分查出来,再回到score表里用多字段匹配,能够同时取回并列最高分的所有人。我在实际项目里就遇到过“线上一门课出现并列最高分,旧SQL漏数”的事故,所以涉及这类统计需求时,默认用IN比用=安全。

3.3 查询没有选课的学生:NOT EXISTS 慎用 NOT IN

这个需求直接对应“赵六”。不少人的第一反应是:

SELECT * FROM student WHERE student_id NOT IN ( SELECT student_id FROM score );

看起来没问题,student_id是的子查询字段,而且score表里没有NULL,所以在这个演示库里能跑出正确结果。但一旦score表的student_id字段允许为空,或者某行数据意外写入了NULL,这条SQL的结果就会变成空集。

原因我放到5.2详细讲,现在先给出更可靠的写法:

SELECT * FROM student s WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = s.student_id );

用NOT EXISTS,逻辑就变成本质上的“不存在任何一条成绩记录,才选中这个学生”。它不关心子查询里有没有NULL,也不用担心优化器的特殊行为,结果始终符合预期。

写“不存在”类需求时,我个人现在默认用NOT EXISTS,除非能确认子查询字段绝对没有NULL,才偶尔用NOT IN。

3.4 UPDATE里的子查询:同一张表的更新陷阱

子查询不只在SELECT里常见,UPDATE里也经常用。比如我想把“成绩低于全校平均分”的记录标记为待补考:

UPDATE score SET remark = '待补考' WHERE score < (SELECT AVG(score) FROM score);

这条SQL在MySQL里会直接报错,错误码是1093,提示不能在同一张表上一边更新一边查。MySQL不允许UPDATE目标表和子查询引用的表是同一张表。

解决办法是包一层派生表,让MySQL把它当成一张“临时结果表”而不是原始目标表:

UPDATE score SET remark = '待补考' WHERE score < ( SELECT avg_score FROM ( SELECT AVG(score) AS avg_score FROM score ) AS t );

如果是更复杂的“低于本课程平均分”,最好直接用UPDATE JOIN的方式:

UPDATE score s JOIN ( SELECT course_id, AVG(score) AS avg_score FROM score GROUP BY course_id ) c ON s.course_id = c.course_id SET s.remark = '待补考' WHERE s.score < c.avg_score;

这种写法先构造各课程平均分的派生表,再和成绩表做关联更新,既绕过了同一张表的限制,又保证了逻辑清晰。记住一个判断原则:更新时如果子查询要引用目标表的数据,优先考虑包一层派生表,或者直接用JOIN更新。

3.5 DELETE里的子查询:安全删除的正确姿势

删除操作里的子查询和更新类似。比如删除没有选课的学生:

DELETE FROM student WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = student.student_id );

删除前务必先用SELECT验证一遍结果。我处理线上数据时的习惯是先把DELETE临时改成SELECT:

SELECT * FROM student WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = student.student_id );

确认结果集没问题,再改成DELETE执行,而且通常放到事务里:

START TRANSACTION; DELETE FROM student WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.student_id = student.student_id ); -- 确认影响行数无误后提交 COMMIT;

大表删除时要注意锁表问题,DML会长期持有锁,尽量避开业务高峰期分批删除。

4. 性能对比与优化:子查询真的慢吗

4.1 子查询性能的争议来源:优化器的执行策略

很多老DBA一听到子查询就皱眉,说“子查询慢,能用JOIN就别用子查询”。这个说法在MySQL 5.5时代基本成立,因为早期优化器对子查询的处理方式非常粗糙,尤其是相关子查询,经常会逐行执行,外层有多少行,内层就执行多少次。

MySQL 5.6之后引入了大量子查询优化策略,包括半连接转换、物化、Exists策略等。到了MySQL 8.0,优化器已经相当成熟,IN子查询在很多情况下会被自动改写成半连接执行,性能并不比JOIN差,甚至在某些场景更快。

所以现在再拿“子查询等于慢”当结论,已经过时了。正确做法是看执行计划。子查询本身没有原罪,真正的问题是用户在写相关子查询时没有建立合适的索引,或者三层嵌套把自己的逻辑搞得复杂到优化器也无从下手。

4.2 用EXPLAIN判断子查询的执行方式

EXPLAIN是分析SQL性能的第一工具。拿刚才“查询每门课最高分”的语句举例:

EXPLAIN SELECT s.*, st.name FROM score s JOIN student st ON s.student_id = st.student_id WHERE (s.course_id, s.score) IN ( SELECT course_id, MAX(score) FROM score GROUP BY course_id );

你会看到执行计划里有很多select_type值,常见的有:

select_type含义
SIMPLE普通查询,没有子查询和UNION
PRIMARY最外层的查询
SUBQUERY非相关子查询,先算一次然后外层使用
DEPENDENT SUBQUERY相关子查询,外层每行都执行一次
DERIVEDFROM中的派生表
MATERIALIZED子查询结果被物化成临时表

看到DEPENDENT SUBQUERY就要格外敏感,如果外层记录多、内层关联字段没有索引,基本可以断定这条SQL会慢。解决办法通常是在内层查询的关联字段上加索引。

比如相关子查询的内层是WHERE sc.student_id = s.student_id,那score.student_id就必须有索引。演示库里我已经建了,所以即便出现相关子查询,性能也不会差太多。实际项目里很多人建表时漏了这类索引,才是“子查询慢”的真正原因。

4.3 子查询、JOIN、EXISTS怎么选:一张表讲清楚

到底哪种写法更好?我的判断标准是先看语义,再看执行计划。不搞“一刀切”。

场景推荐写法说明
WHERE里和聚合值比较标量子查询无法用JOIN表达,必须先把聚合算出来
过滤主表记录,不关心重复EXISTS或INEXISTS避免了NULL问题,语义更稳
需要同时取关联表的字段JOIN子查询无法直接输出另一张表的字段
查询“不存在”类记录NOT EXISTSNOT IN遇NULL会翻车,谨慎使用
对聚合结果做二次过滤派生表把中间结果包装成临时表
每组分组的Top N窗口函数或相关子查询MySQL 8.0优先用窗口函数

我自己写SQL的顺序是:先保证逻辑正确,用最容易理解的子查询把结果跑出来;然后打开EXPLAIN看执行计划;如果发现相关子查询在逐行扫描,再考虑改写。比如把相关子查询改成JOIN,或者把IN改成EXISTS,然后对比两条SQL的执行计划,用数据说话,而不是“听别人说”。

5. 常见问题与排查技巧实录

5.1 Subquery returns more than 1 row:最常见的1242报错

这个报错高频到几乎所有写子查询的人都会遇到。错误信息:

ERROR 1242 (21000): Subquery returns more than 1 row

原因很简单:标量子查询或等于号比较时,内层查出了多行数据。解决办法分情况:

  • 如果业务上只需要任意一个值,内层加LIMIT 1。
  • 如果是“每组最大值”这类需求,用行构造器IN。
  • 如果原本就用错了逻辑,应该改为EXISTS或IN。

排查时可以先把子查询单独拎出来跑一遍,看返回多少行,往往一眼就能发现问题。

5.2 NOT IN遇上NULL:为什么查不到任何数据

这是SQL里最著名的隐蔽坑之一。假设score表的student_id列允许为NULL,并且确实有NULL值:

SELECT * FROM student WHERE student_id NOT IN (1, 2, NULL);

这条SQL的结果是空集。原因是SQL的三值逻辑:student_id无论等于哪个值,与NULL比较时结果都是NULL,而NULL在WHERE里会被当作不成立,所以一行都查不出来。

解决办法就是用NOT EXISTS替代。这也是MySQL面试题里高频出现的一个点,面试官常拿它考候选人到底是否理解NULL语义。

5.3 子查询里的ORDER BY和LIMIT:容易被忽视的坑

子查询里用ORDER BY排序,再交给外层使用,结果顺序不一定保得住。经典问题“取每个班级分数最高的学生之一”:

SELECT * FROM student s WHERE student_id IN ( SELECT student_id FROM score WHERE score GROUP BY class_name ORDER BY score DESC LIMIT 1 );

这个写法本身问题很多,因为子查询里ORDER BY和LIMIT与外层IN组合时,语义可能有变化,而且MySQL优化器可能改写掉内层顺序。更可靠的做法是外层排序:

SELECT * FROM score WHERE student_id IN (...) ORDER BY score DESC;

派生表里的ORDER BY也不保证外层最终结果的顺序,外层需要排序就必须在外层写ORDER BY。别把“内层排好序,外层直接用”当作默认行为。

5.4 优化器改写导致的“惊喜”:结果和预期不一致

MySQL优化器会在不影响语义的情况下重写SQL,比如把IN子查询改写成半连接。半连接在去重逻辑上和普通JOIN不同,有时候你以为会去重,实际却没去;有时候你以为不去重,结果反而去重了。

遇到“结果和预期不一致”时,除了检查业务逻辑,还要看两条线索:一是EXPLAIN的执行计划,看select_type发生了什么改变;二是打开optimizer trace,看优化器到底做了哪些改写:

SET optimizer_trace = 'enabled=on'; SELECT ...; SELECT * FROM information_schema.OPTIMIZER_TRACE;

这个工具能直接输出优化器的决策过程,非常直观。排查“被改写”类问题时,比瞎猜高效得多。MySQL 8.0还支持NO_SEMIJOIN这类优化器hint,可以强制关闭某种策略,但生产环境不建议滥用,更多是用来验证问题。

写到这里,子查询的日常用法、典型实战和关键坑基本都过了一遍。我个人每次写SQL的习惯是:先把业务逻辑用最直白的方式写出来,哪怕是三层嵌套,跑通拿到正确结果,再打开EXPLAIN看执行计划,最后才考虑要不要改成JOIN、EXISTS或者用窗口函数。子查询本身不是洪水猛兽,不了解原理就乱优化才是真正的灾难。

最后再分享一个小技巧:如果你要反复调试一条带子查询的SQL,记得先给相关子查询里的关联列建索引,比如今天这个例子里的score表,student_id、course_id、score三个字段建好索引,90%的子查询慢问题都能缓解。遇到奇怪结果,优先怀疑NULL,其次怀疑优化器改写,按这两条思路排查,大多数坑都能快速定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 13:20:56

PointNet模型权重详解:从加载推理到训练验证

简介&#xff1a;这是一份 PointNet 点云模型权重资源包&#xff0c;包含 TorchScript、ONNX、OpenVINO 与 TensorRT 四种主流部署格式&#xff0c;面向从事点云分类、分割或三维理解的开发者与研究人员。资源基于 Qi 等人 2017 年提出的 PointNet 结构&#xff0c;提供已训练好…

作者头像 李华
网站建设 2026/9/28 13:20:20

计算机组成原理中断系统详解:从原理到408真题实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 13:20:00

人行道检测数据集VOC+YOLO格式训练避坑与难例挖掘实战

简介&#xff1a;一份面向目标检测入门实践的人行道&#xff08;斑马线&#xff09;检测数据集&#xff0c;采用Pascal VOC与YOLO双格式标注&#xff0c;适合算法学习者、毕业设计及工程验证场景直接使用。资源整体共2000个文件&#xff0c;以795个txt标签、793个xml标注与jpg原…

作者头像 李华
网站建设 2026/9/28 13:19:27

RK3568平板EMUELEC移植:U-Boot/内核/设备树三阶适配实战

1. 项目概述&#xff1a;为什么RK3568平板跑EMUELEC不是“刷个镜像就完事”&#xff1f;RK3568平板游戏机EMUELEC移植实战——这标题里每个词都带着硬核的分量。我干嵌入式系统适配快十二年&#xff0c;从ARM9时代焊板子调串口&#xff0c;到今天带团队做RK3568/3588平台整机交…

作者头像 李华
网站建设 2026/9/28 13:19:26

钙钛矿杀入消费电子:CES2026上的发电、发光与传感路线全解析

刚从展馆出来&#xff0c;脚底板还是酸的&#xff0c;但脑子里装着 CES2026 现场挥不去的画面&#xff1a;钙钛矿不再只是角落里的那块实验室“冠军组件”&#xff0c;而是被嵌进手机背板、手表表带、TWS 充电仓盖&#xff0c;甚至是户外电源外壳上的增量功能模块。如果你也是做…

作者头像 李华
网站建设 2026/9/28 13:17:48

混合流水车间调度HFSSPW:工人约束建模与NSGA-II启发式解码实践

HFSSPW这篇文章我断断续续做了一年多&#xff0c;从最初的建模陷阱到最终的Matlab代码框架&#xff0c;中间重写了三版解码器。最初我以为难点在进化算法&#xff0c;后来才意识到&#xff0c;真正拉开差距的是解码策略。今天把完整的求解思路、代码实现的细节和踩坑记录整理出…

作者头像 李华