RonDB training · module 22 of 25 · 18 min
RonSQL: the query goes to the data
Low-latency SQL for feature stores, executed inside the data nodes. Not a generic SQL engine.
RonSQL · part of the REST API server
RonSQL: a subset of MySQL SELECT, run entirely inside the data nodes
Pushed down
Filters, joins, CTEs and aggregation run where the rows are; intermediate rows never travel.
MySQL or nothing
A query gets MySQL's answer or is rejected with an error; test suites compare every result with MySQL.
SELECT only
No DDL, no DML: tables are created and loaded through MySQL or any other API, and RonSQL reads them.
Every data node must run RonDB 26.10.0 or later; otherwise queries get HTTP 503.
docs: ronsql.md (introduction)
Why · features computed at request time
Why RonSQL: aggregates over fresh events, computed when the model asks
The need
- Real-time inference uses aggregates of many events: the last 10 transactions, the last hour.
- Computed offline, they are stale; computed in the client, the events travel.
- Keep recent events in RonDB (TTL, module 23) and aggregate them where they are.
TPC-H Q1, one scale factor
- Through mysqld: about 1 s, the rows sent up to the MySQL server.
- RonSQL, 6 partitions: 100 ms, every partition aggregating in parallel.
- RonSQL, 12 partitions: 50 ms.
docs: ronsql_bench.md (TPC-H Q1 timings) · ronsql_feature_store.md
Execution · compile, push, merge
RonSQL compiles the query; every partition computes its part
RonSQL parses the SQL and builds a query tree with an aggregation program, the pushed query of module 10.
The data nodes run it on all partitions in parallel; each produces a partial aggregate.
Only the partial results travel; RonSQL merges them. No cost optimizer: the feature store knows its data model.
docs: ronsql.md · new_features_2410.md (aggregation interpreter) · release_notes_26041.md (pushdown join aggregation)
Support · at a glance
RonSQL support: aggregates, joins and CTEs in; DML, window functions, UNION out
| Supported |
|---|
| COUNT, SUM, MIN, MAX, AVG |
| WHERE: comparisons, LIKE, IN, IS NULL |
| GROUP BY columns · HAVING · ORDER BY · LIMIT n |
| JOIN, LEFT JOIN: equality on indexed columns |
| WITH: aggregating, single-row and last-N CTEs |
| Scalar, IN and EXISTS subqueries in WHERE |
| Index hints on the first table · AVRO() |
| Not supported |
|---|
| INSERT, UPDATE, DELETE, DDL |
| SELECT *, DISTINCT, expressions in the select list |
| COUNT(DISTINCT), STD, VARIANCE, GROUP_CONCAT |
| BETWEEN, NOT IN, REGEXP |
| RIGHT, FULL, CROSS, NATURAL joins |
| WITH RECURSIVE, derived tables, UNION |
| Window functions, WITH ROLLUP, SQL comments |
docs: ronsql.md (RonSQL at a glance; condensed)
Running · POST /0.2.0/ronsql
Running RonSQL: send the query as JSON, get rows back as JSON or text
curl -X POST -H "Content-Type: application/json" \
http://localhost:4406/0.2.0/ronsql \
-d '{"query": "SELECT o_custkey, SUM(o_total) FROM orders GROUP BY o_custkey;",
"database": "test"}'
{"data":
[{"o_custkey":100,"SUM(o_total)":800.00}
,{"o_custkey":200,"SUM(o_total)":900.00}
]}
| Field | Values |
|---|---|
| query · database | One statement ending in ';' · all tables from this one database |
| outputFormat | JSON (default), JSON_ASCII, TEXT, TEXT_NOHEADER |
| explainMode | ALLOW (default), FORBID, REQUIRE, REMOVE, FORCE |
docs: ronsql.md (Executing queries, REST API; the example and its response, shortened)
EXPLAIN · see the plan, as text
EXPLAIN shows each table's access method and the join tree
Access methods
PK_LOOKUP,UNIQUE_LOOKUP: key lookups.INDEX_SCANwith bounds or ranges;TABLE_SCAN.CTE_LOOKUP,CTE_SCAN: reading a CTE's result.
Plan details
- [ROOT], then [INNER], [LEFT JOIN], [SEMI], [ANTI] with their parent.
- Pushed filters: "Residual filter: yes"; the "Aggregation leaf".
- AVG is finished client-side, from SUM and COUNT.
Explain output needs outputFormat TEXT. Use it to check that a query uses the index you expect; force one with an index hint.
docs: ronsql.md (EXPLAIN, EXPLAIN output)
PART 1
Joins and CTEs
Whole query trees in the data nodes.
Joins · always pushed
Every RonSQL join runs as a pushed join inside the data nodes
Kinds
INNER and LEFT OUTER joins, in chains and stars.
Conditions
Equality on indexed columns, so each step is a lookup or index scan (module 10).
Size
Up to 32 operations in one pushed join.
Unlike the MySQL server, RonSQL has no fallback: a join it cannot push is rejected with a reason, and MySQL remains for everything else.
docs: ronsql.md (Join queries) · ronsql_joins.md · ronsql_limitations.md (at most 32 internal operations)
CTEs · materialized inside the data nodes
Four kinds of CTE body, and independent CTEs run in parallel
| Body | Produces | Typical use |
|---|---|---|
| GROUP BY | One row per group | Per-key aggregates to join or re-aggregate |
| Scalar | Exactly one row of aggregates | A threshold, a total |
| Single-row key lookup | The row with that primary key | Start a join from one entity |
| Last-N | The N newest rows, by ORDER BY … LIMIT | The last 10 events of a customer |
CTEs materialize in dependency order; independent CTEs materialize in parallel. The whole statement, CTEs included, runs in the data nodes.
docs: ronsql_cte.md (four classes of CTE bodies; independent CTEs materialize in parallel)
RonSQL examples · 1 / 3
RonSQL: two windowed features
The last 10 transactions, and the last hour limited to 50. The two CTEs do not depend on each other, so RonDB computes them in parallel on the data nodes, each as an ordered scan of the primary key that stops at its limit. Two more CTEs aggregate them, and the main query joins their single rows.
from the FSS 2026 talk (2026-10/rondb26 #ex-last10) · docs: ronsql_cte.md (last-N bodies) · the output is sample data
RonSQL examples · 2 / 3
RonSQL: a snowflake schema, card to account to customer
Start from one card, walk two joins out to the account and the customer, and return the features of each. The CTE reads one card; the joins are key lookups in the data nodes.
from the FSS 2026 talk (2026-10/rondb26 #ex-snowflake) · docs: ronsql_joins.md · the output is sample data
RonSQL examples · 3 / 3
RonSQL: AVRO() decodes complex features
Arrays and structs are stored Avro-encoded. AVRO() decodes them with the feature store schema that the REST API server holds, so the query goes through rdrs2.
from the FSS 2026 talk (2026-10/rondb26 #ex-avro) · docs: ronsql_types.md (AVRO()) · the output is sample data
Limits · the ones you may meet
RonSQL limits: they keep every query short and bounded
| Limit | Value |
|---|---|
| Values in an IN list, run as key lookups or ranges | 4095 |
| Rows sorted client-side, for ORDER BY without an index | 1 000 000 rows and 256 MB |
| GROUP BY columns · aggregate results | 127 · 255 |
| Operations in one pushed join | 32 |
| Values or rows from a subquery | 1000 |
docs: ronsql_limitations.md (the consolidated limits)
Errors · what to retry
RonSQL errors: the server retries transient ones; 429 is yours to back off
Retried by rdrs2
- Temporary errors: overload, lock wait timeouts, a table changed since planning; not once result rows have started.
- Up to 10 attempts, backing off from 10 ms to 1 s: a few seconds at most.
- Still failing: HTTP 503, class resource, worth resubmitting later.
Not retried
- Rejected queries: unsupported SQL, unknown columns. Fix the query.
- Rate limited, HTTP 429: back off in the client.
- Older data nodes, HTTP 503.
docs: ronsql.md (Error responses, Error handling and retries)
Recap · module 22
What to take away
- RonSQL: MySQL SELECT semantics, executed inside the data nodes, or rejected.
- POST /0.2.0/ronsql with query and database; JSON or text back.
- Partial aggregates on every partition in parallel: TPC-H Q1 in 50–100 ms.
- Pushed joins and four kinds of CTE; independent CTEs run in parallel.
- EXPLAIN to check plans; limits keep queries bounded; the server retries transient errors.
RonDB training · 25 modules · you are at 22