Meta 面试:找出过去 30 天点赞数 > 评论数 5 倍的帖子。
Meta interview: Find posts from the past 30 days where the like count is 5 times greater
考察要点
这道题面试官想看你是否懂:
- 复杂查询构建能力:能否将一个业务需求(过去30天、点赞数、评论数、5倍关系)拆解成
JOIN、WHERE、GROUP BY和HAVING等 SQL 子句。 - 聚合函数与条件聚合:是否熟练使用
COUNT进行计数,并能运用CASE WHEN在一次聚合中完成多种条件的统计,这是衡量 SQL 熟练度的关键。 - SQL 执行效率意识:能否意识到不同写法(如子查询 vs 条件聚合)的性能差异,并选择最优方案。这是从“能用”到“好用”的进阶,是大厂非常看重的能力。
核心概念
在解决本题前,我们需要明确几个核心的 SQL 概念,并设计一个合理的数据表结构。
假设表结构 在 Meta 这种规模的公司,用户的行为(点赞、评论)通常会记录在一张统一的事件表中。
- 帖子表 (
posts)sql1CREATE TABLE posts (2 post_id BIGINT PRIMARY KEY,3 user_id BIGINT,4 content TEXT,5 creation_date DATETIME NOT NULL6); - 帖子行为表 (
post_actions)sql1CREATE TABLE post_actions (2 action_id BIGINT PRIMARY KEY,3 post_id BIGINT NOT NULL,4 user_id BIGINT NOT NULL,5 action_type ENUM('like', 'comment') NOT NULL,6 creation_date DATETIME NOT NULL7);
关键概念说明
-
LEFT JOINLEFT JOIN会返回左表的所有行,即使在右表中没有匹配的行。这对于本题至关重要,因为一个帖子可能有点赞但没有评论,反之亦然。如果使用INNER JOIN,会错误地过滤掉这些帖子。 -
WHEREvsHAVING这是 SQL 面试的经典考点。WHERE子句:在数据分组(GROUP BY)之前进行过滤。它作用于表中的原始行。HAVING子句:在数据分组(GROUP BY)之后进行过滤。它作用于GROUP BY生成的聚合结果。 在本题中,“过去30天”这个条件应该作用于原始行,放在WHERE中以尽早减少数据量;而“点赞数 > 5 * 评论数”这个条件作用于聚合后的计数值,必须放在HAVING中。
-
条件聚合 (Conditional Aggregation) 这是一种高效的聚合模式,通过在
SUM或COUNT函数内部使用CASE WHEN表达式,可以在一次表扫描和一次GROUP BY中完成对多个条件的分别统计。sql1-- 统计 'like' 和 'comment' 的数量2SUM(CASE WHEN action_type = 'like' THEN 1 ELSE 0 END) AS like_count,3SUM(CASE WHEN action_type = 'comment' THEN 1 ELSE 0 END) AS comment_count这种方式远优于多次扫描或多个子查询。
-
日期函数 用于筛选“过去30天”的数据。不同数据库语法稍有差异:
- MySQL:
creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - PostgreSQL:
creation_date >= NOW() - INTERVAL '30 days' - SQL Server:
creation_date >= DATEADD(day, -30, GETDATE())
- MySQL:
多种解法
解法 1:使用子查询分别聚合再 JOIN (直观但低效)
这种思路最符合人的直觉:先分别计算点赞数和评论数,然后将结果合并。
- 版本:✅ 所有 SQL 数据库版本通用。
- SQL 代码:
sql1SELECT2 p.post_id3FROM4 posts p5LEFT JOIN6 (SELECT post_id, COUNT(*) AS like_count7 FROM post_actions8 WHERE action_type = 'like' AND creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)9 GROUP BY post_id) AS likes10ON11 p.post_id = likes.post_id12LEFT JOIN13 (SELECT post_id, COUNT(*) AS comment_count14 FROM post_actions15 WHERE action_type = 'comment' AND creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)16 GROUP BY post_id) AS comments17ON18 p.post_id = comments.post_id19WHERE20 p.creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)21AND22 COALESCE(likes.like_count, 0) > 5 * COALESCE(comments.comment_count, 0);
- 时间/空间复杂度:
- 时间复杂度:较高。
post_actions表被扫描了两次,如果该表很大,开销会非常大。复杂度约 ,其中 是行数。 - 空间复杂度:,需要为两个子查询的结果集和最终的
JOIN分配内存。
- 时间复杂度:较高。
- 索引依赖:
posts(creation_date)post_actions(action_type, creation_date, post_id)(复合索引,顺序很重要)
解法 2:使用条件聚合 (高效推荐)
这是最优解。只需扫描一次 post_actions 表,在 GROUP BY 的过程中同时计算出点赞和评论数。
- 版本:✅ 所有现代 SQL 数据库版本通用 (MySQL, PostgreSQL, SQL Server, Oracle 等)。
- SQL 代码:
sql1SELECT2 p.post_id3FROM4 posts p5LEFT JOIN6 post_actions pa ON p.post_id = pa.post_id7 -- 确保 action 也在 30 天内,如果业务需要的话8 AND pa.creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)9WHERE10 -- 筛选过去 30 天创建的帖子11 p.creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)12GROUP BY13 p.post_id14HAVING15 SUM(CASE WHEN pa.action_type = 'like' THEN 1 ELSE 0 END) >16 5 * SUM(CASE WHEN pa.action_type = 'comment' THEN 1 ELSE 0 END);
- 时间/空间复杂度:
- 时间复杂度:优秀。
posts和post_actions表都只被扫描一次。复杂度约 。 - 空间复杂度:,
GROUP BY操作需要内存来存储每个post_id的中间聚合结果。
- 时间复杂度:优秀。
- 索引依赖:
posts(creation_date, post_id)post_actions(post_id, creation_date, action_type)(这个索引顺序对JOIN和GROUP BY更有利)
解法对比:解法 2 性能远超解法 1,因为它避免了对 post_actions 这个潜在巨型表的多次扫描。在面试中,能写出解法 2 并解释其性能优势是获得高分的关键。
常见低分回答
- 在
WHERE子句中使用聚合函数:这是最常见的语法错误,表明对 SQL 执行顺序理解不清。sql1-- 错误示范2SELECT post_id, COUNT(*)3FROM post_actions4WHERE COUNT(*) > 10 -- 错误!WHERE 不能用聚合函数5GROUP BY post_id; - 使用
INNER JOIN代替LEFT JOIN:这会导致逻辑错误。如果一个帖子只有点赞没有评论,INNER JOIN会直接将其过滤掉,不符合题意。 - 忘记处理
NULL值:在使用子查询解法时,如果一个帖子没有评论,comments.comment_count会是NULL。like_count > 5 * NULL的结果是NULL(不为TRUE),导致该行被过滤。必须使用COALESCE(column, 0)或IFNULL(column, 0)将NULL转换成 0。 - 对
COUNT函数的误用:COUNT(column)不会统计NULL值,而COUNT(*)会统计所有行。在本题的条件聚合解法中,SUM(CASE...END)比COUNT(CASE...END)更健壮,因为COUNT的ELSE分支如果是NULL(默认),该行不会被计数,而SUM的ELSE 0则能正确处理。 - 日期过滤条件不清晰:题目“过去30天”有歧义。是指“过去30天创建的帖子”,还是指“在过去30天内有活动的帖子”?面试时应主动向面试官澄清。上述解法假设为前者,并在
WHERE子句中明确p.creation_date的范围。
工程实践与优化
- 索引是性能的生命线:
posts表必须在creation_date上建立索引,用于快速筛选时间范围。post_actions表应建立(post_id, action_type, creation_date)的复合索引。post_id在前,可以极大地加速JOIN和GROUP BY操作。
- 执行计划分析 (
EXPLAIN):- 对于解法 2,一个好的执行计划应该显示:
posts表使用creation_date索引进行range扫描;post_actions表使用post_id索引进行ref关联。Extra字段不应出现 "Using filesort" 或 "Using temporary",这通常意味着GROUP BY无法利用索引,需要昂贵的额外操作。
- 对于解法 2,一个好的执行计划应该显示:
- 数据量级问题:在 Meta 的规模下,
post_actions表每天可能有数十亿甚至上百亿的新增记录。直接在生产OLTP数据库上运行这种聚合查询是绝对禁止的,因为它会锁住资源,影响线上服务。- 解决方案:这类分析查询应该在数据仓库(如 Hive, Spark SQL, BigQuery, Snowflake)或读写分离的从库上执行。
- 预聚合/物化视图:对于频繁查询的统计指标,最佳实践是采用预聚合。
- 创建一个摘要表 (Summary Table),例如
post_stats_daily。 - 通过离线批处理任务(如每日运行的 Spark Job)计算每个帖子每天的点赞数、评论数等,并写入此表。
- 这样,原始查询就变成了一个对摘要表的简单
SELECT,响应时间可以从几分钟降到毫秒级。
- 创建一个摘要表 (Summary Table),例如
常见追问与延伸题目
-
追问:如果数据量增长 100 倍,这条 SQL 性能怎样?怎么优化?
- 回答要点 1 (瓶颈分析):即使是最高效的条件聚合解法(解法 2),其性能也与
post_actions表的大小成线性关系 ()。数据量增长 100 倍,查询时间也会大致增长 100 倍,从秒级变成小时级,无法接受。瓶颈在于对海量数据的实时聚合计算。 - 回答要点 2 (架构级优化):必须放弃实时计算,转向预计算(Pre-computation)架构。我会设计一个数据管道:
- 数据抽取 (ETL/ELT):将生产库的
post_actions数据增量同步到数据仓库(如 Hive/BigQuery)。 - 批处理计算:编写一个每日或每小时运行的 Spark/MapReduce 任务,来聚合
post_actions数据,生成一个post_summary表,包含post_id,date,likes_count,comments_count。 - 查询服务:最终的查询直接作用于这个小得多的
post_summary表,性能极高,可以满足 BI 报表或线上产品的需求。
- 数据抽取 (ETL/ELT):将生产库的
- 回答要点 1 (瓶颈分析):即使是最高效的条件聚合解法(解法 2),其性能也与
-
追问:如果发现这条 SQL 跑得很慢,你如何定位和修复问题?
- 回答要点 1 (定位):我会使用
EXPLAIN ANALYZE <query>(在 MySQL 8.0+ 或 PostgreSQL 中)来获取详细的执行计划和实际执行信息。我会重点关注:type列:是否为ALL(全表扫描),理想情况应为ref,eq_ref或range。key列:是否正确使用了我期望的索引。如果是NULL,说明索引未生效。rows列:扫描的行数是否过大。如果估算值与实际值差距悬殊,可能需要运行ANALYZE TABLE更新统计信息。Extra列:是否出现了Using temporary或Using filesort,这些是严重的性能杀手。
- 回答要点 2 (修复):根据
EXPLAIN的结果进行修复:- 索引未生效:检查
WHERE子句中的列是否有函数包裹(如DATE_FORMAT(creation_date, ...)),或者JOIN的字段类型是否匹配。 - 无合适索引:根据查询的
JOIN,WHERE,GROUP BY子句,创建或调整复合索引。例如,post_actions(post_id, creation_date)。 - 强制使用索引:在极少数情况下,如果优化器选择了错误的索引,可以使用
FORCE INDEX来强制指定。但这通常是最后的手段。
- 索引未生效:检查
- 回答要点 1 (定位):我会使用
-
追问:如果换成分布式数据库(如 TiDB)或云数据仓库(如 Snowflake),答案有什么不同?
- 回答要点 1 (SQL 逻辑不变):从 SQL 语法层面,最优解法(条件聚合)依然是标准且最高效的,不需要改变。
- 回答要点 2 (执行模型质变):最大的不同在于执行模型。这些系统是分布式的,查询会被拆解成多个子任务,在集群的多个计算节点上并行执行。
JOIN和GROUP BY会触发数据在节点间的 shuffle(洗牌)。 - 回答要点 3 (新的挑战:数据倾斜):在分布式环境下,最需要关注的是数据倾斜 (Data Skew)。如果某个“网红”帖子的
post_id拥有几十亿的actions,那么处理这个post_id的单个节点就会成为整个查询的瓶颈。解决方案包括:- 两阶段聚合:先在
GROUP BYkey 中加入一个随机 salt (GROUP BY post_id, RAND() % 10),进行第一轮局部聚合,打散热点数据。然后再进行第二轮全局聚合,合并结果。 - 利用数据库特性:某些数据仓库(如 Snowflake)的执行引擎对数据倾斜有专门的优化,可以自动检测并处理。在 TiDB 中,可以调整
TIDB_INLJ或TIDB_SMJ等 hint 来影响JOIN策略。
- 两阶段聚合:先在