Files
english/.opencode/skills/databases/transactional.md
2026-04-12 01:06:31 +07:00

4.7 KiB

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:

-- 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:

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:

-- 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

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:

-- 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

-- ❌ BAD: FK without index → slow JOINs
FOREIGN KEY (user_id) REFERENCES users(id)
-- Forgot CREATE INDEX

Over-indexing

-- ❌ 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

-- ❌ 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

-- ❌ 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

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