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()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.

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')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, caststandard SQL behavior

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 value
FROM (
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 deltas
WHERE delta >= 0

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

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

  • SELECT only. Nothing else parses: no DML, no DDL.
  • No UNION, no CTEs (WITH). The only window function is lag(), shown under counter rates.
  • 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.
  • An unbounded statement run through run_sql is limited to the last 7 days. If the WHERE clause carries no lower bound on timestamp, one is added for you. Add timestamp >= '<iso instant>' or timestamp >= now() - INTERVAL n DAY to 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.

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.