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

# Metrics

> Query cost and usage data with the metrics routes

## Overview

The metrics routes give the cost and usage data that the SELECT app shows, for Snowflake, BigQuery, Databricks and Tableau. Each route queries one semantic model:

```
POST https://api.select.dev/v2/metrics/<model>/query
```

All metrics routes use the same request body and the same response. When you know how to query one model, you can query all of them. Only the field names change from model to model. The reference page of each route lists its fields, and gives worked examples.

This page uses the `spend` model for all examples. `spend` is the consolidated cost of all your platforms, for all services. Use it first for all questions about cost. Use a platform route (for example `snowflake-workloads` or `bigquery-projects`) to find which resource in the platform caused the cost.

An API key must have the `usage_data:read` scope to use the metrics routes.

## Your first query

This request gets the daily spend for each platform for the last 30 days:

```bash theme={null}
curl https://api.select.dev/v2/metrics/spend/query \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-tenant-id: org_YOUR_ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "measures": ["sum_spend"],
    "dimensions": ["day", "connection_type"],
    "filter_expression": {
      "operator": "and",
      "filters": [{"field": "day", "operator": "in range", "value": "30D"}]
    },
    "sort": [{"field": "day", "direction": "asc"}],
    "limit": 1000
  }'
```

The response has one row for each combination of `day` and `connection_type`:

```json theme={null}
{
  "items": [
    {"day": "2026-09-01", "connection_type": "Snowflake", "sum_spend": 1520.34},
    {"day": "2026-09-01", "connection_type": "Bigquery", "sum_spend": 410.02},
    {"day": "2026-09-02", "connection_type": "Snowflake", "sum_spend": 1498.77}
  ],
  "page_token": null,
  "row_count": 3,
  "sql": null
}
```

## Request body

