4.4.4 · D4Databases

Exercises — SQL DML — SELECT, INSERT, UPDATE, DELETE

2,900 words13 min readBack to topic

The starting table

Every exercise begins from this exact state (unless it says otherwise). Picture it as a 4-row spreadsheet you talk to with sentences.

CREATE TABLE employees (
  id     INT PRIMARY KEY,
  name   VARCHAR(50),
  dept   VARCHAR(20),
  salary INT
);
 
INSERT INTO employees (id, name, dept, salary) VALUES
  (1, 'Asha', 'Sales', 50000),
  (2, 'Ben',  'Eng',   70000),
  (3, 'Cira', 'Eng',   90000),
  (4, 'Dia',  'Sales', 40000);
Figure — SQL DML — SELECT, INSERT, UPDATE, DELETE

Level 1 — Recognition

Exercise 1.1

Which of the four DML verbs would you use to read the names of everyone in Sales without changing anything?

Recall Solution

==SELECT==. It is the only read-only verb — it looks, it never writes.

SELECT name FROM employees WHERE dept = 'Sales';

What this does: FROM grabs the table, WHERE dept = 'Sales' keeps only Sales rows (Asha, Dia), SELECT name picks the name column → returns Asha, Dia.

Exercise 1.2

You accidentally typed:

UPDATE employees SET salary = 0;

How many rows does this change, and why?

Recall Solution

All 4 rows. There is no WHERE, so the predicate is effectively "always true" — every row matches. That is the "No WHERE → everywhere" rule from the parent note. All salaries become 0.

Exercise 1.3

Match each verb to its job: INSERT, DELETE, UPDATE, SELECTadd a row, remove a row, change a value in a row, read rows.

Recall Solution
  • INSERT → add a row
  • DELETE → remove a row
  • UPDATE → change a value in a row
  • SELECT → read rows

Mnemonic from the parent: Select, Insert, Update, Delete.


Level 2 — Application

Exercise 2.1

Add a new employee: id 5, name Eli, dept Eng, salary 65000. Write the statement so it survives a future schema change (someone might add a column later).

Recall Solution

Name the columns explicitly (order-independent, future-proof):

INSERT INTO employees (id, name, dept, salary)
VALUES (5, 'Eli', 'Eng', 65000);

Why name columns: if a hire_date column is added later, INSERT INTO employees VALUES (...) (no names) would break because the value count no longer matches. Naming columns keeps this statement valid.

Exercise 2.2

From the starting table, return the name and salary of every Eng employee earning more than 60000, highest salary first.

Recall Solution
SELECT name, salary
FROM employees
WHERE dept = 'Eng' AND salary > 60000
ORDER BY salary DESC;

Walkthrough: Eng rows are Ben (70000) and Cira (90000); both clear 60000. ORDER BY salary DESC sorts high→low → Cira 90000, Ben 70000.

Exercise 2.3

Give everyone in Sales a flat 5000 raise. Write the UPDATE, and state the two Sales salaries after it.

Recall Solution
UPDATE employees
SET salary = salary + 5000
WHERE dept = 'Sales';

What the right-hand salary means: the row's current value before this update. Asha 50000 → 55000, Dia 40000 → 45000. Eng rows untouched (they fail the WHERE).


Level 3 — Analysis

Exercise 3.1

From the starting table, compute the average salary and headcount per department, but only show departments with 2 or more people.

Recall Solution
SELECT dept, AVG(salary) AS avg_pay, COUNT(*) AS n
FROM employees
GROUP BY dept
HAVING COUNT(*) >= 2;

Bucketing logic: GROUP BY dept makes two buckets — Sales {50000, 40000} and Eng {70000, 90000}. Aggregates summarise each bucket: Sales avg , n ; Eng avg , n . HAVING COUNT(*) >= 2 filters buckets — both pass. Result: Sales 45000 (n=2), Eng 80000 (n=2).

Exercise 3.2

Why does this query fail, and how do you fix it?

SELECT dept, AVG(salary) AS avg_pay
FROM employees
WHERE avg_pay > 46000
GROUP BY dept;
Recall Solution

It fails because WHERE is evaluated before SELECT and before grouping, so the alias avg_pay does not exist yet — and you cannot filter on an aggregate in WHERE at all (there are no groups yet). Recall the execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Fix: filter groups with HAVING:

