What you'll practice: protecting a table's data with
PRIMARY KEY,UNIQUE,NOT NULL,DEFAULTandCHECKconstraints, and adding/removing them withALTER TABLE.
Constraints are how you turn a table from a "bucket that holds rows" into a proper data contract. A table with good constraints cannot end up in an invalid state, no matter how buggy the application code that talks to it is.
- Add a
PRIMARY KEYto an existing table - Add
UNIQUE,CHECK, andNOT NULLconstraints - Give constraints meaningful names
- Read constraint violation error messages and understand them
DROP CONSTRAINTto remove one- Clean up existing bad data before adding a constraint
psql -U postgres -d sql_exercise -f 04-advanced-databases/05-constraints/schema.sql
psql -U postgres -d sql_exerciseThe reference schema.sql builds the finished table with every constraint in place. We will not load the seed for this section right away — the whole point is to build the table up constraint by constraint. Drop and rebuild without any constraints:
SET search_path TO adv_constraints;
DROP TABLE IF EXISTS employee;
CREATE TABLE employee (
id INTEGER,
email VARCHAR(255),
first_name VARCHAR(100),
last_name VARCHAR(100),
salary NUMERIC(10,2),
country CHAR(2),
joined_on DATE
);Now you have a wide-open table. Start the exercises.
With no constraints at all, insert two rows that share the same id, then insert a row where first_name is NULL, then insert a row where salary = -5000.
INSERT INTO employee (id, email, first_name, last_name, salary, country, joined_on)
VALUES (1, 'a@example.com', 'Ada', 'Lovelace', 100, 'GB', '2015-03-02'),
(1, 'b@example.com', NULL, 'Turing', -5000, 'GB', '2010-09-01');Question: do any of these fail? What does that tell you about the default behaviour of a brand-new table?
Delete every row so the table is empty again:
TRUNCATE employee;Use ALTER TABLE to mark first_name, last_name, and email as NOT NULL.
Verify: \d employee and check the "Nullable" column.
Try it: after adding the constraint, re-run the insert from Exercise 1 with NULL first_name. What error do you get?
Add a primary key on id, and give the constraint a name (employee_pk).
Hint: ALTER TABLE employee ADD CONSTRAINT employee_pk PRIMARY KEY (id);
Verify: \d employee. You should see the primary key listed at the bottom.
Question: what does PRIMARY KEY do that adding UNIQUE + NOT NULL separately would not?
Add a unique constraint on email, named employee_email_unique.
Try it: insert two employees with the same email. What error do you get? What constraint name appears in the error?
Add a CHECK constraint named employee_salary_non_negative that enforces salary >= 0.
Try it: insert a row with salary = -100. Confirm it fails with an error that names your constraint.
Add a CHECK constraint that enforces country is exactly two uppercase letters.
Hint: CHECK (country ~ '^[A-Z]{2}$'). ~ is the Postgres regex match operator.
Try it: insert a row with country = 'gb'. Why does it fail?
ALTER the table so:
salarydefaults to0countrydefaults to'GB'joined_ondefaults toCURRENT_DATE
Hint: ALTER TABLE ... ALTER COLUMN salary SET DEFAULT 0;
Verify: run INSERT INTO employee (id, email, first_name, last_name) VALUES (99, 'x@example.com','X','Y');. It should succeed, and the defaults should be applied.
Drop employee_country_iso2. Then insert a row with country = 'gb' to prove it works now.
Hint: ALTER TABLE ... DROP CONSTRAINT ...;
Add the country constraint back. But this time, before you add it, try inserting an invalid row ('gb') first. Then try adding the constraint — it will fail because there's already bad data.
Question: what's the error? What does it tell you about how ALTER TABLE ADD CONSTRAINT validates existing rows?
Update the bad row (country = 'gb') to 'GB', then add the employee_country_iso2 CHECK back. This time it should succeed.
Lesson: in production databases, you very often have to clean up existing data before you can enforce a new constraint.
Now that the table has all its constraints in place, load the seed data:
psql -U postgres -d sql_exercise -f 04-advanced-databases/05-constraints/seed.sqlVerify: SELECT COUNT(*) FROM employee; should return 5.
Add a UNIQUE constraint on the combination (first_name, last_name, joined_on), named employee_name_hired_unique. (Not realistic, but it demonstrates a composite unique constraint.)
Verify: \d employee.
Use information_schema.table_constraints to list every constraint on the employee table, showing the constraint name and type.
Hint:
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'adv_constraints'
AND table_name = 'employee';Add a CHECK constraint that enforces joined_on <= CURRENT_DATE — you can't hire someone in the future.
A plain UNIQUE treats two NULL emails as distinct. Read about partial unique indexes. Can you enforce "every non-NULL email is unique" without UNIQUE? (You'll need CREATE UNIQUE INDEX ... WHERE email IS NOT NULL.)
Read \h ALTER TABLE and look up NOT VALID. When would you add a CHECK constraint with NOT VALID, and what does VALIDATE CONSTRAINT do afterwards?
DROP SCHEMA IF EXISTS adv_constraints CASCADE;