Warm-up · Activity 1 of 7
// B4.1 · ~30 min · Beginner
UPDATE and DELETE
After this lesson you can change exactly the rows you mean, remove rows that are no longer needed, and check the result before you trust it.
Lesson 1 of 5 in B4 Changing data and schema
You will be able to
- Change chosen rows with UPDATE … SET … WHERE, several columns at once and from their old values
- Predict which rows a change touches: none, some or all, and why = NULL matches nothing
- Remove rows with DELETE … WHERE, empty a table with DELETE or TRUNCATE, and update from another table
Predict · Activity 2 of 7
Predict before you read on. Margaret wants to move to Hamburg, but the WHERE was forgotten. Ada and Linus live in Berlin, Grace in Munich, Margaret’s city is NULL. How many members live in Hamburg afterwards?
UPDATE members SET city = 'Hamburg';Practice · Activity 3 of 7
Neuromancer (id 6) has 1 copy. Fill in the gap so that the library gets one more copy of it.
UPDATE books SET copies = ____ + 1 WHERE id = 6;UPDATE books SET copies = + 1 WHERE id = 6;Practice · Activity 4 of 7
Match each statement to what it does to the table loans.
Practice · Activity 5 of 7
Put the lines of this change in order: Linus (id 3) moves to Hamburg and gets the e-mail address linus@example.com.
- 1.WHERE id = 3;
- 2. email = 'linus@example.com'
- 3.SET city = 'Hamburg',
- 4.UPDATE members
Brain teaser · Activity 6 of 7
Brain teaser. Ada and Linus live in Berlin, Grace in Munich, Margaret’s city is NULL. How many members live in Hamburg after this?
UPDATE members SET city = 'Hamburg' WHERE city <> 'Berlin';Apply · Activity 7 of 7
Mini-task. Three changes at the desk, one statement each: Grace returns Neuromancer (loan 5) on 2026-10-03; Linus (id 3) gets the e-mail address linus@example.com and moves to Munich; then Grace’s returned loans are removed. Before each statement, write down which rows it should touch, and check with a SELECT afterwards.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A day at the library desk
setup.sql loads the library of module B1: books, members and loans. main.sql records a return, adds copies, changes two columns of one member, and removes the loans that have come back, with a SELECT after each change to show its effect. On your computer: psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql.
main.sql
-- 1. Linus brings Dune back (loan 4).
UPDATE loans
SET returned_on = '2026-10-01'
WHERE id = 4;
-- 2. One more copy of every sci-fi book.
UPDATE books
SET copies = copies + 1
WHERE genre = 'sci-fi';
SELECT id, title, copies
FROM books
ORDER BY id;
-- 3. Two columns at once: Margaret moves and changes her address.
UPDATE members
SET city = 'Hamburg', email = 'maggie@example.com'
WHERE id = 4;
SELECT id, name, email, city
FROM members
ORDER BY id;
-- 4. Remove the loans that have come back.
DELETE FROM loans
WHERE returned_on IS NOT NULL;
SELECT id, book_id, member_id, loaned_on
FROM loans
ORDER BY id;
setup.sql
CREATE TABLE books (
id integer,
title text,
author text,
year integer,
genre text,
copies integer
);
INSERT INTO books VALUES
(1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
(2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
(3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
(4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
(5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
(6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);
CREATE TABLE members (
id integer,
name text,
email text,
city text,
joined date
);
INSERT INTO members VALUES
(1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
(2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
(3, 'Linus', NULL, 'Berlin', '2026-02-10'),
(4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');
CREATE TABLE loans (
id integer,
book_id integer,
member_id integer,
loaned_on date,
returned_on date
);
INSERT INTO loans VALUES
(1, 1, 1, '2026-09-01', '2026-09-10'),
(2, 3, 1, '2026-09-12', NULL),
(3, 2, 2, '2026-09-05', '2026-09-20'),
(4, 1, 3, '2026-09-15', NULL),
(5, 6, 2, '2026-09-18', NULL);
Run it with
psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sqlOutput
id | title | copies
----+-------------+--------
1 | Dune | 3
2 | Emma | 1
3 | Kindred | 2
4 | Beloved | 1
5 | The Hobbit | 3
6 | Neuromancer | 2
(6 rows)
id | name | email | city
----+----------+--------------------+---------
1 | Ada | ada@example.com | Berlin
2 | Grace | grace@example.com | Munich
3 | Linus | | Berlin
4 | Margaret | maggie@example.com | Hamburg
(4 rows)
id | book_id | member_id | loaned_on
----+---------+-----------+------------
2 | 3 | 1 | 2026-09-12
5 | 6 | 2 | 2026-09-18
(2 rows)- UPDATE and DELETE print nothing here, because psql runs quietly (-q); without -q it would print tags such as UPDATE 3.
- copies = copies + 1 reads each sci-fi book’s old value: Dune goes from 2 to 3, Kindred and Neuromancer from 1 to 2. The other books keep theirs.
- One UPDATE changes Margaret’s city and email, separated by a comma; Linus’s NULL email stays NULL, because his row does not match WHERE id = 4.
- Step 1 returned loan 4, so step 4 removes loans 1, 3 and 4: only the two open loans are left.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads PostgreSQL for your browser (up to 10.1 MB) and keeps it cached. Every run starts from an empty database. Your code stays on your device.
Exercises
Exercise 1 of 2
Three changes at the desk
Write three UPDATE statements. First, Margaret (id 4) moves to Hamburg. Second, Grace returns Neuromancer: loan 5 gets the return date 2026-10-02. Third, the library buys one more copy of every book published before 1950. Change nothing else.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads PostgreSQL for your browser (up to 10.1 MB) and keeps it cached. Every run starts from an empty database. Your code stays on your device.
Hints
Hint 1
Each UPDATE needs a WHERE that picks exactly the rows meant: id = 4 for Margaret, id = 5 for the loan.
Hint 2
Dates and text go in single quotes: SET returned_on = '2026-10-02'.
Hint 3
For the books, use the old value: SET copies = copies + 1 WHERE year < 1950.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
UPDATE members
SET city = 'Hamburg'
WHERE id = 4;
UPDATE loans
SET returned_on = '2026-10-02'
WHERE id = 5;
UPDATE books
SET copies = copies + 1
WHERE year < 1950;
Run it on your computer
Install PostgreSQL 18 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.sql
-- setup.sql has loaded books, members and loans.
-- Write the three UPDATE statements here.
SELECT id, name, city FROM members ORDER BY id;
test.sql
-- test: Margaret lives in Hamburg, and the other members kept their cities
SELECT string_agg(coalesce(city, '-'), ',' ORDER BY id) = 'Berlin,Munich,Berlin,Hamburg' FROM members;
-- test: Loan 5 was returned on 2026-10-02
SELECT count(*) = 1 FROM loans WHERE id = 5 AND returned_on = '2026-10-02';
-- test: Loans 2 and 4 are still open
SELECT string_agg(id::text, ',' ORDER BY id) = '2,4' FROM loans WHERE returned_on IS NULL;
-- test: Emma and The Hobbit have one copy more, the other books are unchanged
SELECT string_agg(copies::text, ',' ORDER BY id) = '2,2,1,1,4,1' FROM books;
setup.sql
CREATE TABLE books (
id integer,
title text,
author text,
year integer,
genre text,
copies integer
);
INSERT INTO books VALUES
(1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
(2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
(3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
(4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
(5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
(6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);
CREATE TABLE members (
id integer,
name text,
email text,
city text,
joined date
);
INSERT INTO members VALUES
(1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
(2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
(3, 'Linus', NULL, 'Berlin', '2026-02-10'),
(4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');
CREATE TABLE loans (
id integer,
book_id integer,
member_id integer,
loaned_on date,
returned_on date
);
INSERT INTO loans VALUES
(1, 1, 1, '2026-09-01', '2026-09-10'),
(2, 3, 1, '2026-09-12', NULL),
(3, 2, 2, '2026-09-05', '2026-09-20'),
(4, 1, 3, '2026-09-15', NULL),
(5, 6, 2, '2026-09-18', NULL);
psql runs the files on your own PostgreSQL server. Use an empty database you can throw away: add -d and its name to the command.
Run the program:
psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sqlThere is no command for the checks on your computer yet. They are in test.sql: each check query prints t when it passes.
Exercise 2 of 2
Clearing up the loans
Write three statements, in this order. First, delete the loans that have been returned. Second, the library withdraws Beloved: delete the book with id 4. Third, Ada (member 1) returns everything she still has: give her open loans the return date 2026-10-03.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads PostgreSQL for your browser (up to 10.1 MB) and keeps it cached. Every run starts from an empty database. Your code stays on your device.
Hints
Hint 1
Returned loans have a return date: DELETE FROM loans WHERE returned_on IS NOT NULL.
Hint 2
Delete the book by its id, so no other book can match: DELETE FROM books WHERE id = 4.
Hint 3
Ada's open loans: WHERE member_id = 1 AND returned_on IS NULL. Keep the order: if this UPDATE ran first, the DELETE would remove loan 2 too.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
DELETE FROM loans
WHERE returned_on IS NOT NULL;
DELETE FROM books
WHERE id = 4;
UPDATE loans
SET returned_on = '2026-10-03'
WHERE member_id = 1 AND returned_on IS NULL;
Run it on your computer
Install PostgreSQL 18 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.sql
-- setup.sql has loaded books, members and loans.
-- Write the three statements here.
SELECT id, member_id, returned_on FROM loans ORDER BY id;
test.sql
-- test: Only loans 2, 4 and 5 are left
SELECT string_agg(id::text, ',' ORDER BY id) = '2,4,5' FROM loans;
-- test: Beloved is gone, and the other five books are still there
SELECT string_agg(id::text, ',' ORDER BY id) = '1,2,3,5,6' FROM books;
-- test: Ada's loan 2 was returned on 2026-10-03
SELECT count(*) = 1 FROM loans WHERE id = 2 AND returned_on = '2026-10-03';
-- test: Loans 4 and 5 of the other members are still open
SELECT string_agg(id::text, ',' ORDER BY id) = '4,5' FROM loans WHERE returned_on IS NULL;
setup.sql
CREATE TABLE books (
id integer,
title text,
author text,
year integer,
genre text,
copies integer
);
INSERT INTO books VALUES
(1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
(2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
(3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
(4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
(5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
(6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);
CREATE TABLE members (
id integer,
name text,
email text,
city text,
joined date
);
INSERT INTO members VALUES
(1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
(2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
(3, 'Linus', NULL, 'Berlin', '2026-02-10'),
(4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');
CREATE TABLE loans (
id integer,
book_id integer,
member_id integer,
loaned_on date,
returned_on date
);
INSERT INTO loans VALUES
(1, 1, 1, '2026-09-01', '2026-09-10'),
(2, 3, 1, '2026-09-12', NULL),
(3, 2, 2, '2026-09-05', '2026-09-20'),
(4, 1, 3, '2026-09-15', NULL),
(5, 6, 2, '2026-09-18', NULL);
psql runs the files on your own PostgreSQL server. Use an empty database you can throw away: add -d and its name to the command.
Run the program:
psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sqlThere is no command for the checks on your computer yet. They are in test.sql: each check query prints t when it passes.
Common mistakes
A text value without quotes
UPDATE books
SET copies = copies + 1
WHERE title = Dune;
What psql prints
ERROR: column "dune" does not existWhy, and the fix
Without quotes, Dune is read as a name, folded to lower case, and looked up as a column. Text and dates are values in single quotes: WHERE title = 'Dune'. The same holds in SET: SET city = 'Hamburg'.
AND between two assignments
UPDATE books
SET copies = 2 AND year = 1966
WHERE id = 1;
What psql prints
ERROR: argument of AND must be type boolean, not type integerWhy, and the fix
In SET, AND does not separate assignments; it becomes part of the value, copies = (2 AND year = 1966), and 2 is no boolean. Separate assignments with a comma: SET copies = 2, year = 1966. AND belongs in WHERE, where it combines conditions.
The table name in front of a SET column
UPDATE books
SET books.copies = 3
WHERE id = 1;
What psql prints
ERROR: column "books" of relation "books" does not existWhy, and the fix
The column after SET always belongs to the table being updated, so it takes no table name; PostgreSQL reads books.copies as a field copies of a column books. Write SET copies = 3. In WHERE and in the expressions on the right, qualified names such as books.copies are fine.
PostgreSQL in the browser: PGlite 0.5.8 (PostgreSQL 18.3), Apache-2.0 and PostgreSQL License. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.