Skip to Content
Launching the open-source gateway

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:

SQL
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 20

Run 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.

The SQL editor with saved query tabs, a cost-by-model query, a five-row result grid, and the Tables panel listing the logs columns

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.

SQL results shown as a line chart of daily cost over 14 days, with Add to dashboard, Table, Chart, and Export in the toolbar

This query produces the chart above:

SQL
SELECT toDate(timestamp) AS day, sum(cost) AS total_cost FROM logs WHERE timestamp >= now() - INTERVAL 14 DAY GROUP BY day ORDER BY day

Export 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. SELECT and WITH only. Joins, subqueries, and UNION work.
  • 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.
  • IN takes a list or a subquery. To check whether an array contains a value, use has(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

TableOne row perTime column
logsSpan: LLM call, tool call, or workflow steptimestamp
tracesTrace, when grouped by trace_unique_idstart_time
eval_resultsEvaluator scorecreated_at
annotationsHuman feedback on a spantimestamp
log_metrics, log_metrics_hourlyMinute or hour of requests, cost, tokens, and latencytimestamp
latency_quantiles_minute, latency_quantiles_hourlyMinute or hour of latency, throughput, and token quantilestimestamp
metadata_metricsHour of one numeric metadata keytimestamp
organizationYour organizationNone

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.

ColumnTypeDescription
timestampDateTime64Span end time
start_timeDateTime64Span start time
unique_idStringSpan log ID
trace_unique_idStringParent trace ID
span_unique_idStringSpan ID
span_parent_idStringParent span ID, empty for the root span
span_nameStringOperation name
span_workflow_nameStringNearest workflow name
log_typeStringchat, response, tool, agent, workflow, embedding, and more
log_methodStringinference, logging_api, python_tracing, and more
modelStringModel name as logged
provider_idStringProvider, such as openai or anthropic
deployment_nameStringCredential or deployment name
environmentStringprod or test
statusStringsuccess, failed, warning, pending, or running
status_codeInt16HTTP-style status code
error_classStringError category label
error_fingerprintStringHash that groups similar errors
costFloat64Cost in USD
latencyFloat64Total latency in seconds
time_to_first_tokenFloat64Time to first token in seconds
tokens_per_secondFloat64Output token throughput
routing_timeFloat64Gateway routing time in seconds
prompt_tokensUInt32Input tokens
completion_tokensUInt32Output tokens
total_request_tokensUInt32Input plus output tokens
customer_identifierStringEnd-customer ID, empty when unset
customer_nameStringCustomer display name
customer_emailStringCustomer email
custom_identifierStringIndexed custom ID
thread_identifierStringConversation thread ID
evaluation_identifierStringEvaluation run ID
group_identifierStringGroup ID
prompt_idStringManaged prompt ID
prompt_nameStringManaged prompt name
prompt_version_numberUInt16Prompt version
organization_key_idStringID of the API key used
metadataMap(String, String)Custom properties. See Custom properties.
streamBoolWhether the response was streamed
has_tool_callsBoolWhether the model called tools
cache_bitUInt81 when served from the gateway cache
cache_keyStringGateway cache key
blurredBoolWhether 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:

  1. Group by trace_unique_id to get one row per trace.
  2. Filter totals in HAVING, not WHERE. span_count, llm_call_count, error_count, total_cost, total_prompt_tokens, total_completion_tokens, total_tokens, customer_identifier, and metadata work in SELECT, HAVING, and ORDER BY. In WHERE, filter only on start_time, end_time, environment, or trace_unique_id.
  3. Name each column, such as total_cost AS cost. Without an alias, the result header shows the combining function, like sumMerge(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:

SQL
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
ColumnTypeDescription
trace_unique_idStringTrace ID
start_timeDateTime64First span start
end_timeDateTime64Last span end
environmentStringprod or test
span_countUInt32Spans in the trace
llm_call_countUInt32LLM spans in the trace
error_countUInt32Failed spans
total_costFloat64Sum of span cost in USD
total_prompt_tokensUInt64Input tokens across spans
total_completion_tokensUInt64Output tokens across spans
total_tokensUInt64All tokens
customer_identifierStringEnd-customer ID
metadataMap(String, String)Trace metadata

eval_results

One row per evaluator score. Join to logs to break scores down by model, prompt, or customer:

SQL
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
ColumnTypeDescription
created_atDateTime64Score creation time
evaluator_idStringEvaluator ID
evaluator_nameStringEvaluator display name
primary_scoreNullable(Float64)Main numeric score
boolean_valueNullable(UInt8)Boolean score as 0 or 1
customer_identifierStringCustomer of the evaluated span
log_unique_idStringEvaluated span. Joins logs.unique_id.
log_timestampNullable(DateTime64)Evaluated span’s timestamp
environmentStringprod or test

annotations

One row per piece of human feedback on a span.

ColumnTypeDescription
timestampDateTime64Annotation time
log_unique_idStringAnnotated span. Joins logs.unique_id.
is_positive_feedbackUInt81 for thumbs up, 0 for thumbs down
noteStringNote text
updated_atDateTime64Last 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 timestamp or toStartOfDay(timestamp).
  • Use the measure columns in SELECT, HAVING, and ORDER BY, not WHERE. In WHERE, filter on timestamp and environment, plus metric_key and model in metadata_metrics.

This query returns daily requests and cost for the last 30 days:

SQL
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 day

log_metrics (per minute) and log_metrics_hourly (per hour):

ColumnTypeDescription
timestampDateTimeBucket start
environmentStringprod or test
request_countUInt64Requests in the bucket
total_costFloat64Cost in USD
total_tokensUInt64All tokens
total_prompt_tokensUInt64Input tokens
total_completion_tokensUInt64Output tokens
avg_latencyFloat64Average latency in seconds
avg_tpsFloat64Average tokens per second
avg_ttftFloat64Average 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].

ColumnTypeDescription
timestampDateTimeBucket start
environmentStringprod or test
latency_quantilesArray(Float64)Latency in seconds
ttft_quantilesArray(Float64)Time to first token in seconds
tps_quantilesArray(Float64)Tokens per second
prompt_tokens_quantilesArray(Float64)Input tokens
completion_tokens_quantilesArray(Float64)Output tokens

metadata_metrics: hourly totals for each numeric metadata key your organization tracks.

ColumnTypeDescription
timestampDateTimeHour bucket start
metric_keyStringMetadata key being measured
modelStringModel
environmentStringprod or test
dimsMap(String, String)Configured dimension keys
value_countUInt64Number of values
value_sumFloat64Sum of values
value_avgFloat64Average value
value_minFloat64Smallest value
value_maxFloat64Largest value
value_quantilesArray(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:

SQL
SELECT arrayJoin(mapKeys(metadata)) AS key, count(*) AS spans FROM logs WHERE timestamp >= now() - INTERVAL 1 DAY GROUP BY key ORDER BY spans DESC

On traces, metadata is a combined total, so use it in SELECT or HAVING, not WHERE.

More examples

Error rate by model, last 24 hours:

SQL
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 DESC

Cost by a custom property, such as feature:

SQL
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 DESC

Limits

LimitValue
StatementsSELECT only, including WITH
DataYour project only. Respan adds the project filter before the query runs.
Run time30 seconds
Runs10 per minute
Query length10,000 characters
Export1,000,000 rows, 240 seconds, or 1 GB. Your WHERE and LIMIT still apply.