database-design
Design database schemas — tables, relationships, indexes, constraints, and ORM setup. Covers relational design, normalization, and common patterns.
Install
npx skills add https://github.com/spencerpauly/awesome-cursor-skills/tree/main/resources/database-design
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install spencerpauly-awesome-cursor-skills@llmmart
git clone https://github.com/spencerpauly/awesome-cursor-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole spencerpauly/awesome-cursor-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Database Design
Design a database schema from requirements.
Workflow
1. Identify Entities
From the requirements, extract the core entities (nouns):
- Users, Teams, Projects, Tasks, Comments, etc.
- Each entity becomes a table
2. Define Relationships
| Relationship | Implementation |
|---|---|
| One-to-one | Foreign key with unique constraint, or embed in same table |
| One-to-many | Foreign key on the "many" side |
| Many-to-many | Junction/join table |
| Self-referential | Foreign key pointing to same table (e.g. parent_id) |
3. Design the Schema
For each table:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
4. Apply Best Practices
Primary keys:
- Use
UUIDfor distributed systems or public-facing IDs - Use
SERIAL/BIGSERIALfor internal-only IDs (faster joins)
Timestamps:
- Always add
created_atandupdated_at - Use
TIMESTAMPTZ(with timezone), neverTIMESTAMP
Naming:
- Tables: plural snake_case (
users,project_members) - Columns: singular snake_case (
user_id,created_at) - Indexes:
idx_<table>_<columns>(idx_users_email)
Constraints:
NOT NULLon everything unless it's genuinely optionalUNIQUEon natural keys (email, slug, external IDs)REFERENCESwithON DELETEbehavior (CASCADE, SET NULL, RESTRICT)CHECKconstraints for enums or value ranges
5. Add Indexes
-- For columns you filter/sort by frequently
CREATE INDEX idx_projects_owner_id ON projects(owner_id);
-- For unique lookups
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Composite for common query patterns
CREATE INDEX idx_tasks_project_status ON tasks(project_id, status);
When to index:
- Foreign keys (almost always)
- Columns in WHERE clauses
- Columns in ORDER BY
- Columns in JOIN conditions
When NOT to index:
- Small tables (<1000 rows)
- Columns with low cardinality (boolean, status with 3 values)
- Columns that are rarely queried
6. ORM Setup
Prisma:
model User {
id String @id @default(uuid())
email String @unique
name String
projects Project[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
Drizzle:
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
Common Patterns
Soft deletes: Add deleted_at TIMESTAMPTZ instead of actually deleting rows
Audit log: Separate audit_events table with entity_type, entity_id, action, actor_id, payload
Tags/labels: Junction table (task_tags) with task_id + tag_id
Tree/hierarchy: parent_id self-reference, or materialized path (/1/4/7/)
Polymorphic associations: Use entity_type + entity_id columns (avoid if possible, prefer separate FKs)
Tips
- Start normalized (3NF), denormalize only when you have measured performance problems
- Don't store derived data unless you have a caching/invalidation strategy
- Use database enums or check constraints for status fields, not free-text
- Always think about what happens when you delete a parent record
Files (awesome-cursor-skills)
-
SKILL.md 4 KB
--- name: database-design description: Design database schemas — tables, relationships, indexes, constraints, and ORM setup. Covers relational design, normalization, and common patterns. user-invocable: true --- # Database Design Design a database schema from requirements. ## Workflow ### 1. Identify Entities From the requirements, extract the core entities (nouns): - Users, Teams, Projects, Tasks, Comments, etc. - Each entity becomes a table ### 2. Define Relationships | Relationship | Implementation | |-------------|----------------| | One-to-one | Foreign key with unique constraint, or embed in same table | | One-to-many | Foreign key on the "many" side | | Many-to-many | Junction/join table | | Self-referential | Foreign key pointing to same table (e.g. `parent_id`) | ### 3. Design the Schema For each table: ```sql CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, avatar_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` ### 4. Apply Best Practices **Primary keys:** - Use `UUID` for distributed systems or public-facing IDs - Use `SERIAL`/`BIGSERIAL` for internal-only IDs (faster joins) **Timestamps:** - Always add `created_at` and `updated_at` - Use `TIMESTAMPTZ` (with timezone), never `TIMESTAMP` **Naming:** - Tables: plural snake_case (`users`, `project_members`) - Columns: singular snake_case (`user_id`, `created_at`) - Indexes: `idx_<table>_<columns>` (`idx_users_email`) **Constraints:** - `NOT NULL` on everything unless it's genuinely optional - `UNIQUE` on natural keys (email, slug, external IDs) - `REFERENCES` with `ON DELETE` behavior (CASCADE, SET NULL, RESTRICT) - `CHECK` constraints for enums or value ranges ### 5. Add Indexes ```sql -- For columns you filter/sort by frequently CREATE INDEX idx_projects_owner_id ON projects(owner_id); -- For unique lookups CREATE UNIQUE INDEX idx_users_email ON users(email); -- Composite for common query patterns CREATE INDEX idx_tasks_project_status ON tasks(project_id, status); ``` **When to index:** - Foreign keys (almost always) - Columns in WHERE clauses - Columns in ORDER BY - Columns in JOIN conditions **When NOT to index:** - Small tables (<1000 rows) - Columns with low cardinality (boolean, status with 3 values) - Columns that are rarely queried ### 6. ORM Setup **Prisma:** ```prisma model User { id String @id @default(uuid()) email String @unique name String projects Project[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("users") } ``` **Drizzle:** ```typescript export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), email: text('email').notNull().unique(), name: text('name').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); ``` ## Common Patterns **Soft deletes:** Add `deleted_at TIMESTAMPTZ` instead of actually deleting rows **Audit log:** Separate `audit_events` table with `entity_type`, `entity_id`, `action`, `actor_id`, `payload` **Tags/labels:** Junction table (`task_tags`) with `task_id` + `tag_id` **Tree/hierarchy:** `parent_id` self-reference, or materialized path (`/1/4/7/`) **Polymorphic associations:** Use `entity_type` + `entity_id` columns (avoid if possible, prefer separate FKs) ## Tips - Start normalized (3NF), denormalize only when you have measured performance problems - Don't store derived data unless you have a caching/invalidation strategy - Use database enums or check constraints for status fields, not free-text - Always think about what happens when you delete a parent record
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.