Skip to content
Back to skills

Database Design

ASecurity

Design database schemas — tables, relationships, indexes, constraints, and ORM setup. Covers relational design, normalization, and common patterns.

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 10, 2026
databasestypescriptsqldatabaseperformance

Security analysis

A100/100

Scanned September 10, 2026

npx -y skills add 0xAidan/polymarket-bot-test --skill database-design --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Database Design?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Database Design
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/0xaidan-database-design/badge)](https://www.skillsdirectory.com/skills/0xaidan-database-design)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.