The Row Count Surprise
An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- easy
- Seniority
- L3
- Asked in
- Dell Technologies
Problem
The data quality team is auditing how a users-vs-ad_impressions join behaves under the three classic semantics. Some users were never served an impression, and some impressions don't tie back to a known user. In a single result set, surface three rows labeled inner_join, left_join, and full_outer_join, each with the total row count that combination produces.
Same tables. Different handshakes. Wildly different results.
Tables
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
ad_impressions
Columns: impression_id INTEGER, user_id INTEGER, ad_campaign TEXT, impression_time TEXT, clicked INTEGER, revenue REAL
| impression_id | user_id | ad_campaign | impression_time | clicked | revenue |
|---|---|---|---|---|---|
| 253 | 100 | BRAND_AWARENESS_Q1 | 2026-02-02 01:07:00 | 0 | NULL |
| 306 | 197 | PRODUCT_LAUNCH_X | 2026-03-03 02:14:00 | 0 | NULL |
| 359 | 294 | RETARGETING_CART | 2026-04-04 03:21:00 | 0 | NULL |
| 412 | 391 | HOLIDAY_PROMO | 2026-05-05 04:28:00 | 0 | NULL |
| 465 | 488 | NEW_USER_ACQU | 2026-06-06 05:35:00 | 1 | 0.2 |
| 518 | NULL | LOYALTY_PROGRAM | 2026-07-07 06:42:00 | 0 | NULL |
| 571 | 682 | FLASH_SALE_48H | 2026-08-08 07:49:00 | 0 | NULL |
| 624 | 779 | SUMMER_SALE_2024 | 2026-09-09 08:56:00 | 0 | NULL |
| 677 | 876 | BRAND_AWARENESS_Q1 | 2026-01-10 09:03:00 | 0 | NULL |
| 730 | 973 | PRODUCT_LAUNCH_X | 2026-02-11 10:10:00 | 1 | 0.35 |
Expected output
| join_type | row_count |
|---|---|
| inner_join | 180 |
| left_join | 355 |
| full_outer_join | 375 |
Worked solution and explanation
Why this problem exists in real interviews
Same two tables, three different join semantics, three different row counts. The interviewer is checking that you can name what each join keeps and drops, and that you can emulate FULL OUTER on engines that don't support it (SQLite, MySQL pre-8) by stitching two LEFT JOINs together with UNION ALL.
Break down the requirements
Step 1: One row per join type, three branches
Each join type produces one row in the output: 'inner_join', 'left_join', 'full_outer_join'. Stitch them with UNION ALL so all three counts appear in a single result set with two columns: join_type, row_count.
Step 2: Inner and left are direct
Inner: INNER JOIN keeps only matched pairs. Left: LEFT JOIN keeps every user, with one row per matched impression and exactly one row (with NULLs) for unmatched users.
Step 3: Emulate full outer with two LEFT JOINs
Full outer: SQLite has no FULL OUTER JOIN, so build it as the LEFT JOIN result UNION ALL the right-only rows. The right-only branch is ad_impressions LEFT JOIN users with WHERE u.user_id IS NULL, which keeps the impressions that had no matching user.
The solution
SELECT 'inner_join' AS join_type, COUNT(*) AS row_count
FROM users u
INNER JOIN ad_impressions ai ON u.user_id = ai.user_id
UNION ALL
SELECT 'left_join', COUNT(*)
FROM users u
LEFT JOIN ad_impressions ai ON u.user_id = ai.user_id
UNION ALL
SELECT 'full_outer_join', COUNT(*)
FROM (
SELECT u.user_id, ai.impression_id
FROM users u
LEFT JOIN ad_impressions ai ON u.user_id = ai.user_id
UNION ALL
SELECT u.user_id, ai.impression_id
FROM ad_impressions ai
LEFT JOIN users u ON ai.user_id = u.user_id
WHERE u.user_id IS NULL
)Common follow-up questions
- `ad_impressions.user_id` is 15% NULL. Which of the three counts is affected by those NULLs and how? (Tests grasp of NULL semantics on the join key.)
- If a user has 80 impressions on average, what fraction of the `inner_join` row count comes from a single fan-out user? (Tests cardinality intuition with one-to-many relationships.)
- How would you also return `only_in_users` (users with no impressions) and `only_in_impressions` (impressions with no user) as two more rows? (Tests using set operations to compute the symmetric difference.)