# Transactional (OLTP) Rules

> **Note:** Core naming conventions, workflow, and checklist are in `SKILL.md` or `db-design.md` (always loaded).

Guidelines for designing schemas for day-to-day business operations.

---

## Normalization Principles

### Prefer 3NF (Third Normal Form)

- Each table represents one clear entity/relationship
- No repeating information that can be referenced (use FK)
- Clear separation:
  - `orders` (header) vs `order_items` (line items)
  - `products` vs `product_variants`, `product_prices`

### Foreign Key Constraints

Use FK with appropriate ON DELETE / ON UPDATE:

```sql
-- Cascade: delete order → delete order_items
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE

-- Restrict: cannot delete user if orders exist
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT

-- Set null: delete category → product.category_id = NULL
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
```

---

## Indexing Rules

### 1. Primary Key
- Usually `BIGINT` auto-increment or UUID
- Format: `PRIMARY KEY (id)`

### 2. Foreign Key Indexes
**IMPORTANT**: Create indexes for ALL foreign keys for efficient JOINs:
```sql
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
```

### 3. Frequently Filtered Columns
Index columns commonly used in WHERE:
- `status`, `created_at`, `updated_at`
- Code/reference columns: `order_number`, `sku`

### 4. Composite Indexes
Based on actual query patterns:
```sql
-- Query: WHERE user_id = ? AND status = ? ORDER BY created_at DESC
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC);

-- Query: WHERE store_id = ? AND created_at BETWEEN ...
CREATE INDEX idx_orders_store_created ON orders(store_id, created_at);
```

**Composite index rules:**
- Put columns with **high selectivity** (fewer duplicate values) first
- Avoid duplicate/redundant indexes
- Index should cover WHERE + ORDER BY of query

### 5. Unique Constraints
```sql
UNIQUE (order_number)
UNIQUE (sku)
UNIQUE (user_id, email)  -- compound unique
```

---

## Soft Delete Pattern

When you need to keep deleted data instead of permanently deleting:

```sql
-- Add deleted_at column
deleted_at TIMESTAMP NULL

-- Partial index for non-deleted records (PostgreSQL)
CREATE INDEX idx_orders_active ON orders(user_id, status)
    WHERE deleted_at IS NULL;

-- Query only active records
SELECT * FROM orders WHERE deleted_at IS NULL;
```

---

## Anti-patterns to Avoid

### Missing FK Index
```sql
-- ❌ BAD: FK without index → slow JOINs
FOREIGN KEY (user_id) REFERENCES users(id)
-- Forgot CREATE INDEX
```

### Over-indexing
```sql
-- ❌ BAD: Indexing each column separately
CREATE INDEX idx_a ON orders(user_id);
CREATE INDEX idx_b ON orders(status);
CREATE INDEX idx_c ON orders(created_at);

-- ✅ GOOD: Composite index based on query pattern
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC);
```

### Using TEXT instead of ENUM
```sql
-- ❌ BAD: Cannot validate values
status TEXT

-- ✅ GOOD: Use ENUM or CHECK
status ENUM('pending', 'confirmed', 'shipped', 'cancelled')
-- or
status VARCHAR(32) CHECK (status IN ('pending', 'confirmed', 'shipped'))
```

### Missing Audit Columns
```sql
-- ❌ BAD
CREATE TABLE products (id INT, name VARCHAR(255));

-- ✅ GOOD
CREATE TABLE products (
    id INT,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```

---

## Example DDL

```sql
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_number VARCHAR(50) NOT NULL,
    user_id BIGINT NOT NULL,
    status VARCHAR(32) NOT NULL DEFAULT 'pending',
    subtotal DECIMAL(18,2) NOT NULL DEFAULT 0,
    discount_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
    total_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE (order_number),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC);
```

---

## Checklist

- [ ] Audit columns: `created_at`, `updated_at`
- [ ] All FKs have indexes
- [ ] Unique constraints for business keys (`order_number`, `sku`, `email`)
- [ ] ENUM or CHECK for status/type columns
- [ ] Composite index based on main query patterns
- [ ] Soft delete if needed: `deleted_at` + partial index
