聊SQL之前,我建议先把一个最基础但最容易被忽略的问题聊透:为什么同样是SQL,有的人写出来能跑十年不出问题,有的人写完上线一小时就把生产库搞挂了?很多时候不是语法不熟,而是根本没搞清楚自己正在用的这行语句,到底属于哪一类、有什么边界、能不能回滚、会不会锁表。DDL、DML、DQL、DCL这四个缩写,听起来像某种考试大纲,其实就是SQL按职能划分出来的四支队伍。这篇文章我就结合这些年在一线写SQL、调SQL、救SQL的实战经验,把这四类语言一次讲清楚,顺带把那些容易踩的坑也一并排掉。适合刚入门的开发、运维,也适合写了好几年SQL但从来没系统梳理过的老手。
1. SQL四类语言的整体认知
1.1 为什么要把SQL分成四类:一个部门的分工隐喻
你去看一本权威的数据库教材,或者官方文档,都会告诉你SQL语言按功能分为四类:DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)、DCL(数据控制语言)。但很少有人解释一个关键问题:这个分类到底有什么用?
我个人的理解是,这套分类本质上对应的是一个数据系统的“治理逻辑”。你把数据库想象成一家公司,表结构就是公司的组织架构,数据是员工,查询是业务报表,权限是门禁卡。DDL负责搭建和调整组织架构,DML负责员工的入职、调岗、离职,DQL负责盘点人力、输出报表,DCL负责决定谁有资格进这栋楼、能进到哪个楼层。
这个类比不是随便打的。它背后有一个核心的工程理念:不同类型的操作,对系统的安全要求、事务要求、性能影响是完全不同的。DDL一执行,可能整个表的读写都要停下来;DML一条不带WHERE的UPDATE,能把全表数据改得面目全非;DQL虽然看起来人畜无害,但一条笛卡尔积查询能把数据库CPU跑满;DCL配置失误,可能让一个普通账号拿到删库权限。所以,把SQL分成四类,不是教科书为了凑章节硬分的,而是为了让你在写每一行语句的时候,脑子里立刻反应出“我现在在做什么级别的操作,需要多谨慎”。
从面试的角度看,这道题几乎是必考题。从实战角度看,这四类语言的边界感越清晰,你写出的SQL越稳。很多初级开发把DELETE和DROP混在一起,把TRUNCATE当DELETE用,就是因为分类概念模糊。我后面会详细拆,这里先建立一个整体框架。
1.2 四类语言快速对照:先把边界划清楚
在展开细讲之前,我先把这四类语言的核心命令、典型用途、风险等级列一张对照表。这张表你可以存下来,面试前扫一眼,写SQL时也随时拿出来对照。
| 分类 | 英文全称 | 核心命令 | 作用对象 | 事务可控性 | 风险等级 |
|---|---|---|---|---|---|
| DDL | Data Definition Language | CREATE、ALTER、DROP、TRUNCATE | 数据库、表、索引、视图、存储过程等结构 | 大多数数据库隐式提交,不可回滚 | 高 |
| DML | Data Manipulation Language | INSERT、UPDATE、DELETE | 表中的数据行 | 可回滚(事务内) | 中高 |
| DQL | Data Query Language | SELECT | 表中的数据行(查询结果) | 无写入,主要是读 | 中(性能风险) |
| DCL | Data Control Language | GRANT、REVOKE | 用户、权限 | 隐式提交 | 高 |
注意看两个关键差异。第一,DDL和DML最容易混淆的地方在于:DML操作的是“数据”本身,DDL操作的是“数据的容器和规则”。TRUNCATE虽然删除的是数据,但它属于DDL,因为它是通过释放整张表的存储页来清空数据,而不是逐行删除,一旦执行就无法通过事务回滚。第二,DCL虽然命令数量少,但影响范围极大,一个错误的GRANT语句可能让全库数据暴露,它的风险等级一点都不比DDL低。
有了这张表打底,下面每一类我单独展开,结合具体语句和实战场景讲透。
2. DDL详解:定义数据结构的“施工队”
2.1 DDL命令全景与典型场景
DDL的全称是Data Definition Language,翻译过来就是数据定义语言。它管的是“结构”:数据库本身、表、字段、索引、视图、存储过程、触发器,全归它管。你可以把DDL理解成装修队的施工图作业——先确定墙体怎么砌、门窗怎么留、电路怎么布,房子才能住人。
常用的DDL命令其实就五个词:CREATE、ALTER、DROP、TRUNCATE、RENAME。我逐个说。
CREATE是建库建表建索引。比如在一个订单系统里,你要新建一张订单明细表,命令大概是这样的:
CREATE TABLE order_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单编号', sku_id BIGINT NOT NULL COMMENT '商品ID', quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量', price DECIMAL(10,2) NOT NULL COMMENT '成交单价', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', KEY idx_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';ALTER是修改已有结构。最常见的就是加字段、改字段类型、加索引。比如上线后发现订单表缺少支付时间,你就得ALTER:
ALTER TABLE order_detail ADD COLUMN pay_time DATETIME NULL COMMENT '支付时间' AFTER price;DROP是删库删表,属于危险级别最高的操作。DROP TABLE order_detail一执行,整个表连同数据、索引、约束全部消失。很多生产事故就是DROP语句写错环境、或漏了WHERE条件(虽然DROP本身没有WHERE)。
TRUNCATE是快速清空表数据,但保留表结构。它和DELETE DELETE FROM 的最大区别就是:TRUNCATE是DDL,无法通过事务回滚;DELETE是DML,在事务里可以ROLLBACK。
RENAME是重命名,比如把临时表改成正式表。这套动作在数据迁移、灰度发布时很常用。
2.2 建表改表的实操细节与避坑
说几个我在实际项目中反复踩过、也看别人踩过的DDL坑,每一个都值得你记住。
第一个坑:ALTER TABLE在大表上执行会锁表。在MySQL早期版本中,很多ALTER操作(尤其是加字段、改字段类型)会锁住整张表,导致线上业务读写全部阻塞。虽然现在InnoDB支持了ONLINE DDL,但并不是所有操作都能在线完成,比如在5.6以前的版本修改字段类型基本就是锁表。解决思路一般有三个:一是业务低峰期执行,二是使用gh-ost或pt-online-schema-change这类在线改表工具,三是控制单次变更的粒度。很多人上线前一分钟才想起来加字段,就为了图快直接在生产库ALTER,这是最危险的操作。
第二个坑:字段类型选择草率。我见过很多表把订单金额设计成FLOAT或DOUBLE,后来算总账对不上,查了半天才发现是浮点精度丢了。金额类字段必须用DECIMAL,字符串长度也要预估好。用户名用VARCHAR(255)未必错,但如果你给每个字段都默认255,索引大小会膨胀得很厉害,性能也会跟着下降。
第三个坑:TRUNCATE误用。TRUNCATE无法回滚,这是它的天然属性,因为它是DDL。你在测试库怎么玩都行,但在生产库执行TRUNCATE之前,一定确认三件事:这是不是那张要清的表?有没有备份?这个操作会通知业务方吗?我之前带团队时立过一个规矩:所有DDL语句必须经过评审并在工单系统留痕,生产环境禁止手敲DDL。这条规矩后来真的救过我们。
注意:DDL在许多数据库(如MySQL、Oracle)中执行时会隐式提交当前事务。也就是说,即使你事务里先执行了一条UPDATE,再执行了一条DDL,之前的UPDATE也会被一并提交,无法回滚。这个特性让“一条DDL毁掉一个事务”成为可能。
第四个坑:字符集和排序规则不一致。建表时没指定CHARSET,跟着库默认走,结果和另一张表JOIN时发现字符集不一样,查出来的数据乱码或者匹配不上。我建表的原则是:显式指定CHARSET和COLLATE,不要依赖默认值。同一套系统里所有表尽量保持字符集一致,最省心。
DDL是四类语言里最需要“流程敬畏”的。它不像是写个INSERT,错了可以ROLLBACK重来,而是执行完就板上钉钉。你在设计表结构时多花十分钟想清楚,就能避免未来无数个ALTER。
3. DML详解:操作数据的“搬运工”
3.1 增删改的核心语法与事务边界
DML全称Data Manipulation Language,数据操作语言。它管的是“数据行”的生命周期:新增、修改、删除。DML家族成员很精简:INSERT、UPDATE、DELETE。就这三个,但它们是日常开发中写得最多、也最容易出事的语句。
INSERT负责往表里加数据,可以单行插入、多行插入、甚至从另一张表查询结果批量插入:
-- 单行插入 INSERT INTO user_info (username, age, email) VALUES ('zhangsan', 28, 'zs@example.com'); -- 批量插入 INSERT INTO user_info (username, age, email) VALUES ('lisi', 30, 'lisi@example.com'), ('wangwu', 25, 'wangwu@example.com');UPDATE负责修改已有数据。注意:它如果没带WHERE条件,就意味着全表更新。这种操作不是语法错误,恰恰因为它“合法”,才更容易造成事故:
UPDATE user_info SET age = age + 1 WHERE username = 'zhangsan';DELETE负责删除数据行。同样是高危动作,凡是DELETE都建议先SELECT出来看一眼结果集,再决定要不要执行删除:
DELETE FROM order_detail WHERE order_no = '202501010001';DML和DDL最大的区别在于事务边界。DML操作放在事务里,可以在COMMIT之前通过ROLLBACK撤销。MySQL需要关闭自动提交(SET autocommit = 0)或者显式开启事务(BEGIN / START TRANSACTION),才能真正利用回滚能力。这个特性非常宝贵,但很多人根本没有用起来。
3.2 一条UPDATE改全表的教训:DML的高危场景
我举个例子。之前有个同事在做数据订正,目标是只把订单金额超过一万的记录打上“大额”标记。他写了一条:
UPDATE orders SET is_big_order = 1 WHERE amount >= 10000;这条本身没错。但当时他手滑,把WHERE条件漏掉了,变成:
UPDATE orders SET is_big_order = 1;全表几十万条数据,全部都变成了大额订单。发现问题后他傻眼了,因为数据库默认autocommit=1,每一句自动提交,根本没有回滚机会。最后只能从备份恢复,耽误了快两个小时。
这个案例说明三件事。第一,UPDATE和DELETE操作必须形成条件反射:先确认WHERE条件覆盖范围,再确认事务是否能覆盖整条操作。第二,自动提交在生产环境是个危险开关。线上写入比较频繁的业务,建议把读写账号的autocommit设为0,在应用层显式控制事务,增强可控性。第三,订正数据这类操作,最稳妥的流程是:先SELECT COUNT(*)看影响行数,再开启事务执行UPDATE,检查影响行数和预期一致后,追加COMMIT,否则ROLLBACK。
再补充一个DML性能相关的实战经验:大批量UPDATE或DELETE,不要一次性把几百万行的更新塞进一个事务里,也不要用一条语句直接怼。那样会长时间持有大量行锁,对线上其它事务造成阻塞,主从复制延迟也会飙升。正确做法是分批处理,比如每次处理5000行,循环执行,每批次之间加一个短暂停顿。这个思路在处理历史数据清理、大范围状态流转时特别管用。
还有一个容易忽略的点:DELETE的数据,在InnoDB中并不会立即从磁盘物理删除,而是先标记为删除,后续由purge线程异步清理。如果你频繁DELETE又频繁INSERT,表空间可能不会缩小,碎片会越来越多。这个时候通常需要考虑用OPTIMIZE TABLE或重建表来回收空间,但这又是一个DDL操作,同样要谨慎。
DML的通用原则就是:能带WHERE就带WHERE,能限制范围就限制范围,能分批就别一口吃成胖子,能用事务兜底就一定开事务。再配合慢查询日志把每次UPDATE、DELETE的执行时间盯住,基本能避开绝大多数数据事故。
4. DQL详解:查询数据的“侦察兵”
4.1 SELECT执行顺序与数据过滤逻辑
DQL全称Data Query Language,数据查询语言,核心就是SELECT。它不修改数据,只是从数据库中读取、过滤、聚合、排序。单论“对数据的破坏力”,DQL似乎不如DML,但论“对数据库性能的杀伤力”,一条写烂的SELECT绝对不遑多让。
我先把SELECT的语法执行顺序讲清楚。很多新手以为SQL是从SELECT开始执行的,这是大错特错。SQL的逻辑执行顺序和书写顺序不一样,真正的执行顺序大致是这样的:
- FROM:确定从哪张表(或哪些表)取数
- ON:在表的连接阶段执行等值或非等值匹配
- JOIN:把多张表的数据拼起来
- WHERE:对原表或JOIN后的结果集做行级过滤
- GROUP BY:分组
- HAVING:对分组后的结果做过滤
- SELECT:投影出需要的列,计算表达式
- DISTINCT:去重
- ORDER BY:排序
- LIMIT / OFFSET:限制返回行数
这个顺序才是SQL引擎真正的执行逻辑,很多人不关心它,结果就闹出了不少笑话。比如你写“WHERE后的条件里有别名”,大概率会报错,因为在WHERE执行阶段,别名根本还没有生成。再比如你误以为ORDER BY可以用WHERE的别名,其实只能用SELECT阶段的别名。
从性能角度看,一个最基本的原则是:能用WHERE提前过滤的,绝不要拖到HAVING或SELECT阶段才过滤。WHERE是行级过滤,越早过滤掉不需要的行,后面JOIN、GROUP BY时的数据量就越小,整个查询越快。
我在实际工作中最常用的DQL写法长这个样子,也推荐给你们作为常用查询模板:
SELECT u.username, COUNT(o.id) AS order_cnt, SUM(o.amount) AS total_amount FROM user_info u LEFT JOIN orders o ON u.id = o.user_id WHERE u.created_at >= '2024-01-01' AND o.status = 'paid' GROUP BY u.id, u.username HAVING COUNT(o.id) > 3 ORDER BY total_amount DESC LIMIT 50;这个查询里有JOIN、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT,基本覆盖了日常业务的高频查询场景。你可以对照前面说的执行顺序,自己推演一遍:先定位user_info和orders,再LEFT JOIN,再对“2024年之后注册、订单已支付”的行过滤,再按用户分组,再筛选出订单数大于3的分组,最后排序和取前50条。这样推演下来,你的SQL理解会更深。
4.2 慢SQL查询的排查思路
DQL最常见的线上问题就是慢查询。一个慢SELECT一旦占满了数据库连接,整个服务的接口都会跟着变慢,严重时能把数据库拖垮。
排查慢SQL,我建议按下面的步骤来。第一,打开慢查询日志(slow query log),把执行时间超过阈值(比如1秒)的SQL全部记录下来。第二,拿到慢SQL后先用EXPLAIN看执行计划,重点看type列和rows列,如果出现ALL全表扫描、rows扫了几百万行,那基本就是索引问题。第三,检查WHERE条件里的字段是否有索引,以及索引是否被“破坏”。比如在索引列上套了函数、做了隐式类型转换,或者用了前置通配符LIKE '%xxx',都会导致索引失效。
我举一个很常见的例子。订单表查询:
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';这条很直观,但DATE(created_at)把created_at包进了函数里,导致created_at上的索引无法使用,会触发全表扫描。正确写法应该是范围查询:
SELECT * FROM orders WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-01-02 00:00:00';这种用函数包裹索引列的情况,在代码评审里比比皆是。很多人习惯拿日期函数直接套在时间字段上,图一时方便,结果就是把性能坑了。
再补充一个JOIN性能的坑。两表JOIN的时候,驱动表(前面的表)最好是小表,被驱动表的关联字段必须有索引。如果两张表的关联字段都没索引,MySQL就会对其中一个表的每一行都去全表扫描另一个表,也就是典型的“笛卡尔积式”查询,行数直接爆炸。我见过一条没索引的JOIN查询,两张表各几万行,扫出几亿行中间结果,数据库瞬间打满。
DQL的核心修炼目标,不是把所有SQL写成多复杂,而是写得精准、高效、可控。能用索引就用索引,能过滤就早过滤,能不SELECT *就别SELECT *,该分页就分页。一个执行计划良好的SELECT,可以撑起一个高并发系统;一条烂SELECT,分分钟放倒一台高配服务器。
5. DCL详解:控制权限的“保安队长”
5.1 用户授权与回收权限的标准姿势
DCL全称Data Control Language,数据控制语言。它管的是“谁能在数据库上做什么”,核心命令就两个:GRANT(授权)和REVOKE(回收权限)。在MySQL里,和权限相关的还有一个REVOKE ALL PRIVILEGES,可以一口气收回某个用户在当前库的所有权限。
授权语句的标准格式大概是这样:
-- 创建一个只能查和插的应用账号 CREATE USER 'app_user'@'%' IDENTIFIED BY 'StrongPassword123!'; -- 授予查询和插入权限,但只针对mydb库的所有表 GRANT SELECT, INSERT ON mydb.* TO 'app_user'@'%'; -- 刷新权限(部分版本需要,或者用ALTER USER方式避免) FLUSH PRIVILEGES;回收权限的写法:
REVOKE INSERT ON mydb.* FROM 'app_user'@'%';注意,CREATE USER和GRANT这些操作通常只有管理员权限的账号才能执行。生产环境里,root或者SA这类超级管理员账号应该被严格保护,尽量不要让应用连接使用,应用连接只应该拥有它“业务所需最小权限”。
DCL在实际工作中有几个非常典型的使用场景。第一个是应用账号权限最小化:一个只读报表系统,连接数据库的账号就应该只有SELECT权限,最多加上只读视图访问权,绝不能用root或SA。第二个是开发测试环境与生产环境的账号隔离:开发账号可以在测试库随便挥霍,但连到生产库的账号必须按需授权。第三个是人员的权限回收:员工离职或转岗,系统管理员要第一时间REVOKE其账号权限,这个动作看起来不起眼,但往往能避免真正的数据泄露。
5.2 权限最小化原则和常见误配置
关于DCL,我最有感触的一点是:权限最小化不是安全团队的额外要求,而是每个数据开发者必须刻进DNA的习惯。因为DCL配置一旦失误,后果比一条烂SQL严重得多。
先说一个我踩过的坑。有一年我们给一个数据分析师开账号,图省事,直接把整个生产库的SELECT权限交出去了。半年后这哥们写了个笛卡尔积查询,差点把生产库拖垮。后来我们复盘,发现他根本不需要访问全库几千万行的底层表,他只需要通过一个物化视图看每日汇总数据就好——访问视图的权限,完全够用。从那以后,我定的规矩是:默认只授予最少的表级权限,连库级权限都尽量避免,必要时直接建视图给查询账号,从源头上限制数据访问范围和扫描范围,权限和性能风险同时被压下来了。
还有一个常见误配置:把GRANT的权限范围写成mydb.*,但用户只需要访问其中一两张表,结果这个用户对整个库的所有表都有操作权。如果再加上误授了DROP、DELETE之类的高危权限,一个小小的账号漏洞就可能变成删库事故。
注意:不要为了图省事给普通账号授予 ALL PRIVILEGES。权限粒度永远是“够用就好”,这句话在数据安全领域就是铁律。
再聊一个DCL和SQL注入之间的关系。很多人觉得SQL注入是应用层问题,跟数据库权限没什么关系。其实权限设计得当,即使应用被注入了,损失也会被控制住。比如只读报表的账号只有SELECT权限,那么注入进来最多只能读数据,没办法写数据、删表、改库。所以,DCL配置本身也是对抗SQL注入事故的最后一道防线。
从技术栈的角度多说一句:不同数据库的DCL语法大同小异,MySQL用GRANT/REVOKE,SQL Server有一整套CREATE LOGIN、CREATE USER和权限管理的体系,Oracle还有角色(ROLE)机制。但权限最小化这个原则是全通用的。你只需要理解DCL的定位是“管人、管权限、管准入”,具体语法用的时候查文档就好。
6. 常见问题与排查技巧实录
6.1 DELETE、TRUNCATE、DROP到底怎么选
这是每一届新人都会踩的经典三连坑。我直接给你一个对比表和选择建议,你就不会再迷糊了。
| 操作 | 分类 | 删除内容 | 是否可回滚 | 是否保留表结构 | 速度 | 常见用途 |
|---|---|---|---|---|---|---|
| DELETE | DML | 数据行 | 事务内可回滚 | 保留 | 慢(逐行删) | 按条件删除业务数据 |
| TRUNCATE | DDL | 数据行 | 不可回滚 | 保留 | 很快 | 快速清空表数据 |
| DROP | DDL | 数据和表结构 | 不可回滚 | 不保留 | 最快 | 删除整张表 |
你的选择逻辑应该是这样:只删部分行,用DELETE;清空全表但还想留着表结构重用,用TRUNCATE;整张表不要了,连同数据、约束、索引全部抛弃,用DROP。
这里我再给一个特别提醒:TRUNCATE执行前一定想清楚,它不会逐行触发DELETE触发器,也不会返回逐行影响计数,在事务里它通常会隐式提交,回滚基本指望不上。很多人在生产环境里本该写DELETE FROM order_temp WHERE batch_id = 5,结果手误写成了TRUNCATE table order_temp,然后就没有然后了——全表都没了。
6.2 SQL执行“卡住”的排查思路
SQL“卡住”是DML和DQL都容易遇到的问题。很多人第一反应就是数据库性能差,实际上“卡住”往往不是慢查询,而是被锁等住了。
在MySQL InnoDB里,最常见的等待场景有两个:行锁等待和元数据锁(MDL)等待。行锁等待很好理解:你的UPDATE想改一行数据,但这一行正被另一个事务锁住未提交,你的更新只能等。元数据锁则更容易被忽略:通常是因为有一个长事务一直没结束,导致其它对该表的ALTER或查询都被阻塞。
排查步骤我写一下。第一,用SHOW PROCESSLIST查看当前所有连接,看看哪些SQL在Sleep、哪些在Update、哪些在Waiting for table metadata lock。第二,用SHOW ENGINE INNODB STATUS查看最近的事务和锁信息,找出“谁锁了谁”。第三,如果发现某个长事务持有锁很久不释放,和业务方确认后,可以KILL掉对应连接。
有一个真实案例:早上业务方说订单表查不到数据,一点查询就超时。我上去一看,是昨天有人开了个事务执行了UPDATE,但没COMMIT也没ROLLBACK,就直接下班了。这一行数据被锁了一整晚,全表所有和这行相关的查询都在等这个锁,线上体验那就是一片哀嚎。所以,DML操作一定要保持事务短小精悍,开完事务尽快提交或回滚,尤其是“事务里还要调用外部接口”这种操作,尽量避免。
6.3 参数化查询与SQL注入防护
SQL注入虽然不属于DDL、DML、DQL、DCL任何一种语言本身,但它和DQL、DCL的关系非常密切,而且是个高频热搜词,这里必须提一下。
很多注入漏洞的根本原因是“把用户输入直接拼进SQL字符串”。比如登录功能里有人这么写:
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";如果username输入的是' OR '1'='1,拼接出来的SQL就变成了:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'any';这就是经典的万能密码绕过思路,后果是整个用户表的信息都可能被拖走。正确的解法是用参数化查询(PreparedStatement),让数据库把参数当作数据而不是SQL的一部分来绑定执行。这也是为什么我在项目里对SQL编写有个硬性要求:所有动态SQL拼装必须走参数化接口或ORM的参数绑定机制,绝不允许字符串拼接。
从DCL的角度,前面也说了,即使最坏情况发生了,账号权限如果我们做了最小化,注入者能做的破坏也极其有限。
6.4 日常开发中的SQL工作流
这个内容其实不在四个缩写里,但我觉得它对实践非常有帮助。无论你是开发还是运维,一套标准的SQL工作流都应该包含这些动作:先在测试环境推导和执行,确认执行计划和影响行数;再在预发布环境跑一遍;最后才上生产,而且尽可能采用自动化变更工具,避免手工连生产库敲语句。
我的个人习惯是:每条要上生产的SQL,都必须带着回滚方案一起写。DML的DDL改成什么,回滚时如何恢复;DDL的加字段,回滚脚本也要提前准备好;DCL授权错了怎么REVOKE。这不是流程主义,而是我见过太多“直接执行、出问题不知道怎么退回去”的现场事故后,总结出来的最低成本安全网。
这四个缩写的知识本身不难,难的是把分类意识内化成一种肌肉记忆。你看到CREATE就想到DDL、想到不可回滚、想到可能锁表;看到UPDATE就想到WHERE、想到事务、想到影响行数;看到SELECT就想到执行计划、想到索引;看到GRANT就想到最小权限、想到安全边界。做到这一步,你写SQL的段位就已经超过大多数人了。