RonSQL Single-Table Queries#
This chapter describes the RonSQL query classes that read a single
table: aggregate queries, which compute COUNT, SUM, MIN, MAX and
AVG with optional GROUP BY and HAVING, and projection-only
queries, which return plain column values without aggregation. Both
classes support WHERE, ORDER BY and LIMIT, and both push filtering
down to the data nodes; aggregate queries also push the aggregation
itself down, so only the aggregated result travels back. For how to
submit queries and read results, see the RonSQL chapter;
for which column types can be used where, see Data types and
output; for queries over several tables, see Join
queries and CTEs and subqueries.
The examples in this chapter use the following table:
CREATE TABLE orders (
o_id INT NOT NULL,
o_custkey INT NOT NULL,
o_status CHAR(1) NOT NULL,
o_total DECIMAL(12,2) NOT NULL,
o_date DATE NOT NULL,
o_comment VARCHAR(100),
PRIMARY KEY (o_id),
INDEX idx_custkey (o_custkey),
INDEX idx_status_date (o_status, o_date)
) ENGINE=NDB;
Aggregate queries#
An aggregate query has at least one aggregate function in its SELECT
list (or in HAVING). Without GROUP BY it returns exactly one row;
with GROUP BY it returns one row per group.
SELECT o_custkey, COUNT(*), SUM(o_total) AS total
FROM orders
WHERE o_status = 'O'
GROUP BY o_custkey;
The scan, the filter and the aggregation all execute inside the data nodes, in parallel across table fragments. Only the per-group results are returned.
The SELECT list#
Each item in the SELECT list of an aggregate query must be one of:
-
A column name, plain (
o_custkey) or qualified (orders.o_custkey). Every such column must also appear in theGROUP BYclause; otherwise the query is rejected withUngrouped column in non-aggregated SELECT expression.Write the column the same way, qualified or unqualified, in theSELECTlist and inGROUP BY. -
An aggregate function call, described below.
Expressions outside aggregate functions are not supported:
SELECT SUM(a) + 1, SELECT SUM(a) / SUM(b) and SELECT 1 are syntax
errors. Compute such values from the result on the client side.
Any item can be given an alias with AS; the AS keyword is required.
Without an alias, the output column is named by the SQL text of the item
itself, and that text is limited to 64 bytes -- longer expressions must
be aliased. Aliases and identifiers are limited to 64 bytes.
Aggregate functions#
The supported aggregate functions are COUNT, SUM, MIN, MAX and
AVG, plus COUNT(*). COUNT(expr) counts rows where the argument is
not NULL; COUNT(*) counts all rows. Other MySQL aggregate functions
such as STD, STDDEV, VARIANCE, VAR_POP, BIT_AND and
GROUP_CONCAT, as well as COUNT(DISTINCT ...), are not implemented
and are rejected with an “Unimplemented keyword” error.
SUM and AVG accept numeric arguments: integer types, FLOAT,
DOUBLE and DECIMAL. MIN, MAX and COUNT additionally accept
CHAR and VARCHAR columns (compared using the column’s collation) and
the temporal types DATE, YEAR, DATETIME, TIME and TIMESTAMP.
Integer sums are computed exactly in 64 bits, and an overflow fails the
query. SUM, AVG, MIN and MAX over FLOAT, DOUBLE and
DECIMAL columns with a nonzero scale are computed in double precision,
so they can differ from MySQL’s exact DECIMAL arithmetic in the least
significant digits. See Data types and output for the
result types and formatting of each aggregate function.
For a scalar aggregate query (no GROUP BY) over an empty table or an
empty filter result, COUNT returns 0 and the other aggregate functions
return NULL, as in MySQL. RonSQL does not support time zones and
behaves as if the time zone were UTC; TIMESTAMP values are therefore
displayed in UTC.
Aggregate arguments#
The argument of an aggregate function is an arithmetic expression built from:
-
column names (plain or qualified);
-
integer literals (up to 9223372036854775807 in magnitude) -- fractional and string literals are not accepted in arithmetic;
-
unary minus and parentheses;
-
the operators
+,-,*,/(floating point division),DIV(integer division) and%(modulo); -
CASE WHEN condition THEN expr ELSE expr END; -
GREATEST(expr, expr, ...)andLEAST(expr, expr, ...).
CASE is supported in the searched form with exactly one WHEN branch,
and the ELSE branch is mandatory. The THEN and ELSE expressions
cannot be NULL, so the common MySQL idiom
SUM(CASE WHEN c THEN x END) must be written as
SUM(CASE WHEN c THEN x ELSE 0 END) (or use COUNT with a suitable
WHERE condition). The condition must be a single comparison (=,
!=, <, <=, >, >=) between a column and a literal, or a chain
of such comparisons combined only with AND or only with OR; an IN
list counts as an OR chain. In the condition, the literal may also be
a fractional or string literal, and a column can be compared with
another integer or floating point column. IS NULL, LIKE, NOT and
mixed AND/OR are not supported in CASE conditions.
GREATEST and LEAST take two or more operands, each of which is a
column, an integer literal or a nested GREATEST or LEAST; arithmetic
operands are not supported. At least one operand must be a column, and
column operands must have integer types.
Constant integer subexpressions are computed when the query is prepared;
overflow, and DIV or % by the constant zero, are reported as errors.
At runtime, division by zero produces NULL for that row, following
MySQL semantics, while integer overflow fails the query (see Data types
and output). A common pattern is conditional counting:
SELECT o_status,
SUM(CASE WHEN o_custkey < 1000 THEN 1 ELSE 0 END) AS low_keys,
MAX(GREATEST(o_custkey, 500)) AS capped_max
FROM orders
GROUP BY o_status;
GROUP BY#
GROUP BY takes a comma-separated list of column names, plain or
qualified, at most 127 columns. Expressions, aliases, column positions
and WITH ROLLUP are not accepted in GROUP BY. Grouping is supported
on all integer types, FLOAT, DOUBLE, DECIMAL, CHAR, VARCHAR,
DATE, DATETIME and TIMESTAMP columns. Grouping on BIT,
BINARY/VARBINARY, BLOB/TEXT, TIME and YEAR columns is not
supported; such a query fails when the first group is printed. A
GROUP BY clause on a query without aggregate functions is rejected.
The columns in GROUP BY need not appear in the SELECT list.
HAVING#
HAVING filters groups after aggregation. A HAVING condition is built
from aggregate function calls (which need not appear in the SELECT
list) and numeric literals, combined with +, -, *, /, the
comparison operators =, !=, <>, <, <=, >, >=, the logical
operators AND, OR, NOT, and IS NULL / IS NOT NULL on an
aggregate result. The conditions are evaluated in double precision.
SELECT o_custkey, SUM(o_total) AS total
FROM orders
GROUP BY o_custkey
HAVING SUM(o_total) > 10000 AND COUNT(*) >= 3;
Note that HAVING must repeat the aggregate expression: referring to
the alias of an aggregate output, such as HAVING total > 10000 in the
example above, is not supported. The only exception is the alias of a
correlated subquery in the SELECT list; see CTEs and
subqueries. Plain table columns, including GROUP BY
columns, are not supported in HAVING either; filter on them in the
WHERE clause instead. Both are rejected with
HAVING can only reference aggregate functions. XOR, LIKE, DIV,
%, bitwise operators and string literals are not supported in
HAVING.
ORDER BY and LIMIT#
On an aggregate query, ORDER BY accepts a list of GROUP BY columns
and aliases of outputs, each optionally followed by ASC (the default)
or DESC. Ordering by an aggregate expression directly, such as
ORDER BY SUM(o_total), is a syntax error -- give the aggregate an
alias and order by the alias:
SELECT o_custkey, SUM(o_total) AS total
FROM orders
GROUP BY o_custkey
ORDER BY total DESC, o_custkey
LIMIT 10;
An ORDER BY column that is not in the GROUP BY clause is rejected,
which includes any column in ORDER BY of a scalar aggregate query
(which has no GROUP BY). Sorting follows MySQL semantics: NULL
values sort first in ascending order and last in descending order, and
string columns sort according to their collation. The sort is performed
by RonSQL after the data nodes have returned the groups.
LIMIT takes a single non-negative integer and is applied after
HAVING and ORDER BY. LIMIT without ORDER BY is allowed and
returns an arbitrary subset of the groups. LIMIT 0 produces an empty
result -- in text output not even the header row, matching the mysql
client. The MySQL forms LIMIT offset, count and OFFSET are not
supported.
Projection-only queries#
A projection-only query selects plain column values without aggregation:
Every item in the SELECT list must be a plain or qualified column name
or an AVRO(column) call (see Data types and
output), optionally aliased with AS. Expressions in
the SELECT list, such as SELECT o_total + 1, are a syntax error, and
SELECT * is not supported. GROUP BY and HAVING are not allowed on
projection-only queries, and a single-table projection may not carry a
WITH clause that it does not use.
Columns of all integer types, FLOAT, DOUBLE, DECIMAL, CHAR,
VARCHAR, BINARY, VARBINARY, DATE, YEAR, DATETIME, TIME and
TIMESTAMP can be projected. Binary values are returned base64 encoded
in the JSON output formats. Projecting a BIT or BLOB/TEXT column
fails with the error
Unsupported column type (N) in pass-through result., where N is an
internal type code.
How the query executes#
RonSQL automatically chooses an access method, visible in EXPLAIN:
-
Primary key lookup. When the
WHEREclause covers every primary key column with equality conditions (in any order, including composite andVARCHARkeys), a projection-only query executes as a single-row lookup. Any additional conditions ride along as a filter evaluated on the data node, so a row that exists but fails the filter yields an empty result. Contradictory conditions such aso_id = 77 AND o_id = 78correctly yield an empty result. A very large residual filter makes the query fall back to a scan. -
Several primary key lookups. When one of the primary key columns is given by an
INlist and the others by equality conditions, the query executes as one primary key lookup per distinct value, all in one round trip, for up to 4095 values. This also applies to aggregate queries, which then aggregate the rows read by the lookups: -
Ordered index scan. When the
WHEREclause matches an ordered index -- equality conditions on a prefix of the index columns, optionally followed by a range condition on the next column -- the query scans only the matching index range. AnINlist on an index column becomes a scan of several ranges, one per distinct value, for up to 4095 values. Conditions not expressible as index bounds are pushed down as filters on the scanned rows. Among several usable indexes, RonSQL prefers the one with the most equality bounds. -
Table scan. Otherwise the whole table is scanned, with all
WHEREconditions pushed down as filters, so non-matching rows are discarded inside the data nodes. -
Row count statistics. A query whose only outputs are
COUNT(*), with noWHERE,GROUP BYorHAVINGclause, reads the row counts that the data nodes maintain for each table fragment instead of scanning the table, like a MySQL server does for the same query.
EXPLAIN prints Execute as primary key lookup.,
Execute as N primary key lookups (IN list on ...),
Execute as index scan. (with the chosen index and, for each condition,
whether it became an index bound or a filter), Execute as table scan.
or Execute as fragment-stats COUNT(*) (no row scan). The same
access-method selection applies to the scans of aggregate queries,
except that an aggregate query with equality conditions on the whole
primary key executes as a scan restricted to that key.
ORDER BY and LIMIT on projections#
LIMIT on a projection-only query streams rows and stops as soon as the
limit is reached, closing the scan early -- an effective way to sample a
large table. LIMIT 0 produces an empty result. Without ORDER BY,
which rows are returned by a truncating LIMIT is not deterministic.
ORDER BY accepts table columns (plain or qualified, not necessarily in
the SELECT list) and output aliases, each with optional ASC/DESC.
It executes in one of two ways:
-
Index order (streaming). When the ordered index chosen for the scan also delivers the requested order -- the
ORDER BYlist equals the index columns that follow any leading equality conditions, with one direction across all keys -- the per-fragment ordered scans are merged into one globally ordered stream. Combined withLIMIT, this returns the top N rows while reading only as much of the index as needed. An index can be chosen for its ordering alone, even without a usableWHEREbound. A scan of several ranges from anINlist does not deliver index order. -
Client-side sort (buffered). Otherwise all matching rows are buffered and sorted before printing, and
LIMITis applied after the sort. The buffer is capped at 1,000,000 rows or 256 MB; exceeding the cap fails with an error suggesting to tighten theWHEREclause or use an aggregate query.
This example streams in index order via idx_status_date: the equality
on o_status binds the first index column and the index delivers
o_date order. EXPLAIN reports the strategy as
ORDER BY: index order ... or ORDER BY: client-side sort .... Mixed
ASC/DESC directions always use the client-side sort. NULL values
sort first in ascending and last in descending order.
The WHERE clause#
Both query classes share the same WHERE support. A condition is a
combination of predicates using AND (&&), OR (||), XOR, NOT
and parentheses. Note that || is logical OR, never string
concatenation. The supported predicate forms are:
-
column <cmp> literalandliteral <cmp> column, where<cmp>is=,!=,<>,<,<=,>or>=; -
column <cmp> column, comparing two columns of the table that have identical types; -
column LIKE 'pattern'with the MySQL wildcards%and_(the pattern must be a string literal); -
column IS NULLandcolumn IS NOT NULL; -
column IN (literal, literal, ...), equivalent to a chain ofOR-ed equalities; -
GREATEST(...)orLEAST(...)compared against a constant with<,<=,>or>=; the arguments must be columns or integer literals, including at least one column; -
scalar subqueries,
INsubqueries andEXISTSsubqueries -- see CTEs and subqueries.
Every predicate must compare a column: WHERE 1 = 1, a bare column used
as a truth value (WHERE flag), and arithmetic on columns
(WHERE a + 1 > 5) are rejected. Arithmetic between constants is not
computed either, so WHERE a > 1 + 2 is rejected; write the computed
value. The operator ! is not supported in WHERE; use NOT.
BETWEEN, x NOT IN (...) and x NOT LIKE ... are not supported;
write x >= a AND x <= b, NOT (x IN (...)) and NOT (x LIKE ...)
instead. A WHERE clause can have at most 64 conditions combined with
AND at the top level.
Literals are integer literals (up to 9223372036854775807 in magnitude,
optionally negated), fractional literals written with a decimal point
and digits on both sides (e.g. 123.45; scientific notation is not
supported), and single-quoted string literals with MySQL backslash
escapes. Adjacent string literals are concatenated; double-quoted
strings are not supported. Date and time values are written as string
literals, e.g. o_date >= '1997-06-01'. The type of a literal must
match the type of the column it is compared with; for example, an
integer column can only be compared with integer literals, and a
DATETIME column only with literals that include a time part. See Data
types and output for the complete rules.
LIKE patterns are matched in the data nodes using the column’s
collation. The escape character is always the backslash, and the
ESCAPE clause is not supported. Trailing spaces of CHAR values are
ignored.
For temporal comparisons, DATE_ADD and DATE_SUB are supported as
constant expressions: the first argument must be a string literal (or a
nested DATE_ADD/DATE_SUB call) and the second an INTERVAL n unit
literal, where the amount is an integer or a string holding an integer
(negative allowed) and the unit is one of MICROSECOND, SECOND,
MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER or YEAR. The whole
expression is folded to a constant when the query is prepared:
DATE_ADD and DATE_SUB cannot be applied to columns and cannot be
used in the SELECT list. The composite interval units (such as
DAY_HOUR or YEAR_MONTH) and the EXTRACT function are not
supported.
Comparisons are supported on columns of all integer types, FLOAT,
DOUBLE, DECIMAL, CHAR, VARCHAR, DATE, DATETIME and
TIMESTAMP. String comparisons use the column’s collation. TIME,
YEAR, binary and BLOB/TEXT columns cannot be used in comparisons,
except with IS NULL and IS NOT NULL.
Index hints#
Since RonSQL has no cost-based optimizer, index selection can be steered
with MySQL-style hints placed after the table name (KEY is a synonym
for INDEX):
SELECT o_custkey, SUM(o_total) AS total
FROM orders FORCE INDEX (idx_custkey)
WHERE o_custkey = 100
GROUP BY o_custkey;
SELECT o_id, o_total FROM orders USE INDEX () WHERE o_total > 500;
-
FORCE INDEX (name, ...)requires one of the named indexes to be used. If no listed index exists or is usable -- noWHEREcondition matches its leading column and it does not serve theORDER BY-- the query is rejected rather than silently falling back to a table scan. -
USE INDEX (name, ...)restricts the choice to the named indexes but allows a table scan if none is usable.USE INDEX ()with an empty list forces a table scan. -
IGNORE INDEX (name, ...)removes the named indexes from consideration.
Only ordered indexes can be used for scans, so hints name ordered
indexes; the ordered index of the primary key is called PRIMARY (it
does not exist when the primary key is defined with USING HASH). Index
names in hints are case insensitive. Unlike MySQL, USE INDEX and
IGNORE INDEX do not report index names that do not exist. Hints are
honored only on the scanned root table of a query (or of a CTE body); a
hint on a joined table is rejected -- see Join
queries. On projection-only queries, hints also
influence the ORDER BY strategy: forcing an index that delivers the
requested order enables index-ordered streaming, and ignoring it forces
the client-side sort.
Not supported#
The following are not supported on single-table queries. See Restrictions and MySQL compatibility for the complete list of RonSQL restrictions.
-
Expressions in the
SELECTlist (syntax error), except aggregate functions andAVRO(column); function calls other than these (“Unknown function”).GREATEST/LEASTas aSELECTitem is only accepted over scalar CTE outputs, see CTEs and subqueries. -
SELECT *,DISTINCT,BETWEEN,OFFSET,UNION,CASTand all other unimplemented MySQL keywords: “Unimplemented keyword. If this was intended as an identifier, use backtick quotation.” -
The aggregate functions
STD,STDDEV,STDDEV_POP,STDDEV_SAMP,VARIANCE,VAR_POP,GROUP_CONCATandCOUNT(DISTINCT ...). -
CASEwithoutELSE, with severalWHENbranches, in the simple formCASE x WHEN ..., or outside an aggregate argument. -
GROUP BYon expressions, aliases or column positions;GROUP BYwithout aggregation;WITH ROLLUP. -
Aliases and plain columns in
HAVING. -
ORDER BYon expressions, aggregate calls or column positions; on aggregate queries,ORDER BYon a column not inGROUP BY. -
LIMIT offset, count, negative or non-integerLIMITvalues, andLIMITplaced beforeORDER BY(syntax errors). -
Constant arithmetic,
!,EXTRACTand composite interval units inWHERE. -
SQL comments (
--,#,/* */) and double-quoted strings anywhere in the statement. -
Columns of unsupported types in the various parts of a query; see Data types and output.
-
Keywords as unquoted identifiers: MySQL keywords, whether implemented in RonSQL or not, must be backtick-quoted to be used as identifiers.
Note also that every statement must end with a semicolon, and that a non-aggregate query shape outside the supported classes is rejected with a message beginning “This query has no aggregate expression, so it is not an aggregate query”, listing the supported projection-only shapes.