| Field | Meaning |
| - | - |
| `measures` | The values to calculate, for example `sum_spend`. |
| `dimensions` | The fields to group by. |
| `filter_expression` | Filters on dimensions. SELECT applies them before it calculates the measures. |
| `measure_filters` | Filters on measures. SELECT applies them after it calculates the measures. |
| `sort` | The order of the rows. |
| `limit` | The maximum number of rows. |
| `aggregate` | Set to `false` to get rows with no grouping. See [Listing rows](#listing-rows). |
| `return_sql` | Set to `true` to get the SQL that SELECT ran, in the `sql` field of the response. |

## Measures

A measure is a value that SELECT calculates across a group of rows, for example a sum or an average. The name of a measure usually tells you how SELECT calculates it: `sum_spend` is the sum of spend, and `avg_daily_spend` is the average spend per day.

If a request has measures and no dimensions, the response has one row: the total for all rows that match the filters.

```json theme={null}
{
  "measures": ["sum_spend"],
  "filter_expression": {
    "operator": "and",
    "filters": [{"field": "day", "operator": "in range", "value": "MTD"}]
  }
}
```

<Warning>
  Measures that end in `_annualized` are projections, not spend. SELECT extends the spend of the filtered period to one year. For example, with a 7-day range, `sum_spend_annualized` is the spend of that week × 365 / 7. Do not show a projection as the spend of a period.
</Warning>

## Dimensions

A dimension is a field that you group by. The response has one row for each unique combination of the values of the dimensions, and each row contains the value of each dimension. The measures in a row are calculated from only the data in that group.

`dimensions` is equivalent to a SQL `GROUP BY`. More dimensions give more rows, and each row contains less data:

| `dimensions` | Rows in the response |
| - | - |
| `[]` | 1 row: the total. |
| `["connection_type"]` | 1 row for each platform. |
| `["connection_type", "usage_type"]` | 1 row for each service on each platform. |
| `["day", "connection_type"]` | 1 row for each platform on each day. |

A dimension that is not in `dimensions` does not split the rows. For example, `["connection_type"]` with a 30-day filter gives the total of the 30 days for each platform, not a row for each day.

## Time dimensions

To show a trend, add a time dimension to `dimensions`. The time dimension sets the size of each time bucket:

| Dimension | Value in each row |
| - | - |
| `hour` | The start of the hour, as a timestamp with a UTC offset, for example `2026-09-01T14:00:00.000+0000`. |
| `day` | The date, for example `2026-09-01`. |
| `week` | The date of the Monday that starts the week. |
| `month` | The date of the first day of the month. |
| `quarter` | The date of the first day of the quarter. |
| `day_of_week` | The name of the day, for example `Mon`. Use it to compare days of the week. |

All dates and hours are in UTC.

Some rules apply to time buckets:

* **The first and last buckets can be partial.** For example, a `30D` range with `week` usually starts and stops in the middle of a week. The first and last rows then contain less data than the other rows.
* **Filter on `day`, and group by the larger bucket.** To get monthly spend for the year, filter `day` with `YTD` and add `month` to `dimensions`. Do not filter on `week`, `month` or `quarter`.
* **Hour-level data comes from a different table.** SELECT keeps spend in a daily table and in an hourly table. If your request uses `hour`, or another field that only the hourly table has, SELECT reads the hourly table. Otherwise, SELECT reads the daily table, which is faster. The totals are the same. You do not select the table.

## Filters

`filter_expression` limits the rows that SELECT uses to calculate the measures. It is equivalent to a SQL `WHERE`. It is a tree: each node has an `operator` of `and` or `or`, and a list of `filters`. An item in `filters` can be a filter or another node.

This request gets the spend on Snowflake compute or storage for the last 90 days:

```json theme={null}
{
  "measures": ["sum_spend"],
  "dimensions": ["usage_type"],
  "filter_expression": {
    "operator": "and",
    "filters": [
      {"field": "day", "operator": "in range", "value": "90D"},
      {"field": "connection_type", "operator": "=", "value": "Snowflake"},
      {
        "operator": "or",
        "filters": [
          {"field": "spend_category", "operator": "=", "value": "Compute"},
          {"field": "spend_category", "operator": "=", "value": "Storage"}
        ]
      }
    ]
  },
  "limit": 1000
}
```

| Operators | Value |
| - | - |
| `=`, `!=`, `>`, `>=`, `<`, `<=` | `value`: one value. |
| `in`, `not in` | `values`: a list. |
| `is`, `is not` | No value. Compares to null. |
| `ilike`, `startswith` | `value` or `values`. `ilike` matches a part of the text and ignores case. |
| `in range`, `not in range` | `value`: a relative date range. |
| `array_contains` | `values`: a list. The array field must contain one of the values. |

### Date ranges

Filter dates on the `day` field. To use fixed dates, use two filters. Both bounds are inclusive:

```json theme={null}
[
  {"field": "day", "operator": ">=", "value": "2026-08-01"},
  {"field": "day", "operator": "<=", "value": "2026-08-31"}
]
```

To use a range relative to today, use one `in range` filter:

| Value | Range |
| - | - |
| `30D` | The last 30 days, today included. |
| `3M` | The last 3 months, today included. |
| `WTD`, `MTD`, `QTD`, `YTD` | From the start of the current week, month, quarter or year, to today. |
| `LMTD` | All of the previous calendar month. |

<Warning>
  Text values are case-sensitive. A value with incorrect case matches no rows, and SELECT does not return an error. For example, on `spend`, `connection_type` is `Snowflake`, `Bigquery` or `Databricks`. The value `snowflake` matches no rows.
</Warning>

## Measure filters

`measure_filters` removes groups after SELECT calculates the measures. It is equivalent to a SQL `HAVING`. Use it to remove small groups from the response.

This request gets the warehouses that cost more than \$1,000 last month:

```json theme={null}
{
  "measures": ["sum_spend"],
  "dimensions": ["warehouse_name"],
  "filter_expression": {
    "operator": "and",
    "filters": [
      {"field": "day", "operator": "in range", "value": "LMTD"},
      {"field": "connection_type", "operator": "=", "value": "Snowflake"}
    ]
  },
  "measure_filters": [{"field": "sum_spend", "operator": ">", "value": 1000}],
  "sort": [{"field": "sum_spend", "direction": "desc"}],
  "limit": 1000
}
```

A filter in `filter_expression` changes the value of each measure. A filter in `measure_filters` does not change a value. It only removes rows.

## Usage groups

A usage group set divides your cost into named groups, for example departments or teams. You define usage group sets in the SELECT app, or with the `/v2/usage-group-sets` routes. A model that has a `usage_group` dimension can group or filter by the groups of your sets.

To group by the groups of one set, give the name of the set in `path`:

```json theme={null}
{
  "measures": ["sum_spend"],
  "dimensions": [{"field": "usage_group", "path": ["Department"]}],
  "filter_expression": {
    "operator": "and",
    "filters": [{"field": "day", "operator": "in range", "value": "MTD"}]
  },
  "limit": 1000
}
```

* `path[0]` is the name of the set, not its ID.
* A `usage_group` dimension with no `path` groups by all your sets at the same time.
* To get the cost of one group, use the same form in a filter: `{"field": {"field": "usage_group", "path": ["Department"]}, "operator": "=", "value": "Engineering"}`.
* To test a set before you save it, send its definition in `usage_group_sets`. SELECT then uses that definition in place of your saved sets.

## Sort and limit

`sort` is a list of fields and directions. The first item sorts first. You can sort by a dimension or a measure:

```json theme={null}
"sort": [{"field": "day", "direction": "asc"}, {"field": "sum_spend", "direction": "desc"}]
```

`limit` is the maximum number of rows in the response. The metrics routes do not paginate: `page_token` is always `null`. If a query has more rows than `limit`, SELECT removes the rows after the limit, and does not return an error. Always set a `limit`. To get the top N groups, sort by a measure and set `limit` to N.

## Period over period

Some models have measures that compare a period to the period before it: `<measure>_previous_period`, `change_<measure>` and `percent_change_<measure>`. The previous period has the same number of days, and stops on the day before your range starts.

For these measures, the request must have one `day` range at the top level of `filter_expression`. Use one `in range` filter, or one `>=` filter and one `<=` filter.

```json theme={null}
{
  "measures": ["sum_spend", "sum_spend_previous_period", "change_sum_spend", "percent_change_sum_spend"],
  "dimensions": ["connection_type"],
  "filter_expression": {
    "operator": "and",
    "filters": [{"field": "day", "operator": "in range", "value": "30D"}]
  },
  "limit": 1000
}
```

<Note>
  For `QTD` on 2026-09-28, the range is 2026-07-01 to 2026-09-28 (90 days). The previous period is the 90 days from 2026-04-02 to 2026-06-30. It is not the previous calendar quarter.
</Note>

## Listing rows

To get rows with no grouping, set `aggregate` to `false`, and send only `dimensions`. For example, on `snowflake-queries`, this gives the individual queries. If the request also has `measures` or `measure_filters`, SELECT groups the rows, and does not return an error. Always set a `limit` when you list rows.

## Teams

To query as one of your teams, send the `X-Team-Id` header. The response then contains only the data that the roles of the team give access to. This header may not be functional on all routes, reach out to the SELECT team if needed.
