RiverQL Reference
Query PostgreSQL, MySQL, SQLite, MongoDB, and SQL Server through a single consistent syntax. Read the docs, earn XP, and collect badges as you learn.
Query Basics
Every RiverQL query starts with find. It's the universal entry point that maps to SELECT in SQL. Write once, run on PostgreSQL, MySQL, SQLite, MongoDB, or SQL Server.
find — SELECT
Retrieve all columns from a table. The simplest possible query.
find usersEquivalent to SELECT * FROM users in standard SQL.
Column Selection
Use square brackets to pick specific columns instead of fetching everything.
find [name, email] from usersfind [name, email, department] from userswhere status = "active"Filtering with where
Filter rows using where. Chain conditions with and / or.
find users where status = "active"find users where status = "active" and age > 21Sorting with order by
Sort results ascending or descending with order by.
find [name, salary] from usersorder by salary descfind [name, department, salary] from employeesorder by department asc, salary descLimiting & Pagination
Control result count with limit and paginate using offset.
find users limit 10find [name, email] from usersorder by created_at desclimit 20 offset 40find → from → where → order by → limit → offsetFull Combined Query
find [name, email, department, salary] from userswhere status = "active" and salary > 50000order by salary desclimit 10Expressions & Operators
RiverQL supports a rich set of operators for comparisons, logic, pattern matching, arithmetic, type casting, and time intervals.
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
| = | Equal | status = "active" |
| != | Not equal | status != "banned" |
| <> | Not equal (alt) | status <> "inactive" |
| > | Greater than | age > 21 |
| >= | Greater or equal | salary >= 75000 |
| < | Less than | price < 100 |
| <= | Less or equal | total <= 50.00 |
find users where age > 21find users where salary >= 75000find products where price < 100Logical Operators
Combine conditions with and, or, and not. Use parentheses to control precedence.
find users where status = "active" and department = "Engineering"find users where department = "Sales" or department = "Marketing"find users where not status = "banned"find userswhere (department = "Sales" or department = "Marketing") and salary > 60000NULL Handling
find users where deleted_at is nullfind users where email_verified_at is not null-- coalesce: first non-null valuefind [name, coalesce(nickname, name) as display_name] from users -- nullif: returns NULL if values are equal (avoids division by zero)find [name, nullif(discount, 0) as effective_discount] from products -- ifnull: two-argument coalesce shorthandfind [name, ifnull(phone, "N/A") as contact] from usersBETWEEN / IN / NOT IN
find [name, created_at] from userswhere created_at between "2024-01-01" and "2024-12-31" find products where price between 10 and 50find users where status in ("active", "pending")find users where department not in ("HR", "Legal")Pattern Matching — LIKE / ILIKE
% matches any number of characters. _ matches exactly one character. ilike is case-insensitive.
find users where name like "%smith%"find users where email like "%@gmail.com"find users where name like "J_n"find users where name ilike "%smith%" -- case-insensitiveArithmetic & String Concat
find [name, price * 1.1 as price_with_tax] from productsfind [first || " " || last as full_name] from usersfind [abs(amount), round(price, 2), ceil(score), floor(score)] from productsType Casting
-- Function stylefind [name, cast(age as string) as age_str] from users -- Shorthand operator ::find [name, created_at::string as date_str] from usersstring, integer, float, boolean, datetime, jsonInterval Literals
Use shorthand suffixes for relative time calculations with now().
find users where created_at > now() - 30dfind users where last_login < now() - 90dfind users where created_at > now() - 1h| Suffix | Meaning |
|---|---|
| y | Years |
| mon | Months |
| w | Weeks |
| d | Days |
| h | Hours |
| m | Minutes |
| s | Seconds |
Named Parameters
:start_date = "2024-01-01":end_date = "2024-12-31" find [name, total, created_at] from orderswhere created_at between :start_date and :end_dateJoins
Joins combine rows from two or more tables based on a related column. RiverQL supports all standard join types with concise, readable syntax.
Inner Join
Returns only rows that have matching values in both tables. join is shorthand for inner join.
find [u.name, o.total]from users as ujoin orders as o on u.id = o.user_idLeft Join
Returns all rows from the left table, with matched rows from the right (or NULL if no match).
find [u.name, o.total]from users as uleft join orders as o on u.id = o.user_idRight Join
Returns all rows from the right table, with matched rows from the left.
find [u.name, o.total]from users as uright join orders as o on u.id = o.user_idFull Join
Returns all rows from both tables, with NULLs where there is no match on either side.
find [u.name, o.total]from users as ufull join orders as o on u.id = o.user_idCross Join
Returns the Cartesian product of both tables. No ON clause needed. Use with caution on large tables.
find [u.name, p.name as product]from users as ucross join products as plimit 100limit.Multiple Joins
Chain joins to combine three or more tables in a single query.
find [u.name, o.total, p.name as product]from users as ujoin orders as o on u.id = o.user_idjoin order_items as oi on o.id = oi.order_idjoin products as p on oi.product_id = p.idSelf Join
Join a table to itself using different aliases — great for hierarchical data like org charts.
find [e.name, m.name as manager]from employees as eleft join employees as m on e.manager_id = m.idJoins with Filters
find [u.name, o.total, o.status]from users as ujoin orders as o on u.id = o.user_idwhere o.status = "paid" and o.total > 100order by o.total desclimit 20Join Type Reference
| Type | Keyword | Returns |
|---|---|---|
| Inner | join / inner join | Matching rows from both tables |
| Left | left join | All left rows + matched right (NULL if none) |
| Right | right join | All right rows + matched left (NULL if none) |
| Full | full join | All rows from both, NULLs for no match |
| Cross | cross join | Cartesian product (no ON clause) |
Aggregation
Aggregate functions compute a single value from a set of rows. Combine with group by to summarize groups, and having to filter them.
Aggregate Functions
| Function | Description |
|---|---|
| count(*) | Count all rows |
| count(expr) | Count non-NULL values |
| count_distinct(expr) | Count distinct non-NULL values |
| sum(expr) | Sum of values |
| avg(expr) | Average (mean) |
| min(expr) | Minimum value |
| max(expr) | Maximum value |
find [count(*)] from usersfind [sum(total)] from ordersfind [avg(salary)] from employeesfind [min(price), max(price)] from productsMultiple Aggregates
find [ count(*) as total_orders, sum(total) as revenue, avg(total) as avg_order, min(total) as smallest, max(total) as largest]from orderswhere status = "paid"GROUP BY
Group rows by one or more columns, then apply aggregates to each group.
find [department, count(*) as headcount]from employeesgroup by departmentfind [department, status, count(*) as cnt]from employeesgroup by department, statusGROUP BY with Full Projection
find [ category, count(*) as product_count, avg(price) as avg_price, min(price) as cheapest, max(price) as most_expensive]from productsgroup by categoryorder by product_count descHAVING — Filter Groups
having filters groups after aggregation. Unlike where, it operates on aggregate values.
find [user_id, count(*) as order_count]from ordersgroup by user_idhaving count(*) > 5having filters groups after aggregation.
WHERE + HAVING Combined
find [department, avg(salary) as avg_sal]from employeeswhere status = "active"group by departmenthaving avg(salary) > 75000Aggregation with Joins
find [u.name, count(*) as order_count, sum(o.total) as total_spent]from users as ujoin orders as o on u.id = o.user_idwhere o.status = "paid"group by u.namehaving total_spent > 1000order by total_spent desclimit 10Window Functions
Window functions perform calculations across related rows without collapsing them. They preserve every row while computing values like rankings, running totals, and moving averages.
Window Syntax
function() over (partition by col order by col)partition by divides rows into groups (like GROUP BY, but rows are preserved).order by defines row order within each partition.ROW_NUMBER
Assigns a unique sequential integer to each row within its partition, ordered by the specified column.
find [ name, department, salary, row_number() over (partition by department order by salary desc) as rank]from employeesRANK and DENSE_RANK
rank() leaves gaps after ties. dense_rank() does not.
-- rank(): ties share a rank, next rank has a gap (1, 1, 3...)find [name, score, rank() over (order by score desc) as position]from players -- dense_rank(): no gaps (1, 1, 2...)find [name, score, dense_rank() over (order by score desc) as position]from playersLAG and LEAD
Access values from previous or next rows within the partition.
find [ date, revenue, lag(revenue, 1) over (order by date) as prev_day, lead(revenue, 1) over (order by date) as next_day]from daily_stats-- Day-over-day changefind [ date, revenue, revenue - lag(revenue, 1) over (order by date) as daily_change]from daily_statsRunning Totals
find [ date, amount, sum(amount) over (order by date) as running_total]from transactionsAggregates Over Windows
find [ name, department, salary, avg(salary) over (partition by department) as dept_avg, salary - avg(salary) over (partition by department) as diff_from_avg]from employeesNamed Windows
Define a reusable window spec with window to avoid repetition.
find [ name, department, salary, avg(salary) over w as dept_avg, max(salary) over w as dept_max, min(salary) over w as dept_min]from employeeswindow w as (partition by department)Top N Per Group
-- Top 3 highest-paid employees per departmentfind * from ( find [ name, department, salary, row_number() over (partition by department order by salary desc) as rn ] from employees) as rankedwhere rn <= 3Window Function Reference
| Function | Description |
|---|---|
| row_number() | Unique sequential number per partition |
| rank() | Rank with gaps after ties |
| dense_rank() | Rank without gaps |
| lag(expr, N) | Value from N rows before |
| lead(expr, N) | Value from N rows after |
| first_value(expr) | First value in the window frame |
| last_value(expr) | Last value in the window frame |
| nth_value(expr, N) | Nth value in the window frame |
| sum/avg/min/max over (...) | Any aggregate used as a window function |
Advanced Queries
CTEs, subqueries, set operations, CASE expressions, and cross-database joins — the full toolkit for complex data retrieval.
CTEs — Common Table Expressions
Define temporary named result sets with with. Makes complex queries readable and composable.
with active_users as ( find * from users where status = "active")find [name, email] from active_userswith paid_orders as ( find * from orders where status = "paid" ), user_totals as ( find [user_id, sum(total) as revenue] from paid_orders group by user_id )find [u.name, ut.revenue]from users as ujoin user_totals as ut on u.id = ut.user_idwhere ut.revenue > 10order by ut.revenue descRecursive CTEs
Traverse hierarchical data like org charts and category trees using with recursive.
with recursive org_tree as ( -- Base case: top-level employees find * from employees where manager_id is null union all -- Recursive: join children to parent find [e.*] from employees as e join org_tree as t on e.manager_id = t.id)find * from org_treeSubqueries
-- Scalar subquery in WHEREfind [name, salary] from userswhere salary > ( find [avg(salary)] from users)-- IN subqueryfind [name, department] from employeeswhere department in ( find distinct [department] from departments where budget > 100000)-- EXISTS / NOT EXISTSfind [name] from users as uwhere exists ( find [1] from orders as o where o.user_id = u.id) -- Find users with no ordersfind [name] from users as uwhere not exists ( find [1] from orders as o where o.user_id = u.id)-- Derived table (subquery in FROM)find * from ( find [user_id, sum(total) as revenue] from orders group by user_id) as user_revenuewhere revenue > 500Set Operations
-- UNION (deduplicates)find [name, email] from customersunionfind [name, email] from suppliers -- UNION ALL (keeps duplicates — faster)find [name, email] from customersunion allfind [name, email] from suppliers -- INTERSECT (rows in both)find [user_id] from orders where year = 2024intersectfind [user_id] from orders where year = 2025 -- EXCEPT (rows only in first)find [user_id] from usersexceptfind [user_id] from orders where created_at > now() - 30dCASE Expressions
-- Searched CASEfind [ name, salary, case when salary < 50000 then "Low" when salary >= 50000 and salary < 100000 then "Medium" when salary >= 100000 then "High" else "Unknown" end as salary_band]from users-- Simple CASEfind [ name, case status when "active" then "Active User" when "suspended" then "Suspended" else "Unknown" end as status_label]from users-- CASE in ORDER BY for custom sort priorityfind [name, priority] from tasksorder by case priority when "urgent" then 1 when "high" then 2 when "normal" then 3 else 4endCross-Database Queries
Append @connection to any table name to query across different database systems.
-- Join PostgreSQL users with MySQL ordersfind [u.name, o.total]from users@pg as ujoin orders@mysql as o on u.id = o.user_idwhere o.status = "paid"-- Cross-database CTEwith pg_users as ( find [id, name] from users@pg where status = "active" ), mongo_logs as ( find [user_id, action, timestamp] from logs@mongo where timestamp > now() - 7d )find [pg_users.name, count(*) as login_count]from pg_usersjoin mongo_logs on pg_users.id = mongo_logs.user_idwhere mongo_logs.action = "login"group by pg_users.nameorder by login_count desclimit 10where filters before cross-database joins to minimize data transfer. Index join columns on both sides.Data Modification
RiverQL maps SQL DML and DDL to human-readable commands: create for INSERT, update for UPDATE, remove for DELETE, and standard alter table syntax.
INSERT — create
-- Single rowcreate users { name: "Alice", email: "alice@example.com", age: 30 } -- Multiple rowscreate users [ { name: "Alice", email: "alice@example.com" }, { name: "Bob", email: "bob@example.com" }, { name: "Carol", email: "carol@example.com" }] -- Insert from querycreate active_users_backup ( find * from users where status = "active") -- Target a specific connectioncreate users@pg { name: "Dave", email: "dave@example.com" }UPDATE
Modify existing rows with update ... set ... where. Always include where unless you intend to update all rows.
-- Basic updateupdate usersset status = "inactive", updated_at = now()where last_login < now() - 90d -- Update with expressionsupdate productsset price = price * 1.1where category = "premium" -- Target a specific connectionupdate users@pgset status = "verified"where email_verified_at is not nullwhere, the update applies to ALL rows in the table.DELETE — remove
-- Basic deleteremove users where status = "banned" -- Delete with subqueryremove userswhere id not in ( find distinct [user_id] from orders where created_at > now() - 365d) -- Delete on a specific connectionremove logs@mongowhere timestamp < now() - 30dwhere, all rows are deleted. Always use where unless you intend a full-table deletion.CREATE TABLE
-- Basic tablecreate table products ( name string, price float, category string default "general", created_at datetime) -- With primary key and constraintscreate table users ( id int primary key, name string not null, email string not null, status string default "active") -- Idempotent (if not exists)create table if not exists cache ( key string primary key, value json, expires_at datetime)ALTER TABLE
alter table users add column bio stringalter table users add column tier string not null default "free"alter table users drop column temp_dataalter table users alter column age type floatalter table users alter column status type string not null default "active"alter table users alter column status drop defaultalter table users rename column name to full_namealter table users rename to customersalter table users@pg add column notes stringPersisting Query Results — >>
Save results of any query to a table using the >> operator.
-- Simple persistfind * from users >> user_backup -- With conflict handling (upsert)find [user_id, sum(total) as revenue]from ordersgroup by user_id>> user_revenue@pginsert if exists on conflict replace -- Ignore duplicatesfind distinct [email] from new_signups>> verified_emailsinsert if exists on conflict ignoreOperations Reference
| Operation | RiverQL | SQL Equivalent |
|---|---|---|
| Insert one | create t { ... } | INSERT INTO t VALUES (...) |
| Insert many | create t [{ ... }, { ... }] | INSERT INTO t VALUES (...), (...) |
| Insert from query | create t (find ...) | INSERT INTO t SELECT ... |
| Persist results | find ... >> target | CREATE TABLE AS + INSERT |
| Upsert | >> target insert if exists on conflict replace | ON CONFLICT DO UPDATE |
| Update | update t set ... where ... | UPDATE t SET ... WHERE ... |
| Delete | remove t where ... | DELETE FROM t WHERE ... |
Meta Commands
Meta commands inspect database structure and query plans without fetching application data. Essential tools for exploration and optimization.
DESCRIBE
View the schema of any table — column names, types, constraints, and defaults.
describe users -- Target a specific connectiondescribe users@pgdescribe orders@mysqldescribe inventory.products@pgSHOW TABLES
List all tables (or collections) in the current or specified connection.
show tables -- For a specific connectionshow tables @pgshow tables @mongoshow tables @mysqlEXPLAIN
View the execution plan without running the query. Essential before executing expensive operations.
explain find [name] from userswhere department = "Engineering"order by salary desc-- Cross-database explainexplain find [u.name, o.total]from users@pg as ujoin orders@mysql as o on u.id = o.user_idexplain before running expensive cross-database joins to understand the fetch strategy.Named Parameters
Define reusable session-scoped values with the : prefix. Persist for the duration of the session.
:start_date = "2024-01-01":end_date = "2024-12-31":min_amount = 100 find [name, total, created_at] from orderswhere created_at between :start_date and :end_date and total > :min_amountComments
-- This is a single-line commentfind users -- inline comment /* This is a multi-line comment */find users where status = "active"Multiple Statements
Separate multiple statements with semicolons to execute them in sequence.
:threshold = 1000;find [name, total] from orders where total > :threshold;describe ordersConnection Configuration (river.yaml)
- name: pg kind: postgres uri: "postgres://river:river@localhost:5432/river" schema: public - name: mysql kind: mysql uri: "mysql://river:river@localhost:3306/river" schema: river - name: mongo kind: mongodb uri: "mongodb://localhost:27017" - name: sqlite kind: sqlite uri: "sqlite:river.db?mode=rwc"-) in connection names. Use underscores instead.The Cardinal Rules
| Rule | Description |
|---|---|
| 1. Every query starts with find | find is the universal SELECT keyword |
| 2. Strict clause order | find → from → where → group by → having → order by → limit → offset |
| 3. Always use where with update/remove | Omitting where affects all rows |
| 4. @connection for cross-database | Append @name to route queries to specific databases |
| 5. explain before expensive queries | Understand the plan before executing it |
Your Progress
Mark sections complete to earn XP and unlock badges.
Achievements
0/8 unlocked1
Level
0
Total XP
0d
Streak
Quick Reference
The essentials at a glance.
Clause Order
find [columns] from table[@conn]
where condition
group by columns
having condition
window name as (spec)
order by col [asc|desc]
limit N offset MCardinal Rules
- 1.Every query starts with find
- 2.Strict clause order — no exceptions
- 3.Always use where with update/remove
- 4.@connection enables cross-DB queries
- 5.Use explain before expensive queries
Operators
Aggregate Functions
RiverQL — write once, query anywhere.