Database Design Guide

Normal Forms

FormRuleViolation Example
1NFAtomic values, no repeating groups, unique rowsStoring "tag1,tag2" in one column
2NF1NF + no partial dependency on composite keyorder_items table storing product_name (depends only on product_id)
3NF2NF + no transitive dependency (non-key → non-key)Storing zip_code and city together (city depends on zip, not PK)
BCNF3NF + every determinant is a candidate keyInstructor → room (instructor is not a candidate key)
DenormalizationIntentional rule violation for read performanceStoring order_total instead of recalculating each time

Relationship Types

RelationshipImplementationExample
One-to-OneFK in either table (or shared PK)user → user_profile
One-to-ManyFK on the "many" sideuser → orders (user_id FK in orders)
Many-to-ManyJunction table with two FKsorders ↔ products via order_items
Self-ReferentialFK references same tableemployees.manager_id → employees.id
PolymorphicType column + FK to multiple tablescomments (commentable_type, commentable_id)

Schema Design Patterns

-- E-commerce schema example (normalized) CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, sku VARCHAR(64) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, price NUMERIC(10, 2) NOT NULL CHECK (price >= 0), category_id INT REFERENCES categories(id) ); CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','paid','shipped','completed','cancelled')), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE order_items ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, product_id BIGINT NOT NULL REFERENCES products(id), quantity INT NOT NULL CHECK (quantity > 0), unit_price NUMERIC(10, 2) NOT NULL, -- snapshot price at purchase time UNIQUE (order_id, product_id) ); -- Soft delete pattern ALTER TABLE products ADD COLUMN deleted_at TIMESTAMPTZ; -- Filter: WHERE deleted_at IS NULL

Naming Conventions & Anti-Patterns

TopicGood PracticeAnti-Pattern
Table namesPlural, snake_case: order_itemstblOrderItem, OrderItem
Primary keysid (surrogate) or natural keyOrderID (camelCase), oid (obscure)
Foreign keysreferenced_table_id: user_idusr, fk1, parent
Booleansis_active, has_verified_emailactive (ambiguous), flag
Datescreated_at, updated_at (with TZ)date1, ts (no timezone)
EAV patternAvoid for structured dataentity_attribute_value tables for everything
NULL usageUse NULL for unknown; avoid for "false"Empty string "" instead of NULL