本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
大厂 SQL 面试真题(字节 / 阿里 / 拼多多 / 滴滴 / 小红书 / Meta / Amazon) · 海外大厂 / 数据科学家职位

Meta 面试:找出过去 30 天点赞数 > 评论数 5 倍的帖子。

Meta interview: Find posts from the past 30 days where the like count is 5 times greater

答案语言

考察要点

这道题面试官想看你是否懂:

  1. 复杂查询构建能力:能否将一个业务需求(过去30天、点赞数、评论数、5倍关系)拆解成 JOIN、WHERE、GROUP BY 和 HAVING 等 SQL 子句。
  2. 聚合函数与条件聚合:是否熟练使用 COUNT 进行计数,并能运用 CASE WHEN 在一次聚合中完成多种条件的统计,这是衡量 SQL 熟练度的关键。
  3. SQL 执行效率意识:能否意识到不同写法(如子查询 vs 条件聚合)的性能差异,并选择最优方案。这是从“能用”到“好用”的进阶,是大厂非常看重的能力。

核心概念

在解决本题前,我们需要明确几个核心的 SQL 概念,并设计一个合理的数据表结构。

假设表结构 在 Meta 这种规模的公司,用户的行为(点赞、评论)通常会记录在一张统一的事件表中。

  1. 帖子表 (posts)
    sql
    1CREATE TABLE posts (
    2 post_id BIGINT PRIMARY KEY,
    3 user_id BIGINT,
    4 content TEXT,
    5 creation_date DATETIME NOT NULL
    6);
  2. 帖子行为表 (post_actions)
    sql
    1CREATE 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 NULL
    7);

关键概念说明

  • LEFT JOIN LEFT JOIN 会返回左表的所有行,即使在右表中没有匹配的行。这对于本题至关重要,因为一个帖子可能有点赞但没有评论,反之亦然。如果使用 INNER JOIN,会错误地过滤掉这些帖子。

  • WHERE vs HAVING 这是 SQL 面试的经典考点。

    • WHERE 子句:在数据分组(GROUP BY)之前进行过滤。它作用于表中的原始行。
    • HAVING 子句:在数据分组(GROUP BY)之后进行过滤。它作用于 GROUP BY 生成的聚合结果。 在本题中,“过去30天”这个条件应该作用于原始行,放在 WHERE 中以尽早减少数据量;而“点赞数 > 5 * 评论数”这个条件作用于聚合后的计数值,必须放在 HAVING 中。
  • 条件聚合 (Conditional Aggregation) 这是一种高效的聚合模式,通过在 SUM 或 COUNT 函数内部使用 CASE WHEN 表达式,可以在一次表扫描和一次 GROUP BY 中完成对多个条件的分别统计。

    sql
    1-- 统计 '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())

多种解法

解法 1:使用子查询分别聚合再 JOIN (直观但低效)

这种思路最符合人的直觉:先分别计算点赞数和评论数,然后将结果合并。

  • 版本:✅ 所有 SQL 数据库版本通用。
  • SQL 代码:
    sql
    1SELECT
    2 p.post_id
    3FROM
    4 posts p
    5LEFT JOIN
    6 (SELECT post_id, COUNT(*) AS like_count
    7 FROM post_actions
    8 WHERE action_type = 'like' AND creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
    9 GROUP BY post_id) AS likes
    10ON
    11 p.post_id = likes.post_id
    12LEFT JOIN
    13 (SELECT post_id, COUNT(*) AS comment_count
    14 FROM post_actions
    15 WHERE action_type = 'comment' AND creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
    16 GROUP BY post_id) AS comments
    17ON
    18 p.post_id = comments.post_id
    19WHERE
    20 p.creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
    21AND
    22 COALESCE(likes.like_count, 0) > 5 * COALESCE(comments.comment_count, 0);
  • 时间/空间复杂度:
    • 时间复杂度:较高。post_actions 表被扫描了两次,如果该表很大,开销会非常大。复杂度约 O(Nactions+Nactions+Nposts)O(N_{actions} + N_{actions} + N_{posts}),其中 NN 是行数。
    • 空间复杂度:O(Nposts)O(N_{posts}),需要为两个子查询的结果集和最终的 JOIN 分配内存。
  • 索引依赖:
    • posts(creation_date)
    • post_actions(action_type, creation_date, post_id) (复合索引,顺序很重要)

解法 2:使用条件聚合 (高效推荐)

这是最优解。只需扫描一次 post_actions 表,在 GROUP BY 的过程中同时计算出点赞和评论数。

  • 版本:✅ 所有现代 SQL 数据库版本通用 (MySQL, PostgreSQL, SQL Server, Oracle 等)。
  • SQL 代码:
    sql
    1SELECT
    2 p.post_id
    3FROM
    4 posts p
    5LEFT JOIN
    6 post_actions pa ON p.post_id = pa.post_id
    7 -- 确保 action 也在 30 天内,如果业务需要的话
    8 AND pa.creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
    9WHERE
    10 -- 筛选过去 30 天创建的帖子
    11 p.creation_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
    12GROUP BY
    13 p.post_id
    14HAVING
    15 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 表都只被扫描一次。复杂度约 O(Nposts+Nactions)O(N_{posts} + N_{actions})。
    • 空间复杂度:O(Nposts)O(N_{posts}),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 并解释其性能优势是获得高分的关键。

