# QuerySQL

> The complete QuerySQL dialect: tables, dynamic attributes, functions, operators, and how it differs from standard SQL.

QuerySQL is Fixter's read-only SQL dialect for querying your telemetry. It's standard
`SELECT` SQL at heart; if you know SQL, you know most of it already. This page is the
exact surface: what you can query, the functions and operators Fixter adds, and where
the dialect deliberately differs from standard SQL.

QuerySQL runs everywhere Fixter accepts a query: the SQL mode on the
[Logs page](https://app.fixter.dev/logs), the `run_sql`
[MCP tool](/reference/mcp-tools/#telemetry), and alert rule conditions.

## Tables

Three tables, matching the [data model](/reference/data-model/):

| Table | Queryable columns |
| --- | --- |
| `logs` | `timestamp`, `service`, `level`, `message`, `trace_id`, `span_id`, `parent_span_id`, `source_instance_id`, `log_id` |
| `spans` | `timestamp`, `service`, `name`, `kind`, `status_code`, `status_message`, `trace_id`, `span_id`, `parent_span_id`, `source_instance_id`, `duration_ms` |
| `metrics` | `timestamp`, `metric_name`, `service`, `source_instance_id`, `value` |

`duration_ms` on `spans` is milliseconds (fractional). `source_instance_id`
identifies which instance of a service emitted the record. Your own attributes aren't
listed here because they're dynamic: they live behind each table's attribute storage
(`payload`, and `resource` for resource attributes) and are addressed by name, as
described next. `SELECT *` expands to the table's columns plus the raw attribute
object.

## Dynamic attributes

Any attribute your instrumentation emits is queryable **by name, as if it were a
column**, with no registration and no special syntax:

```sql
SELECT http_method, count(*) FROM logs WHERE http_method = 'POST' GROUP BY http_method
```

- **Dotted OpenTelemetry keys** work as-is: `http.response.status_code` addresses the
  attribute literally named `http.response.status_code`.
- **Resource attributes** (host, deployment, …) use the `resource.` prefix:
  `resource.host_name`, `resource.service.name`.
- **In joins**, qualify attributes with the table alias (`s.http_method`); unqualified
  unknown names across two tables are rejected as ambiguous. Don't name a join alias
  `resource` or `payload`; those words are reserved for attribute access.
- If an attribute name collides with a reserved SQL word, backtick-quote it:
  `` `end` ``.

## Functions

### Aggregates

| Function | What it does |
| --- | --- |
| `count(*)` | row count (on `logs`, counts unique log records) |
| `count_distinct(x)` | distinct values of `x` |
| `sum(x)`, `avg(x)`, `min(x)`, `max(x)` | standard aggregates |
| `p50(x)`, `p95(x)`, `p99(x)` | latency percentiles (approximate, fast at any scale) |
| `countIf(condition)` | count of rows where the condition holds |
| `error_rate()` | fraction of spans with `status_code = 'ERROR'` |
| `request_count()` | span count (no arguments) |
| `error_burn_rate(budget)` | error rate divided by your error budget; `budget` must be a positive number literal, e.g. `error_burn_rate(0.01)` |
| `latency_burn_rate(measure, threshold, budget)` | fraction of rows where `measure > threshold`, divided by `budget`, e.g. `latency_burn_rate(duration_ms, 500, 0.03)` |
| `rate(x)` | per-second rate of a counter metric over each minute |
| `value(x)` | the natural aggregate of a metric's samples (sum for counters, average for gauges) |

### Scalar functions

| Function | What it does |
| --- | --- |
| `bucket(timestamp, '5m')` | truncate a timestamp to fixed windows; the interval is `<n>m`, `<n>h`, or `<n>d` |
| `contains(column, 'needle')` | fast token search inside one column |
| `matches('needle')` | full-text search across the table's searchable fields (message/name, your attributes, ids, service); takes a string literal |
| `regexp_extract(str, pattern[, group])` | extract a regex match (or capture group); returns NULL when no match |
| `now()` | current time |
| `coalesce`, `if`, `nullif`, `concat`, `length`, `lower`, `upper`, `trim`, `substring`, `replace`, `abs`, `round`, `ceil`, `floor`, `greatest`, `least`, `cast` | standard SQL behavior |

## Operators beyond standard SQL

| Operator | Meaning |
| --- | --- |
| `col =~ 'pattern'` | case-insensitive glob match, where `*` matches any run and `?` one character: `message =~ '*timeout*'` |
| `col = 'pat*tern'` | an `=` whose right side contains `*` automatically becomes a glob match; without wildcards it stays exact. There is currently no way to `=`-match a value containing a literal `*`; use `contains()` for token matching in that case |
| `col != 'pat*tern'` | same, negated |
| `col ILIKE 'pattern'` | case-insensitive `LIKE` |

Standard comparison operators, `LIKE`, `IN`, `BETWEEN` (plain only; `SYMMETRIC` is
rejected), `AND`/`OR`/`NOT`, and `CASE` expressions all behave normally.

## Differences from standard SQL

- **`SELECT` only.** Nothing else parses: no DML, no DDL.
- **No `UNION`, no CTEs (`WITH`), no window functions.**
- **Joins:** inner `JOIN` between tables and subqueries is supported; `CROSS JOIN` and
  comma joins are rejected. Outer joins are not currently supported.
- **Subqueries** work in `WHERE … IN (SELECT …)` and as derived tables in `FROM`.
- **`GROUP BY` / `HAVING` / `ORDER BY` / `LIMIT` / `OFFSET`** behave normally.
- **Identifiers** are case-insensitive and quoted with **backticks** (MySQL-style), not
  double quotes.
- **Timestamps:** ISO-8601 string literals are understood anywhere and normalized to
  UTC: `'2026-07-04T19:28:00+03:00'` means `2026-07-04 16:28:00` UTC. All query times
  are UTC.
- **Comments** (`--` and `/* */`) are rejected by the API and MCP surfaces, so leave them
  out of queries you save or automate.

## Examples

Errors per service over the last hour, in 5-minute windows:

```sql
SELECT bucket(timestamp, '5m') AS window, service, count(*) AS errors
FROM logs
WHERE level = 'ERROR' AND timestamp > '2026-07-17T09:00:00Z'
GROUP BY bucket(timestamp, '5m'), service
ORDER BY bucket(timestamp, '5m')
```

Full-text hunt across everything the logs table knows:

```sql
SELECT timestamp, service, message FROM logs WHERE matches('timeout') LIMIT 50
```

p95 latency for one operation:

```sql
SELECT p95(duration_ms) FROM spans WHERE name = 'GET /checkout'
```

Join spans to their logs through the trace id:

```sql
SELECT s.http_method, l.level, l.message
FROM spans s JOIN logs l ON s.trace_id = l.trace_id
WHERE s.status_code = 'ERROR'
```

SLO burn rate, as used in alert rules:

```sql
SELECT latency_burn_rate(duration_ms, 500, 0.03) FROM spans WHERE service = 'checkout'
```

Discover what's queryable, including your own attribute keys, with the
`describe_schema` [MCP tool](/reference/mcp-tools/), or visually via the field rail on
the [Logs page](https://app.fixter.dev/logs).