What you'll practice: modelling parent/child relationships with
FOREIGN KEY, enforcing referential integrity, and controlling what happens to children when a parent goes away withON DELETE CASCADE / SET NULL / RESTRICT.
- Declare a foreign key in
CREATE TABLEand withALTER TABLE - Watch what happens when you insert a child row that points to a parent that doesn't exist
- Choose between
ON DELETE CASCADE,ON DELETE SET NULL, andON DELETE RESTRICT - Understand why
"order"is quoted in SQL (it's a reserved word) - Drop a foreign key
Four tables:
customer— people who buy thingsproduct— things that can be boughtorder— a customer placing an order (note the quotes —orderis a reserved word!)order_item— line items on an order, each pointing at a product
Relationships:
customer (1) ──< order (N) ──< order_item (N) >── product (1)
order.customer_id→customer.idwithON DELETE CASCADE(delete the customer, their orders go too)order_item.order_id→order.idwithON DELETE CASCADEorder_item.product_id→product.idwithON DELETE SET NULL(keep the historical line item even if the product is removed)
psql -U postgres -d sql_exercise -f 04-advanced-databases/06-foreign-keys/schema.sql
psql -U postgres -d sql_exercise -f 04-advanced-databases/06-foreign-keys/seed.sql
psql -U postgres -d sql_exerciseInside psql:
SET search_path TO adv_fks;Use \d "order" and \d order_item to see the foreign key declarations on each table.
Question: what do the FOREIGN KEY lines at the bottom of the output look like? Which actions did Postgres pick up from the schema?
Insert a new order for customer_id = 2 (Alan Turing) with a total_cents of 12999. Don't specify an id — the SERIAL takes care of it.
Verify: SELECT id, customer_id, total_cents FROM "order" WHERE customer_id = 2;
Try to insert an order for customer_id = 9999.
Question: what error do you get, and which constraint name appears in the error message?
Try to insert an order_item with order_id = 9999.
Question: same error pattern? Which constraint name this time?
Join "order" and customer and return columns order_id, customer_name, placed_at, total_cents. Order by placed_at ascending.
Join "order", customer, order_item, and product and return:
| order_id | customer_name | product_name | quantity |
Order by order_id, then product_name.
Delete customer id = 3 (Grace Hopper). Before running the delete:
- Count how many
"order"rows belong to Grace. - Run the
DELETE FROM customer WHERE id = 3;. - Count her orders again.
Question: why did her orders disappear without you explicitly deleting them?
Delete order_id = 1. Before:
SELECT COUNT(*) FROM order_item WHERE order_id = 1;After:
DELETE FROM "order" WHERE id = 1;
SELECT COUNT(*) FROM order_item WHERE order_id = 1;Question: why was the behaviour the same as Exercise 7?
Delete product_id = 4 (Ergonomic Mouse).
SELECT id, order_id, product_id, quantity FROM order_item;Question: what did the order_item.product_id become for rows that used to reference product 4? Why is SET NULL sometimes the right choice for historical line items, instead of CASCADE?
The schema currently uses CASCADE. Let's switch to RESTRICT and watch the difference.
- Drop the existing FK on
"order".customer_id:ALTER TABLE "order" DROP CONSTRAINT order_customer_fk;
- Re-add it with
ON DELETE RESTRICT:ALTER TABLE "order" ADD CONSTRAINT order_customer_fk FOREIGN KEY (customer_id) REFERENCES customer (id) ON DELETE RESTRICT;
- Try to delete customer id
1(Ada).
Question: what error do you get? When would RESTRICT be the right choice instead of CASCADE?
Create a brand new table review with columns id SERIAL PK, product_id INT NOT NULL, rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5), body TEXT NOT NULL. Do not declare the foreign key in the CREATE TABLE.
Then, using ALTER TABLE, add a foreign key named review_product_fk from review.product_id to product.id with ON DELETE CASCADE.
Verify: \d review.
Write a query that finds every order_item whose product_id is NULL (orphaned from Exercise 9). Show id, order_id, product_id, quantity.
Return customer_name, order_count, sorted by order_count DESC. Include customers with zero orders using a LEFT JOIN.
Drop order_item_product_fk from order_item. Then show \d order_item to confirm it's gone.
Add a manager_id INTEGER REFERENCES customer(id) column to customer. This lets a customer "manage" another customer (silly for an e-commerce model, but it's the classic example of a self-referencing FK). Insert one row, set another row's manager_id to that row, and write a query that joins customer to itself to return (employee_name, manager_name) pairs.
Read the Postgres docs on DEFERRABLE INITIALLY DEFERRED. When is it useful? Rewrite the order_customer_fk so its check only runs at COMMIT time instead of on every statement.
Could you replace order_item.order_id with a composite key (order_id, customer_id) that references both "order".id and "order".customer_id together? Why would you want to?
DROP SCHEMA IF EXISTS adv_fks CASCADE;