SQL(一):解决不确定感

0 阅读19分钟

一、引言

最近,我做过一套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每组前NROW_NUMBER() ... <= N窗口函数排名后过滤,比 LIMIT 子查询更简洁
9连续区间分组日期 - 行号 = 常量利用等差数列性质,将连续段映射到同一组

如何训练这些模型: 每天选一个模型,找 10 道变体题(LeetCode 标签:Database)。每道题先盲写,再对比最优解,并口头解释“为什么这个模型适用于此场景”。

三、12 周刻意训练计划

总体目标

  • 第 4 周:独立解出本试卷所有题,平均 < 10 分钟/题。
  • 第 8 周:能写出两种解法并分析性能差异。
  • 第 12 周:面对陌生业务需求,10 秒内匹配模型,写出高效 SQL,并能用 EXPLAIN 定位瓶颈。

每日学习流程(1 小时)

  1. 热身(5分钟) :回顾昨日错题或一个模型卡片。
  2. 新题训练(30分钟) :做 3~5 道当前周主题的题目,限时,写完后用 EXPLAIN 分析。
  3. 复盘(15分钟) :对比答案,记录自己的错误类型(逻辑/语法/性能)。
  4. 整理笔记(10分钟) :更新模型卡片,记录一个“今天学到的最重要优化点”。

每周主题与任务

主题每日题量重点补充内容
1基础查询 + 区间重叠5练习 BETWEEN,画数轴理解交集;索引设计:为什么范围列放后面
2窗口函数入门5ROW_NUMBER, RANK, DENSE_RANK 区别;分组内排序的内存/磁盘开销
3窗口函数进阶5LAG/LEAD 的偏移;默认窗口帧;同比环比计算细节
4连续区间问题4日期减行号法的数学原理;处理跨年、重复日期
5字符串处理4LENGTH vs CHAR_LENGTH;正则表达式性能陷阱
6剔除极端值4NTILEPERCENT_RANK 区别;数据倾斜时如何调整
7自连接 vs 窗口函数3同一需求两种写法,用 EXPLAIN 对比扫描行数和排序代价
8多表关联优化4JOIN 顺序、小表驱动大表;STRAIGHT_JOIN 强制顺序;索引下推
9分组汇总高级技巧3GROUP_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 为什么 NTILELIMIT + 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 后,右表无匹配时字段为 NULLCOALESCE 返回第一个非空值。初始价格 100 作为默认值,避免了子查询中处理 NULL 的复杂逻辑。

4.7 EXPLAIN 关键指标解读与行动

字段坏值好值为什么改进方法
typeALLrange, ref, constALL 全表扫描,数据量大时灾难加索引,或改写条件
rows接近表总行数很小扫描行数越少越快优化索引,增加过滤
ExtraUsing filesortUsing indexfilesort 需要额外排序,内存/磁盘ORDER BY 列加索引
ExtraUsing temporary(空)临时表用于 GROUP BYDISTINCT调整索引,使分组有序
keyNULL具体索引名没用到索引检查条件是否满足最左前缀

实战:每写一个查询,立刻 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_priceDISTINCT。如果有些商品从未调价(初始价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()
  • 看到“排除极端值” → 自动浮现 NTILEPERCENT_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 并记录关键指标

    • typeALL(全表扫描)→ 必须加索引;range/ref/eq_ref 可接受
    • rows:估算扫描行数,是否远大于预期?
    • ExtraUsing filesortUsing temporary 通常需要优化
    • key:是否使用了预期索引?
  • 19. 检查索引是否被正确使用

    • 复合索引最左前缀原则:(a,b,c) 可支持 aa,ba,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浪潮下,最需要解决的问题。

最后,我借用一下鲁迅所写的《拿来主义》中的话来结尾:

”所以我们要运用脑髓,放出眼光,自己来拿!“

“总之,我们要拿来。我们要或使用,或存放,或毁灭。那么,主人是新主人,宅子也就会成为新宅子。然而首先要这人沉着,勇猛,有辨别,不自私。没有拿来的,人不能自成为新人,没有拿来的,文艺不能自成为新文艺。 ”