SQL 50, decoded
0 of 50 solved/50

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.

  1. Day 1
    Learn: 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.

  2. Day 2
    Learn: 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.

  3. Day 3
    Learn: 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.

  4. Day 4
    Learn: 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.

  5. Day 5
    Learn: 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.

  6. Day 6
    Learn: Subquery, CTE or window. Solve: Subqueries 0 of 7 solved

    Two-pass problems. Compute something in a CTE, then filter or aggregate it.

  7. Day 7
    Learn: 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.

  8. Week 2
    Review: 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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?

    
              
    employee e
    bonus b
    result

    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.

    Empty result must be NULL. Wrap it: SELECT (subquery) AS x returns NULL instead of no rows (176, 619).

    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.

    Select Day 1 · 0 of 5 solved

    Basic Joins Day 2 · 0 of 9 solved

    Basic Aggregate Functions Day 3 · 0 of 8 solved

    Sorting and Grouping Day 4 · 0 of 7 solved

    Advanced Select and Joins Day 5 · 0 of 7 solved

    Subqueries Day 6 · 0 of 7 solved

    Advanced String Functions, Regex, Clause Day 7 · 0 of 7 solved