Applies to VAST Cluster 5.5.
Functions › Table functions › Series of numbers and timestamps. In FROM, range returns rows: integers, or timestamps, from start up to but not including stop, step apart. generate_series is the same but includes stop. In the SELECT list, range returns the same values as one list instead: see range (list function).
Syntax
range(stop)
range(start, stop)
range(start, stop, step)
range(start_ts, stop_ts, interval)
| Argument | Type | Description |
|---|---|---|
start |
BIGINT (any integer) |
The first value. Leave it out to start at 0. |
stop |
BIGINT (any integer) |
The value to stop before. It isn't returned. |
step |
BIGINT (any integer) |
The distance between values; negative to count down. Default 1. |
start_ts, stop_ts |
TIMESTAMP (a DATE is converted) |
The first timestamp and the one to stop before. |
interval |
INTERVAL |
The distance between timestamps. Required for timestamps. |
Returns: one column named range, of type BIGINT for integers or TIMESTAMP for timestamps. Name it with an alias: FROM range(3) t(x).
Set up the examples
The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_table_functions_range. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.
This block connects, defines the run() and plan() helpers, and creates an empty table of sensor readings, which an example fills from range:
import os
import re
import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb
BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_table_functions_range"'
conn = dbapi.connect(
driver=adbc_driver_vastdb.get_driver_path(),
db_kwargs={
"vast.db.endpoint": os.environ["QE_ENDPOINT"],
"vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
"vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
},
autocommit=True,
)
def server_message(e):
"""The server's message in a driver error, without the request ID or the echoed statement."""
text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
found = (re.search(r'message: "(.*?)", request_id', text, re.S)
or re.search(r"description: (.*)\)", text, re.S))
message = found.group(1) if found else text.strip().splitlines()[-1]
message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
return message.replace(BUCKET, "<bucket>")
def run(sql):
"""Run one statement. Print the rows it returns, or the error."""
try:
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall() if cur.description else []
except (adbc_driver_manager.Error, OSError) as e:
print(f"{type(e).__name__}: {server_message(e)}")
else:
print(rows)
def plan(sql):
"""Print the plan without its ~N Rows lines: the estimated row count."""
with conn.cursor() as cur:
cur.execute("EXPLAIN " + sql)
steps = cur.fetchall()[0][1]
print("\n".join(line for line in steps.splitlines() if "Rows" not in line).replace(BUCKET, "<bucket>"))
TABLE = f"{SCHEMA}.readings"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, label VARCHAR, at TIMESTAMP)")
[]
[]
What it does
range counts from start (0 if you leave it out) by step (1 if you leave it out), and stops before it reaches stop. Each value is one row:
| Call | Rows |
|---|---|
range(3) |
0, 1, 2 |
range(2, 5) |
2, 3, 4 |
range(0, 10, 3) |
0, 3, 6, 9 |
range(10, 0, -3) |
10, 7, 4, 1 |
Integer arguments of any size give BIGINT rows. To include stop, use generate_series.
Timestamps. With two timestamps and an INTERVAL, range returns the timestamps from start_ts, interval apart, before stop_ts. A DATE is converted to a TIMESTAMP at midnight. A month step is added to the previous value: from January 31, INTERVAL 1 MONTH gives February 28, then March 28 and April 28. A step of INTERVAL 1 YEAR from February 29, 2024 gives February 28 in the following years.
NULLs and edge cases
| Input | Result |
|---|---|
| Any argument NULL | No rows |
range(0), range(2, 2) |
No rows |
range(-2), range(5, 2) |
Error: start is bigger than end, but increment is positive: cannot generate infinite series |
range(0, 10, -1) |
Error: start is smaller than end, but increment is negative: cannot generate infinite series |
A step of 0, or INTERVAL 0 DAY |
Error: interval cannot be 0! |
A decimal or a string, such as range(1, 3, 0.5) or range('3') |
Error: No function matches the given name and argument types |
range(9223372036854775806, 9223372036854775807) |
One row, 9223372036854775806 |
The same cases, as the cluster returns them. They use the helpers from Set up the examples:
run("SELECT * FROM range(1, NULL)")
run("SELECT * FROM range(0)")
run("SELECT * FROM range(2, 2)")
run("SELECT * FROM range(-2)")
run("SELECT * FROM range(5, 2)")
run("SELECT * FROM range(0, 10, -1)")
run("SELECT * FROM range(0, 10, 0)")
run("SELECT * FROM range(1, 3, 0.5)")
run("SELECT * FROM range('3')")
run("SELECT * FROM range(9223372036854775806, 9223372036854775807)")
[]
[]
[]
OSError: start is bigger than end, but increment is positive: cannot generate infinite series
OSError: start is bigger than end, but increment is positive: cannot generate infinite series
OSError: start is smaller than end, but increment is negative: cannot generate infinite series
OSError: interval cannot be 0!
InternalError: Binder Error: No function matches the given name and argument types 'range(INTEGER, INTEGER, DECIMAL(2,1))'. You might need to add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'range(VARCHAR)'. You might need to add explicit type casts.
[(9223372036854775806,)]
Examples
Name the column with an alias, and compute other columns from it:
run("SELECT x, x * x, 'id-' || x FROM range(1, 4) t(x)")
[(1, 1, 'id-1'), (2, 4, 'id-2'), (3, 9, 'id-3')]
Timestamps 20 minutes apart within an hour, and a monthly series from January 31:
run("SELECT * FROM range(TIMESTAMP '2026-10-01 00:00', TIMESTAMP '2026-10-01 01:00', INTERVAL 20 MINUTE)")
run("SELECT * FROM range(TIMESTAMP '2026-01-31', TIMESTAMP '2026-05-01', INTERVAL 1 MONTH)")
[(datetime.datetime(2026, 10, 1, 0, 0),), (datetime.datetime(2026, 10, 1, 0, 20),), (datetime.datetime(2026, 10, 1, 0, 40),)]
[(datetime.datetime(2026, 1, 31, 0, 0),), (datetime.datetime(2026, 2, 28, 0, 0),), (datetime.datetime(2026, 3, 28, 0, 0),), (datetime.datetime(2026, 4, 28, 0, 0),)]
Fill a table with 1,000 hourly readings in one statement, then check it. INSERT ... SELECT creates the rows on the cluster, so nothing is sent from your client:
run(f"INSERT INTO {TABLE} SELECT x, 'row-' || x, TIMESTAMP '2026-10-01' + x * INTERVAL 1 HOUR FROM range(1000) t(x)")
run(f"SELECT count(*), min(id), max(id), min(at), max(at) FROM {TABLE}")
[(1000,)]
[(1000, 0, 999, datetime.datetime(2026, 10, 1, 0, 0), datetime.datetime(2026, 11, 11, 15, 0))]
An aggregate over a series, and the first rows of a series too large to list:
run("SELECT count(*), min(x), max(x) FROM range(100000) t(x)")
run("SELECT * FROM range(1000000000) t(x) LIMIT 3")
[(100000, 0, 99999)]
[(0,), (1,), (2,)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes. A query on a table starts with a scan in VAST DataBase (VAST_SCAN); range replaces that scan. It produces its rows in the Query Engine, and the plan shows a RANGE step where VAST_SCAN would be. Nothing is read from storage, so a WHERE condition on the rows runs in a FILTER step above RANGE:
plan("SELECT x FROM range(10) t(x) WHERE x > 5")
┌───────────────────────────┐
│ FILTER │
│ ──────────────────── │
│ (x > 5) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ RANGE │
│ ──────────────────── │
│ Function: RANGE │
│ │
└───────────────────────────┘
A LIMIT is a step of its own above RANGE:
plan("SELECT * FROM range(1000000000) t(x) LIMIT 3")
┌───────────────────────────┐
│ LIMIT │
│ ──────────────────── │
│ Limit: 3 │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ RANGE │
│ ──────────────────── │
│ Function: RANGE │
│ │
└───────────────────────────┘
Make it faster
- Put the bounds in the arguments, not in
WHERE.range(5, 10)generates five rows;range(10)withWHERE x >= 5generates ten, and theFILTERstep drops five of them. - Generate test data on the cluster. One
INSERT INTO t SELECT ... FROM range(n)statement creates the rows in the Query Engine instead of sending them from your client. 100,000 rows in one statement worked. - Use the step for regular samples.
range(0, 1000, 10)gives every tenth value directly, instead ofWHERE x % 10 = 0on a longer series.
Limitations and errors
ORDER BYneeds aLIMIT. Without one it fails withThe operator 'ORDER_BY' type=1 is currently not supported for this usage.- One source per query.
rangecan't be joined with a table or with another table function: a join fails withThe operator 'HASH_JOIN' type=32 is currently not supported., two table functions inFROMwithThe operator 'CROSS_PRODUCT' type=33 is currently not supported., andrange(col)with a table's column (FROM t, range(t.n)) withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.UNION ALLof two series works, but the rows can come back in any order. - No
GROUP BY. It fails withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.An aggregate over the whole series, as in the examples, works. - No time-zone timestamps.
TIMESTAMP WITH TIME ZONEbounds fail withNo function matches the given name and argument types 'range(TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH TIME ZONE, INTERVAL)'. Cast them toTIMESTAMP.TIMEvalues aren't accepted either. - No
CREATE TABLE ... AS SELECT. It fails withNot implemented Error: not supported; create the table, thenINSERT ... SELECT.
run("SELECT x FROM range(5) t(x) ORDER BY x DESC")
run("SELECT x FROM range(5) t(x) ORDER BY x DESC LIMIT 3")
run(f"SELECT id, x FROM {TABLE} JOIN range(3) r(x) ON id = x")
run("SELECT * FROM range(2) a(x), range(2) b(y)")
run("SELECT x % 3 AS k, count(*) FROM range(10) t(x) GROUP BY k ORDER BY k LIMIT 20")
run("SELECT * FROM range(TIMESTAMPTZ '2026-10-09 12:34:56+00', TIMESTAMPTZ '2026-10-11 12:34:56+00', INTERVAL 1 DAY)")
run("SELECT * FROM range(CAST(TIMESTAMPTZ '2026-10-09 12:34:56+00' AS TIMESTAMP), TIMESTAMP '2026-10-11 12:34:56', INTERVAL 1 DAY)")
InternalError: The operator 'ORDER_BY' type=1 is currently not supported for this usage.
[(4,), (3,), (2,)]
InternalError: The operator 'HASH_JOIN' type=32 is currently not supported.
InternalError: The operator 'CROSS_PRODUCT' type=33 is currently not supported.
InternalError: The operator 'HASH_GROUP_BY' type=9 is currently not supported.
InternalError: Binder Error: No function matches the given name and argument types 'range(TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH TIME ZONE, INTERVAL)'. You might need to add explicit type casts.
[(datetime.datetime(2026, 10, 9, 12, 34, 56),), (datetime.datetime(2026, 10, 10, 12, 34, 56),)]
Similar functions
| Function | Includes stop |
(1, 4) |
(0) |
|---|---|---|---|
range in FROM |
no | rows 1, 2, 3 |
no rows |
generate_series in FROM |
yes | rows 1, 2, 3, 4 |
one row, 0 |
range (list function), in the SELECT list |
no | the list [1, 2, 3] |
[] |
repeat in FROM |
the same value in each row |
run("SELECT * FROM range(1, 4)")
run("SELECT * FROM generate_series(1, 4)")
run("SELECT range(1, 4), range(0)")
run("SELECT * FROM range(0)")
run("SELECT * FROM generate_series(0)")
[(1,), (2,), (3,)]
[(1,), (2,), (3,), (4,)]
[([1, 2, 3], [])]
[]
[(0,)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Table functions: all the table functions.
- generate_series, unnest and range (list function).
- SELECT and INSERT.
- EXPLAIN: how to read a plan.