ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL每日一题180天:窗口函数、去重与慢SQL优化实战总结

SQL每日一题180天:窗口函数、去重与慢SQL优化实战总结 我运营这个“sql每日一题”练习项目到今天已经满180天了。最开始的原因特别简单我从业务岗转数据岗面试时被一道“找出连续登录3天的用户”问得说不出话入职后写报表SQL更是各种卡壳。为了把这块短板补起来我给自己定了个死规矩每天至少做一道SQL题哪怕加班到晚上十点也要花15分钟写完再睡。半年过去了这个习惯直接让我从“只会SELECT”变成了能跟开发讨论索引、敢于接手慢查询优化的人。这篇就把我这套“SQL每日一题”的完整玩法拆开讲题目怎么设计、每天怎么练、哪些坑是必须踩一遍才知道的。1. 为什么我会坚持做“SQL每日一题”1.1 学SQL的三个真实痛点先说第一个痛点看得懂写不出。这是绝大多数人的状态看教程里那些经典查询每一行都认识合上书让自己写就是憋不出来。本质上是因为SQL是“声明式”语言你写的是“我要什么”而不是“怎么拿”思维方式和过程式的编程语言完全不一样不靠大量实操根本建立不起这种思维。第二个痛点是会写基础但不会写复杂场景。单表过滤、简单JOIN人人都会可一到去重取最新、分组内排名、连续区间归并这类业务题就不知道从哪下手。第三个痛点是缺少标准答案。SQL题不像其他编程题有一堆公开解法讨论写完连对不对都不知道更别说性能好不好。这三个痛点叠加光是“刷题”解决不了需要一套带有反馈和复盘机制的练习体系。1.2 每日一题背后的设计逻辑为对付上面三个痛点我把练习设计成“每日一题”而不是“每周十题”核心逻辑有三个。第一是刻意练习。任何一个技能从“会”到“熟”需要的是在最近发展区做题而不是反复做舒适区以内的题目。每天一道题难度贴着头皮逼自己每天用SQL思考一次90天左右就能形成条件反射。第二是利用遗忘曲线。SQL语法和函数不去用一个星期就忘。每天一题相当于用最简单的方式做间隔重复尤其窗口函数、日期处理这类容易忘的知识点通过高频率的短时复现比周末一口气刷5道题的记忆留存率高很多。第三是建立“最小启动成本”。每天15分钟这个时间量级几乎不需要意志力加班再晚也能挤出来。一旦把目标拆到15分钟坚持的门槛会大幅降低。我当时把题目放在一个在线表格里手机也能看回家路上就把题读了到家直接开写。2. 题目设计一张覆盖半年的SQL知识地图2.1 难度阶梯从单表查询到综合场景我的题目不是网上随便扒的而是按阶段自己搭的难度阶梯总共分六个阶段每阶段30天覆盖180天。第一个月做单表查询训练SELECT、WHERE、ORDER BY、LIMIT、聚合函数目标是消灭语法不熟的问题。第二个月做多表连接INNER JOIN、LEFT JOIN、RIGHT JOIN、自连接、简单子查询这个阶段最容易劝退因为JOIN的结果集变多了需要脑补临时表的样子。第三个月做分组与去重GROUP BY、HAVING、DISTINCT、复合排序把“每个分类取前N”“统计每个用户订单数”这类问题练熟。第四个月练窗口函数ROW_NUMBER、RANK、DENSE_RANK、LAG、LEAD、SUM OVER这一个月下来几乎所有“排名、环比、同比、分组内比较”类问题都不再是难题。第五个月是综合场景题连续登录、最大在线人数、中位数、留存率、漏斗转化这些是把前面所有知识串起来。第六个月做优化类题目看执行计划、找索引失效原因、改写慢SQL从“写得对”过渡到“写得好”。注意这个阶梯不是严格按照“从易到难”线性走而是参考了“同类知识点集中训练”的原理。比如窗口函数如果分散在好几个月里练每次都要重新回忆语法不如花一个月集中突破形成肌肉记忆后反而省时间。2.2 核心知识点的覆盖与串线这道题能不能坚持下去很大程度上取决于题目是不是覆盖了足够多的高频知识点并且在不同题型中反复出现。我把核心知识点整理成了下面这个覆盖表你可以对照看看自己有没有漏网之鱼知识点典型题目类型重要程度基础查询条件过滤、排序、分页必会聚合分组GROUP BY HAVING必会多表关联INNER/LEFT/RIGHT JOIN、自连接必会子查询标量子查询、 EXISTS必会去重DISTINCT、GROUP BY、窗口函数必会窗口函数排名、滑动求和、分组TopN高频日期处理DATEDIFF、DATE_SUB、DATEADD高频字符串处理CONCAT、SUBSTRING、LIKE高频空值处理IS NULL、COALESCE、NULLIF高频集合操作UNION ALL、INTERSECT、EXCEPT中频递归查询生成数字序列、树形结构低频性能优化EXPLAIN、索引、慢SQL改写进阶在实际做题过程中还要有意识地把知识点“串线”。比如“每个用户最近一单”这道题就可以用三种方式做子查询取MAX、窗口函数ROW_NUMBER、关联NOT EXISTS。把同一道题用不同方法解一遍比做三道新题更能帮你打通知识间的联系。2.3 多数据库差异同一道题换个引擎就翻车日常工作中MySQL、SQL Server、Oracle、PostgreSQL都可能遇到。设计题库时我特意加了“多数据库兼容”维度同一个业务题目分别用MySQL和SQL Server各写一遍你很快就会发现差距。分页就是一个经典差异MySQL用LIMIT 10 OFFSET 20SQL Server 2012用OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLYSQL Server 2008只能用ROW_NUMBER模拟分页Oracle则是ROWNUM或FETCH FIRST。日期函数差异更大MySQL的DATE_SUB、DATE_ADDSQL Server的DATEADD、DATEDIFFOracle的SYSDATE、ADD_MONTHS。字符串拼接也不一样MySQL用CONCATSQL Server直接用号但SQL Server里NULL拼接会变成NULLMySQL不会。还有排序差异同样一个ORDER BYMySQL和PostgreSQL默认把NULL排在最前面Oracle默认把NULL排在最后SQL Server默认NULL最小升序时在最前。这些差异不实际对比做一次很难记住。我的做法是同一道题每周抽一天在另一个数据库里跑一遍。跑完之后不光是语法差异连SQL模式例如MySQL的ONLY_FULL_GROUP_BY都会让你重新思考自己的写法是否规范。3. 每天15分钟的做题流程3.1 审题把业务需求翻译成SQL做题的第一步不是打开编辑器而是审题。我见过的很多新手拿到题就开始写SELECT写到一半发现表关系没搞清楚再回去看题一来一回浪费10分钟。正确的审题顺序是先读三遍题把业务问题抽象成数据问题。比如“找出连续3天登录的用户”业务问题是“用户活跃连续性”数据问题是“同一用户相邻日期的间隔是否为1”。这一步做不扎实后面全白搭。接着要圈定数据范围涉及哪几张表、需要什么字段、表的粒度是什么。比如订单明细表一行代表一个SKU订单表一行代表一个订单如果你要“统计每个用户的订单金额”就必须搞清楚金额字段在哪个粒度。审题时最好能快速把题里的关键条件标注出来时间范围、分组维度、去重字段、排序规则、是否包含无数据的用户、NULL怎么处理。这些都是SQL题里最常见的“隐藏坑”。我建议你做一个“审题模板”固定成五个问题输出几列行粒度是什么过滤条件有哪些分组维度是什么排序和去重要求是什么每次做题花两分钟把这五个问题写下来后面写SQL会顺畅非常多。3.2 写题从“能跑”到“能看”写题阶段的第一个目标不是性能而是先跑出正确结果。先把最笨的写法写出来哪怕是一个大嵌套子查询只要结果是正确的就已经拿到了60分。比如上面说的“连续登录”题笨办法是先给每个用户的登录日期按天排序再跟上一行日期做差然后数连续天数。这时候不用想效率先确保逻辑闭环。拿到正确结果后再看一眼自己的SQL能不能更“吓人”一点——不是炫技而是让代码更简洁、更接近生产环境的需求。以连续登录为例最终版本可以用一个非常经典的写法-- MySQL 环境 WITH login_with_group AS ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_date) DAY) AS group_date FROM login_log GROUP BY user_id, login_date ) SELECT user_id, COUNT(*) AS consecutive_days FROM login_with_group GROUP BY user_id, group_date HAVING COUNT(*) 3;这段SQL的原理非常值得说清楚ROW_NUMBER()按登录日期排序后如果是连续日期登录日期减去行号按天会得到同一个“锚点日期”group_date如果中间断了一天这个锚点日期就会变。再对user_id和group_date同时分组连续的部分就被自动切在了一起。这个思路本质上是用“日期减排名”构造分组特征理解了它连续登录、连续签到、活跃周期这类问题就全部通了。写完优化版之后记得把边界条件都验证一遍空表、所有用户都不满足条件、同一天有重复登录记录很可能需要先GROUP BY去重、日期跨年。不要只看表面结果对就觉得完事了SQL里很多隐蔽错误在边界数据上才会暴露。3.3 复盘一道题变成三道题很多人刷题只刷“写出来”这道工序我却把复盘当成整个项目里最重要的一环。我的复盘分三步。第一步是写“一题多解”。同一道题至少再写一种不同思路的解法。比如要求“每个分组的第一条记录”除了窗口函数ROW_NUMBER还可以用NOT EXISTS或者关联子查询。做多了你会发现每种解法背后对应一种数据处理的思维模型多解训练就是在建立模型库。第二步是换环境跑。如果今天用的是MySQL复盘时我会把同样的逻辑拿到SQL Server里写一遍。可能只是把DATE_SUB改成DATEADD把LIMIT改成OFFSET FETCH但就是这种“翻译”训练让我在后来接手不同数据库的项目时几乎没有适应期。第三步是沉淀笔记。用一句话记录这道题的核心思路、踩到的坑、最优解法的名字。比如“连续N天问题日期减ROW_NUMBER按差值分组”或者“分组TopNROW_NUMBER() OVER(PARTITION BY)外层过滤rn1”。这些一句话笔记积累到第100天的时候就是一本私人SQL题库地图后期复习效率极高。4. 答题中的踩坑与排查实录4.1 NULL值SQL里的隐形地雷我敢说“每日一题”做下来遇到最多的坑就是NULL。NULL不是空字符串也不是0它表示“未知”。这里面最经典的是“IS NULL”问题WHERE 字段 NULL 是永远查不出数据的必须用WHERE 字段 IS NULL。这个坑几乎所有新手都会踩一次。聚合函数跟NULL的纠缠更多。COUNT(*)统计的是行数COUNT(字段)统计的是非NULL值个数这俩在有空值的表上结果完全不同。SUM和AVG会忽略NULL值这意味着如果一个班有10个学生3个人没参加考试AVG(score)其实是按7个人算的均值不是10个人。如果你想要“按全班人数算均分”就得用COALESCE把NULL转成0。NULL还影响JOIN结果尤其是LEFT JOIN时右表没有匹配记录右表字段全是NULL如果你在WHERE里用了右表字段做过滤比如WHERE b.id 1NULL就会被过滤掉LEFT JOIN直接退化成了INNER JOIN。这个“看似没写错其实结果少了一截”的问题排查起来特别隐蔽。我现在做题时凡是碰到聚合、JOIN、日期过滤都会下意识先问一句这里面有没有NULL用不用COALESCE这个习惯帮我躲过了无数个线上报表数据的坑。4.2 去重的多种写法与选型“sql语句去重”“清洗sql语句去重”这类关键词搜索量很大说明去重也是高频问题。去重并不是只有DISTINCT一条路我做过一道题专门对比了三种去重方式的区别。拿“每天每个用户的第一条订单”举例第一种DISTINCT只能去掉完全相同的整行无法做“按某字段分组去重取最新”这种操作适用范围很窄。第二种GROUP BY MAX/聚合函数适合“取每个分组的最新时间、最大ID”这类标量需求写法直观性能也不错。 第三种窗口函数ROW_NUMBER() 过滤条件这是最通用的方案能取到整行记录-- SQL Server / PostgreSQL 写法 SELECT user_id, order_id, order_time FROM ( SELECT order_id, user_id, order_time, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY order_time DESC) AS rn FROM orders ) t WHERE rn 1;这个写法的核心在于PARTITION BY决定“去重的分组依据”ORDER BY决定“保留哪一条”。如果你要保留时间最早的一条就把ORDER BY改成ASC。做这个题目时我还发现一种容易出错的写法有人把外层过滤条件rn 1写进子查询的WHERE里直接在窗口函数前面加WHERE这样会报错因为窗口函数是在WHERE之后才计算的。记住一个顺序WHERE先过滤再算窗口函数所以“过滤窗口函数结果”必须放到外层查询。4.3 窗口函数中的排序细节窗口函数是我练了整整一个月才彻底吃透的知识点最难的不是语法而是排序函数的细微区别。ROW_NUMBER()、RANK()、DENSE_RANK()这三个函数在排名场景下结果差异很容易让人困惑。ROW_NUMBER()是纯粹的物理编号永远产生不重复的序号RANK()是跳跃排名遇到并列会跳过后续序号DENSE_RANK()是连续排名并列之后不会有跳跃。举例来说一班成绩为98、95、95、90ROW_NUMBER给出1、2、3、4RANK给出1、2、2、4DENSE_RANK给出1、2、2、3。业务上“取前三名”如果用RANK可能返回4行用ROW_NUMBER一定返回3行用DENSE_RANK在三者里最接近“并列都算”的直觉。理解了这点很多面试题和业务需求就再不会搞错。窗口函数里还有一个隐藏坑就是ORDER BY配合窗口聚合。SUM(amount) OVER(ORDER BY date)是累加求和这大家都知道但它默认的窗口范围是从分区起点到当前行如果你不写ORDER BY就是全部分区求和写了ORDER BY就变成累计求和。同样一个SQL多写一个ORDER BY语义完全不同。4.4 慢SQL优化从写完到写好的分水岭第151天到第180天我进入“慢SQL优化”专题每天的题不再是算结果而是给一段“跑得慢”的SQL想办法让它变快。这个阶段学的东西直接改变了我的写法习惯。第一是看执行计划。MySQL用EXPLAINSQL Server看“显示估计的执行计划”。你很快会发现全表扫描和索引查找之间的差距是天壤之别。EXPLAIN里type字段从ALL到range到ref的变化意味着SQL从扫全表变成了走索引。第二是理解索引失效的常见场景。对字段做函数运算是最常见的杀手比如WHERE DATE(order_time) 2024-01-01哪怕order_time有索引因为函数包裹导致索引失效。正确写法是WHERE order_time 2024-01-01 AND order_time 2024-01-02。还有隐式类型转换字符串字段跟数字比较也会导致索引失效。第三是避免不必要的开销。SELECT * 会导致回表读取更多数据能写明确字段就写明确字段。子查询和JOIN之间要做取舍很多场景下EXISTS比IN更高效但数据量小时差异不明显。不要盲目迷信哪种写法一切以实际执行计划为准。优化题做得多了我再看自己早期的SQL发现几乎每一句都能挑出毛病——这个阶段才是真正从“能把SQL写出来”变成了“能把SQL写好”。5. 环境与工具准备5.1 本地SQL练习环境怎么搭做“每日一题”环境搭建不需要很复杂我的建议是本地装一套轻量数据库加上一个可视化客户端就够了。如果你主力用MySQL装MySQL 8社区版如果偏向SQL Server装SQL Server Express版免费且足够练习用。两个都可以在官方渠道下载安装时记得选择正确的身份验证模式SQL Server Express装完默认是Windows身份验证连接字符串和远程访问跟开发版略有差异。个人练习完全可以先装MySQL因为MySQL语法通用度最高社区讨论也最多。装完之后可以用下面这个脚本快速造一批练习数据CREATE DATABASE sql_daily; USE sql_daily; CREATE TABLE users ( user_id INT PRIMARY KEY, user_name VARCHAR(50), created_at DATE ); CREATE TABLE orders ( order_id INT PRIMARY KEY, user_id INT, order_amount DECIMAL(10,2), order_time DATETIME ); -- 批量插入一行 INSERT INTO users (user_id, user_name, created_at) VALUES (1, zhangsan, 2023-01-01);模拟数据的技巧是写一段循环插入逻辑或直接用递归CTE造日期序列比如生成2023年1月1日到2023年12月31日的每一天配合RAND()生成随机金额。有了足够多的数据题目的边界才能跑出来。可视化客户端我用过几款最顺手的是DBeaver免费、跨平台、支持MySQL/SQL Server/PostgreSQL一网打尽。它可以直接查看执行计划这对做优化专题特别实用。如果你想练SQL Server语法也可以在SQL Server Management Studio里做SSMS本身免费但只支持Windows新版可以从官方渠道下载。5.2 题库与打卡工具推荐自己设计题库当然是最贴合需求的但前期的题目从哪来也是很多新人卡住的地方。我推荐几个渠道LeetCode的数据库题库题面质量高很多经典业务场景题都在里面牛客网的SQL题库偏国内互联网公司面试风格字符串处理和连续登录类的题比较多GitHub上也有很多开源SQL刷题仓库比如一些“SQL面试题合集”“SQL练习100题”项目答案质量参差但胜在免费且题目量大。我的做法是LeetCode当主题库自己根据工作场景订单、用户、商品、日志改编题目。这套“每日一题”做完后我又把这些题目沉淀成了一个个人仓库每个题目一个SQL文件加一个README笔记。放到GitHub上之后居然还能帮到一些同样想练SQL的朋友。打卡工具我用的是一张在线表格列分别是日期、题目编号、涉及知识点、是否一遍过、解法关键词、耗时。每天做完题填一行一个月过去那张表格就是你的进步曲线。我后来还加入一列“这道题我有没有更好的写法”这列会逼我在复盘时不偷懒。到了这个阶段我回头看最初为什么会被“连续登录3天”那道题难住原因已经很清楚没有建立“把业务问题转化为数据操作”的思维链路也没有掌握窗口函数这类高频工具。每天一题的做法看起来笨实际上是把一个复杂技能拆成了无数个小循环审题、写SQL、验证、复盘、积累笔记。这个过程不神奇但只要你愿意坚持它就是普通人在SQL这条路上最可靠的成长方式。最后分享一个我在第100天时悟出来的小技巧不要追求每天都做新题。每周留一天把本周做过的题重新跑一遍这道题一跑比做三道新题对记忆的巩固效果都好。如果你也想启动自己的“SQL每日一题”项目我的建议很简单——今天就是第1天先把环境装上找一道最简单的题写完它把它记进你的打卡表。剩下的就交给时间。
返回列表