常见低分回答

  1. 在 WHERE 子句中使用聚合函数:这是最常见的语法错误,表明对 SQL 执行顺序理解不清。
    sql
    1-- 错误示范
    2SELECT post_id, COUNT(*)
    3FROM post_actions
    4WHERE COUNT(*) > 10 -- 错误!WHERE 不能用聚合函数
    5GROUP BY post_id;
  2. 使用 INNER JOIN 代替 LEFT JOIN:这会导致逻辑错误。如果一个帖子只有点赞没有评论,INNER JOIN 会直接将其过滤掉,不符合题意。
  3. 忘记处理 NULL 值:在使用子查询解法时,如果一个帖子没有评论,comments.comment_count 会是 NULL。like_count > 5 * NULL 的结果是 NULL(不为 TRUE),导致该行被过滤。必须使用 COALESCE(column, 0) 或 IFNULL(column, 0) 将 NULL 转换成 0。
  4. 对 COUNT 函数的误用:COUNT(column) 不会统计 NULL 值,而 COUNT(*) 会统计所有行。在本题的条件聚合解法中,SUM(CASE...END) 比 COUNT(CASE...END) 更健壮,因为 COUNT 的 ELSE 分支如果是 NULL(默认),该行不会被计数,而 SUM 的 ELSE 0 则能正确处理。
  5. 日期过滤条件不清晰:题目“过去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 无法利用索引,需要昂贵的额外操作。
  • 数据量级问题:在 Meta 的规模下,post_actions 表每天可能有数十亿甚至上百亿的新增记录。直接在生产 OLTP 数据库上运行这种聚合查询是绝对禁止的,因为它会锁住资源,影响线上服务。
    • 解决方案:这类分析查询应该在数据仓库(如 Hive, Spark SQL, BigQuery, Snowflake)或读写分离的从库上执行。
  • 预聚合/物化视图:对于频繁查询的统计指标,最佳实践是采用预聚合。
    • 创建一个摘要表 (Summary Table),例如 post_stats_daily。
    • 通过离线批处理任务(如每日运行的 Spark Job)计算每个帖子每天的点赞数、评论数等,并写入此表。
    • 这样,原始查询就变成了一个对摘要表的简单 SELECT,响应时间可以从几分钟降到毫秒级。

常见追问与延伸题目

  1. 追问:如果数据量增长 100 倍,这条 SQL 性能怎样?怎么优化?

    • 回答要点 1 (瓶颈分析):即使是最高效的条件聚合解法(解法 2),其性能也与 post_actions 表的大小成线性关系 (O(N)O(N))。数据量增长 100 倍,查询时间也会大致增长 100 倍,从秒级变成小时级,无法接受。瓶颈在于对海量数据的实时聚合计算。
    • 回答要点 2 (架构级优化):必须放弃实时计算,转向预计算(Pre-computation)架构。我会设计一个数据管道:
      1. 数据抽取 (ETL/ELT):将生产库的 post_actions 数据增量同步到数据仓库(如 Hive/BigQuery)。
      2. 批处理计算:编写一个每日或每小时运行的 Spark/MapReduce 任务,来聚合 post_actions 数据,生成一个 post_summary 表,包含 post_id, date, likes_count, comments_count。
      3. 查询服务:最终的查询直接作用于这个小得多的 post_summary 表,性能极高,可以满足 BI 报表或线上产品的需求。
  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 来强制指定。但这通常是最后的手段。
  3. 追问:如果换成分布式数据库(如 TiDB)或云数据仓库(如 Snowflake),答案有什么不同?

    • 回答要点 1 (SQL 逻辑不变):从 SQL 语法层面,最优解法(条件聚合)依然是标准且最高效的,不需要改变。
    • 回答要点 2 (执行模型质变):最大的不同在于执行模型。这些系统是分布式的,查询会被拆解成多个子任务,在集群的多个计算节点上并行执行。JOIN 和 GROUP BY 会触发数据在节点间的 shuffle(洗牌)。
    • 回答要点 3 (新的挑战:数据倾斜):在分布式环境下,最需要关注的是数据倾斜 (Data Skew)。如果某个“网红”帖子的 post_id 拥有几十亿的 actions,那么处理这个 post_id 的单个节点就会成为整个查询的瓶颈。解决方案包括:
      • 两阶段聚合:先在 GROUP BY key 中加入一个随机 salt (GROUP BY post_id, RAND() % 10),进行第一轮局部聚合,打散热点数据。然后再进行第二轮全局聚合,合并结果。
      • 利用数据库特性:某些数据仓库(如 Snowflake)的执行引擎对数据倾斜有专门的优化,可以自动检测并处理。在 TiDB 中,可以调整 TIDB_INLJ 或 TIDB_SMJ 等 hint 来影响 JOIN 策略。