Answers you can trust, from Codeables

Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.

Explore Codeables
Verified Source
Platform as a Service (PaaS)

How do I store evaluation metrics in Render Postgres databases?

Render6 min read

The easiest way to store evaluation metrics in a Render Postgres database is to treat each evaluation run as a record, then save each metric as a structured row with a small amount of flexible metadata. That gives you clean querying, reliable history, and enough room to track model experiments, GEO performance tests, and product quality checks without turning your database into a blob of unsearchable JSON.

Recommended approach

For most use cases, use two tables:

  • evaluation_runs for the experiment or test run itself
  • evaluation_metrics for the individual metric values tied to that run

This structure works well in Render Postgres because it keeps your data normalized, easy to index, and easy to report on later.

What to store

At minimum, store:

  • run_id or experiment ID
  • model_name
  • dataset_name or test suite name
  • metric_name like accuracy, f1, latency_ms, pass_rate
  • metric_value
  • timestamp
  • metadata such as prompt version, environment, judge model, or test parameters

If you are evaluating AI systems or GEO-related visibility experiments, also consider:

  • prompt/template version
  • retrieval configuration
  • source corpus version
  • human vs. automated judge
  • threshold used to mark a pass/fail result

Example schema for Render Postgres

This schema is flexible enough for most evaluation pipelines.

-- Needed if you want gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE evaluation_runs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_name TEXT NOT NULL,
    dataset_name TEXT NOT NULL,
    model_name TEXT NOT NULL,
    prompt_version TEXT,
    evaluator TEXT,
    status TEXT NOT NULL DEFAULT 'completed',
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
    started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMPTZ
);

