Measure QL: Remaining Gaps and Order of Work
Status: design — not implemented.
This document lists what the measure query language still lacks compared with InfluxDB 3.0 SQL, and the order in which to close those gaps.
Two things are deliberately absent from the list:
- Time bucketing, tag aggregation and
COUNT(DISTINCT tag)are already supported onmain(#1365, #1366):GROUP BY TIME_BUCKET(...), aggregates over a tag, andCOUNT(DISTINCT ...). - Window functions and gap filling (
OVER,lag,rank,date_bin_gapfill,locf,interpolate) are out of scope.
SQL data writes (INSERT/UPDATE/DELETE) are not listed either; ingestion stays on the write
APIs. Schema definition (DDL) is in scope (group S), even though InfluxDB 3.0 manages schema
outside SQL: every BanyanDB schema object already has a registry service, so DDL is mostly a
language over existing RPCs.
Remaining gaps
| # | Missing piece | What it needs |
|---|---|---|
| B. Core SQL semantics | ||
| B1 | Time as a normal predicate: WHERE time >= now() - INTERVAL '1h' |
Grammar only |
| B2 | AS aliases |
Grammar, plus renaming on the liaison |
| B3 | More than one aggregate per query | The request carries only one aggregate; the liaison must merge several requests |
| B4 | Real HAVING (filter after aggregation). The current HAVING means “tag array contains” and needs a new name |
Operator on the liaison |
| B5 | ORDER BY any column, alias or aggregate; several sort keys; NULLS FIRST/LAST |
Sort on the liaison (today ordering requires an index rule) |
| B6 | DISTINCT |
Operator on the liaison |
| B7 | Expressions: arithmetic, CASE, COALESCE, CAST/::type |
Expression evaluator on the liaison |
| B8 | Filtering on fields, and float literals | Filter on the liaison first; later push it down to data nodes |
| B9 | GROUP BY an expression or column number (GROUP BY 1) |
Grammar and planner |
| C. Functions | ||
| C1 | Scalar functions: math, string, regex, date/time (date_trunc, extract, tz) |
Function registry |
| C2 | Statistical and approximate aggregates (stddev, percentile, approx_distinct) |
Engine or liaison |
| C3 | Selectors returning a value with its timestamp (selector_max(v, time)) |
Liaison, plus struct-valued results |
| D. Combining queries | ||
| D1 | Subqueries and CTEs (WITH) |
Relational planner |
| D2 | UNION / INTERSECT / EXCEPT |
Relational planner |
| D3 | JOIN across measures |
Relational planner, plus hash join |
| S. Schema definition (DDL) | ||
| S1 | SHOW GROUPS, SHOW MEASURES IN g, DESCRIBE MEASURE g.m (and the same for every kind) |
Grammar, mapped to the registry Get/List RPCs |
| S2 | CREATE GROUP / ALTER GROUP / DROP GROUP, with catalog, shard count, segment interval, TTL and lifecycle stages |
Grammar, mapped to GroupRegistryService |
| S3 | CREATE MEASURE / ALTER MEASURE / DROP MEASURE: tag families, fields (type, encoding, compression), ENTITY, SHARDING KEY, INTERVAL, index mode |
Grammar, mapped to MeasureRegistryService |
| S4 | The same for STREAM, TRACE and PROPERTY |
Grammar, mapped to their registry services |
| S5 | CREATE INDEX ... ON MEASURE g.m (tag) USING INVERTED | SKIPPING | TREE, with analyzer and no_sort; DROP INDEX |
One statement creates both the index rule and its binding |
| S6 | CREATE TOPN / DROP TOPN: source measure, field, sort, group-by tags, filter, counter and LRU sizes |
Grammar, mapped to TopNAggregationRegistryService |
| S7 | Shared behavior: IF [NOT] EXISTS; DROP GROUP ... DRY RUN, which lists what would be deleted; return the new schema revision; optionally wait until the change has reached every node |
Grammar, plus the existing dry-run and revision-barrier support |
Design first: the foundation
Apart from B1 and B2, every remaining gap needs operators that run on the liaison, so the foundation comes before any feature work:
- Language surface. A SQL dialect with
FROM group.measure, atimecolumn,?parameters, and results returned as a table (columns and rows). - Plan layer. Parse SQL into a tree of logical operators. Whatever the engine can already do
goes down to the measure scan — today’s
QueryRequest, which now includes time buckets, tag aggregation and count-distinct. Everything else runs as operators on the liaison. The plan is compiled once, cached and reused, as described in the plan-cache design.
Next move: B1–B3
These prove the foundation works end to end:
- B1 and B2 (time predicate, aliases). They need no new operator, so they test the new grammar and the planner’s pushdown on their own.
- B3 (several aggregates). This is the first query the planner must split into more than one scan and merge on the liaison, so it exercises the liaison-operator path.
After that
- B4, B5, B6, B7, B9 — post-aggregation operators and expressions. Build C1 alongside B7, since both need the same function and expression evaluation.
- B8 — field filters and float literals. Filter on the liaison first, then push the filter down to data nodes, which is an engine change.
- C2, C3 — added as they are needed.
- D — D1, then D2, then D3. The liaison needs memory limits before these land, because joins and set operations hold state.
DDL track (S): in parallel with the query work
DDL does not need the plan layer. Each statement maps to one registry call (or two, for
CREATE INDEX), so it can proceed alongside the query work:
- S1 first. It is read-only, so it needs only
schema:readand no new safety work. - Per-statement authorization and safety. Do this before any statement that changes schema.
Today the BydbQL
QueryRPC is authorized once, asdata:read(banyand/liaison/grpc/methodpolicy.go:275). DDL needsschema:write, so permission must be decided per statement after parsing, or DDL gets its own RPC. Read-only clients must also keep rejecting DDL: the bydbctl/MCP guardIsReadOnly(bydbctl/internal/tui/bydbql/read_only.go) currently does this by keyword matching and should decide from the parsed statement instead. - S2, then S3.
ALTERis a read-modify-write through the registry’sUpdateRPC. It must reject the write if the schema changed in the meantime (compare revisions), and it stays subject to the registry’s existing compatibility checks — for example, on changing a tag’s type. - S5, S6, S4.
- S7 — build
IF [NOT] EXISTSand the returned revision in from the start; add dry-run and wait-for-propagation last.