Write complex SQL for cohorts and retention
Company: TikTok
Role: Software Engineer
Category: Data Manipulation (SQL/Python)
Difficulty: medium
Interview Round: Technical Screen
Given tables users(uid, created_at, country), orders(order_id, uid, amount, created_at, status), and events(uid, ts, event_type, campaign), write one SQL query that outputs, for each month and country:
(
1) new users,
(
2) conversion rate within 7 days of signup,
(
3) 7-day rolling retention (active on day D and again on D+
7),
(
4) GMV excluding refunded/canceled orders, and
(
5) top campaign by last-touch attribution from events. Handle late-arriving events, deduplicate by (uid, ts, event_type), ensure timezone consistency, and document window-function choices.
Overview: This question evaluates advanced SQL and data-engineering competencies such as cohort and retention analysis, deduplication, timezone normalization, last-touch attribution, aggregate revenue calculations, and effective use of window functions in the Data Manipulation (SQL/Python) domain.
You are given three tables:
- users(uid, created_at, country)
- orders(order_id, uid, amount, created_at, status)
- events(uid, ts, event_type, campaign)
Assume all timestamps are stored in UTC.
Write a single SQL query (you may use CTEs) that produces, for each **cohort month** and **country**, the following metrics:
1) **new_users**: number of users whose signup date (users.created_at) falls in that calendar month, grouped by country.
2) **conversion_rate_7d**: for that cohort, the fraction of users who place **at least one order** within 7 days (inclusive) of their signup timestamp. A user is considered converted if they have any order (regardless of status) with orders.created_at between users.created_at and users.created_at + 7 days.
3) **retention_rate_7d**: for that cohort, the fraction of users who have at least one event on their signup calendar date **and** at least one event exactly 7 days after their signup date. Use events.ts (converted to UTC date) to decide activity days.
4) **gmv**: total order amount for that cohort, summing orders.amount for orders whose created_at falls in the **same calendar month as the user’s signup** and whose status is **not** in ('REFUNDED', 'CANCELED').
5) **top_campaign**: the campaign with the largest number of converted users in that cohort, under a **last-touch attribution** rule:
- Consider only users who converted within 7 days (as defined in #2).
- For each such user, find their earliest order within 7 days of signup (the conversion order).
- For that user, look at all marketing events (from events) with non-NULL campaign where events.ts is **less than or equal** to the conversion order’s timestamp.
- After deduplicating events by (uid, ts, event_type), define the **last-touch campaign** as the campaign of the chronologically latest such event.
- For each cohort month and country, count how many converted users are attributed to each campaign and pick the campaign with the highest count. If there is a tie, choose the lexicographically smallest campaign name.
Additional requirements:
- **Deduplicate** events on (uid, ts, event_type) using a window function so that only one row per (uid, ts, event_type) is used.
- **Ensure timezone consistency** by treating users.created_at, orders.created_at, and events.ts uniformly as UTC when truncating to dates and months.
- Use window functions where appropriate (e.g., for deduplication and last-touch selection), and keep everything in a **single query** that returns one row per (cohort_month, country) with:
- cohort_month (truncated to the first day of the month, e.g. '2025-01-01')
- country
- new_users
- conversion_rate_7d
- retention_rate_7d
- gmv
- top_campaign
Return cohort_month formatted as YYYY-MM-DD, conversion_rate_7d and retention_rate_7d as numeric rates, and gmv formatted with two decimal places.
Tables
users(uid INT, created_at TIMESTAMP WITH TIME ZONE, country VARCHAR(2))
orders(order_id INT, uid INT, amount DECIMAL(10,2), created_at TIMESTAMP WITH TIME ZONE, status VARCHAR(20))
events(uid INT, ts TIMESTAMP WITH TIME ZONE, event_type VARCHAR(50), campaign VARCHAR(50))
Hints
- Start by normalizing timestamps to UTC and building a cohort_month from users.created_at, then compute per-user conversion and retention flags in CTEs.
- Use ROW_NUMBER window functions both to deduplicate events on (uid, ts, event_type) and to pick the last-touch campaign per converted user before their first order within 7 days.