很多人拿到一份SQL题库第一反应是“刷完它”。刷完五十题、一百题之后再面对一道新题却还是卡壳感觉那些题都白做了。这不是你不够努力而是把题库用错了方向。我刷过不少SQL题库也拿题库的场景给新人做过辅导一个很深的体会是题库不是用来背答案的它是用来暴露你思维短板的。每一道题背后都对应一类思维模型——执行顺序、集合逻辑、聚合粒度、窗口函数、去重与空值处理。这些模型通了题怎么变你都不怕模型不通刷三百题也只是记住三百个“标准答案”换个壳就认不出来。这篇文章就围绕“SQL题库知识点”这件事把我个人认为最重要的底层能力拆开讲一讲。包括为什么刷完题还是不会写、执行顺序到底怎么影响你的SQL、高频题型怎么拆解、业务题怎么从题干翻译成代码、以及那些总在低级错误里反复出现的细节。文章适合正在刷题准备面试的人也适合工作里写SQL总感觉不够利索的人。废话不多说直接进正题。1. 刷题刷不出能力问题出在“看懂答案”和“会写”之间那道鸿沟见过太多人这样刷题打开一道题看一眼题目思考十分钟写不出来点开答案心里想“哦原来是这样”然后下一题。五十题刷完真正独立写出来的没几道但自我感觉“我都看过了”。这里藏着一个很常见的认知偏差看懂答案是一种识别能力而独立写出SQL是一种生成能力。识别比生成容易得多就像你能看懂一幅画不代表你能画出来。SQL刷题想要有效必须把“看懂”推进到“闭卷重写”的层次。我自己刷题时有一个硬规矩一道题做完之后合上答案隔几个小时或者第二天凭记忆和思路重新写一遍。第二遍写不出来说明第一遍只是眼睛记住了手和脑子没有。第二遍能写出来再对照答案看细节差异这时候的差异才是真正的学习点。比如我用的是子查询而答案是JOIN说明我对JOIN的适用场景还不敏感我用CASE WHEN而答案是GROUP BY HAVING说明我对分组后过滤的时机理解还不够这些差异比“题目做对了”有价值得多。刷题数量这件事我也想说两句。我见过有人打卡式地每天刷二十题一个月六百题看起来很吓人。但这类人往往有一个特点对每一道题的分类、考点、易错点说不出一句完整的话。反观那些每天只刷三到五道题、每道题认真复盘的人一两个月之后的水平反而更扎实。原因很简单——SQL题目的考点是有边界的你不需要用题海覆盖你需要把每个考点的思维模式练到肌肉记忆。另一个妨碍刷题效果的问题是“为了通过而使用暴力解法”。什么意思就是不管题目怎么变一律先把所有数据SELECT出来再去内存里过滤或者用一堆嵌套子查询硬凑结果。能跑但复杂度和可读性都很差。真实面试或工作场景里一条SQL写得好不好不只看结果对不对还看逻辑是不是清晰、能不能被别人维护。我建议每做完一道题都问自己一句我这个写法用了几次扫描能不能换成JOIN或窗口函数让结构更直接这个反思习惯是刷题题库之外最值钱的收获。最后还有一个心态问题遇到不会的题就慌。实际上面试和工作中没有哪道题是“没见过就完蛋”的SQL知识点就那么几个大类——查询与过滤、聚合与分组、多表连接、子查询与CTE、窗口函数、集合操作、增删改查约束。只要你把这些类别的底层逻辑吃透任何一道新题都是在这些基础模块上做组合。见到新题第一反应不应该是“我是不是没刷过这道类型”而应该是“这道题叫我做什么、它的主体是哪个表、它的粒度是什么、需要哪些加工手段”。这个思路转变过来题目对你来说就只是不同组合的拼图而不是一个个孤立的谜题。2. 先搞懂执行顺序不然你写的SQL永远是“试出来的”我有个观察很多刷题刷得痛苦的人不是不熟悉语法而是不懂一条SQL到底先跑哪一步后跑哪一步。他们写SQL基本上是试出来的——先写个SELECT碰碰运气报错了再改改对了就完事。这种方式应付简单题还行一旦遇到多表、分组、窗口函数叠加就完全失控。SQL的逻辑执行顺序和书写顺序是两回事。你写的时候是先写SELECT再写FROM但计算机读的时候FROM才是起点。完整顺序大致是这样的FROM确定数据源先加载表做连接WHERE对源数据做行级过滤这一步里不能使用聚合函数和列别名GROUP BY按照指定列把数据分组HAVING对分组之后的结果做过滤过滤条件里可以用聚合函数SELECT计算需要输出的列、聚合结果、表达式ORDER BY对最终结果排序LIMIT/OFFSET截取结果集这个顺序的重要性在哪里我举一个高频基础题的例子统计每个部门的平均工资只保留平均工资大于5000的部门。很多人第一次写会写成这样SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE AVG(salary) 5000 GROUP BY department_id;这条SQL在几乎所有数据库里都会直接报错原因就是WHERE的执行时机在GROUP BY之前而AVG是分组之后才能算的聚合值。WHERE执行时根本还没有“部门平均工资”这个东西。正确写法是把过滤放在分组后的HAVING里SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) 5000;这个例子能说明一个核心原则WHERE管的是“分组前的行”HAVING管的是“分组后的组”。你如果不知道执行顺序面对这类题就只能死记“聚合条件放HAVING”碰到变体还是懵。知道了执行顺序你就能自己推导出来——WHERE先于GROUP BY所以它过滤的是原始行HAVING后于GROUP BY所以它过滤的是聚合结果。再举一个更隐蔽的例子题目要求“查2019年以后入职的员工并按部门统计平均薪水的最大值”。如果你脑子里有执行顺序你会非常清楚它分几步先FROM确定员工表WHERE筛掉2019年之前入职的行GROUP BY部门SELECT里算AVG(salary)最后再对这个部门级别的平均值求MAX。但“部门平均薪水的最大值”这个MAX不是行级别的它是在一层分组结果之上的再次聚合。SQL标准不允许SELECT里直接嵌套聚合函数比如AVG(MAX(...))这种所以需要用子查询或CTE包一层WITH dept_avg AS ( SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE hire_date 2019-01-01 GROUP BY department_id ) SELECT MAX(avg_salary) AS max_dept_avg FROM dept_avg;很多人做不出这种题不是不会CTE而是脑子里对“执行的层次”没有概念。数据从行到组到最终结果是一层一层递进加工的。你越能画出这条加工链路写SQL就越有底气。执行顺序还会影响你对SELECT列的理解。GROUP BY之后SELECT里能出现的列只有两类一是分组键本身二是聚合函数。如果你SELECT了非聚合也非分组的列在严格模式下会报错。比如“查每个部门中工资最高的员工姓名”你可能会本能地想SELECT department_id, name, MAX(salary)但name既不是分组键也没被聚合这在逻辑上就是错的——每个部门工资最高的员工“不止一个姓名字段”你让它显示哪一条这种题需要窗口函数或者关联子查询去做组内筛选而不是普通分组。我建议所有刷题的人花一个下午把这个执行顺序背到滚瓜烂熟然后拿十道题反复对照每写一条SQL心里默念它从FROM到LIMIT每一步做了什么。这个习惯坚持两周你写SQL的准确率会明显提升而且更敢去写复杂的多步SQL因为你不再需要靠试错来验证猜测了。3. 高频题型拆解连表、聚合、窗口是一套组合拳题库里的题目再多高频题型也就那么几类。把每类题型的“思维入口”搞清楚比做一百道同类题更有用。我个人最常用的拆题方式是先回答三个问题要查的主体是什么表输出结果的粒度是每一行还是每个组在过滤和排序上有什么特殊条件这三个问题想明白了SQL的大框架就出来了。3.1 连表查询别只会JOIN要会判断“主表是谁”连表是几乎所有SQL题绕不开的考点。但很多人一看到两张表就无脑JOIN真正需要思考的地方——主表、从表、保留哪边的全部数据——反而没想清楚。题目常见场景有两张表一张是用户表一张是订单表让你查“没有下过订单的用户”。如果你选用户表做主表LEFT JOIN订单表再看订单表字段是否为NULL这个思路就是对的SELECT u.user_id, u.name FROM users u LEFT JOIN orders o ON u.user_id o.user_id WHERE o.order_id IS NULL;但同样这个场景如果题目变成“查询下过至少一次订单的用户”你是不是还在用LEFT JOIN WHERE过滤当然可以但你其实已经用了更重的手段——LEFT JOIN会保留所有用户再通过WHERE去掉没订单的这在结果上没问题但在中间的连接过程里你产生的临时行数是全部用户数。如果用户表有几百万行这个写法就显得笨拙了。更简单直观的方式是直接用IN或者EXISTSSELECT user_id, name FROM users WHERE user_id IN ( SELECT DISTINCT user_id FROM orders );这两种写法在逻辑上都对区别在于一个是“连接后再过滤”一个是“子查询直接判断是否存在”。我个人的习惯是判断“是否出现”优先用EXISTS或IN需要取另一张表的字段才用JOIN。这样写出来的SQL更贴近语义也更容易让别人读明白你想干什么。连表还要特别注意一点两张表都有同名列时SELECT里必须写前缀。而且JOIN的ON条件里要明确主外键关系别省。我见过不少人在刷题平台写答案时不写前缀也能过因为测试数据里没有歧义但实际工程里这是灾难性的习惯。顺手写了前缀养成习惯不亏。3.2 聚合分组先想清楚粒度再选函数聚合类题目看着简单但容易栽在“粒度”上。什么是粒度就是最终结果里每一行代表什么。比如“统计每个客户的订单总金额”——每一行代表一个客户这是客户粒度“统计每个客户每个月的下单次数”——每一行代表每个客户在每个月的行为这是客户月份粒度。粒度决定了你要GROUP BY哪些列。我在做这类题时的顺序是先看输出列里有哪些字段除了聚合函数之外的字段每个都必须出现在GROUP BY里。比如输出列是“月份、消费总金额”那GROUP BY就是月份输出列是“城市、产品分类、销量”那GROUP BY就是城市和产品分类。这个规则基本是死的照着走不会错。接下来是聚合函数的选择。除了SUM和AVG之外最容易被忽略的是COUNT和COUNT(DISTINCT)的区别。题目问“每个客户下单了多少个不同商品”你就要用COUNT(DISTINCT product_id)而不是COUNT(product_id)。后者会重复计数同一个商品买三次会被算成三件。这个知识点很基础但实际刷题时总会有人在“要不要DISTINCT”上犹豫。判断方法就一句话题目说的到底是“次数”还是“去重后的数量”对应到你选函数上。条件聚合也是高频考法在GROUP BY内部嵌入CASE WHENSELECT department_id, SUM(CASE WHEN status completed THEN 1 ELSE 0 END) AS completed_cnt, SUM(CASE WHEN status pending THEN 1 ELSE 0 END) AS pending_cnt FROM orders GROUP BY department_id;这种写法很常见把“按状态统计数量”变成了“在分组内部做条件计数”。它在逻辑上等于对每个组做了一次条件筛选再计数但只用一条SQL就完成了。这就是把执行顺序理解透之后非常自然的想法——CASE WHEN在SELECT阶段对所有行做了一次逐行判断SUM再把判断结果为1的行累加。3.3 窗口函数分组排序和累计计算的大杀器窗口函数这个考点是区分“会写SQL”和“写得优雅”的重要分水岭。很多在刷题阶段没接触过窗口函数的人一看到“分组TopN、组内排名、移动累计”就开始头大。窗口函数的核心思想是在每一行上基于一个窗口范围做计算但行数不减少。这和GROUP BY完全不同——GROUP BY会把多行压缩成一行窗口函数保留所有行。这个区别是理解窗口函数的前提。最经典的是RANK、DENSE_RANK和ROW_NUMBER的取舍。题目“按部门分组查每个部门工资最高的前三个人”用ROW_NUMBERSELECT department_id, name, salary FROM ( SELECT department_id, name, salary, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn FROM employees ) t WHERE rn 3;如果并列第三名也想保留那就把ROW_NUMBER换成DENSE_RANK它不会因为前人有并列名次就跳过下一名。这两个函数的差别是最常考的细节ROW_NUMBER同一排名区间内不会出现并列编号第2名只有一个哪怕工资一样RANK并列时会留下空档比如两个第1名之后直接是第3名DENSE_RANK并列不跳号两个第1名之后还是第2名实际场景里具体要选哪个看题目问“前三个岗位”还是“前三名的工资”。这在面试题里特别喜欢拿来考察你“是不是只会抄模板”。窗口函数还有一个高频用法是累计求和。比如“统计每个日期当天的订单量并算出截至当天的累计单量”窗口函数里的SUM配合ORDER BY就把“前缀和”这件事变成了一行代码SELECT order_date, COUNT(*) AS daily_cnt, SUM(COUNT(*)) OVER (ORDER BY order_date) AS cumulative_cnt FROM orders GROUP BY order_date;这里值得注意窗口函数SUM(COUNT(*))是先算分组内部的COUNT再把COUNT的每行结果做累计。窗口函数本身是在GROUP BY之后执行的所以能和GROUP BY一起用让输出结果里既有每日粒度又有累计粒度非常直观。刷题阶段对窗口函数的要求我认为有三条一是能用三组函数写出分组排序二是能讲清PARTITION BY和ORDER BY分别管什么三是知道窗口函数和子查询先后套用。4. 从“会做题”到“会解题”把业务题翻译成可执行的SQL题库里有一类题题干特别长场景特别完整比如“统计每个城市每个月的用户留存情况”。这类题在工作里最常出现也是刷题时最容易让人崩溃的因为它把阅读理解能力、表结构分析能力、SQL表达能力揉在一起考。我做这类题有一个固定的拆解流程分享出来给你参考。第一步圈出题干里的业务名词。用户、城市、订单、月份、留存这些词大概率对应表或字段。读题的时候我会用笔把每个业务名词标出来然后在草稿纸上列出各自的属性用户表有什么字段订单表有什么字段两个表靠什么关联这一步看似傻但80%的答题偏差都出在这——没搞清楚表结构就急着写代码。第二步确定分析粒度。留存率这个指标它的粒度是什么通常是一个用户在一个注册月份下的某个行为状态。你要输出的表格是“每个月/城市”的留存情况那粒度就是注册月份城市是否在目标月份活跃。粒度定了GROUP BY的字段也就定了。第三步用一个例子走通逻辑。比如2024年1月注册的用户中有多大比例在2024年2月还访问过这个“例子”能把抽象的留存率变成具体的计算路径——先找“注册时间在1月的用户”再判断“这些用户是否在2月出现过”最后用人数除以总注册数。这一步做完你的SQL其实已经写了一大半。第四步翻译成SQL结构。我用CTE分步写每层只做一件事。以“留存率”为例WITH registered AS ( SELECT user_id, DATE_FORMAT(register_time, %Y-%m) AS register_month, city FROM users ), monthly_active AS ( SELECT DISTINCT user_id, DATE_FORMAT(login_date, %Y-%m) AS active_month FROM logins ), joined AS ( SELECT r.city, r.register_month, ma.active_month, COUNT(DISTINCT r.user_id) AS retained_users FROM registered r LEFT JOIN monthly_active ma ON r.user_id ma.user_id AND ma.active_month DATE_FORMAT(DATE_ADD(STR_TO_DATE(CONCAT(r.register_month, -01), %Y-%m-%d), INTERVAL 1 MONTH), %Y-%m) GROUP BY r.city, r.register_month, ma.active_month ) SELECT city, register_month, SUM(retained_users) / COUNT(DISTINCT ...) AS retention_rate FROM joined ...当然上面这段是为了展示分步逻辑的示意真正的留存率SQL还要考虑分母口径——是当月注册总人数还是当月注册且满足某些条件的用户总数。不同口径算出来的留存率差别很大这也是业务题的坑题目没说明分母你就需要根据上下文判断口径并在答案里解释你选择的口径。那“连续登录N天”这类题逻辑也是一样的。题干里“连续”这个动作翻译成SQL通常就用日期减去行号差值相同的就是连续区间SELECT user_id, MIN(login_date) AS streak_start, MAX(login_date) AS streak_end FROM ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS diff FROM logins ) t GROUP BY user_id, diff;这里ROW_NUMBER给用户内部的日期排了序日期减去排序序号后同一个连续区间会产生相同的diff值再GROUP BY这个diff就能把连续区间分出来。很多人第一次看会觉得很妙但原理其实清晰连续日期的每相邻两行日期差1而排序差1一减就一样了。这种“翻译技巧”在题库中大量出现你掌握会话了遇题不慌。我特别想强调前面说的“用例子走通逻辑”这一步。我在辅导别人时发现大部分写不出SQL的人其实是卡在第一步和第三步——他们没有把业务描述转化成具体的输入输出样例。只要你心里能举出一个具体的用户某用户某月注册某月又登录那我该数谁不该数谁这个因果关系清楚了SQL的结构自然而然就出来了。SQL说白了就是集合和关系运算而业务翻译就是把人的语言转化成集合关系的第一步。5. 那些“一看就会、一写就错”的基础细节刷题到了一定数量你会发现出错往往不在大逻辑而在细节。这些细节单独拎出来每个都很简单但组合在一起足够坑掉一大批人。我按自己的经验列几个最高频的。第一个是COUNT(DISTINCT)在多表连接后“膨胀”。两张表JOIN之后行数可能会因为一对多关系成倍增加这时候COUNT(DISTINCT user_id)是安全的但直接把度量值SUM(order_amount)就危险了。比如用户表和订单表JOIN后一个用户如果有三个订单他的贷款额度或基本信息会在结果里出现三遍SUM这个额度就会变成三倍。这是实际业务里极其容易踩的坑。解决办法是先在大表上做聚合再和小表JOIN或者用窗口函数/子查询保证度量值不被稀释。第二个是NULL的判断习惯。很多人写“查所有未填写手机号的用户”会写WHERE phone 或者WHERE phone 123但NULL不参与任何比较运算phone 匹配不到NULL值。正确写法是WHERE phone IS NULL。反过来的场景同样如此WHERE phone 123会把NULL行过滤掉可你本来也许想保留它们。碰到这种题第一反应应该是“这题考NULL吗”而不是直接套字符串比较。第三个是日期和字符串的处理。题目里给“2024-01-01 12:30:00”这种完整时间戳要你按“月”统计你就必须先做格式提取。不同数据库的函数不一样MySQL里是DATE_FORMATSQL Server里是FORMATPostgreSQL里是TO_CHAR。刷题平台一般固定一种方言问题不大真到了工作里写之前先确认“当前数据库是哪个”别把MySQL的函数甩到SQL Server上再对着报错发呆。我建议刷题时顺手把主流三四个数据库的日期函数差异记下来这也算一种“题库知识点”。第四个是LIMIT与OFFSET在分页题里的坑。有些面试题会问“第二高工资的人”你发现用ORDER BY LIMIT 1 OFFSET 1可以拿到但如果工资有并列这个解法就可能漏掉排第二的那批人。这类题的考点其实是DISTINCT或DENSE_RANK看你到底需要“第二行的数据”还是“第二名的数据”。分页思路没错但要先搞清楚题目语义。第五个是LIKE的性能问题和转义。LIKE %abc%在有大量数据时基本走不了索引刷题时数据量小无所谓可面试官一追问“这个查询在千万级数据上怎么优化”你就得能接住“前缀通配符会放弃索引”这个知识点。另外如果搜索内容本身带有%或_要用ESCAPE指定转义字符SELECT * FROM articles WHERE title LIKE 100\%完成率 ESCAPE \;这种细节点很少有题库会覆盖但实际工作或面试加问环节非常容易碰到。我的建议是刷题过程中不要只盯着“这一题过没过”每过一道题顺手看看它涉及的操作在真实生产环境下会有什么隐患把题库知识网织得更密一点。6. 一套可复制的刷题路线和复盘方法讲完知识模块最后给一套我个人验证过、周围人也反馈有效的刷题路线。它不适合所有人但对初学SQL和准备面试的人是一个可靠的参考。路线分四个阶段每个阶段的刷法不一样目标也不一样第一阶段是语法熟悉期。这个阶段的目标只有一个——把SELECT、WHERE、JOIN、GROUP BY、ORDER BY、LIMIT这些基本语法写到条件反射。你的任务就是用最简单的基础题堆量不需要一上来挑战难题关键是保证对子查询、聚合、连接这些词不陌生。我建议这个阶段每天固定写十道极简题手不能生。第二阶段是题型分类期。把常见题型按考点分类一类一类地横向刷。比如今天只刷连表题明天只刷窗口函数后天只刷条件聚合。分类刷的好处是你可以在同一天内反复看到同一类题目的不同变体归纳出规律后再进入下一类。这个阶段是建立“题型脑图”的黄金期。第三阶段是综合应用期。这个阶段直接挑战综合性偏高的题目一道题里往往同时包含连接、聚合、窗口和子查询。目的是解决“每个单独知识点会一组合就乱”的问题。综合题不要求刷太多每天两三道就够重点是画清执行链路把多个知识点串起来。第四阶段是限时模拟期。如果你是为了面试这个阶段非常重要。给自己定时二十分钟一道题模拟面试现场的紧张感。平时不限时刷题容易陷入“慢慢试”的状态限时能逼迫你把第二轮建立的思维模型快速调出来。我自己试过限时和不限时的准确率差别很大越早适应越好。刷题之外我强烈建议每一个人建立错题本不用花哨就是一个表格或者文档记录以下内容题目出处和日期知识点标签连接、窗口、去重、日期等当时的错误思路或卡点正确的思考路径同类题的变体提醒这个错题本的价值不在于“记录”而在于复盘。我见过太多人刷完题万事大吉下次遇到同样的坑照样踩。而复盘动作本身就是在把短期记忆转换为长期技能。每周花一小时把本周错题看一遍比多刷十道新题有价值得多。路线说完了再说说复盘时的几个问法。复盘一道做对的题你可以问自己我用的方法是最直接的吗如果数据量放大100倍这个写法还好使吗有没有人可能写出比我更简洁的版本复盘一道做错的题就问我当时为什么想偏了是漏看了题干条件还是没想起这个函数这类“元认知”问题比题目本身的答案更能提升你的SQL水平。如果要说这套路线最大的特点那就是不以“做完”为目标而以“能把思路讲出来”为目标。每道题做完之后试着用大白话把思路讲给身边的人听或者对着镜子自言自语。如果你能讲清楚“为什么这里要用LEFT JOIN而不是INNER JOIN为什么窗口函数比GROUP BY更好”那这道题就算真正吃透了。讲不清楚的地方就是你还没理解透的地方也恰恰是你需要回去补的地方。我个人刷了几年SQL相关的东西最大的体会就是SQL题库只是载体真正重要的是你脑子里有没有能够灵活组合的基础模块以及有没有一套高效的复盘系统。这两件事做扎实了SQL题目对你来说就是固定的几个套路哪怕换再多花样也不影响你稳定发挥。
阅读完成 · 觉得有帮助?