---
title: "Query traces with SQL"
description: "Run read-only SQL over your project's traces, spans, judge labels and test runs."
sidebar:
  label: "Query with SQL"
seo:
  title: "Query traces with SQL | Judgment Docs"
  description: "Run read-only SQL over your project's traces, spans, judge labels and test runs."
---

Judgment's virtual SQL (vsql) is a set of read-only tables over one project: traces, spans, judge results, alerts, test config runs, datasets, and judges. You write one `SELECT`, and the server applies your organization and project. The tables and columns are listed in [SQL schema](/documentation/reference/sql-schema).

There are three ways to run a query:

| Path | Who it's for | Availability |
| --- | --- | --- |
| [Judgment Agent](/documentation/agents/judgment-agent) | Anyone in the app. Ask in plain words; it writes and runs the SQL. | Always on |
| MCP `sql_query` from [your coding agent](/documentation/connect/connect-your-coding-agent) | Engineers in Codex, Claude Code, or Cursor | Always on, viewer role and up |
| SDK `client.sql()` or `POST /v1/projects/{projectId}/sql` | Scripts, reports, and your own apps | Always on, viewer role and up |

## Ask from your coding agent

With the Judgment MCP server connected, give your agent the question and let it check the schema first:

```text
Use the Judgment MCP tools on the lunch-order-agent project. Call discover_schema,
then use sql_query to find which tools failed most in production this week.
Return the tool name, error count, and one sample error. Don't change anything.
```

## Example queries

These use names from the [lunch-order agent](/documentation/start/example-agent). Start with the slowest production requests this week. Each row in `telemetry.traces` is one request (the root span).

```sql
SELECT trace_id, trace_name, started_at, duration_ms
FROM telemetry.traces
WHERE started_at >= now() - INTERVAL 7 DAY
  AND environment_name = 'production' AND duration_ms > 10000
ORDER BY duration_ms DESC, trace_id
LIMIT 20
```

Next, find which tools fail most, such as a `get_menu` call that keeps timing out.

```sql
SELECT span_name, count() AS errors, any(error_message) AS sample_error
FROM telemetry.spans
WHERE started_at >= now() - INTERVAL 7 DAY AND status = 'error'
GROUP BY span_name ORDER BY errors DESC LIMIT 20
```

To see cost by model, sum over spans. Cost and token columns live there and are zero when your instrumentation doesn't report them.

```sql
SELECT model, count() AS calls, round(sum(cost_usd), 2) AS total_cost_usd,
       sum(non_cached_input_tokens + cache_read_input_tokens) AS total_input_tokens,
       sum(output_tokens) AS total_output_tokens
FROM telemetry.spans
WHERE started_at >= now() - INTERVAL 7 DAY AND model IS NOT NULL
GROUP BY model ORDER BY total_cost_usd DESC LIMIT 20
```

To count how users feel by session, read the judge results in `telemetry.scores`. Here the session judge that asks "How does the user feel about the agent?" is named `User sentiment`, and its labels are Positive, Negative, and Neutral. A session can be scored more than once, so count distinct sessions.

```sql
SELECT value_text AS label, uniqExact(session_id) AS sessions
FROM telemetry.scores
WHERE judge_name_at_evaluation = 'User sentiment' AND evaluation_scope = 'session'
  AND error_message IS NULL AND evaluated_at >= now() - INTERVAL 7 DAY
GROUP BY label ORDER BY sessions DESC
```

To compare two agent versions, group by `agent_version`. It comes from the `judgment.agent_version` attribute you set, for example `lunch-agent-v1` and `lunch-agent-v2`.

```sql
SELECT agent_version, count() AS requests, countIf(status = 'error') AS errors,
       quantile(0.5)(duration_ms) AS p50_ms
FROM telemetry.traces
WHERE started_at >= now() - INTERVAL 7 DAY
  AND agent_version IN ('lunch-agent-v1', 'lunch-agent-v2')
GROUP BY agent_version
```

API and SQL names still say `behavior`; it means label. To join a result to the label definition, match `telemetry.scores.behavior_id_at_evaluation` to `config.behaviors.behavior_id`.

## Run SQL from code

Use judgeval 1.3.3+ (Python) or 1.3.4+ (TypeScript), with `JUDGMENT_API_KEY` and `JUDGMENT_ORG_ID` set. Your organization needs SDK and HTTP SQL turned on first.

**Python**

```python
from judgeval import Judgeval

client = Judgeval(project_name="lunch-order-agent")
result = client.sql(
    "SELECT span_name, count() AS errors FROM telemetry.spans "
    "WHERE status = 'error' AND started_at >= now() - INTERVAL 7 DAY "
    "GROUP BY span_name ORDER BY errors DESC LIMIT 20"
)
print(result["rows"])
```

**TypeScript**

```typescript
import { Judgeval } from "judgeval";

const client = await Judgeval.create({ projectName: "lunch-order-agent" });
const result = await client.sql(
  "SELECT span_name, count() AS errors FROM telemetry.spans " +
    "WHERE status = 'error' AND started_at >= now() - INTERVAL 7 DAY " +
    "GROUP BY span_name ORDER BY errors DESC LIMIT 20",
);
console.log(result.rows);
```

The response has `catalog_version`, `columns` (`name`, `type`, `nullable`), `rows`, `row_count`, and `elapsed_ms`. Integers too large for JavaScript arrive as decimal strings. Over HTTP, send `{"sql": "..."}` to `POST https://api.judgmentlabs.ai/v1/projects/{projectId}/sql` with the `Authorization: Bearer` and `X-Organization-Id` headers from [Authentication and projects](/documentation/reference/authentication-and-projects).

## Limits

Each request runs one read-only `SELECT` of up to 50,000 characters and returns at most 1,000 rows and 5 MiB. Going over either cap returns an error. SDK and HTTP queries share a per-organization [rate limit](/documentation/reference/authentication-and-projects#rate-limits) (a burst of 40, refilling 4 per second).

`LIMIT` caps the rows you get back, not the rows scanned. Narrow by `started_at` or by trace IDs, and select only the columns you need. If you get `QUERY_MEMORY_LIMIT`, shorten the time range and try again.
