Guidelines for schema design, migrations, queries, data modeling, indexing, and data integrity
Use when designing schemas, writing migrations, creating queries, modeling data relationships, or working on data access layers.
types/)| Element | Convention | Example |
|---|---|---|
| Tables | plural snake_case | user_profiles, order_items |
| Columns | singular snake_case | first_name, created_at |
| Primary keys | id | id (auto-generated) |
| Foreign keys | <referenced_table_singular>_id | user_id, order_id |
| Boolean columns | is_/has_ prefix | is_active, has_verified |
| Timestamps | _at suffix | created_at, updated_at, deleted_at |
| Indexes | idx_<table>_<columns> | idx_users_email |
id -- Primary key (UUID or auto-increment)
created_at -- When the record was created (UTC timestamp)
updated_at -- When the record was last modified (UTC timestamp)
Prefer soft deletes over hard deletes for important data:
deleted_at -- NULL if active, timestamp if soft-deleted
Always filter by WHERE deleted_at IS NULL in queries (or use a view/scope).
up and down — every migration must be reversibleadd_email_index_to_users not migration_042up and down (forward and rollback)For breaking schema changes, use the expand-contract pattern:
SELECT * in application codePrefer cursor-based pagination for large/changing datasets:
-- Instead of: OFFSET 1000 LIMIT 20 (slow for high offsets)
-- Use: WHERE id > :last_seen_id ORDER BY id LIMIT 20
Use offset pagination only for small, static datasets.
| Scenario | Index type |
|---|---|
Filter by column (WHERE x = ?) | Single column index |
| Filter by multiple columns | Composite index (most selective column first) |
Sort results (ORDER BY x) | Index on sort column |
| Unique constraint | Unique index |
| Full-text search | Full-text / GIN index |
| JSON field queries | GIN / expression index |
quantity > 0, status IN (...))All three layers should agree, but the database is the ultimate truth.
SELECT * in new queries