SQL exercises with solutions: NULL and COALESCE
Updated on
NULL = missing value: test it with IS NULL / IS NOT NULL; COALESCE replaces it. These 14 exercises range from level 4 to level 8; they use the syntax of SQLite, SpeedQL’s SQL engine.
Tip: col = NULL never works: write col IS NULL.
Read the “NULL, COALESCE” card in the cheat sheet
Exercise 1
Show each contact's name and email, replacing missing emails with 'unknown'. Use COALESCE.
| id | name | phone | city | |
|---|---|---|---|---|
| 1 | Alice Martin | alice@mail.com | 0612345678 | Paris |
| 2 | bob durand | NULL | 0698765432 | Lyon |
| 3 | CLAIRE ROUX | claire@work.org | NULL | NULL |
| 4 | David Lefevre | NULL | NULL | Nantes |
| 5 | Emma Petit | emma@mail.com | 0611223344 | NULL |
Show the hint
Topics to use: NULL, COALESCE.
Query structure:
SELECT …, COALESCE(…, …)
FROM …Show the solution
SELECT name, COALESCE(email, 'unknown')
FROM contacts;Expected result (5 rows):
| name | COALESCE(email, 'unknown') |
|---|---|
| Alice Martin | alice@mail.com |
| bob durand | unknown |
| CLAIRE ROUX | claire@work.org |
| David Lefevre | unknown |
| Emma Petit | emma@mail.com |
Exercise 2
Show each contact's name and, in a column called contact, their phone if it exists, otherwise their email, otherwise 'none'. Use COALESCE.
| id | name | phone | city | |
|---|---|---|---|---|
| 1 | Alice Martin | alice@mail.com | 0612345678 | Paris |
| 2 | bob durand | NULL | 0698765432 | Lyon |
| 3 | CLAIRE ROUX | claire@work.org | NULL | NULL |
| 4 | David Lefevre | NULL | NULL | Nantes |
| 5 | Emma Petit | emma@mail.com | 0611223344 | NULL |
Show the hint
Topics to use: NULL, COALESCE.
Query structure:
SELECT …, COALESCE(…, …, …) AS …
FROM …Show the solution
SELECT name, COALESCE(phone, email, 'none') AS contact
FROM contacts;Expected result (5 rows):
| name | contact |
|---|---|
| Alice Martin | 0612345678 |
| bob durand | 0698765432 |
| CLAIRE ROUX | claire@work.org |
| David Lefevre | none |
| Emma Petit | 0611223344 |
Exercise 3
Show the name of the contacts who have no email.
| id | name | phone | city | |
|---|---|---|---|---|
| 1 | Alice Martin | alice@mail.com | 0612345678 | Paris |
| 2 | bob durand | NULL | 0698765432 | Lyon |
| 3 | CLAIRE ROUX | claire@work.org | NULL | NULL |
| 4 | David Lefevre | NULL | NULL | Nantes |
| 5 | Emma Petit | emma@mail.com | 0611223344 | NULL |
Show the hint
Topics to use: WHERE (filters), NULL, COALESCE.
Query structure:
SELECT …
FROM …
WHERE … IS NULLShow the solution
SELECT name
FROM contacts
WHERE email IS NULL;Expected result (2 rows):
| name |
|---|
| bob durand |
| David Lefevre |
Exercise 4
Show the number of contacts whose email, phone, or both are missing.
| id | name | phone | city | |
|---|---|---|---|---|
| 1 | Alice Martin | alice@mail.com | 0612345678 | Paris |
| 2 | bob durand | NULL | 0698765432 | Lyon |
| 3 | CLAIRE ROUX | claire@work.org | NULL | NULL |
| 4 | David Lefevre | NULL | NULL | Nantes |
| 5 | Emma Petit | emma@mail.com | 0611223344 | NULL |
Show the hint
Topics to use: WHERE (filters), COUNT, SUM, AVG, MIN, MAX, NULL, COALESCE.
Query structure:
SELECT COUNT(*)
FROM …
WHERE … IS NULL OR … IS NULLShow the solution
SELECT COUNT(*)
FROM contacts
WHERE email IS NULL OR phone IS NULL;Expected result (1 row):
| COUNT(*) |
|---|
| 3 |
Exercise 5
For the contacts who have an email, show the email and its domain (the part after the @).
| id | name | phone | city | |
|---|---|---|---|---|
| 1 | Alice Martin | alice@mail.com | 0612345678 | Paris |
| 2 | bob durand | NULL | 0698765432 | Lyon |
| 3 | CLAIRE ROUX | claire@work.org | NULL | NULL |
| 4 | David Lefevre | NULL | NULL | Nantes |
| 5 | Emma Petit | emma@mail.com | 0611223344 | NULL |
Show the hint
Topics to use: WHERE (filters), NULL, COALESCE, Text and dates.
Query structure:
SELECT …, SUBSTR(…, INSTR(…, …) + …)
FROM …
WHERE … IS NOT NULLShow the solution
SELECT email, SUBSTR(email, INSTR(email, '@') + 1)
FROM contacts
WHERE email IS NOT NULL;Expected result (3 rows):
| SUBSTR(email, INSTR(email, '@') + 1) | |
|---|---|
| alice@mail.com | mail.com |
| claire@work.org | work.org |
| emma@mail.com | mail.com |
Exercise 6
Show the title of each movie and its director's name, with 'unknown' instead of the name when the director is unknown. Use COALESCE.
| id | title | genre | year | duration | rating | director_id |
|---|---|---|---|---|---|---|
| 1 | Night Train | Thriller | 2015 | 118 | 7.8 | 1 |
| 2 | Blue Harbor | Drama | 2018 | 102 | 7.1 | 2 |
| 3 | Paper Moon City | Comedy | 2012 | 95 | 6.4 | 5 |
| 4 | Silent Peak | Drama | 2020 | 131 | 8.2 | 3 |
| 5 | Last Signal | Sci-Fi | 2019 | 142 | 7.5 | 1 |
| 6 | Summer Keys | Comedy | 2016 | 88 | 5.9 | 4 |
| 7 | Iron Garden | Sci-Fi | 2021 | 125 | 8 | 3 |
| 8 | Dust and Gold | Western | 2014 | 110 | 6.8 | 2 |
| 9 | The Quiet Hour | Drama | 2022 | 97 | 7.4 | 5 |
| 10 | Deep Current | Thriller | 2017 | 105 | 6.9 | NULL |
| id | name | country |
|---|---|---|
| 1 | Nora Ellis | UK |
| 2 | Paulo Reis | Brazil |
| 3 | Kenji Mori | Japan |
| 4 | Anna Berg | Sweden |
| 5 | Luc Martin | France |
| 6 | Sara Diaz | Spain |
Show the hint
Topics to use: JOIN, NULL, COALESCE.
Query structure:
SELECT …, COALESCE(…, …)
FROM … …
LEFT JOIN … … ON … = …Show the solution
SELECT m.title, COALESCE(d.name, 'unknown')
FROM movies m
LEFT JOIN directors d ON d.id = m.director_id;Expected result (10 rows):
| title | COALESCE(d.name, 'unknown') |
|---|---|
| Night Train | Nora Ellis |
| Blue Harbor | Paulo Reis |
| Paper Moon City | Luc Martin |
| Silent Peak | Kenji Mori |
| Last Signal | Nora Ellis |
| Summer Keys | Anna Berg |
| Iron Garden | Kenji Mori |
| Dust and Gold | Paulo Reis |
| The Quiet Hour | Luc Martin |
| Deep Current | unknown |
Exercise 7
For each returned loan, show the loan id and the number of days between the loan and the return.
| id | book_id | member_id | loan_date | return_date |
|---|---|---|---|---|
| 1 | 1 | 1 | 2025-01-05 | 2025-01-19 |
| 2 | 3 | 2 | 2025-01-10 | 2025-02-02 |
| 3 | 5 | 1 | 2025-02-01 | 2025-02-10 |
| 4 | 7 | 3 | 2025-02-03 | NULL |
| 5 | 3 | 4 | 2025-02-15 | 2025-03-01 |
| 6 | 2 | 2 | 2025-03-02 | 2025-03-30 |
| 7 | 8 | 5 | 2025-03-05 | NULL |
| 8 | 1 | 3 | 2025-03-10 | 2025-03-18 |
| 9 | 6 | 1 | 2025-03-20 | 2025-04-15 |
| 10 | 3 | 5 | 2025-04-01 | NULL |
| 11 | 9 | 4 | 2025-04-05 | 2025-04-12 |
| 12 | 10 | 2 | 2025-04-08 | 2025-04-20 |
Show the hint
Topics to use: WHERE (filters), NULL, COALESCE, Text and dates.
Query structure:
SELECT …, CAST(JULIANDAY(…) - JULIANDAY(…) AS INTEGER)
FROM …
WHERE … IS NOT NULLShow the solution
SELECT id, CAST(julianday(return_date) - julianday(loan_date) AS INTEGER)
FROM loans
WHERE return_date IS NOT NULL;Expected result (9 rows):
| id | CAST(julianday(return_date) - julianday(loan_date) AS INTEGER) |
|---|---|
| 1 | 14 |
| 2 | 23 |
| 3 | 9 |
| 5 | 14 |
| 6 | 28 |
| 8 | 8 |
| 9 | 26 |
| 11 | 7 |
| 12 | 12 |
Exercise 8
Show the title of the books currently on loan (loans with no return date).
| id | title | author_id | genre | pages | year |
|---|---|---|---|---|---|
| 1 | Cold River | 1 | Novel | 320 | 2011 |
| 2 | Salt Roads | 2 | Travel | 210 | 2016 |
| 3 | The Glass Hive | 3 | Sci-Fi | 412 | 2019 |
| 4 | Winter Ledger | 1 | Crime | 288 | 2014 |
| 5 | Desert Letters | 2 | Novel | 356 | 2020 |
| 6 | Small Engines | 4 | Sci-Fi | 198 | 2022 |
| 7 | Harbor Lights | 5 | Novel | 445 | 2008 |
| 8 | Night Garden | 3 | Crime | 301 | 2017 |
| 9 | Paper Birds | 5 | Poetry | 96 | 2012 |
| 10 | Open Maps | 4 | Travel | 240 | 2021 |
| id | book_id | member_id | loan_date | return_date |
|---|---|---|---|---|
| 1 | 1 | 1 | 2025-01-05 | 2025-01-19 |
| 2 | 3 | 2 | 2025-01-10 | 2025-02-02 |
| 3 | 5 | 1 | 2025-02-01 | 2025-02-10 |
| 4 | 7 | 3 | 2025-02-03 | NULL |
| 5 | 3 | 4 | 2025-02-15 | 2025-03-01 |
| 6 | 2 | 2 | 2025-03-02 | 2025-03-30 |
| 7 | 8 | 5 | 2025-03-05 | NULL |
| 8 | 1 | 3 | 2025-03-10 | 2025-03-18 |
| 9 | 6 | 1 | 2025-03-20 | 2025-04-15 |
| 10 | 3 | 5 | 2025-04-01 | NULL |
| 11 | 9 | 4 | 2025-04-05 | 2025-04-12 |
| 12 | 10 | 2 | 2025-04-08 | 2025-04-20 |
Show the hint
Topics to use: WHERE (filters), JOIN, NULL, COALESCE.
Query structure:
SELECT …
FROM … …
JOIN … … ON … = …
WHERE … IS NULLShow the solution
SELECT b.title
FROM books b
JOIN loans l ON l.book_id = b.id
WHERE l.return_date IS NULL;Expected result (3 rows):
| title |
|---|
| Harbor Lights |
| Night Garden |
| The Glass Hive |
Exercise 9
Using an outer join, show the name and price of the products that have never been ordered.
| id | name | category | price |
|---|---|---|---|
| 1 | Desk Lamp | Home | 35 |
| 2 | Coffee Mug | Kitchen | 12 |
| 3 | Notebook | Office | 6 |
| 4 | Office Chair | Office | 149 |
| 5 | Kettle | Kitchen | 45 |
| 6 | Cushion | Home | 22 |
| 7 | Stapler | Office | 9 |
| order_id | product_id | qty |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 2 | 2 |
| 2 | 4 | 1 |
| 3 | 3 | 5 |
| 3 | 2 | 1 |
| 4 | 5 | 1 |
| 5 | 6 | 2 |
| 5 | 1 | 1 |
| 6 | 2 | 4 |
| 6 | 3 | 3 |
| 7 | 4 | 1 |
| 7 | 6 | 1 |
| 8 | 5 | 2 |
| 8 | 2 | 1 |
Show the hint
Topics to use: WHERE (filters), JOIN, NULL, COALESCE.
Query structure:
SELECT …, …
FROM … …
LEFT JOIN … … ON … = …
WHERE … IS NULLShow the solution
SELECT p.name, p.price
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
WHERE oi.product_id IS NULL;Expected result (1 row):
| name | price |
|---|---|
| Stapler | 9 |
Exercise 10
Using COALESCE, show the title of each task and its end date, or 'pending' if it is not finished (done_on empty).
| id | project_id | dev_id | title | hours | status | done_on |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | Landing page | 12 | done | 2025-03-10 |
| 2 | 1 | 3 | Data model | 20 | done | 2025-03-20 |
| 3 | 1 | 2 | Login | 8 | doing | NULL |
| 4 | 2 | 4 | ETL job | 16 | done | 2025-04-02 |
| 5 | 2 | 5 | CI pipeline | 6 | done | 2025-03-15 |
| 6 | 3 | 1 | Dashboard | 14 | todo | NULL |
| 7 | 3 | 3 | Report | 10 | done | 2025-04-25 |
| 8 | 4 | 2 | API | 18 | doing | NULL |
| 9 | 4 | 5 | Monitoring | 9 | todo | NULL |
| 10 | 4 | 4 | Forecast | 22 | done | 2025-05-05 |
Show the hint
Topics to use: NULL, COALESCE.
Query structure:
SELECT …, COALESCE(…, …)
FROM …Show the solution
SELECT title, COALESCE(done_on, 'pending')
FROM tasks;Expected result (10 rows):
| title | COALESCE(done_on, 'pending') |
|---|---|
| Landing page | 2025-03-10 |
| Data model | 2025-03-20 |
| Login | pending |
| ETL job | 2025-04-02 |
| CI pipeline | 2025-03-15 |
| Dashboard | pending |
| Report | 2025-04-25 |
| API | pending |
| Monitoring | pending |
| Forecast | 2025-05-05 |
Exercise 11
For each finished task, show its title, the project name and the number of days between its end date and the project's deadline (deadline − done_on).
| id | project_id | dev_id | title | hours | status | done_on |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | Landing page | 12 | done | 2025-03-10 |
| 2 | 1 | 3 | Data model | 20 | done | 2025-03-20 |
| 3 | 1 | 2 | Login | 8 | doing | NULL |
| 4 | 2 | 4 | ETL job | 16 | done | 2025-04-02 |
| 5 | 2 | 5 | CI pipeline | 6 | done | 2025-03-15 |
| 6 | 3 | 1 | Dashboard | 14 | todo | NULL |
| 7 | 3 | 3 | Report | 10 | done | 2025-04-25 |
| 8 | 4 | 2 | API | 18 | doing | NULL |
| 9 | 4 | 5 | Monitoring | 9 | todo | NULL |
| 10 | 4 | 4 | Forecast | 22 | done | 2025-05-05 |
| id | name | client | budget | deadline |
|---|---|---|---|---|
| 1 | Atlas | Acme | 20000 | 2025-06-30 |
| 2 | Beacon | Bolt | 12000 | 2025-05-15 |
| 3 | Comet | Acme | 8000 | 2025-04-30 |
| 4 | Delta | Cyan | 15000 | 2025-07-31 |
Show the hint
Topics to use: WHERE (filters), JOIN, NULL, COALESCE, Text and dates.
Query structure:
SELECT …, …, CAST(JULIANDAY(…) - JULIANDAY(…) AS INTEGER)
FROM … …
JOIN … … ON … = …
WHERE … IS NOT NULLShow the solution
SELECT t.title, p.name, CAST(julianday(p.deadline) - julianday(t.done_on) AS INTEGER)
FROM tasks t
JOIN projects p ON p.id = t.project_id
WHERE t.done_on IS NOT NULL;Expected result (6 rows):
| title | name | CAST(julianday(p.deadline) - julianday(t.done_on) AS INTEGER) |
|---|---|---|
| Landing page | Atlas | 112 |
| Data model | Atlas | 102 |
| ETL job | Beacon | 43 |
| CI pipeline | Beacon | 61 |
| Report | Comet | 5 |
| Forecast | Delta | 87 |
Exercise 12
For each invoice, show its id and its number of days late: days between the due date and the payment date (or 2025-04-15 if it is unpaid), and 0 if it is not late. Use COALESCE.
| id | client | issued | due | amount | paid_on |
|---|---|---|---|---|---|
| 1 | Acme | 2025-01-15 | 2025-02-14 | 1200 | 2025-02-10 |
| 2 | Acme | 2025-03-01 | 2025-03-16 | 800 | NULL |
| 3 | Bolt | 2025-01-20 | 2025-03-06 | 450 | 2025-03-01 |
| 4 | Bolt | 2025-02-25 | 2025-03-27 | 950 | 2025-04-02 |
| 5 | Cyan | 2025-03-05 | 2025-05-04 | 300 | NULL |
| 6 | Cyan | 2024-12-10 | 2024-12-30 | 600 | 2025-01-05 |
Show the hint
Topics to use: NULL, COALESCE, Text and dates.
Query structure:
SELECT …, MAX(…, CAST(JULIANDAY(COALESCE(…, …)) - JULIANDAY(…) AS INTEGER))
FROM …Show the solution
SELECT id, MAX(0, CAST(julianday(COALESCE(paid_on, '2025-04-15')) - julianday(due) AS INTEGER))
FROM invoices;Expected result (6 rows):
| id | MAX(0, CAST(julianday(COALESCE(paid_on, '2025-04-15')) - julianday(due) AS INTEGER)) |
|---|---|
| 1 | 0 |
| 2 | 30 |
| 3 | 0 |
| 4 | 6 |
| 5 | 0 |
| 6 | 6 |
Exercise 13
Show the clients who have at least one unpaid invoice, except those who have already paid an invoice late (payment after the due date).
| id | client | issued | due | amount | paid_on |
|---|---|---|---|---|---|
| 1 | Acme | 2025-01-15 | 2025-02-14 | 1200 | 2025-02-10 |
| 2 | Acme | 2025-03-01 | 2025-03-16 | 800 | NULL |
| 3 | Bolt | 2025-01-20 | 2025-03-06 | 450 | 2025-03-01 |
| 4 | Bolt | 2025-02-25 | 2025-03-27 | 950 | 2025-04-02 |
| 5 | Cyan | 2025-03-05 | 2025-05-04 | 300 | NULL |
| 6 | Cyan | 2024-12-10 | 2024-12-30 | 600 | 2025-01-05 |
Show the hint
Topics to use: WHERE (filters), NULL, COALESCE, UNION, INTERSECT, EXCEPT.
Query structure:
SELECT …
FROM …
WHERE … IS NULL
EXCEPT SELECT …
FROM …
WHERE … > …Show the solution
SELECT client
FROM invoices
WHERE paid_on IS NULL
EXCEPT SELECT client
FROM invoices
WHERE paid_on > due;Expected result (1 row):
| client |
|---|
| Acme |
Exercise 14
For each project with at least 2 finished tasks, show its name, the first and the last end date, and the number of days between them.
| id | name | client | budget | deadline |
|---|---|---|---|---|
| 1 | Atlas | Acme | 20000 | 2025-06-30 |
| 2 | Beacon | Bolt | 12000 | 2025-05-15 |
| 3 | Comet | Acme | 8000 | 2025-04-30 |
| 4 | Delta | Cyan | 15000 | 2025-07-31 |
| id | project_id | dev_id | title | hours | status | done_on |
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | Landing page | 12 | done | 2025-03-10 |
| 2 | 1 | 3 | Data model | 20 | done | 2025-03-20 |
| 3 | 1 | 2 | Login | 8 | doing | NULL |
| 4 | 2 | 4 | ETL job | 16 | done | 2025-04-02 |
| 5 | 2 | 5 | CI pipeline | 6 | done | 2025-03-15 |
| 6 | 3 | 1 | Dashboard | 14 | todo | NULL |
| 7 | 3 | 3 | Report | 10 | done | 2025-04-25 |
| 8 | 4 | 2 | API | 18 | doing | NULL |
| 9 | 4 | 5 | Monitoring | 9 | todo | NULL |
| 10 | 4 | 4 | Forecast | 22 | done | 2025-05-05 |
Show the hint
Topics to use: WHERE (filters), COUNT, SUM, AVG, MIN, MAX, GROUP BY, JOIN, HAVING, NULL, COALESCE, Text and dates.
Query structure:
SELECT …, MIN(…), MAX(…), CAST(JULIANDAY(MAX(…)) - JULIANDAY(MIN(…)) AS INTEGER)
FROM … …
JOIN … … ON … = …
WHERE … IS NOT NULL
GROUP BY …
HAVING COUNT(*) >= …Show the solution
SELECT p.name, MIN(t.done_on), MAX(t.done_on), CAST(julianday(MAX(t.done_on)) - julianday(MIN(t.done_on)) AS INTEGER)
FROM projects p
JOIN tasks t ON t.project_id = p.id
WHERE t.done_on IS NOT NULL
GROUP BY p.id
HAVING COUNT(*) >= 2;Expected result (2 rows):
| name | MIN(t.done_on) | MAX(t.done_on) | CAST(julianday(MAX(t.done_on)) - julianday(MIN(t.done_on)) AS INTEGER) |
|---|---|---|---|
| Atlas | 2025-03-10 | 2025-03-20 | 10 |
| Beacon | 2025-03-15 | 2025-04-02 | 18 |
Practice with automatic checking
In SpeedQL, you write your query and it is checked straight away, on these tables and then on a hidden dataset.