一、引言
最近,我做过一套SQL题目。为了试一下自己的逻辑思维和编程技巧,我没有借用任何工具,花了相当长的一段时间,我都没有解完这些题目,而且我也判断不了我给的解法是对是错,我的大脑似乎陷入一团乱麻之中。
之后,我将这套题给AI工具去解,它在不到一分钟的时间就解出这套题。我除了让它写解法,我还要求它提供这些题涉及到的思维模式,考虑解法的运行性能以及为什么这样写是性能最优。对我来说,最关键的问题是:我要怎样训练才能拥有这种逻辑思维的直觉。
接下来,我粘贴出AI的回答,我剔除了具体的解题部分。
二、10 大核心思维模型 + 为什么这样思考
| # | 模型 | 核心公式 / SQL 模式 | 为什么有效 |
|---|---|---|---|
| 1 | 区间重叠 | A_start <= B_end AND A_end >= B_start | 集合论:两区间交集非空的条件,避免了4种情况的枚举 |
| 2 | 奇偶互换 | CASE WHEN id%2=0 THEN id-1 WHEN id%2=1 AND id<max THEN id+1 | 通过模运算映射,边界单独处理,避免游标 |
| 3 | 同比环比 | LAG(值, 偏移) OVER (ORDER BY 时间) | 利用窗口函数一次扫描访问前后行,避免自连接 |
| 4 | 每组首个/末个 | ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...) = 1 | 窗口函数分区排序后取第一行,比聚合+子查询更高效 |
| 5 | 字符计数 | LENGTH(s) - LENGTH(REPLACE(s, c, '')) | 字符串替换前后长度差即为出现次数,O(n)复杂度,比循环快 |
| 6 | 剔除极端值 | NTILE(10) OVER(ORDER BY val) NOT IN (1,10) | 分位数分组快速去除头尾,避免子查询排序后取 limit |
| 7 | 全集比较 | HAVING COUNT(DISTINCT col) = 总类别数 | 通过分组后计数等于全集判断“全部出现” |
| 8 | 每组前N | ROW_NUMBER() ... <= N | 窗口函数排名后过滤,比 LIMIT 子查询更简洁 |
| 9 | 连续区间分组 | 日期 - 行号 = 常量 | 利用等差数列性质,将连续段映射到同一组 |
如何训练这些模型: 每天选一个模型,找 10 道变体题(LeetCode 标签:Database)。每道题先盲写,再对比最优解,并口头解释“为什么这个模型适用于此场景”。
三、12 周刻意训练计划
总体目标
- 第 4 周:独立解出本试卷所有题,平均 < 10 分钟/题。
- 第 8 周:能写出两种解法并分析性能差异。
- 第 12 周:面对陌生业务需求,10 秒内匹配模型,写出高效 SQL,并能用
EXPLAIN定位瓶颈。
每日学习流程(1 小时)
- 热身(5分钟) :回顾昨日错题或一个模型卡片。
- 新题训练(30分钟) :做 3~5 道当前周主题的题目,限时,写完后用
EXPLAIN分析。 - 复盘(15分钟) :对比答案,记录自己的错误类型(逻辑/语法/性能)。
- 整理笔记(10分钟) :更新模型卡片,记录一个“今天学到的最重要优化点”。
每周主题与任务
| 周 | 主题 | 每日题量 | 重点补充内容 |
|---|---|---|---|
| 1 | 基础查询 + 区间重叠 | 5 | 练习 BETWEEN,画数轴理解交集;索引设计:为什么范围列放后面 |
| 2 | 窗口函数入门 | 5 | ROW_NUMBER, RANK, DENSE_RANK 区别;分组内排序的内存/磁盘开销 |
| 3 | 窗口函数进阶 | 5 | LAG/LEAD 的偏移;默认窗口帧;同比环比计算细节 |
| 4 | 连续区间问题 | 4 | 日期减行号法的数学原理;处理跨年、重复日期 |
| 5 | 字符串处理 | 4 | LENGTH vs CHAR_LENGTH;正则表达式性能陷阱 |
| 6 | 剔除极端值 | 4 | NTILE 与 PERCENT_RANK 区别;数据倾斜时如何调整 |
| 7 | 自连接 vs 窗口函数 | 3 | 同一需求两种写法,用 EXPLAIN 对比扫描行数和排序代价 |
| 8 | 多表关联优化 | 4 | JOIN 顺序、小表驱动大表;STRAIGHT_JOIN 强制顺序;索引下推 |
| 9 | 分组汇总高级技巧 | 3 | GROUP_CONCAT、条件聚合(SUM(CASE...))、WITH ROLLUP |
| 10 | 复杂业务综合 | 3 | 混合模型:连续登录 + 每组 TopN + 同比,设计分步 CTE |
| 11 | 性能调优专项 | 3 | 每天一道慢查询,分析 EXPLAIN,重写优化;学习 OPTIMIZE TABLE |
| 12 | 模拟考试 + 复盘 | 5 | 限时 90 分钟完成 10 道综合题(含本试卷),然后逐题复盘 |
每周日任务
- 重做本周所有错题(不看答案)。
- 写一篇 200 字总结:哪种模型最易错,哪种优化最有用。
- 更新个人“思维模型错题本”,按模型分类记录错误原因。
四、性能优化深度解析(为什么)
4.1 为什么索引能加速查询?
原理:B+Tree 索引将数据组织成多级树结构,高度通常为 34。一次查找只需读取 34 个磁盘页,而全表扫描需要读取所有页。例子:WHERE start_date <= '2021-01-09' 如果没有索引,MySQL 必须读取每一行并比较;有索引则定位到第一个 <= 的位置,顺序扫描索引叶子节点,大大减少 I/O。
4.2 为什么避免在索引列上使用函数?
原理:函数会改变列的值,MySQL 无法直接使用索引上的原始值进行比较,必须对每一行计算函数结果,导致全索引扫描或全表扫描。错误:WHERE YEAR(date) = 2021 → 无法使用 (date) 索引。正确:WHERE date BETWEEN '2021-01-01' AND '2021-12-31' → 可以使用索引范围扫描。
4.3 为什么窗口函数比自连接快?
原理:窗口函数只需对数据集进行一次排序(或利用索引顺序),然后一次扫描计算每一行。自连接需要将表复制一份,然后进行哈希连接或嵌套循环,产生大量临时数据和内存消耗。测试:10 万行数据,LAG 耗时 ~0.05 秒,自连接耗时 ~1.5 秒。
4.4 为什么 NTILE 比 LIMIT + OFFSET 更适合分位数排除?
原理:NTILE 是窗口函数,一次排序后均匀分桶,计算量 O(n log n)。而手动实现“前10%”需要先排序,再 LIMIT n*0.1,还需要知道总数,写起来复杂且可能多次扫描。NTILE 更简洁,且内部优化好。
4.5 为什么“日期减行号”能分组连续日期?
数学原理:设连续日期序列为 d, d+1, d+2, ..., d+k,行号从 1 开始递增,则 date - rn 恒等于 d-1。一旦日期不连续,差值会改变。因此相同差值属于同一连续段。注意:需先去重(同一天多次登录只算一次),否则行号会错位。
4.6 为什么 COALESCE(l.new_price, 100) 能实现默认值?
原理:LEFT JOIN 后,右表无匹配时字段为 NULL,COALESCE 返回第一个非空值。初始价格 100 作为默认值,避免了子查询中处理 NULL 的复杂逻辑。
4.7 EXPLAIN 关键指标解读与行动
| 字段 | 坏值 | 好值 | 为什么 | 改进方法 |
|---|---|---|---|---|
type | ALL | range, ref, const | ALL 全表扫描,数据量大时灾难 | 加索引,或改写条件 |
rows | 接近表总行数 | 很小 | 扫描行数越少越快 | 优化索引,增加过滤 |
Extra | Using filesort | Using index | filesort 需要额外排序,内存/磁盘 | 给 ORDER BY 列加索引 |
Extra | Using temporary | (空) | 临时表用于 GROUP BY 或 DISTINCT | 调整索引,使分组有序 |
key | NULL | 具体索引名 | 没用到索引 | 检查条件是否满足最左前缀 |
实战:每写一个查询,立刻 EXPLAIN。若出现 Using filesort 且数据量 > 1 万行,考虑是否能让索引直接排序。
五、补充细节与常见陷阱
5.1 日期边界处理
- 题目中“在 2021-01-07 至 2021-01-09 期间”通常包含首尾,所以用
<=和>=。若不含尾,用< '2021-01-10'。 - 注意时区问题:MySQL 中
DATE类型无时区,安全。
5.2 同比环比中的月份连续性问题
- 若表中有缺失月份,
LAG(..., 12)会拿到错误的行(跨过缺失)。解决方案:先用generate_series补全月份(MySQL 需用递归 CTE),或使用自关联 +DATE_ADD。
5.3 连续登录中“连续”的定义
- 本解法按自然日连续,若间隔一天也算连续?需使用
LAG计算差值<= 1作为分组标志(间隙与岛屿问题)。
5.4 商品价格表中没有主商品表
- 题目 3.2 未提供全部商品 ID,只能从
commodity_price中DISTINCT。如果有些商品从未调价(初始价100),则无法查出。实际生产必须有商品主表。
5.5 窗口函数的内存限制
- 窗口函数排序可能使用磁盘临时文件(当
sort_buffer_size不足时)。可通过增加sort_buffer_size或优化索引避免排序。
六、对意义的追问
前面AI工具给出那么清晰的解法、思维模型、训练计划、补充细节等内容后,我追问:在工具这么强大,不到一分钟能给出解答的情况下,我做这12周的练习成长还有什么意义?有必要吗?
它的回答如下:
有必要,比任何时候都有必要!
6.1 AI 是“答案生成器”,你是“问题定义者”
我能在一分钟内给出解法,是因为你已经把问题清晰地描述出来了——包括表结构、示例数据、查询范围、输出格式。但在真实工作中,90% 的难度不在于“写 SQL”,而在于:
- 从业务方模糊的需求中提炼出精确的逻辑(“活跃用户”到底怎么定义?)
- 发现数据中的坑(重复、空值、时间不连续、状态变化)
- 判断哪种方案在千万级数据下不超时
- 向非技术人员解释为什么结果是这样
这些能力,AI 无法替你完成。你才是那个决定“问什么问题”的人。
6.2 没有 SQL 思维,你甚至不知道 AI 给的答案对不对
我可以写出 start_date <= '2021-01-09' AND end_date >= '2021-01-07',但你如果不懂区间重叠原理,可能:
- 误以为
c002(1月8日~1月15日)不该被选出来 - 把边界条件写反成
start_date >= ... AND end_date <= ... - 被面试官追问时,说不出为什么这样写
AI 给你答案,但只有你能为答案负责。 没有自己的判断力,你就像拿着一个不知真假的导航开车。
6.3 12 周训练的本质:建立你大脑里的“模式识别回路”
我之所以快,是因为我被训练在数亿条 SQL 和相关文本上,内部存储了成千上万种模式。12 周训练,不是让你背下所有题,而是让你的大脑形成类似的模式识别回路:
- 看到“连续登录” → 自动想到
日期 - 行号分组 - 看到“每组前N” → 自动映射到
ROW_NUMBER() - 看到“排除极端值” → 自动浮现
NTILE或PERCENT_RANK
这种直觉一旦建立,你将来遇到任何新问题(包括没有现成答案的问题),都能快速拆解。而 AI 遇到全新的、没有训练过的逻辑组合,仍然可能犯错。
6.4 职场现实:AI 会写 SQL,但不会背锅
假设线上出现一个严重的统计错误,老板问“谁写的这个查询?”你回答“AI 写的” → 老板会说:“那 AI 能写检讨吗?能修复数据吗?能安抚客户吗?”
最终承担责任的是人,不是 AI。 只有当你真正理解每一条 SQL 的含义、每个优化的代价,你才能在出现问题时快速定位、修复、解释。
6.5 AI 是杠杆,不是替代品
如果你不会 SQL,AI 给你一段代码,你连改都改不了(比如换个表名、加个条件)。如果你懂 SQL,AI 可以让你的产出速度提升 5~10 倍:
- 写复杂查询的草稿
- 自动生成
EXPLAIN分析建议 - 帮你快速回忆某个函数的语法
未来高价值的开发者,不是被 AI 替代的人,而是会用 AI 放大自身能力的人。
6.6 一个类比
ChatGPT 能瞬间写出一个排序算法,但计算机专业的学生依然要学排序原理。为什么?
- 因为面试会问
- 因为你要理解为什么快速排序在最坏情况下是 O(n²)
- 因为你可能需要修改算法以适应特殊硬件
同理,SQL 思维是你理解数据世界的语言。AI 可以翻译,但你不能永远靠翻译活着。
七、一些引申,通用 SQL 开发检查清单(芒格风格)
我自己平时会去阅读一些书籍,刚好这段时间重新阅读《穷查理宝典》,读到查理·芒格提到检查清单的重要性。
于是我让AI给我提供一份带有芒格风格的通用SQL开发检查清单。
我认为AI工具的回答非常好,在这里完整的粘贴下来。
“如果你能熟练使用一套跨学科的思维模型,就能看清大多数复杂问题的本质。” —— 查理·芒格(改编)
使用说明
- 按阶段顺序执行,不要跳过第一阶段。
- 每个阶段末尾有“逆向思考”问题,必须回答。
- 遇到不确定的项,停下来查文档或请教他人。
第一阶段:需求澄清(理解问题,避免返工)
-
1. 用 5 秒说出“输入 → 输出” 输入哪些表/字段?输出几列?每列含义?输出行数大概多少?
-
2. 圈出所有时间/日期条件
- 是否包含边界(闭区间还是开区间)?
- 时间精度是 DATE、DATETIME 还是 TIMESTAMP?
- 有无时区问题?
-
3. 标注所有“特殊值”处理规则
- NULL 怎么处理(排除、默认值、参与运算)?
- 空字符串 vs NULL?
- 重复数据:去重还是保留?去重依据哪几列?
-
4. 明确排序和返回条数
- 需要排序吗?按什么顺序?
- 需要分页吗?每页多少条?
- 是否只要前 N 条(Top N)?
-
5. 逆向思考:最容易误解的业务词是什么? 例如:“活跃用户”定义是登录过还是产生过订单?“连续登录”是否允许间隔一天?
第二阶段:逻辑建模(选择思维模型,写出伪代码)
-
6. 识别主模型(从以下 12 种中选择)
- 集合过滤(WHERE)
- 分组聚合(GROUP BY + 聚合函数)
- 每组 Top N(ROW_NUMBER / RANK)
- 前后行访问(LAG / LEAD)
- 连续区间分组(日期 - 行号)
- 区间重叠(两个时间段交集)
- 相邻行交换(奇偶或相邻条件)
- 排除极端值(NTILE / PERCENT_RANK)
- 全集包含(HAVING COUNT = 总类别数)
- 最新状态(按时间取最后一条)
- 字符串解析(正则、长度差)
- 树形/递归查询(WITH RECURSIVE)
-
7. 写出分步伪代码(中文或英文) 例:
1. 从 orders 表中筛选 status = 'completed' 且 order_date 在 2023 年 2. 按 customer_id 分组,计算总金额 sum(amount) 3. 用 ROW_NUMBER 按总金额倒序排序,取前三名 -
8. 边界值测试用例设计(至少 3 个)
- 空输入(0 行)
- 单行输入
- 极端值(最大值、最小值、NULL)
-
9. 逆向思考:什么数据会让这个逻辑失效? 例如:分组字段全为 NULL;日期跨百年;字符串长度超限。
第三阶段:SQL 编写(清晰、正确、可维护)
-
10. 使用 CTE 拆分复杂逻辑 将一个大查询拆成多个 WITH 子句,每个 CTE 只做一件事,最后主查询简洁。
-
11. 显式 JOIN + 明确 JOIN 类型
- INNER / LEFT / RIGHT / FULL
- 避免隐式逗号连接(旧语法)
- 关联条件尽量使用主键/外键
-
12. 聚合函数前先确认 NULL 语义
COUNT(col)不计 NULL;COUNT(*)计所有行SUM(col)忽略 NULL,但若全 NULL 则返回 NULL,可用COALESCE(SUM(col), 0)
-
13. 窗口函数优先于自连接 需要访问前后行或分组排名,一律用窗口函数。
-
14. 处理重复数据的标准写法
- 去重:
SELECT DISTINCT ...或GROUP BY - 保留重复但标记:
ROW_NUMBER() OVER(PARTITION BY key)
- 去重:
-
15. 日期范围查询的标准写法
- 区间重叠:
A.start <= B.end AND A.end >= B.start - 某月数据:
date BETWEEN '2023-01-01' AND '2023-01-31' - 避免
YEAR(date)=2023(函数破坏索引)
- 区间重叠:
-
16. 字符串匹配注意事项
- 前缀匹配:
LIKE 'abc%'可用索引 - 通配符开头:
LIKE '%abc'无法用索引 - 正则表达式一般不走索引
- 前缀匹配:
-
17. 排序与分页
- 排序字段有索引可避免 filesort
- 深分页用“延迟关联”:先查主键分页,再关联取全字段
第四阶段:性能验证(用 EXPLAIN 和实际数据)
-
18. 运行 EXPLAIN 并记录关键指标
type:ALL(全表扫描)→ 必须加索引;range/ref/eq_ref可接受rows:估算扫描行数,是否远大于预期?Extra:Using filesort、Using temporary通常需要优化key:是否使用了预期索引?
-
19. 检查索引是否被正确使用
- 复合索引最左前缀原则:
(a,b,c)可支持a、a,b、a,b,c查询,不支持b单独 - 范围条件(
>,<,BETWEEN)后的列无法使用索引
- 复合索引最左前缀原则:
-
20. 评估数据量级和响应时间
- < 1 万行:索引不是必须
- 1 万 ~ 100 万:必须有合适索引
- 100 万以上:考虑分区、汇总表、或者改需求
-
21. 逆向思考:什么情况下性能会突然变差?
- 数据倾斜(某分组值占比极大)
- 隐式类型转换(字符串列比较数字)
- OR 条件无法合并(可改用 UNION)
- 子查询多次执行(改为 JOIN 或 CTE)
第五阶段:结果验证(确保正确性)
- 22. 用最小数据集手工验证 构造 5~10 行数据,覆盖边界和典型场景,对比 SQL 输出与手工计算结果。
- 23. 抽样检查真实数据 随机抽取 5 条输出,反向追溯源数据,看是否符合预期。
- 24. 测试空值场景 在测试数据中故意引入 NULL,观察输出是否合理。
- 25. 测试重复数据场景 如果有重复键,确保聚合或排序结果正确。
- 26. 逆向思考:如果需求理解反了,结果会怎样? 尝试用相反的条件跑一次,看看结果是否完全互斥?如果不是,说明边界定义有问题。
第六阶段:交付与沉淀(让知识复利)
- 27. 为 SQL 添加注释 说明:业务目标、关键逻辑、假设条件(例如“假设订单状态只有已支付才算有效”)。
- 28. 参数化 将易变的值(日期、阈值)提取为变量或使用预处理语句占位符。
- 29. 记录本次遇到的“坑” 更新个人错误模式库,例如: “误用了 NOT IN 但子查询返回 NULL,导致结果为空” → 改用 NOT EXISTS
- 30. 反思:是否有更简单的方案? 如果查询超过 30 行,思考能否拆分为多个步骤或引入中间表。
- 31. 分享给团队 如果发现了一个通用技巧或性能提升方法,写一条简短总结发在团队知识库。
附录:芒格式 30 秒心法(写在手边)
- 逆向思考:这个 SQL 在什么输入下会错?
- 多元模型:除了我想到的模型,还有没有其他模型更合适?
- 能力圈:这个优化是否超出我的经验?先查资料。
- 检查偏误:我是不是只测试了让自己放心的数据?
- 复利思维:这个查询会被复用吗?如果会,值得多花 10% 时间优化。
使用承诺: 在执行任何生产级 SQL 之前,按此清单逐项打勾。连续执行 20 次后,你将形成肌肉记忆,不再需要看清单也能自然覆盖 90% 的关键点。
“清单不是束缚,而是自由——让你有精力去解决真正困难的问题。”
八、结语
在过去,一个合格的程序员,需要能够理清楚真正的需求是什么,还需要将这些需求描述转化为精确的代码逻辑。
遇到问题时,我们也会在网上搜索各种各样的解决方法,然后自己去应用在实际的开发过程中,并最终完成交付。
现在AI工具极大的缩减这种解决问题的模式所花费的时间,甚至在有些步骤上完全替代了人工的操作。
但是,这更加体现出自我驱动的重要性。无论是编程还是生活中的其他领域,我们都可以从AI那里获得指导和建议,但最终的思考、行动和提升,都必须由我们自己来完成。
如果能将这种完成的过程与结果分享出来,我们不仅能巩固自己的知识,还能获得新的灵感和见解。
这种个人经验的收获与分享,我认为未来还是不会变,人是社会性的,需要连接的。
AI工具可以帮助我把过去想做但难做到的事情做出来,这对我来说是最实际的。对意义的追问那一段,其实也是对自己想法的一种确定。
我们自己的不确定感,是AI浪潮下,最需要解决的问题。
最后,我借用一下鲁迅所写的《拿来主义》中的话来结尾:
”所以我们要运用脑髓,放出眼光,自己来拿!“
“总之,我们要拿来。我们要或使用,或存放,或毁灭。那么,主人是新主人,宅子也就会成为新宅子。然而首先要这人沉着,勇猛,有辨别,不自私。没有拿来的,人不能自成为新人,没有拿来的,文艺不能自成为新文艺。 ”