init
This commit is contained in:
176
.opencode/skills/databases/transactional.md
Normal file
176
.opencode/skills/databases/transactional.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user