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.
Tables
Section titled “Tables”Three tables, matching the 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
Section titled “Dynamic attributes”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_codeaddresses the attribute literally namedhttp.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 aliasresourceorpayload; those words are reserved for attribute access. - If an attribute name collides with a reserved SQL word, backtick-quote it:
`end`.
Functions
Section titled “Functions”Aggregates
Section titled “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() | percentage of spans with status_code = 'ERROR', on a 0 to 100 scale. Five percent is error_rate() > 5, not > 0.05 |
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) |
count_distinct(x), count_distinct_if(x, condition) | distinct values of x, optionally only where the condition holds |
quantile(x, q), quantileIf(x, q, condition) | an arbitrary quantile, e.g. quantile(duration_ms, 0.75) |
rate(x) and value(x) have been removed and now raise an error naming their
replacement. Both ignored their argument: rate() divided a sum of raw readings by a
fixed 60, and value() always returned avg(value), which is right for a gauge and
meaningless for a cumulative counter. For a gauge, write avg(value). For a counter,
see counter rates below.
Scalar functions
Section titled “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') | substring search inside one column, index-accelerated |
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 |
regexp_replace(str, pattern, replacement) | replace every regex match |
regexp_matches(str, pattern) | whether the string matches the pattern |
truncate_chars(str, n, suffix) | cut a string to n characters, appending suffix when it was cut |
to_string(x) | render a value as text |
now(), epoch_second(timestamp) | current time; a timestamp as epoch seconds |
coalesce, if, nullif, concat, length, lower, upper, trim, substring, replace, abs, round, ceil, floor, greatest, least, cast | standard SQL behavior |
Counter rates
Section titled “Counter rates”There is one window function, lag(x) OVER (PARTITION BY ... ORDER BY ...), and it
exists for this shape. A counter’s rate is a query, not an aggregate, because the
per-point delta has to be taken before anything is summed:
SELECT sum(delta) / 300 AS valueFROM ( SELECT value - lag(value) OVER ( PARTITION BY service, source_instance_id, metric_name ORDER BY timestamp ) AS delta FROM metrics WHERE metric_name = 'http.server.request.count') AS deltasWHERE delta >= 0Replace 300 with your window in seconds and the metric name with yours. The derived
table has to be aliased (AS deltas) or the outer select has nothing to resolve delta
against, and delta >= 0 drops counter restarts.
This is correct only where the metric carries one series per service, instance, and
metric name. When attributes split it into several series, lag() steps between
interleaved series and the summed rate is silently wrong. Pin the query to a single
series in its WHERE, or use a metric alert rule, which
partitions per series for you.
Operators beyond standard SQL
Section titled “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 substring 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
Section titled “Differences from standard SQL”SELECTonly. Nothing else parses: no DML, no DDL.- No
UNION, no CTEs (WITH). The only window function islag(), shown under counter rates. - Joins: inner
JOINbetween tables and subqueries is supported;CROSS JOINand comma joins are rejected. Outer joins are not currently supported. - Subqueries work in
WHERE … IN (SELECT …)and as derived tables inFROM. GROUP BY/HAVING/ORDER BY/LIMIT/OFFSETbehave 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'means2026-07-04 16:28:00UTC. 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. - An unbounded statement run through
run_sqlis limited to the last 7 days. If theWHEREclause carries no lower bound ontimestamp, one is added for you. Addtimestamp >= '<iso instant>'ortimestamp >= now() - INTERVAL n DAYto look further back. A statement that bounds itself is untouched, and so is SQL mode on the Logs page, which supplies its range from the picker.
Examples
Section titled “Examples”Errors per service over the last hour, in 5-minute windows:
SELECT bucket(timestamp, '5m') AS window, service, count(*) AS errorsFROM logsWHERE level = 'ERROR' AND timestamp > '2026-07-17T09:00:00Z'GROUP BY bucket(timestamp, '5m'), serviceORDER BY bucket(timestamp, '5m')Full-text hunt across everything the logs table knows:
SELECT timestamp, service, message FROM logs WHERE matches('timeout') LIMIT 50p95 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.messageFROM spans s JOIN logs l ON s.trace_id = l.trace_idWHERE 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.