SELECT dept, AVG(salary) AS avg_pay
FROM employees
GROUP BY dept
HAVING AVG(salary) > 46000;

Now only Eng (avg 80000) survives; Sales (45000) is dropped.

Exercise 3.3

Starting from the table, run these two statements in this order, then predict the final rows:

UPDATE employees SET salary = salary + 5000 WHERE dept = 'Sales';
DELETE FROM employees WHERE salary < 60000;
Recall Solution

Step 1 (UPDATE runs first): Asha 50000 → 55000, Dia 40000 → 45000. Table now: Asha 55000, Ben 70000, Cira 90000, Dia 45000. Step 2 (DELETE sees the new values): salary < 60000 matches Asha (55000) and Dia (45000) → both removed. Final rows: Ben 70000, Cira 90000. The key insight: the DELETE filter operates on the updated salaries, not the originals — order of statements changes the outcome.


Level 4 — Synthesis

Exercise 4.1

Goal: transfer everyone from Sales into a new dept Retail, then insert a new Sales hire so Sales is not empty. Achieve it and list the (dept, count) per department afterward. Start from the starting table.

Recall Solution
UPDATE employees SET dept = 'Retail' WHERE dept = 'Sales';
INSERT INTO employees (id, name, dept, salary)
VALUES (6, 'Faye', 'Sales', 48000);

Trace: Asha and Dia move Sales→Retail (2 rows). Then Faye added to Sales (1 row). Counts per dept:

  • Eng: Ben, Cira → 2
  • Retail: Asha, Dia → 2
  • Sales: Faye → 1

Total rows = 5.

Exercise 4.2

Give a 10% raise to every Eng employee, then report each Eng person's new salary sorted low→high. Start from the starting table.

Recall Solution
UPDATE employees SET salary = salary * 1.10 WHERE dept = 'Eng';
 
SELECT name, salary FROM employees
WHERE dept = 'Eng'
ORDER BY salary ASC;

Compute the raises: Ben ; Cira . Sorted low→high: Ben 77000, Cira 99000. (Note: salary is an INT; and are whole numbers, so no rounding surprise here.)

Exercise 4.3

Safe-edit drill. You must halve the salary of employee id = 3 only, but you're terrified of the no-WHERE disaster. Write the full preview-then-commit ritual.

Recall Solution
BEGIN;                                   -- open a transaction (undoable)
 
-- 1. Preview the EXACT rows the WHERE will hit:
SELECT id, name, salary FROM employees WHERE id = 3;   -- Cira, 90000
 
-- 2. Do the change with the SAME where:
UPDATE employees SET salary = salary / 2 WHERE id = 3; -- 90000 -> 45000
 
-- 3. Re-check:
SELECT id, name, salary FROM employees WHERE id = 3;   -- Cira, 45000
 
COMMIT;                                  -- lock it in; or ROLLBACK; to undo

Why the ritual works: BEGIN starts a transaction so nothing is permanent until COMMIT. Previewing with the same WHERE guarantees you change exactly the rows you inspected. If the preview showed 4 rows instead of 1, you'd catch a missing/wrong WHERE before committing. Cira: .


Level 5 — Mastery

Exercise 5.1 — the NULL trap

Insert an intern with an unknown salary, then try to select "everyone earning less than 60000". Does the intern appear? Start from the starting table.

INSERT INTO employees (id, name, dept, salary) VALUES (7, 'Gus', 'Eng', NULL);
SELECT name FROM employees WHERE salary < 60000;
Recall Solution

Gus does NOT appear. Any comparison with NULL (which means unknown) yields UNKNOWN, never TRUE. WHERE keeps a row only when the predicate is TRUE, so NULL < 60000UNKNOWN → row dropped. Rows returned: Asha (50000), Dia (40000). (Ben 70000, Cira 90000, Gus NULL all excluded.) To include unknown-salary rows you must ask explicitly:

SELECT name FROM employees WHERE salary < 60000 OR salary IS NULL;

Note you must use IS NULL, not = NULL — because salary = NULL is itself UNKNOWN and matches nothing.

Exercise 5.2 — order sensitivity of two DELETEs

