Set Operations

Set operations combine the results of two or more SELECT queries. Stoolap supports UNION, UNION ALL, INTERSECT, and EXCEPT.

UNION

Combines results from two queries, removing duplicate rows:

SELECT name FROM employees
UNION
SELECT name FROM contractors;

UNION ALL

Keeps all rows including duplicates (faster since no deduplication is needed):

SELECT name FROM employees
UNION ALL
SELECT name FROM contractors;

INTERSECT

Returns only rows that appear in both result sets:

SELECT name FROM current_employees
INTERSECT
SELECT name FROM bonus_recipients;
-- Returns employees who received a bonus

INTERSECT ALL

Keeps duplicate rows in the intersection (a duplicate appears as many times as it exists in both inputs):

SELECT name FROM current_employees
INTERSECT ALL
SELECT name FROM bonus_recipients;

EXCEPT

Returns rows from the first query that do not appear in the second:

SELECT name FROM all_employees
EXCEPT
SELECT name FROM terminated_employees;
-- Returns only active employees

Order matters: A EXCEPT B is different from B EXCEPT A.

EXCEPT ALL

Keeps duplicates: each matching row from the second query removes one occurrence from the first:

SELECT name FROM all_employees
EXCEPT ALL
SELECT name FROM terminated_employees;

Chaining Multiple Operations

Set operations can be chained:

SELECT id FROM table1
UNION
SELECT id FROM table2
UNION
SELECT id FROM table3
UNION
SELECT id FROM table4;

INTERSECT binds tighter than UNION and EXCEPT, which bind equally and apply from left to right. Parentheses through a derived table group a chain any other way:

SELECT 2 AS x UNION SELECT 1 EXCEPT SELECT 2;
-- (2, 1) EXCEPT (2): 1

SELECT 1 AS x EXCEPT SELECT 1 UNION SELECT 3 INTERSECT SELECT 3;
-- ((1) EXCEPT (1)) UNION ((3) INTERSECT (3)): 3

ORDER BY and LIMIT

ORDER BY and LIMIT apply to the entire combined result set:

SELECT name, salary FROM employees
UNION
SELECT name, salary FROM contractors
ORDER BY salary DESC
LIMIT 10;

An ORDER BY key reads the combined result’s columns by the names the first query gives them, or by position. A name that two of those columns carry, compared without regard to case, is an error, column reference 'a' is ambiguous, and so is a name two columns of a star over a join end in, such as l.a and r.a; sort by position instead. A key written as an aliased item or an aggregate of the first query reads that item’s column, whatever names the columns carry:

SELECT a, b AS a FROM t UNION SELECT x, y FROM u ORDER BY a;  -- error: ambiguous
SELECT a, b AS a FROM t UNION SELECT x, y FROM u ORDER BY 2;  -- sorts by the second column
SELECT a + 1 AS z, b AS a FROM t UNION SELECT x, y FROM u ORDER BY a + 1;  -- sorts by z

A key may hold a subquery, which reads those columns by name too, as in ORDER BY (SELECT COUNT(*) FROM v WHERE v.k <= salary).

With WHERE Clauses

Each SELECT in a set operation can have its own WHERE clause:

SELECT val FROM sales WHERE val > 1000
UNION
SELECT val FROM returns WHERE val < 500;

Column Requirements

All queries in a set operation must have the same number of columns. Column names are taken from the first query:

-- Both queries must return 2 columns
SELECT id, name FROM employees
UNION
SELECT id, name FROM contractors;

Duplicate Handling Summary

Operation Duplicates
UNION Removed
UNION ALL Kept
INTERSECT Removed
INTERSECT ALL Kept (min count from both sides)
EXCEPT Removed
EXCEPT ALL Kept (subtracts one per match)

SELECT DISTINCT belongs to its own SELECT: in a set operation it removes duplicates from the first SELECT’s rows before they are combined, and a later ALL keeps the duplicates the other side brings:

SELECT DISTINCT 1 AS x UNION ALL SELECT 1;
-- 1
-- 1