Warm-up · Activity 1 of 7
// B4.2 · ~30 min · Beginner
RETURNING: the rows you just changed
After this lesson you can get a new row’s id back from its INSERT, see what an UPDATE or DELETE changed, and show old and new values side by side.
You will be able to
- Get the rows that INSERT, UPDATE and DELETE changed back with RETURNING, including a generated id
- Predict what RETURNING shows by default: the inserted row, the new values, the deleted row
- Show old and new values side by side with PostgreSQL 18’s old. and new., and rename them with WITH
Predict · Activity 2 of 7
Predict before you read on. Dune has 2 copies, The Hobbit 3, every other book 1. Which title and copies does the UPDATE return?
UPDATE books SET copies = copies - 1 WHERE copies > 1 RETURNING title, copiesPractice · Activity 3 of 7
The loans have the ids 1 to 5. Fill in the column so that the INSERT returns the number PostgreSQL gives the new loan.
INSERT INTO loans (book_id, member_id, loaned_on) VALUES (4, 4, '2026-10-03') RETURNING ____;INSERT INTO loans (book_id, member_id, loaned_on) VALUES (4, 4, '2026-10-03') RETURNING ;Practice · Activity 4 of 7
Match each RETURNING to what it shows.
Practice · Activity 5 of 7
Put the parts of the statement in order.
- 1.SET copies = copies + 1
- 2.WHERE genre = 'sci-fi'
- 3.RETURNING title, old.copies, new.copies;
- 4.UPDATE books
Brain teaser · Activity 6 of 7
Brain teaser. Three books are sci-fi. SET leaves every value as it was. How many rows does this return?
UPDATE books SET copies = copies WHERE genre = 'sci-fi' RETURNING titleApply · Activity 7 of 7
Mini-task. With the library of the worked example: Hedy (hedy@example.com, Vienna) joins on 2026-10-03. Add her with an INSERT that returns her new id. Use that id to record her loan of The Hobbit (book 5) on the same day, again returning the new loan’s id. Then the library buys one more copy of every fantasy book: return each title with its copies before and after.
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
Getting changed rows back
setup.sql loads the library of module B1; here PostgreSQL numbers the members and the loans itself. main.sql adds a loan and gets its id back, marks a loan as returned, adds a copy of every sci-fi book with the values before and after, and deletes a member while showing the deleted row. On your computer: psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql.
main.sql
-- 1. A new loan: PostgreSQL picks the id, RETURNING shows it.
INSERT INTO loans (book_id, member_id, loaned_on)
VALUES (4, 4, '2026-10-03')
RETURNING id, book_id, loaned_on;
-- 2. Kindred comes back: RETURNING shows the row after the update.
UPDATE loans
SET returned_on = '2026-10-03'
WHERE id = 2
RETURNING id, loaned_on, returned_on;
-- 3. PostgreSQL 18: the old and the new value side by side.
UPDATE books
SET copies = copies + 1
WHERE genre = 'sci-fi'
RETURNING title, old.copies AS before, new.copies AS after;
-- 4. A deleted row, shown one last time.
DELETE FROM members
WHERE name = 'Margaret'
RETURNING *;
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);
-- PostgreSQL numbers the members and the loans itself (lesson B4.4).
CREATE TABLE members (
id integer GENERATED ALWAYS AS IDENTITY,
name text,
email text,
city text,
joined date
);
INSERT INTO members (name, email, city, joined) VALUES
('Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
('Grace', 'grace@example.com', 'Munich', '2025-03-02'),
('Linus', NULL, 'Berlin', '2026-02-10'),
('Margaret', 'margaret@example.com', NULL, '2026-05-20');
CREATE TABLE loans (
id integer GENERATED ALWAYS AS IDENTITY,
book_id integer,
member_id integer,
loaned_on date,
returned_on date
);
INSERT INTO loans (book_id, member_id, loaned_on, returned_on) VALUES
(1, 1, '2026-09-01', '2026-09-10'),
(3, 1, '2026-09-12', NULL),
(2, 2, '2026-09-05', '2026-09-20'),
(1, 3, '2026-09-15', NULL),
(6, 2, '2026-09-18', NULL);
Run it with
psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sqlOutput
id | book_id | loaned_on
----+---------+------------
6 | 4 | 2026-10-03
(1 row)
id | loaned_on | returned_on
----+------------+-------------
2 | 2026-09-12 | 2026-10-03
(1 row)
title | before | after
-------------+--------+-------
Dune | 2 | 3
Kindred | 1 | 2
Neuromancer | 1 | 2
(3 rows)
id | name | email | city | joined
----+----------+----------------------+------+------------
4 | Margaret | margaret@example.com | | 2026-05-20
(1 row)- The INSERT gave no id; PostgreSQL chose 6, the next number, and RETURNING shows it.
- returned_on is already the new date: an UPDATE’s RETURNING shows the row after the change.
- old.copies and new.copies show each sci-fi book before and after the update, in one statement.
- The DELETE returns Margaret’s row as it was; her empty city is NULL.
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
Get the new ids back
Hedy (hedy@example.com, Vienna) joins the library on 2026-10-03. The starter adds her but returns nothing. Make the INSERT return her id and name. Then add her loan of The Hobbit (book 5) on 2026-10-03, with the id the first statement returned as member_id, and return the loan’s id, book_id and member_id.
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
RETURNING goes at the very end of the INSERT, after the VALUES list.
Hint 2
The first statement returns 5; use it as member_id in the loan.
Hint 3
INSERT INTO loans (book_id, member_id, loaned_on) VALUES (5, 5, '2026-10-03') RETURNING id, book_id, member_id;
Show a solution
One way to solve it. Yours can look different and still pass the checks.
INSERT INTO members (name, email, city, joined)
VALUES ('Hedy', 'hedy@example.com', 'Vienna', '2026-10-03')
RETURNING id, name;
INSERT INTO loans (book_id, member_id, loaned_on)
VALUES (5, 5, '2026-10-03')
RETURNING id, book_id, member_id;
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.
INSERT INTO members (name, email, city, joined)
VALUES ('Hedy', 'hedy@example.com', 'Vienna', '2026-10-03');
test.sql
-- test: Hedy is a member from Vienna, numbered 5 by PostgreSQL
SELECT EXISTS (SELECT 1 FROM members WHERE id = 5 AND name = 'Hedy' AND city = 'Vienna');
-- test: Her open loan of The Hobbit is loan 6
SELECT EXISTS (SELECT 1 FROM loans WHERE id = 6 AND book_id = 5 AND member_id = 5 AND loaned_on = '2026-10-03' AND returned_on IS NULL);
-- test: Both statements return the new rows
-- output:
-- id | name
-- ----+------
-- 5 | Hedy
-- (1 row)
--
-- id | book_id | member_id
-- ----+---------+-----------
-- 6 | 5 | 5
-- (1 row)
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);
-- PostgreSQL numbers the members and the loans itself (lesson B4.4).
CREATE TABLE members (
id integer GENERATED ALWAYS AS IDENTITY,
name text,
email text,
city text,
joined date
);
INSERT INTO members (name, email, city, joined) VALUES
('Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
('Grace', 'grace@example.com', 'Munich', '2025-03-02'),
('Linus', NULL, 'Berlin', '2026-02-10'),
('Margaret', 'margaret@example.com', NULL, '2026-05-20');
CREATE TABLE loans (
id integer GENERATED ALWAYS AS IDENTITY,
book_id integer,
member_id integer,
loaned_on date,
returned_on date
);
INSERT INTO loans (book_id, member_id, loaned_on, returned_on) VALUES
(1, 1, '2026-09-01', '2026-09-10'),
(3, 1, '2026-09-12', NULL),
(2, 2, '2026-09-05', '2026-09-20'),
(1, 3, '2026-09-15', NULL),
(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
Before and after
The library doubles every book that has only one copy. The starter does that but returns only the new copies. Make it return title, the copies before as before and after it as after. Then delete every returned loan (returned_on is not NULL), returning the id and book_id of each deleted loan.
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
In an UPDATE, a plain copies in RETURNING is the new value. old.copies is the value before.
Hint 2
Give the columns their names with AS: old.copies AS before, new.copies AS after.
Hint 3
DELETE FROM loans WHERE returned_on IS NOT NULL RETURNING id, book_id;
Show a solution
One way to solve it. Yours can look different and still pass the checks.
UPDATE books
SET copies = copies * 2
WHERE copies = 1
RETURNING title, old.copies AS before, new.copies AS after;
DELETE FROM loans
WHERE returned_on IS NOT NULL
RETURNING id, book_id;
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
-- Double the copies of every book with one copy.
UPDATE books
SET copies = copies * 2
WHERE copies = 1
RETURNING title, copies;
test.sql
-- test: The books now have 13 copies in all
SELECT sum(copies) = 13 FROM books;
-- test: Only the three open loans are left
SELECT count(*) = 3 AND count(returned_on) = 0 FROM loans;
-- test: The UPDATE shows before and after, the DELETE the deleted loans
-- output:
-- title | before | after
-- -------------+--------+-------
-- Emma | 1 | 2
-- Kindred | 1 | 2
-- Beloved | 1 | 2
-- Neuromancer | 1 | 2
-- (4 rows)
--
-- id | book_id
-- ----+---------
-- 1 | 1
-- 3 | 2
-- (2 rows)
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);
-- PostgreSQL numbers the members and the loans itself (lesson B4.4).
CREATE TABLE members (
id integer GENERATED ALWAYS AS IDENTITY,
name text,
email text,
city text,
joined date
);
INSERT INTO members (name, email, city, joined) VALUES
('Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
('Grace', 'grace@example.com', 'Munich', '2025-03-02'),
('Linus', NULL, 'Berlin', '2026-02-10'),
('Margaret', 'margaret@example.com', NULL, '2026-05-20');
CREATE TABLE loans (
id integer GENERATED ALWAYS AS IDENTITY,
book_id integer,
member_id integer,
loaned_on date,
returned_on date
);
INSERT INTO loans (book_id, member_id, loaned_on, returned_on) VALUES
(1, 1, '2026-09-01', '2026-09-10'),
(3, 1, '2026-09-12', NULL),
(2, 2, '2026-09-05', '2026-09-20'),
(1, 3, '2026-09-15', NULL),
(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
RETURNING before WHERE
UPDATE loans
SET returned_on = '2026-10-03'
RETURNING *
WHERE id = 2;
What psql prints
ERROR: syntax error at or near "WHERE"Why, and the fix
RETURNING is the last clause of the statement. PostgreSQL reads RETURNING * as the end of the UPDATE and does not expect a WHERE after it. Move WHERE up: UPDATE loans SET returned_on = '2026-10-03' WHERE id = 2 RETURNING *. The same holds for DELETE … WHERE … RETURNING.
Giving a value to a GENERATED ALWAYS id
INSERT INTO members (id, name, email)
VALUES (5, 'Hedy', 'hedy@example.com')
RETURNING id;
What psql prints
ERROR: cannot insert a non-DEFAULT value into column "id"Why, and the fix
members.id is GENERATED ALWAYS AS IDENTITY: PostgreSQL chooses every id itself and refuses one you supply. Leave id out of the column list and let RETURNING id tell you which number the row got. psql adds a HINT about OVERRIDING SYSTEM VALUE; that is for copying existing data, not for everyday inserts.
old. after renaming it
UPDATE books
SET copies = copies + 1
WHERE id = 5
RETURNING WITH (OLD AS o, NEW AS n) old.copies, n.copies;
What psql prints
ERROR: missing FROM-clause entry for table "old"Why, and the fix
WITH (OLD AS o, NEW AS n) gives the old and new rows other names, and then the names old and new are hidden in that RETURNING list. Use the new names throughout: RETURNING WITH (OLD AS o, NEW AS n) o.copies, n.copies. Without the WITH part, old.copies and new.copies work as they are.
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.