Capstone — Migrating and Extending a MySQL Database in PostgreSQL
PostgreSQL
Chapter 12 · Capstone: Migrating and Extending a MySQL Database in PostgreSQL
Eleven chapters covered what's genuinely different about Postgres. This capstone combines three of them — JSONB, recursive CTEs, and PL/pgSQL — into one real, working schema, deliberately reusing ordinary relational design everywhere else, exactly the way postgres1-1 promised this course would work.
The Scenario
Start from a small, ordinary mysql2/mysql3-style e-commerce schema — customers, categories, products, orders, order items — and reimplement it in Postgres, reaching for this course's own distinguishing features only where they provide genuine value, echoing postgres1-3's own warning against reaching for richness that isn't actually earning its keep.
The Schema
CREATE TABLE customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL ); CREATE TABLE categories ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, parent_id INT REFERENCES categories(id), tax_rate NUMERIC(4,3) NOT NULL DEFAULT 0.000 ); CREATE TABLE products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, category_id INT REFERENCES categories(id), price NUMERIC(10,2) NOT NULL, attributes JSONB ); CREATE INDEX idx_products_attributes ON products USING GIN (attributes); CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INT REFERENCES customers(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE order_items ( id SERIAL PRIMARY KEY, order_id INT REFERENCES orders(id), product_id INT REFERENCES products(id), quantity INT NOT NULL, unit_price NUMERIC(10,2) NOT NULL );
customers, orders, and order_items are ordinary relational tables — nothing here needed anything Postgres-specific. categories and products are where the interesting work happens.
Recursive CTE — Category Breadcrumbs
categories self-references via parent_id, the same hierarchical pattern postgres1-5 introduced — but this time walking up toward the root, rather than down toward the leaves the way that chapter's own employee-hierarchy example did, building a display-ready breadcrumb path:
WITH RECURSIVE breadcrumb AS ( SELECT id, name, parent_id, name::TEXT AS path FROM categories WHERE id = 15 -- e.g. "Laptops" UNION ALL SELECT c.id, c.name, c.parent_id, c.name || ' > ' || breadcrumb.path FROM categories c JOIN breadcrumb ON c.id = breadcrumb.parent_id ) SELECT path FROM breadcrumb WHERE parent_id IS NULL; -- 'Electronics > Computers > Laptops'
JSONB — Product Attributes
products.attributes reuses postgres1-4's own product catalog pattern directly — a shirt-style product might store {"size": "M", "color": "blue"}, a laptop-style product {"ram_gb": 32, "storage_gb": 1024}, all in the same column, all queryable through the GIN index already declared on it.
A Custom PL/pgSQL Function — Order Total With Tax
This function combines postgres1-8's own function-writing pattern with this chapter's own recursive category walk — for each order line, it walks up the category tree to find the nearest set tax rate, exactly the kind of composition this whole course has been building toward:
CREATE FUNCTION order_total_with_tax(order_id_param INT)
RETURNS NUMERIC AS $$
DECLARE
subtotal NUMERIC := 0;
tax_total NUMERIC := 0;
item RECORD;
cat_tax NUMERIC;
BEGIN
FOR item IN
SELECT oi.quantity, oi.unit_price, p.category_id
FROM order_items oi
JOIN products p ON p.id = oi.product_id
WHERE oi.order_id = order_id_param
LOOP
subtotal := subtotal + (item.quantity * item.unit_price);
WITH RECURSIVE cat_walk AS (
SELECT id, parent_id, tax_rate FROM categories WHERE id = item.category_id
UNION ALL
SELECT c.id, c.parent_id, c.tax_rate
FROM categories c
JOIN cat_walk ON c.id = cat_walk.parent_id
)
SELECT tax_rate INTO cat_tax FROM cat_walk WHERE tax_rate > 0 LIMIT 1;
tax_total := tax_total + (item.quantity * item.unit_price * COALESCE(cat_tax, 0));
END LOOP;
RETURN subtotal + tax_total;
END;
$$ LANGUAGE plpgsql;
SELECT order_total_with_tax(42);Chapter Attribution
| Capstone element | Chapter |
|---|---|
| Ordinary relational schema (customers/orders/order_items) | mysql2 / mysql3 (assumed baseline) |
| Self-referencing categories, recursive breadcrumb walk | postgres1-5 |
| JSONB product attributes + GIN index | postgres1-4, postgres1-7 |
| Dollar-quoted function, FOR loop, RECORD variable | postgres1-8 |
| Nested recursive CTE inside the function | postgres1-5, postgres1-8 (composed) |
pgloader) is deliberately out of scope. No replication/HA setup from postgres1-11 is applied here, and full-text search from postgres1-6 isn't exercised, since this particular schema's own natural fit was JSONB, recursion, and PL/pgSQL instead — not every chapter's feature needs to appear in every real schema, which is itself the point. This is also a deliberately lighter, single-engine echo of the site's own cp1 bucket-list interest in relational-to-document conversion — a real reimplementation exercise, not the harder cross-engine MySQL-to-MongoDB problem cp1 itself describes.
customers, orders, order_items) needed nothing Postgres-specific at all — exactly as expected, since postgres1-1 opened this course by promising it wouldn't re-teach SQL fundamentals mysql2/mysql3 already cover. Postgres's own distinguishing features were reached for only where categories and products genuinely needed them — postgres1-3's own "richness is a capability, not an obligation" warning, now demonstrated at the whole-schema level.
Hands-On Exercises
Explain why the breadcrumb recursive CTE in this chapter walks in the opposite direction from postgres1-5's own employee-hierarchy example, and explain what would need to change in the anchor/recursive terms to reverse the direction.
📄 View solutionExplain how order_total_with_tax() composes postgres1-8's own function-writing material with postgres1-5's own recursive CTE material, rather than being purely one or the other.
📄 View solutionUsing this chapter's own tip-box, explain why customers/orders/order_items needed nothing Postgres-specific, and explain how this demonstrates postgres1-3's own "richness is a capability, not an obligation" warning at the whole-schema level.
📄 View solutionChapter 12 Quick Reference — Course Complete
- Ordinary relational design (customers/orders/order_items) reused unchanged, exactly per postgres1-1's own opening promise
- Recursive CTE walks UP the category tree for breadcrumbs — the reverse direction of postgres1-5's own DOWN-walking employee example
- JSONB + GIN reuses postgres1-4/postgres1-7's own product-attributes pattern directly
- The PL/pgSQL function composes postgres1-8's own function-writing pattern WITH a nested recursive CTE — a genuine synthesis, not three separate demos
- Honest scope note: no live migration tooling, no replication/HA, no full-text search — not every feature belongs in every schema
- This closes the full 12-chapter PostgreSQL course