ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

自学SQL刷题总卡壳:用难题笔记拆解多表关联与窗口函数

自学SQL刷题总卡壳:用难题笔记拆解多表关联与窗口函数 刷自学SQL网上的题我陆陆续续刷了两轮。第一轮基本是边翻语法边写写完就过错题抄个答案了事第二轮才换了做法——每道卡住我的题我都单独开一页记下来记的不是正确答案而是我卡在哪一步、为什么那一步会卡、下次遇到同类题该怎么起手。真正让我水平往上跳一档的不是刷题量从三十道涨到一百道而是那本越写越厚的难题笔记。这篇东西不打算重抄一遍 SQL 语法手册也不讲入门我想聊的是当你在自学SQL这类刷题站上突然过不去某一道题时到底该怎么拆、怎么记、怎么让同一类题下次不再卡。它适合已经能写基本 SELECT、但一碰到多表关联和窗口函数就发懵的人也适合刷了几十道题感觉在原地打转、想找一套复盘方法的人。SQL 这东西门槛低、天花板高卡点几乎人人都一样区别只在于有没有把卡点沉淀成方法论。1. 自学SQL刷题为什么会卡住难题笔记到底记什么1.1 从语法期到分析期失效点通常出现在哪自学 SQL 的路径大致是三个阶段而且这三个阶段之间的两处跳跃是绝大多数人掉队的地方。第一阶段是语法期关键词是 SELECT、WHERE、ORDER BY、LIMIT、简单的聚合函数。这个阶段做题的体验很顺题目问“查年龄大于 25 的用户”你就把条件直译成 WHERE几乎是英译中的感觉。很多人在这里会误判自己“已经会 SQL 了”。第二阶段是组合期JOIN、GROUP BY、HAVING、子查询、UNION 全部登场。这一阶段的题目不再是直译而是需要你先在脑子里想清楚“数据要经过几道加工才能变成目标形态”。第一次的跳跃就在这里从“把需求翻译成语句”变成“为需求设计一条数据流水线”。很多人卡在这是因为他们还在用翻译思维做题——看到题目里有“每个用户”就抓一个 user_id 塞进 GROUP BY根本没想过中间的连接会不会让行数翻倍。第三阶段是分析期窗口函数、CTE、递归查询、行列转换、连续区间问题。第二次跳跃出现了题目开始考验你对“数据在某一时刻的形状”的想象力。比如“求每个用户第二笔订单的金额”“求连续登录三天以上的用户”这类题你光靠翻译是做不出来的必须先抽象出一个中间结果集再在这个结果集上做二次加工。我自己的失效点卡在第二到第三阶段之间具体表现是JOIN 写出来能跑但结果条数总是不对GROUP BY 后面接了不该接的列窗口函数背下语法却不知道怎么落 PARTITION BY。后来我发现这不是语法问题是没有把“中间结果长什么样”这一步写下来。难题笔记的第一个价值就在这里——它是强迫你把脑内那团模糊的东西画到纸上的工具。1.2 三类卡点语法卡、思路卡、语义卡给卡点分类是让笔记有价值的第一步。不分清类型你的笔记最后会变成一锅乱炖复习的时候根本抓不住重点。我把它分成三类卡点类型典型表现笔记该记什么大约占比语法卡报错、函数名拼错、日期函数记不住只记“函数 参数 一个最小可跑例子”不要记整题约 30%思路卡语句没报错但根本不知道该从哪起手记破题的第一步该先聚合还是先连接该不该上窗口函数约 50%语义卡语句能跑、结果看着也像其实定义理解错了记题目关键词的精确含义比如“每笔订单”到底指明细行还是主表行约 20%语法卡最不值钱也最容易骗人。你花半小时研究一个日期函数的参数顺序记了满满一页其实下次写的时候查一下文档三秒就解决。我的做法是语法类的东西只记一行——“函数名 一句话用途 一个能跑的最小片段”比如-- 取当月第一天MySQL 写法 SELECT DATE_FORMAT(CURDATE(), %Y-%m-01);多余的不要写。真正需要花时间记的是思路卡。思路卡的核心是记录“起手式”。比如遇到“每个用户最近一次下单的金额”这种题起手式是先用窗口函数按 user_id 分组、按时间倒序排 ROW_NUMBER外层再筛 rn 1。这个起手式一旦固化你以后遇到“每个用户”“每个部门”“每个品类”加“最近一次”“最大一次”这类结构条件反射就能写出骨架剩下的只是换表名和列名。语义卡最隐蔽也最容易在面试和实际工作中出事。举个我踩过的坑一道题要求“统计每个用户的订单数”我直接对订单主表 COUNT(*)结果和参考答案差了几个。原因是题目的“订单”指的是已支付的订单而主表里包含未支付和已取消的记录。这类错误不是 SQL 写错了是你对业务词的理解和出题人不一样。注意遇到结果“差一点点”的题先别怀疑语法回到题干去找那个限定词。多数时候是你漏了一个状态条件或者一个时间范围。1.3 难题笔记和错题本不是一回事学生时代的错题本逻辑是“这题我做错了我要记住正确解法”。难题笔记的逻辑完全不同它记的是过程不是结果。我给你对比一下同一道题的两种记法。假设题目是“查出每个部门工资第二高的员工”。错题本式记法答案 SELECT * FROM ( SELECT *, DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) rk FROM emp ) t WHERE rk 2;难题笔记式记法题目抽象分组内取第 N 名 - 窗口函数的经典标志 我的第一反应想用 GROUP BY MAX 排除最大值写了一半写不下去 卡在哪第二高需要“先排序再看位置”而 GROUP BY 只能给聚合值给不了位置 破题关键排名类问题一律上窗口函数PARTITION BY 是分组ORDER BY 是名次依据 为什么用 DENSE_RANK 不用 ROW_NUMBER题干里“第二高”是薪资档次同薪并列算同一档 可复用模板分组内 TopN - 子查询 窗口函数 外层筛 rn后者看起来啰嗦但它训练的是迁移能力。第二次遇到“每个门店销量第三的商品”你不用重新想直接调模板。笨办法是记一百道题的答案聪明的办法是记十类破题结构然后用这十类去覆盖一百道题。2. 练习环境怎么搭别把时间全耗在装数据库上2.1 本地库选型MySQL、PostgreSQL、SQL Server 怎么选刷题站上的题目通常自带在线判题但那只够你验证答案对不对。想真正写难题笔记你必须有本地环境——因为你需要改数据、加边界、看执行计划这些在线判题器给不了。新手最容易在这里卡住装了三个小时数据库最后报个服务起不来的错热情直接凉一半。我建议按下面的对比表选一个然后只装一个数据库上手难度窗口函数支持语法特点适合谁MySQL 8低8.0 起完整支持方言最常见教程最多绝大多数自学者首选PostgreSQL中支持最完整最早支持语法严谨报错清晰想练扎实基本功的人SQL Server中支持良好日期函数和 T-SQL 自成一派工作环境用 SQL Server 的人我自己的组合是主刷 MySQL 8因为绝大多数题库和网络文章都是这个方言另外备一个 SQL Server 环境专门用来记 T-SQL 的差异。这样做的好处是写笔记的时候我会顺手标一句“这条在 MySQL 能跑SQL Server 要改成 TOP”时间久了方言差异笔记就成了我的附加资产。关于安装本身只说三个我踩过的坑。第一装 SQL Server 时如果卡在服务启动失败八成是 Windows Management Instrumentation 服务没起来先去服务管理器确认它处于运行状态再重试安装。第二SQL Server 有个内存占用会随着运行慢慢爬升的现象本机练习的话在配置里给它设个内存上限别让它把开发机吃干净。第三装完之后记得装一个顺手的客户端管理工具自带的那个够用但笨重第三方轻客户端连着看执行计划、导出结果会更顺手。至于版本2019、2022 都行练习用不出差别别在选版本上纠结。2.2 造数据比刷题更需要花心思这是我这两年里最有价值的一条经验题库给的示例数据太干净了。干净的意思是几乎不会有 NULL不会有重复值不会有同一天同一秒的多条记录不会有金额为零或负数的行。你在这种数据上写出来的正确语句换到真实数据上十有八九会出错。所以我的做法是本地自己建一张表然后故意往里面塞脏数据CREATE TABLE orders ( order_id BIGINT PRIMARY KEY, user_id BIGINT, amount DECIMAL(10,2), status VARCHAR(16), create_time DATETIME ); INSERT INTO orders VALUES (1, 101, 100.00, paid, 2024-03-01 10:00:00), (2, 101, 200.00, paid, 2024-03-01 10:00:00), -- 同一用户同一秒两笔 (3, 101, 0.00, paid, 2024-03-02 09:00:00), -- 零金额 (4, 102, 50.00, cancel, 2024-03-02 11:00:00), -- 取消状态 (5, 102, NULL, paid, 2024-03-03 12:00:00), -- 金额为 NULL (6, NULL, 300.00, paid, 2024-03-03 13:00:00); -- 用户为 NULL就这六行你可以测出很多问题COUNT(*) 和 COUNT(amount) 差几AVG 里 NULL 有没有被算进分母按 user_id 分组时 NULL 会不会单独成一组同一个用户同一秒两笔怎么排先后。这些全都是真实场景里会遇到的东西题库里基本给不了你。提示每次练完一道题先别关窗口花两分钟往表里加一两行极端数据再跑一遍。如果结果没崩说明你的写法是稳的如果崩了这就是一道最值得写进笔记的题。2.3 一套稳定的刷题流程我把自己的流程固定成了五步固定下来之后效率提升非常明显因为它把“想”和“写”分开了读题两遍圈出限定词。第一遍看需求第二遍专门找“每个”“最近”“连续”“不超过”“除……以外”这类词。这些词决定了后面所有设计。先用中文写出中间结果集。比如“先按用户聚合出总金额再连接用户表拿名字最后按金额倒序取前十”。这一步不写 SQL只写人话。实现最笨的版本。什么招数都行能跑出结果就行。先要有正确答案做锚点才谈得上优化。优化到符合题目要求。题目如果要求不能用子查询、或者要求一条语句就在这一步调整。同时打开执行计划看看有没有全表扫描。写笔记。按第 4 章的模板填。这一步绝不能省省了就等于白刷。这五步里第三步最反直觉——很多人一上手就想写最优解结果在细节上反复打磨半小时过去了连正确答案长什么样都不知道最后根本没法判断自己的优化是对是错。先用笨办法跑出正确结果是把“正确性”和“性能”这两个维度解耦一次只解决一个问题。3. 高频难题类型逐个拆从会写到达标3.1 多表连接行数爆炸的第一现场多表 JOIN 出错九成不是语法问题是你没意识到 JOIN 是乘法关系。假设你的订单明细表里一个订单有三行商品明细。你用订单主表去 JOIN 明细表一条订单就变成三行。如果这时候你顺手再 JOIN 一张用户地址表一个用户两个地址行数就变成六行。然后你在这个结果集上写 SUM(amount)金额直接翻倍。语句能跑不报错结果错得离谱。我总结了三种最常见的“先聚合再连接”场景场景一比率计算。求“每个用户的支付成功订单占比”。正确做法是先把订单按用户聚合算出总数和成功数再相除错误做法是先连接用户表再 COUNT用户表里的多行会把分母顶起来。-- 正确先聚合再算比率 SELECT user_id, SUM(CASE WHEN status paid THEN 1 ELSE 0 END) / COUNT(*) AS paid_rate FROM orders GROUP BY user_id;场景二多张事实表相加。一个用户既有订单又有退款你把两张表 JOIN 起来求净额结果两张表行数一乘金额全乱。正确做法是各自聚合到用户粒度再用 UNION ALL 或者 FULL JOIN 拼起来。场景三一对多连接后去重。有人用 DISTINCT 去压行数压是压住了但你要小心——DISTINCT 会把你想保留的重复明细也压掉。注意判断有没有“行数爆炸”最简单的办法是在连接后先跑一次 COUNT()再分别跑两张表的 COUNT()。如果前者远大于后两者你就是在做乘法。3.2 窗口函数从“能写”到“写得对”窗口函数的语法不难背难的是三个决策PARTITION BY 放什么、ORDER BY 放什么、用哪个排名函数。先说排名函数的选择这张表我建议直接抄进笔记函数并列名次处理下一个名次典型场景ROW_NUMBER不并列强行编号连续取“最近一条”去重RANK并列占名次跳号1,1,3有明确“跳档”语义的排名DENSE_RANK并列占名次不跳号1,1,2“第 N 高”这类值排名这个选择的坑我踩过一道“取每个部门薪资排第二的人”的题我用 RANK结果如果出现两个人并列第一第二名就被跳过查出来是空的。换成 DENSE_RANK 就对了。所以判断标准很简单题干说的是“第几名”还是“第几个”。“第二高薪资”是名次用 DENSE_RANK“第二条记录”是位置用 ROW_NUMBER。分组内 TopN 是最经典的模板我给你一个可以闭眼背下来的骨架SELECT * FROM ( SELECT user_id, order_id, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC, order_id) AS rn FROM orders ) t WHERE rn 3;这里有个细节值得专门记ORDER BY 后面为什么加了 order_id因为如果两个订单金额完全相同排序是不确定的两次跑出来的结果可能不一样。加一个唯一的 tie-breaker 列是让结果可复现的关键操作。第二个大坑是窗口帧。当你在窗口函数里同时写了 ORDER BY 和聚合默认的窗口帧是 RANGE UNBOUNDED PRECEDING AND CURRENT ROW它会把“当前行排序值相等的行”也算进来。如果你想要严格的逐行累计必须显式写 ROWSSUM(amount) OVER ( PARTITION BY user_id ORDER BY create_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total我在同一秒两条订单的数据上测过不加 ROWS 时两条记录会拿到相同的累计值加上 ROWS 之后才是一行一行累加。这个差别不做实验根本看不出来而它恰好又是最容易造成业务数据偏差的地方。3.3 分组聚合里的四个隐形陷阱GROUP BY 我列了四条最容易出错的规则每一条都能单独写一页笔记。陷阱一WHERE 和 HAVING 的分工。记住一句话WHERE 过滤的是行在分组之前执行HAVING 过滤的是组在分组之后执行。所以“金额大于 100 的订单”用 WHERE“总金额大于 1000 的用户”用 HAVING。写反了不一定报错但性能会差而且逻辑上其实就是错的。陷阱二COUNT 的三种形态。这是我觉得最应该一开始就搞清楚的东西COUNT(*)统计行数NULL 也算一行。COUNT(col)统计这一列非 NULL 的行数。COUNT(DISTINCT col)统计这一列去重之后的非 NULL 值个数。三个写法在这种数据上差别巨大一列有 10 行其中 3 行是 NULL且非空值里有 2 个重复。那么结果是 10、7、5。你写 AVG 的时候同理NULL 不参与分母所以AVG(col)的分母是 7 不是 10。如果业务上要求“零金额也算平均值”你就得写SUM(col) / COUNT(*)。陷阱三SELECT 里的非聚合列。严格模式ONLY_FULL_GROUP_BY下SELECT 里出现的列必须出现在 GROUP BY 里。有人为了绕开报错把列一股脑塞进 GROUP BY结果分组粒度变细统计结果全错。正确做法是问自己这个列的取值在每个组内是唯一的吗如果唯一它可以放进 GROUP BY如果不唯一你就得先想清楚业务上到底要哪一个值用 MIN、MAX 还是聚合函数。陷阱四NULL 自成一组的理解。GROUP BY 的时候NULL 会作为一个单独的组出现。如果在订单表里有些记录 user_id 是空的你的结果里就会多出一行 user_id 为 NULL 的统计。要不要过滤掉取决于业务定义但你必须知道它存在而不是被结果里凭空多出来的一行搞懵。3.4 连续 N 天问题日期减序号这一招“连续登录三天以上的用户”这类题是刷题站上的经典难点也是我觉得最能体现思路价值的一道题。第一次做的时候我尝试过自连接、尝试过递归写得很痛苦。后来学会了一个几乎通杀的思路用日期减去一个连续序号如果日期是连续的差值就恒定。原理不复杂假设用户连续三天登录日期是 3 月 1 日、2 日、3 日序号是 1、2、3。日期减序号得到 2 月 28 日、2 月 28 日、2 月 28 日三个值完全相同。而如果中间断了一天比如 1 日、2 日、4 日得到的就变成 2 月 28 日、2 月 28 日、3 月 1 日第一组断了。于是“连续”这个语义就被转换成了“分组后计数”。SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date, COUNT(*) AS continuous_days FROM ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp FROM ( SELECT DISTINCT user_id, DATE(login_time) AS login_date FROM login_log ) d ) s GROUP BY user_id, grp HAVING COUNT(*) 3;写这段的时候有两点必须注意。第一内层一定要先做 DISTINCT 去日期因为一个用户一天可能登录多次不去重的话序号会跳整个逻辑就废了。第二日期减序号这一步在不同数据库方言里的写法不一样MySQL 用 DATE_SUBPostgreSQL 直接日期减整数SQL Server 用 DATEADD 负数。这个差异值得在笔记里单独开一栏记。3.5 行列转换与 NULL 的三值逻辑行列转换行转列看着花哨其实就是一个固定套路条件聚合。SELECT user_id, SUM(CASE WHEN status paid THEN amount ELSE 0 END) AS paid_amount, SUM(CASE WHEN status cancel THEN amount ELSE 0 END) AS cancel_amount FROM orders GROUP BY user_id;这里有一个非常容易被忽略的口诀SUM 里用 ELSE 0COUNT 里别用 ELSE 0。原因是 SUM 遇到 NULL 会直接跳过如果某个用户没有已取消的订单CASE 返回的全是 NULLSUM 结果就是 NULL你期望的 0 变成了 NULL。而 COUNT 统计的是非 NULL 行数写成COUNT(CASE WHEN ... THEN 1 ELSE 0 END)会把 ELSE 0 也算成非 NULL结果永远等于总行数。再说 NULL 的三值逻辑。SQL 里的布尔值其实有三种TRUE、FALSE、UNKNOWN。任何和 NULL 的比较结果都是 UNKNOWN不是 FALSE。所以SELECT * FROM orders WHERE amount NULL; -- 永远查不到行 SELECT * FROM orders WHERE amount NULL; -- 也永远查不到行正确的写法是 IS NULL / IS NOT NULL。另外NOT IN子查询里如果出现 NULL整个条件会失效什么都查不出来——这是我认为最阴的一个坑因为语句不报错静悄悄给你返回空结果。避开的办法是用 NOT EXISTS或者在子查询里显式加上WHERE col IS NOT NULL。判空赋值用 COALESCE它按顺序返回第一个非 NULL 参数比数据库特有的 IFNULL、NVL 更通用SELECT user_id, COALESCE(amount, 0) AS amount FROM orders;4. 难题笔记怎么写模板、执行计划与复盘节奏4.1 一题一页的笔记模板我最后稳定下来的模板一共六行写在文档里大概半屏填起来两三分钟一点都不费劲题目一句话把题干压缩成一句话去掉所有故事背景 我的起手式我第一反应想怎么写哪怕是错的也记下来 卡点定位语法卡 / 思路卡 / 语义卡具体卡在哪一步 破题关键一句话比如“分组内排序取位置 - 窗口函数” 最终解法代码带注释标出关键那几行 迁移场景这类结构还能套在哪些题上第三行“我的起手式”是很多人会跳过的一步但它恰恰是最有价值的。因为你的错误直觉是有规律的记下来之后你会发现自己在某几类题上总是往同一个方向想错。比如我总想用 GROUP BY 解决排名问题发现规律之后我现在一看到“第 N 个”就直接跳窗口函数不用再试错了。“迁移场景”这一行也别省。写完一道窗口函数的题顺手补一句“还能用在每个品类销量前 N、每个学生最近一次考试、每个设备最新一条状态”你的模板库就是这样一点点长起来的。4.2 给笔记加第二图层执行计划写完正确答案的笔记是不是就结束了我建议再花三分钟做一件事跑一次执行计划把关键信息抄进笔记。原因很实在刷题站上的数据只有几十行你怎么写都是毫秒级返回看不出性能差距。但真实表是百万行级别的同一道题的两种写法可能差一百倍。提前在笔记里建立“写法—执行计划”的对应关系等你在工作中改慢查询的时候会非常省事。执行计划我主要看四个地方关注点看什么出现什么要注意访问类型是全表扫描还是走索引大表出现全表扫描想办法加条件或索引用到的索引实际命中了哪个索引建了索引没命中可能是条件上做了函数运算预估扫描行数优化器认为要读多少行数字接近表总行数基本等于白建索引额外信息是否出现临时表、文件排序出现排序考虑排序列能不能用上索引有一类问题特别值得单独记在索引列上做运算或函数索引会失效。比如WHERE DATE(create_time) 2024-03-01会导致索引失效改成范围查询WHERE create_time 2024-03-01 AND create_time 2024-03-02就能走索引。这个知识点在刷题时完全体现不出来但在实际工作里就是慢查询和快查询的分界线。4.3 3 天 / 7 天 / 30 天的复盘节奏笔记写完不复习等于没写。我试过几种复盘节奏最后固定成三个时间点第 3 天遮住答案重写一遍。只看“题目一句话”那一行不看解法从零写一遍。写不出来就说明当时的“懂了”是假懂。这一步最重要的是把重写时的卡点和三天前对比——如果卡在同一个地方说明这个知识点没进肌肉记忆需要单独拎出来多做几道同类题。第 7 天只看破题关键那一行。把所有笔记的“破题关键”抄出来列成一列快速扫一遍。这个动作的训练目标是建立条件反射。当“连续”两个字和“日期减序号”这个动作绑定了以后读题读到这里手就已经开始敲了。第 30 天按类型重组。到这个时候你手里可能有五六十条笔记了把它们按类型重新排一遍——所有连接类放一起所有窗口函数类放一起。排完你会发现高频卡点其实就那么七八个剩下全是它们的变体。到了这一步刷题站上的新题对你来说基本都是老面孔了。5. 常见问题与排查技巧实录5.1 报错速查表报错是最好查的因为数据库会告诉你问题在哪。我把遇到的报错整理成了一张表报错信息关键词大概率原因处理方向syntax error near ...逗号多写、关键字拼错、保留字当成了列名看报错位置前面那一段从后往前排查unknown column列名拼错、别名不能用在 WHERE 里检查别名作用域WHERE 用原列名ambiguous column多表连接时有同名列没加表前缀给每个列都写 表名.列名only_full_group_by 相关SELECT 里的列没在 GROUP BY 里要么聚合要么加进分组别硬改配置cannot add ... column建表或改表语句的字段定义有问题对照已有表结构检查类型和约束命名管道提供程序无法打开客户端连接配置不对换成走 TCP 端口连接检查服务是否启动关于“unknown column”还有一个变体你在 SELECT 里给列取了别名AS total然后在 WHERE 里写WHERE total 100报找不到列。原因是 WHERE 的执行顺序在 SELECT 之前别名还没生成。解法是套一层子查询或者干脆重复写一遍表达式。还有一类不算报错但很烦人的问题出在数据导出上。比如从 Oracle 里把身份证号一类的长数字导成 CSV用表格软件打开就变成了科学计数法尾巴上的数字全变成 0数据直接废掉。原因是表格软件把长数字识别成了数值类型超过 15 位就丢精度。处理办法有三种导出时就用 TO_CHAR 把数字转成字符串导出后不要直接双击打开用导入向导把该列明确指定成文本格式或者在文件里给数字前加个制表符强制成文本。这个坑我踩过一次几十万行数据重新导了一遍从那之后我在笔记里专门留了一栏记导出格式。5.2 语句能跑但结果不对四步定位这类问题最难因为没有报错给你指路。我用固定四步来排查第一步看行数。把你的查询拆成中间步骤一步步 COUNT。连接的中间结果行数对不对分组后的组数对不对行数和组数是一切问题的源头。第二步去掉分组看原始行。把 GROUP BY 和聚合函数全删掉直接 SELECT 明细肉眼看前二十行。很多错误一眼就能看出来比如金额翻倍、状态值不对、时间范围超了。第三步单独跑每个条件。把 WHERE 里的多个条件拆开一个一个加上去看是哪一条开始让结果数量骤降。骤降往往意味着这个条件里有 NULL 或者类型不匹配。第四步检查 NULL 和类型。特别关注连接条件上的列有没有 NULL以及字符串和数字有没有隐式转换。WHERE user_id 101在某些数据库里能跑但可能不走索引甚至在某些严格类型下直接比不出来。这四步的价值在于它把一个模糊的“结果不对”变成了四个可以明确回答的问题你不需要靠感觉猜。5.3 慢查询的排查顺序慢查询的排查顺序很重要顺序对了能省很多时间。我自己的顺序是先看执行计划不要先改 SQL。看是全表扫描还是走了索引看预估行数是多少。看 WHERE 条件上的列有没有索引以及条件里有没有对列做函数运算或类型转换。看 JOIN 的顺序和驱动表通常小表驱动大表效果更好优化器大部分时候能选对但统计信息过期时会选错。看有没有不必要的排序和去重ORDER BY 和 DISTINCT 在大数据量下都是成本。最后才考虑改写 SQL比如把子查询改成 JOIN、把大 IN 改成临时表关联。提示优化的时候一次只改一个点改完立刻对比执行计划和耗时。一次改三处你永远不知道是哪一处起了作用。还有一点值得记进笔记分页查询在深翻页时会突然变慢。原因是LIMIT 100000, 20需要先扫描并丢弃前十万行。常见的优化思路是用游标式的写法——从上一页的最后一个排序值继续往下取而不是用偏移量。这个知识点在刷题时用不上但一旦你在实际项目里做列表接口它就是救命的那一条。5.4 我踩过的坑清单最后把几条零散但很值钱的经验集中放一下拼接字符串构造 SQL 是坏习惯。不管什么场景只要条件里带外部输入就用参数化查询把值交给数据库驱动去处理。这既是安全习惯也顺便解决了引号和类型转换的麻烦。不要在同一个查询里既用别名又在 WHERE 里引用它。执行顺序决定的不是书写习惯问题。数字和字符串不要混着比较。隐式转换可能让索引失效更糟的是在有些排序规则下结果和你想的不一样。写完一条有 GROUP BY 的语句先数一下组数。组数不对后面全错。训练时把数据量加个零。一百行和一百万行同一句话的体感完全不同。6. 把笔记用起来几个能立刻上手的扩展6.1 把题目改造成自己的业务数据集刷题站上的题目都是通用场景——学生、成绩、订单、员工。做久了会有一个副作用你能做题但看到真实表结构时还是发懵因为真实表有几十个字段名字还都是缩写。我的做法是每学完一个模块就把它改造成自己熟悉的数据集。比如学完窗口函数我不去重做订单题而是自己造一套“设备状态上报”的表设备 ID、上报时间、状态值。然后给自己出题——“每台设备最近一次上报的状态是什么”“每台设备状态连续为异常的时长最长是多少”。这么做有两个好处。一是题目是你自己出的你知道正确答案应该长什么样验证成本极低。二是当你把抽象模板套到一个全新的场景上时你才能真正检验自己是记住了模板还是背下了答案。6.2 方言差异笔记一次写多处跑只要你在实际工作里跨过两个数据库就会知道方言差异有多烦。与其每次现查不如建一页专门的对照表边用边加功能MySQLSQL ServerOracle取前 N 行LIMIT NTOP NROWNUM / FETCH FIRST字符串拼接CONCAT / CONCAT_WS 或 CONCAT||空值替换IFNULL / COALESCEISNULL / COALESCENVL / COALESCE日期加减DATE_ADD / DATE_SUBDATEADDADD_MONTHS / 直接加减当前时间NOW()GETDATE()SYSDATE这张表的用法不是背是写完一条语句之后顺手补一行。半年下来你换数据库时的适应成本会低得惊人。我个人的建议是优先用标准写法比如判空统一写 COALESCE取前 N 行如果数据库都支持窗口函数就统一用 ROW_NUMBER 筛能少踩很多方言的坑。6.3 笔记的复用与输出笔记写到一定量之后我建议做一次整理输出——把散落的条目按主题合成几篇长文。这个动作的收益比看起来大得多因为你必须把零散的“这题这样写”升级成“这类问题这样拆”才写得下去。整理的时候有个判断标准如果一段笔记你不能用一句话总结出适用场景说明你只是记住了这道题没提炼出结构。这时候就回到原题重新问自己一次“这道题真正的难点是什么”。刷题站上那些标着“难题”的题目其实难的地方高度集中在几个点上分组粒度的判断、连接后的行数、排名与位置的区别、NULL 的传播、连续区间的识别。把这五个点吃透剩下的题基本都是换个壳。我刷到后期最大的感受就是题目越做越像不是题目变简单了是你终于能一眼看到骨架子了。
返回列表