Data Modeling & Persistence
This skill ensures your data layer is scalable, consistent, and maintainable. It covers schema design, relationship patterns, and safe migration workflows.
When to Use
- Designing a new database schema
- Adding features that require new tables/collections
- Choosing a database technology (SQL vs NoSQL)
- Writing migration scripts
- Optimizing query performance via indexing
1. Schema Design Principles (SQL)
Normalization
- 1NF: Atomic values (no comma-separated lists in columns).
- 2NF: No partial dependencies (everything depends on Primary Key).
- 3NF: No transitive dependencies (no non-key columns depending on other non-key columns).
- Denormalization: Only strictly for read-performance (e.g.,
cached_user_name on comments table).
Naming Conventions
- Tables: Plural snake_case (
users, order_items).
- Columns: specific snake_case (
created_at, is_active, user_id).
- Primary Keys:
id (UUIDv7 or BigInt preferred over random UUIDv4 for indexing).
- Foreign Keys:
[table]_id or [entity]_id.
Field Types
- Timestamps: Always include
created_at and updated_at.
- Booleans: Prefix with
is_ or has_ (e.g., is_published).
- Enums: Use native Enums for fixed sets, or reference tables for dynamic sets.
2. Relationship Patterns
One-to-One (1:1)
One-to-Many (1:N)
Many-to-Many (M:N)
- Use Case: Students <-> Classes, Tags <-> Articles.
- Pattern: Junction (Join) Table.
CREATE TABLE article_tags (
article_id UUID REFERENCES articles(id),
tag_id UUID REFERENCES tags(id),
PRIMARY KEY (article_id, tag_id)
);
3. Indexing Strategies
Proper indexing is the #1 factor in database performance.
Index Types
| Type |
Use Case |
Example |
| B-tree |
Default. Equality (=) and Range (<, >) |
created_at, user_id, email |
| Hash |
Equality only. Smaller size. |
Exact UUID lookups (Postgres only) |
| GIN |
JSONB, Arrays, Full-text search |
metadata JSONB column |
| GiST |
Geometric data, generic ranges |
Location data, Time ranges |
Guidelines
- Primary Keys: Indexed automatically.
- Foreign Keys: ALWAYS index these. Most joins happen here.
- Filters: Index columns frequently used in
WHERE clauses.
- Composite: Index
(a, b) if you query WHERE a = x AND b = y. Order matters (left-to-right).
- Partial: Index subset of data.
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active'.
When NOT to Index
- Low Cardinality: Columns with few unique values (e.g.,
gender, is_active boolean) relative to table size. The optimizer will ignore them in favor of a table scan.
- Write-Heavy Tables: Every index slows down
INSERT/UPDATE operations. DELETE unused indexes.
4. ORM Best Practices
Prisma / Drizzle / TypeORM
- Selection: Select only fields you need.
- ❌
findMany() (selects all)
- ✅
findMany({ select: { id: true, name: true } })
- Transactions: Wrap multiple related writes in a transaction.
- N+1 Prevention: Use
include or with to fetch relations in a single query.
- ❌ Loop over users and fetch profile for each.
- ✅ Fetch users
with profiles in one go.
5. Migration Workflow
Evolving the schema without downtime or data loss.
- Modify Schema: Update
schema.prisma or Drizzle schema.
- Generate Migration: Create SQL file.
npx prisma migrate dev --name add_profile.
- Review SQL: Check for destructive actions (DROPs).
- Apply: Run migration.
- Regenerate Client: Update Typescript types.
Zero-Downtime Strategies
- Adding Required Column:
- Add column as
NULL.
- Backfill data for existing rows.
- Alter column to
NOT NULL.
- Renaming Column:
- Add new column.
- Double-write to both old and new columns in app.
- Backfill old data to new column.
- Switch reads to new column.
- Remove old column.
6. Query Performance
Explain Analyze
Before optimizing, measure.
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
Look for Seq Scan (bad) vs Index Scan (good) on large tables.
Common Performance Killers
- SELECT *: Fetches unnecessary data (especially bad with JSONB/Text columns).
- N+1 Queries: See ORM section.
- Unindexed Joins: Joining on columns without indexes forces a full table hash join.
- Deep Offset Pagination:
OFFSET 100000 scans and discards 100k rows. Use Cursor/Keyset pagination instead (WHERE created_at < last_seen_date).
7. NoSQL Patterns (MongoDB / DynamoDB)
MongoDB Aggregation Pipelining
Move logic to the DB, not the app.
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
]);
DynamoDB Indexing (GSI)
- Partition Key (PK): Uniform distribution is key.
- Sort Key (SK): Enables range queries and sorting.
- Global Secondary Index (GSI): Create a "view" of your data with a different PK/SK to support alternative access patterns.
- Main Table PK:
UserId.
- GSI PK:
Email.
- Allows lookup by UserID AND Email.
8. Advanced Patterns
Deep-dive patterns located in patterns/:
- Soft Deletes: Safely "archiving" data instead of permanent deletion.
- Audit Trails: tracking who changed what and when.
- Polymorphic Associations: Relationships that can point to multiple table types.
Deployment Checklist
1---2name: data-modeling3description: Use when designing database schemas, adding tables/collections, choosing SQL vs NoSQL, writing migration scripts, or optimizing query performance via indexing. Covers normalization, relationship patterns, ORM best practices, and zero-downtime migrations.4---56# Data Modeling & Persistence78This skill ensures your data layer is scalable, consistent, and maintainable. It covers schema design, relationship patterns, and safe migration workflows.910## When to Use1112- Designing a new database schema13- Adding features that require new tables/collections14- Choosing a database technology (SQL vs NoSQL)15- Writing migration scripts16- Optimizing query performance via indexing1718## 1. Schema Design Principles (SQL)1920### Normalization2122- **1NF**: Atomic values (no comma-separated lists in columns).23- **2NF**: No partial dependencies (everything depends on Primary Key).24- **3NF**: No transitive dependencies (no non-key columns depending on other non-key columns).25- **Denormalization**: Only strictly for read-performance (e.g., `cached_user_name` on `comments` table).2627### Naming Conventions2829- **Tables**: Plural snake_case (`users`, `order_items`).30- **Columns**: specific snake_case (`created_at`, `is_active`, `user_id`).31- **Primary Keys**: `id` (UUIDv7 or BigInt preferred over random UUIDv4 for indexing).32- **Foreign Keys**: `[table]_id` or `[entity]_id`.3334### Field Types3536- **Timestamps**: Always include `created_at` and `updated_at`.37- **Booleans**: Prefix with `is_` or `has_` (e.g., `is_published`).38- **Enums**: Use native Enums for fixed sets, or reference tables for dynamic sets.3940## 2. Relationship Patterns4142### One-to-One (1:1)4344- **Use Case**: User Profile, Config Settings.45- **Pattern**: Unique Foreign Key on the "child" table.46 ```sql47 CREATE TABLE profiles (48 user_id UUID UNIQUE REFERENCES users(id)49 );50 ```5152### One-to-Many (1:N)5354- **Use Case**: User -> Posts, Team -> Players.55- **Pattern**: Foreign Key on the "Many" side.56 ```sql57 CREATE TABLE posts (58 author_id UUID REFERENCES users(id)59 );60 ```6162### Many-to-Many (M:N)6364- **Use Case**: Students <-> Classes, Tags <-> Articles.65- **Pattern**: Junction (Join) Table.66 ```sql67 CREATE TABLE article_tags (68 article_id UUID REFERENCES articles(id),69 tag_id UUID REFERENCES tags(id),70 PRIMARY KEY (article_id, tag_id)71 );72 ```7374## 3. Indexing Strategies7576Proper indexing is the #1 factor in database performance.7778### Index Types7980| Type | Use Case | Example |81| ---------- | -------------------------------------- | ---------------------------------- |82| **B-tree** | Default. Equality (=) and Range (<, >) | `created_at`, `user_id`, `email` |83| **Hash** | Equality only. Smaller size. | Exact UUID lookups (Postgres only) |84| **GIN** | JSONB, Arrays, Full-text search | `metadata` JSONB column |85| **GiST** | Geometric data, generic ranges | Location data, Time ranges |8687### Guidelines8889- **Primary Keys**: Indexed automatically.90- **Foreign Keys**: ALWAYS index these. Most joins happen here.91- **Filters**: Index columns frequently used in `WHERE` clauses.92- **Composite**: Index `(a, b)` if you query `WHERE a = x AND b = y`. Order matters (left-to-right).93- **Partial**: Index subset of data. `CREATE INDEX idx_active_users ON users(email) WHERE status = 'active'`.9495### When NOT to Index9697- **Low Cardinality**: Columns with few unique values (e.g., `gender`, `is_active` boolean) relative to table size. The optimizer will ignore them in favor of a table scan.98- **Write-Heavy Tables**: Every index slows down `INSERT`/`UPDATE` operations. DELETE unused indexes.99100## 4. ORM Best Practices101102### Prisma / Drizzle / TypeORM103104- **Selection**: Select only fields you need.105 - ❌ `findMany()` (selects _all_)106 - ✅ `findMany({ select: { id: true, name: true } })`107- **Transactions**: Wrap multiple related writes in a transaction.108- **N+1 Prevention**: Use `include` or `with` to fetch relations in a single query.109 - ❌ Loop over users and fetch profile for each.110 - ✅ Fetch users `with` profiles in one go.111112## 5. Migration Workflow113114Evolving the schema without downtime or data loss.1151161. **Modify Schema**: Update `schema.prisma` or Drizzle schema.1172. **Generate Migration**: Create SQL file. `npx prisma migrate dev --name add_profile`.1183. **Review SQL**: Check for destructive actions (DROPs).1194. **Apply**: Run migration.1205. **Regenerate Client**: Update Typescript types.121122### Zero-Downtime Strategies123124- **Adding Required Column**:125 1. Add column as `NULL`.126 2. Backfill data for existing rows.127 3. Alter column to `NOT NULL`.128- **Renaming Column**:129 1. Add new column.130 2. Double-write to both old and new columns in app.131 3. Backfill old data to new column.132 4. Switch reads to new column.133 5. Remove old column.134135## 6. Query Performance136137### Explain Analyze138139Before optimizing, measure.140141```sql142EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';143```144145Look for `Seq Scan` (bad) vs `Index Scan` (good) on large tables.146147### Common Performance Killers148149- **SELECT \***: Fetches unnecessary data (especially bad with JSONB/Text columns).150- **N+1 Queries**: See ORM section.151- **Unindexed Joins**: Joining on columns without indexes forces a full table hash join.152- **Deep Offset Pagination**: `OFFSET 100000` scans and discards 100k rows. Use Cursor/Keyset pagination instead (`WHERE created_at < last_seen_date`).153154## 7. NoSQL Patterns (MongoDB / DynamoDB)155156### MongoDB Aggregation Pipelining157158Move logic to the DB, not the app.159160```js161db.orders.aggregate([162 { $match: { status: "completed" } },163 { $group: { _id: "$customerId", total: { $sum: "$amount" } } },164]);165```166167### DynamoDB Indexing (GSI)168169- **Partition Key (PK)**: Uniform distribution is key.170- **Sort Key (SK)**: Enables range queries and sorting.171- **Global Secondary Index (GSI)**: Create a "view" of your data with a different PK/SK to support alternative access patterns.172 - _Main Table PK_: `UserId`.173 - _GSI PK_: `Email`.174 - Allows lookup by UserID AND Email.175176## 8. Advanced Patterns177178Deep-dive patterns located in `patterns/`:179180- **[Soft Deletes](patterns/soft-delete.md)**: Safely "archiving" data instead of permanent deletion.181- **[Audit Trails](patterns/audit-trails.md)**: tracking who changed what and when.182- **[Polymorphic Associations](patterns/polymorphic-associations.md)**: Relationships that can point to multiple table types.183184## Deployment Checklist185186- [ ] **Indexes** created for all Foreign Keys and query filters?187- [ ] **Unique Constraints** enforced at DB level?188- [ ] **Cascade rules** (Delete/Update) defined?189- [ ] **Data types** appropriate (e.g., Decimal for money, UUID for IDs)?190- [ ] **Migration safety** checked (no locking of large tables)?