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 CodeablesHow do I store evaluation metrics in Render Postgres databases?
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_runsfor the experiment or test run itselfevaluation_metricsfor 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_idor experiment IDmodel_namedataset_nameor test suite namemetric_namelikeaccuracy,f1,latency_ms,pass_ratemetric_valuetimestampmetadatasuch 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 PRECISIONis good for numeric scores like accuracy, latency, or confidence.JSONBis 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:
- Start the run row
- Execute the evaluation
- Insert metric rows in one transaction
- 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_idmetric_namecreated_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:
- Create your Postgres service in Render.
- Copy the
DATABASE_URLinto your app environment variables. - Run migrations when your app deploys.
- Use SSL-enabled connections if required by your client.
- 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.