> ## Documentation Index
> Fetch the complete documentation index at: https://docs.braintrust.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# SQL charts

> Build dashboard charts from SQL with a visual builder or query text, chart queries on the Query page, and plot time series, top lists, or tables.

> [!WARNING]
>
> SQL charts are in [public preview](https://docs.braintrust.dev/feature-lifecycle.md) and can change before reaching general availability.

A SQL chart stores a query as its configuration. When the chart renders, Braintrust adds the dashboard's timeframe, page filters, project, and grouping to that query, runs it once, and plots the result. Your query is never rewritten on disk, so the same chart configuration produces correct results on any dashboard you paste it into.

Use a SQL chart when the metric you want is easier to express as a query than to assemble from measure and filter controls, or when you want to move between a chart and the [Query page](https://docs.braintrust.dev/reference/sql.md#sql-sandbox) without rebuilding the logic.

> [!NOTE]
>
> Custom charts are only available on [Pro and Enterprise plans](https://docs.braintrust.dev/plans-and-limits.md#plans).

## Availability

SQL charts are available on dashboards that aggregate logs. Dashboards that aggregate experiments continue to use the structured chart editor.

Charts you already built with the structured editor stay on that editor. There is no conversion between the two editors in either direction.

Requires data plane v2.1.1+ (Self-hosted customers must upgrade to use this feature.)

## Create a SQL chart

On a dashboard you created, click  **Chart** to open the chart editor on a new SQL chart. Every new chart starts from a query you can run as-is:

```sql theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SELECT percentile(metrics.duration, 0.95) AS p95_duration FROM project_logs()
```

Leaving `project_logs()` empty keeps the chart portable. Braintrust fills in the current project when the chart renders. To pin a chart to one project regardless of where it is viewed, pass the project ID, as in `project_logs('<project-id>')`.

To build a chart with the structured editor instead, click the  caret next to  **Chart** and select **Structured chart**.

Charts can only be added to dashboards you create. To add charts to the built-in **Cost and quality** dashboard, [clone it first](https://docs.braintrust.dev/observe/dashboards/create.md#create-a-dashboard).

## Edit the query

The editor gives you two surfaces over the same query. Switch between them with the **SQL** and **Builder** tabs. The editor opens on **SQL**, or on **Presets** when you edit a preset chart.

Both surfaces edit one query, so a change made in either is visible in the other. The chart preview above the editor updates as you edit.

**Builder** is disabled when the SQL query has an error or uses SQL that the builder cannot represent. Hovering the disabled tab explains why.

To start from a built-in chart instead of writing a query, select the **Presets** tab and pick one under **Select a preset**. Switching to **SQL** or **Builder** deselects the preset.

### Builder

The builder exposes the query as a set of collapsible sections:

* **Values**: The aggregates to plot. Each row pairs a field with an aggregator: sum, average, minimum, maximum, count, count distinct, or percentile. Drag the  handle to reorder outputs, and set a display name to label a value in axes, tooltips, and legends without changing its alias. A value whose expression is more than one of these aggregators, such as `100 * sum(errors) / count(id)`, reads as **Custom SQL** here. Write and change those expressions in the **SQL** tab.
* **Filters**: Narrow results by span or trace conditions:
  * **Span filter**: Limits which individual spans contribute to the values. Click **Span filter** to add one.
  * **Trace filter**: Narrows results to traces where any span satisfies the [filter conditions](https://docs.braintrust.dev/reference/sql/query-structure.md#where). To add one, open the more options menu () next to **Span filter** and select **Trace filter**. Existing trace conditions appear under a **Trace filters (optional)** subsection.

    Adding a trace filter switches the query to the `traces` [data shape](https://docs.braintrust.dev/reference/sql/query-structure.md#data-shapes) and wraps each span filter in [`FILTER_SPANS()`](https://docs.braintrust.dev/reference/sql/filtering#return-only-the-matching-spans), so span filters still limit which spans contribute to the values. Removing the last trace filter returns the query to the default `spans` shape. A query that sets `shape => 'summary'` keeps the summary shape.

    If the query in the **SQL** tab sets `shape => 'spans'` explicitly, the builder can't add a trace filter and shows an error. Change or remove that argument in the **SQL** tab first.
* **Group by**: Split the chart into a series per value of a field, such as `metadata.model`.

Click **Add value** to add another aggregate, and use the  button on a row to remove one.

### SQL

The **SQL** tab holds the query text with syntax highlighting and formatting. Write any query that meets the [query requirements](#query-requirements) below. For the full syntax, see the [SQL reference](https://docs.braintrust.dev/reference/sql.md).

Because Braintrust adds the timeframe, page filters, project, and grouping at render time, leave those out of the query you author. Write the shape of the data you want, and let the dashboard supply the context.

## Chart types

Select a chart type at the top of the editor. The type determines how the query result is read, which is why switching types can require a change to the query.

| Type | Reads |
| - | - |
|  **Time series** | One or more aggregate values over time, split into a series per group. |
|  **Top list** | Groups ranked by an aggregate value over the selected timeframe. |
|  **Big number** | A single aggregate value. |
|  **Table** | Rows of query output, either raw rows or aggregated values. See [Table charts](#table-charts). |

Switching to a compatible type changes only the visualization. When a type needs a small, predictable change to the query, Braintrust applies it. Larger changes, such as turning a row query into an aggregate query, stay explicit query edits.

When a query's result can only be shown as a table, such as a row query, the editor switches the chart to **Table** and disables **Time series**, **Top list**, and **Big number** until you change the query. An  indicator appears next to the **Chart type** heading, and both the indicator and the disabled types show the tooltip "This query result can only be shown as a table".

A saved chart whose result does not fit its selected type renders as a table on the dashboard, with the same indicator on its card. Opening the chart in the editor switches it to **Table**, which counts as an unsaved change.

## Options

The **Options** panel shows the settings for the selected chart type. Time series, top list, and big number charts all have **Unit**, which sets how values are formatted in axes, tooltips, and legends: duration, percent, count, cost, or bytes. Except for top lists shown as a donut, they also have **Markers**, which highlight values or ranges by severity. See [Add markers](https://docs.braintrust.dev/observe/dashboards/build-charts.md#add-markers). A big number has no other options. A table has only **Sort by**, described in [Sort a table](#sort-a-table).

### Time series options

* **Visualization**: Draw the series as **Lines** or **Bars**.
* **Target time interval**: The time bucket size. **Auto** picks a bucket size from the visible time range. **Hour**, **Day**, and **Week** are targets rather than guarantees, and Braintrust falls back to the nearest readable size when the requested one would be unreadable at the current range.

### Top list options

* **Visualization**: Show the ranked groups as a **Toplist** or a [**Donut**](#donut-charts).
* **Sort by**: Rank by **Value** or **Name**, **Ascending** or **Descending**.
* **Show overall**: Include the aggregate across all groups alongside the ranked rows.
* **Limit**: How many groups to show.

### Donut charts

A donut chart shows each group's share of the overall value. The overall value appears in the center, and the groups beyond the **Limit** are combined into an **Other** slice. Groups with negative values are left out of the chart.

**Donut** is available when the query has exactly one value, aggregated with sum or count. Selecting it turns on **Show overall** and keeps it on.

Requires data plane v2.18.0+ (Self-hosted customers must upgrade to use this feature.)

## Table charts

A table chart shows rows of query output alongside the rest of your charts. Use one to keep the traces, errors, or examples behind a metric on the same dashboard as the metric itself.

### Raw rows and aggregated values

Table charts are the only type with two query modes. Switch between them with the toggle above the column list. Changing modes rewrites the query rather than only the display:

* **Raw rows**: One row per row the query returns, with no grouping or aggregation. The editor lists **Columns**, which are ordered outputs. What a row represents follows the [data shape](https://docs.braintrust.dev/reference/sql/query-structure.md#data-shapes) you query: `spans`, the default, returns one row per matching span, `traces` returns every span of each matching trace, and `summary` returns one row per trace.
* **Aggregated values**: One row per group. The editor lists **Values** with aggregators, plus a **Group by** section, the same as the other chart types.

A mode is available only when the current outputs convert to it safely. A query the editor cannot convert, such as one built on a subquery, keeps its current mode and disables the other.

### Sort a table

Set the saved order with **Sort by** in the editor. **Default** keeps the order the query returns. Selecting a column sorts by that column, **Ascending** or **Descending**. A configured sort has to resolve to one of the query's outputs.

Clicking a column header on the dashboard re-sorts the table for your current session. It does not change the chart's saved order.

### Read a table

Column widths start sized to their contents, and the table scrolls horizontally when the columns are wider than the card. In a table with more than one column, resize a column in any of these ways:

* **Drag**: Drag the handle at the right edge of the column header.
* **Keyboard**: Press Tab to focus the handle, then press the left or right arrow key.
* **Fit to content**: Double-click the handle.

Braintrust remembers your column widths for each dashboard chart in your current browser. Resizing doesn't change the saved chart or what other people see.

To view a row's details or copy a cell's text:

1. Click any cell to open a menu of actions.
2. Select an action:
   * **View row details**: Opens a panel with every field in the row, where you can copy the row as JSON.
   * **Open trace**: Opens the clicked row's trace in a side panel, at that row's span. Appears on raw row tables that query project logs with the `spans` or `traces` [data shape](https://docs.braintrust.dev/reference/sql/query-structure.md#data-shapes), where each row is one span. Other queries, such as aggregated values or queries that use the `summary` shape, a subquery, or [`PIVOT` or `UNPIVOT`](https://docs.braintrust.dev/reference/sql/query-structure#pivot-and-unpivot), show **Open sample traces** instead.
   * **Open sample traces**: Opens traces that match the chart's whole time range and the chart and dashboard filters in a side panel. It doesn't narrow to the values in the clicked row.
   * **Copy "\[cell text]" to clipboard**: Copies the cell's text, including any text cut off by the column width.

**View row details**, **Open trace**, and **Open sample traces** don't appear when the chart is open in fullscreen or in the chart editor's preview.

Other actions that open traces or queries, such as **See traces in logs page**, use the chart's whole time range and the chart and dashboard filters, not the values in the clicked row. See [Interact with chart data](https://docs.braintrust.dev/observe/dashboards.md#interact-with-chart-data).

Values in the `input`, `output`, `expected`, `error`, and `metadata` columns are truncated to 1500 characters, both in table cells and in the row details panel.

Tables load 100 rows at a time. Click **Load 100 more rows** below the table to fetch the next page.

## Query requirements

A SQL chart query has to be something Braintrust can add dashboard context to and read back predictably. A query that does not meet these requirements stays editable, and the editor reports what to change.

Every chart query must:

* Project each output explicitly. A query that uses `SELECT *` is accepted but renders as a table, because its columns cannot be mapped to the saved visualization.
* Give every output a unique name.
* Use one linear source chain ending in `project_logs`. A subquery in `FROM` is fine, but joins and common table expressions (CTEs) are not supported. A query that uses `GROUP BY GROUPING SETS` is accepted but renders as a table when the result does not fit the saved visualization type.
* Keep trace-scoped and span-scoped conditions separable, so the two cannot share an `OR`.
* Avoid nesting an aggregate expression directly inside another aggregate.

Time series, top list, and big number charts additionally require:

* At least one aggregate value. Any categorical output has to appear in `GROUP BY`.
* No authored `ORDER BY` or `LIMIT`. Sorting and limits belong to the chart, which applies them at render time. A query that includes either is still accepted, and renders as a table when the result does not fit the saved visualization type.
* For a time series, no authored time bucketing. Braintrust buckets the results to the dashboard's timeframe. A query with authored time bucketing is still accepted, and renders as a table when the result does not fit the saved visualization type.
* For a top list, at least one grouping.

Table charts are less constrained. They accept a query with no aggregates at all, and they keep an authored `ORDER BY` and `LIMIT` instead of overriding them.

By default, results are bucketed and filtered by `created`, the time a span was ingested. To use `metrics.start` instead, which is useful for projects that batch-ingest historical spans, see [Configure Monitor chart time dimension](https://docs.braintrust.dev/admin/projects.md#configure-monitor-chart-time-dimension).

### Example queries

```sql Duration by model theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SELECT
  metadata.model AS model,
  avg(metrics.end - metrics.start) AS avg_duration,
  percentile(metrics.end - metrics.start, 0.95) AS p95_duration
FROM project_logs()
GROUP BY model
```

```sql Total cost theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SELECT sum(estimated_cost()) AS total_cost
FROM project_logs()
```

```sql Tokens by model theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SELECT
  metadata.model AS model,
  sum(metrics.tokens) AS tokens
FROM project_logs()
GROUP BY model
```

## Save a chart

Saving requires a query free of SQL errors. While an error is present, the editor reports the problem and the save buttons stay disabled.

Click **Save** to update the chart, or **Save as new** to keep the original and add a copy. You can also press **Cmd+Enter** (or **Ctrl+Enter**) to save. The shortcut is inactive while the save buttons are disabled.

Read the [SQL best practices](https://docs.braintrust.dev/reference/sql/best-practices.md) before charting a query over a large project. A SQL chart runs the query you author, so a query that is slow on the [Query page](https://docs.braintrust.dev/reference/sql.md#sql-sandbox) is slow as a chart.

## Chart a query on the Query page

On the [** Query**](https://www.braintrust.dev/app/~/sql) page, run a `project_logs` query that names a project, such as `project_logs('<project-id>')`, and chart tabs appear beside the **Table** tab in the results panel. A chart tab is a view of the query, so rerunning the query updates every chart tab.

When the query's result already forms a chart, a **Chart** tab appears automatically. Its [chart type](#chart-types) follows the query's grouping:

* Grouping by a date or time field produces a time series.
* Grouping by any other field produces a top list.
* No grouping produces a big number.

To add a different chart, click the  button with the **Add chart** tooltip. A gallery of charts suggested for your query opens, and clicking a suggestion adds it as a new chart tab. To rename or delete a chart tab you added, hover over the tab, click , and select **Rename** or **Delete**.

### Change how a chart looks

On a chart tab you added from the gallery, click the  button with the **Chart options** tooltip. The panel holds the chart type and the [options](#options) for that type, but not the query. Changing an option affects only how the chart looks. Switching the chart type can also change the chart's query when the new type needs it, as described in [Chart types](#chart-types), but the query in the editor stays the same. Changes apply as you make them.

### Change the time range

A chart tab's time range comes from the query's `created` filter, or covers the last 3 days when the query has none. To change it, use the time range picker in the results header. On a time series, drag across the chart to zoom in, and double-click to zoom out.

### When a chart no longer matches the query

If you rerun the query and a chart tab no longer fits the new result, the tab shows  and keeps the chart from the previous run, with the message "Showing chart from a previous query. The latest query no longer matches this chart."

### Save a chart from the Query page to a dashboard

> [!NOTE]
>
> Saving charts from the Query page to dashboards is only available on [Pro and Enterprise plans](https://docs.braintrust.dev/plans-and-limits.md#plans).

1. On a chart tab, click  **Save to dashboard**.
2. Enter a **Title**, or keep the one filled in from the chart. **Add chart** stays disabled until the chart has a title.
3. Choose a dashboard and click **Add chart**.

When a chart tab shows , such as when the chart [no longer matches the query](#when-a-chart-no-longer-matches-the-query), **Save to dashboard** is disabled. Hover over the button to see why.

Braintrust removes `created` filters from the query before saving, so the chart follows the dashboard's timeframe.

## Build charts with Loop

When SQL charts are enabled, [Loop](https://docs.braintrust.dev/loop.md) builds dashboard charts from project logs as SQL charts instead of structured charts.

Describe the chart you want, such as "Add a bar chart of span count grouped by `metadata.repo`". Loop adds a preview of the chart to the thread but does not save the preview. Ask Loop to save the chart to create a new dashboard, or to add or change charts on an existing one. Open the new or updated dashboard in the workspace to review it beside the thread.

When Loop saves a chart to an existing dashboard, the dashboard refreshes automatically. When Loop generates a chart and the chart editor is open, the editor shows the generated chart so you can review and refine it before saving.

## Next steps

* [Build charts](https://docs.braintrust.dev/observe/dashboards/build-charts.md) with the structured editor, and move, copy, or export any chart.
* [Filter and group data](https://docs.braintrust.dev/observe/dashboards.md#filter-and-group-data) across every chart on a dashboard at once.
* Read the [SQL reference](https://docs.braintrust.dev/reference/sql.md) for the full query syntax, and [SQL best practices](https://docs.braintrust.dev/reference/sql/best-practices.md) for keeping queries fast.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.