Skip to main content

SELECT

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.

Syntax

[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]]

Parameters

ParameterTypeDescription
result_columnexpression, *, or table.*Columns or expressions to return. Use * for all columns
table_or_subqueryidentifier or subqueryTable name, aliased table, or parenthesized SELECT
expressionexpressionAny valid SQL expression
ordering_termexpression + directionExpression followed by optional ASC/DESC and NULLS FIRST/LAST
compound_operatorkeywordUNION, UNION ALL, INTERSECT, or EXCEPT
cte_nameidentifierName for a Common Table Expression
window_nameidentifierName for a reusable window definition

Basic Queries

Selecting Columns

-- All columns
SELECT * FROM employees;

-- Specific columns
SELECT name, department FROM employees;

-- Expressions and aliases
SELECT name, salary * 12 AS annual_salary FROM employees;

Column and Table Aliases

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;

FROM Clause

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;

WHERE Clause

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';

Comparison Operators

OperatorDescription
=Equal
!= or <>Not equal
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal
ISEqual (NULL-safe)
IS NOTNot equal (NULL-safe)
IS DISTINCT FROMNot identical (NULL-safe, SQL standard)
IS NOT DISTINCT FROMIdentical (NULL-safe, SQL standard)

Logical Operators

Combine conditions with AND, OR, and NOT.
SELECT * FROM products
WHERE (category = 'Electronics' OR category = 'Books')
  AND price < 50
  AND NOT discontinued;

Pattern Matching

-- 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:';

Range and Membership Tests

-- 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);

NULL Tests

SELECT * FROM employees WHERE manager_id IS NULL;

SELECT * FROM employees WHERE phone IS NOT NULL;

CASE Expressions

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;

JOIN Clause

Combines rows from two or more tables based on a related column.

Supported Join Types

Join TypeDescription
INNER JOIN or JOINReturns rows that have matching values in both tables
LEFT JOIN or LEFT OUTER JOINReturns all rows from the left table, with NULLs for unmatched right-side columns
FULL OUTER JOINReturns all rows from both tables, with NULLs where there is no match on either side
NATURAL JOINJoins on all columns with the same name in both tables
JOIN ... USING (column)Joins on the specified column that exists in both tables

INNER JOIN

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;

LEFT OUTER JOIN

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;

FULL OUTER JOIN

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)

NATURAL JOIN

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

JOIN … USING

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);

Multi-Table Joins

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';

GROUP BY and HAVING

GROUP BY

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:
FunctionDescription
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

HAVING

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;

GROUP BY with Expressions

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;

DISTINCT

Removes duplicate rows from the result set.
SELECT DISTINCT department FROM employees;

SELECT DISTINCT department, title FROM employees;
DISTINCT applies to the entire result row, not to a single column. Two rows are considered duplicates only if every column value is identical.

ORDER BY

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;

NULLS FIRST / NULLS LAST

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;
DirectionDefault NULL Placement
ASCNULLS LAST
DESCNULLS FIRST

LIMIT and OFFSET

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;
Always use ORDER BY with LIMIT and OFFSET. Without ORDER BY, the set of rows skipped or returned is arbitrary.

Subqueries

A subquery is a SELECT statement nested inside another query.

Scalar Subqueries

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;

Subqueries with IN

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);

Subqueries with EXISTS

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
);

Subqueries in FROM

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;

Common Table Expressions (CTE)

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;

Multiple CTEs

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;
CTEs in Turso are SELECT-only. RECURSIVE CTEs and the MATERIALIZED/NOT MATERIALIZED hints are not supported.

Window Functions

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;

Named Windows

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;

Window Clause Syntax

function_name() OVER (
  [PARTITION BY expression [, ...]]
  [ORDER BY expression [ASC | DESC] [, ...]]
)
ComponentDescription
PARTITION BYDivides rows into groups (partitions). The function resets for each partition
ORDER BYDefines 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.
Window functions in Turso use the default frame definition (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW). Custom frame specifications (ROWS, RANGE, GROUPS with explicit bounds) are not supported. Built-in ranking functions (row_number, rank, dense_rank) are not yet available.

Set Operations

Combine the results of two or more SELECT statements.
OperatorDescription
UNIONCombines results and removes duplicates
UNION ALLCombines results and keeps duplicates
INTERSECTReturns only rows present in both result sets
EXCEPTReturns 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;

UNION ALL

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;

Set Operations with ORDER BY

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;

Examples

Paginated Report with Aggregation

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;

CTE with Filtered Join

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;

Subquery with EXISTS and LEFT JOIN

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;

See Also