Shared Category Purchasers
A medium SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- medium
- Seniority
- L4
- Asked in
- Meta
Problem
The recommendations team is building a collaborative filtering feature and wants co-purchasers who shop the same aisle. For each product category, take every pair of users who both bought in that category and count the ways one user's purchases in that category combine with the other's, reporting each pair only once with the smaller of the two user IDs as the pair's first member.
They bought different things from the same aisle.
Tables
transactions
Columns: transaction_id INTEGER, user_id INTEGER, product_id INTEGER, quantity INTEGER, total_amount REAL, transaction_date TEXT
| transaction_id | user_id | product_id | quantity | total_amount | transaction_date |
|---|---|---|---|---|---|
| 1067 | 197 | 1001 | 2 | 23.46 | 2026-02-02 |
| 1134 | 294 | 1050 | 3 | 36.93 | 2026-03-03 |
| 1201 | 391 | 1099 | 4 | 50.4 | 2026-04-04 |
| 1268 | 488 | 1148 | 5 | 63.87 | 2026-05-05 |
| 1335 | 585 | 1197 | 0 | 0 | 2026-06-06 |
| 1402 | 682 | 1246 | 2 | 90.81 | 2026-07-07 |
| 1469 | 779 | 1295 | 3 | 104.28 | 2026-08-08 |
| 1536 | 876 | 1344 | 4 | 117.75 | 2026-09-09 |
| 1603 | 973 | 1393 | 5 | -45 | 2026-01-10 |
| 1670 | 1070 | 1442 | 1 | 144.69 | 2026-02-11 |
products
Columns: product_id INTEGER, product_name TEXT, category TEXT, price REAL, rating REAL, in_stock INTEGER
| product_id | product_name | category | price | rating | in_stock |
|---|---|---|---|---|---|
| 1001 | Basic Device 1X | Books | 17.52 | 2.7 | 1 |
| 1050 | Deluxe Bundle 2X | Clothing | 25.05 | 4.4 | 1 |
| 1099 | Pro Unit 3X | Home & Kitchen | 32.58 | 2.1 | 1 |
| 1148 | Ultra Tool 4X | Collectibles | 40.11 | 3.8 | 1 |
| 1197 | Essential Set 5X | Toys | 47.64 | 1.5 | 1 |
| 1246 | Classic System 6X | Beauty | 0 | 3.2 | 1 |
| 1295 | Smart Gadget 7X | Automotive | 62.7 | 4.9 | 0 |
| 1344 | Eco Kit 8X | Garden | 70.23 | 2.6 | 1 |
| 1393 | Turbo Pack 9X | Music | 77.76 | 4.3 | 0 |
| 1442 | Premium Widget 10X | Electronics | 85.29 | 2 | 1 |
Expected output
| category | user1 | user2 | shared_categories |
|---|---|---|---|
| Automotive | 294 | 779 | 24 |
| Automotive | 294 | 1264 | 18 |
| Automotive | 779 | 1264 | 48 |
| Beauty | 197 | 682 | 30 |
| Beauty | 197 | 1167 | 30 |
Worked solution and explanation
Why this problem exists in real interviews
This is a per-category pairwise co-occurrence dressed up as a recommender feature. The real skill is joining transactions to itself so two different buyers meet in one row, then canonicalizing each unordered pair with t1.user_id < t2.user_id. Anyone can group by user_id and category; that only counts what one person bought and never relates two people. The trap is the join predicate: reach for <> and you emit every pair twice in mirror order plus self-pairs, doubling and corrupting the collaborative-filtering signal. Note what the count is: COUNT(*) over the self-join is the number of ways to pair one buyer's purchases in a category with the other buyer's, not a set intersection of the same product.
Break down the requirements
Step 1: Self-join the fact table to pair buyers
Join transactions t1 to transactions t2 on t1.user_id < t2.user_id. The strict inequality does double duty: it drops self-pairs (a user can never satisfy id < id) and it keeps only one of the two mirror orderings, so each unordered pair surfaces exactly once with the smaller user ID as user1.
Step 2: Scope the pairing to a single category
Bring in products twice, once per buyer, and require p.category = p2.category. Without that predicate the two sides fan out across every category combination and the pairing you count becomes meaningless.
Step 3: Count the pairings per category and pair
GROUP BY p.category, t1.user_id, t2.user_id and COUNT(*) counts, per category per pair, every way one buyer's purchase in that category lines up with the other buyer's. Each output row is one category and one canonical pair with user1 the smaller ID, holding the pairing count in shared_categories.
The solution
SELECT p.category, t1.user_id AS user1, t2.user_id AS user2, COUNT(*) AS shared_categories
FROM transactions t1
INNER JOIN transactions t2 ON t1.user_id < t2.user_id
INNER JOIN products p ON t1.product_id = p.product_id
INNER JOIN products p2 ON t2.product_id = p2.product_id AND p.category = p2.category
GROUP BY p.category, t1.user_id, t2.user_idCommon follow-up questions
- On 100M `transactions`, the self-join is the expensive step. How would you bound it, for example by pairing only within a single category or date partition at a time? (Tests understanding that the self-join is the cost driver and that partitioning on `transaction_date` bounds the pairing.)
- How would you turn the raw pairing count into a normalized affinity score so a pair with a few purchases in a small category isn't buried under a pair with many in a huge one? (Tests whether the candidate can generalize the pair count into a similarity metric.)
- If a user buys the same `product_id` twice, that inflates the pairing count with a co-buyer. Should it, and how would you change the count if not? (Tests handling of duplicate purchases of the same product by the same user.)