Skip to content

QuerySQL

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, the run_sql MCP tool, and alert rule conditions.

Three tables, matching the data model:

TableQueryable columns
logstimestamp, service, level, message, trace_id, span_id, parent_span_id, source_instance_id, log_id
spanstimestamp, service, name, kind, status_code, status_message, trace_id, span_id, parent_span_id, source_instance_id, duration_ms
metricstimestamp, 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.

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

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`.
FunctionWhat 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)
FunctionWhat 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, caststandard SQL behavior
OperatorMeaning
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.

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

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

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:

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

p95 latency for one operation:

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

Join spans to their logs through the trace id:

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:

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, or visually via the field rail on the Logs page.