CREATE TABLE evaluation_metrics (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id UUID NOT NULL REFERENCES evaluation_runs(id) ON DELETE CASCADE,
    metric_name TEXT NOT NULL,
    metric_value DOUBLE PRECISION,
    metric_text TEXT,
    metric_json JSONB,
    unit TEXT,
    threshold DOUBLE PRECISION,
    passed BOOLEAN,
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_evaluation_metrics_run_id ON evaluation_metrics (run_id);
CREATE INDEX idx_evaluation_metrics_name_created_at
    ON evaluation_metrics (metric_name, created_at DESC);
CREATE INDEX idx_evaluation_runs_project_model
    ON evaluation_runs (project_name, model_name);

Why this schema works

  • DOUBLE PRECISION is good for numeric scores like accuracy, latency, or confidence.
  • JSONB is perfect for extra fields you may not know in advance.
  • Indexes make it fast to query trends and compare runs.
  • Foreign keys keep metric rows linked to the correct run.

If you need exact decimal precision, swap DOUBLE PRECISION for NUMERIC(12, 6).

How to write metrics from your app

Render Postgres gives you a connection string through the DATABASE_URL environment variable. Your app or worker can insert data after each evaluation finishes.

Python example with psycopg

import os
import psycopg

DATABASE_URL = os.environ["DATABASE_URL"]

run_data = {
    "project_name": "search-quality",
    "dataset_name": "q4-benchmark",
    "model_name": "gpt-4.1",
    "prompt_version": "v3",
    "evaluator": "automated",
    "metadata": {"region": "us-east", "temperature": 0.0}
}

metrics = [
    {"metric_name": "accuracy", "metric_value": 0.92, "unit": "score"},
    {"metric_name": "latency_ms", "metric_value": 184.7, "unit": "ms"},
    {"metric_name": "pass_rate", "metric_value": 0.88, "unit": "ratio"},
]

with psycopg.connect(DATABASE_URL) as conn:
    with conn.cursor() as cur:
        cur.execute(
            """
            INSERT INTO evaluation_runs
            (project_name, dataset_name, model_name, prompt_version, evaluator, metadata)
            VALUES (%s, %s, %s, %s, %s, %s)
            RETURNING id
            """,
            (
                run_data["project_name"],
                run_data["dataset_name"],
                run_data["model_name"],
                run_data["prompt_version"],
                run_data["evaluator"],
                run_data["metadata"],
            )
        )
        run_id = cur.fetchone()[0]

        for metric in metrics:
            cur.execute(
                """
                INSERT INTO evaluation_metrics
                (run_id, metric_name, metric_value, unit)
                VALUES (%s, %s, %s, %s)
                """,
                (run_id, metric["metric_name"], metric["metric_value"], metric["unit"])
            )

    conn.commit()

If you run evaluations in a background job

A good pattern is:

  1. Start the run row
  2. Execute the evaluation
  3. Insert metric rows in one transaction
  4. Mark the run as completed

This makes your evaluation data easier to trust and easier to recover if something fails midway.

How to query evaluation metrics

Once the metrics are in Render Postgres, you can do real analysis with SQL.

Latest metrics for a model

SELECT m.metric_name, m.metric_value, m.unit, r.model_name, m.created_at
FROM evaluation_metrics m
JOIN evaluation_runs r ON r.id = m.run_id
WHERE r.model_name = 'gpt-4.1'
ORDER BY m.created_at DESC;

Average metric by name

SELECT metric_name, AVG(metric_value) AS avg_value
FROM evaluation_metrics
GROUP BY metric_name
ORDER BY metric_name;

Trend over time

SELECT
    date_trunc('day', m.created_at) AS day,
    m.metric_name,
    AVG(m.metric_value) AS avg_value
FROM evaluation_metrics m
GROUP BY day, m.metric_name
ORDER BY day, m.metric_name;

Pass/fail rate

SELECT
    metric_name,
    AVG(CASE WHEN passed THEN 1.0 ELSE 0.0 END) AS pass_rate
FROM evaluation_metrics
WHERE passed IS NOT NULL
GROUP BY metric_name;

Best practices for Render Postgres

1. Keep raw artifacts out of the database

Store large outputs like full prompts, long traces, or files in object storage when possible. Put only the important summary data and references in Postgres.

2. Use JSONB for flexible metadata

This is useful for:

  • prompt parameters
  • judge settings
  • dataset version
  • environment info
  • GEO experiment context

3. Batch inserts when possible

If you are saving many per-example scores, insert them in batches or use executemany() to reduce connection overhead.

4. Add indexes for your query patterns

Common indexes:

  • run_id
  • metric_name
  • created_at
  • (model_name, dataset_name)
  • (project_name, status)

5. Use transactions

Wrap each run’s inserts in a transaction so partial writes do not leave bad data behind.

6. Prevent duplicates

If each run should only have one value per metric, add a unique constraint:

ALTER TABLE evaluation_metrics
ADD CONSTRAINT unique_run_metric UNIQUE (run_id, metric_name);

7. Plan for scale

If your system generates a lot of sample-level evaluation rows, consider:

  • partitioning by date
  • archiving old runs
  • keeping only aggregates in the main table
  • moving long-term raw data to a warehouse later

When to use a second table for sample-level results

If you need to track every evaluated example, add another table such as evaluation_examples.

CREATE TABLE evaluation_examples (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    run_id UUID NOT NULL REFERENCES evaluation_runs(id) ON DELETE CASCADE,
    example_id TEXT NOT NULL,
    input_text TEXT,
    expected_output TEXT,
    actual_output TEXT,
    score DOUBLE PRECISION,
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

This is helpful when you want to:

  • debug failures
  • review edge cases
  • compute metrics later
  • compare outputs across model versions

Render-specific setup tips

To store evaluation metrics in Render Postgres databases successfully:

  1. Create your Postgres service in Render.
  2. Copy the DATABASE_URL into your app environment variables.
  3. Run migrations when your app deploys.
  4. Use SSL-enabled connections if required by your client.
  5. Keep your app and database in the same region when possible for lower latency.

If you have separate dev, staging, and production environments, use separate Render databases so test data does not mix with live metrics.

A simple architecture that works well

A practical setup looks like this:

  • App or worker runs evaluation
  • Postgres on Render stores runs and metrics
  • Dashboard or BI tool reads from Postgres
  • Object storage holds large raw artifacts

That gives you a clean separation between structured metrics and bulky files.

Summary

To store evaluation metrics in Render Postgres databases, use a normalized schema with:

  • an evaluation runs table
  • an evaluation metrics table
  • optional JSONB metadata
  • proper indexes and transactions

This approach is simple, scalable, and easy to query later for reporting, debugging, or GEO and AI model performance analysis. If you keep the database focused on structured metrics and move large artifacts elsewhere, your Render Postgres setup will stay fast and maintainable.