
1. 为什么我会坚持做“sql每日一题”说实话一开始并不是什么宏大计划。那时候团队晨会前总有十几分钟空档几个人刷手机也是刷手机我就随手往小群里丢了一道SQL题说“今天有空的人写一下晚上对答案”。结果第一天就炸出好几个解法有人用GROUP BY有人用DISTINCT还有人用子查询吵得有来有回。后来这个习惯就留下来了一天一道雷打不动慢慢攒成了一个大系列。很多我认识的人学SQL最大的问题不是不会写而是不动手语法书、视频教程看了无数遍真到了“给你两张表自己查”的时候就傻眼。sql每日一题想解决的就是这个问题用高频、轻量的节奏把真实的查询场景拆成小题目逼着人每天亲手在键盘上敲一遍。这个系列适合谁三类人。第一类是刚学完SQL语法、但不知道怎么用的小白第二类是写了好几年SQL、天天在做“复制粘贴工程师”的同行第三类是准备面试、想系统梳理知识点的学生。每一道题的解析里我不光会给标准答案还会讲清楚为什么这么写、有没有更好写法、换了数据库要怎么调整。原因很简单SQL的方言差异实在太大了同一个需求在MySQL、SQL Server、PostgreSQL、Oracle里能长出完全不同的写法很多新人一看报错就懵其实就是方言在作怪这东西不靠日积月累根本学不扎实。我另外还发现把这个系列坚持下来以后我在平时工作里遇到复杂数据需求的第一反应明显变快了。以前看到“这个报表要按部门算累计占比”会愣一下现在脑子里直接浮现出一个窗口函数的骨架。这不是天赋就是每天喂一道题喂出来的条件反射。所以这篇文章我把自己做这个系列的全套思路、题目布局、经典题拆解、踩过的大坑都翻出来给想动手的人一份可以直接抄走的参考。2. 题型布局我的每日一题分四个阶段日更最忌讳的就是今天想到什么出什么东一榔头西一棒子。我吃过这个亏后来老老实实按能力维度把题目排成专题每两周一个主题中间穿插复习。整个选题体系大体分四块基础语法、进阶函数、性能优化、安全边界。这四块不是割裂的而是层层递进的关系新手先啃前两块老手可以直接跳到后两块去磨练功力。2.1 基础语法区练的是肌肉记忆基础题最容易被低估。就拿“去重”来说你以为有 DISTINCT 就完事了真落到业务里什么时候用 DISTINCT、什么时候用 GROUP BY、什么时候用 ROW_NUMBER() 保留唯一一条很多人其实分不清。我专门出过一组三连题第一道是统计用户表里有多少个不同的城市这个直接SELECT COUNT(DISTINCT city) FROM users就完事。第二道是统计每个城市的用户数那就得SELECT city, COUNT(*) FROM users GROUP BY cityDISTINCT 已经搞不定了。第三道难度上去要按城市分组取每个城市中年龄最小的那个用户的完整信息这是很典型的“分组内取Top1”不能靠去重硬凑必须用子查询或者窗口函数。结果让我很意外不少做了两三年业务开发的同事前两题秒过第三题写出来的嵌套又长又错这说明基础语法真的需要反反复复练肌肉记忆不是说看书能练出来的。类型转换也是这个阶段的常客而且它不像去重那么显眼却是无数报错的源头。SQL Server 里的CONVERT(VARCHAR(10), order_date, 120)可以把日期转成yyyy-MM-dd的字符串第三个参数是风格码不写明白很容易转出离谱结果。MySQL 里更习惯用DATE_FORMAT(order_date, %Y-%m-%d)。同样是转日期两边写法南辕北辙。我出这种题不是为了刁难人是因为实际项目里到处是两个系统做数据交换日期格式不统一简直是家常便饭。今天不练明天上线准出状况。除此之外空值的处理我也坚持放在基础区反复敲打。SQL 的NULL和任何值比都是未知NULL NULL的结果不是真而是未知这个特性直接决定了很多查询行为的怪异性。我用题让大家体会“为什么这里要写IS NULL而不是 NULL”以及聚合函数为什么经常自动把NULL忽略掉。这些细节不通过具体题目遇到光看文档是记不住的。2.2 进阶函数区重在培养分析思维如果说基础语法是手艺那窗口函数就是一道分水岭。我自己的体会是窗口函数把 SQL 从一个“只能看结果”的语言变成了“能看过程又能看结果”的语言。传统的GROUP BY会把多行压成一行想看到明细还想保留分组结果就得把表连接好几遍性能和可读性都让人头疼。窗口函数可以在不用合并行的前提下对每一行计算一个基于分组的值典型的像ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC)它的含义是“在每个部门内部按工资降序编号”这个编号作用到每一行上既保留了明细又给出了分组排名一条语句就能完成以前三条语句的活。这个专题我会出各种变体累计求和、移动平均、同比环比、分组排名每道题都逼着大家去想“窗口到底是怎么划分的”“ORDER BY 在这里到底影响了什么”。很多人学窗口函数老是把ROW_NUMBER、RANK、DENSE_RANK三个函数混在一起其实区别就在并列名次的处理上。用题来区分它们比背定义管用一百倍。2.3 性能优化区练的是执行思维题目练到一定量之后会写不是终点写得好才是。性能优化区的第一课是教大家看执行计划。MySQL 里一条EXPLAIN SELECT ...会把查询的执行方式摊开能清楚地看到是不是全表扫描、是不是走了索引、有没有产生临时表。SQL Server 的图形化执行计划更是直观鼠标一放就能看到每个步骤的 IO 开销。我让大家养成一个习惯写完一条复杂度稍微上去的 SQL顺手执行计划扫一眼看到红色警告就先别急着交差。这个习惯一开始会觉得很麻烦但练久了你写 SQL 的第一稿就会自动避开那些明显费资源的写法。这个区域最常练的主题是慢SQL识别与改写。比如SELECT *为什么不好因为它会把根本用不到的字段也捞出来尤其是在宽表上白白增加网络传输和内存消耗。再比如在索引列上套函数像WHERE DATE_FORMAT(order_time, %Y-%m-%d) 2025-01-15表面看没问题但数据库对这类写法往往无能为力只能放弃索引扫描。真正合适的写法是把函数挪到另一边写成WHERE order_time 2025-01-15 AND order_time 2025-01-16既保留了语义又让索引有机会生效。这种经验光背“索引失效的几种情况”是完全不够的必须在具体题目里撞一次南墙才记得住。2.4 安全基线区练的是防御本能这块我是在几次事故之后才坚决加进来的。很多人以为写后台查询接口用不到防御直到有一天发现输入的查询条件被拼进字符串别人只要在参数里动点手脚SQL 语句的语义就可能被改写。这不是科幻片这是每天发生在大量站点上的现实。我出这类题的目标非常明确让大家识别“拼接 SQL 字符串”的经典模样以及如何用参数化查询来替代它。参数化的原理其实不复杂把 SQL 骨架和参数值分开传给数据库数据库把它们当成语句结构和普通数据分别处理结构永远不可能被参数里的内容篡改。像 Python 里的cursor.execute(SELECT * FROM users WHERE name %s, (name,))Java 里的PreparedStatement的?占位符都是这个思路。我反复在题里强调讨论安全问题永远是从防御者的角色出发我们写出的每一段代码都应该是修复问题的而不是制造问题的。3. 让我印象最深的几道题拆解这个系列做了这么久有几道题每次翻出来都觉得特别经典。它们未必复杂但每一道都能牵出一串知识点是从新手到熟练工都值得反复咀嚼的。3.1 去重三兄弟DISTINCT、GROUP BY、ROW_NUMBER题目背景很简单一张订单表字段有order_id、user_id、order_time、amount。问题是“统计每个用户的订单数和总金额”。这个题看着基础但一深挖就发现很多人不知道 GROUP BY 之后到底能选哪些字段。在 MySQL 里由于默认配置的原因你甚至可以选一个不在 GROUP BY 里的字段结果出来是乱猜的而在 SQL Server 和 PostgreSQL 里这种写法直接报错提示你“选中的列必须出现在 GROUP BY 中或者聚合函数里”。一道题同时牵出 SQL 语法规则和数据库方言差异非常典型。然后我把题目升级成“查询每个用户最近的一笔订单的完整信息”这时 DISTINCT 用不上GROUP BY 也选不出整行只能靠子查询或者ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC)。我推荐的写法是SELECT * FROM ( SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn FROM orders o ) t WHERE t.rn 1;这个写法的好处在于思路直线先给每个用户内部的订单按时间排序编号再把编号为 1 的行留下来既保留了完整字段又有很强的扩展性。如果哪天需求改成“取每个用户第二新的订单”只需要把rn 1改成rn 2连大结构都不用动。做这类题的时候我总会提醒大家遇到“分组内取前几行”的需求第一反应就应该是窗口函数而不是去写各种复杂的自连接。3.2 分组拼接GROUP_CONCAT 和它的兄弟们这道题源自一个真实需求一套系统要导出每个班级的学生名单格式是“班级名: 张三, 李四, 王五”一个班一行名字之间用逗号拼起来。MySQL 里的标准答案就是GROUP_CONCATSELECT class_name, GROUP_CONCAT(student_name ORDER BY student_name SEPARATOR ,) FROM students GROUP BY class_name;注意GROUP_CONCAT内部还可以写ORDER BY控制拼接顺序这个细节很多人不知道。SQL Server 里没有GROUP_CONCATSQL Server 2017 之后的写法是STRING_AGG(student_name, ,)它同样支持WITHIN GROUP (ORDER BY ...)来控制顺序。PostgreSQL 用的是STRING_AGG写法更接近 SQL Server。这个题的价值就在于让你意识到数据库没有提供“同名函数”并不可怕怕的是你只背过一种数据库的方言就以为全世界都这样。掌握一个需求在不同数据库里的映射关系才是真正通用型的 SQL 能力。3.3 累计求和自连接、变量和窗口函数累计求和是分析报表里的高频需求。比如销售表sales(month, amount)要算每个月以及之前所有月份的累计销售额。老一代 SQL 工程师可能会用自连接每个月的累计就是把所有月份小于等于该月的记录全部加起来。但自连接的写法在月份多的时候是灾难性的复杂度直线上升。后来有人用 MySQL 变量写效果不错但可读性很差逻辑藏在running_total : running_total amount里一个变量初始化出错就满盘皆输。窗口函数出现之后这个问题的标准解非常简洁SELECT month, amount, SUM(amount) OVER (ORDER BY month) AS cumulative_amount FROM sales ORDER BY month;没有一个 GROUP BY没有一个表连接却能在每一行上保留“截至当前所有行”的合计数。我拿这道题出来是想让大家真切感受到窗口函数带来的思维升级。别急着背语法先理解窗口函数是“在结果集的每一行上开一个可见的窗口”窗口边界由ORDER BY决定这种看问题的角度才是它真正值钱的地方。3.4 在 Spark SQL 里找 GROUP_CONCAT 的替代品有一段时间团队在做数据平台迁移好多 MySQL 上的查询要改写成 Spark SQL 作业。社区里被问爆的一个问题就是“Spark SQL 里有没有类似 MySQL GROUP_CONCAT 的函数”答案是直接使用GROUP_CONCAT是不行的Spark SQL 的替代方案是用collect_list配合concat_wsSELECT class_name, CONCAT_WS(,, COLLECT_LIST(student_name)) AS student_names FROM students GROUP BY class_name;collect_list会把分组内的字段收集成一个数组concat_ws再用逗号把数组里的元素拼成字符串。如果想去掉重复名字就用collect_set它会自动去重但代价是顺序不保证这时如果必须保序就得先对子查询排序或者用sort_array。这个题的价值在底层逻辑SQL 的“分组聚合”思想在整个数据处理领域是通用的但每一种引擎给出的具体函数可能完全不一样迁移到新的计算引擎时别抱怨“怎么连个 GROUP_CONCAT 都没有”先想想这个函数背后的本质是什么再去目标引擎里找对应能力久而久之反而学得更扎实。3.5 一条慢SQL的优化全过程性能区的题目不能用纸面解析应付我都让大家把真实数据灌进来对比。经典案例是一张日志表数据量一千多万每天跑统计经常把库拖垮。原始写法里有一个特别明显的问题在查询条件中对索引列做了隐式类型转换字段本身是字符串类型的用户编号参数传进去时却是数字数据库为了比较可能把字符串列整体转成数字索引就废了。发现这个问题的路径很简单EXPLAIN里看到typeALL全表扫描再用SHOW WARNINGS看数据库改写后的 SQL就会看到它偷偷加了 CAST 操作。这类题目我一般分三步带大家做。第一步先看执行计划找出全表扫描、临时表、文件排序这些标志性关键词。第二步看 SQL 有没有对字段做加工比如函数包裹列、隐式转换、模糊匹配前置通配符。第三步是结合业务想改写方案比如把LIKE %keyword%改成全文索引方案把深分页的LIMIT 1000000, 20改成基于上一页最大 ID 的分页方式。这三步走完一条慢 SQL 基本无所遁形。3.6 为什么拼字符串会出事一道改写题的教训安全类的题目我从来不搞花架子。题目是给一段有问题的代码要求在不改变功能的前提下修复它。那段代码是这样的# 问题代码直接拼接 sql SELECT * FROM users WHERE name user_input 表面看需求很简单用户输入一个名字查出来就好。但问题在于 user_input 里万一带着单引号和其它 SQL 片段整个语句的结构就会被带偏。这个题的正确修法是把输入当成参数而不是语句的一部分# 修复参数化查询 sql SELECT * FROM users WHERE name %s cursor.execute(sql, (user_input,))为了让这个道理变得深刻我带大家做了一次简单实验在本地库里分别用两种方式执行同一段伪造输入拼接版的语句直接可以被解析成完全不同的查询而参数化版本永远只会把输入当作一个普通字符串值来比较。整个实验没有任何攻击演示的成分纯粹是从数据库的服务端视角理解“结构”与“数据”的区别。做完之后大家都有一个共识SQL 语句的结构部分永远只能由开发者在代码里写死任何来自外部的输入都只准出现在参数位置。4. 一次完整的“每日一题”日课是怎么跑起来的很多人问我你们每天发一道题到底是怎么坚持下来的答案是我把整个流程做成了固定节奏到点就干不用想今天到底要不要更这样才有持续的动力。4.1 早上出题核心是“小步高频”每天上午我花十分钟把题目发到群里题目一定配建表语句和样例数据这样不管大家用 MySQL 还是 SQL Server 或者 SQLite都能把环境跑起来。不给出完整环境、只丢一句文字描述的题等于白出因为每个人会为环境差异浪费大量时间真正练到手的反而不多。题目难度控制在新手需要思考五到十分钟、老手能顺手秒掉的程度。如果一道题让老手都觉得费劲说明它更适合当周赛题硬塞进每日一题里容易挫伤积极性反而不利于坚持。4.2 白天动手关键在“先写自己的版本”我不建议大家上午看了题晚上直接对标准答案那样印象最浅。白天有空的时候哪怕只是写个开头试着建个临时表、把字段列出来也是在脑子里种了一颗种子。到了晚上对答案的时候你再看别人的解法就会有“原来这一步是这么处理”的恍然大悟而不是“哦反正答案是这样的”的过眼云烟。很多人坚持打卡却感觉没什么长进根本原因就是只看不做省了想的过程也就省掉了长进的过程。4.3 晚上对答案重点不是谁对谁错晚上八点统一对答案时最热闹的是解法大比拼。同一道题经常能收到三四种不同思路有人用子查询有人用 JOIN有人用窗口函数。我会把每种解法都拿出来看一遍分析各自适用场景比如小数据量怎么写都无所谓大数据量时窗口函数和 JOIN 的差异就值得讨论。这种讨论比标准答案有用得多因为实际工作里根本不存在唯一正确答案重要的是理解不同写法的代价和收益。所以我经常说每日一题的“题”只是载体“讨论”才是精华。4.4 一周复盘把零散的知识串起来每周五我不出新题专门做复盘。把这一周的五道题放到一起看找出共同点。比如某周做过日期格式化、字符串截取、类型转换复盘时就会总结出“做数据清洗时先统一字段类型再处理业务逻辑”这条规律。这样坚持几周后知识点不再是孤岛慢慢就织成了一张网。5. 我做“每日一题”踩过的坑和避坑心得做这个系列这么久我自己也踩了不少坑有些教训如果不说出来新人大概率还会再踩一遍所以我把它们整理成几条注意事项就当是朋友之间的提醒。5.1 同一条SQL在不同数据库里的方言坑有一期我用了 SQL Server 的TOP语法取前五条结果群里用 MySQL 的同学全在报错。MySQL 用的是LIMIT 5SQL Server 是SELECT TOP 5Oracle 则用FETCH FIRST 5 ROWS ONLYPostgreSQL 支持LIMIT也支持FETCH FIRST。这道题让我彻底改变出题方式此后每一题都在题目里注明“默认使用 MySQL 8.0如果你用的是 SQL Server 或 Oracle请自行转换语法”。这个教训搬到实际工作中也一样成立跨库迁移的时候不要天真地以为 SQL 是通用语言先查方言差异否则上线前一晚有你受的。5.2 去重不只有 DISTINCT先想清楚业务要什么DISTINCT 是最好用的去重也是最容易用错的去重。它是对整个返回结果的所有列一起去重如果查询的列里包含订单编号这类唯一字段那 DISTINCT 基本形同虚设。有一次我让大家统计“有哪些用户下过单”很多人直接SELECT DISTINCT user_id FROM orders这没问题。第二天我把题改成“每个用户的第一笔下单时间”还有人试图用 DISTINCT 解决结果自然是做不出来的。去重的本质是先想清楚“重复”的定义是同一个用户算重复还是同一张订单算重复或者是同一个用户同一产品算重复定义不同SQL 写法天差地别这比背多少个函数都重要。5.3 学习SQL别停留在“会写”要懂得看执行计划很多同学写 SQL 报错少了就觉得毕业了但我的看法是SQL 不报错只是及格线写得慢照样会被业务投诉。看执行计划这件事是区分“会用 SQL”和“懂 SQL”的重要标志。我建议大家把执行计划当成体检报告EXPLAIN一下看看有没有全表扫描有没有 Using filesort有没有产生临时表。每个关键词背后都对应一个可以优化的具体动作。我当时为了给自己培养这个习惯定了一条规矩凡是练习题里涉及两张表以上的查询必须写一行注释说明大致执行思路。一开始很别扭两个月后成了本能现在看到别人写的长 SQL脑子里会自动模拟执行计划。5.4 安全红线别让业务为你的习惯买单写代码可以有个性但在安全红线面前没有借口。我见过一些老系统里全是拼接 SQL一问为什么这么写回答说“以前一直这么干的”“数据量小没关系”。要知道一条恶意输入可能根本与数据量无关它改变的是语句语义这个后果是任何性能优化都救不回来的。所以做每日一题这个系列我特意把它放在最后压轴的位置提醒每一位读者写完 SQL 之后再多问一句这里有没有拼接外部输入如果有立刻改成参数化。这个习惯不需要天赋只需要你在每一次练习里都刻意遵守。6. 一点个人体会坚持做下来之后最有意思的变化不是技术上的而是思维方式上的。以前看到业务需求脑子里第一个念头是“这个功能用什么框架写”现在看到数据需求下意识会先拆解成“数据落在几张表、关联关系是什么、筛选条件怎么传参”。这种切换很难通过读一本书完成它就是靠一天一天一道题一道题磨出来的。所以如果你也想做这个练习我建议不要追求一天刷十道题那是突击不是积累真正有效的是每天和 SQL 打个照面保持手感。你可以从最简单的单表查询开始也可以直接拿手头工作里一个写得不顺的查询来开刀。今天一道明天一道一个月后你再翻回第一天做的题一定会看到明显的不同。这个系列我会继续做下去也希望读到这里的你从今天就开始写上第一条 SQL。