The SQL 50 study guide
A 7-day plan for LeetCode’s SQL 50: learn the handful of ideas that keep coming up, solve each section with hints that arrive one at a time, and check yourself against tested solutions in MySQL and PostgreSQL. Progress, notes and your own solutions are saved in your browser.
Read SQL in the order the database runs it
You write SELECT first, but it runs fifth. Most trial and error comes from not knowing this. Step through one query and watch the table change at each clause.
7-day SQL 50 study plan
About two hours a day. Each day: learn one idea here, then solve that LeetCode section. Before writing SQL, answer the five decode questions in a comment. Pick your dialect at the top; every snippet and solution follows it.
- Day 1Learn: Execution order, Decode a question. Solve: Select 0 of 5 solved
Step through the execution order twice, learn the five decode questions, then solve these five using only WHERE.
- Day 2Learn: Which join. Solve: Basic Joins 0 of 9 solved
Try every join type and filter in the playground first. The WHERE-vs-ON trap decides half of these.
- Day 3Learn: Toolbox, MySQL vs PostgreSQL. Solve: Basic Aggregate Functions 0 of 8 solved
Conditional aggregation and rates. In PostgreSQL, watch integer division and ROUND on floats.
- Day 4Learn: WHERE vs HAVING, Phrase finder. Solve: Sorting and Grouping 0 of 7 solved
Every problem here is a grain question. Write the output row in a comment before typing.
- Day 5Learn: Window functions. Solve: Advanced Select and Joins 0 of 7 solved
LAG, LEAD and running sums. Play with every function in the window playground first.
- Day 6Learn: Subquery, CTE or window. Solve: Subqueries 0 of 7 solved
Two-pass problems. Compute something in a CTE, then filter or aggregate it.
- Day 7Learn: MySQL vs PostgreSQL, Quiz. Solve: Advanced String Functions, Regex, Clause 0 of 7 solved
String and regex syntax differs most between dialects. Finish with the quiz.
- Week 2Review: open All 50 problems and pick the Review filter. It lists problems marked Revisit and anything you solved over a week ago. Re-solve them without hints.
Decode a question before you type
Answer these five on paper. The query then almost writes itself, in the same order the database runs it.
-
What does one output row represent?Read the expected output, not the story. One row per user? Per product? Per (month, country)? This is the grain, and it decides your GROUP BY.
-
What is one row of each input table?If the input is finer than the output (many visits per customer), you need GROUP BY. If it matches, you probably don’t.
-
Which tables, and must unmatched rows survive?Columns in two tables means a join. Rows with no match must still appear (“0 if none”, “never did”) means LEFT JOIN. Otherwise INNER.
-
Filter rows or filter groups?Condition on a raw column goes in WHERE. Condition on a count, sum or average goes in HAVING. If a condition affects only a numerator, use CASE inside the aggregate instead of WHERE.
-
Does a row need another row’s value?Previous day, rank in group, running total, first per user. That means a window function, or a self join.
Practice decodes
Pick a problem, answer all five yourself, then reveal one at a time.
Which join, on what
Join on the column that means the same thing in both tables. Then ask one question: must rows without a match still appear?
INNER JOIN
Only pairs that match. Use when an unmatched row is meaningless for the answer.
LEFT JOIN
Every row of the left table, NULLs where no match. Use for
“including those with none”, “0 if none”, or with
IS NULL for “never did”.
Self join
The same table twice with two aliases. Use for manager–employee, today vs yesterday, start vs end rows.
CROSS JOIN
Every combination. Use when the output needs all pairs even if no data exists, like every student × every subject (1280).
Toolbox: what to use when
Four tools cover most of SQL 50. Each has one job.
WHERE vs HAVING
WHERE removes rows before grouping. HAVING removes groups after. If the condition contains COUNT, SUM, AVG, MIN or MAX, it goes in HAVING.
SELECT class
FROM courses
WHERE student IS NOT NULL -- rows
GROUP BY class
HAVING COUNT(*) >= 5; -- groups
CASE WHEN
Use it to label rows, or inside an aggregate to count only some rows while keeping the full denominator. That second use is called conditional aggregation.
SELECT month, COUNT(*) AS trans_count, SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END) AS approved_count, SUM(state = 'approved') AS same_in_mysql FROM t GROUP BY month;SELECT month, COUNT(*) AS trans_count, SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END) AS approved_count, COUNT(*) FILTER (WHERE state = 'approved') AS same_in_pg FROM t GROUP BY month;
Percentages and rates
In MySQL a true condition equals 1, so the average of a
condition is the share of rows where it is true.In PostgreSQL a condition is a boolean; cast it with
::int to get 1 or 0, and its average is the share
of rows where it is true.
Multiply by 100 for a percentage.
SELECT user_id, ROUND(IFNULL(AVG(action = 'confirmed'), 0), 2) AS confirmation_rate FROM signups s LEFT JOIN confirmations c USING (user_id) GROUP BY user_id;SELECT user_id, ROUND(COALESCE(AVG((action = 'confirmed')::int), 0), 2) AS confirmation_rate FROM signups s LEFT JOIN confirmations c USING (user_id) GROUP BY user_id;
“First” per group
Find the minimum per group, then keep rows matching that pair. Or number rows with ROW_NUMBER and keep row 1.
SELECT *
FROM sales
WHERE (product_id, year) IN (
SELECT product_id, MIN(year)
FROM sales GROUP BY product_id
);
Subquery, CTE or window function?
| Tool | Use when | Example |
|---|---|---|
| Scalar subquery | You need one number from elsewhere, like a total for a percentage. |
COUNT(*) * 100.0 / (SELECT
COUNT(*) FROM users)
|
| IN / NOT IN subquery | Keep rows whose key is (or isn’t) in another set. Watch for NULLs in NOT IN lists. | 1978: manager not in employees |
| CTE (WITH) | You need two passes: compute something, then filter or aggregate it. Also to filter on a window function. | Rank in a CTE, then WHERE rnk <= 3 |
| Window function | Each row needs a group value but you must keep every row: rank, previous row, running total, count per group. | 185, 180, 1204, 1321 |
| UNION ALL | Stack results with the same columns, or turn two columns into one, or force rows that may be missing. | 602, 1907, 1341 |
Output polish (where easy marks are lost)
NULL to zero. IFNULL(x, 0)COALESCE(x, 0) after a LEFT JOIN.
COUNT(col) skips NULLs,
COUNT(*) doesn’t.
Rounding. ROUND(x, 2)ROUND(x::numeric, 2) on the final
value, not on parts of it.
ROUND with a precision does not accept a float in
PostgreSQL.
Dates.
DATEDIFF(a, b) = 1 for “next day”.
DATE_FORMAT(d, '%Y-%m') for month.a = b + 1 for “next day”.
TO_CHAR(d, 'YYYY-MM') for month.
BETWEEN includes both ends.
Window functions: GROUP BY that keeps every row
GROUP BY squashes rows into one per group. OVER computes a group value but leaves every row in place. PARTITION BY says what the group is. ORDER BY inside OVER says the order within it.
Phrase finder
Questions repeat the same phrases. Each phrase points at a tool. Tap one.
MySQL vs PostgreSQL for SQL 50
LeetCode accepts both. Most of SQL 50 is the same in each; these are the places where a correct idea fails on syntax or gives a different number.
| Task | MySQL | PostgreSQL |
|---|---|---|
| NULL to 0 | IFNULL(x, 0) |
COALESCE(x, 0) (works in MySQL too) |
| Share of rows where true | AVG(cond), SUM(cond) |
AVG((cond)::int),
COUNT(*) FILTER (WHERE cond)
|
| Divide two integers | 7 / 2 is 3.5000 |
7 / 2 is 3. Write 7.0 / 2 or
x::numeric / y
|
| Round a float column | ROUND(x, 2) |
ROUND(x::numeric, 2);
ROUND(float, 2) is an error
|
| Year-month label | DATE_FORMAT(d, '%Y-%m') |
TO_CHAR(d, 'YYYY-MM') |
| Next / previous day |
DATEDIFF(a, b) = 1,
DATE_SUB(d, INTERVAL 1 DAY)
|
a = b + 1, d - 1 (date ± integer
counts days)
|
| Join strings in a group | GROUP_CONCAT(x ORDER BY x SEPARATOR ',') |
STRING_AGG(x, ',' ORDER BY x) |
| Regex match |
REGEXP_LIKE(s, p, 'c'); plain REGEXP ignores
case
|
s ~ p (case-sensitive),
s ~* p (ignores case)
|
| LIKE and case | Ignores case (default collation) | Case-sensitive; ILIKE ignores case |
| String length | CHAR_LENGTH (LENGTH counts bytes) |
LENGTH = CHAR_LENGTH (characters)
|
| Delete with a join | DELETE a FROM t a JOIN t b ON … |
DELETE FROM t a USING t b WHERE … |
| NULL-safe equals | a <=> b |
a IS NOT DISTINCT FROM b |
Same in both: LIMIT … OFFSET, CTEs, window functions,
tuple IN like (a, b) IN (SELECT …),
CASE, % for remainder.
Check yourself
Eight scenarios. Pick the approach, then read why.
All 50 SQL problems
Set a status as you go. Each problem has its own page with hints one level at a time, a tested solution, and space for your notes and your own query. Everything is saved in this browser; export it to keep a backup or move it to another device.
Nothing here yet.
Select Day 1 · 0 of 5 solved
- 1757Recyclable and Low Fat ProductsEasyLeetCode ↗
- 584Find Customer RefereeEasyLeetCode ↗
- 595Big CountriesEasyLeetCode ↗
- 1148Article Views IEasyLeetCode ↗
- 1683Invalid TweetsEasyLeetCode ↗
Basic Joins Day 2 · 0 of 9 solved
- 1378Replace Employee ID With The Unique IdentifierEasyLeetCode ↗
- 1068Product Sales Analysis IEasyLeetCode ↗
- 1581Customer Who Visited but Did Not Make Any TransactionsEasyLeetCode ↗
- 197Rising TemperatureEasyLeetCode ↗
- 1661Average Time of Process per MachineEasyLeetCode ↗
- 577Employee BonusEasyLeetCode ↗
- 1280Students and ExaminationsEasyLeetCode ↗
- 570Managers with at Least 5 Direct ReportsMediumLeetCode ↗
- 1934Confirmation RateMediumLeetCode ↗
Basic Aggregate Functions Day 3 · 0 of 8 solved
- 620Not Boring MoviesEasyLeetCode ↗
- 1251Average Selling PriceEasyLeetCode ↗
- 1075Project Employees IEasyLeetCode ↗
- 1633Percentage of Users Attended a ContestEasyLeetCode ↗
- 1211Queries Quality and PercentageEasyLeetCode ↗
- 1193Monthly Transactions IMediumLeetCode ↗
- 1174Immediate Food Delivery IIMediumLeetCode ↗
- 550Game Play Analysis IVMediumLeetCode ↗
Sorting and Grouping Day 4 · 0 of 7 solved
- 2356Number of Unique Subjects Taught by Each TeacherEasyLeetCode ↗
- 1141User Activity for the Past 30 Days IEasyLeetCode ↗
- 1070Product Sales Analysis IIIMediumLeetCode ↗
- 596Classes With at Least 5 StudentsEasyLeetCode ↗
- 1729Find Followers CountEasyLeetCode ↗
- 619Biggest Single NumberEasyLeetCode ↗
- 1045Customers Who Bought All ProductsMediumLeetCode ↗
Advanced Select and Joins Day 5 · 0 of 7 solved
- 1731The Number of Employees Which Report to Each EmployeeEasyLeetCode ↗
- 1789Primary Department for Each EmployeeEasyLeetCode ↗
- 610Triangle JudgementEasyLeetCode ↗
- 180Consecutive NumbersMediumLeetCode ↗
- 1164Product Price at a Given DateMediumLeetCode ↗
- 1204Last Person to Fit in the BusMediumLeetCode ↗
- 1907Count Salary CategoriesMediumLeetCode ↗
Subqueries Day 6 · 0 of 7 solved
- 1978Employees Whose Manager Left the CompanyEasyLeetCode ↗
- 626Exchange SeatsMediumLeetCode ↗
- 1341Movie RatingMediumLeetCode ↗
- 1321Restaurant GrowthMediumLeetCode ↗
- 602Friend Requests II: Who Has the Most FriendsMediumLeetCode ↗
- 585Investments in 2016MediumLeetCode ↗
- 185Department Top Three SalariesHardLeetCode ↗
Advanced String Functions, Regex, Clause Day 7 · 0 of 7 solved
- 1667Fix Names in a TableEasyLeetCode ↗
- 1527Patients With a ConditionEasyLeetCode ↗
- 196Delete Duplicate EmailsEasyLeetCode ↗
- 176Second Highest SalaryMediumLeetCode ↗
- 1484Group Sold Products By The DateEasyLeetCode ↗
- 1327List the Products Ordered in a PeriodEasyLeetCode ↗
- 1517Find Users With Valid E-MailsEasyLeetCode ↗