SQL 面试系列(一)【留存率问题】

简介: SQL 面试系列(一)【留存率问题】

前言

       在学 HQL 之前是不太了解 SQL 的,以为 SQL 只可以实现 CRUD ,直到面试的公司让我下去多了解一些 SQL ,我才最近开始再次深入学习 MySQL 和 Oracle。而且越学越发现 SQL 真的是一门很有深度的语言,我以前的使用只是皮毛而已,自惭形秽之下决定必须深入学习 SQL ,毕竟大数据工作之后主要就是 SQL 嘛。

       关于留存率问题是一个面试的重点,面一个暑期实习都几乎每次都被问,每次问到留存率我都试图通过障耳法搞晕面试官,结果事实上面试官都能准确发现我回答中的问题。所以不能再拖了,留存率的问题应该好好拿出来深入理解掌握了。

1、留存率问题

1.1、留存率的定义

留存率:用户在注册之后的第 N 天仍然登录的比例,称为第 N 日留存率。

1.2、数据准备

下面我们通过 SQL 先创建一些数据(用户数据 5000条、登录数据 50w条):

-- 留存率问题
-- 示例表
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user(
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_name VARCHAR(50) NOT NULL,
  register_time DATETIME NOT NULL
);
 
SET SESSION cte_max_recursion_depth=9999999;
 
INSERT INTO t_user(user_name, register_time)
WITH RECURSIVE t AS (
  SELECT 1 n, '2022-01-01 00:00:00' d
  UNION ALL
  SELECT n+1, d + INTERVAL '1' MINUTE
  FROM t
  WHERE n<10000
)
SELECT concat('user', n), d FROM t;
 
SELECT date(register_time), count(*) FROM t_user GROUP BY date(register_time);
 
DROP TABLE IF EXISTS t_user_login;
CREATE TABLE t_user_login(
  id INT AUTO_INCREMENT PRIMARY KEY,
  uid INT NOT NULL,
  login_time DATETIME NOT NULL
);
 
SET SESSION cte_max_recursion_depth=9999999;
 
SELECT @@cte_max_recursion_depth;
 
INSERT INTO t_user_login(uid, login_time)
WITH RECURSIVE t AS (
  SELECT 1 n, rand()*10000 id, '2022-01-01 00:00:00' d
  UNION ALL
  SELECT n+1, rand()*10000, d + INTERVAL CEIL(n/3000) second
  FROM t
  WHERE n<500000
)
SELECT CEIL(id), d FROM t;

1.3、计算留存率

1.3.1、思路1:多表连接

我们先计算 1月1日的近一日留存率:

SELECT COUNT(DISTINCT tul.uid)/COUNT(DISTINCT tu.id) rr1
FROM t_user tu
LEFT JOIN t_user_login tul ON (tul.uid = tu.id AND DATE(tul.login_time) = DATE(tu.register_time) + INTERVAL '1' DAY)
WHERE tu.register_time BETWEEN '2022-01-01 00:00:00' AND '2022-01-01 23:59:59';

根据这个思路,我们可以同样实现多日的留存率:

SELECT date(u.register_time),
       100*count(DISTINCT ul1.uid)/count(DISTINCT u.id) rr1,
       100*count(DISTINCT ul2.uid)/count(DISTINCT u.id) rr3,
       100*count(DISTINCT ul3.uid)/count(DISTINCT u.id) rr7,
       100*count(DISTINCT ul4.uid)/count(DISTINCT u.id) rr30
FROM t_user u
LEFT JOIN t_user_login ul1 ON (ul1.uid = u.id AND date(ul1.login_time) = date(u.register_time) + INTERVAL '1' DAY)
LEFT JOIN t_user_login ul2 ON (ul2.uid = u.id AND date(ul2.login_time) = date(u.register_time) + INTERVAL '3' DAY)
LEFT JOIN t_user_login ul3 ON (ul3.uid = u.id AND date(ul3.login_time) = date(u.register_time) + INTERVAL '7' DAY)
LEFT JOIN t_user_login ul4 ON (ul4.uid = u.id AND date(ul4.login_time) = date(u.register_time) + INTERVAL '30' DAY)
GROUP BY date(u.register_time);

运行结果

date(u.register_time) rr1 rr3 rr7 rr30
2022-01-01 61.1806 45.4167 34.5833 18.9583
2022-01-02 50.0000 43.6806 32.2917 18.4028
2022-01-03 44.5139 38.7500 31.4583 18.5417
2022-01-04 43.0556 36.1111 29.5139 16.7361
2022-01-05 38.1944 34.6528 29.3056 16.3194
2022-01-06 35.4167 33.3333 28.1944 18.1944
2022-01-07 34.4118 29.9265 27.5735 17.6471

1.3.2、思路2:窗口函数

       需要细心的地方就是每个聚合函数的粒度(group by 哪些字段),不同的粒度代表的含义是完全不一样的(比如 group by register_date,login_date 和 group by register_date 是不同的粒度,前者的最细粒度是 login_date,后者的最细粒度是 register_date)

