SQL reference
Query your telemetry with ClickHouse SQL: the tables and columns you can read, the required macros, result shapes for each chart type, and limits.
SQL widgets on dashboards, raw-SQL alert rules, and the MCP run_sql tool all run ClickHouse SQL against the same tables Maple’s own pages read. This page lists what you can query and the rules a query has to follow.
A first query
SELECT toStartOfInterval(Timestamp, INTERVAL $__interval_s SECOND) AS bucket,
count() AS logs
FROM logs
WHERE $__orgFilter AND $__timeFilter(Timestamp)
GROUP BY bucket
ORDER BY bucket
Every query must contain $__orgFilter. It expands to your organization’s filter, and a query without it is rejected before it runs. Access is also enforced by the credentials the query runs with, so the macro is a correctness check rather than your only protection.
Macros
| Macro | Expands to |
|---|---|
$__orgFilter | Your organization filter. Required. |
$__timeFilter(col) | col >= <start> AND col <= <end> for the selected time range |
$__timeGroup(col) | toStartOfInterval(col, INTERVAL <bucket> SECOND) |
$__startTime | The range start, as a DateTime |
$__endTime | The range end, as a DateTime |
$__interval_s | The bucket width in seconds, an integer of at least 1 |
The argument to $__timeFilter and $__timeGroup must be a plain column name. Any other $__name is an error. When a widget has no explicit granularity, buckets target about 30 points with a 5-minute minimum.
On dashboards, variables are written $name or ${name} and are substituted as quoted string literals. A variable set to All becomes a comma-separated list, so use it with IN:
WHERE $__orgFilter AND $__timeFilter(Timestamp) AND ServiceName IN ($service)
Tables
The main tables are below. Run describe_warehouse_tables from the MCP server for the full list with every column and its type.
| Table | One row per | Time column |
|---|---|---|
traces | Span | Timestamp |
service_overview_spans | Entry-point span (server, consumer or root) | Timestamp |
logs | Log record | Timestamp |
error_events | Exception occurrence | Timestamp |
metrics_sum | Counter data point | TimeUnix |
metrics_gauge | Gauge data point | TimeUnix |
metrics_histogram | Histogram data point | TimeUnix |
metrics_exponential_histogram | Exponential histogram data point | TimeUnix |
session_replays | Browser session | StartTime |
session_events | Session timeline event | Timestamp |
product_events | Product event | Timestamp |
service_usage | Service and hour | Hour |
attribute_keys_hourly | Attribute key and hour | Hour |
traces
| Column | Type | Notes |
|---|---|---|
Timestamp | DateTime64(9) | Span start |
TraceId, SpanId, ParentSpanId | String | |
ServiceName, SpanName | LowCardinality(String) | |
SpanKind | LowCardinality(String) | Internal, Server, Client, Producer, Consumer |
Duration | UInt64 | Nanoseconds. Divide by 1e6 for milliseconds. |
StatusCode | LowCardinality(String) | Ok, Error, Unset (Title Case) |
StatusMessage | String | |
SpanAttributes, ResourceAttributes | Map(LowCardinality(String), String) | Read with SpanAttributes['http.route'] |
EventsTimestamp, EventsName, EventsAttributes | Arrays | Span events, index-aligned |
SampleRate | Float64 | 1.0 when unsampled. Multiply counts by it to estimate throughput. |
IsEntryPoint | UInt8 | 1 for server, consumer and root spans |
For per-service request rate, error rate and latency, query service_overview_spans instead. It has the same core columns, holds only entry-point spans, and is much smaller, but it has no attribute maps.
logs
| Column | Type | Notes |
|---|---|---|
Timestamp | DateTime64(9) | |
TimestampTime | DateTime | Second precision |
ServiceName | LowCardinality(String) | |
SeverityText | LowCardinality(String) | Casing varies by SDK |
SeverityNumber | UInt8 | 1-4 trace, 5-8 debug, 9-12 info, 13-16 warn, 17-20 error, 21-24 fatal |
Body | String | |
TraceId, SpanId | String | |
LogAttributes, ResourceAttributes, ScopeAttributes | Map(LowCardinality(String), String) |
Filter severity on SeverityNumber rather than SeverityText: some SDKs send Error and others ERROR. SeverityNumber BETWEEN 17 AND 20 matches every error.
Metrics tables
All four metric tables share ServiceName, MetricName, MetricDescription, MetricUnit, Attributes (a map of data-point attributes), ResourceAttributes, StartTimeUnix and TimeUnix. Then:
metrics_sumandmetrics_gaugecarryValue Float64.metrics_sumaddsIsMonotonicandAggregationTemporality(1 is delta, 2 is cumulative). Delta rows already hold the per-interval increase, sosum(Value)per bucket is exact; cumulative rows need a rate.metrics_histogramcarriesCount,Sum,Min,Max,BucketCountsandExplicitBounds.metrics_exponential_histogramcarriesCount,Sum,Scale,ZeroCountand the positive and negative bucket arrays.
Things that trip people up
- Missing map keys read as
'', notNULL. Test withmapContains(SpanAttributes, 'key')orSpanAttributes['key'] != ''. - Status and span kind are Title Case.
StatusCode = 'Error'matches;'ERROR'does not. - Hourly rollups use
Hour, notTimestamp. Snap your range withtoStartOfHour, or a window shorter than an hour returns nothing. - Rollup columns are aggregate states. Read
SimpleAggregateFunctioncolumns withsum(...)andAggregateFunctioncolumns with the matching…Merge(...). - Filter on the sort key first.
tracesis sorted by service and span name, so addingServiceName = '…'makes a query dramatically cheaper.
Result shapes for charts
| Display | Return |
|---|---|
| Line, area, bar | A time column (name it bucket) plus one numeric column per series |
| Stat | One row with a numeric column named value |
| Pie, funnel, horizontal bar | A name column and a value column |
| Heatmap | x, y and value columns |
| Histogram | One value column, one row per observation |
| Table | Any columns |
For time series, every non-time column becomes a series named after the column, and non-numeric columns are dropped. Rows are not split by a string column, so to draw several lines, return one column per line, for example with countIf:
SELECT $__timeGroup(Timestamp) AS bucket,
countIf(StatusCode = 'Error') AS errors,
count() AS requests
FROM service_overview_spans
WHERE $__orgFilter AND $__timeFilter(Timestamp)
GROUP BY bucket
ORDER BY bucket
What is allowed
A query is a single SELECT or WITH statement. A trailing ; is fine, and a trailing FORMAT clause is dropped. Rejected:
- More than one statement.
- Any write or DDL keyword:
INSERT,UPDATE,DELETE,DROP,ALTER,TRUNCATE,RENAME,ATTACH,DETACH,CREATE,GRANT,REVOKE,OPTIMIZE,SYSTEM,KILL, andINTO OUTFILE. - A
SETTINGSclause. - Table functions that reach outside Maple, such as
url,file,s3,remote,mysql,postgresqlanddictionary.
A rejected query returns 400 with one of these codes: MissingOrgFilter, InvalidMacro, UnresolvedMacro, DisallowedStatement, DisallowedFunction, MultipleStatements, ResourceLimit.
Limits
| Limit | Value |
|---|---|
| Query text | 32,768 characters |
| Rows returned | 1,000. More is an error, not a silent cut: aggregate or add a LIMIT. |
| Result size | 5 MB of JSON, 64,000 characters per cell |
| Execution time | 10 seconds for dashboards and run_sql, 5 seconds for alerts |
| Raw-SQL alert rules | Must use $__timeFilter(…) |
Your query is wrapped in an outer LIMIT, so WITH TOTALS, LIMIT BY and WITH FILL in the outermost query are discarded.
Related
- Maple MCP server:
run_sqlanddescribe_warehouse_tables - OpenTelemetry conventions: which attributes land in which column
- Limits