cswork / libraryOpen practice workspace

Coding questions · Amazon

Diagnose MySQL joins and GROUP BY/HAVING errors

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.

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().

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:

columnmeaning
join_typeone of 'CROSS', 'FULL', 'LEFT', 'RIGHT' identifying which join produced the row
a_idA.id for the row (NULL when the row came from B with no A match)
a_val_aA.val_a for the row
b_idB.id for the row (NULL when the row came from A with no B match)
b_val_bB.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).

Table A

columntypeconstraints
idINTPRIMARY KEY, NOT NULL
val_aVARCHAR(10)

Sample data:

idval_a
1x
2y
3z
4NULL

Table B

columntypeconstraints
idINT
val_bVARCHAR(10)

Sample data:

idval_b
2p
3q
3r
5s
NULLt

Expected output

37 rows: 20 CROSS (4x5 Cartesian product), 7 FULL (emulated), 5 LEFT, 5 RIGHT, with columns join_type, a_id, a_val_a, b_id, b_val_b, ordered by join_type, COALESCE(a_id,b_id), b_val_b, a_val_a.

a_ida_val_ab_idb_val_bjoin_type
1x2pCROSS
1x3qCROSS
1x3rCROSS
1x5sCROSS
1xNULLtCROSS
2y2pCROSS
2y3qCROSS
2y3rCROSS
2y5sCROSS
2yNULLtCROSS
3z2pCROSS
3z3qCROSS
3z3rCROSS
3z5sCROSS
3zNULLtCROSS
4NULL2pCROSS
4NULL3qCROSS
4NULL3rCROSS
4NULL5sCROSS
4NULLNULLtCROSS
1xNULLNULLFULL
2y2pFULL
3z3qFULL
3z3rFULL
4NULLNULLNULLFULL
NULLNULL5sFULL
NULLNULLNULLtFULL
1xNULLNULLLEFT
2y2pLEFT
3z3qLEFT
3z3rLEFT
4NULLNULLNULLLEFT
2y2pRIGHT
3z3qRIGHT
3z3rRIGHT
NULLNULL5sRIGHT
NULLNULLNULLtRIGHT

Hints

  1. 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.
  2. Emulate FULL OUTER JOIN as the LEFT JOIN result UNION the RIGHT JOIN rows that have no left match (a_id IS NULL).
  3. PostgreSQL rejects an expression like COALESCE(...) in an ORDER BY placed directly on a UNION chain — wrap the union in a subquery/CTE and apply ORDER BY in the outer SELECT.

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;

Table t

columntypeconstraints
aINT
bVARCHAR(10)

Sample data:

ab
1u
1v
1v
2w
NULLx

Expected output

For each query Q1–Q5, we classify it as either 'executes' or 'errors' under ONLY_FULL_GROUP_BY and give the reason. Q1 is valid and returns a single row (a = 1, COUNT() = 3). Q2 errors because b is neither aggregated nor listed in GROUP BY. Q3 has a syntax error because HAVING appears before GROUP BY. Q4 is valid; the alias table in HAVING refers to COUNT(), and reserved words can be used as column aliases. Q5 is also valid; the backticked alias table behaves the same as in Q4.

detailsquery_namestatus
Valid GROUP BY; returns 1 row where a = 1 and COUNT(*) = 3Q1executes
ONLY_FULL_GROUP_BY violation: nonaggregated column b is selected but not in GROUP BYQ2errors
Syntax error: HAVING clause appears before GROUP BYQ3errors
Valid; alias table in HAVING refers to COUNT(*); returns 1 row where a = 1 and table = 3Q4executes
Valid; backticked alias table in HAVING refers to COUNT(*); returns 1 row where a = 1 and table = 3Q5executes

Hints

  1. ONLY_FULL_GROUP_BY requires every nonaggregated column in the SELECT list to be named in the GROUP BY.
  2. 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.

Table users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
nameVARCHAR(50)NOT NULL

Sample data:

nameuser_id
Ann1
Bob2
Cara3

Table orders

columntypeconstraints
order_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
created_atDATETIMENOT NULL
amount_centsINTNOT NULL

Sample data:

amount_centscreated_atorder_iduser_id
12002025-08-30 10:00101
3002025-08-31 09:30111
45002025-09-01 12:45122

Expected output

All users are listed regardless of whether they have orders. For each user, orders_count is the number of orders and total_spend_dollars is the sum of amount_cents / 100.00 rounded to two decimals. With the sample data, Bob has one order for $45.00, Ann has two orders totaling $15.00, and Cara has none. Sorted by total_spend_dollars DESC then user_id ASC, the result is:

nameorders_counttotal_spend_dollarsuser_id
Bob1452
Ann2151
Cara003

Hints

  1. Use a LEFT JOIN from users to orders so that users with no orders are still returned.
  2. 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.

Table events

columntypeconstraints
event_idINTPRIMARY KEY, NOT NULL
duration_secondsINTNOT NULL

Sample data:

duration_secondsevent_id
59100
61101
90061102
0103
86400104

Expected output

One row per event with the original duration and its 'X days Y hours Z minutes W seconds' decomposition, ordered by event_id.

duration_secondsevent_idhuman_readable
591000 days 0 hours 0 minutes 59 seconds
611010 days 0 hours 1 minutes 1 seconds
900611021 days 1 hours 1 minutes 1 seconds
01030 days 0 hours 0 minutes 0 seconds
864001041 days 0 hours 0 minutes 0 seconds

Hints

  1. There are 86400 seconds in a day, 3600 in an hour, and 60 in a minute.
  2. 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.
  3. Subtract each larger unit before computing the next: take % 86400 to get the remainder after full days, then divide by 3600 for hours, and so on.

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.

columntypenotes
session_idINTprimary key
user_idINTwho the session belongs to
login_atTIMESTAMPwhen the session started (never NULL)
logout_atTIMESTAMPwhen 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?

Table sessions

columntypeconstraints
session_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
login_atTIMESTAMPNOT NULL
logout_atTIMESTAMP

Sample data:

login_atlogout_atsession_iduser_id
2025-08-31 23:50:002025-09-01 00:10:4010001
2025-09-01 09:00:002025-09-01 09:00:0010012
2025-09-01 10:15:00NULL10023
2025-09-01 14:00:002025-09-01 15:30:0510034

Expected output

One row per closed session (logout_at not NULL), with duration as HH:MI:SS text and as integer seconds, ordered by session_id. Session 1000 = 20m40s (1240s), 1001 = 0s, 1003 = 1h30m05s (5405s); session 1002 is excluded (open).

duration_secondshhmmss_durationsession_iduser_id
124000:20:4010001
000:00:0010012
540501:30:0510034

Hints

  1. Subtracting two TIMESTAMP columns (logout_at - login_at) gives an interval — Postgres's answer to MySQL's TIMEDIFF.
  2. TO_CHAR(interval, 'HH24:MI:SS') formats the interval; EXTRACT(EPOCH FROM interval)::int gives total seconds (replacing TIME_TO_SEC).
  3. Filter with WHERE logout_at IS NOT NULL and sort by session_id for a deterministic result.