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

querySELECT … SUM() RonSQLparse, compile partition 0 · partial aggregate partition 1 · partial aggregate partition 2 · partial aggregate … every partition merged resultsmall: only aggregates travel

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}
]}
FieldValues
query · databaseOne statement ending in ';' · all tables from this one database
outputFormatJSON (default), JSON_ASCII, TEXT, TEXT_NOHEADER
explainModeALLOW (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_SCAN with 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

BodyProducesTypical use
GROUP BYOne row per groupPer-key aggregates to join or re-aggregate
ScalarExactly one row of aggregatesA threshold, a total
Single-row key lookupThe row with that primary keyStart a join from one entity
Last-NThe N newest rows, by ORDER BY … LIMITThe 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.

ronsql_cli sample data
ronsql_cli -e "
WITH last10 AS (
SELECT customer_id, event_time, amount
FROM transactions_1
WHERE customer_id = 42
ORDER BY event_time DESC
LIMIT 10),
last1h AS (
SELECT customer_id, event_time, amount
FROM transactions_1
WHERE customer_id = 42
AND event_time >= '2026-10-05 23:00:00'
ORDER BY event_time DESC
LIMIT 50),
a10 AS (SELECT COUNT(*) AS n_10, AVG(amount) AS avg_10
FROM last10),
a1h AS (SELECT COUNT(*) AS n_1h, AVG(amount) AS avg_1h
FROM last1h)
SELECT n_10, avg_10, n_1h, avg_1h
FROM a10, a1h;"
n_10 avg_10 n_1h avg_1h
10 186.420000 2 76.150000

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.

ronsql_cli sample data
ronsql_cli -e "
WITH `b` AS (SELECT `account_id`, COUNT(*) AS `hw_cnt`
FROM `cards_1`
WHERE `card_id` = 4711
GROUP BY `account_id`)
SELECT `j2`.`account_age_days` AS `acct_account_age_days`,
`j2`.`status` AS `acct_status`,
`j3`.`kyc_level` AS `cust_kyc_level`,
`j3`.`risk_segment` AS `cust_risk_segment`
FROM `b`
JOIN `accounts_1` AS `j2`
ON `j2`.`account_id` = `b`.`account_id`
JOIN `customers_1` AS `j3`
ON `j3`.`customer_id` = `j2`.`customer_id`;"
acct_account_age_days acct_status cust_kyc_level cust_risk_segment
1290 active 2 low

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.

rdrs · /0.1.0/ronsql sample data
Q='
SELECT station_id,
AVRO(no2_hourly) AS no2_hourly
FROM station_measurements_1
WHERE station_id = 17;'
jq -n --arg q "$Q" \
'{database: "air_quality", query: $q}' |
curl -s --json @- "$RDRS/0.1.0/ronsql"
{"data":
[{"station_id":17,"no2_hourly":[31,28,35,42,39,37]}
]
}

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

LimitValue
Values in an IN list, run as key lookups or ranges4095
Rows sorted client-side, for ORDER BY without an index1 000 000 rows and 256 MB
GROUP BY columns · aggregate results127 · 255
Operations in one pushed join32
Values or rows from a subquery1000

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.

Next · 23 · TTL and ring buffer tables