Write SQL for shop visibility and activity metric
Company: Meta
Role: Data Scientist
Category: Data Manipulation (SQL/Python)
Difficulty: medium
Interview Round: Onsite
Assume 'today' is 2025-09-01. Schema and tiny samples: 1) shops(shop_id INT, created_at DATE)
Sample:
shop_id | created_at
1 | 2025-08-25
2 | 2025-07-15
3 | 2025-08-30
4 | 2025-06-01
5 | 2025-08-05
2) listings(listing_id INT, shop_id INT, created_at DATE, is_visible TINYINT) -- is_visible is a 2025-09-01 snapshot
Sample:
listing_id | shop_id | created_at | is_visible
101 | 1 | 2025-08-26 | 1
102 | 1 | 2025-08-27 | 0
103 | 1 | 2025-08-29 | 1
104 | 2 | 2025-07-20 | 1
105 | 2 | 2025-07-25 | 1
106 | 3 | 2025-08-31 | 0
107 | 3 | 2025-08-31 | 0
108 | 4 | 2025-06-05 | 1
109 | 5 | 2025-08-10 | 1
3) shop_events(shop_id INT, event_date DATE, event_type VARCHAR, cnt INT) -- event_type in ('product_created','listing_updated','message_replied','order_placed')
Sample:
shop_id | event_date | event_type | cnt
1 | 2025-08-26 | product_created | 3
1 | 2025-08-29 | message_replied | 5
2 | 2025-08-27 | order_placed | 2
3 | 2025-08-31 | listing_updated | 4
3 | 2025-08-30 | order_placed | 1
4 | 2025-08-28 | product_created | 1
5 | 2025-08-21 | product_created | 2
5 | 2025-08-25 | message_replied | 1
Tasks: A) Write SQL to compute per-shop visibility_rate on 2025-09-01 = AVG(is_visible) over that shop’s listings; return only shops with at least 5 listings and rank by visibility_rate DESC, breaking ties by total listings DESC. B) Define a 'new shop' as created_at >= 2025-08-02 (last 30 days). Define activity_score over the window [2025-08-19, 2025-09-01] as 3*order_placed + 1*product_created + 0.5*listing_updated + 0.2*message_replied using counts from shop_events; shops with no events have score 0. Write SQL to output one row per shop with: shop_id, is_new, visibility_rate (from A; default NULL if <1 listing), activity_score. C) Using that output, write SQL to produce a 3-row summary: metric_name, new_shops_value, existing_shops_value, pct_diff for (i) mean activity_score, (ii) median activity_score (use an analytic function), and (iii) share_of_active_shops (fraction with activity_score > 0). D) Finally, list the top 5 new shops by activity_score whose visibility_rate < 0.5 to identify active-but-underexposed shops. Explain any assumptions you make about NULL vs 0 and how you’d prevent survivorship bias.
Overview: This question evaluates SQL-based data manipulation and analytical skills, including aggregations, window functions, cohort comparisons, ranking, median calculation with analytic functions, and careful handling of NULLs versus zeros when constructing time-windowed metrics like visibility_rate and activity_score.
Read the full Meta Data Scientist interview experience this question came from
Shop visibility rate on snapshot date
You work with three tables: shops, listings, and shop_events. On the fixed snapshot date 2025-06-01, the listings table records whether each listing is visible via the is_visible column (1 = visible, 0 = hidden).
Define a shop's visibility_rate on 2025-06-01 as the average of is_visible across all of that shop's listings (i.e., AVG(is_visible) for that shop).
Write a SQL query to:
1) Compute visibility_rate per shop.
2) Return only shops that have at least 5 listings.
3) Output columns: shop_id, visibility_rate, total_listings.
4) Sort the result in descending order of visibility_rate, breaking ties by total_listings in descending order (and optionally by shop_id ascending for deterministic output).
Tables
shops(shop_id INT, created_at DATE)
listings(listing_id INT, shop_id INT, created_at DATE, is_visible TINYINT)
shop_events(shop_id INT, event_date DATE, event_type VARCHAR(32), cnt INT)
Hints
- Aggregate over listings by shop_id and use AVG on the is_visible flag.
- Filter after aggregation with HAVING and sort with ORDER BY using multiple keys.
Per-shop new/existing flag, visibility, and activity score
Using the same tables, define metrics relative to the fixed reference date 2025-06-01.
Definitions:
- A 'new shop' is a shop with created_at >= '2025-05-02' (shops opened in the last 30 days, from 2025-05-02 to 2025-06-01 inclusive).
- The visibility_rate for a shop is the average of is_visible across all its listings on the 2025-06-01 snapshot. If a shop has no listings, its visibility_rate should be NULL.
- The activity_score for a shop over the window from '2025-05-19' to '2025-06-01' (inclusive) is defined as:
activity_score = 3 * order_placed
+ 1 * product_created
+ 0.5 * listing_updated
+ 0.2 * message_replied
where order_placed, product_created, listing_updated, and message_replied are the summed cnt values from shop_events of each event_type over that date range.
- Shops with no events in that window should have activity_score = 0.
Write a SQL query that outputs one row per shop with the columns:
- shop_id
- is_new (1 for new shops, 0 for existing shops)
- visibility_rate (NULL if the shop has no listings)
- activity_score (0 if the shop has no events in the window)
Order the result by shop_id ascending.
Tables
shops(shop_id INT, created_at DATE)
listings(listing_id INT, shop_id INT, created_at DATE, is_visible TINYINT)
shop_events(shop_id INT, event_date DATE, event_type VARCHAR(32), cnt INT)
Hints
- Pre-aggregate listings by shop_id to get visibility_rate, then left join to shops.
- Pre-aggregate shop_events by shop_id over the given date range and COALESCE NULL scores to 0.
Compare new vs existing shops using mean, median, and activity share
You are analyzing seller shops on a marketplace. You have three tables:
**`shops`** — one row per shop.
| column | type | notes |
|---|---|---|
| `shop_id` | INT | primary key |
| `created_at` | DATE | date the shop was created |
**`listings`** — one row per product listing (provided for context; not required for this question).
| column | type | notes |
|---|---|---|
| `listing_id` | INT | primary key |
| `shop_id` | INT | the shop the listing belongs to |
| `created_at` | DATE | date the listing was created |
| `is_visible` | SMALLINT | 1 if the listing is publicly visible, else 0 |
**`shop_events`** — one row per (shop, day, event type), with an aggregated count.
| column | type | notes |
|---|---|---|
| `shop_id` | INT | the shop |
| `event_date` | DATE | day the events occurred |
| `event_type` | VARCHAR(32) | one of `order_placed`, `product_created`, `listing_updated`, `message_replied` |
| `cnt` | INT | number of events of that type on that day |
**Per-shop definitions (carried over from the previous question):**
- A shop is **new** (`is_new = 1`) if `created_at >= '2025-05-02'`, otherwise it is **existing** (`is_new = 0`).
- A shop's **`activity_score`** is computed over the window `event_date` from `'2025-05-19'` to `'2025-06-01'` (inclusive on both ends) as a weighted sum of its events:
- `order_placed` → 3.0 per event
- `product_created` → 1.0 per event
- `listing_updated` → 0.5 per event
- `message_replied` → 0.2 per event
A shop with no events in that window has `activity_score = 0`. (Every shop in `shops` must appear, scored 0 if it has no qualifying events.)
**Task:** Produce a summary that compares **new** vs **existing** shops across three metrics. Return **exactly three rows** with these four columns:
- `metric_name` — the metric label (text).
- `new_shops_value` — the metric's value for new shops.
- `existing_shops_value` — the metric's value for existing shops.
- `pct_diff` — defined as `100 * (new_shops_value - existing_shops_value) / existing_shops_value`; return `NULL` if `existing_shops_value` is 0.
The three metrics (one row each) are:
1. `mean_activity_score` — the average `activity_score` across all shops in the group.
2. `median_activity_score` — the median `activity_score` across all shops in the group, computed with an analytic/ordered-set function (`PERCENTILE_CONT(0.5)`).
3. `share_of_active_shops` — the fraction of shops in the group whose `activity_score > 0`.
Round every numeric value (`new_shops_value`, `existing_shops_value`, `pct_diff`) to **4 decimal places**. Order the result rows alphabetically by `metric_name`.
Tables
shops(shop_id INT, created_at DATE)
listings(listing_id INT, shop_id INT, created_at DATE, is_visible SMALLINT)
shop_events(shop_id INT, event_date DATE, event_type VARCHAR(32), cnt INT)
Hints
- Build a per-shop CTE first: LEFT JOIN shops to the windowed event scores and COALESCE missing scores to 0 so every shop is counted, then tag is_new.
- Aggregate per is_new group with GROUP BY — use AVG for the mean, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY activity_score) for the median (do NOT add OVER; ordered-set aggregates can't be window functions in Postgres), and AVG of a 1/0 flag for the active share.
Identify active but under-exposed new shops
Using the per-shop metrics from Question 2 (shop_id, is_new, visibility_rate, activity_score), find new shops that are active but have low visibility.
Definitions recap:
- is_new = 1 if created_at >= '2025-05-02', else 0.
- visibility_rate is the average is_visible across the shop's listings on 2025-06-01; shops with no listings have visibility_rate = NULL.
- activity_score is computed over 2025-05-19 to 2025-06-01 as in Question 2.
Write a SQL query to:
1) Consider only new shops (is_new = 1).
2) Among them, filter to shops with visibility_rate < 0.5. (Assume that shops with visibility_rate = NULL are excluded from this filter.)
3) Return the top 5 such shops ordered by activity_score in descending order, breaking ties by shop_id ascending.
4) Output columns: shop_id, visibility_rate, activity_score.
In an interview setting, you should also be ready to explain:
- Why visibility_rate is NULL (not 0) for shops with no listings, and why activity_score is 0 (not NULL) for shops with no events.
- How you would prevent survivorship bias in a real analysis (e.g., by ensuring the denominator includes shops that churned before the snapshot date), but you do not need to express that explanation in SQL here.
Tables
shops(shop_id INT, created_at DATE)
listings(listing_id INT, shop_id INT, created_at DATE, is_visible TINYINT)
shop_events(shop_id INT, event_date DATE, event_type VARCHAR(32), cnt INT)
Hints
- Re-use (or rebuild) the per-shop metrics CTE from Question 2 to get is_new, visibility_rate, and activity_score in one place.
- Filter on is_new = 1 and visibility_rate < 0.5, then order by activity_score DESC and limit to 5 rows.