SQL reference
R2 SQL is Cloudflare's serverless, distributed, analytics query engine for querying Apache Iceberg ↗ tables stored in R2 Data Catalog. This page documents the supported SQL syntax.
SELECT column_list | expression | aggregation_functionFROM namespace_name.table_name[WHERE conditions][GROUP BY column_list][HAVING conditions][ORDER BY expression [ASC | DESC]][LIMIT number]Lists all available namespaces.
SHOW DATABASES;Alias for SHOW DATABASES. Lists all available namespaces.
SHOW NAMESPACES;Lists all tables within a specific namespace.
SHOW TABLES IN namespace_name;Describes the structure of a table, showing column names and data types.
DESCRIBE namespace_name.table_name;SELECT column_specification [, column_specification, ...]- Column name:
column_name - All columns:
* - Qualified wildcard:
table_name.* - Column alias:
column_name AS alias - Expressions: arithmetic, function calls, CASE expressions, and casts
SELECT * FROM my_namespace.sales_data LIMIT 10SELECT customer_id, region, total_amount FROM my_namespace.sales_data LIMIT 10SELECT region, total_amount * 1.1 AS total_with_tax FROM my_namespace.sales_data LIMIT 10CTEs let you define named temporary result sets using WITH that you can reference in the main query. All CTEs must reference the same single table.
WITH cte_name AS ( SELECT ... FROM namespace_name.table_name [WHERE ...])SELECT ... FROM cte_nameA CTE can reference a previously defined CTE. All CTEs in the chain must derive from the same underlying table.
WITH filtered AS ( SELECT customer_id, department, total_amount FROM my_namespace.sales_data WHERE total_amount > 0),summary AS ( SELECT department, COUNT(*) AS order_count, round(AVG(total_amount), 2) AS avg_amount FROM filtered GROUP BY department)SELECT *FROM summaryWHERE order_count > 100ORDER BY avg_amount DESCSELECT * FROM namespace_name.table_nameR2 SQL queries reference exactly one table, specified as namespace_name.table_name.
SELECT * FROM namespace_name.table_name WHERE condition [AND | OR condition ...]=, !=, <>, <, >, <=, >=
column_name IS NULLcolumn_name IS NOT NULL
IS TRUE,IS FALSE,IS NOT TRUE,IS NOT FALSEIS UNKNOWN,IS NOT UNKNOWN
column_name BETWEEN value1 AND value2column_name NOT BETWEEN value1 AND value2
column_name IN ('value1', 'value2')column_name NOT IN ('value1', 'value2')
column_name LIKE 'pattern'column_name NOT LIKE 'pattern'column_name ILIKE 'pattern'(case-insensitive)column_name NOT ILIKE 'pattern'column_name SIMILAR TO 'regex_pattern'
ANDORNOT
SELECT * FROM my_namespace.sales_dataWHERE timestamp BETWEEN '2025-09-24T01:00:00Z' AND '2025-09-25T01:00:00Z'
SELECT * FROM my_namespace.sales_dataWHERE status = 200 AND response_time > 1000
SELECT * FROM my_namespace.sales_dataWHERE (region = 'North' OR region = 'South') AND total_amount IS NOT NULL
SELECT * FROM my_namespace.sales_dataWHERE department ILIKE '%eng%'SELECT column_list, aggregation_function(column)FROM namespace_name.table_name[WHERE conditions]GROUP BY column_listSELECT department, COUNT(*) AS dept_countFROM my_namespace.sales_dataGROUP BY department
SELECT department, category, SUM(total_amount) AS totalFROM my_namespace.sales_dataGROUP BY department, categorySELECT column_list, aggregation_function(column) AS aliasFROM namespace_name.table_nameGROUP BY column_listHAVING aggregation_function(column) comparison_operator valueSELECT department, COUNT(*) AS dept_countFROM my_namespace.sales_dataGROUP BY departmentHAVING COUNT(*) > 1000
SELECT region, SUM(total_amount) AS totalFROM my_namespace.sales_dataGROUP BY regionHAVING SUM(total_amount) > 1000000ORDER BY expression [ASC | DESC] [, expression [ASC | DESC], ...]- ASC: Ascending order (default)
- DESC: Descending order
- Multi-column ordering is supported
SELECT customer_id, total_amountFROM my_namespace.sales_dataWHERE total_amount IS NOT NULLORDER BY total_amount DESCLIMIT 50
SELECT department, COUNT(*) AS dept_countFROM my_namespace.sales_dataGROUP BY departmentORDER BY dept_count DESC, department ASCLIMIT number- Type: Integer only
- Default: 500
SELECT * FROM my_namespace.sales_data LIMIT 100Returns the execution plan for a query without running it.
EXPLAIN SELECT department, COUNT(*) AS dept_countFROM my_namespace.sales_dataWHERE total_amount IS NOT NULLGROUP BY department;Expressions can be used in SELECT, WHERE, GROUP BY, HAVING, and ORDER BY clauses.
SELECT 42 AS int_val, 3.14 AS float_val, 'hello' AS str_val, TRUE AS bool_val, NULL AS null_valFROM my_namespace.sales_data LIMIT 1+, -, *, /, %
SELECT customer_id, total_amount * 1.1 AS total_with_tax, total_amount % 10 AS remainderFROM my_namespace.sales_dataWHERE total_amount IS NOT NULLLIMIT 5SELECT customer_id || ' - ' || region AS labelFROM my_namespace.sales_dataLIMIT 5Searched form:
SELECT customer_id, CASE WHEN total_amount > 1000 THEN 'high' WHEN total_amount > 100 THEN 'medium' ELSE 'low' END AS tierFROM my_namespace.sales_dataLIMIT 10Simple form:
SELECT customer_id, CASE region WHEN 'North' THEN 'N' WHEN 'South' THEN 'S' ELSE 'Other' END AS region_codeFROM my_namespace.sales_dataLIMIT 10-- CASTSELECT CAST(total_amount AS INT) AS amount_int FROM my_namespace.sales_data LIMIT 5
-- TRY_CAST (returns NULL on failure instead of error)SELECT TRY_CAST(customer_id AS INT) AS id_int FROM my_namespace.sales_data LIMIT 5
-- Shorthand (::)SELECT total_amount::INT AS amount_int FROM my_namespace.sales_data LIMIT 5SELECT EXTRACT(YEAR FROM timestamp) AS yr, EXTRACT(MONTH FROM timestamp) AS mo, EXTRACT(DAY FROM timestamp) AS dyFROM my_namespace.sales_dataLIMIT 1| Type | Description | Example Values |
|---|---|---|
integer | Whole numbers | 1, 42, -10, 0 |
float | Decimal numbers | 1.5, 3.14, -2.7, 0.0 |
string | Text values | 'hello', 'GET', '2024-01-01' |
boolean | Boolean values | true, false |
timestamp | RFC3339 | '2025-09-24T01:00:00Z' |
date | Date values | '2025-09-24' |
struct | Named fields | struct_col['field_name'] |
array | Ordered list | array_col[1] (1-indexed) |
map | Key-value pairs | map_keys(map_col) |
- Comparison operators:
=,!=,<,<=,>,>=,LIKE,BETWEEN,IS NULL,IS NOT NULL - AND (higher precedence)
- OR (lower precedence)
Use parentheses to override default precedence:
SELECT * FROM my_namespace.sales_data WHERE (status = 404 OR status = 500) AND region = 'North'SELECT *FROM my_namespace.sales_dataWHERE timestamp BETWEEN '2025-09-24T01:00:00Z' AND '2025-09-25T01:00:00Z'LIMIT 100SELECT customer_id, timestamp, status, total_amountFROM my_namespace.sales_dataWHERE status >= 400 AND total_amount > 5000ORDER BY total_amount DESCLIMIT 50SELECT region, COUNT(*) AS region_count, AVG(total_amount) AS avg_amountFROM my_namespace.sales_dataWHERE status = 'completed'GROUP BY regionHAVING COUNT(*) > 1000ORDER BY avg_amount DESCLIMIT 20SELECT customer_id, CASE WHEN total_amount >= 1000 THEN 'Premium' WHEN total_amount >= 100 THEN 'Standard' ELSE 'Basic' END AS tier, total_amountFROM my_namespace.sales_dataWHERE total_amount IS NOT NULLORDER BY total_amount DESCLIMIT 20