Problem Source
This problem is from the SQL section of Niuke's Big Company Real Interview Questions, specifically scenario 02: User Growth (Baidu Information Flow), question SQL164: Calculate the next-day retention rate of new users for each day in November 2021.
Shifting from Macro-to-Micro to Micro-to-Macro
Previous chapters emphasized a top-down framework: first write the SELECT-FROM-WHERE skeleton, then break down the problem and fill in the details. This macro-to-micro approach works well for straightforward or moderately difficult queries, ensuring clarity and reducing syntax errors.
How ever, complex problems often require multiple nested subqueries, JOINs, and UNIONs. Applying the same top-down method can lead to missed conditions and logic gaps. The reason: solving hard problems is inherently micro-to-macro—we decompose the big problem into small, manageable sub-problems and then combine them. This article introduces a bottom-up approach tailored for difficult SQL challenges.
Problem Description
Table tb_user_log:
| Column | Description |
|---|---|
| uid | User ID |
| article_id | Article ID |
| in_time | Entry time |
| out_time | Exit time |
| sign_in | Sign-in flag |
Requirement: Calculate the next-day retention rate of new users for each day in November 2021. The retention rate (uv_left_rate) should be rounded to two decimal places.
Note:
- A user is considered active on a day if either their
in_timeorout_timefalls on that day (i.e., cross-day sessions count for both days). - The next-day retention rate is defined as (number of new users who are active the next day) / (number of new users on the current day).
- Only users whose first activity in the system occurred in November 2021 are considered "new users." Users who had any activity before November 2021 should be excluded.
- Order results by date ascending.
Step 1: Break Down the Problem
We need to identify the core sub-problems and their dependencies:
-
Identify new users in November 2021: Users whose earliest recorded
in_timefalls in November 2021, and who had no earlier activity. UsingWHEREon theMIN(in_time)result would not correctly exclude users with activity before November; we needHAVINGafterGROUP BY. -
Identify all active days for each user: Combine
in_timeandout_time(converted to dates) usingUNIONto get a unified list of active dates per user. -
Find next-day retention: For each new user (from step 1), check if the set of active days (from step 2) contains the day after their first activity date.
-
Calculate rate: Group by the first activity date, count distinct new users, and count distinct users who also appear in the next day's active set. Use
ROUNDfor two decimal places and handleNULL.
Step 2: Write Sub-queries from Micro to Macro
Instead of starting with the outer SELECT, we build each logical block independent and then assemble them. Using Common Table Expressions (CTEs) with WITH ... AS makes the final query cleaner and matches the bottom-up thought process.
CTE a: New users and their first activity date
WITH a AS (
SELECT
uid,
MIN(DATE(in_time)) AS dt
FROM tb_user_log
GROUP BY uid
HAVING dt LIKE '2021-11%'
),
CTE b: All active dates per user
b AS (
SELECT uid, DATE(in_time) AS dt
FROM tb_user_log
UNION
SELECT uid, DATE(out_time) AS dt
FROM tb_user_log
)
Final query: Join and compute retention
SELECT
a.dt,
IFNULL(ROUND(COUNT(DISTINCT b.uid) / COUNT(a.uid), 2), 0) AS uv_left_rate
FROM a
LEFT JOIN b
ON a.uid = b.uid
AND b.dt = DATE_ADD(a.dt, INTERVAL 1 DAY)
GROUP BY a.dt
ORDER BY a.dt;
Step 3: Complete Code
WITH a AS (
SELECT
uid,
MIN(DATE(in_time)) AS dt
FROM tb_user_log
GROUP BY uid
HAVING dt LIKE '2021-11%'
),
b AS (
SELECT uid, DATE(in_time) AS dt
FROM tb_user_log
UNION
SELECT uid, DATE(out_time) AS dt
FROM tb_user_log
)
SELECT
a.dt,
IFNULL(ROUND(COUNT(DISTINCT b.uid) / COUNT(a.uid), 2), 0) AS uv_left_rate
FROM a
LEFT JOIN b
ON a.uid = b.uid
AND b.dt = DATE_ADD(a.dt, INTERVAL 1 DAY)
GROUP BY a.dt
ORDER BY a.dt;
Key Takeaways
- For complex problems, decompose the task into self-contained sub-queries first, then combine them. CTEs (
WITH ... AS) help keep the logic modular and readable. - Use
UNIONto merge multiple date columns (likein_timeandout_time) when both indicate user activity. LEFT JOINensures that new users with no next-day activity are still counted in the denominator (with0retention).- The
IFNULLfunction preventsNULLvalues when there is no matching next-day record for a group. - Filtering new users via
HAVINGon the result ofMINcorrectly restricts the set to those whose very first activity was in November 2021.
Common Table Expression (CTE) Notes
Purpose:
- Improves SQL readability.
- In
UNION ALLor repeated references, the optimizer may materialize the CTE into a temp table, reducing execution cost.
Basic Syntax:
WITH <cte_name> AS (<subquery>)
Multiple CTEs:
WITH <cte1> AS (<subquery>),
<cte2> AS (<subquery>)
SELECT ...
Important:
- CTEs must be immediately fololwed by the main query that references them.
- Each CTE name is temporary and scoped to the statement.
- If a CTE is referenced only once, the optimizer may not materialize it; you can use hints to force materialization if needed.