三年前,我以为做一个Java后端开发,SQL只要能写增删改查就够了。直到有次线上列表接口超时,运维把慢SQL日志甩到我面前,那条SQL跑了8秒,我盯着它看了十分钟,连EXPLAIN都不会用,最后是DBA加了两个索引救的场。从那次起我才明白,所谓"Java后端",一半的功夫其实在SQL里。这篇内容就是我从Java到SQL的完整实践路径,包含执行计划怎么看、慢SQL怎么查、索引原理怎么理解、Java代码里写SQL的工程细节,以及面试时这两块知识怎么结合,希望对跟我一样从课堂学Java、工作后又被迫补SQL的人有用。
1. 从Java到SQL:为什么写了几年CRUD,我反而回头补SQL基础
1.1 一次让我在会议桌上沉默的线上事故
当时我们做的是订单管理系统,MyBatis封装得挺完善,业务代码里基本看不到SQL。某天客户反馈,订单列表翻到后面几页特别慢,运维给出数据库监控截图:数据库CPU接近100%,接口P99耗时从300ms涨到8秒。
我当时的第一个反应是"数据库出问题了",甚至想申请扩容。结果DBA从慢查询日志里拉出来一条SQL:
SELECT * FROM orders ORDER BY create_time DESC LIMIT 100000, 20;
DBA问我:"这条SQL哪里有问题?"我看了半天,说:"这不就是个普通的排序分页查询吗?"
DBA说:"你没有索引,深度分页,还排序,当然慢。"那一刻我才发现自己对SQL的理解停留在"能查出结果就行",完全不知道数据库是怎么执行这条SQL的。我连执行计划都不会看,更别说分析为什么慢。
1.2 ORM让业务代码好写了,也把SQL问题藏了起来
很多Java工程师应该都有类似的经历:MyBatis、JPA、Spring Data帮我们把表结构映射成对象,写CRUD确实快,但代价是SQL被藏进了XML和注解里,我们对SQL执行计划、索引、锁的感知会越来越弱。
我见过不少工作两三年的Java开发,SELECT、INSERT、UPDATE写得飞起,但一遇到"SQL慢"就只会加索引或者找DBA。这不是态度问题,是ORM把SQL的复杂度完全屏蔽掉了,大脑会默认"这是框架的事"。
但有个关键认知必须建立:ORM只是帮你生成了SQL,数据库执行SQL的成本一分钱都没少。接口超时、CPU告警、死锁报错,最终都会以各种方式回到你面前。你能看懂Java堆栈,却看不懂SQL执行计划,这中间的信息差就是事故发生时最难受的地方。
1.3 这篇文章适合谁
如果你是下面这三类人之一,这篇内容可能会帮你少走点弯路:
- Java入门、准备找工作:面试里SQL优化几乎是必考题,光会写CRUD远远不够。
- 工作1-3年的后端开发:项目里已经遇到慢SQL、死锁、连接池打满,但解决起来还是很被动。
- 复习Java面试和SQL面试题的人:八股文背了不少,但一被追问"为什么"就容易露馅。
这篇内容不是SQL语法教程,而是从一个Java开发者的视角,把"写Java"和"写SQL"这两件事串起来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SQL执行计划:Java开发者最容易忽略的调试器
2.1 把EXPLAIN当成你的"SQL调试器"
Java调Bug,靠的是IDE断点、日志、堆栈。SQL调Bug,靠的就是EXPLAIN。它的作用是把一条SQL的执行过程摊开给你看:数据库先查哪张表、用了哪个索引、预估扫多少行、有没有额外的排序和临时表操作。
基础语法很简单:
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
在MySQL 8.0里还可以用EXPLAIN ANALYZE,它会真实执行这条SQL,并给出每一步的耗时;EXPLAIN FORMAT=JSON可以看到更细的成本信息。我平时在生产环境不方便跑真实SQL时,会先用EXPLAIN看执行计划,再用FORMAT=JSON看关键环节。
这就像一个调试器,把数据库的执行策略原原本本暴露在你面前。读懂了它,你才真正开始具备"调SQL"的能力。
2.2 执行计划里先看哪几个字段
一条完整的EXPLAIN输出有十几列,新手容易懵。别管其他,先盯住这几个:
| 字段 | 含义 | 关注点 |
|---|---|---|
| type | 访问类型 | 性能从好到坏:system > const > eq_ref > ref > range > index > ALL |
| key | 实际用到的索引 | 是NULL就说明没走索引 |
| rows | 预估扫描行数 | 这个数字越大,执行成本越高 |
| Extra | 额外信息 | 出现Using filesort、Using temporary都是性能隐患 |
type这一列尤其关键。ALL代表全表扫描,是最差的;index代表全索引扫描,好一点但也不一定快;ref和range是正常范围查询;const和eq_ref说明按主键或唯一索引精确命中,非常快。
举个实际例子。一张500万行的订单表,执行:
SELECT * FROM orders WHERE user_id = 123;
如果user_id没有索引,EXPLAIN结果里type=ALL,rows=500万,Extra是空,意思是把整张表扫一遍。给user_id建完索引再EXPLAIN,type变成ref,rows变成几十行,性能天差地别。
2.3 从执行计划里一眼看出"全表扫描"和"文件排序"
我见过很多Java写得好的人,第一次看EXPLAIN时,眼睛只盯着key是不是NULL,却忽略了Extra里的"InUsing filesort"和"Using temporary"。
Using filesort的意思是:排序没有走索引,MySQL必须在内存或磁盘上额外做一次排序操作。它不一定慢,但数据量一大,排序成本就会暴涨。Using temporary的意思是用了临时表,常见于GROUP BY、DISTINCT、多表JOIN场景,同样需要警惕。
我自己的习惯是:看EXPLAIN时先看type,如果出现ALL就说明索引有问题;再看key,确认实际用的索引是不是自己预期的那一个;最后看Extra,只要出现filesort或temporary,就问自己一句——这查询能不能用索引替代排序和临时表?这三个问题排查完,80%的慢SQL都能找到改进方向。
3. 慢SQL排查实录:一次分页查询拖垮接口的完整定位链路
3.1 事故背景:翻页越深,接口越慢
回到开头的线上事故。业务方反馈订单管理系统的列表页,翻到第几百页之后接口直接超时。当时业务代码用的是PageHelper,逻辑很简单,最终生成的SQL就是常见的LIMIT分页。
接到告警后,我按下面的步骤做了定位:
第一步,看应用监控。发现接口耗时的瓶颈集中在数据库调用,Java方法本身没跑什么复杂逻辑。
第二步,打开MySQL慢查询日志:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
这样超过1秒的SQL都会被记录下来。线上环境不要随便打开,但临时确认问题后一定要记得关闭。
第三步,从慢查询日志里拿到罪魁祸首,就是那条:
SELECT * FROM orders ORDER BY create_time DESC LIMIT 100000, 20;
第四步,EXPLAIN分析。结果让我印象深刻:type=ALL,rows=500万,Extra里有Using filesort。等于说这条SQL要扫描500万行,排完序,再丢掉前10万条,最后返回20条。
3.2 深度分页为什么慢,慢在哪里
LIMIT 100000, 20的语义是:先取出前100020条数据,然后扔掉前100000条,只返回最后20条。数据库不会智能地"从第100001条开始读",它必须一行一行数过去。
类比一下:让你在纸质Excel表格里找到第5000行,你不能直接翻到那一行,而必须从第一行开始数。数据量小的时候没感觉,数据量大了就非常痛苦。
更麻烦的是ORDER BY create_time DESC。这条SQL先要对500万条数据做排序,再去做LIMIT。如果create_time上有索引,数据库可以按索引顺序直接跳着读;但当时没索引,MySQL只能把所有数据拿出来排序,排序的结果很可能还要落盘临时文件,这个代价在并发请求下会把数据库CPU直接打满。
3.3 我是怎么把它优化成毫秒级的
那次事故之后,我针对深度分页做了三套替代方案,现在已经成了我的标准操作。
方案一:延迟关联。先只查主键,再用主键回表查完整数据。SQL大致这样:
SELECT * FROM orders
WHERE id IN (
SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 20
);
原理是:子查询只查id,而id是主键,可以走覆盖索引,MySQL不需要回表读整行,扫描成本大幅下降。拿到20个id之后,再回表查完整记录,总成本比直接SELECT *低很多。
方案二:游标分页,彻底告别OFFSET。如果产品允许"上一页/下一页"而不是"第N页",就用这个方式:
SELECT * FROM orders
WHERE create_time < '上次返回的最小创建时间'
ORDER BY create_time DESC
LIMIT 20;
它在业务场景里很常见,微博、电商Feed流基本都是这种模式。每次查询都是基于上次游标继续往下找,数据库只要扫20条就够了,跟页数无关。
方案三:给排序字段建联合索引。当时我在(create_time, id)上建了复合索引,让ORDER BY走索引,去掉Using filesort。但要注意,如果查询还要回表拿整行数据,深度分页的"大OFFSET扫描"问题依然存在,所以方案三通常要配合方案一或方案二一起用。
3.4 热搜词里的"并行SQL优化"到底指什么
热搜词里有个"并行sql优化",我得专门说下,因为很多人在问,但理解其实比较模糊。
并行优化的本意是让一条大SQL利用多核CPU同时处理,比如大表JOIN、聚合计算,数据库底层可以做并行扫描、并行聚合。MySQL 8.0在这方面有一些改进,但默认不会把所有单条查询都并行化,因为并行会消耗更多CPU和内存。
在业务系统里,我更建议先做单条SQL优化,把全表扫描、深度分页、filesort这些问题解决掉。如果还慢,再考虑把大任务拆成多个小任务并行执行,最后合并结果。比如按月份拆开统计再UNION ALL,但要注意事务边界和锁冲突,别为了并行把数据库搞得更乱。
4. 索引的本质:把B+树讲成Java工程师熟悉的TreeMap
4.1 索引就是数据库里的TreeMap
很多Java开发者对集合很熟,尤其TreeMap,知道它内部是红黑树,key有序,查找时间复杂度O(log n)。数据库索引想解决的问题完全一样:让查找变快、让范围查询有顺序。
但数据库的数据在磁盘上,不可能直接用红黑树。红黑树每个节点只存一个key,树的高度会很高,磁盘IO次数会爆炸。于是数据库用了一种更"矮胖"的结构——B+树。
做一个生活化类比:TreeMap是内存里有条理的便签本,索引是磁盘上有条理的档案柜。便签本翻起来快,档案柜虽然要找,但只要层数少,找起来也不慢。
4.2 为什么索引偏偏是B+树不是二叉树
数据库读写以"页"为单位,一个页默认16KB。每读一个页,就是一次磁盘IO,性能远低于内存操作。所以核心目标是:尽量减少磁盘IO次数。
B+树的一个节点可以存多个键,通常一个节点就能存几百上千个键。这样树的高度会非常矮,三层B+树轻松撑起上亿条数据。换句话说,查找一条数据最多只需要三四次磁盘IO,这就是B+树存在的核心理由。
还有一个关键设计:B+树的叶子节点用链表串起来,数据按顺序排列。这意味着范围查询比如WHERE id BETWEEN 100 AND 200,只要找到起点,顺链表往后扫就行,效率极高。红黑树、二叉树做范围查询需要回溯父节点,在磁盘场景下并不合适。
4.3 最左前缀、回表、覆盖索引,一次讲透
联合索引(a, b, c)是Java后端面试高频问题,很多人只知道"最左前缀",但不太理解为什么。
最左前缀说的是:查询条件命中索引必须从最左侧列开始。比如索引是(user_id, create_time),你可以只查user_id,也可以查user_id和create_time,但直接查create_time就匹配不上索引。原因是B+树先按第一列排序,第一列相同才按第二列排序,你跳过了第一列,树结构就没办法直接定位。
回表:二级索引的叶子节点只存索引列和主键,不存整行数据。查到主键之后,还要再到主键索引里按照主键查一次完整记录,这个步骤叫回表。回表不是错误,但回表次数多了,性能就会下降。
覆盖索引:如果查询需要的列全部包含在索引里,那MySQL就不需要回表了。比如SELECT user_id, create_time FROM orders WHERE user_id = 123,而(user_id, create_time)正好是联合索引,数据直接读索引就有,性能非常好。设计索引时优先考虑覆盖,是SQL优化里性价比很高的手段。
索引下推(ICP)是MySQL 5.6引入的优化:在索引遍历过程中,直接把不满足条件的记录过滤掉,减少回表次数。比如联合索引(user_id, create_time)查询user_id=123 AND create_time>'2023-01-01',MySQL会在索引层先过滤create_time,而不是把所有user_id=123的主键都回表再去过滤。
4.4 从"快速排序/冒泡排序"热搜词聊聊SQL排序
热搜词里"快速排序java实现""冒泡排序java"频繁出现,这让我想到SQL里同样高频的排序问题。
Java里自己写排序,冒泡排序O(n²)只适合小规模数据,快速排序O(n log n)是内存排序的常用选择。但SQL里ORDER BY的优化逻辑完全不同:最高效的排序是"不排序"。
什么意思?如果排序字段正好在索引里,MySQL直接按索引顺序读数据,天然就是有序的,根本不需要额外的排序操作。如果排序字段没有索引,MySQL就要把查询结果全部拿到内存或临时文件里做filesort,数据量大时非常耗资源。
这也是为什么WHERE条件里过滤字段和ORDER BY排序字段经常一起建联合索引。举例:查询某个用户的订单按创建时间倒序,建联合索引(user_id, create_time),不仅过滤命中索引,排序也走索引,整个查询的执行计划里就看不到Using filesort。
所以,每次搜"快速排序Java实现"的时候,可以顺手想想:Java的排序算法是主动排序,SQL的排序优化很多时候是尽量让数据本来就按目标顺序存储。这个思维转换,我觉得比再刷一遍算法题更值钱。
5. Java代码里写SQL的工程细节:预编译、连接池、事务边界
5.1 PreparedStatement为什么能挡掉"万能密码绕过"
热搜词"sql注入万能密码绕过"挺吓人的,很多初学者好奇甚至想试试。它本质上是字符串拼接SQL导致的漏洞:用户的输入被当成了SQL代码的一部分,而不是单纯的数据。
拿登录场景举例,如果代码这么写:
SELECT * FROM user WHERE username = 'admin' AND password = '输入的密码';
攻击者可以在密码框里输入一段SQL逻辑,比如让password字段永远成立,这样就能绕过校验。网上很多资料会给出具体的绕过字符串,我不想在这里复述那类payload,因为这不是什么技巧,这是一个真实漏洞。真正值得研究的是怎么从根上堵住它。
PreparedStatement解决的方式非常优雅:先把SQL骨架发给数据库预编译,参数用占位符?代替,之后再用setString等方法传入值。数据库对占位符的处理是把值当作纯数据,不会解析成SQL逻辑。
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM user WHERE username = ? AND password = ?"
);
ps.setString(1, username);
ps.setString(2, password);
这样,即使用户输入了一堆SQL关键词,它也只是一个普通字符串。对应到MyBatis里,规则就是能用#{}就不用${},#{}是预编译参数,${}是字符串拼接,后者给用户可控的内容,等于把SQL注入的大门敞开了。
5.2 连接池:Java应用性能的隐形瓶颈
Java应用每次访问数据库如果不走连接池,而是一次请求新建一个连接,那性能会非常难看。创建连接要TCP握手、数据库认证、分配资源,一趟下来几十毫秒甚至上百毫秒,高并发下根本扛不住。
连接池做的事就是把它变成"用一条连接执行一次查询,用完还回去",达到复用的效果。Spring Boot默认的HikariCP性能很好,国内很多项目也喜欢用Druid,因为它带监控页面,方便看连接池占用情况。
关键参数我列几个:
- minimum-idle:最小空闲连接数,保证池里始终有连接可用。
- maximum-pool-size:最大连接数,这个不是越大越好,每个连接都会占用数据库资源,过大会把数据库压垮。
- connection-timeout:获取连接的超时时间,长时间拿不到连接说明连接被占满或SQL跑太久。
- max-lifetime:连接最大存活时间,防止数据库侧断开了连接,而应用还在用。
我踩过一个坑:连接池参数调得很大,但真正的问题是某条慢SQL占住了所有连接。客户端排队等连接,池子越调越大只会让数据库更快崩溃。遇到"Connection is not available, request timed out"这类报错,先跑一句SHOW PROCESSLIST看有没有长时间未结束的SQL,再调整连接池参数,顺序别反了。
5.3 事务边界:为什么长事务会让SQL变慢
Java里一个@Transactional注解非常方便,但很多人对它的范围控制很随意。比如在事务方法里调用远程HTTP接口,或者做耗时的文件处理,这就把事务拉得很长。
长事务会带来几个问题:持有数据库锁的时间变长,其他请求全部卡在锁等待上;连接被长时间占用,连接池很快被打满;undo log不断膨胀,影响数据库清理和备份。
排查长事务有一个很实用的SQL:
SELECT * FROM information_schema.innodb_trx;
它可以看当前所有事务的开始时间和执行状态。如果发现某个事务开始时间很早,还没提交,那基本可以确认是长事务。
我的实践原则是:事务只包住必要的写操作,查询和外部调用放到事务外面。读多写少的场景,事务范围越小越好,宁可多写几个方法拆分,也别图省事把一个长流程全部框进去。
6. 环境与工具链:把Java和SQL的调试环境一次配明白
6.1 Java环境变量配置与"源发行版17"报错
热搜词里"java环境变量配置""java: 警告: 源发行版 17 需要目标发行版 17"都是很常见的问题,我多说两句。
Windows下装JDK,JAVA_HOME指向JDK安装目录,比如D:\Java\jdk-17,然后把%JAVA_HOME%\bin加到PATH里。命令行输入java -version和javac -version能正常输出版本号,说明环境变量配好了。CLASS_PATH现在基本不需要手动配,JDK 9之后模块化默认行为已经覆盖了。
"源发行版17需要目标发行版17"这个警告的根源是:代码编译级别和运行级别不一致。最常见的情况是项目用Maven,但IDEA的JDK设置和Maven的 compiler 配置对不上。
解决方式分两步:
第一步,在pom.xml里明确统一:
第二步,在IDEA里检查File -> Project Structure -> Project,把SDK和Language level都改成一致的版本。如果项目要求用Java 8,那SDK就选jdk1.8,Language level也选8,别再让17陪着8,肯定会出警告。
6.2 数据库环境:MySQL 8.0与SQL Server的安装注意点
热搜词里"sql server"相关搜索很多,比如"sql server 2019安装教程""sql server 2008 r2下载"。
如果你用的是MySQL,我建议直接从8.0开始。安装时注意字符集选utf8mb4,否则存Emoji或生僻字会报错。Linux环境下的my.cnf建议加上这几行:
[mysqld]
character-set-server=utf8mb4
default-time-zone='+08:00'
slow_query_log=1
long_query_time=1
慢查询日志建议开发环境直接打开,这样你自己的测试接口出了慢SQL能马上看到。
SQL Server需要注意的点是安装时的认证模式选择,建议选"混合模式",同时配好sa密码,否则后面用工具连不上。SQL Server 2008 R2在Windows 2016上经常有兼容性问题,有条件直接用高版本。
开发机没必要老老实实装完整数据库,Docker一把梭最快:
docker run -p 3306:3306 --name mysql8
-e MYSQL_ROOT_PASSWORD=root
-d mysql:8.0
不过我还是建议至少完整安装过一次MySQL或SQL Server,因为面试被问"数据库怎么装的"时,有真实经验比只会Docker跑容器要靠谱得多。
6.3 DBeaver导入SQL文件与SQL代码排版
热搜词里"dbeaver导入sql文件""sql代码排版工具"也上榜了,说明很多人在实际工程里被这两个问题卡过。
DBeaver是免费好用的数据库客户端,支持MySQL、PostgreSQL、SQL Server等。导入SQL文件的正确姿势是:先建立数据库连接,创建好目标数据库,然后右键数据库 -> 工具 -> 执行脚本,找到你的.sql文件执行。
导入大SQL文件有几个坑:
- 文件编码要和数据库字符集一致,否则中文全变乱码。MySQL脚本开头建议加一句
SET NAMES utf8mb4;。 - 先选中目标库,再执行脚本,否则表可能建到默认库里去了。
- 几千行的SQL别直接在编辑器里粘,让DBeaver直接按脚本执行,或者用命令行
source导入,速度快也稳定。
SQL代码排版工具,我推荐IDEA里的SQL格式化插件,或者在线工具sqlformat。团队协作时,SQL语句大小写统一、JOIN条件对齐,不是强迫症,是为了让一条慢SQL里的问题能被一眼看出来。你见过一段挤成一行、没有缩进的几千字SQL吗?那简直是灾难。
7. 从热搜词看Java+SQL的真实考点:八股文与实战能力的差距
7.1 为什么Java面试八股文里总有SQL
"java面试八股文""java面试大全""java面试题"这些热搜词常年居高不下,说明很多人在背面试题。但SQL这个方向,面试官很少只背题目就能满意,因为它是Java后端和数据库之间最直接的桥梁。
面试中考SQL的常见切入点:索引为什么快、事务隔离级别、锁机制、慢SQL怎么排查。这些知识点不是孤立存在的,背后都有真实的工程场景。比如问"索引为什么快",本质上是在考你对B+树和磁盘IO的理解;问"慢SQL怎么排查",其实是在考你遇到线上故障的定位思路。
我见过不少把八股文背得很好的人,问"EXPLAIN里的type有哪几种、从好到坏怎么排"能答上来,但给他一条真实SQL却不知道怎么分析。八股文是启动材料,不是终点。
7.2 去重查询、BETWEEN AND 这些"基础词"为什么总被搜
"sql语句去重查询""sql between and的用法总结""清洗---sql语句去重"这些热搜词看起来基础,但实际使用中很容易翻车。
DISTINCT和GROUP BY去重有什么区别?简单场景,只想看有哪些用户下过单:
SELECT DISTINCT user_id FROM orders;
如果还要统计每个用户下了多少单:
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id;
注意MySQL的ONLY_FULL_GROUP_BY模式:SELECT的列必须出现在GROUP BY里或者被聚合函数包住。如果只是去重,DISTINCT更直观;如果要去重的同时做统计,GROUP BY是标准方案。
BETWEEN AND的一个大坑是边界。BETWEEN 1 AND 5是包含1和5的。但日期查询就有问题了:
SELECT * FROM orders
WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31';
这条SQL会漏掉1月31日23:59:59之后的数据。因为create_time如果是datetime类型,'2023-01-31 00:00:00'到'2023-01-31 23:59:59'这部分被BETWEEN的右边边界算进去了吗?其实只要create_time大于等于'2023-01-31 00:00:00'且在'2023-01-31 23:59:59'之内,都会包含。真正的问题是BETWEEN右边如果是'2023-01-31',它默认当成'2023-01-31 00:00:00',所以1月31日00:00:00之后到23:59:59的数据全没被包含。更安全的写法是用半开区间:
WHERE create_time >= '2023-01-01' AND create_time < '2023-02-01';
这个知识在刷"清洗---sql语句去重"这类需求时很关键。很多数据清洗任务,表面上是去重,实际是日期边界、NULL值、空格这些细节叠加在一起,把数据搞乱。
7.3 当面试官问"一条SQL很慢,你怎么排查"时,他到底想听什么
我觉得这个面试题是Java+SQL方向的"压轴题"。它没有标准答案,但最怕听到的回答是:"加索引。"
太笼统了,甚至可能是错的。比如前面提到的深度分页问题,你把create_time索引加上去,解决了filesort,但LIMIT 100000,20照样要扫描大量数据,问题还在。
我推荐一套回答框架,也是我平时真实排查时的步骤:
- 先确认慢在哪:是接口调用慢,还是数据库执行慢。看监控、看日志。
- 拿到具体SQL:打开慢查询日志,定位耗时最长的SQL,别凭空猜。
- 用EXPLAIN看执行计划:type、key、rows、Extra逐个看,确认是全表扫描、深度分页、filesort、临时表,还是锁等待。
- 针对性优化:没索引建索引、索引失效改SQL、深度分页改游标、filesort改联合索引、锁等待查事务。
- 最后压测验证:用实际数据量复测,看耗时和数据库负载有没有真的降下来。
这套回答的妙处在于,它把"八股文"里的知识点串成了一条完整的排查链路,面试官一听就知道你不是背的。
7.4 我的实际学习路径,供你参考
我自己从那次线上事故之后,给自己定了一套阶段目标:
第一阶段,把项目里最常用的SQL在DBeaver里手写一遍,别再用MyBatis自动生成日志看一眼就完事。写过一遍,才知道ORM替你做了什么。
第二阶段,遇到慢SQL先EXPLAIN,再查索引,再改SQL。不要一上来就找DBA,DBA可以帮你一次,帮不了你一辈子。
第三阶段,看锁、看事务、看执行计划成本,开始能主动设计表结构和索引。到这个阶段,你已经不是"写SQL的人",而是"设计数据访问方案的人"。
最后说句实在话:Java工程师的SQL水平,不靠刷题,靠的是把每条慢SQL都当成一次Debug。你花一下午把执行计划看明白,比背一百道八股文都有用。
