Applies to VAST Cluster 5.5.
Functions › Date and time functions › Truncating and bucketing. Returns the start of the fixed-width bucket a date or timestamp falls in, for example the six-hour period or the seven-day week that contains it. An origin or an offset moves where the buckets start.
Syntax
time_bucket(width, value)
time_bucket(width, value, origin)
time_bucket(width, value, offset)
| Argument | Type | Description |
|---|---|---|
width |
INTERVAL |
The width of each bucket, greater than zero. Either months and years (INTERVAL 1 MONTH, INTERVAL 10 YEAR) or days and smaller units (INTERVAL 7 DAY, INTERVAL 15 MINUTE), not both. |
value |
DATE or TIMESTAMP (also TIMESTAMP_NS and TIMESTAMP_MS) |
The value to place in a bucket. |
origin |
DATE or TIMESTAMP |
Optional. A moment where a bucket starts; every bucket start is a whole number of widths from it. It can be before or after value. |
offset |
INTERVAL |
Optional. Moves every bucket start by this interval; it can be negative. |
Returns: the type of value: DATE for a DATE, TIMESTAMP for a TIMESTAMP or another timestamp type. A DATE with a TIMESTAMP origin returns a TIMESTAMP.
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_date_time_time_bucket. 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 a table of five events, each with a date (d) and a timestamp (ts). Row 4 has no values:
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_date_time_time_bucket"'
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 table's row count, which can still be 0 just after an insert."""
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))
TABLE = f"{SCHEMA}.events"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, d DATE, ts TIMESTAMP)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, DATE '2026-02-28', TIMESTAMP '2026-02-28 23:15:00'), "
"(2, DATE '2026-03-01', TIMESTAMP '2026-03-01 08:30:45.123456'), "
"(3, DATE '2024-12-30', TIMESTAMP '2024-12-30 12:00:00'), "
"(4, NULL, NULL), "
"(5, DATE '2024-02-29', TIMESTAMP '2024-02-29 00:00:00')")
[]
[]
[(5,)]
What it does
time_bucket divides the time line into buckets of width and returns the start of the one that contains value. It always rounds down, also before 1970: TIMESTAMP '1969-12-31 23:59:59.999999' in one-day buckets is 1969-12-31 00:00:00.
Without an origin or an offset, the buckets are aligned like this:
| Width | Bucket starts | Example | Result |
|---|---|---|---|
| Hours, minutes, seconds | Multiples of the width from midnight | INTERVAL 15 MINUTE on 08:37:12; INTERVAL 6 HOUR on 2026-02-28 23:15 |
08:30:00; 2026-02-28 18:00 |
INTERVAL 1 DAY |
Midnight | TIMESTAMP '1969-12-31 23:59:59.999999' |
1969-12-31 00:00:00 |
INTERVAL 7 DAY, INTERVAL 1 WEEK |
Monday | DATE '2026-03-04' |
2026-03-02 (a Monday) |
| Months | The first of a month; three-month buckets start in January, April, July and October | INTERVAL 3 MONTH on 2026-05-15 |
2026-04-01 |
| Years | 1 January; ten-year buckets start in a year that ends in 0 | INTERVAL 10 YEAR on 2026-03-15 |
2020-01-01 |
Origin. With an origin, every bucket starts a whole number of widths from it. One-day buckets with origin TIMESTAMP '2026-01-01 06:00:00' run from 06:00 to 06:00, and weekly buckets with a Sunday as origin start on Sundays. The origin can be after the value.
Offset. An offset moves every default bucket start: time_bucket(INTERVAL 1 DAY, ts, INTERVAL 6 HOUR) gives days that start at 06:00, and INTERVAL '-6 hours' days that start at 18:00 the evening before. With a month width, INTERVAL 9 DAY gives months that start on the 10th.
The width, the origin and the offset can come from columns, so each row can use a different bucket.
NULLs and edge cases
| Input | Result |
|---|---|
| A NULL value (row 4) | NULL |
| A NULL width, origin or offset | NULL |
An untyped NULL value: time_bucket(INTERVAL 6 HOUR, NULL) |
NULL |
INTERVAL 1 MICROSECOND |
The value unchanged |
A DATE with a width under a day: time_bucket(INTERVAL 1 HOUR, DATE '2026-03-01') |
2026-03-01, still a DATE |
A TIMESTAMP_NS value |
A TIMESTAMP: the nanoseconds are dropped |
DATE 'infinity' |
infinity |
The width as a string: time_bucket('1 day', ts) |
Works: the string is read as an interval |
| A zero or negative width | Error: Period must be greater than 0 |
A width with months and days: INTERVAL '1 month 1 day' |
Error: Month intervals cannot have day or time component |
A TIME or INTERVAL value |
Error: No function matches the given name and argument types 'time_bucket(INTERVAL, TIME)' |
A string value, such as 'abc' |
Error: Could not choose a best candidate function for the function call "time_bucket(INTERVAL, STRING_LITERAL)"; cast it to TIMESTAMP |
A number, boolean, BLOB or list |
Error: No function matches the given name and argument types, for example 'time_bucket(INTERVAL, INTEGER_LITERAL)' |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run(f"SELECT id, time_bucket(INTERVAL 6 HOUR, ts) FROM {TABLE} WHERE id = 4")
run("SELECT time_bucket(NULL, TIMESTAMP '2026-03-01 08:30:00'), "
"time_bucket(INTERVAL 1 DAY, TIMESTAMP '2026-03-01 08:30:00', NULL::TIMESTAMP), "
"time_bucket(INTERVAL 1 DAY, TIMESTAMP '2026-03-01 08:30:00', NULL::INTERVAL), time_bucket(INTERVAL 6 HOUR, NULL)")
run("SELECT time_bucket(INTERVAL 1 MICROSECOND, TIMESTAMP '2026-03-01 08:37:12.123456'), "
"time_bucket(INTERVAL 1 HOUR, DATE '2026-03-01'), "
"CAST(time_bucket(INTERVAL 1 SECOND, TIMESTAMP_NS '2026-03-01 08:30:00.123456789') AS VARCHAR), "
"CAST(time_bucket(INTERVAL 1 DAY, DATE 'infinity') AS VARCHAR), time_bucket('1 day', TIMESTAMP '2026-03-01 08:30:00')")
run("SELECT time_bucket(INTERVAL 0 DAY, DATE '2026-03-01')")
run("SELECT time_bucket(INTERVAL '1 month 1 day', DATE '2026-03-01')")
run("SELECT time_bucket(INTERVAL 1 HOUR, TIME '08:30:00')")
run("SELECT time_bucket(INTERVAL 6 HOUR, 'abc')")
run("SELECT time_bucket(INTERVAL 6 HOUR, 123)")
[(4, None)]
[(None, None, None, None)]
[(datetime.datetime(2026, 3, 1, 8, 37, 12, 123456), datetime.date(2026, 3, 1), '2026-03-01 08:30:00', 'infinity', datetime.datetime(2026, 3, 1, 0, 0))]
OSError: Period must be greater than 0
OSError: Month intervals cannot have day or time component
InternalError: Binder Error: No function matches the given name and argument types 'time_bucket(INTERVAL, TIME)'. You might need to add explicit type casts.
InternalError: Binder Error: Could not choose a best candidate function for the function call "time_bucket(INTERVAL, STRING_LITERAL)". In order to select one, please add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'time_bucket(INTERVAL, INTEGER_LITERAL)'. You might need to add explicit type casts.
The default alignment, an origin and an offset, before and after 1970:
run("SELECT time_bucket(INTERVAL 15 MINUTE, TIMESTAMP '2026-03-01 08:37:12'), "
"time_bucket(INTERVAL 7 DAY, DATE '2026-03-04'), dayname(time_bucket(INTERVAL 7 DAY, DATE '2026-03-04')), "
"time_bucket(INTERVAL 3 MONTH, TIMESTAMP '2026-05-15 08:30:00'), time_bucket(INTERVAL 10 YEAR, DATE '2026-03-15')")
run("SELECT time_bucket(INTERVAL 1 DAY, TIMESTAMP '2026-03-01 08:30:00', TIMESTAMP '2026-01-01 06:00:00'), "
"time_bucket(INTERVAL 1 WEEK, DATE '2026-03-04', DATE '2026-03-01'), "
"time_bucket(INTERVAL 1 DAY, TIMESTAMP '2026-03-01 08:30:00', INTERVAL '-6 hours'), "
"time_bucket(INTERVAL 1 MONTH, DATE '2026-03-15', INTERVAL 9 DAY)")
run("SELECT time_bucket(INTERVAL 1 DAY, TIMESTAMP '1969-12-31 23:59:59.999999'), time_bucket(INTERVAL 7 DAY, DATE '1969-12-31')")
[(datetime.datetime(2026, 3, 1, 8, 30), datetime.date(2026, 3, 2), 'Monday', datetime.datetime(2026, 4, 1, 0, 0), datetime.date(2020, 1, 1))]
[(datetime.datetime(2026, 3, 1, 6, 0), datetime.date(2026, 3, 1), datetime.datetime(2026, 2, 28, 18, 0), datetime.date(2026, 3, 10))]
[(datetime.datetime(1969, 12, 31, 0, 0), datetime.date(1969, 12, 29))]
Examples
Each event in six-hour buckets, in weeks and in months:
run(f"SELECT id, time_bucket(INTERVAL 6 HOUR, ts), time_bucket(INTERVAL 7 DAY, d), time_bucket(INTERVAL 1 MONTH, d) "
f"FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.datetime(2026, 2, 28, 18, 0), datetime.date(2026, 2, 23), datetime.date(2026, 2, 1)), (2, datetime.datetime(2026, 3, 1, 6, 0), datetime.date(2026, 2, 23), datetime.date(2026, 3, 1)), (3, datetime.datetime(2024, 12, 30, 12, 0), datetime.date(2024, 12, 30), datetime.date(2024, 12, 1)), (4, None, None, None), (5, datetime.datetime(2024, 2, 29, 0, 0), datetime.date(2024, 2, 26), datetime.date(2024, 2, 1))]
Days that start at 06:00, written with an origin and with an offset; both give the same buckets:
run(f"SELECT id, time_bucket(INTERVAL 1 DAY, ts, TIMESTAMP '2026-01-01 06:00:00'), "
f"time_bucket(INTERVAL 1 DAY, ts, INTERVAL 6 HOUR) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.datetime(2026, 2, 28, 6, 0), datetime.datetime(2026, 2, 28, 6, 0)), (2, datetime.datetime(2026, 3, 1, 6, 0), datetime.datetime(2026, 3, 1, 6, 0)), (3, datetime.datetime(2024, 12, 30, 6, 0), datetime.datetime(2024, 12, 30, 6, 0)), (4, None, None), (5, datetime.datetime(2024, 2, 28, 6, 0), datetime.datetime(2024, 2, 28, 6, 0))]
A bucket width that comes from a column: here, id days:
run(f"SELECT id, time_bucket(to_days(CAST(id AS INTEGER)), d) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.date(2026, 2, 28)), (2, datetime.date(2026, 3, 1)), (3, datetime.date(2024, 12, 28)), (4, None), (5, datetime.date(2024, 2, 26))]
Keep only the events in the bucket that starts on 1 March 2026:
run(f"SELECT id FROM {TABLE} WHERE time_bucket(INTERVAL 1 DAY, ts) = TIMESTAMP '2026-03-01 00:00:00' ORDER BY id LIMIT 10")
[(2,)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). VAST DataBase can evaluate a comparison of a column with a constant, but not a function of a column, so time_bucket() is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computestime_bucket()for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine computestime_bucket()and keeps the rows that match. No rows are skipped in storage.
The plan of the WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE time_bucket(INTERVAL 1 DAY, ts) = TIMESTAMP '2026-03-01 00:00:00'")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (time_bucket('1 day': │
│ :INTERVAL, ts) = '2026-03 │
│ -01 00:00:00'::TIMESTAMP) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ ts │
│ id │
│ │
└───────────────────────────┘
Make it faster
- To select one bucket, filter on a range of the raw column.
ts >= TIMESTAMP '2026-03-01 00:00:00' AND ts < TIMESTAMP '2026-03-02 00:00:00'keeps the same rows astime_bucket(INTERVAL 1 DAY, ts) = TIMESTAMP '2026-03-01 00:00:00', and it's pushed down to VAST DataBase, so rows that don't match never leave storage. Its plan has noFILTERstep:
run(f"SELECT id FROM {TABLE} WHERE ts >= TIMESTAMP '2026-03-01 00:00:00' AND ts < TIMESTAMP '2026-03-02 00:00:00' "
f"ORDER BY id LIMIT 10")
plan(f"SELECT id FROM {TABLE} WHERE ts >= TIMESTAMP '2026-03-01 00:00:00' AND ts < TIMESTAMP '2026-03-02 00:00:00'")
[(2,)]
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Store the bucket if you filter on it often. Compute it when you write the rows and keep it in its own column:
day_bucket = TIMESTAMP '2026-03-01 00:00:00'is pushed down,time_bucket(INTERVAL 1 DAY, ts) = ...isn't.
Limitations and errors
- No
GROUP BY. Grouping by a bucket, as inGROUP BY time_bucket(...), fails withThe operator 'HASH_GROUP_BY' type=9 is currently not supported.Return the bucket for each row and aggregate in your client, or count one bucket at a time with a range filter, as in Make it faster. - No time-zone support. On a
TIMESTAMP WITH TIME ZONEvalue,time_bucketfails with anExtension Autoloading Error, and the form that takes a time-zone name as its third argument isn't available. Cast the value toTIMESTAMPfirst; see time zones. - Months and days don't mix in one width. Use
INTERVAL 1 MONTHwith an offset such asINTERVAL 9 DAYinstead ofINTERVAL '1 month 9 days'. - No
TIMEvalues.time_buckettakes aDATEor a timestamp, not aTIME.
run(f"SELECT time_bucket(INTERVAL 1 MONTH, d) AS m, count(*) FROM {TABLE} GROUP BY m ORDER BY m LIMIT 10")
run("SELECT time_bucket(INTERVAL 1 DAY, TIMESTAMP '2026-03-01 08:30:00', 'UTC')")
InternalError: The operator 'HASH_GROUP_BY' type=9 is currently not supported.
InternalError: Binder Error: Could not choose a best candidate function for the function call "time_bucket(INTERVAL, TIMESTAMP, STRING_LITERAL)". In order to select one, please add explicit type casts.
Similar functions
| Function | Returns | Result for TIMESTAMP '2024-12-30 13:45:30.123456' |
|---|---|---|
time_bucket(INTERVAL 1 MONTH, ...) |
The start of the bucket, as a TIMESTAMP |
2024-12-01 00:00:00 |
date_trunc ('month', ...) |
The start of the calendar part; a DATE for 'day' and coarser parts |
2024-12-01 |
time_bucket(INTERVAL 1 HOUR, ...) |
Same as date_trunc('hour', ...) |
2024-12-30 13:00:00 |
time_bucket(INTERVAL 6 HOUR, ...) |
Buckets date_trunc has no part for |
2024-12-30 12:00:00 |
| strftime | The value as text, in a format you choose | strftime(..., '%Y-%m-01') is '2024-12-01' |
date_trunc works only with calendar parts (one month, one hour); time_bucket takes any width, such as six hours or ten years, and an origin.
run("SELECT time_bucket(INTERVAL 1 MONTH, TIMESTAMP '2024-12-30 13:45:30.123456'), "
"date_trunc('month', TIMESTAMP '2024-12-30 13:45:30.123456'), "
"time_bucket(INTERVAL 1 HOUR, TIMESTAMP '2024-12-30 13:45:30.123456'), "
"date_trunc('hour', TIMESTAMP '2024-12-30 13:45:30.123456'), "
"time_bucket(INTERVAL 6 HOUR, TIMESTAMP '2024-12-30 13:45:30.123456'), "
"strftime(TIMESTAMP '2024-12-30 13:45:30.123456', '%Y-%m-01')")
[(datetime.datetime(2024, 12, 1, 0, 0), datetime.date(2024, 12, 1), datetime.datetime(2024, 12, 30, 13, 0), datetime.datetime(2024, 12, 30, 13, 0), datetime.datetime(2024, 12, 30, 12, 0), '2024-12-01')]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Date and time functions: all the date and time functions.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.