LeetCode 1148 文章浏览 I:SQL条件过滤、去重与排序的经典实战
这道题我有一个很深的印象——不是因为解法复杂恰恰相反它是不少人在LeetCode上接触的第一道数据库题表结构简单、逻辑直白、最后只输出去重后的ID列表。“1148. 文章浏览 I”看起来简单到有点像是在送分但它其实把SQL查询里最容易被忽略的三件事全揉在一起了条件过滤、去重、排序。很多人第一次写完就报错不是逻辑错了而是漏了ORDER BY或者用了SELECT DISTINCT但没想清楚它到底干了什么。这篇文章我就从题目本身出发把这道SQL题的完整拆解、解题姿势、背后的业务含义以及从它延伸出来的真实场景都讲一遍。适合刚入门SQL的读者也适合准备面试想系统过一遍基础查询的人——因为这道题考的不是你会不会写SELECT而是你会不会“严谨地写SELECT”。1. 题目到底在问什么先读懂文章浏览这个表结构LeetCode 1148题给的是一张名叫Views的表四个字段article_id文章ID、author_id作者ID、viewer_id浏览者ID、view_date浏览日期。表里的每一行代表某个人在某一天浏览了某一篇文章。题目的要求是找出所有浏览过自己文章的作者按作者ID升序返回。判断标准就一条author_id和viewer_id相等——也就是说一篇文章的作者本人打开了这篇自己的文章这行记录就符合条件。我先说说这张表结构里值得注意的细节。它存储的是浏览行为不是浏览统计。每一行是一次独立的浏览事件同一个人一天看了同一篇文章三次表里就可能有三行记录日期、观看者、文章、作者四个维度缺一不可。这个表在真实业务里通常叫用户行为日志表、埋点明细表或者访问流水表底下往往还带着时间戳精确到秒甚至毫秒。真正的生产场景里这种表极其庞大动不动几亿行做题的目的是在这个模型上用最标准的SQL写法拿到预期结果。细看字段名author_id出现在行里意味着这张表在设计上容忍文章作者信息被冗余在浏览记录里这件事。你可能觉得这违反了范式但真实场景里恰恰常见——数据分析师拿到手的明细表经常是已经关联好维度的宽表作者ID直接放在行为记录里就是为了减少查询时的JOIN操作。所以这个表结构本身就不是随便设计的它背后对应的是宽表建模的思路。再看题目的输出要求只要 author_id 这一个字段而且要去重。一个作者可能浏览了自己的好几篇文章也可能分好几天浏览自己的文章——但这些都不重要题目只要这个人的ID出现一次就够了。所以至少有一次这个隐含语义是整道题第一个容易被忽略的点。2. 一条SELECT就能过但里面藏着三个小坑2.1 条件过滤WHERE子句该怎么写我们需要筛选出author_id viewer_id的行。写成SQL就是SELECT author_id FROM Views WHERE author_id viewer_id这一条语句几乎是条件过滤的标准写法了。但我要强调一个容易出错的地方题目里有一个隐含要求——同一个作者如果有多条符合条件的行为记录最终输出中不能出现重复。比如某个作者浏览了自己的三篇文章按上面的写法输出三行ID这就不符合题目的至少一次的语义了。2.2 去重的三种姿势DISTINCT、GROUP BY、还是窗口函数既然要避免重复最常见的做法是加DISTINCTSELECT DISTINCT author_id FROM Views WHERE author_id viewer_id ORDER BY author_id这样去重、排序一步到位提交就能过这是最简单直接的写法。除了DISTINCT也完全可以用GROUP BY来做去重SELECT author_id FROM Views WHERE author_id viewer_id GROUP BY author_id ORDER BY author_idGROUP BY的语义是按作者分组每组只返回一行自然也就实现了去重。这两种写法在结果上完全等价差别在底层的执行逻辑上——DISTINCT本质上是在做排序去重或者哈希去重GROUP BY则要先分组再聚合在只取分组列的时候它追求的结果就是唯一值。在小数据量表上性能差异可以忽略不计但在千万行的真实明细表上两者的执行计划会走不同的算子这个差别我后面专门展开讲。说到这还得提一句窗口函数方案SELECT DISTINCT author_id FROM ( SELECT author_id, ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY view_date) AS rn FROM Views WHERE author_id viewer_id ) t WHERE rn 1 ORDER BY author_id这种写法在本题里属于杀鸡用牛刀但它能给你一个新的理解维度——如果题目变成了找出浏览过自己文章且最近一次浏览日期是某某时间之后的作者窗口函数的优势就体现出来了。就本题而言我不推荐用它但知道它的存在能帮你理解SQL解决重复问题的全景。2.3 排序为什么就不能省掉ORDER BY很多人在DISTINCT版本里忘记ORDER BY结果提交直接报错。MySQL的默认输出顺序可能是插入顺序、索引顺序、哈希分布顺序——没有任何一种可以保证是最终想要的ID升序。题目明确要求按ID升序返回得不到这个顺序就属于输出不满足要求判题系统直接判定错误。在没有索引的情况下MySQL返回行的顺序通常是全表扫描的物理顺序这在多版本数据库上都没法作为可靠预期。就算碰巧这一次升序了换个数据分布顺序就乱掉——所以ORDER BY author_id是这道题的刚需不是可选项。3. 从作者浏览自己的文章说起这道题背后的三种业务逻辑这道题的WHERE条件inner join到真实业务里其实对应着至少三类分析场景。先说最常见的作者创作积极性分析。在内容社区产品里作者发布文章后反复查看自己的文章页面可能是检查排版、数评论、盯数据。如果一位作者频繁浏览自己的文章说明他对自己作品的投入度高反过来也说明他刚发布不久、处于活跃期。拿这道题的SQL去扫描每天的文章浏览流水就能圈出今天就着自己的文章看了至少一次的作者列表再配上浏览次数的统计就成了运营做作者召回的基础指标。第二类是异常行为的初筛。在反作弊场景里一个账号既是作者又是浏览者、且频繁浏览自己的高价展示内容——平台方会引入作者自浏览的行为特征来配合风控判断。这不是说作者看自己的文章就一定有恶意为难而是自浏览这个行为在风控模型里需要单独记录。如果一张行为宽表里没有author_id和viewer_id两个字段风控分析师就得通过文章ID反查作者表做JOIN成本和复杂度都上去了——所以宽表设计在这里有它非常现实的意义。第三类是内容推荐去重。推荐系统在做协同过滤时如果拿用户浏览记录当训练数据必须把作者浏览自己文章这类自交互动作剔除否则模型容易学出一个荒谬的偏置——把文章推荐给作者本人。说到底数据分析里面经常要做排除自己看自己的清洗正是这道题的SQL帮我们筛选出来的是反向集合。做题时只是输出一个作者ID列表放到真实场景里它就是数据管道上的一个清洗节点。4. 为什么面试官爱问这道题看上去在考SQL其实在考严谨性这道题被LeetCode放在数据库入门区但它频繁出现在一线公司的SQL初面里是有原因的。我见过不少候选人选了DISTINCT ORDER BY提交通过但一问为什么要加DISTINCT就答不上来——他们以为DISTINCT是可选优化甚至有人觉得表里数据本来就该是唯一的。第二个追问是如果这张Views表里恰好没有重复行你的SQL还需要DISTINCT吗答案是如果业务语义要求唯一不管数据现状怎样都得加因为数据质量不是永远可靠的今天没有重复不代表明天没有schema里也没有对(author_id, article_id, viewer_id, view_date)建唯一约束。这道题就在这里它考的是一条SQL里的业务语义是不是被你想透了而不仅仅是语法上对不对。第三个追问通常是关于GROUP BY和DISTINCT哪个更好。这问题在我当年的面试里也遇到过没有绝对答案得看数据分布和索引情况。在字段可空性不同的场合两者的语义都会有细微差别。GROUP BY在部分数据库的优化器里可能走HashAggregateDISTINCT在一些场景下会走Unique排序——如果查询带索引GROUP BY可以利用索引的有序性去分组DISTINCT同样可以利用索引。真到定义性能对比时要在具体的执行计划上说话。还有第四种追问为什么不用*或者不用子查询如果SELECT *会把所有字段输出不符合题目要求子查询用IN的话SQL一样能跑但嵌套层数和可读性都不如直接WHERE等值比较。这道题的优秀解法恰恰是最简单的写法——数据库查询的最高境界是能用一层SELECT解决的问题绝不套多余结构。5. 避坑实测我自己在跑通这道题时遇到的三个意外5.1 空指针和NULL值陷阱Views表里如果author_id或者viewer_id存在NULLWHERE author_id viewer_id 这个条件会直接把这些行过滤掉。因为SQL里NULL和任何值做等值比较结果都是UNKNOWN既不是TRUE也不是FALSE。所以带NULL的行会被排除。这个行为在题目里不影响结果但真实数据里NULL值很常见你在清洗脚本里得明确知道它们是被静默过滤掉的。要做包含NULL也视为相等的判断得写上WHERE author_id IS NOT DISTINCT FROM viewer_id或者用操作符——不过这属于边界情况绝大多数业务需求里NULL行本来就是脏数据。5.2 DISTINCT加多列的结果陷阱如果哪天题目改成找出浏览过自己文章的所有作者并同时列出他们浏览过的文章ID写法就变成SELECT DISTINCT author_id, article_id——这时候去重是以(author_id, article_id)二元组为单位的。一个作者可能对应多行记录因为文章ID不同。这是很多改动时容易踩的坑要时刻注意DISTINCT到底作用在哪一列组合上。SQL是声明式语言你在SELECT列表后加一个DISTINCT它作用的是整个列表所有列的组合而不只是后面的第一个字段。5.3 MySQL的ONLY_FULL_GROUP_BY模式在5.7以上版本的MySQL里默认开启了sql_modeONLY_FULL_GROUP_BY。如果你写了类似GROUP BY author_id却非要在SELECT里取article_id会直接报错。这对做题影响不大但正好提醒你区分两种问题去重取唯一作者和按作者分组并聚合多行信息是完全不同的两件事。前者用GROUP BY没问题后者必须搭配聚合函数或者窗口函数。理解这个边界比背下一条SQL语句重要得多。6. 高频变体把这道题改一改能考出完全不同的知识点这道题作为I自然有II甚至III。但我说的不光是LC后面的题目编号而是面试官可能临场出的变体。变体一输出有自我浏览行为的作者ID和浏览总次数。SQL写法SELECT author_id, COUNT(*) AS cnt FROM Views WHERE author_id viewer_id GROUP BY author_id ORDER BY author_id这一改DISTINCT就完全不够用了必须分组聚合。这里要注意COUNT(*)统计的是行数也就是浏览事件数如果想统计作者浏览了几篇不同文章得用COUNT(DISTINCT article_id)。变体二找出三位及以上作者共同浏览过的文章ID。这要自连接Views表本质上是多表关联分组过滤考察的难度上了一个台阶。变体三找出作者最后一篇浏览自己文章的时间。这就轮到MAX和GROUP BY登场SELECT author_id, MAX(view_date) AS last_view_date FROM Views WHERE author_id viewer_id GROUP BY author_id这类变体万变不离其宗——先是条件过滤再是分组聚合最后是排序或HAVING条件过滤分组。把这三板斧吃透了不管题目换成文章表、用户表、订单表你都难不倒。7. 真实执行计划的简单观察DISTINCT与GROUP BY的性能差异这道题的数据量只有不到十行压根谈不上性能但为了让你在面试里能说出个子丑寅卯我建议你到本地MySQL里执行一下EXPLAIN观察执行计划。在我自己常用的MySQL 8.0环境里数据量构造到一百万多行、并且对(author_id, viewer_id)建了索引之后DISTINCT语句的执行计划往往显示Using index for group-by优化器会选择利用索引的有序性做去重GROUP BY版本在同样索引下也可能走上类似的路径。两者最终殊途同归。但如果你在查询里加上了额外的大字段或者索引建得不合理优化器可能改为Using temporary Using filesort这时候两版本的性能差异才真正拉开。Distinct天然自带排序去重GROUP BY则需要内存临时表配合聚合。在小表上这些执行细节都是0和1毫秒的区别可一旦上了真实的生产数据规模执行计划的不同就是秒级和分钟级的差异了。我推荐一个习惯拿到这类去重排序的需求先写DISTINCT版本这是最符合声明式思维的写法遇到分组聚合字段、或者要配合HAVING过滤分组结果时再换成GROUP BY。不要为了显得技术高深非得选GROUP BYSQL的第一审美是可读性和正确的业务语义。最后一个我自己常用来加深印象的做法是把这道题的SQL改成数据质量校验脚本每天跑一遍看看有没有作者ID在浏览流水里异常高频地作为浏览者出现。工具没变SQL也没变但角度一变它就从一道面试题变成了一个能用在实际业务里的数据巡检小脚本。这也是我特别推荐每一位刷题的同学做的事把一道题还原成一个分析场景你会发现它的价值远不是AC一个用例那么简单。

