Loading learning content...
Throughout this module, we've built a comprehensive understanding of one-to-many relationship mapping: the foreign key strategy, participation constraints, referential integrity, and cascading behaviors. Now it's time to synthesize this knowledge through complete, real-world examples.
This page presents end-to-end mapping case studies from diverse domains—e-commerce, content management, human resources, and financial systems. Each example walks through the complete process: from ER diagram analysis, through design decisions, to final SQL implementation with all constraints, indexes, and best practices applied.
These examples serve as templates and reference implementations for your own database designs.
By the end of this page, you will have seen complete 1:N mapping implementations across multiple domains, understand how to apply all module concepts together, recognize common patterns and anti-patterns, and have reference schemas you can adapt for your projects.
The e-commerce domain exemplifies layered 1:N relationships, where several parent-child hierarchies chain together. Let's map the core ordering subsystem.
ER Diagram Analysis:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ CUSTOMER │ 1 ─── N │ ORDER │ 1 ─── N │ ORDER_LINE │
│─────────────│ │─────────────│ │─────────────│
│ customer_id │ │ order_id │ │ line_id │
│ email │ │ order_date │ │ quantity │
│ name │ │ status │ │ unit_price │
│ phone │ │ total │ │ │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌─────────────┐ │
│ PRODUCT │ 1 ─── N ┘
│─────────────│
│ product_id │
│ name │
│ price │
│ stock │
└─────────────┘
Relationships:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
-- =========================================-- E-Commerce Order System - Complete Schema-- ========================================= -- Customer: Parent entity (independent)CREATE TABLE Customer ( customer_id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, full_name VARCHAR(200) NOT NULL, phone VARCHAR(20), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Product: Parent entity (independent)CREATE TABLE Product ( product_id SERIAL PRIMARY KEY, sku VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(300) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL CHECK (price >= 0), stock_quantity INT NOT NULL DEFAULT 0 CHECK (stock_quantity >= 0), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Order: Child of Customer, Parent of OrderLineCREATE TABLE "Order" ( order_id SERIAL PRIMARY KEY, order_number VARCHAR(20) UNIQUE NOT NULL, -- Business-friendly ID order_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')), subtotal DECIMAL(12,2) NOT NULL DEFAULT 0, tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0, shipping_address TEXT, -- FK to Customer: Total participation (order MUST have customer) customer_id INT NOT NULL, CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES Customer(customer_id) ON DELETE RESTRICT -- Don't delete customers with orders ON UPDATE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- CRITICAL: Index on FK for join performanceCREATE INDEX idx_order_customer ON "Order"(customer_id);CREATE INDEX idx_order_status ON "Order"(status);CREATE INDEX idx_order_date ON "Order"(order_date); -- OrderLine: Child of both Order and Product (Weak Entity)CREATE TABLE OrderLine ( order_line_id SERIAL PRIMARY KEY, -- FK to Order: Total participation (line MUST belong to order) order_id INT NOT NULL, -- FK to Product: Total participation (line MUST reference product) product_id INT NOT NULL, -- Line details quantity INT NOT NULL CHECK (quantity > 0), unit_price DECIMAL(10,2) NOT NULL, -- Snapshot at order time line_total DECIMAL(12,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, -- Unique constraint: one entry per product per order CONSTRAINT uq_order_product UNIQUE (order_id, product_id), CONSTRAINT fk_orderline_order FOREIGN KEY (order_id) REFERENCES "Order"(order_id) ON DELETE CASCADE -- Delete order → delete all lines ON UPDATE CASCADE, CONSTRAINT fk_orderline_product FOREIGN KEY (product_id) REFERENCES Product(product_id) ON DELETE RESTRICT -- Don't delete products on active orders ON UPDATE CASCADE); -- Indexes for FK columnsCREATE INDEX idx_orderline_order ON OrderLine(order_id);CREATE INDEX idx_orderline_product ON OrderLine(product_id); -- =========================================-- Sample Queries-- ========================================= -- Customer's order history with totalsSELECT c.full_name, o.order_number, o.order_date, o.status, o.total_amountFROM Customer cJOIN "Order" o ON c.customer_id = o.customer_idWHERE c.customer_id = 1ORDER BY o.order_date DESC; -- Order details with line itemsSELECT o.order_number, p.name AS product, ol.quantity, ol.unit_price, ol.line_totalFROM "Order" oJOIN OrderLine ol ON o.order_id = ol.order_idJOIN Product p ON ol.product_id = p.product_idWHERE o.order_id = 100; -- Products never ordered (partial participation allows this)SELECT p.name, p.skuFROM Product pLEFT JOIN OrderLine ol ON p.product_id = ol.product_idWHERE ol.order_line_id IS NULL;Notice that OrderLine stores unit_price independently of Product.price. This captures the price at order time. If product prices change, historical orders retain their original pricing. This is a crucial pattern for financial accuracy.
Content management systems have rich 1:N relationships with hierarchical and categorical structures. This example demonstrates a blog platform.
ER Diagram Analysis:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ AUTHOR │ 1 ─── N │ POST │ 1 ─── N │ COMMENT │
│─────────────│ │─────────────│ │─────────────│
│ author_id │ │ post_id │ │ comment_id │
│ username │ │ title │ │ content │
│ email │ │ content │ │ author_name │
│ bio │ │ status │ │ created_at │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
│ N
│
│ 1
┌──────┴──────┐
│ CATEGORY │
│─────────────│
│ category_id │
│ name │
│ slug │
└─────────────┘
Relationships:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
-- =========================================-- Content Management System - Blog Platform-- ========================================= -- Author: Parent entityCREATE TABLE Author ( author_id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, display_name VARCHAR(100) NOT NULL, bio TEXT, avatar_url VARCHAR(500), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Category: Parent entity (optional relationship)CREATE TABLE Category ( category_id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, description TEXT, parent_id INT, -- Self-referential for subcategories CONSTRAINT fk_category_parent FOREIGN KEY (parent_id) REFERENCES Category(category_id) ON DELETE SET NULL -- Parent category deleted → subcategories become top-level ON UPDATE CASCADE); CREATE INDEX idx_category_parent ON Category(parent_id); -- Post: Child of Author and CategoryCREATE TABLE Post ( post_id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, slug VARCHAR(500) UNIQUE NOT NULL, excerpt TEXT, content TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')), published_at TIMESTAMP, view_count INT DEFAULT 0, -- FK to Author: Total participation (post MUST have author) author_id INT NOT NULL, -- FK to Category: Partial participation (post MAY have category) category_id INT, -- Nullable allows uncategorized posts CONSTRAINT fk_post_author FOREIGN KEY (author_id) REFERENCES Author(author_id) ON DELETE RESTRICT -- Don't delete authors with posts ON UPDATE CASCADE, CONSTRAINT fk_post_category FOREIGN KEY (category_id) REFERENCES Category(category_id) ON DELETE SET NULL -- Category deleted → post becomes uncategorized ON UPDATE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_post_author ON Post(author_id);CREATE INDEX idx_post_category ON Post(category_id);CREATE INDEX idx_post_status ON Post(status);CREATE INDEX idx_post_published ON Post(published_at) WHERE status = 'published'; -- Comment: Child of Post (Weak Entity semantically)CREATE TABLE Comment ( comment_id SERIAL PRIMARY KEY, content TEXT NOT NULL, author_name VARCHAR(100) NOT NULL, -- Guest comments allowed author_email VARCHAR(255), is_approved BOOLEAN DEFAULT FALSE, -- FK to Post: Total participation (comment MUST belong to post) post_id INT NOT NULL, -- Self-referential for reply threads parent_comment_id INT, CONSTRAINT fk_comment_post FOREIGN KEY (post_id) REFERENCES Post(post_id) ON DELETE CASCADE -- Post deleted → all comments deleted ON UPDATE CASCADE, CONSTRAINT fk_comment_parent FOREIGN KEY (parent_comment_id) REFERENCES Comment(comment_id) ON DELETE CASCADE -- Parent comment deleted → replies deleted ON UPDATE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_comment_post ON Comment(post_id);CREATE INDEX idx_comment_parent ON Comment(parent_comment_id);CREATE INDEX idx_comment_approved ON Comment(is_approved) WHERE is_approved = TRUE; -- =========================================-- Sample Queries-- ========================================= -- Published posts with author and categorySELECT p.title, a.display_name AS author, c.name AS category, p.published_at, p.view_countFROM Post pJOIN Author a ON p.author_id = a.author_idLEFT JOIN Category c ON p.category_id = c.category_id -- LEFT: category optionalWHERE p.status = 'published'ORDER BY p.published_at DESCLIMIT 10; -- Post with comment tree (PostgreSQL recursive CTE)WITH RECURSIVE comment_tree AS ( -- Base: top-level comments SELECT comment_id, content, author_name, parent_comment_id, 0 AS depth FROM Comment WHERE post_id = 1 AND parent_comment_id IS NULL AND is_approved = TRUE UNION ALL -- Recursive: replies SELECT c.comment_id, c.content, c.author_name, c.parent_comment_id, ct.depth + 1 FROM Comment c JOIN comment_tree ct ON c.parent_comment_id = ct.comment_id WHERE c.is_approved = TRUE)SELECT * FROM comment_tree ORDER BY depth, comment_id; -- Categories with post countsSELECT c.name, c.slug, COUNT(p.post_id) AS post_countFROM Category cLEFT JOIN Post p ON c.category_id = p.category_id AND p.status = 'published'GROUP BY c.category_idORDER BY post_count DESC;Human resources systems feature organizational hierarchies with self-referential relationships. This example demonstrates a classic employee-department-manager structure.
Key 1:N Relationships:
Department (1) → Employee (N): A department has many employees. Each employee belongs to one department.
Employee (1) → Employee (N): Manager-subordinate self-reference.
Employee (1) → PerformanceReview (N): An employee has many reviews over time.
Department (1) → Department (N): Self-referential for division hierarchy.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
-- =========================================-- Human Resources System - Complete Schema-- ========================================= -- Department: Self-referential hierarchyCREATE TABLE Department ( dept_id SERIAL PRIMARY KEY, dept_code VARCHAR(10) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, budget DECIMAL(15,2), -- Self-reference for department hierarchy parent_dept_id INT, -- NULL for top-level departments -- Manager reference (added after Employee exists) manager_id INT, -- Set later via ALTER CONSTRAINT fk_dept_parent FOREIGN KEY (parent_dept_id) REFERENCES Department(dept_id) ON DELETE SET NULL -- Parent deleted → becomes top-level ON UPDATE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_dept_parent ON Department(parent_dept_id); -- Employee: Core entity with multiple 1:N relationshipsCREATE TABLE Employee ( emp_id SERIAL PRIMARY KEY, employee_number VARCHAR(20) UNIQUE NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, phone VARCHAR(20), hire_date DATE NOT NULL, job_title VARCHAR(100) NOT NULL, salary DECIMAL(12,2) NOT NULL CHECK (salary > 0), employment_status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (employment_status IN ('active', 'on_leave', 'terminated')), -- FK to Department: Total participation dept_id INT NOT NULL, -- Self-reference for manager: Partial (CEO has no manager) manager_id INT, CONSTRAINT fk_emp_department FOREIGN KEY (dept_id) REFERENCES Department(dept_id) ON DELETE RESTRICT -- Cannot delete department with employees ON UPDATE CASCADE, CONSTRAINT fk_emp_manager FOREIGN KEY (manager_id) REFERENCES Employee(emp_id) ON DELETE SET NULL -- Manager leaves → subordinates manager-less ON UPDATE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_emp_department ON Employee(dept_id);CREATE INDEX idx_emp_manager ON Employee(manager_id);CREATE INDEX idx_emp_status ON Employee(employment_status); -- Now add department manager FKALTER TABLE DepartmentADD CONSTRAINT fk_dept_manager FOREIGN KEY (manager_id) REFERENCES Employee(emp_id) ON DELETE SET NULL -- Manager leaves → department temporarily without manager ON UPDATE CASCADE; CREATE INDEX idx_dept_manager ON Department(manager_id); -- Performance Review: Time-series relationshipCREATE TABLE PerformanceReview ( review_id SERIAL PRIMARY KEY, review_period VARCHAR(20) NOT NULL, -- e.g., '2024-Q1' review_date DATE NOT NULL, rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5), strengths TEXT, improvements TEXT, goals TEXT, -- FK to Employee: Total participation employee_id INT NOT NULL, -- FK to reviewer (also an employee) reviewer_id INT NOT NULL, CONSTRAINT fk_review_employee FOREIGN KEY (employee_id) REFERENCES Employee(emp_id) ON DELETE CASCADE -- Employee deleted → reviews deleted ON UPDATE CASCADE, CONSTRAINT fk_review_reviewer FOREIGN KEY (reviewer_id) REFERENCES Employee(emp_id) ON DELETE RESTRICT -- Cannot delete reviewer with active reviews ON UPDATE CASCADE, -- One review per employee per period CONSTRAINT uq_review_period UNIQUE (employee_id, review_period)); CREATE INDEX idx_review_employee ON PerformanceReview(employee_id);CREATE INDEX idx_review_reviewer ON PerformanceReview(reviewer_id); -- =========================================-- Sample Queries-- ========================================= -- Organizational chart: employees with managersSELECT e.first_name || ' ' || e.last_name AS employee, e.job_title, d.name AS department, m.first_name || ' ' || m.last_name AS managerFROM Employee eJOIN Department d ON e.dept_id = d.dept_idLEFT JOIN Employee m ON e.manager_id = m.emp_idWHERE e.employment_status = 'active'ORDER BY d.name, e.last_name; -- Department hierarchy with recursive CTEWITH RECURSIVE dept_tree AS ( -- Base: top-level departments SELECT dept_id, name, parent_dept_id, 0 AS level, name::TEXT AS path FROM Department WHERE parent_dept_id IS NULL UNION ALL -- Recursive: sub-departments SELECT d.dept_id, d.name, d.parent_dept_id, dt.level + 1, dt.path || ' > ' || d.name FROM Department d JOIN dept_tree dt ON d.parent_dept_id = dt.dept_id)SELECT * FROM dept_tree ORDER BY path; -- Find all subordinates (direct and indirect)WITH RECURSIVE subordinates AS ( -- Base: direct reports SELECT emp_id, first_name, last_name, manager_id, 1 AS level FROM Employee WHERE manager_id = 1 -- Starting manager ID UNION ALL -- Recursive: reports of reports SELECT e.emp_id, e.first_name, e.last_name, e.manager_id, s.level + 1 FROM Employee e JOIN subordinates s ON e.manager_id = s.emp_id)SELECT * FROM subordinates ORDER BY level, last_name;Notice the Department.manager_id FK is added after Employee table exists. This handles the circular dependency: Department references Employee (manager), and Employee references Department. Create tables first, add cross-references second.
Financial systems require extremely careful referential integrity handling because of regulatory and audit requirements. This example demonstrates a banking account-transaction structure.
Critical Design Principles for Financial Systems:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
-- =========================================-- Financial System - Banking Schema-- ========================================= -- Customer: Core entity with strict handlingCREATE TABLE Customer ( customer_id SERIAL PRIMARY KEY, customer_number VARCHAR(20) UNIQUE NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE, phone VARCHAR(20), date_of_birth DATE, ssn_last_four CHAR(4), -- Partial SSN for verification -- Soft delete: never actually delete customers is_active BOOLEAN DEFAULT TRUE, deactivated_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Account Types: Reference dataCREATE TABLE AccountType ( type_id SERIAL PRIMARY KEY, type_code VARCHAR(20) UNIQUE NOT NULL, type_name VARCHAR(100) NOT NULL, interest_rate DECIMAL(5,4) DEFAULT 0, min_balance DECIMAL(12,2) DEFAULT 0, monthly_fee DECIMAL(10,2) DEFAULT 0); -- Insert default account typesINSERT INTO AccountType (type_code, type_name, interest_rate, min_balance) VALUES('CHECKING', 'Checking Account', 0, 0),('SAVINGS', 'Savings Account', 0.0025, 100),('MONEY_MARKET', 'Money Market', 0.0100, 2500); -- Account: Child of Customer and AccountTypeCREATE TABLE Account ( account_id SERIAL PRIMARY KEY, account_number VARCHAR(20) UNIQUE NOT NULL, current_balance DECIMAL(15,2) NOT NULL DEFAULT 0, available_balance DECIMAL(15,2) NOT NULL DEFAULT 0, opened_date DATE NOT NULL DEFAULT CURRENT_DATE, -- Account status (soft close instead of delete) status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'frozen', 'closed')), closed_date DATE, -- FK to Customer: Total participation customer_id INT NOT NULL, -- FK to AccountType: Total participation type_id INT NOT NULL, CONSTRAINT fk_account_customer FOREIGN KEY (customer_id) REFERENCES Customer(customer_id) ON DELETE RESTRICT -- NEVER delete customers with accounts ON UPDATE CASCADE, CONSTRAINT fk_account_type FOREIGN KEY (type_id) REFERENCES AccountType(type_id) ON DELETE RESTRICT -- Cannot delete account types in use ON UPDATE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_account_customer ON Account(customer_id);CREATE INDEX idx_account_type ON Account(type_id);CREATE INDEX idx_account_status ON Account(status); -- Transaction: Critical financial record (NEVER delete)CREATE TABLE Transaction ( transaction_id SERIAL PRIMARY KEY, transaction_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, type VARCHAR(20) NOT NULL CHECK (type IN ('deposit', 'withdrawal', 'transfer', 'fee', 'interest')), amount DECIMAL(12,2) NOT NULL, description VARCHAR(500), reference_number VARCHAR(50) UNIQUE, -- Running balance after this transaction balance_after DECIMAL(15,2) NOT NULL, -- FK to Account: Total participation (transaction MUST belong to account) account_id INT NOT NULL, -- For transfers: the other account related_account_id INT, CONSTRAINT fk_transaction_account FOREIGN KEY (account_id) REFERENCES Account(account_id) ON DELETE RESTRICT -- NEVER allow account deletion with transactions ON UPDATE CASCADE, CONSTRAINT fk_transaction_related FOREIGN KEY (related_account_id) REFERENCES Account(account_id) ON DELETE RESTRICT ON UPDATE CASCADE , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Indexes for transaction queriesCREATE INDEX idx_trans_account ON Transaction(account_id);CREATE INDEX idx_trans_date ON Transaction(transaction_date);CREATE INDEX idx_trans_type ON Transaction(type);CREATE INDEX idx_trans_related ON Transaction(related_account_id); -- Audit Log: Track all changes to financial dataCREATE TABLE AuditLog ( audit_id SERIAL PRIMARY KEY, table_name VARCHAR(100) NOT NULL, record_id INT NOT NULL, action VARCHAR(20) NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')), old_values JSONB, new_values JSONB, changed_by VARCHAR(100), changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_audit_table ON AuditLog(table_name, record_id);CREATE INDEX idx_audit_date ON AuditLog(changed_at); -- =========================================-- Trigger for Audit Trail-- ========================================= CREATE OR REPLACE FUNCTION audit_account_changes()RETURNS TRIGGER AS $$BEGIN IF TG_OP = 'UPDATE' THEN INSERT INTO AuditLog (table_name, record_id, action, old_values, new_values, changed_by) VALUES ('Account', NEW.account_id, 'UPDATE', row_to_json(OLD)::JSONB, row_to_json(NEW)::JSONB, current_user); END IF; RETURN NEW;END;$$ LANGUAGE plpgsql; CREATE TRIGGER tr_audit_accountAFTER UPDATE ON AccountFOR EACH ROWEXECUTE FUNCTION audit_account_changes(); -- =========================================-- Sample Queries-- ========================================= -- Customer accounts with balancesSELECT c.first_name || ' ' || c.last_name AS customer, a.account_number, at.type_name, a.current_balance, a.statusFROM Customer cJOIN Account a ON c.customer_id = a.customer_idJOIN AccountType at ON a.type_id = at.type_idWHERE c.is_active = TRUEORDER BY c.last_name, a.account_number; -- Recent transactions for an accountSELECT t.transaction_date, t.type, t.amount, t.balance_after, t.descriptionFROM Transaction tWHERE t.account_id = 1ORDER BY t.transaction_date DESCLIMIT 50; -- Monthly statement (aggregated by type)SELECT type, COUNT(*) AS transaction_count, SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) AS total_credits, SUM(CASE WHEN amount < 0 THEN ABS(amount) ELSE 0 END) AS total_debitsFROM TransactionWHERE account_id = 1 AND transaction_date >= DATE_TRUNC('month', CURRENT_DATE)GROUP BY type;In financial systems, use RESTRICT for all critical FKs. Never CASCADE delete accounts or transactions. Use soft-delete patterns (status columns) instead. Financial records are often legally required for 7+ years.
Learning from mistakes is valuable. Here are common 1:N mapping anti-patterns and how to fix them.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
-- =========================================-- Anti-Pattern 1: Missing FK Constraint-- (Application thinks there's a relationship, but DB doesn't enforce it)-- ========================================= -- BAD: No constraintCREATE TABLE Order_Bad ( order_id INT PRIMARY KEY, customer_id INT -- No FK constraint!); -- This allows invalid data:INSERT INTO Order_Bad VALUES (1, 99999); -- Customer 99999 doesn't exist! -- GOOD: Proper constraintCREATE TABLE Order_Good ( order_id INT PRIMARY KEY, customer_id INT NOT NULL, CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES Customer(customer_id)); -- =========================================-- Anti-Pattern 2: Missing FK Index-- ========================================= -- BAD: FK defined but no indexCREATE TABLE Employee_Bad ( emp_id INT PRIMARY KEY, dept_id INT NOT NULL REFERENCES Department(dept_id) -- No index on dept_id!); -- Operations on Department become O(n) scans!-- DELETE FROM Department WHERE dept_id = 10;-- ↑ Scans entire Employee_Bad table to check references -- GOOD: Index the FKCREATE TABLE Employee_Good ( emp_id INT PRIMARY KEY, dept_id INT NOT NULL REFERENCES Department(dept_id));CREATE INDEX idx_emp_dept ON Employee_Good(dept_id); -- Critical! -- =========================================-- Anti-Pattern 3: Wrong Participation Mapping-- ========================================= -- BAD: Total participation but nullable FK-- Business rule: "Every order line MUST belong to an order"CREATE TABLE OrderLine_Bad ( line_id INT PRIMARY KEY, order_id INT, -- Nullable! Allows orphan order lines product_id INT NOT NULL); -- GOOD: NOT NULL enforces total participationCREATE TABLE OrderLine_Good ( line_id INT PRIMARY KEY, order_id INT NOT NULL, -- Cannot be NULL product_id INT NOT NULL, CONSTRAINT fk_orderline_order FOREIGN KEY (order_id) REFERENCES "Order"(order_id));We've completed a comprehensive journey through one-to-many relationship mapping, from conceptual understanding to production-ready implementations. Let's consolidate everything:
| Decision | Consideration | Implementation |
|---|---|---|
| Child must have parent? | Total participation | FK NOT NULL |
| Child can be orphan? | Partial participation | FK nullable (allow NULL) |
| Delete parent with children? | Weak entity / dependent data | ON DELETE CASCADE |
| Preserve children on delete? | Independent entities | ON DELETE RESTRICT or SET NULL |
| Performance on joins? | All 1:N relationships | CREATE INDEX on FK column |
| Circular references? | Cross-table dependencies | DEFERRABLE constraints |
Congratulations! You have mastered the mapping of one-to-many relationships—the most common pattern in relational database design. You understand the theory, the implementation techniques, the edge cases, and have seen production-quality examples across multiple domains. You're ready to design robust, well-constrained database schemas.