agentleFS
VictoriakaeyVictoriakaey/build-reliable-agents/skills/database-design/SKILL.md

database-design

Skill0 starsChanged 7 months agoMITView on GitHub

Use when designing a database schema for a new system, or when an existing schema needs to be revised. Two modes: (1) general schema design for any application, (2) AI-specific schema design for systems with conversation history, embeddings, agent state, or LLM outputs. Guides through data modeling decisions, type choices, indexing, and AI-specific storage patterns.

58 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use intelligent-routing, bash-linux and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • SKB3002Skill

    intelligent-routing

    Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.

    Also in 6 projects that use this

  • xenitV1Skill

    bash-linux

    Bash/Linux terminal patterns. Critical commands, piping, error handling, scripting. Use when working on macOS or Linux systems.

    Also in 7 projects that use this

  • xenitV1Skill

    red-team-tactics

    Red team tactics principles based on MITRE ATT&CK. Attack phases, detection evasion, reporting.

    Also in 7 projects that use this

  • pepebot-spaceSkill

    llm-application-dev

    Building applications with Large Language Models - prompt engineering, RAG patterns, and LLM integration. Use for AI-powered features, chatbots, or LLM-based automation.

    Also in 4 projects that use this

  • +8 more from xenitV1

The file

We split this skill into the rules it gives an agent