相关新闻

易语言乐玩插件多线程索引方案:告别串线崩溃,稳定并发找图

易语言乐玩插件多线程索引方案:告别串线崩溃,稳定并发找图

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 5:03:07 阅读更多 →
Apollo-11源码解析:嵌入式高可靠系统设计启示

Apollo-11源码解析:嵌入式高可靠系统设计启示

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 5:03:07 阅读更多 →
Android传统蓝牙聊天室开发实战:RFCOMM连接与权限适配

Android传统蓝牙聊天室开发实战:RFCOMM连接与权限适配

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 5:03:07 阅读更多 →

最新新闻

I2C、SMBus与PMBus协议本质区别及实战避坑指南

I2C、SMBus与PMBus协议本质区别及实战避坑指南

简介:本资源是一份面向嵌入式硬件工程师、电源系统设计人员及电子通信专业学习者的总线协议深度解析资料,聚焦I2C、SMBus与PMBus三大工业级串行通信协议的原理差异、应用场景与实现要点。内容系统梳理I2C基础架构(含SDA/SCL时序、7/10位寻址、…

2026/10/15 5:47:36 阅读更多 →
UDP组播跨网段搜设备IP实战指南

UDP组播跨网段搜设备IP实战指南

1. 项目概述:为什么“UDP组播改网段搜设备IP”是现场工程师的高频痛点你有没有遇到过这样的场景:手提一台新配的工业相机,连上现场交换机,打开配套软件——设备列表空空如也;换台笔记本重试,还是找不到&…

2026/10/15 5:47:36 阅读更多 →
单卡显存不足如何跑大模型?airllm分层加载推理实战

单卡显存不足如何跑大模型?airllm分层加载推理实战

1. 单卡跑大模型这件事,到底卡在哪第一次看到 airllm 这个项目名的时候,我正被显存问题折磨得够呛。手头只有一张 24GB 显存的卡,想跑一个 70B 参数量的模型,按常规思路算一下就知道不可能——光是权重加载,FP16 精度下…

2026/10/15 5:47:36 阅读更多 →
碳信用流动性如何影响全球股市估值?三条传导链路拆解

碳信用流动性如何影响全球股市估值?三条传导链路拆解

1. 先搞清楚两个市场在说什么这几年做全球资产配置的朋友,估计都有一个直观感受:ESG、碳中和、碳关税这些词,已经从“概念讨论”变成了“交易现实”。但真正把碳信用交易市场和股市估值放在一起研究的人,其实不算多。大部分时候&a…

2026/10/15 5:47:36 阅读更多 →
用WinForms+Vulkan集成APISR,打造老动画视频超分修复工具

用WinForms+Vulkan集成APISR,打造老动画视频超分修复工具

把老动画“洗”清楚这件事,我已经认认真真做了好几周。我最早做这个小工具,纯粹是想把小时候收藏的480p老动画,用本地电脑放到4K电视上给孩子看。折腾下来发现,传统播放器的拉伸画面一团糊,直接上超分模型又有一堆工程…

2026/10/15 5:47:36 阅读更多 →
三款开源AI效率工具实测:会议转写、知识库问答与批量处理,每天省2小时

三款开源AI效率工具实测:会议转写、知识库问答与批量处理,每天省2小时

先说结论:最近我一直在折腾三款开源的AI效率工具,加起来装一次,就能让每天重复劳动的时间少掉一大截。你说“省下2小时”是标题党?我自己也不信,但连续用了两周之后,我翻了翻时间记录,确实每天多…

2026/10/15 5:46:36 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →