Warm-up · Activity 1 of 7
Warm-up from the last lesson. members.email is UNIQUE, and Linus already has no email. What happens when a fifth member without an email is added?
INSERT INTO members VALUES (5, 'Alan', NULL, NULL, '2026-10-01');// B4.4 · ~30 min · Beginner
After this lesson you can change a table that already holds data: new columns and names, new defaults and types, new rules, and ids PostgreSQL assigns itself.
You will be able to
Warm-up · Activity 1 of 7
INSERT INTO members VALUES (5, 'Alan', NULL, NULL, '2026-10-01');Predict · Activity 2 of 7
ALTER TABLE members
ADD COLUMN active boolean NOT NULL DEFAULT true;Practice · Activity 3 of 7
ALTER TABLE members ____ COLUMN city TO town;Practice · Activity 4 of 7
Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
ALTER TABLE books ADD COLUMN signed text DEFAULT 'no';
ALTER TABLE books
ALTER COLUMN signed TYPE boolean USING signed = 'yes';Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
setup.sql builds the library with keys and rules from the last lesson. main.sql adds a status column to members, changes its default for new members, renames city to town, and lets PostgreSQL number new loans after the existing ids. On your computer: psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql.
main.sql
-- 1. A new column: existing rows get the default.
ALTER TABLE members ADD COLUMN status text NOT NULL DEFAULT 'active';
-- 2. A new default: only for rows inserted from now on.
ALTER TABLE members ALTER COLUMN status SET DEFAULT 'new';
INSERT INTO members (id, name, joined) VALUES (5, 'Alan', '2026-10-01');
-- 3. A new name for a column.
ALTER TABLE members RENAME COLUMN city TO town;
SELECT id, name, town, status FROM members ORDER BY id;
-- 4. Number new loans automatically, after the existing ids.
ALTER TABLE loans ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (START WITH 6);
INSERT INTO loans (book_id, member_id, loaned_on) VALUES (4, 5, '2026-10-02');
SELECT id, book_id, member_id, loaned_on FROM loans WHERE id >= 5 ORDER BY id;
setup.sql
CREATE TABLE members (
id integer PRIMARY KEY,
name text NOT NULL,
email text UNIQUE,
city text,
joined date NOT NULL
);
CREATE TABLE books (
id integer PRIMARY KEY,
title text NOT NULL,
author text,
year integer,
genre text,
copies integer NOT NULL CHECK (copies >= 0)
);
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');
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 loans (
id integer PRIMARY KEY,
book_id integer NOT NULL REFERENCES books (id),
member_id integer NOT NULL REFERENCES members (id) ON DELETE CASCADE,
loaned_on date NOT NULL,
returned_on date,
CHECK (returned_on >= loaned_on)
);
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 | name | town | status
----+----------+--------+--------
1 | Ada | Berlin | active
2 | Grace | Munich | active
3 | Linus | Berlin | active
4 | Margaret | | active
5 | Alan | | new
(5 rows)
id | book_id | member_id | loaned_on
----+---------+-----------+------------
5 | 6 | 2 | 2026-09-18
6 | 4 | 5 | 2026-10-02
(2 rows)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.
Exercise 1 of 2
Every book needs a shelf. The six books there now stand on main; books added later go to new arrivals first. Make shelf a NOT NULL column with the right default for each group, then add Persuasion (id 7, 1 copy) without a shelf and list id, title and shelf by id. The starter adds the column, but all shelves stay empty.
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.
A DEFAULT given in ADD COLUMN fills the existing rows.
Then change the default with ALTER COLUMN shelf SET DEFAULT …: that touches no existing row.
ADD COLUMN shelf text NOT NULL DEFAULT 'main', then SET DEFAULT 'new arrivals', then the INSERT.
One way to solve it. Yours can look different and still pass the checks.
ALTER TABLE books ADD COLUMN shelf text NOT NULL DEFAULT 'main';
ALTER TABLE books ALTER COLUMN shelf SET DEFAULT 'new arrivals';
INSERT INTO books (id, title, copies) VALUES (7, 'Persuasion', 1);
SELECT id, title, shelf FROM books ORDER BY id;
Install PostgreSQL 18 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.sql
-- Every book needs a shelf: the six old ones stand on 'main',
-- new books go to 'new arrivals' first.
ALTER TABLE books ADD COLUMN shelf text;
INSERT INTO books (id, title, copies) VALUES (7, 'Persuasion', 1);
SELECT id, title, shelf FROM books ORDER BY id;
test.sql
-- test: The six old books stand on main, Persuasion on new arrivals
-- output:
-- id | title | shelf
-- ----+-------------+--------------
-- 1 | Dune | main
-- 2 | Emma | main
-- 3 | Kindred | main
-- 4 | Beloved | main
-- 5 | The Hobbit | main
-- 6 | Neuromancer | main
-- 7 | Persuasion | new arrivals
-- (7 rows)
setup.sql
CREATE TABLE members (
id integer PRIMARY KEY,
name text NOT NULL,
email text UNIQUE,
city text,
joined date NOT NULL
);
CREATE TABLE books (
id integer PRIMARY KEY,
title text NOT NULL,
author text,
year integer,
genre text,
copies integer NOT NULL CHECK (copies >= 0)
);
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');
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 loans (
id integer PRIMARY KEY,
book_id integer NOT NULL REFERENCES books (id),
member_id integer NOT NULL REFERENCES members (id) ON DELETE CASCADE,
loaned_on date NOT NULL,
returned_on date,
CHECK (returned_on >= loaned_on)
);
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
From now on every member needs an email, and the column city should be called town. Remove the -- in front of the ALTER in the starter and run it: it fails, because Linus has no email. His address is linus@example.com: store it first, then add the rule, then rename the column.
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.
Read the error of the starter: column "email" of relation "members" contains null values. Which row is it?
UPDATE members SET email = 'linus@example.com' WHERE id = 3; comes first.
Then SET NOT NULL succeeds, and RENAME COLUMN city TO town changes the name.
One way to solve it. Yours can look different and still pass the checks.
UPDATE members SET email = 'linus@example.com' WHERE id = 3;
ALTER TABLE members ALTER COLUMN email SET NOT NULL;
ALTER TABLE members RENAME COLUMN city TO town;
Install PostgreSQL 18 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.sql
-- Every member needs an email from now on, and city becomes town.
-- ALTER TABLE members ALTER COLUMN email SET NOT NULL;
SELECT id, name, email FROM members ORDER BY id;
test.sql
-- test: email is NOT NULL
SELECT attnotnull FROM pg_attribute WHERE attrelid = 'members'::regclass AND attname = 'email';
-- test: Linus has his email
SELECT email = 'linus@example.com' FROM members WHERE id = 3;
-- test: The column is called town and still holds Berlin for Ada
SELECT town = 'Berlin' FROM members WHERE id = 1;
-- test: There is no column city any more
SELECT NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'members'::regclass AND attname = 'city' AND NOT attisdropped);
-- test: All four members are still there
SELECT count(*) = 4 FROM members;
setup.sql
CREATE TABLE members (
id integer PRIMARY KEY,
name text NOT NULL,
email text UNIQUE,
city text,
joined date NOT NULL
);
CREATE TABLE books (
id integer PRIMARY KEY,
title text NOT NULL,
author text,
year integer,
genre text,
copies integer NOT NULL CHECK (copies >= 0)
);
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');
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 loans (
id integer PRIMARY KEY,
book_id integer NOT NULL REFERENCES books (id),
member_id integer NOT NULL REFERENCES members (id) ON DELETE CASCADE,
loaned_on date NOT NULL,
returned_on date,
CHECK (returned_on >= loaned_on)
);
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.
ALTER TABLE members ALTER COLUMN email SET NOT NULL;
What psql prints
ERROR: column "email" of relation "members" contains null valuesWhy, and the fix
SET NOT NULL, like ADD CONSTRAINT, checks every row that is already there. Find the rows with SELECT * FROM members WHERE email IS NULL; give them a value with UPDATE (or decide the rule is wrong), then run the ALTER again.
ALTER TABLE loans ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY;
INSERT INTO loans (book_id, member_id, loaned_on) VALUES (4, 4, '2026-10-02');
What psql prints
ERROR: duplicate key value violates unique constraint "loans_pkey"Why, and the fix
The new counter does not look at the ids already in the table: it hands out 1, which loan 1 has. Start after the highest id: ADD GENERATED BY DEFAULT AS IDENTITY (START WITH 6), or move an existing counter with ALTER COLUMN id RESTART WITH 6.
ALTER TABLE books ADD COLUMN signed text DEFAULT 'no';
ALTER TABLE books ALTER COLUMN signed TYPE boolean USING signed = 'yes';
What psql prints
ERROR: default for column "signed" cannot be cast automatically to type booleanWhy, and the fix
USING converts the rows, not the default. Drop the default first and set a new one afterwards: ALTER COLUMN signed DROP DEFAULT, then the TYPE change, then ALTER COLUMN signed SET DEFAULT false.
PostgreSQL in the browser: PGlite 0.5.8 (PostgreSQL 18.3), Apache-2.0 and PostgreSQL License. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.