尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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一个用例那么简单。
RELATED

相关推荐

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

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

/* 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:02:55
飞火动态壁纸从下载到配置:全程避坑实操指南

飞火动态壁纸从下载到配置:全程避坑实操指南

1. 从零开始:飞火动态壁纸到底能做什么说实话,我最早接触飞火动态壁纸的时候,心里是有疑问的——不就是个壁纸软件吗?能玩出什么花来?直到我真正装完、配好之后,才发现这事比我想象的要复杂,也远…

📅 2026/10/15 4:57:55
Keras路透社数据集多分类实战:从二分类进阶到46类新闻主题分类

Keras路透社数据集多分类实战:从二分类进阶到46类新闻主题分类

1. 从二分类跨到多分类:路透社数据集到底特殊在哪很多人跟着教程走完IMDB二分类,觉得自己已经摸到了Keras处理文本的门道,结果一切换到路透社数据集就卡住了——标签不再是0和1,而是46个互斥的类别,输出层的维度、损失…

📅 2026/10/15 4:57:55
MORE NEWS

更多资讯

📰

PyTorch核心设计原理与工业级训练实践

1. 这不是“学个框架”那么简单:PyTorch到底在解决什么真实问题?很多人第一次听说PyTorch,是在某次技术分享会上听到“动态图好调试”“API设计更Pythonic”这类评价;也有人是从Kaggle竞赛排行榜上看到大量Top方案的代码仓库里&am…

📰

从零搭建大学生就业信息管理系统:需求、设计到实现

1. 这个系统解决什么问题:需求拆解与功能边界大学生就业信息管理系统我接触得比较多,每年都会看到不少计算机相关专业的同学拿它当毕设选题。第一反应往往是“这不就是一个信息管理的增删改查吗”,但真正动手做起来才会发现,角色权…

📰

TensorFlow全流程实战:从环境搭建到模型部署的完整链路

这不是一篇入门教程,也不是API文档搬运。从我第一次在项目里跑通TensorFlow,到现在能用它顺畅地完成从数据准备到模型部署的全流程,中间绕了很远的路,也踩了不少坑。这篇文章把你真正会用到的那些东西——环境、数据、模型、训练、…

📰

airllm:低显存机器跑超大语言模型的实践指南

实际测试完成的部署方案:如何在低内存机器上跑超大容量语言模型最近 AI 本地部署群里讨论最多的一个话题,不是新模型的评测,而是“我到底要多少显存才能跑得动这些模型”。动不动几十GB的权重,让很多人的消费级电脑直接劝退。如果…

📰

RAR归档文件解压全指南:从格式识别到批量处理与避坑

简介:GC-PowerStation 2019中文版是面向表面贴装工艺工程师的坐标导出与光绘文件处理工具,支持导入电子表格、文本等多种坐标文件格式,并完成多层光绘文件的预览、合并与校验,帮助电子制造从业者在生产前检查设计准确性&#xff0…

📰

易语言乐玩插件窗口句柄获取与后台绑定模式实战指南

1. 窗口句柄获取与后台绑定模式的核心逻辑拆解1.1 为什么窗口句柄是一切自动化操作的起点做桌面自动化或者辅助工具开发,绕不开的第一个坎就是拿到目标窗口的句柄。你可以把窗口句柄理解成操作系统给每个窗口发的一张身份证号,它是一个长整型数值&#x…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