Files
gh-byunk-claude-code-toolki…/skills/erd-skill/best-practices.md
2025-11-29 18:02:57 +08:00

6.2 KiB

Database Design Best Practices

Normalization

First Normal Form (1NF)

  • Each column contains atomic (indivisible) values
  • Each column contains values of a single type
  • Each column has a unique name
  • The order of rows doesn't matter

Second Normal Form (2NF)

  • Meets all requirements of 1NF
  • No partial dependencies (all non-key attributes fully depend on the primary key)
  • Relevant for composite primary keys

Third Normal Form (3NF)

  • Meets all requirements of 2NF
  • No transitive dependencies (non-key attributes don't depend on other non-key attributes)
  • Most common target normalization level

Denormalization

Consider denormalization for:

  • Read-heavy workloads where query performance is critical
  • Aggregated data that's expensive to compute
  • Historical snapshots that shouldn't change

Naming Conventions

Tables

  • Use plural nouns (e.g., users, posts, orders)
  • Use snake_case for multi-word names (e.g., user_profiles, order_items)
  • Keep names descriptive but concise

Columns

  • Use singular nouns (e.g., id, name, email)
  • Use snake_case for multi-word names (e.g., created_at, user_id)
  • Boolean columns should use prefixes like is_, has_, can_ (e.g., is_active, has_verified_email)

Foreign Keys

  • Use format: {referenced_table_singular}_id (e.g., user_id, post_id)
  • Be consistent across the entire schema

Indexes

  • Name format: idx_{table}_{columns} or {table}_{columns}_idx
  • Include purpose when relevant: idx_users_email_unique, idx_posts_author_created

Primary Keys

Auto-incrementing Integers

Table users {
  id integer [primary key, increment]
}

Pros: Simple, compact, sequential, human-readable Cons: Exposes record count, potential security concern, not globally unique

UUIDs

Table users {
  id uuid [primary key]
}

Pros: Globally unique, can be generated client-side, harder to enumerate Cons: Larger storage, less human-readable, non-sequential (worse for indexing)

Composite Keys

Table user_roles {
  user_id integer
  role_id integer

  indexes {
    (user_id, role_id) [primary key]
  }
}

Use when: Natural composite identifier exists and makes sense

Indexing Strategies

Single-column Indexes

Create indexes on columns frequently used in:

  • WHERE clauses
  • JOIN conditions
  • ORDER BY clauses
  • Foreign keys

Composite Indexes

  • Order matters: most selective column first
  • Consider query patterns when designing
  • Can satisfy multiple query types
Table posts {
  id integer [primary key]
  author_id integer
  status varchar
  created_at timestamp

  indexes {
    (author_id, created_at) [name: 'idx_posts_author_created']
    status [name: 'idx_posts_status']
  }
}

Unique Indexes

Use for:

  • Ensuring data uniqueness (e.g., email, username)
  • Natural keys
  • Business constraints
Table users {
  id integer [primary key]
  email varchar [unique]
  username varchar [unique]
}

Relationships

One-to-Many

Most common relationship type. Use foreign key in the "many" table.

Table users {
  id integer [primary key]
}

Table posts {
  id integer [primary key]
  author_id integer [ref: > users.id]
}

Many-to-Many

Requires junction/join table.

Table users {
  id integer [primary key]
}

Table roles {
  id integer [primary key]
}

Table user_roles {
  user_id integer [ref: > users.id]
  role_id integer [ref: > roles.id]

  indexes {
    (user_id, role_id) [primary key]
  }
}

One-to-One

Less common. Can be modeled with unique foreign key or by merging tables.

Table users {
  id integer [primary key]
}

Table user_profiles {
  id integer [primary key]
  user_id integer [ref: - users.id, unique]
}

Constraints and Validation

NOT NULL

Use for required fields:

Table users {
  id integer [primary key]
  email varchar [not null]
  username varchar [not null]
}

Default Values

Provide sensible defaults:

Table posts {
  id integer [primary key]
  status varchar [default: 'draft']
  created_at timestamp [default: `now()`]
}

Check Constraints

Enforce business rules at the database level:

Table products {
  id integer [primary key]
  price decimal [note: 'CHECK (price >= 0)']
  stock integer [note: 'CHECK (stock >= 0)']
}

Common Patterns

Timestamps

If tracking creation and update times has business value:

Table posts {
  id integer [primary key]
  // ... other fields
  created_at timestamp [not null, default: `now()`]
  updated_at timestamp [not null, default: `now()`]
}

Soft Deletes

For preserving deleted records:

Table users {
  id integer [primary key]
  // ... other fields
  deleted_at timestamp [null]
}

Polymorphic Associations

When a table can belong to multiple parent types:

Table comments {
  id integer [primary key]
  commentable_type varchar // 'Post' or 'Product'
  commentable_id integer   // ID of Post or Product
  content text
}

Audit Trail

Track changes to important data:

Table order_history {
  id integer [primary key]
  order_id integer [ref: > orders.id]
  changed_by integer [ref: > users.id]
  old_status varchar
  new_status varchar
  changed_at timestamp [default: `now()`]
}

Versioning

For maintaining version history:

Table documents {
  id integer [primary key]
  version integer [not null, default: 1]
  // ... other fields

  indexes {
    (id, version) [unique]
  }
}

Performance Considerations

Avoid Over-indexing

  • Each index adds write overhead
  • Maintain only indexes that are actually used
  • Monitor query performance and adjust

Use Appropriate Data Types

  • VARCHAR vs TEXT: Use VARCHAR with appropriate length
  • INT vs BIGINT: Choose based on expected range
  • DECIMAL for money: Avoid floating point for currency
  • TIMESTAMP vs DATE: Use appropriate precision

Schema Organization

Use Table Groups

Organize related tables:

TableGroup user_management {
  users
  user_profiles
  user_roles
}

TableGroup content {
  posts
  comments
  tags
}

Use Schemas/Namespaces

For large applications:

Table auth.users {
  id integer [primary key]
}

Table cms.posts {
  id integer [primary key]
  author_id integer [ref: > auth.users.id]
}