Diagnose MySQL joins and GROUP BY/HAVING errors
Company: Amazon
Role: Data Scientist
Category: Data Manipulation (SQL/Python)
Difficulty: medium
Interview Round: Online Assessment
You are using MySQL 8.0 with ONLY_FULL_GROUP_BY enabled. Answer all parts precisely. Provide the exact SQL you would run and the final result shapes/values where requested.
Schema (use these small samples exactly as given):
Table A
+----+------+
| id | valA |
+----+------+
| 1 | 'x' |
| 2 | 'y' |
| 3 | 'z' |
| 4 | NULL|
+----+------+
Table B
+----+------+
| id | valB |
+----+------+
| 2 | 'p' |
| 3 | 'q' |
| 3 | 'r' |
| 5 | 's' |
|NULL| 't' |
+----+------+
Table t
+------+------+
| a | b |
+------+------+
| 1 | 'u' |
| 1 | 'v' |
| 1 | 'v' |
| 2 | 'w' |
| NULL | 'x' |
+------+------+
Table users
+---------+---------+
| user_id | name |
+---------+---------+
| 1 | 'Ann' |
| 2 | 'Bob' |
| 3 | 'Cara' |
+---------+---------+
Table orders
+----------+---------+---------------------+--------------+
| order_id | user_id | created_at | amount_cents |
+----------+---------+---------------------+--------------+
| 10 | 1 | '2025-08-30 10:00' | 1200 |
| 11 | 1 | '2025-08-31 09:30' | 300 |
| 12 | 2 | '2025-09-01 12:45' | 4500 |
+----------+---------+---------------------+--------------+
Table events
+-----------+------------------+
| event_id | duration_seconds |
+-----------+------------------+
| 100 | 59 |
| 101 | 61 |
| 102 | 90061 |
+-----------+------------------+
Table sessions
+------------+---------+---------------------+---------------------+
| session_id | user_id | login_at | logout_at |
+------------+---------+---------------------+---------------------+
| 1000 | 1 | '2025-08-31 23:50' | '2025-09-01 00:10:40'|
| 1001 | 2 | '2025-09-01 09:00' | '2025-09-01 09:00' |
| 1002 | 3 | '2025-09-01 10:15' | NULL |
+------------+---------+---------------------+---------------------+
A) Explain precisely what LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, and CROSS JOIN mean in terms of set semantics and duplicate handling. Then, using A and B:
1) Write queries for each join type (note: MySQL lacks FULL OUTER JOIN—emulate it with correct SQL).
2) For each join type, give the exact row count returned and list the rows for the first three results when ordered by COALESCE(A.id, B.id), B.valB, A.valA. Explain how NULLs affect equality joins here.
B) For the following MySQL queries over table t, indicate for each whether it executes or errors under ONLY_FULL_GROUP_BY. If it errors, state the exact reason (e.g., nonaggregated column not in GROUP BY, clause order, reserved word usage). Do not change the text.
Q1: SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 2;
Q2: SELECT a, b, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 2;
Q3: SELECT a, COUNT(*) AS cnt FROM t HAVING cnt > 2 GROUP BY a ORDER BY 1,2;
Q4: SELECT a, COUNT(*) AS table FROM t GROUP BY a HAVING table > 2 ORDER BY 1,2;
Q5: SELECT a, COUNT(*) AS `table` FROM t GROUP BY a HAVING `table` > 2 ORDER BY 1,2;
C) Write a single query that returns, for every user (including those with zero orders): user_id, name, orders_count, total_spend_dollars (amount_cents/100 with two decimals). Sort by total_spend_dollars DESC then user_id ASC. Ensure users with no orders appear with 0 and 0.00.
D) Convert duration_seconds in events to a human-readable string exactly in the form 'X days Y hours Z minutes W seconds' using integer arithmetic (no loops/UDFs). For example, 90061 should render '1 days 1 hours 1 minutes 1 seconds'. Provide the SELECT that produces both the original seconds and the string.
E) Using sessions, output session_id, user_id, hhmmss_duration (as TIME) and duration_seconds (as INT). Use TIMEDIFF and TIME_TO_SEC. Treat NULL logout_at as exclude-from-output (i.e., only return rows with non-NULL logout_at). Also explain how your query would change if you had to treat NULL logout_at as NOW().
Overview: This question evaluates mastery of SQL join semantics (LEFT, RIGHT, FULL OUTER emulation, CROSS), NULL equality and duplicate handling, GROUP BY/HAVING behavior under ONLY_FULL_GROUP_BY, aggregation result shaping and type/format conversions in the Data Manipulation (SQL/Python) domain.
Understanding Join Types and NULL Handling in MySQL
## Join Types and NULL Handling
You are given two tables, `A` and `B`.
- `A(id, val_a)` — `id` is the primary key (never NULL); `val_a` may be NULL.
- `B(id, val_b)` — `id` may be NULL and is **not** unique (duplicate ids allowed); `val_b` may be NULL.
**Conceptual part (explain in words):** Describe what `LEFT JOIN`, `RIGHT JOIN`, `FULL OUTER JOIN`, and `CROSS JOIN` mean in terms of (a) set semantics — which input rows are guaranteed to appear — and (b) duplicate handling — how many output rows appear when a key matches multiple rows on the other side. Also note how the equality predicate `A.id = B.id` treats NULL (since `NULL = NULL` evaluates to UNKNOWN, NULL keys never match and only appear through outer-join padding).
**Query part:** Write **one** SQL query that produces the result of all four join types over `A` and `B` (joining on `A.id = B.id` for the three equality joins, and the unconditional Cartesian product for `CROSS`), stacked into a single result set. The output must have exactly these five columns:
| column | meaning |
|--------|---------|
| `join_type` | one of `'CROSS'`, `'FULL'`, `'LEFT'`, `'RIGHT'` identifying which join produced the row |
| `a_id` | `A.id` for the row (NULL when the row came from B with no A match) |
| `a_val_a` | `A.val_a` for the row |
| `b_id` | `B.id` for the row (NULL when the row came from A with no B match) |
| `b_val_b` | `B.val_b` for the row |
For `FULL`, emulate a full outer join by combining the `LEFT JOIN` result with the rows of the `RIGHT JOIN` result that have no A match (`a_id IS NULL`), de-duplicating exact duplicates (use `UNION`).
Each output row represents one row of the corresponding join. **Order the combined output by** `join_type`, then `COALESCE(a_id, b_id)`, then `b_val_b`, then `a_val_a` (all ascending; PostgreSQL sorts NULLs last for ascending order).
Tables
A(id INT, val_a VARCHAR(10))
B(id INT, val_b VARCHAR(10))
Hints
- Build one CTE per join type, all projecting the same columns, then stack them with UNION ALL and tag each with a literal join_type string.
- Emulate FULL OUTER JOIN as the LEFT JOIN result UNION the RIGHT JOIN rows that have no left match (a_id IS NULL).
Diagnosing ONLY_FULL_GROUP_BY Errors for GROUP BY / HAVING
You are using MySQL 8.0 with ONLY_FULL_GROUP_BY enabled. For each of the following queries over table t, state whether it executes successfully or errors. If it errors, specify why (e.g., nonaggregated column not in GROUP BY, clause order, reserved word usage). Do not change the query text.
Q1: SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 2;
Q2: SELECT a, b, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 2;
Q3: SELECT a, COUNT(*) AS cnt FROM t HAVING cnt > 2 GROUP BY a ORDER BY 1,2;
Q4: SELECT a, COUNT(*) AS table FROM t GROUP BY a HAVING table > 2 ORDER BY 1,2;
Q5: SELECT a, COUNT(*) AS `table` FROM t GROUP BY a HAVING `table` > 2 ORDER BY 1,2;
Tables
t(a INT, b VARCHAR(10))
Hints
- ONLY_FULL_GROUP_BY requires every nonaggregated column in the SELECT list to be named in the GROUP BY.
- Check the legal clause order in a SELECT statement: FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT.
Users with Order Counts and Total Spend
Write a single MySQL query that returns, for every user (including those with zero orders): user_id, name, orders_count, and total_spend_dollars. total_spend_dollars should be the sum of amount_cents divided by 100 with exactly two decimal places. Users with no orders must appear with orders_count = 0 and total_spend_dollars = 0.00. Sort the result by total_spend_dollars in descending order, then by user_id in ascending order.
Tables
users(user_id INT, name VARCHAR(50))
orders(order_id INT, user_id INT, created_at DATETIME, amount_cents INT)
Hints
- Use a LEFT JOIN from users to orders so that users with no orders are still returned.
- Remember that SUM over no rows is NULL; use COALESCE to turn it into 0.00.
Formatting Durations as Days/Hours/Minutes/Seconds
Using the `events` table, write a **PostgreSQL** query that converts each event's `duration_seconds` into a human-readable string of the exact form `'X days Y hours Z minutes W seconds'`, using only integer arithmetic (no loops, no user-defined functions, no interval/date functions).
The decomposition uses fixed conversions: 86400 seconds per day, 3600 per hour, 60 per minute. For example, 90061 seconds renders as `'1 days 1 hours 1 minutes 1 seconds'`, and 61 seconds renders as `'0 days 0 hours 1 minutes 1 seconds'`. The unit words are always plural (no special-casing for 0 or 1).
**Return** one row per event with these three columns:
- `event_id` — the event's id
- `duration_seconds` — the original (unchanged) duration in seconds
- `human_readable` — the formatted string described above
**Sort** the result by `event_id` ascending.
Tables
events(event_id INT, duration_seconds INT)
Hints
- There are 86400 seconds in a day, 3600 in an hour, and 60 in a minute.
- In PostgreSQL, integer division is `/` (on integer operands) and modulo is `%` — there is no `DIV` operator and `MOD` is a function, not an infix operator.
Computing Session Durations with TIMEDIFF and TIME_TO_SEC
## Compute closed-session durations
You are given a `sessions` table that records when each user logged in and out.
| column | type | notes |
|--------|------|-------|
| `session_id` | INT | primary key |
| `user_id` | INT | who the session belongs to |
| `login_at` | TIMESTAMP | when the session started (never NULL) |
| `logout_at` | TIMESTAMP | when the session ended; **NULL** means the session is still open |
Write a **PostgreSQL** query that, for every **closed** session (a session where `logout_at IS NOT NULL`), returns its duration in two forms:
- `session_id`
- `user_id`
- `hhmmss_duration` — the elapsed time formatted as a `HH:MI:SS` text string (e.g. `00:20:40`)
- `duration_seconds` — the same elapsed time as a whole number of seconds (INT)
Exclude sessions that are still open (`logout_at IS NULL`). Sort the result by `session_id` ascending.
**Follow-up (describe, don't code):** how would the query change if open sessions should instead be treated as ending *now* rather than excluded?
Tables
sessions(session_id INT, user_id INT, login_at TIMESTAMP, logout_at TIMESTAMP)
Hints
- Subtracting two TIMESTAMP columns (`logout_at - login_at`) gives an interval — Postgres's answer to MySQL's TIMEDIFF.
- `TO_CHAR(interval, 'HH24:MI:SS')` formats the interval; `EXTRACT(EPOCH FROM interval)::int` gives total seconds (replacing TIME_TO_SEC).