SQL editor
Query your spans, traces, and scores with read-only ClickHouse SQL.
The SQL editor runs read-only ClickHouse SQL over your project’s data. Use it for questions the Logs filters can’t answer, such as cost per model or error rate per customer.
Run a query
Open the editor
Go to Observability › SQL editor. A new tab offers Start from a template: Cost by day, Top models, Error rate, and Traces overview. Pick one, or write your own.
Write the SQL
The Tables panel on the right lists every table and column. Click a column to insert it, or use Preview rows on a table. The editor also autocompletes tables, columns, and functions, and underlines names it doesn’t recognize before you run.
This query returns cost and latency per model for the last 7 days:
SELECT
model,
count(*) AS requests,
round(sum(cost), 2) AS total_cost,
round(avg(latency), 2) AS avg_latency
FROM logs
WHERE timestamp >= now() - INTERVAL 7 DAY
GROUP BY model
ORDER BY total_cost DESC
LIMIT 20Run it
Click Run or press ⌘ Enter (Ctrl Enter on Windows and Linux). If the tab has several statements, it runs the one at the cursor. If you select text, it runs the selection.
The results show below the editor, with the row count, duration, and time. Click a column header to sort, or click Inspect row to read one row in full.


Always filter on the table’s time column, such as timestamp for logs. Queries without one scan more data and are more likely to time out.
Save and manage queries
Every tab is a saved query. Your SQL saves as you type or run, and the editor shows Saved when it’s done.
- New query: click the button after the last tab. New queries are named Untitled query.
- Rename: hover the open tab and click the pencil.
- Delete: click × on the tab, then confirm. This deletes the saved query for good.
Chart the results
When the result has a time column and a numeric column, click Chart to plot it. Click Add to dashboard to put the chart on a Metrics view.
This query produces the chart above:
SELECT
toDate(timestamp) AS day,
sum(cost) AS total_cost
FROM logs
WHERE timestamp >= now() - INTERVAL 14 DAY
GROUP BY day
ORDER BY dayExport results
Click Export, or press E, and choose Export as CSV or Export as JSONL. The export reruns your query in the background, so it isn’t limited to the rows on screen. When it’s done, download it from the dialog or from Settings › Exports.
Copy rows as TSV, in the same menu, copies the rows shown.
How queries work
You query tables that Respan defines, not the underlying database. Before a query runs, Respan checks it and adapts it:
- Respan’s tables and columns only. Use the tables and columns listed under Tables. Anything else fails with table not found or column not found.
- Your project only. Respan adds your project filter to every table. You don’t write it, and you can’t remove it.
- Read-only.
SELECTandWITHonly. Joins, subqueries, andUNIONwork. - ClickHouse functions. Function names are case-sensitive:
countIf,toStartOfHour,quantile(0.95)(latency). Date, string, math, array, map, JSON, and aggregate functions work. Functions that read files, URLs, or other servers are blocked. INtakes a list or a subquery. To check whether an array contains a value, usehas(array, value).
traces and the rollup tables store partial totals that Respan combines when you query, so they have a few extra rules. See traces and Rollup tables.
Tables
| Table | One row per | Time column |
|---|---|---|
logs | Span: LLM call, tool call, or workflow step | timestamp |
traces | Trace, when grouped by trace_unique_id | start_time |
eval_results | Evaluator score | created_at |
annotations | Human feedback on a span | timestamp |
log_metrics, log_metrics_hourly | Minute or hour of requests, cost, tokens, and latency | timestamp |
latency_quantiles_minute, latency_quantiles_hourly | Minute or hour of latency, throughput, and token quantiles | timestamp |
metadata_metrics | Hour of one numeric metadata key | timestamp |
organization | Your organization | None |
eval_results and annotations join to logs on log_unique_id = unique_id.
logs
One row per span. LLM calls, tool calls, agent steps, and workflow steps are all spans.
| Column | Type | Description |
|---|---|---|
timestamp | DateTime64 | Span end time |
start_time | DateTime64 | Span start time |
unique_id | String | Span log ID |
trace_unique_id | String | Parent trace ID |
span_unique_id | String | Span ID |
span_parent_id | String | Parent span ID, empty for the root span |
span_name | String | Operation name |
span_workflow_name | String | Nearest workflow name |
log_type | String | chat, response, tool, agent, workflow, embedding, and more |
log_method | String | inference, logging_api, python_tracing, and more |
model | String | Model name as logged |
provider_id | String | Provider, such as openai or anthropic |
deployment_name | String | Credential or deployment name |
environment | String | prod or test |
status | String | success, failed, warning, pending, or running |
status_code | Int16 | HTTP-style status code |
error_class | String | Error category label |
error_fingerprint | String | Hash that groups similar errors |
cost | Float64 | Cost in USD |
latency | Float64 | Total latency in seconds |
time_to_first_token | Float64 | Time to first token in seconds |
tokens_per_second | Float64 | Output token throughput |
routing_time | Float64 | Gateway routing time in seconds |
prompt_tokens | UInt32 | Input tokens |
completion_tokens | UInt32 | Output tokens |
total_request_tokens | UInt32 | Input plus output tokens |
customer_identifier | String | End-customer ID, empty when unset |
customer_name | String | Customer display name |
customer_email | String | Customer email |
custom_identifier | String | Indexed custom ID |
thread_identifier | String | Conversation thread ID |
evaluation_identifier | String | Evaluation run ID |
group_identifier | String | Group ID |
prompt_id | String | Managed prompt ID |
prompt_name | String | Managed prompt name |
prompt_version_number | UInt16 | Prompt version |
organization_key_id | String | ID of the API key used |
metadata | Map(String, String) | Custom properties. See Custom properties. |
stream | Bool | Whether the response was streamed |
has_tool_calls | Bool | Whether the model called tools |
cache_bit | UInt8 | 1 when served from the gateway cache |
cache_key | String | Gateway cache key |
blurred | Bool | Whether the payload is blurred |
traces
traces stores each trace as partial totals, which Respan combines when you query. That makes it fast, but it works differently from logs:
- Group by
trace_unique_idto get one row per trace. - Filter totals in
HAVING, notWHERE.span_count,llm_call_count,error_count,total_cost,total_prompt_tokens,total_completion_tokens,total_tokens,customer_identifier, andmetadatawork inSELECT,HAVING, andORDER BY. InWHERE, filter only onstart_time,end_time,environment, ortrace_unique_id. - Name each column, such as
total_cost AS cost. Without an alias, the result header shows the combining function, likesumMerge(total_cost). Pick a name that isn’t already a column name.
This query lists yesterday’s traces that had at least one failed span, most expensive first:
SELECT
trace_unique_id,
span_count AS spans,
error_count AS errors,
total_cost AS cost,
start_time AS started_at
FROM traces
WHERE start_time >= now() - INTERVAL 1 DAY
GROUP BY trace_unique_id
HAVING error_count > 0
ORDER BY cost DESC
LIMIT 50| Column | Type | Description |
|---|---|---|
trace_unique_id | String | Trace ID |
start_time | DateTime64 | First span start |
end_time | DateTime64 | Last span end |
environment | String | prod or test |
span_count | UInt32 | Spans in the trace |
llm_call_count | UInt32 | LLM spans in the trace |
error_count | UInt32 | Failed spans |
total_cost | Float64 | Sum of span cost in USD |
total_prompt_tokens | UInt64 | Input tokens across spans |
total_completion_tokens | UInt64 | Output tokens across spans |
total_tokens | UInt64 | All tokens |
customer_identifier | String | End-customer ID |
metadata | Map(String, String) | Trace metadata |
eval_results
One row per evaluator score. Join to logs to break scores down by model, prompt, or customer:
SELECT
l.model,
e.evaluator_name,
count(*) AS scores,
round(avg(e.primary_score), 3) AS avg_score
FROM eval_results e
JOIN logs l ON e.log_unique_id = l.unique_id
WHERE e.created_at >= now() - INTERVAL 1 DAY
AND l.timestamp >= now() - INTERVAL 1 DAY
GROUP BY l.model, e.evaluator_name
ORDER BY scores DESC| Column | Type | Description |
|---|---|---|
created_at | DateTime64 | Score creation time |
evaluator_id | String | Evaluator ID |
evaluator_name | String | Evaluator display name |
primary_score | Nullable(Float64) | Main numeric score |
boolean_value | Nullable(UInt8) | Boolean score as 0 or 1 |
customer_identifier | String | Customer of the evaluated span |
log_unique_id | String | Evaluated span. Joins logs.unique_id. |
log_timestamp | Nullable(DateTime64) | Evaluated span’s timestamp |
environment | String | prod or test |
annotations
One row per piece of human feedback on a span.
| Column | Type | Description |
|---|---|---|
timestamp | DateTime64 | Annotation time |
log_unique_id | String | Annotated span. Joins logs.unique_id. |
is_positive_feedback | UInt8 | 1 for thumbs up, 0 for thumbs down |
note | String | Note text |
updated_at | DateTime64 | Last edit |
Rollup tables
Rollup tables hold pre-aggregated totals per minute or hour, and the Tables panel lists them under Rollups. Use them for charts over weeks or months, where logs would be slow. Like traces, they combine partial totals when you query:
- Group by a time bucket, such as
timestamportoStartOfDay(timestamp). - Use the measure columns in
SELECT,HAVING, andORDER BY, notWHERE. InWHERE, filter ontimestampandenvironment, plusmetric_keyandmodelinmetadata_metrics.
This query returns daily requests and cost for the last 30 days:
SELECT
toStartOfDay(timestamp) AS day,
request_count AS requests,
round(total_cost, 2) AS cost
FROM log_metrics_hourly
WHERE timestamp >= now() - INTERVAL 30 DAY
GROUP BY day
ORDER BY daylog_metrics (per minute) and log_metrics_hourly (per hour):
| Column | Type | Description |
|---|---|---|
timestamp | DateTime | Bucket start |
environment | String | prod or test |
request_count | UInt64 | Requests in the bucket |
total_cost | Float64 | Cost in USD |
total_tokens | UInt64 | All tokens |
total_prompt_tokens | UInt64 | Input tokens |
total_completion_tokens | UInt64 | Output tokens |
avg_latency | Float64 | Average latency in seconds |
avg_tps | Float64 | Average tokens per second |
avg_ttft | Float64 | Average time to first token in seconds |
latency_quantiles_minute and latency_quantiles_hourly: each quantile column is an array of p50, p90, p95, and p99, in that order. Read p95 with latency_quantiles[3].
| Column | Type | Description |
|---|---|---|
timestamp | DateTime | Bucket start |
environment | String | prod or test |
latency_quantiles | Array(Float64) | Latency in seconds |
ttft_quantiles | Array(Float64) | Time to first token in seconds |
tps_quantiles | Array(Float64) | Tokens per second |
prompt_tokens_quantiles | Array(Float64) | Input tokens |
completion_tokens_quantiles | Array(Float64) | Output tokens |
metadata_metrics: hourly totals for each numeric metadata key your organization tracks.
| Column | Type | Description |
|---|---|---|
timestamp | DateTime | Hour bucket start |
metric_key | String | Metadata key being measured |
model | String | Model |
environment | String | prod or test |
dims | Map(String, String) | Configured dimension keys |
value_count | UInt64 | Number of values |
value_sum | Float64 | Sum of values |
value_avg | Float64 | Average value |
value_min | Float64 | Smallest value |
value_max | Float64 | Largest value |
value_quantiles | Array(Float64) | p50, p90, p95, and p99 |
organization
One row with your organization’s name.
Custom properties
metadata holds the custom properties you send with each span, as a map of string keys to string values.
- Read a key:
metadata['feature']. A missing key returns an empty string. - Check a key exists:
mapContains(metadata, 'feature'). - Use a number:
toFloat64OrNull(metadata['score']). Values are stored as text.
To see which keys your spans carry:
SELECT
arrayJoin(mapKeys(metadata)) AS key,
count(*) AS spans
FROM logs
WHERE timestamp >= now() - INTERVAL 1 DAY
GROUP BY key
ORDER BY spans DESCOn traces, metadata is a combined total, so use it in SELECT or HAVING, not WHERE.
More examples
Error rate by model, last 24 hours:
SELECT
model,
count(*) AS requests,
round(countIf(status = 'failed') / count(*) * 100, 2) AS error_rate_pct
FROM logs
WHERE timestamp >= now() - INTERVAL 1 DAY
GROUP BY model
HAVING count(*) > 10
ORDER BY error_rate_pct DESCCost by a custom property, such as feature:
SELECT
metadata['feature'] AS feature,
count(*) AS requests,
round(sum(cost), 2) AS total_cost
FROM logs
WHERE timestamp >= now() - INTERVAL 7 DAY
GROUP BY feature
ORDER BY total_cost DESCLimits
| Limit | Value |
|---|---|
| Statements | SELECT only, including WITH |
| Data | Your project only. Respan adds the project filter before the query runs. |
| Run time | 30 seconds |
| Runs | 10 per minute |
| Query length | 10,000 characters |
| Export | 1,000,000 rows, 240 seconds, or 1 GB. Your WHERE and LIMIT still apply. |