Applies to VAST Cluster 5.5.
Functions › Date and time functions › Truncating and bucketing. Rounds a date, timestamp or interval down to the start of the unit you name: its year, quarter, month, week, day, hour, minute, second, millisecond or microsecond. datetrunc is the same function.
Syntax
date_trunc(part, value)
datetrunc(part, value)
| Argument | Type | Description |
|---|---|---|
part |
VARCHAR |
The unit to round down to, such as 'month' or 'hour'. See Parts. Not case-sensitive. |
value |
DATE, TIMESTAMP (also TIMESTAMP_NS, TIMESTAMP_MS, TIMESTAMP_S) or INTERVAL |
The value to round down. |
Returns: DATE for 'day' and coarser parts, TIMESTAMP for 'hour' and finer parts, whether value is a DATE or a timestamp. INTERVAL for an INTERVAL. When part comes from a column rather than a constant, the result is always a TIMESTAMP.
Aliases: datetrunc.
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_date_trunc. 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 has a date (d), a timestamp (ts), a timestamp with nanoseconds (tns) and a part name (part). Row 4 has no values, and row 5 is the last moment of 1969:
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_date_trunc"'
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, tns TIMESTAMP_NS, part VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, DATE '2026-02-28', TIMESTAMP '2026-02-28 23:15:00', "
"CAST('2026-02-28 23:15:00.123456789' AS TIMESTAMP_NS), 'year'), "
"(2, DATE '2026-03-01', TIMESTAMP '2026-03-01 08:30:45.123456', "
"CAST('2026-03-01 08:30:45.123456' AS TIMESTAMP_NS), 'month'), "
"(3, DATE '2024-12-30', TIMESTAMP '2024-12-30 12:00:00', "
"CAST('2024-12-30 12:00:00' AS TIMESTAMP_NS), 'week'), "
"(4, NULL, NULL, NULL, NULL), "
"(5, DATE '1969-12-31', TIMESTAMP '1969-12-31 23:59:59.999999', "
"CAST('1969-12-31 23:59:59.999999999' AS TIMESTAMP_NS), 'hour')")
[]
[]
[(5,)]
What it does
date_trunc sets every field finer than part to its lowest value: date_trunc('month', TIMESTAMP '2024-02-29 13:45:30.123456') is 2024-02-01, and date_trunc('hour', ...) of the same timestamp is 2024-02-29 13:00:00. A week starts on Monday: date_trunc('week', DATE '2026-03-01'), a Sunday, is 2026-02-23.
Before 1970 it still rounds down: date_trunc('second', TIMESTAMP '1969-12-31 23:59:59.999999') is 1969-12-31 23:59:59.
On an INTERVAL, date_trunc drops the fields finer than part, and works toward zero for a negative interval: date_trunc('month', INTERVAL '1 year 2 months 3 days') is 1 year 2 months, and date_trunc('hour', INTERVAL '-90 minutes') is minus one hour. It doesn't carry days into months: date_trunc('day', INTERVAL '40 days 30 hours') is 40 days.
Parts
| Part (and other spellings) | Rounds down to | Result on a DATE or timestamp |
|---|---|---|
'millennium' ('mil', 'mils', 'millennia', 'millenniums', 'millenium') |
The start of the millennium: 2000-01-01 for 2026 |
DATE |
'century' ('c', 'cent', 'centuries') |
The start of the century: 2000-01-01 for 2026 |
DATE |
'decade' ('dec', 'decs', 'decades') |
The start of the decade: 2020-01-01 for 2029-12-31 |
DATE |
'year' ('y', 'yr', 'yrs', 'years') |
1 January | DATE |
'isoyear' |
The Monday that starts the ISO year: 2025-12-29 for 2027-01-03 |
DATE |
'quarter' ('quarters') |
The first day of the quarter: 2026-10-01 for 2026-12-31 |
DATE |
'month' ('mon', 'mons', 'months') |
The first day of the month | DATE |
'week' ('w', 'weeks', 'weekofyear', 'yearweek') |
Monday | DATE |
'day' ('d', 'days', 'dayofmonth'; also 'dow', 'dayofweek', 'weekday', 'isodow', 'doy', 'dayofyear', 'julian', 'jd') |
Midnight | DATE |
'hour' ('h', 'hr', 'hrs', 'hours') |
The start of the hour | TIMESTAMP |
'minute' ('m', 'min', 'mins', 'minutes') |
The start of the minute | TIMESTAMP |
'second' ('s', 'sec', 'secs', 'seconds'; also 'epoch') |
The whole second | TIMESTAMP |
'millisecond' ('ms', 'msec', 'msecs', 'msecond', 'mseconds', 'milliseconds') |
The whole millisecond | TIMESTAMP |
'microsecond' ('us', 'usec', 'usecs', 'usecond', 'useconds', 'microseconds') |
The whole microsecond | TIMESTAMP |
Part names aren't case-sensitive ('YEAR' works), but spaces aren't trimmed (' year' fails). 'nanosecond', 'era' and the time-zone parts aren't available; see Limitations and errors.
NULLs and edge cases
| Input | Result |
|---|---|
A NULL value (row 4), or NULL::DATE |
NULL |
A NULL part: date_trunc(NULL, ts) |
NULL |
DATE 'infinity', TIMESTAMP '-infinity' |
The same special value |
date_trunc('week', DATE '2026-03-02') (a Monday) |
2026-03-02 |
date_trunc('millisecond', TIMESTAMP '1969-12-31 23:59:59.999999') |
1969-12-31 23:59:59.999 |
date_trunc('microsecond', TIMESTAMP_NS '2024-02-29 13:45:30.123456789') |
2024-02-29 13:45:30.123456, a TIMESTAMP |
date_trunc('year', INTERVAL '30 months') |
24 months |
date_trunc('week', INTERVAL '10 days') |
7 days |
An untyped NULL value: date_trunc('month', NULL) |
Error: Could not choose a best candidate function |
An unknown part, such as 'fortnight' or 'q' |
Error: extract specifier "fortnight" not recognized |
A TIME |
Error: No function matches the given name and argument types 'date_trunc(STRING_LITERAL, TIME)' |
| A string value | Error: Could not choose a best candidate function; cast it to DATE or TIMESTAMP first |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run(f"SELECT id, date_trunc('month', ts) FROM {TABLE} WHERE id = 4")
run("SELECT date_trunc('month', NULL::DATE), date_trunc(NULL, TIMESTAMP '2024-02-29 13:45:30'), "
"CAST(date_trunc('month', DATE 'infinity') AS VARCHAR), CAST(date_trunc('hour', TIMESTAMP '-infinity') AS VARCHAR)")
run("SELECT date_trunc('week', DATE '2026-03-02'), date_trunc('millisecond', TIMESTAMP '1969-12-31 23:59:59.999999'), "
"CAST(date_trunc('microsecond', TIMESTAMP_NS '2024-02-29 13:45:30.123456789') AS VARCHAR)")
run("SELECT CAST(date_trunc('year', INTERVAL '30 months') AS VARCHAR), CAST(date_trunc('week', INTERVAL '10 days') AS VARCHAR)")
run("SELECT date_trunc('month', NULL)")
run("SELECT date_trunc('fortnight', TIMESTAMP '2024-02-29 13:45:30')")
run("SELECT date_trunc('hour', TIME '13:45:30')")
run("SELECT date_trunc('month', '2024-02-29')")
[(4, None)]
[(None, None, 'infinity', '-infinity')]
[(datetime.date(2026, 3, 2), datetime.datetime(1969, 12, 31, 23, 59, 59, 999000), '2024-02-29 13:45:30.123456')]
[('2 years', '7 days')]
InternalError: Binder Error: Could not choose a best candidate function for the function call "date_trunc(STRING_LITERAL, "NULL")". In order to select one, please add explicit type casts.
InternalError: Conversion Error: extract specifier "fortnight" not recognized
InternalError: Binder Error: No function matches the given name and argument types 'date_trunc(STRING_LITERAL, TIME)'. You might need to add explicit type casts.
InternalError: Binder Error: Could not choose a best candidate function for the function call "date_trunc(STRING_LITERAL, STRING_LITERAL)". In order to select one, please add explicit type casts.
Examples
The month of each event's timestamp, the week of its date, and the hour of its date. 'hour' returns a TIMESTAMP even for a DATE:
run(f"SELECT id, date_trunc('month', ts), date_trunc('week', d), date_trunc('hour', d) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.date(2026, 2, 1), datetime.date(2026, 2, 23), datetime.datetime(2026, 2, 28, 0, 0)), (2, datetime.date(2026, 3, 1), datetime.date(2026, 2, 23), datetime.datetime(2026, 3, 1, 0, 0)), (3, datetime.date(2024, 12, 1), datetime.date(2024, 12, 30), datetime.datetime(2024, 12, 30, 0, 0)), (4, None, None, None), (5, datetime.date(1969, 12, 1), datetime.date(1969, 12, 29), datetime.datetime(1969, 12, 31, 0, 0))]
The same timestamp rounded down to each of several parts:
run("SELECT date_trunc('year', TIMESTAMP '2024-02-29 13:45:30.123456'), "
"date_trunc('quarter', TIMESTAMP '2024-02-29 13:45:30.123456'), "
"date_trunc('week', TIMESTAMP '2024-02-29 13:45:30.123456'), "
"date_trunc('minute', TIMESTAMP '2024-02-29 13:45:30.123456'), "
"date_trunc('millisecond', TIMESTAMP '2024-02-29 13:45:30.123456')")
[(datetime.date(2024, 1, 1), datetime.date(2024, 1, 1), datetime.date(2024, 2, 26), datetime.datetime(2024, 2, 29, 13, 45), datetime.datetime(2024, 2, 29, 13, 45, 30, 123000))]
Take the part from a column. Each row is rounded to its own part, and every result is a TIMESTAMP:
run(f"SELECT id, part, date_trunc(part, ts) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'year', datetime.datetime(2026, 1, 1, 0, 0)), (2, 'month', datetime.datetime(2026, 3, 1, 0, 0)), (3, 'week', datetime.datetime(2024, 12, 30, 0, 0)), (4, None, None), (5, 'hour', datetime.datetime(1969, 12, 31, 23, 0))]
Keep only the events from February 2026. This works, but Make it faster has a faster way to write it:
run(f"SELECT id FROM {TABLE} WHERE date_trunc('month', ts) = TIMESTAMP '2026-02-01 00:00:00' ORDER BY id LIMIT 10")
[(1,)]
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 date_trunc() of a column is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computesdate_trunc()for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine computesdate_trunc()and keeps the rows that match. No rows are skipped in storage. This is the same for aDATEcolumn.
The plan of the WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE date_trunc('month', ts) = TIMESTAMP '2026-02-01 00:00:00'")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (date_trunc('month', ts) =│
│ '2026-02-01'::DATE) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ ts │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Filter on a range of the raw column, not on
date_trunc().ts >= TIMESTAMP '2026-02-01 00:00:00' AND ts < TIMESTAMP '2026-03-01 00:00:00'returns the same rows asdate_trunc('month', ts) = TIMESTAMP '2026-02-01 00:00:00', and it's pushed down to VAST DataBase, so rows from other months never leave storage. The plan has noFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE ts >= TIMESTAMP '2026-02-01 00:00:00' AND ts < TIMESTAMP '2026-03-01 00:00:00'")
run(f"SELECT id FROM {TABLE} WHERE ts >= TIMESTAMP '2026-02-01 00:00:00' AND ts < TIMESTAMP '2026-03-01 00:00:00' "
"ORDER BY id LIMIT 10")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
[(1,)]
- Apply
date_truncto a constant, not to the column.ts >= date_trunc('month', TIMESTAMP '2026-03-15 00:00:00')(everything from the start of March 2026) is computed once, and the comparison is pushed down:
plan(f"SELECT id FROM {TABLE} WHERE ts >= date_trunc('month', TIMESTAMP '2026-03-15 00:00:00')")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Use
date_trunc()in theSELECTlist, where it doesn't stop a filter on the raw column from being pushed down.
Limitations and errors
- No time-zone support. On a
TIMESTAMP WITH TIME ZONEvalue, such asnow(),date_truncfails with anExtension Autoloading Error. Cast the value toTIMESTAMPfirst:date_trunc('month', CAST(now() AS TIMESTAMP)). See time zones. - Not for
TIME.date_trunc('hour', TIME ...)fails withNo function matches the given name and argument types 'date_trunc(STRING_LITERAL, TIME)'. - No nanoseconds.
'nanosecond'(and'ns','nsec'and the other spellings) fails withextract specifier "nanosecond" not recognized. On aTIMESTAMP_NS, the result is aTIMESTAMP, which holds microseconds: row 5's1969-12-31 23:59:59.999999999gives1970-01-01 00:00:00with'second', while row 5'sts(1969-12-31 23:59:59.999999) gives1969-12-31 23:59:59. - No
'era'or time-zone parts.'era','timezone','timezone_hour'and'timezone_minute'fail withNot implemented Error: Specifier type not implemented for DATETRUNC statistics. - Other SQL dialects' forms aren't available. Format-style parts such as
'yyyy','mm','dd','hh','mi','ss'and'q'aren't recognized, andtrunc(DATE, 'MM')fails withNo function matches the given name and argument types. Use the part names in Parts.
run(f"SELECT id, CAST(date_trunc('second', tns) AS VARCHAR), typeof(date_trunc('second', tns)), "
f"CAST(date_trunc('second', ts) AS VARCHAR) FROM {TABLE} ORDER BY id LIMIT 10")
run("SELECT date_trunc('nanosecond', TIMESTAMP '2024-02-29 13:45:30')")
run("SELECT date_trunc('era', TIMESTAMP '2024-02-29 13:45:30')")
run("SELECT date_trunc('mm', TIMESTAMP '2024-02-29 13:45:30')")
run("SELECT date_trunc('month', CAST(TIMESTAMPTZ '2024-02-29 13:45:30+02' AS TIMESTAMP))")
[(1, '2026-02-28 23:15:00', 'TIMESTAMP', '2026-02-28 23:15:00'), (2, '2026-03-01 08:30:45', 'TIMESTAMP', '2026-03-01 08:30:45'), (3, '2024-12-30 12:00:00', 'TIMESTAMP', '2024-12-30 12:00:00'), (4, None, 'TIMESTAMP', None), (5, '1970-01-01 00:00:00', 'TIMESTAMP', '1969-12-31 23:59:59')]
InternalError: Conversion Error: extract specifier "nanosecond" not recognized
InternalError: Not implemented Error: Specifier type not implemented for DATETRUNC statistics
InternalError: Conversion Error: extract specifier "mm" not recognized
[(datetime.date(2024, 2, 1),)]
Similar functions
| Function | Returns | Result for TIMESTAMP '2024-12-30 13:45:30.123456' |
|---|---|---|
date_trunc('month', ...), datetrunc('month', ...) |
The first day of the month, as a DATE |
2024-12-01 |
time_bucket with INTERVAL 1 MONTH |
The start of the bucket, as a TIMESTAMP; buckets of any width |
2024-12-01 00:00:00 |
time_bucket with INTERVAL 6 HOUR |
A six-hour bucket, which date_trunc can't do |
2024-12-30 12:00:00 |
strftime with '%Y-%m-01' |
The first day of the month, as text | '2024-12-01' |
| last_day | The last day of the month | 2024-12-31 |
run("SELECT date_trunc('month', TIMESTAMP '2024-12-30 13:45:30.123456'), datetrunc('month', TIMESTAMP '2024-12-30 13:45:30.123456'), "
"time_bucket(INTERVAL 1 MONTH, 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'), last_day(TIMESTAMP '2024-12-30 13:45:30.123456')")
[(datetime.date(2024, 12, 1), datetime.date(2024, 12, 1), datetime.datetime(2024, 12, 1, 0, 0), datetime.datetime(2024, 12, 30, 12, 0), '2024-12-01', datetime.date(2024, 12, 31))]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- time_bucket: buckets of any width.
- date_part: the part names, and getting one part as a number.
- Date and time functions: all the date and time functions.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.