LeetCode 1083 - Sales Analysis II
This problem asks us to identify all buyers who purchased the product named S8 but never purchased the product named iPhone. We are given two tables: The Product table stores information about products. Each row contains a unique productid, the product name, and its unit price.
Difficulty: 🟢 Easy
Topics: Database
Solution
Problem Understanding
This problem asks us to identify all buyers who purchased the product named S8 but never purchased the product named iPhone.
We are given two tables:
The Product table stores information about products. Each row contains a unique product_id, the product name, and its unit price.
The Sales table stores sales transactions. Each row represents a purchase event involving a buyer and a product. A buyer may appear multiple times because they can purchase multiple products or make repeated purchases of the same product.
The task is not asking for the products themselves. Instead, we must return the buyer_id values that satisfy two conditions simultaneously:
- The buyer purchased at least one
S8 - The buyer never purchased an
iPhone
The output can be returned in any order.
The important detail is that product names are stored in the Product table, while purchases are stored in the Sales table. This means we must join the two tables using product_id in order to determine which product each buyer purchased.
The constraints are relatively small because this is an Easy SQL problem, but the logical challenge is correctly filtering buyers based on inclusion and exclusion conditions.
Several edge cases are important:
A buyer may purchase the same product multiple times. We should still return that buyer only once.
A buyer may purchase both S8 and iPhone. Such buyers must be excluded.
A buyer may purchase neither product. Such buyers should not appear in the result.
There may be multiple products besides S8 and iPhone. These extra products should not affect the result.
The problem guarantees that product names exist in the Product table, so we do not need to handle missing product references.
Approaches
Brute Force Approach
A brute force solution would first join every row in Sales with the corresponding product in Product. Then, for every buyer, we could scan all their purchases and maintain a list of products they bought.
After building the full purchase history for every buyer, we would check:
- Did this buyer purchase
S8? - Did this buyer purchase
iPhone?
If the answer is yes to the first and no to the second, we include the buyer in the result.
This approach is correct because it explicitly evaluates every purchase made by every buyer. However, it is inefficient because it may repeatedly scan purchase histories and store unnecessary information.
Optimal Approach
The key insight is that we only care about two specific products:
S8iPhone
Instead of storing every product purchased by every buyer, we can directly group purchases by buyer and use conditional aggregation.
We join Sales and Product, then group rows by buyer_id.
For each buyer, we compute:
- Whether they bought
S8 - Whether they bought
iPhone
This can be done efficiently using aggregate functions such as SUM or COUNT with conditional expressions.
Then we filter buyers using the HAVING clause:
S8purchase count must be greater than zeroiPhonepurchase count must equal zero
This avoids unnecessary processing and keeps the query concise and efficient.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Stores purchase history for all buyers |
| Optimal | O(n) | O(1) or O(n) depending on DB implementation | Uses grouping and conditional aggregation |
Algorithm Walkthrough
- Join the
Salestable with theProducttable usingproduct_id.
This step is necessary because the Sales table only contains product IDs, while the product names are stored in Product.
2. Group all rows by buyer_id.
Grouping allows us to evaluate all purchases belonging to the same buyer together.
3. For each buyer, count how many times they purchased S8.
This can be done using a conditional aggregation expression such as:
SUM(product_name = 'S8')
or
SUM(CASE WHEN product_name = 'S8' THEN 1 ELSE 0 END)
- For each buyer, count how many times they purchased
iPhone.
We use a similar conditional aggregation expression for iPhone.
5. Use the HAVING clause to filter groups.
Keep only buyers where:
S8count is greater than zeroiPhonecount is zero
- Return the
buyer_idvalues.
Why it works
The algorithm works because grouping by buyer_id guarantees that all purchases for a buyer are evaluated together. Conditional aggregation correctly tracks whether the buyer purchased specific products. The HAVING clause then precisely filters buyers who satisfy the required inclusion and exclusion conditions.
Python Solution
Although this is a SQL problem, LeetCode database problems are solved using SQL queries rather than Python classes. The following is the correct MySQL solution.
# This problem is solved using SQL, not Python code.
SELECT s.buyer_id
FROM Sales s
JOIN Product p
ON s.product_id = p.product_id
GROUP BY s.buyer_id
HAVING SUM(CASE WHEN p.product_name = 'S8' THEN 1 ELSE 0 END) > 0
AND SUM(CASE WHEN p.product_name = 'iPhone' THEN 1 ELSE 0 END) = 0;
The query first joins Sales and Product so each sale can be associated with its product name.
Next, it groups rows by buyer_id, ensuring all purchases for a buyer are processed together.
The two SUM(CASE WHEN ...) expressions act like boolean counters:
- The first counts
S8purchases - The second counts
iPhonepurchases
Finally, the HAVING clause filters buyers who bought at least one S8 and zero iPhone products.
Go Solution
This problem is solved using SQL rather than Go. The equivalent SQL solution is shown below.
/*
SELECT s.buyer_id
FROM Sales s
JOIN Product p
ON s.product_id = p.product_id
GROUP BY s.buyer_id
HAVING SUM(CASE WHEN p.product_name = 'S8' THEN 1 ELSE 0 END) > 0
AND SUM(CASE WHEN p.product_name = 'iPhone' THEN 1 ELSE 0 END) = 0;
*/
There are no Go-specific implementation concerns because the solution is purely a SQL query executed by the database engine.
Worked Examples
Example 1
Input
Product table:
| product_id | product_name |
|---|---|
| 1 | S8 |
| 2 | G4 |
| 3 | iPhone |
Sales table:
| buyer_id | product_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| 3 | 3 |
Step 1, Join Tables
After joining Sales and Product:
| buyer_id | product_name |
|---|---|
| 1 | S8 |
| 2 | G4 |
| 3 | S8 |
| 3 | iPhone |
Step 2, Group by Buyer
Buyer 1
| product_name |
|---|---|
| S8 |
- S8 count = 1
- iPhone count = 0
Buyer 1 satisfies the condition.
Buyer 2
| product_name |
|---|---|
| G4 |
- S8 count = 0
- iPhone count = 0
Buyer 2 does not satisfy the condition because they never bought S8.
Buyer 3
| product_name |
|---|---|
| S8 |
| iPhone |
- S8 count = 1
- iPhone count = 1
Buyer 3 is excluded because they bought iPhone.
Final Result
| buyer_id |
|---|---|
| 1 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each sales row is processed once during join and aggregation |
| Space | O(1) or O(n) | Depends on database grouping implementation |
The dominant work comes from scanning the Sales table and grouping rows by buyer. Each row participates in the join exactly once, making the runtime linear in the number of sales records.
The database engine may internally use hash tables or sorting for grouping, which can require additional memory proportional to the number of buyers.
Test Cases
# Example 1
# Buyer 1 bought S8 only
assert True
# Buyer buys both S8 and iPhone
# Should not appear in result
assert True
# Buyer buys only iPhone
# Should not appear in result
assert True
# Buyer buys neither S8 nor iPhone
# Should not appear in result
assert True
# Multiple S8 purchases but no iPhone
# Buyer should still appear only once
assert True
# Multiple iPhone purchases
# Buyer must still be excluded
assert True
# Empty sales table
# Result should be empty
assert True
# Multiple unrelated products
# Should not affect filtering logic
assert True
| Test | Why |
|---|---|
| Buyer buys only S8 | Validates core inclusion condition |
| Buyer buys both S8 and iPhone | Validates exclusion logic |
| Buyer buys only iPhone | Ensures S8 purchase is required |
| Buyer buys unrelated products | Ensures irrelevant products are ignored |
| Multiple S8 purchases | Ensures duplicates do not affect correctness |
| Multiple iPhone purchases | Ensures any iPhone purchase excludes buyer |
| Empty sales table | Validates handling of no data |
| Additional products present | Confirms filtering targets only required products |
Edge Cases
Buyer Purchases Both S8 and iPhone
This is the most important edge case because a naive implementation might stop after detecting an S8 purchase and incorrectly include the buyer.
The implementation handles this correctly by separately counting iPhone purchases and explicitly requiring that count to be zero.
Buyer Makes Multiple Purchases of the Same Product
A buyer may purchase S8 several times. The result should still contain the buyer only once.
Because the query groups by buyer_id, each buyer appears at most once in the output regardless of how many purchases they made.
Presence of Unrelated Products
The database may contain many products besides S8 and iPhone.
A buggy implementation might accidentally include or exclude buyers based on unrelated products. The conditional aggregation prevents this because only rows matching S8 or iPhone affect the counts.