Applies to VAST Cluster 5.5.
Functions › Table functions › Series of numbers and timestamps. In FROM, generate_series returns rows: integers, or timestamps, from start to stop included, step apart. range is the same but stops before stop. In the SELECT list, generate_series returns the same values as one list instead: see generate_series (list function).
Syntax
generate_series(stop)
generate_series(start, stop)
generate_series(start, stop, step)
generate_series(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 last value. It's returned if a step lands on it. |
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 and the last timestamp. |
interval |
INTERVAL |
The distance between timestamps. Required for timestamps. |
Returns: one column named generate_series, of type BIGINT for integers or TIMESTAMP for timestamps. Name it with an alias: FROM generate_series(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_generate_series. 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 time slots, which an example fills from generate_series:
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_generate_series"'
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}.slots"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, at TIMESTAMP)")
[]
[]
What it does
generate_series counts from start (0 if you leave it out) by step (1 if you leave it out), up to and including stop when a step lands on it. Each value is one row:
| Call | Rows |
|---|---|
generate_series(3) |
0, 1, 2, 3 |
generate_series(2, 5) |
2, 3, 4, 5 |
generate_series(0, 10, 3) |
0, 3, 6, 9 (10 isn't a step from 0) |
generate_series(10, 0, -3) |
10, 7, 4, 1 |
Integer arguments of any size give BIGINT rows. To stop before stop, use range.
Timestamps. With two timestamps and an INTERVAL, generate_series returns the timestamps from start_ts, interval apart, up to and including 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.
NULLs and edge cases
| Input | Result |
|---|---|
| Any argument NULL | No rows |
generate_series(0) |
One row, 0 |
generate_series(2, 2) |
One row, 2 |
generate_series(-2), generate_series(5, 2) |
Error: start is bigger than end, but increment is positive: cannot generate infinite series |
generate_series(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 generate_series(1, 3, 0.5) or generate_series('3') |
Error: No function matches the given name and argument types |
generate_series(9223372036854775807, 9223372036854775807) |
One row, 9223372036854775807 |
The same cases, as the cluster returns them. They use the helpers from Set up the examples:
run("SELECT * FROM generate_series(1, NULL)")
run("SELECT * FROM generate_series(0)")
run("SELECT * FROM generate_series(2, 2)")
run("SELECT * FROM generate_series(-2)")
run("SELECT * FROM generate_series(5, 2)")
run("SELECT * FROM generate_series(0, 10, -1)")
run("SELECT * FROM generate_series(0, 10, 0)")
run("SELECT * FROM generate_series(1, 3, 0.5)")
run("SELECT * FROM generate_series('3')")
run("SELECT * FROM generate_series(9223372036854775807, 9223372036854775807)")
[]
[(0,)]
[(2,)]
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 'generate_series(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 'generate_series(VARCHAR)'. You might need to add explicit type casts.
[(9223372036854775807,)]
Examples
Name the column with an alias, and compute other columns from it:
run("SELECT x, x * x, 'id-' || x FROM generate_series(1, 4) t(x)")
[(1, 1, 'id-1'), (2, 4, 'id-2'), (3, 9, 'id-3'), (4, 16, 'id-4')]
Every 20 minutes of an hour, both ends included, and every day of a short period:
run("SELECT * FROM generate_series(TIMESTAMP '2026-10-01 00:00', TIMESTAMP '2026-10-01 01:00', INTERVAL 20 MINUTE)")
run("SELECT * FROM generate_series(DATE '2026-10-01', DATE '2026-10-04', INTERVAL 1 DAY)")
[(datetime.datetime(2026, 10, 1, 0, 0),), (datetime.datetime(2026, 10, 1, 0, 20),), (datetime.datetime(2026, 10, 1, 0, 40),), (datetime.datetime(2026, 10, 1, 1, 0),)]
[(datetime.datetime(2026, 10, 1, 0, 0),), (datetime.datetime(2026, 10, 2, 0, 0),), (datetime.datetime(2026, 10, 3, 0, 0),), (datetime.datetime(2026, 10, 4, 0, 0),)]
Fill a table with one slot per hour of a day, both midnights included (25 rows), numbering them with row_number(). INSERT ... SELECT creates the rows on the cluster:
run(f"INSERT INTO {TABLE} SELECT row_number() OVER (), ts "
"FROM generate_series(TIMESTAMP '2026-10-01', TIMESTAMP '2026-10-02', INTERVAL 1 HOUR) t(ts)")
run(f"SELECT count(*), min(id), max(id), min(at), max(at) FROM {TABLE}")
[(25,)]
[(25, 1, 25, datetime.datetime(2026, 10, 1, 0, 0), datetime.datetime(2026, 10, 2, 0, 0))]
An aggregate over a series, which has 100,001 rows because stop is included, and the first rows of a series too large to list:
run("SELECT count(*), min(x), max(x) FROM generate_series(100000) t(x)")
run("SELECT * FROM generate_series(1000000000) t(x) LIMIT 3")
[(100001, 0, 100000)]
[(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); generate_series replaces that scan. It produces its rows in the Query Engine, and the plan shows a GENERATE_SERIES step where VAST_SCAN would be. Nothing is read from storage, so a WHERE condition on the rows runs in a FILTER step above it:
plan("SELECT x FROM generate_series(10) t(x) WHERE x > 5")
┌───────────────────────────┐
│ FILTER │
│ ──────────────────── │
│ (x > 5) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ GENERATE_SERIES │
│ ──────────────────── │
│ Function: │
│ GENERATE_SERIES │
│ │
└───────────────────────────┘
Make it faster
- Put the bounds in the arguments, not in
WHERE.generate_series(5, 10)generates six rows;generate_series(10)withWHERE x >= 5generates eleven, and theFILTERstep drops five of them. - Generate test data on the cluster. One
INSERT INTO t SELECT ... FROM generate_series(...)statement creates the rows in the Query Engine instead of sending them from your client. - Use the step for regular samples.
generate_series(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.
generate_seriescan'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., andgenerate_series(col)with a table's column withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.UNION ALLof two series works. - No
GROUP BY. It fails withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.An aggregate over the whole series works. - No time-zone timestamps.
TIMESTAMP WITH TIME ZONEbounds fail withNo function matches the given name and argument types 'generate_series(TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH TIME ZONE, INTERVAL)'. UseTIMESTAMPvalues.TIMEvalues aren't accepted either. - Timestamps need an interval.
generate_series(ts1, ts2)fails; add the step, such asINTERVAL 1 DAY.
run("SELECT x FROM generate_series(5) t(x) ORDER BY x DESC")
run("SELECT x FROM generate_series(5) t(x) ORDER BY x DESC LIMIT 3")
run(f"SELECT id, x FROM {TABLE} JOIN generate_series(3) r(x) ON id = x")
run("SELECT x % 3 AS k, count(*) FROM generate_series(10) t(x) GROUP BY k ORDER BY k LIMIT 20")
run("SELECT * FROM generate_series(TIMESTAMPTZ '2026-10-09 12:34:56+00', TIMESTAMPTZ '2026-10-11 12:34:56+00', INTERVAL 1 DAY)")
run("SELECT * FROM generate_series(TIMESTAMP '2026-10-09 12:34:56', TIMESTAMP '2026-10-11 12:34:56', INTERVAL 1 DAY)")
run("SELECT * FROM generate_series(TIMESTAMP '2026-10-01', TIMESTAMP '2026-10-04')")
InternalError: The operator 'ORDER_BY' type=1 is currently not supported for this usage.
[(5,), (4,), (3,)]
InternalError: The operator 'HASH_JOIN' type=32 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 'generate_series(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),), (datetime.datetime(2026, 10, 11, 12, 34, 56),)]
InternalError: Binder Error: No function matches the given name and argument types 'generate_series(TIMESTAMP, TIMESTAMP)'. You might need to add explicit type casts.
Similar functions
| Function | Includes stop |
(1, 4) |
(0) |
|---|---|---|---|
generate_series in FROM |
yes | rows 1, 2, 3, 4 |
one row, 0 |
range in FROM |
no | rows 1, 2, 3 |
no rows |
generate_series (list function), in the SELECT list |
yes | the list [1, 2, 3, 4] |
[0] |
run("SELECT * FROM generate_series(1, 4)")
run("SELECT * FROM range(1, 4)")
run("SELECT generate_series(1, 4), generate_series(0)")
[(1,), (2,), (3,), (4,)]
[(1,), (2,), (3,)]
[([1, 2, 3, 4], [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.
- range, unnest and generate_series (list function).
- SELECT and INSERT.
- EXPLAIN: how to read a plan.