From the starting table, compare the final row count of these two scripts:

Script A:

DELETE FROM employees WHERE dept = 'Sales';
DELETE FROM employees WHERE salary < 60000;

Script B:

DELETE FROM employees WHERE salary < 60000;
DELETE FROM employees WHERE dept = 'Sales';
Recall Solution

Both leave the same rows, but the intermediate steps differ.

  • Script A: first DELETE removes Sales (Asha, Dia) → left with Ben 70000, Cira 90000. Second DELETE (salary < 60000) matches none → 2 rows (Ben, Cira).
  • Script B: first DELETE (salary < 60000) removes Asha (50000) and Dia (40000) → left with Ben 70000, Cira 90000. Second DELETE (dept = 'Sales') matches none → 2 rows (Ben, Cira).

Final answer: both end with 2 rows (Ben, Cira). Here order didn't change the result because the two conditions happened to overlap on the same victims. Contrast this with Exercise 3.3, where an UPDATE between operations did change outcomes. Lesson: order matters when an earlier statement changes the data a later filter reads — pure deletes on the starting set may or may not, so always trace it.

Exercise 5.3 — TRUNCATE vs DELETE recoverability

You want to empty the whole table but keep the option to undo within your transaction. Which do you use, and why not the other?

Recall Solution

Use ==DELETE FROM employees;== (no WHERE — intentionally, to hit all rows) inside a transaction:

BEGIN;
DELETE FROM employees;   -- all rows gone, but...
-- changed your mind:
ROLLBACK;                -- all rows restored

Why not TRUNCATE: TRUNCATE TABLE employees is faster (it deallocates data pages instead of logging each row) but is usually not rollback-able in most engines and does not fire row triggers. If undo-ability matters, DELETE is the safe choice; if you need a fast blunt wipe and don't care about undo, TRUNCATE.

Exercise 5.4 — the aggregate-of-NULL surprise

After Exercise 5.1's insert (Gus has NULL salary), what does this return, and why?

SELECT COUNT(*) AS all_rows, COUNT(salary) AS with_salary, AVG(salary) AS avg_pay
FROM employees WHERE dept = 'Eng';
Recall Solution

Eng rows now: Ben 70000, Cira 90000, Gus NULL (3 rows).

  • COUNT(*) counts rows3.
  • COUNT(salary) counts non-NULL values2 (Gus's NULL is skipped).
  • AVG(salary) ignores NULLs, averaging only the 2 present values → .

The subtlety: AVG divides by the count of non-NULL salaries (2), not by COUNT(*) (3). So the NULL neither counts as zero nor drags the average down — it's simply excluded.


Recall

Quick self-check across all levels.

Which verb is read-only, changing nothing?
SELECT.
A statement with no WHERE affects how many rows?
Every row in the table.
SET salary = salary + 5000 — what does the right-hand salary mean?
The row's current value before the update.
Why can't WHERE avg_pay > 46000 reference a SELECT alias?
WHERE runs before SELECT (and before grouping), so the alias and the aggregate don't exist yet — use HAVING.
Does WHERE salary < 60000 include rows where salary IS NULL?
No — NULL < 60000 is UNKNOWN, so the row is dropped; add OR salary IS NULL to include them.
Difference between COUNT(*) and COUNT(salary)?
COUNT(*) counts all rows; COUNT(salary) counts only non-NULL salary values.
Does AVG(salary) treat a NULL as zero?
No — it skips NULLs and divides by the count of non-NULL values.
Which of DELETE / TRUNCATE is rollback-able inside a transaction?
DELETE (logged, undoable); TRUNCATE usually is not.

Connections

  • Parent DML note — the concepts these exercises drill.
  • WHERE clause and Predicates — the predicate logic behind every filter here (incl. NULL/UNKNOWN).
  • Aggregate Functions & GROUP BY — the L3/L5 bucketing and COUNT/AVG behaviour.
  • Transactions — ACID, COMMIT, ROLLBACK — the BEGIN/ROLLBACK safety ritual in L4/L5.
  • SQL DDL — CREATE, ALTER, DROP — where TRUNCATE/DROP live vs DML's DELETE.
  • Indexes — what makes the WHERE/ORDER BY in these queries fast at scale.