ARTICLE DETAIL

资讯详情

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

小红书数据库面试题:连续登录忠实粉丝查询实现

小红书数据库面试题:连续登录忠实粉丝查询实现 1. 题目背景与核心考察点这道数据库题目出现在2026年小红书春季招聘的笔试环节作为3月25日场次的第一道编程题。从企业招聘的角度来看首题通常设置为基础能力筛查关卡主要考察候选人对数据库基础操作的熟练程度和算法实现能力。根据互联网大厂的出题规律此类题目往往具有以下特征场景来源于真实业务简化如用户关系、内容管理、商品库存等需要组合运用SQL知识和编程语言实现包含1-2个需要特别注意的边界条件时间复杂度要求通常为O(nlogn)以内2. 题目还原与需求分析2.1 原始题目描述题目给出一个用户关注关系的数据库表结构CREATE TABLE user_relations ( user_id INT, follower_id INT, follow_date DATE, PRIMARY KEY (user_id, follower_id) );要求实现一个查询找出每个用户的忠实粉丝定义为连续30天以上每天都有登录且关注该用户的粉丝最后返回用户ID和其忠实粉丝数量按用户ID升序排列。2.2 核心解题思路拆解数据理解阶段需要关联用户登录日志题目隐含和关注关系表连续登录判定是核心难点忠实粉丝的定义包含时间连续性要求技术实现路径方案A使用窗口函数计算连续登录天数方案B通过自连接实现连续性检测方案C应用日期差值法识别连续区间最优方案选择窗口函数方案代码简洁但需要数据库支持自连接方案通用性强但性能较差差值法在大多数场景下综合表现最佳3. 多语言实现方案3.1 Java实现企业级方案public ListMap.EntryInteger, Integer findLoyalFollowers(Connection conn) throws SQLException { String sql WITH daily_active AS ( SELECT DISTINCT user_id, follower_id, login_date FROM user_relations ur JOIN user_logins ul ON ur.follower_id ul.user_id WHERE ul.login_date BETWEEN ur.follow_date AND CURRENT_DATE ), consecutive_groups AS ( SELECT user_id, follower_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id, follower_id ORDER BY login_date) DAY) AS grp FROM daily_active ) SELECT user_id, COUNT(DISTINCT follower_id) AS loyal_count FROM ( SELECT user_id, follower_id FROM consecutive_groups GROUP BY user_id, follower_id, grp HAVING COUNT(*) 30 ) t GROUP BY user_id ORDER BY user_id; try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { ListMap.EntryInteger, Integer result new ArrayList(); while (rs.next()) { result.add(Map.entry( rs.getInt(user_id), rs.getInt(loyal_count) )); } return result; } }关键点说明使用CTE和窗口函数创建临时结果集通过日期差值法识别连续登录区间最后统计符合条件的忠实粉丝数。3.2 Python实现快速原型方案import sqlite3 from collections import defaultdict def find_loyal_followers(db_path): conn sqlite3.connect(db_path) cursor conn.cursor() # 获取所有用户-粉丝的活跃日期对 cursor.execute( SELECT DISTINCT ur.user_id, ur.follower_id, ul.login_date FROM user_relations ur JOIN user_logins ul ON ur.follower_id ul.user_id WHERE ul.login_date BETWEEN ur.follow_date AND date(now) ORDER BY ur.user_id, ur.follower_id, ul.login_date ) user_follower_dates defaultdict(lambda: defaultdict(list)) for user_id, follower_id, login_date in cursor.fetchall(): user_follower_dates[user_id][follower_id].append(login_date) result [] for user_id in sorted(user_follower_dates.keys()): loyal_count 0 for follower_id, dates in user_follower_dates[user_id].items(): if has_consecutive_days(dates, 30): loyal_count 1 result.append((user_id, loyal_count)) conn.close() return result def has_consecutive_days(dates, threshold): dates sorted(set(dates)) # 去重并排序 max_streak current_streak 1 for i in range(1, len(dates)): if (dates[i] - dates[i-1]).days 1: current_streak 1 max_streak max(max_streak, current_streak) else: current_streak 1 return max_streak threshold实现特点在内存中处理日期连续性判断适合数据量不大的场景代码可读性更强。3.3 C实现高性能方案#include vector #include string #include map #include set #include algorithm #include mysql_driver.h #include mysql_connection.h #include cppconn/statement.h #include cppconn/resultset.h using namespace std; struct Date { int year, month, day; bool operator(const Date other) const { return tie(year, month, day) tie(other.year, other.month, other.day); } }; vectorpairint, int findLoyalFollowers(sql::Connection* conn) { vectorpairint, int result; mapint, mapint, setDate userData; unique_ptrsql::Statement stmt(conn-createStatement()); unique_ptrsql::ResultSet res(stmt-executeQuery( SELECT DISTINCT ur.user_id, ur.follower_id, ul.login_date FROM user_relations ur JOIN user_logins ul ON ur.follower_id ul.user_id WHERE ul.login_date BETWEEN ur.follow_date AND CURDATE() )); while (res-next()) { int userId res-getInt(user_id); int followerId res-getInt(follower_id); string dateStr res-getString(login_date); Date d; sscanf(dateStr.c_str(), %d-%d-%d, d.year, d.month, d.day); userData[userId][followerId].insert(d); } for (auto [userId, followers] : userData) { int loyalCount 0; for (auto [followerId, dates] : followers) { vectorDate sortedDates(dates.begin(), dates.end()); int maxStreak 1, current 1; for (size_t i 1; i sortedDates.size(); i) { time_t t1 mktime(tm{0, 0, 0, sortedDates[i-1].day, sortedDates[i-1].month-1, sortedDates[i-1].year-1900}); time_t t2 mktime(tm{0, 0, 0, sortedDates[i].day, sortedDates[i].month-1, sortedDates[i].year-1900}); if (difftime(t2, t1) 86400) { // 86400秒1天 if (current maxStreak) maxStreak current; } else { current 1; } } if (maxStreak 30) loyalCount; } result.emplace_back(userId, loyalCount); } sort(result.begin(), result.end()); return result; }性能优化点使用原生C数据结构处理数据避免多次数据库交互适合海量数据处理。4. 测试用例设计与验证4.1 标准测试用例-- 准备测试数据 INSERT INTO user_relations VALUES (1, 101, 2026-01-01), (1, 102, 2026-01-15), (2, 101, 2026-02-01); -- 用户登录数据假设user_logins表存在 -- 用户1011月1日-2月1日连续32天登录 -- 用户1021月15日-1月30日连续16天登录预期输出user_id | loyal_count -------------------- 1 | 1 # 只有101是忠实粉丝 2 | 0 # 101虽然连续登录但不够30天4.2 边界条件测试跨年连续登录粉丝在12月20日到次年1月20日连续登录验证日期计算逻辑是否正确处理年份变更闰年二月情况包含2月29日的连续登录序列验证日期差值计算是否准确正好30天边界从第1天到第30天严格连续验证等号条件是否正确处理5. 性能优化建议5.1 数据库层面优化索引策略CREATE INDEX idx_follower_login ON user_logins(user_id, login_date); CREATE INDEX idx_relations ON user_relations(user_id, follower_id, follow_date);查询优化对于超大规模数据可以按用户ID分片处理考虑使用物化视图预计算部分结果5.2 算法优化方向滑动窗口法维护一个30天的时间窗口统计窗口内活跃天数窗口滑动时增量更新计数位图压缩法将每日登录状态用bit表示使用位运算检测连续30个1的模式适合内存充足且需要极致性能的场景6. 实际业务场景扩展6.1 小红书的应用场景该算法可以扩展应用于识别高价值用户持续活跃的粉丝内容推荐权重计算忠实粉丝的互动加权创作者激励计划资格判定6.2 类似业务场景电商领域连续购买用户的识别会员等级晋升条件判定社交平台亲密好友关系发现社群核心成员识别SaaS产品高留存客户分析付费转化预测7. 常见问题解决方案7.1 时区问题处理-- 在查询时统一转换为UTC时间 CONVERT_TZ(login_date, session.time_zone, 00:00)7.2 大数据量分页查询// 使用游标分批处理 stmt.setFetchSize(1000);7.3 日期格式不一致# 统一格式化日期 from datetime import datetime datetime.strptime(date_str, %Y-%m-%d).date()8. 不同数据库的适配方案8.1 MySQL特定优化-- 利用MySQL的日期计算函数 SELECT DATE_ADD(login_date, INTERVAL -1 DAY)8.2 PostgreSQL方案-- 使用PostgreSQL特有的范围类型 WHERE login_date daterange(start_date, end_date)8.3 分布式数据库方案Hive实现-- 使用LAG窗口函数 LAG(login_date, 1) OVER (PARTITION BY user_id, follower_id ORDER BY login_date)Spark SQL优化df.groupBy(user_id, follower_id).agg( countWhen(datediff(login_date, lag(login_date)) 1).alias(streak) )
返回列表