WITH t1 AS(
SELECT tu.id,tul.uid,DATE(tu.register_time) reg_date,DATE(tul.login_time) login_date,
       DENSE_RANK() OVER (PARTITION BY DATE(tu.register_time) ORDER BY tu.id) reg_rk, -- 按照用户注册日期进行分区 并按照用户id进行排名 得到的max(排名)就是当天注册的总人数
       DENSE_RANK() OVER (PARTITION BY DATE(tu.register_time),DATE(tul.login_time) ORDER BY tul.uid) log_rk -- max(排名)就是当天登录的人数
FROM t_user tu
LEFT JOIN t_user_login tul
ON (tu.id = tul.uid
    AND(DATE(tul.login_time) = DATE(tu.register_time) + INTERVAL 1 DAY
        OR DATE(tul.login_time) = DATE(tu.register_time) + INTERVAL 3 DAY
        OR DATE(tul.login_time) = DATE(tu.register_time) + INTERVAL 7 DAY
        OR DATE(tul.login_time) = DATE(tu.register_time) + INTERVAL 30 DAY ))
),
t2 AS (
    -- 这里的 MAX(reg_rk) 并不代表 reg_date 这一天注册的人数,因为它是按照 reg_date 和 login_date 共同分组的
    -- 所以最细粒度是 log_date 而不是 reg_date,所以当只按照 reg_date 分组时得到的才是
    -- 所以这里得到的结果中 log_sum 可以代表reg_date注册后的在log_date登录的用户数,而 reg_sum 并不能代表reg_date注册的总人数
    SELECT reg_date, login_date, MAX(reg_rk) reg_sum, MAX(log_rk) log_sum
    FROM t1
    GROUP BY reg_date,login_date -- 因为是 left join 所以会保留所有注册信息,这里同时按照 login_date 分组就会把 login_date为null的数据排除在统计范围之外
)
SELECT reg_date, MAX(reg_sum), -- 这里的max的粒度才是reg_date,得到才是正确的注册人数
       MAX(CASE WHEN login_date = reg_date + INTERVAL 1 DAY THEN log_sum END)/MAX(reg_sum) rr1,
       MAX(CASE WHEN login_date = reg_date + INTERVAL 3 DAY THEN log_sum END)/MAX(reg_sum) rr3,
       MAX(CASE WHEN login_date = reg_date + INTERVAL 7 DAY THEN log_sum END)/MAX(reg_sum) rr7,
       MAX(CASE WHEN login_date = reg_date + INTERVAL 30 DAY THEN log_sum END)/MAX(reg_sum) rr30
from t2
GROUP BY reg_date;


相关文章
|
1月前
|
SQL 数据库
SQL面试50题------(初始化工作、建立表格)
这篇文章提供了SQL面试中可能会遇到的50道题目的建表和初始化数据的SQL脚本,包括学生、教师、课程和成绩表的创建及数据插入示例。
SQL面试50题------(初始化工作、建立表格)
|
1月前
|
SQL
sql面试50题------(1-10)
这篇文章提供了SQL面试中的前10个问题及其解决方案,包括查询特定条件下的学生信息、教师信息和课程成绩等。
sql面试50题------(1-10)
|
1月前
|
SQL
sql面试50题------(11-20)
这篇文章提供了SQL面试中的50道题目,其中详细解释了11至20题,包括查询与学号为“01”的学生所学课程相同的学生信息、不及格课程的学生信息、各科成绩统计以及学生的总成绩排名等问题的SQL查询语句。
|
1月前
|
SQL
sql面试50题------(21-30)
这篇文章是SQL面试题的21至30题,涵盖了查询不同老师所教课程的平均分、按分数段统计各科成绩人数、查询学生平均成绩及其名次等问题的SQL查询语句。
sql面试50题------(21-30)
|
2月前
|
存储 SQL 索引
面试题MySQL问题之使用SQL语句创建一个索引如何解决
面试题MySQL问题之使用SQL语句创建一个索引如何解决
45 1
|
3月前
|
SQL 大数据
常见大数据面试SQL-每年总成绩都有所提升的学生
一张学生成绩表(student_scores),有year-学年,subject-课程,student-学生,score-分数这四个字段,请完成如下问题: 问题1:每年每门学科排名第一的学生 问题2:每年总成绩都有所提升的学生
|
3月前
|
SQL 关系型数据库 MySQL
sql面试题库
sql面试题库
|
2月前
|
SQL Java 关系型数据库
Java面试题:描述JDBC的工作原理,包括连接数据库、执行SQL语句等步骤。
Java面试题:描述JDBC的工作原理,包括连接数据库、执行SQL语句等步骤。
50 0
|
2月前
|
SQL 监控 Java
Java面试题:简述数据库性能优化的常见手段,如索引优化、SQL语句优化等。
Java面试题:简述数据库性能优化的常见手段,如索引优化、SQL语句优化等。
194 0
|
4月前
|
SQL BI HIVE
【Hive SQL 每日一题】统计用户留存率
用户留存率是衡量产品成功的关键指标,表示用户在特定时间内持续使用产品的比例。计算公式为留存用户数除以初始用户数。例如,游戏发行后第一天有10000玩家,第七天剩5000人,第一周留存率为50%。提供的SQL代码展示了如何根据用户活动数据统计每天的留存率。需求包括计算系统上线后的每日留存率,以及从第一天开始的累计N日留存率。通过窗口函数`LAG`和`COUNT(DISTINCT user_id)`,可以有效地分析用户留存趋势。