Relix

Coming from SQL

If you write SQL, you already know most of Relix: the same questions, asked with operators rather than clauses. This page puts the statements you write every day beside their Relix, so you can read one in terms of the other.

Every pair here is checked on each build. The SQL runs on a real database, the Relix runs in the engine, and the two must return the same rows.

One word to watch

In SQL, SELECT chooses columns. In Relix, SELECT chooses rows: it is the relational-algebra selection, the thing SQL calls WHERE. Choosing columns is PROJECT. Every other keyword means what you would guess.

Relix reads inside out. Each operator takes a relation in parentheses and returns one, so a query is a pipeline you read from the innermost name outwards. Every operator also has a symbol from relational algebra (σ for SELECT, π for PROJECT), and the examples here can be switched between the two.

The tables

The examples use two small tables. An empty cell is NULL.

Customers

customer_idnamecity
10AcmeLeeds
11GlobexYork
12Initech
13UmbrellaLeeds

Orders

order_idcustomer_idstatusamount
110OPEN120
210SHIPPED80
311OPEN250
411HELD40
512SHIPPED95
610OPEN60

Filtering rows

WHERE is SELECT. The condition comes first, then the relation it filters.

SQL

SELECT * FROM orders WHERE status = 'OPEN' AND amount > 100

Relix

SELECT status = 'OPEN' AND amount > 100 (Orders)

A list of values and a null test read the way you would expect.

SQL

SELECT * FROM orders WHERE status IN ('OPEN', 'HELD')

Relix

SELECT status IN {"OPEN", "HELD"} (Orders)

SQL

SELECT * FROM customers WHERE city IS NULL

Relix

SELECT city IS NULL (Customers)

Choosing and computing columns

The column list is PROJECT. A computed column takes its name from ->, where SQL uses AS.

SQL

SELECT order_id, amount * 1.2 AS gross FROM orders

Relix

PROJECT order_id, amount * 1.2 -> gross (Orders)

CASE is the IIf function, and COALESCE is Coalesce.

SQL

SELECT order_id, CASE WHEN amount >= 100 THEN 'large' ELSE 'small' END AS size FROM orders

Relix

PROJECT order_id, IIf(amount >= 100, "large", "small") -> size (Orders)

SQL

SELECT name, COALESCE(city, 'unknown') AS city FROM customers

Relix

PROJECT name, Coalesce(city, "unknown") -> city (Customers)

Like SQL, a projection keeps duplicate rows. DISTINCT removes them.

SQL

SELECT DISTINCT status FROM orders

Relix

DISTINCT (PROJECT status (Orders))

Sorting and limiting

ORDER BY is SORT, and LIMIT wraps the sorted relation.

SQL

SELECT * FROM orders ORDER BY amount DESC LIMIT 3

Relix

LIMIT 3 (SORT amount DESC (Orders))

Grouping

GROUP BY and the aggregates go in one operator: the grouping columns, then the aggregates. HAVING is an ordinary SELECT over the grouped result.

SQL

SELECT customer_id, SUM(amount) AS total, COUNT(*) AS orders
FROM orders
GROUP BY customer_id

Relix

GROUP customer_id, SUM(amount) -> total, COUNT(*) -> orders (Orders)

SQL

SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 150

Relix

SELECT total > 150 (GROUP customer_id, SUM(amount) -> total (Orders))

With no grouping columns, the aggregate is over the whole table.

SQL

SELECT COUNT(*) AS n, AVG(amount) AS average FROM orders

Relix

GROUP COUNT(*) -> n, AVG(amount) -> average (Orders)

Joining

JOIN on its own is a natural join: it matches on the columns the two tables share, which is what USING does in SQL, and keeps one copy of each.

SQL

SELECT order_id, name, amount FROM orders JOIN customers USING (customer_id)

Relix

PROJECT order_id, name, amount (Orders JOIN Customers)

A join on a condition is written ><, with the condition between the two relations. Other comparisons work as well as equality.

SQL

SELECT o.order_id, c.name
FROM orders o JOIN customers c ON o.customer_id = c.customer_id AND o.amount > 90

Relix

PROJECT order_id, name (
    Orders >< Orders.customer_id = Customers.customer_id AND Orders.amount > 90 Customers
)

A left join is |><: every customer, with the orders they have, and NULL where they have none.

SQL

SELECT c.name, o.order_id
FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id

Relix

PROJECT name, order_id (
    Customers |>< Customers.customer_id = Orders.customer_id Orders
)

Existence

EXISTS and NOT EXISTS are joins in their own right. SEMI keeps the rows that have a match, and ANTI keeps the rows that have none. Neither repeats a row for each match, and ANTI has no NOT IN null trap.

SQL

SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)

Relix

Customers SEMI Customers.customer_id = Orders.customer_id Orders

SQL

SELECT * FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)

Relix

Customers ANTI Customers.customer_id = Orders.customer_id Orders

Combining results

UNION, EXCEPT and INTERSECT work on whole relations, and like SQL's they remove duplicates. UALL is UNION ALL.

SQL

SELECT customer_id FROM customers
EXCEPT
SELECT customer_id FROM orders

Relix

PROJECT customer_id (Customers) EXCEPT PROJECT customer_id (Orders)

SQL

SELECT customer_id FROM customers WHERE city = 'Leeds'
INTERSECT
SELECT customer_id FROM orders WHERE status = 'OPEN'

Relix

PROJECT customer_id (SELECT city = 'Leeds' (Customers))
    INTERSECT PROJECT customer_id (SELECT status = 'OPEN' (Orders))

Window functions

The two window functions people write most have operators of their own. The top row per group is TOP … PER, which returns whole rows:

SQL

SELECT order_id, customer_id, status, amount
FROM (
    SELECT o.*, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
    FROM orders o
) ranked
WHERE rn = 1

Relix

TOP 1 amount DESC PER customer_id (Orders)

A running total is ROLLING:

SQL

SELECT order_id, customer_id, status, amount,
       SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_id) AS running
FROM orders

Relix

ROLLING SUM(amount) OVER ALL ROWS SORT order_id ASC PER customer_id AS running (Orders)

Where to go from here

The language reference has a page for each of these operators, and Beyond SQL covers the questions that have no SQL counterpart at all: routes through a graph, choices under a budget, lineage, sessions and time-aligned joins.