docs/sql-reference/statements/select.mdx
Retrieves rows from one or more tables. SELECT is the primary way to read data in Turso and supports filtering, joining, aggregation, sorting, subqueries, and set operations.
[WITH cte_name AS (select_statement) [, ...]]
SELECT [DISTINCT] result_column [, ...]
[FROM table_or_subquery [join_clause ...]]
[WHERE expression]
[GROUP BY expression [, ...]]
[HAVING expression]
[WINDOW window_name AS (window_definition) [, ...]]
[compound_operator select_statement]
[ORDER BY ordering_term [, ...]]
[LIMIT expression [OFFSET expression]]
| Parameter | Type | Description |
|---|---|---|
result_column | expression, *, or table.* | Columns or expressions to return. Use * for all columns |
table_or_subquery | identifier or subquery | Table name, aliased table, or parenthesized SELECT |
expression | expression | Any valid SQL expression |
ordering_term | expression + direction | Expression followed by optional ASC/DESC and NULLS FIRST/LAST |
compound_operator | keyword | UNION, UNION ALL, INTERSECT, or EXCEPT |
cte_name | identifier | Name for a Common Table Expression |
window_name | identifier | Name for a reusable window definition |
-- All columns
SELECT * FROM employees;
-- Specific columns
SELECT name, department FROM employees;
-- Expressions and aliases
SELECT name, salary * 12 AS annual_salary FROM employees;
Use AS to assign aliases to columns or tables. The AS keyword is optional for column aliases.
SELECT e.name, e.salary * 12 annual_pay
FROM employees AS e;
The FROM clause specifies the source tables for the query. It accepts table names, aliased tables, subqueries, and join expressions.
-- Single table
SELECT * FROM employees;
-- Subquery as table source
SELECT * FROM (SELECT name, salary FROM employees WHERE salary > 50000) AS high_earners;
Filters rows based on a condition. Only rows where the expression evaluates to true are included in the result.
SELECT * FROM employees WHERE department = 'Engineering';
SELECT * FROM orders WHERE total > 100 AND status != 'cancelled';
| Operator | Description |
|---|---|
= | Equal |
!= or <> | Not equal |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
IS | Equal (NULL-safe) |
IS NOT | Not equal (NULL-safe) |
IS DISTINCT FROM | Not identical (NULL-safe, SQL standard) |
IS NOT DISTINCT FROM | Identical (NULL-safe, SQL standard) |
Combine conditions with AND, OR, and NOT.
SELECT * FROM products
WHERE (category = 'Electronics' OR category = 'Books')
AND price < 50
AND NOT discontinued;
-- LIKE: case-insensitive pattern matching (% = any chars, _ = one char)
SELECT * FROM employees WHERE name LIKE 'J%';
-- GLOB: case-sensitive pattern matching (* = any chars, ? = one char)
SELECT * FROM files WHERE path GLOB '*.txt';
-- REGEXP: regular expression matching
SELECT * FROM logs WHERE message REGEXP '^ERROR:';
-- BETWEEN (inclusive on both ends)
SELECT * FROM orders WHERE total BETWEEN 100 AND 500;
-- IN with a list
SELECT * FROM employees WHERE department IN ('Engineering', 'Design', 'Product');
-- IN with a subquery
SELECT * FROM employees WHERE department_id IN (SELECT id FROM departments WHERE active = 1);
SELECT * FROM employees WHERE manager_id IS NULL;
SELECT * FROM employees WHERE phone IS NOT NULL;
SELECT name,
CASE
WHEN salary >= 100000 THEN 'Senior'
WHEN salary >= 60000 THEN 'Mid'
ELSE 'Junior'
END AS level
FROM employees;
-- Simple CASE form
SELECT name,
CASE department
WHEN 'Engineering' THEN 'Eng'
WHEN 'Marketing' THEN 'Mkt'
ELSE 'Other'
END AS dept_code
FROM employees;
Combines rows from two or more tables based on a related column.
| Join Type | Description |
|---|---|
INNER JOIN or JOIN | Returns rows that have matching values in both tables |
LEFT JOIN or LEFT OUTER JOIN | Returns all rows from the left table, with NULLs for unmatched right-side columns |
FULL OUTER JOIN | Returns all rows from both tables, with NULLs where there is no match on either side |
NATURAL JOIN | Joins on all columns with the same name in both tables |
JOIN ... USING (column) | Joins on the specified column that exists in both tables |
Returns only rows where the join condition is satisfied in both tables.
SELECT e.name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;
Returns all rows from the left table. When no matching row exists in the right table, the right-side columns contain NULL.
SELECT e.name, m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Returns all rows from both tables. When a row in either table has no match in the other table, the missing side's columns contain NULL.
SELECT e.name, d.department_name
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id;
-- Returns all employees (even without a department)
-- and all departments (even without employees)
Automatically joins on all columns with identical names in both tables. Equivalent to JOIN ... USING with every shared column name.
SELECT * FROM orders NATURAL JOIN customers;
-- Joins on every column name shared between orders and customers
Joins on the specified column that must exist in both tables. The shared column appears only once in the result.
SELECT * FROM orders JOIN customers USING (customer_id);
SELECT o.id, c.name, p.product_name, oi.quantity
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.order_date > '2024-01-01';
Groups rows that share the same values in the specified columns. Typically used with aggregate functions.
SELECT department, COUNT(*) AS employee_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
Aggregate functions that can be used with GROUP BY:
| Function | Description |
|---|---|
COUNT(*) | Number of rows in the group |
COUNT(expression) | Number of non-NULL values |
SUM(expression) | Sum of non-NULL values |
AVG(expression) | Average of non-NULL values |
MIN(expression) | Minimum value |
MAX(expression) | Maximum value |
TOTAL(expression) | Sum as a REAL (returns 0.0 instead of NULL for empty sets) |
GROUP_CONCAT(expression, separator) | Concatenation of values |
STRING_AGG(expression, separator) | Alias for GROUP_CONCAT |
Filters groups after aggregation. WHERE filters individual rows before grouping; HAVING filters groups after.
SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department
HAVING COUNT(*) >= 5;
SELECT
CASE WHEN salary >= 80000 THEN 'High' ELSE 'Standard' END AS band,
COUNT(*) AS count
FROM employees
GROUP BY CASE WHEN salary >= 80000 THEN 'High' ELSE 'Standard' END;
Removes duplicate rows from the result set.
SELECT DISTINCT department FROM employees;
SELECT DISTINCT department, title FROM employees;
Sorts the result set. Without ORDER BY, the row order is unspecified.
SELECT * FROM employees ORDER BY salary DESC;
-- Multiple sort keys
SELECT * FROM employees ORDER BY department ASC, salary DESC;
-- Ordering by column position
SELECT name, salary FROM employees ORDER BY 2 DESC;
-- Ordering by alias
SELECT name, salary * 12 AS annual FROM employees ORDER BY annual DESC;
Controls where NULL values appear in the sorted result.
-- NULLs at the end (default for ASC is NULLS LAST, for DESC is NULLS FIRST)
SELECT * FROM employees ORDER BY manager_id ASC NULLS LAST;
-- NULLs at the beginning
SELECT * FROM employees ORDER BY manager_id DESC NULLS FIRST;
| Direction | Default NULL Placement |
|---|---|
| ASC | NULLS LAST |
| DESC | NULLS FIRST |
Restricts the number of rows returned and optionally skips a number of rows.
-- Return at most 10 rows
SELECT * FROM employees ORDER BY salary DESC LIMIT 10;
-- Skip 20 rows, then return 10
SELECT * FROM employees ORDER BY salary DESC LIMIT 10 OFFSET 20;
A subquery is a SELECT statement nested inside another query.
Returns a single value. Can be used anywhere an expression is expected.
SELECT name, salary,
salary - (SELECT AVG(salary) FROM employees) AS diff_from_avg
FROM employees;
Tests whether a value matches any row returned by the subquery.
SELECT * FROM products
WHERE category_id IN (SELECT id FROM categories WHERE active = 1);
SELECT * FROM products
WHERE category_id NOT IN (SELECT id FROM categories WHERE discontinued = 1);
Tests whether the subquery returns at least one row. The actual values are ignored.
SELECT * FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total > 1000
);
A subquery in the FROM clause acts as a derived table and must have an alias.
SELECT dept, avg_salary
FROM (
SELECT department AS dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_stats
WHERE avg_salary > 70000;
A WITH clause defines one or more temporary named result sets that exist for the duration of the query.
WITH regional_sales AS (
SELECT region, SUM(amount) AS total_sales
FROM orders
GROUP BY region
)
SELECT region, total_sales
FROM regional_sales
WHERE total_sales > 100000
ORDER BY total_sales DESC;
WITH
active_customers AS (
SELECT * FROM customers WHERE active = 1
),
recent_orders AS (
SELECT * FROM orders WHERE order_date > '2024-01-01'
)
SELECT ac.name, COUNT(ro.id) AS order_count
FROM active_customers ac
JOIN recent_orders ro ON ac.id = ro.customer_id
GROUP BY ac.name;
A window function performs a calculation across a set of rows related to the current row, without collapsing them into a single output row.
SELECT name, department, salary,
SUM(salary) OVER (PARTITION BY department ORDER BY salary) AS running_total
FROM employees;
Use the WINDOW clause to define a reusable window definition.
SELECT name, department, salary,
SUM(salary) OVER w AS dept_running_total,
COUNT(*) OVER w AS dept_running_count
FROM employees
WINDOW w AS (PARTITION BY department ORDER BY salary)
ORDER BY department, salary;
function_name() OVER (
[PARTITION BY expression [, ...]]
[ORDER BY expression [ASC | DESC] [, ...]]
)
| Component | Description |
|---|---|
PARTITION BY | Divides rows into groups (partitions). The function resets for each partition |
ORDER BY | Defines the order of rows within each partition |
Standard aggregate functions (SUM, AVG, COUNT, MIN, MAX, TOTAL, GROUP_CONCAT) can all be used as window functions.
Combine the results of two or more SELECT statements.
| Operator | Description |
|---|---|
UNION | Combines results and removes duplicates |
UNION ALL | Combines results and keeps duplicates |
INTERSECT | Returns only rows present in both result sets |
EXCEPT | Returns rows from the first result set that are not in the second |
All set operations require the same number of columns in each SELECT, with compatible types.
-- Customers who are also employees
SELECT name FROM customers
INTERSECT
SELECT name FROM employees;
-- All people from both tables, no duplicates
SELECT name, email FROM customers
UNION
SELECT name, email FROM employees;
-- Customers who are not employees
SELECT name FROM customers
EXCEPT
SELECT name FROM employees;
When duplicates are acceptable, UNION ALL is faster because it skips the deduplication step.
SELECT id, 'order' AS source FROM orders
UNION ALL
SELECT id, 'return' AS source FROM returns;
ORDER BY applies to the entire combined result set and must appear after the last SELECT.
SELECT name FROM customers
UNION
SELECT name FROM employees
ORDER BY name;
SELECT
d.department_name,
COUNT(e.id) AS headcount,
ROUND(AVG(e.salary), 2) AS avg_salary,
MIN(e.hire_date) AS earliest_hire
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
GROUP BY d.department_name
HAVING COUNT(e.id) > 0
ORDER BY avg_salary DESC
LIMIT 10 OFFSET 0;
WITH high_value_orders AS (
SELECT customer_id, COUNT(*) AS order_count, SUM(total) AS total_spent
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
HAVING SUM(total) > 5000
)
SELECT c.name, h.order_count, h.total_spent
FROM customers c
JOIN high_value_orders h ON c.id = h.customer_id
ORDER BY h.total_spent DESC;
SELECT p.product_name, p.price, c.category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
)
ORDER BY p.price DESC
LIMIT 20;