12 rules. 0 are common, 11 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 55 edited versions we read keep these.

  • 11

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before creating any tables or collections, answer these questions:

    line 28
  2. Distinctive:

    Do not design any tables until you can answer all of these.

    line 50
  3. Distinctive:

    Index columns you filter or sort by frequently: Every index speeds up reads but slows down writes.

    line 109
  4. Distinctive:

    Always index foreign keys

    line 123
  5. Distinctive:

    Always index columns used in WHERE clauses on large tables

    line 124
  6. Distinctive:

    Never index columns with very low cardinality (e.g.

    line 125
  7. Distinctive:

    Don't over-index — each index adds write overhead

    line 126
  8. Distinctive:

    Use this in addition to Mode 1 when your system involves any of:

    line 148
  9. Very specific:

    Use JSONB for metadata — model versions, latency, tool calls, etc.

    line 187
  10. Distinctive:

    When you need to store and audit LLM outputs separately from conversation messages:

    line 195
  11. Distinctive:

    Use HNSW index for production (faster queries).

    line 256
  12. Distinctive:

    Before finalizing an AI system schema:

    line 319

“Common” is measured: kept by at least 6 in 10 of the 55 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Database Design

The Core Problem

Bad schema design is expensive to fix later. The most common mistakes are: modeling the wrong entities, choosing the wrong database type for the use case, not thinking about query patterns before designing tables, and — for AI systems specifically — not accounting for the unique storage requirements of conversations, embeddings, and agent state.

The discipline: Understand the query patterns before designing the schema. The schema should make your most common queries fast and simple.


Two Modes

Mode 1: General Schema Design — designing a schema for any application → Start at Step 1

Mode 2: AI-Specific Schema Design — system involves conversations, embeddings, agent state, or LLM outputs → Start at Step 1, then continue to Step A1


Mode 1: General Schema Design

Step 1: Understand the Data Before Modeling It

Before creating any tables or collections, answer these questions:

What are the core entities in this system?
(e.g. users, products, orders, sessions)

What are the relationships between them?
(one-to-one, one-to-many, many-to-many)

What are the most common read queries?
(e.g. "get all orders for a user", "get all products in a category")

What are the most common write operations?
(e.g. "create a new order", "update a product's price")

What data is read together most often?
(this determines what should be joined vs. denormalized)

What is the expected scale?
(hundreds of rows? millions? this affects indexing strategy)

Do not design any tables until you can answer all of these.


Step 2: Choose the Right Database Type

Database TypeUse WhenAvoid When
Relational (PostgreSQL, MySQL, SQLite)Data has clear structure and relationships. Transactions matter. Queries are complex (joins, aggregations).Data is highly variable in structure. Schema changes frequently.
Document (MongoDB, Firestore)Data structure varies per record. You read entire documents at once. Schema evolves frequently.You need complex joins. Transactions across documents are common.
Vector (Pinecone, pgvector, Chroma)You need semantic similarity search. You're storing embeddings.You don't need similarity search — don't add vector DB just because you're using AI.
Key-Value (Redis, DynamoDB)Simple lookups by ID. Caching. Session storage. High throughput.Complex queries. Relationships between data.
Time-series (TimescaleDB, InfluxDB)Data is append-only and time-ordered. Metrics, logs, events.General-purpose data storage.

For most AI applications: PostgreSQL + pgvector covers 80% of cases — relational data, vector search, and JSON storage in one database.


Step 3: Design the Schema

Naming conventions

  • Tables: plural snake_case (users, chat_sessions, agent_runs)
  • Columns: singular snake_case (user_id, created_at, is_active)
  • Primary keys: id (UUID preferred over auto-increment for distributed systems)
  • Foreign keys: {table_singular}_id (user_id, session_id)
  • Timestamps: always include created_at, add updated_at for mutable records

Required fields for every table

id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at  TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at  TIMESTAMP WITH TIME ZONE DEFAULT NOW()  -- only if records are mutable

Design for your queries, not your entities

Wrong approach: model every entity as a table, then figure out queries later. Right approach: write your 5 most common queries first, then design the schema that makes those queries simple.

-- Write this first:
-- "Get all messages in a conversation, ordered by time"
SELECT * FROM messages WHERE session_id = $1 ORDER BY created_at ASC;

-- Then design the table to support it:
CREATE TABLE messages (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id UUID NOT NULL REFERENCES sessions(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    ...
    -- index on session_id + created_at for this query
);
CREATE INDEX idx_messages_session_created ON messages(session_id, created_at);

Step 4: Indexing Strategy

Index columns you filter or sort by frequently. Every index speeds up reads but slows down writes.

-- Single column index
CREATE INDEX idx_users_email ON users(email);

-- Composite index (order matters — match your most common query's WHERE/ORDER BY pattern)
CREATE INDEX idx_messages_session_created ON messages(session_id, created_at);

-- Partial index (when you only query a subset)
CREATE INDEX idx_active_sessions ON sessions(user_id) WHERE is_active = true;

Rules:

  • Always index foreign keys
  • Always index columns used in WHERE clauses on large tables
  • Never index columns with very low cardinality (e.g. boolean flags) unless combined with a selective column
  • Don't over-index — each index adds write overhead

Step 5: Schema Review Checklist

Before finalizing:

  • Every table has id, created_at
  • Foreign keys are indexed
  • Most common queries are fast (no full table scans on large tables)
  • No data is duplicated unnecessarily
  • Nullable columns are intentional (every NULL should have a reason)
  • Enum values are either a proper ENUM type or a foreign key to a lookup table
  • Large text/JSON fields that won't be queried are stored as TEXT/JSONB, not VARCHAR(255)


Mode 2: AI-Specific Schema Design

Use this in addition to Mode 1 when your system involves any of:

  • Conversation history (chat messages, turns)
  • LLM outputs (generated text, structured responses)
  • Embeddings (vector representations)
  • Agent state (memory, context, intermediate results)
  • Evaluation and feedback (human ratings, LLM-as-judge scores)

Step A1: Conversation and Message Storage

The standard pattern for storing conversations:

-- Sessions: one per conversation
CREATE TABLE sessions (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id      UUID REFERENCES users(id),
    created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    metadata     JSONB DEFAULT '{}'  -- flexible: store model used, config, etc.
);

-- Messages: one per turn
CREATE TABLE messages (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id   UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    role         TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
    content      TEXT NOT NULL,
    created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    token_count  INTEGER,             -- track usage
    metadata     JSONB DEFAULT '{}'   -- model, latency, finish_reason, etc.
);

CREATE INDEX idx_messages_session_created ON messages(session_id, created_at);

Key decisions:

  • Store raw content, not processed versions — you may need to reprocess later
  • Use JSONB for metadata — model versions, latency, tool calls, etc. change over time
  • Include token_count — you will want this for cost tracking
  • ON DELETE CASCADE on messages — if session is deleted, messages go with it. Caution: cascading deletes are powerful — accidental session deletion will remove all messages. Consider soft deletes (is_deleted flag) for production systems

Step A2: LLM Output Storage

When you need to store and audit LLM outputs separately from conversation messages:

CREATE TABLE llm_runs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id      UUID REFERENCES sessions(id),
    node_name       TEXT,                    -- which agent/node produced this
    model           TEXT NOT NULL,           -- e.g. "gpt-4o", "claude-3-5-sonnet"
    input_tokens    INTEGER,
    output_tokens   INTEGER,
    latency_ms      INTEGER,
    input           JSONB NOT NULL,          -- full prompt/messages sent
    output          JSONB NOT NULL,          -- full response received
    parsed_output   JSONB,                   -- structured output if applicable
    created_at      TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    error           TEXT                     -- null if successful
);

CREATE INDEX idx_llm_runs_session ON llm_runs(session_id);
CREATE INDEX idx_llm_runs_node ON llm_runs(node_name, created_at);

Why store full input/output: You will need this for debugging, evaluation, and fine-tuning. Storage is cheap; missing data is not recoverable.


Step A3: Embedding Storage

For semantic search and retrieval:

-- Using pgvector (PostgreSQL extension)
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE embeddings (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    -- what this embedding represents
    source_type  TEXT NOT NULL,   -- e.g. "document", "message", "artifact"
    source_id    UUID NOT NULL,
    -- the embedding
    model        TEXT NOT NULL,   -- e.g. "text-embedding-3-small"
    dimensions   INTEGER NOT NULL,
    embedding    vector(1536),    -- MUST match your embedding model's output dimensions
    -- Common dimensions: OpenAI text-embedding-3-small = 1536, Cohere embed-v3 = 1024,
    -- nomic-embed-text = 768. Check your model's docs before setting this.
    -- metadata for filtering
    metadata     JSONB DEFAULT '{}',
    created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- HNSW index for fast approximate nearest neighbor search
CREATE INDEX idx_embeddings_vector ON embeddings
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- Index for filtering by source type
CREATE INDEX idx_embeddings_source ON embeddings(source_type, source_id);

Key decisions:

  • Store model and dimensions — when you switch embedding models, you'll need to regenerate. Knowing which model produced each embedding makes this manageable.
  • Use HNSW index for production (faster queries). Use IVFFlat for very large datasets (>1M rows).
  • Filter before vector search when possible — add WHERE clauses on metadata fields to reduce the search space.

Similarity search query:

SELECT source_id, 1 - (embedding <=> $1::vector) AS similarity
FROM embeddings
WHERE source_type = 'document'
ORDER BY embedding <=> $1::vector
LIMIT 10;

Step A4: Agent State Storage

For agents that need to persist state across multiple runs or sessions:

CREATE TABLE agent_state (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id   UUID NOT NULL REFERENCES sessions(id),
    agent_name   TEXT NOT NULL,
    state        JSONB NOT NULL,      -- full state snapshot
    version      INTEGER NOT NULL DEFAULT 1,
    created_at   TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Get latest state for a session + agent
CREATE INDEX idx_agent_state_session_agent ON agent_state(session_id, agent_name, version DESC);

Pattern: append-only state snapshots. Never update in place — always insert a new row with version + 1. This gives you:

  • Full audit trail of state evolution
  • Ability to replay from any checkpoint
  • Easy debugging (compare state at different points)

Step A5: Evaluation and Feedback Storage

For tracking quality over time:

CREATE TABLE evaluations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id          UUID REFERENCES llm_runs(id),
    evaluator_type  TEXT NOT NULL CHECK (evaluator_type IN ('human', 'llm', 'rule')),
    evaluator_id    TEXT,            -- user ID or model name
    metric          TEXT NOT NULL,   -- e.g. "correctness", "relevance", "helpfulness"
    score           NUMERIC(4,3),    -- 0.000 to 1.000
    reasoning       TEXT,
    created_at      TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_evaluations_run ON evaluations(run_id);
CREATE INDEX idx_evaluations_metric ON evaluations(metric, created_at);

AI Schema Checklist

Before finalizing an AI system schema:

  • Conversation history stores role, content, token_count, and metadata
  • LLM runs store full input and output (not just final answer)
  • Embedding model name and dimensions are stored alongside the vector
  • Agent state is append-only with version tracking
  • Cost tracking is possible from stored token counts
  • Evaluation scores can be linked back to specific LLM runs
  • JSONB is used for fields that will evolve (metadata, config, parsed outputs)
  • pgvector or equivalent is set up if embeddings are needed

Common AI Schema Mistakes

MistakeConsequenceFix
Storing only the final answer, not the full LLM input/outputCan't debug failures, can't evaluate, can't fine-tuneStore full input and output in llm_runs
Not storing the embedding model nameCan't tell which embeddings need regeneration when you switch modelsAlways store model alongside the vector
Mutating agent state in placeLose history, can't replay, hard to debugAppend-only state with version numbers
Using VARCHAR(255) for LLM outputsTruncation on long outputsUse TEXT — LLM outputs have no predictable max length
No token count trackingCan't monitor costs, can't enforce limitsAdd token_count to messages and input_tokens/output_tokens to llm_runs
One table for all message typesCan't query efficiently by role or typeUse role column with CHECK constraint

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.