177 lines
4.7 KiB
Markdown
177 lines
4.7 KiB
Markdown
# 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
|