2026年3月4日,我用AI复盘了一遍MySQL的DDL/DML/DQL
今天本来只是想把笔记整理归档,结果越整理越觉得,SQL三大类语句——DDL、DML、DQL——被我以前学得太糙了。以前总想着赶快写完CRUD赶快下班,根本没认真想过"为什么建表要这么建""为什么这个查询写成这样性能才好"。这几个月我一直拿AI当陪练,逮着问题就问,让它帮我解释执行计划、举反例、出练习题,把之前欠的账慢慢补了回来。这篇笔记就是把这段复盘过程里最值得记录的东西捞出来,给同样在啃MySQL的人一点参考。
无论你是刚学SQL的新手,还是写了好几年代码但没系统捋过这三类语句的开发者,这篇笔记都适合。我不会照着官方文档给你念一遍语法,而是讲清楚:三类语句各自解决什么问题、实操时最容易在哪几个地方翻车、以及怎么用AI辅助把这些知识真正变成自己的。
1. 先搞清楚DDL/DML/DQL是干什么的,为什么要把它们分开学
SQL语句按功能分成好几类,最常用的是DDL、DML、DQL这三板斧。我们逐个捋清楚。
1.1 三类语句的职责边界
- DDL(Data Definition Language):负责定义数据结构。建库、建表、改表结构、删表,都归它管。核心动词是CREATE、ALTER、DROP、TRUNCATE。
- DML(Data Manipulation Language):负责操作数据。往表里插入、更新、删除记录,核心动词是INSERT、UPDATE、DELETE。
- DQL(Data Query Language):负责查询数据。就是SELECT,它是我们跟数据库对话最频繁的一扇窗口。
这三类语句地位完全不同。DDL是打地基,DML是日常搬砖,DQL是验收和发现问题的手段。地基没打好,后面DML、DQL全都难受。这也是我这次复盘最大的一个体会:很多线上事故、慢查询、数据不一致问题,根源根本不在这条SQL写得烂不烂,而在建表阶段就已经埋了雷。
1.2 为什么用"职责边界"的方式来学
我见过不少朋友学SQL是抱着函数手册啃,今天学个DATE_FORMAT,明天学个LEFT JOIN,学得零零碎碎。这种学法最大的问题是:遇到真实需求时,根本不知道该从哪个方向下手。
按"职责边界"来学就不一样了。你拿到需求先问自己:
- 是要新建结构吗?→ DDL
- 是要改数据吗?→ DML
- 是要查数据吗?→ DQL
先定位到大类,再往下想具体语法和优化,思路会清晰很多。我让AI给我出过一套"乱序判断题",它随机给出场景描述,我判断该用哪类语句。练了大概几十道之后,反应速度快了不少。
1.3 AI辅助复盘时,我建议先用"框架式提问"
拿AI当学习工具时,第一个问题千万别问得太细。我试过那种"给我讲讲LEFT JOIN"的提问,AI给了一堆语法示例,看完记不住多少。后来我换了提问方式,效果好很多。比如:
请用表格对比DDL/DML/DQL的职责、常用命令、典型使用场景、常见误用情况。
这种"框架式提问"能逼着AI一次性帮你把骨架搭好,你再顺着骨架往里面填充具体内容。学SQL尤其适合这种方式——它的知识体系天然有层级,很适合"先框架后细节"的学习路径。
2. DDL建的不仅是表,是数据地基
很多人觉得DDL没啥好学的,无非就是CREATE TABLE IF NOT EXISTS,然后写几个字段。我以前也这么想,直到被线上表结构坑过几次之后才明白,建表阶段的每一个选择,都在为未来的数据质量打底。
2.1 字段类型选不对,后面全是泪
MySQL字段类型看着简单,实际坑特别多。举几个我遇到过的真实例子。
整数类型别用错。INT是4字节,范围大约是正负21亿。BIGINT是8字节。如果一张表的自增主键预期会超过21亿,或者存储的是雪花ID这类大数值,必须用BIGINT。见过有人把订单号设计成INT,结果业务量上来后,ID直接溢出,等于把一张线上表干废了,只能重建。
金额不要用FLOAT/DOUBLE。这是老生常谈但永远有人犯。浮点数在二进制里不能精确表示,做金额加减乘除会产生误差。正确的做法是用DECIMAL。比如DECIMAL(10,2)可以表示最大99,999,999.99的金额,精度可控。
时间字段区分DATE/DATETIME/TIMESTAMP。DATE只存日期,DATETIME存日期+时间,TIMESTAMP有一个2038年问题(不过现在已经很少有人真的担心这个了,因为MySQL 8.0已经支持到更远的范围)。一般业务用DATETIME就够了,如果要考虑时区自动转换,TIMESTAMP会更方便。
我当时让AI帮我整理了一张"MySQL常用字段类型选择指南",它把每个类型的存储大小、取值范围、适用场景列得非常清楚。我会在上面标注实际业务中踩过的坑,这样这张表就成了我自己的知识库,比单纯收藏一篇文章有用得多。
2.2 约束条件:数据质量的守门员
建表时最容易被忽略的就是约束条件。很多人只记得加一个PRIMARY KEY,其他约束能省就省。但实际上,约束是数据库帮我们守住数据质量的第一道防线。
- NOT NULL:防止"空值污染"。如果一个业务字段不能为空,必须在建表时就声明NOT NULL。
- UNIQUE:防止重复数据。比如用户手机号、订单流水号这类业务上必须唯一的字段,不要只靠应用层判断,直接在数据库层加UNIQUE约束才保险。
- DEFAULT:给字段设计合理的默认值。注意,只有带DEFAULT的字段,插入时才能省略这个字段的值。
- FOREIGN KEY:外键约束。说实话,在互联网高并发业务中,我们一般不建议在数据库层用物理外键,会拖慢写入性能。但前提是应用层必须保证关联数据的一致性。这也是一个"要不要用、怎么用"的权衡问题,不是非黑即白的。
2.3 ALTER TABLE,DDL里最需要谨慎的操作
改表结构比建表更考验功夫。一次线上ALTER TABLE,如果操作不当,可能导致数据库长时间锁表,业务直接阻塞甚至不可用。
比较常见的坑:
- ADD COLUMN一般可以快速完成,但如果表数据量很大,而且你设置的DEFAULT值是非固定值,MySQL可能要重建整张表,耗时很长。
- MODIFY COLUMN修改字段类型或长度时,MySQL有时会做全表扫描和重建,特别是把VARCHAR改长或改短,都会触发额外开销。
- DROP COLUMN同样需要重建表,5.7版本尤其明显,8.0.29版本等优化了部分元数据操作。8.0版本里,DROP COLUMN依然是会重建表的,虽然在很多场景下是INSTANT的,但严格来说还是先查执行计划和官方文档更稳妥。
我在AI辅助学习时,特别让它总结了一份"ALTER TABLE 操作类型与锁表现",并用真实业务场景去模拟。比如一张1000万行的表,执行ALTER TABLE ADD COLUMN,不同版本的MySQL表现差异很大。这让我意识到:线上操作前,必须评估影响,选择业务低峰期执行,并且时刻准备回滚方案。
2.4 TRUNCATE和DELETE的区别,必须刻进脑子里
TRUNCATE是DDL,DELETE是DML,它们虽然都用于删除数据,但底层机制完全不同。
- TRUNCATE:直接删除整个表的数据并释放空间,速度极快。它是DDL,会隐式提交事务,不能回滚。
- DELETE:逐行删除数据,是DML,可以配合事务回滚,也可以通过WHERE条件删除指定行。
我让AI帮我总结过一张对比表:
| 维度 | TRUNCATE | DELETE |
|---|---|---|
| 类型 | DDL | DML |
| 是否可回滚 | 否 | 是 |
| 效率 | 高 | 低 |
| 释放空间 | 是 | 否(需要额外处理) |
| 触发触发器 | 否 | 是 |
| WHERE条件 | 不支持 | 支持 |
列出来之后一目了然,以后再也不会在需要回滚的场景里用TRUNCATE了。
3. DML的增删改没那么简单
DML看着最简单,INSERT、UPDATE、DELETE三个动词,但真正用好的前提是,你得理解事务、锁、以及大数据量下的性能影响。
3.1 INSERT的多种姿势与实际陷阱
INSERT是日常用得最频繁的DML语句。除了基本的INSERT INTO ... VALUES之外,还有几个高频姿势:
- INSERT ... SET:MySQL特有语法,可读性更好。
- INSERT IGNORE:遇到唯一键冲突时,直接忽略错误,不回滚当前语句之前插入的数据。
- INSERT ... ON DUPLICATE KEY UPDATE:遇到主键或唯一键冲突时,自动执行更新操作。这个特别好用,比如记录用户登录次数的表,第一次插入,以后每次冲突就+1。
- REPLACE INTO:遇到主键冲突就直接删除旧行、插入新行。注意,这会导致自增ID变化,而且会先DELETE再INSERT,代价不小,使用时要想清楚。
还有一个常见的性能问题:逐条INSERT大量数据时会有巨大的网络开销和事务日志开销。正确做法是批量插入,比如一条INSERT语句后面跟多组VALUES。有测试表明,一次插入1000行比逐条插入1000次性能好很多。如果再配合事务手动提交,效率会更稳定。
3.2 UPDATE的大坑:忘写WHERE
这大概是DML里最著名的事故类型了。一句UPDATE users SET status = 0没有WHERE条件,直接把全表状态全改了。等到发现的时候,逻辑已经不可逆地发生了。
防护措施:
- 先SELECT确认范围:执行UPDATE前,先跑一条相同WHERE条件的SELECT,确认影响行数符合预期。
- 开启事务:执行UPDATE后先不提交,SELECT验证结果,确认无误后再COMMIT。
- 使用LIMIT限制影响行数:批量更新时可以限制条数,分批执行。
- 在测试环境演练:特别是第一次写的复杂UPDATE语句,先在测试库用相同数据量跑一遍。
另外,UPDATE和锁的关系也很值得一提。UPDATE会对涉及的行加排他锁,高并发下容易阻塞其他读写。做大量更新时,要尽量选择好索引,让锁定的行范围尽可能小,减少对业务的影响。
3.3 DELETE的隐忧:数据空间不释放
DELETE删除数据后,表空间不会自动收缩,因为InnoDB只是把数据标记为删除,磁盘空间不会立即释放。这就是为什么你会发现,明明删了上百万行数据,表文件大小几乎没变化。
如果确定要清理一个表中的大部分数据,可以考虑:
- 先DELETE,再执行OPTIMIZE TABLE(会重建表,耗时长,适合低峰期)。
- 直接用TRUNCATE重建(如果完全不要数据)。
- 创建新表,把需要保留的数据INSERT过去,然后DROP旧表、RENAME新表。这是运维中常见的大表清理方式。
我用AI模拟过一个大表清理的完整流程,它给了我三种方案让我对比优劣。后来我实际在一个数据量比较大的表上做过类似操作,那种动态规划方案让我少走了很多弯路。
3.4 事务:DML的护身符
DML操作最好都放在事务里,这是一个即使写了好几年SQL也经常被忽略的好习惯。
事务的四大特性ACID——原子性、一致性、隔离性、持久性——不用死记硬背,理解这三个关键场景就够了:
- 转账操作:A扣钱、B加钱,两个操作必须要么都成功,要么都失败。
- 批量更新:一批数据更新到一半,发现某一条不合法,整体回滚,不留下脏数据。
- 并发控制:两个事务同时改同一条记录,通过锁机制避免互相覆盖。
MySQL默认自动提交,也就是说你写一条INSERT/UPDATE/DELETE,它自己就会立即提交,出问题没法回滚。所以我现在的习惯是:比较重要的DML操作,先SET autocommit=0,或者显式BEGIN/START TRANSACTION,操作完确认无误再COMMIT。
这里可以强调一下,AI辅助学习时,我让它用一个"转账场景"帮我画了事务的完整执行流程,包括提交、回滚、锁等待、死锁等分支。虽然是文字描述,但看完之后,对事务的理解明显深了一层。
4. DQL是SQL的重头戏:会写SELECT不算会查
DQL是Data Query Language,它的核心就是SELECT。但SELECT远不止"查出来"这么简单。真正考验一个开发者SQL水平的地方,几乎都在DQL:关联逻辑、分组聚合、子查询、排序分页、性能优化。
4.1 SELECT的执行顺序,比你想的更重要
很多人写SQL的时候脑子里没有"执行顺序"这个概念,以为SQL就是"先查表,再过滤,再显示"。但实际上,SQL的语义顺序和书写顺序不一样。
一个完整的SELECT语句,各子句的逻辑执行顺序大致是:
- FROM:确定数据源,包括JOIN操作。
- WHERE:对源数据逐行过滤。
- GROUP BY:分组。
- HAVING:对分组结果过滤。
- SELECT:选择列,可以做表达式计算。
- ORDER BY:排序。
- LIMIT:分页。
为什么这个顺序重要?举个例子。如果你想筛选出"平均分大于80分的学生",就不能写WHERE AVG(score) > 80,因为WHERE在GROUP BY之前执行,此时还没有分组,也没有聚合结果。你必须用HAVING AVG(score) > 80,因为HAVING在GROUP BY之后执行。
我让AI帮我出过一组"判断这段SQL哪里错了"的题,几乎每道都涉及WHERE和HAVING混用、SELECT别名在WHERE中直接引用这类问题。练完之后,看别人写的SQL,一眼就能看出问题出在哪个环节。
4.2 JOIN别只记语法,要理解驱动表和被驱动表
JOIN有INNER JOIN、LEFT JOIN、RIGHT JOIN、CROSS JOIN。会写语法不难,难的是理解执行时MySQL怎么处理这些表。这里需要引入一个概念:驱动表。
以LEFT JOIN为例,左表通常是驱动表。MySQL会先读取驱动表,再到被驱动表里找匹配行。怎么设计驱动表和被驱动表的顺序、怎么建索引,直接影响查询性能。
比如这个查询:
SELECT u.name, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = 1;如果users是小表(比如10万行),orders是大表(比如5000万行),那么更优的策略是让users作为驱动表,依次扫描每一行,到orders上通过user_id索引快速匹配。
如果反过来,orders作为驱动表扫5000万行,再回到users小表匹配,那性能就爆炸了。
总结一下JOIN优化的核心要点:
- 小表驱动大表。
- 被驱动表的连接字段必须有索引。
- 避免SELECT *,尽量只取需要的列,减少回表。
4.3 聚合函数+GROUP BY,别把小细节搞错
聚合函数(COUNT、SUM、AVG、MAX、MIN)配合GROUP BY,是DQL里最容易出逻辑错误的地方。
几个常见问题:
- COUNT(*)和COUNT(1)以及COUNT(字段)的区别:COUNT()统计行数,COUNT(1)基本等价于COUNT(),两者性能差异不大。COUNT(字段)只统计该字段非NULL的行数,这是重点,NULL不参与计数。
- GROUP BY之后只能放分组列和聚合列:MySQL在关闭ONLY_FULL_GROUP_BY模式时,允许SELECT非分组列,但这时的结果是随机的,容易踩坑。
- HAVING和WHERE别搞混:WHERE是分组前过滤原数据行,HAVING是分组后过滤分组结果。能在WHERE里过滤掉的,就不要放到HAVING。
我专门让AI帮我构造了一个"员工表,按部门分组,统计每个部门平均工资超过8000元的部门"的场景,让我手写SQL再让它点评。这种刻意练习比我背十遍语法都管用。
4.4 排序、分页与索引利用
ORDER BY默认升序ASC,降序DESC。排序如果走不上索引,MySQL就会使用文件排序(filesort),当数据量大时效率明显下降。
分页时常用的LIMIT偏移量越大,性能越差。比如LIMIT 100000, 10,MySQL会先读取前100010行,再丢掉前100000行,代价非常大。优化方案是"延迟关联"或"基于游标的分页"。
延迟关联示例:
SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 10 ) tmp ON t.id = tmp.id ORDER BY t.created_at DESC;先用子查询在主键索引上快速找到需要的数据ID,再用ID去关联原表获取其他字段,避免大量无用回表。
4.5 子查询的威力与窗口函数
子查询可以写在WHERE、FROM、SELECT等位置。但以往很多用子查询的场景,现在更推荐用窗口函数来解决,更简洁也更容易理解。
比如你想查"每个部门工资最高的员工"。传统写法是子查询+GROUP BY,但其实用窗口函数ROW_NUMBER()会更优雅:
SELECT dept_id, emp_name, salary FROM ( SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employees ) t WHERE rn = 1;窗口函数还有RANK、DENSE_RANK、SUM() OVER等,做排名、累计、移动平均都非常方便。MySQL 8.0才支持窗口函数,如果你还在用5.7,建议尽早规划升级。
5. 用AI辅助学SQL的正确打开方式
这次复盘过程中,AI的作用是"陪练+解释器+出题人"。但AI不是万能的,它也会出错,尤其是涉及具体版本的行为差异、最新特性时,必须保持怀疑态度,以官网资料为准。
5.1 我总结的AI学习提问模板
用得顺手了,就会发现AI学习SQL的正确姿势不是"让它直接给你答案",而是"让它引导你自己想出答案"。分享一下我常用的几个提问模板:
- 概念梳理型:请用面向新手的方式解释XXX,并给出至少三个实际业务场景。
- 对比分析型:请对比XXX和YYY的底层区别,从原理、性能、使用场景三个维度展开。
- 错误调试型:我有一段SQL,运行报错/结果不对,请帮我分析可能原因。然后把SQL贴给它。
- 练习出题型:针对XXX知识点,给我出5道从易到难的练习题,不要直接给答案,先让我写。
- 复盘总结型:请帮我总结一下我在XXX问题上的理解,并指出是否有遗漏或误解。
5.2 AI辅助学习的三个坑
第一个坑:AI会把旧版本的行为当作通用规则。MySQL的版本差异非常大。5.7和8.0在索引、窗口函数、默认字符集、DDL原子性等方面都不一样。AI有时候会混着讲,这时候一定要追问一句"这个行为在MySQL哪个版本有效"。
第二个坑:AI给的建议不一定适合你的数据规模。AI可能给你一个听起来很专业的优化建议,但它不知道你的表是1万行还是1亿行。比如全表扫描,在1万行的表上根本无所谓,在1亿行的表上就是灾难。所以学SQL优化时,一定要结合自己的实际数据规模去验证。
第三个坑:只看不练等于白学。SQL是技能,不是知识。AI给你讲得天花乱坠,你不亲手在数据库里跑一遍,永远记不牢。我一般会让AI生成一段示例数据,然后自己动手写查询,再让AI对比我的答案和标准答案,指出差异和优化空间。
5.3 推荐的学习路径
如果你的目标是快速掌握DDL/DML/DQL并能在实际项目里用起来,我建议按这个步骤来:
- 第一周:搞懂三类语句的职责边界,把DDL的建表语法和约束条件练熟。
- 第二周:练DML,重点理解事务和DELETE/UPDATE的坑。
- 第三、四周:主攻DQL,先学会单表查询,再学JOIN、GROUP BY、子查询、窗口函数。
- 接下来:用AI当出题官,每天练3-5条SQL,持续两周。
- 学有余力:研究索引和慢查询优化,这是SQL从"能跑"到"跑得快"的关键跨越。
每次学完一个章节,我都会要求自己:"不查资料,用脑子写一遍这个知识点能用到的SQL,然后在数据库里验证。"这一招特别能暴露自己到底哪里没懂。
6. 用一个小项目串起DDL/DML/DQL完整链路
理论讲再多,不如完整跑一遍。我这次复盘就手动搭建了一个简单的"用户订单管理系统",把三类语句全串起来了。
6.1 建表(DDL)
我建了users和orders两张表。users存储用户基础信息,orders存储订单信息,两表通过user_id关联。特别注意了字符集、自增主键、时间字段和必要的索引。
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shop; CREATE TABLE users ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', phone VARCHAR(20) NOT NULL COMMENT '手机号', nickname VARCHAR(50) NOT NULL DEFAULT '' COMMENT '昵称', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1=正常 0=禁用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINE=InnoDB COMMENT='用户表';这里有个细节值得注意:phone字段加了UNIQUE KEY,防的就是同一手机号注册两次。这种约束不是业务层能完全兜得住的,数据库层必须卡死。
订单表:
CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', order_no VARCHAR(32) NOT NULL COMMENT '订单号', amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0=待支付 1=已支付 2=已取消', paid_at DATETIME NULL COMMENT '支付时间', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINE=InnoDB COMMENT='订单表';amount用DECIMAL(10,2),是为了精确存储金额。如果以后订单金额可能超过9999万,DECIMAL(10,2)的整数部分最大是9位(99999999.99),一般电商足够了。
6.2 插入与更新(DML)
先插入几个用户和订单。
INSERT INTO users (phone, nickname) VALUES ('13800000001', '张三'), ('13800000002', '李四'), ('13800000003', '王五'); INSERT INTO orders (user_id, order_no, amount, status) VALUES (1, 'NO20260304001', 99.90, 0), (1, 'NO20260304002', 199.00, 1), (2, 'NO20260304003', 59.90, 0), (3, 'NO20260304004', 999.00, 1);更新一条订单状态,同时记录支付时间:
UPDATE orders SET status = 1, paid_at = NOW() WHERE order_no = 'NO20260304001';写UPDATE前,我习惯先用SELECT确认一下这条订单确实存在、状态确实还是0,再动手更新。养成这个习惯之后,基本告别了UPDATE全表事故。
6.3 查询与分析(DQL)
查一下每个用户的订单总金额和下单次数,只保留有订单的用户:
SELECT u.nickname, COUNT(o.id) AS order_count, SUM(o.amount) AS total_amount FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.id, u.nickname ORDER BY total_amount DESC;如果想看所有用户,包括没下过单的,把INNER JOIN改成LEFT JOIN就行。这种"加不加一个词,结果天差地别"的对比,自己跑一遍体会最深。
还可以查"待支付订单数大于等于2的用户":
SELECT user_id, COUNT(*) AS pending_cnt FROM orders WHERE status = 0 GROUP BY user_id HAVING pending_cnt >= 2;这里WHERE先把已支付和已取消的订单滤掉了,GROUP BY再按用户分组,HAVING过滤出符合条件的组。整条SQL的逻辑顺序一旦清楚了,写起来非常顺手。
6.4 清理数据(DDL)
演示完就可以清理掉了:
TRUNCATE TABLE orders; TRUNCATE TABLE users;注意我这里是演示环境,所以才敢直接TRUNCATE。生产环境谁敢这么干,估计会被运维追着打。
7. 一些实际操作中的体会
这次用AI辅助复盘MySQL的DDL/DML/DQL,我最强烈的感受是:AI最大的价值不是替你写SQL,而是逼你说清楚问题。在提问的过程中,你会发现自己哪里模模糊糊、哪里轻飘飘地以为自己懂了。这种"知道自己在边界"的感觉,比记住任何一条语法都重要。
另外,MySQL的版本差异是绕不开的坎。AI给出的答案,一定要在真实的MySQL环境里验证一遍。可以本机装一个最新版的MySQL 8.0,再装一个5.7的docker容器,两边跑同样的SQL,看行为差异。这种对比训练,会让你对"版本兼容性"有非常直观的体感。
最后再分享一个小技巧:学完任何一类语句,尝试"无参考默写"。关掉所有的文档和AI,手写一条符合复杂场景的SQL,然后去数据库里执行。能跑通,说明你真的会了;跑不通,说明你的盲区在那里。把盲区一个个补上,SQL这个技能就真的焊死在身上了。