Applies to VAST Cluster 5.5.
Functions › Date and time functions › Adding and interval arithmetic. Adds an interval to a date, timestamp or time, or a number of days to a date. date_add(a, b) is a shorthand for a + b and works exactly like the + operator: adding a month to the end of a month gives the end of the shorter month, so 31 January plus one month is 28 February.
Syntax
date_add(value, interval)
date_add(date, days)
| Argument | Type | Description |
|---|---|---|
value |
DATE, TIMESTAMP (also TIMESTAMP_NS and TIMESTAMP_MS) or TIME |
The value to add to. |
interval |
INTERVAL |
The time to add; it can be negative, and it can come from columns, as in to_days(n). |
date, days |
DATE and INTEGER |
A number of days to add to a date; negative goes back. |
The arguments can also be in the other order (date_add(INTERVAL 1 DAY, d)), or a DATE and a TIME, which gives the timestamp of that time on that day, as with +.
Returns:
| Arguments | Result |
|---|---|
DATE, INTERVAL |
TIMESTAMP, also when the interval is whole days |
TIMESTAMP (any precision), INTERVAL |
TIMESTAMP |
TIME, INTERVAL |
TIME, wrapping past midnight |
DATE, INTEGER |
DATE |
DATE, TIME |
TIMESTAMP |
INTERVAL, INTERVAL |
INTERVAL |
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_add. 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), a timestamp (ts), a time of day (t) and a number of days (n, an INTEGER). 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_date_add"'
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, t TIME, n INTEGER)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, DATE '2026-02-28', TIMESTAMP '2026-02-28 23:15:00', TIME '23:15:00', 1), "
"(2, DATE '2026-03-01', TIMESTAMP '2026-03-01 08:30:45.123456', TIME '08:30:45.123456', -2), "
"(3, DATE '2024-12-30', TIMESTAMP '2024-12-30 12:00:00', TIME '12:00:00', 0), "
"(4, NULL, NULL, NULL, NULL), "
"(5, DATE '2024-02-29', TIMESTAMP '2024-02-29 00:00:00', TIME '00:00:00', 12)")
[]
[]
[(5,)]
What it does
date_add adds the interval's months first, then its days, then its time:
- Months and years move the calendar month and keep the day of the month. When that day doesn't exist in the new month, the result is the month's last day:
2026-01-31plus one month is2026-02-28,2024-01-31plus one month is2024-02-29, and2024-02-29plus one year is2025-02-28(plus four years,2028-02-29). - Days move whole calendar days:
2026-01-31plusINTERVAL '1 month 1 day'is2026-02-28plus one day,2026-03-01. - Time moves the clock. On a
TIMEit wraps:TIME '23:30:00'plus one hour is00:30:00.
Add a total interval in one call rather than one month at a time: date_add(DATE '2026-01-31', INTERVAL 2 MONTH) is 2026-03-31, but adding one month and then another gives 2026-03-28, because the first step stops at 28 February.
NULLs and edge cases
| Input | Result |
|---|---|
A NULL value (row 4), NULL::DATE, NULL::INTERVAL or a NULL number of days |
NULL |
An untyped NULL value: date_add(NULL, INTERVAL 1 MONTH) |
Error: Could not choose a best candidate function for the function call "+("NULL", INTERVAL)"; use NULL::TIMESTAMP |
A negative interval: INTERVAL '-1 month' on DATE '2026-01-01' |
2025-12-01 |
Mixed signs: INTERVAL '1 month -1 day' on DATE '2026-03-01' |
2026-03-31 |
A TIMESTAMP_NS value |
A TIMESTAMP: the nanoseconds are dropped |
A date past the supported range: date_add(DATE '2026-01-01', 2147483647) |
Error: Date out of range |
A fraction of a day: date_add(DATE '2026-01-01', 1.5) |
Error: No function matches the given name and argument types '+(DATE, DECIMAL(2,1))'; add INTERVAL 36 HOUR |
A number added to a TIMESTAMP: date_add(ts, 1) |
Error: No function matches the given name and argument types '+(TIMESTAMP, INTEGER_LITERAL)'; add INTERVAL 1 DAY |
A string value, such as 'abc' |
Error: Could not choose a best candidate function for the function call "+(STRING_LITERAL, INTERVAL)"; cast it |
Three arguments, as in date_add('day', 1, d) |
Error: Macro function date_add(date, interval) requires 2 positional arguments, but 3 positional arguments were provided. |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run(f"SELECT id, date_add(d, INTERVAL 1 MONTH), date_add(d, n) FROM {TABLE} WHERE id = 4")
run("SELECT date_add(NULL::DATE, INTERVAL 1 DAY), date_add(DATE '2026-01-01', NULL::INTERVAL), "
"date_add(DATE '2026-01-01', INTERVAL '-1 month'), date_add(DATE '2026-03-01', INTERVAL '1 month -1 day'), "
"CAST(date_add(TIMESTAMP_NS '2026-01-01 00:00:00.000000001', INTERVAL 1 DAY) AS VARCHAR)")
run("SELECT date_add(NULL, INTERVAL 1 MONTH)")
run("SELECT date_add(DATE '2026-01-01', 2147483647)")
run("SELECT date_add(DATE '2026-01-01', 1.5)")
run("SELECT date_add(TIMESTAMP '2026-01-01 00:00:00', 1)")
run("SELECT date_add('abc', INTERVAL 1 MONTH)")
run("SELECT date_add('day', 1, DATE '2026-01-01')")
[(4, None, None)]
[(None, None, datetime.datetime(2025, 12, 1, 0, 0), datetime.datetime(2026, 3, 31, 0, 0), '2026-01-02 00:00:00')]
InternalError: Binder Error: Could not choose a best candidate function for the function call "+("NULL", INTERVAL)". In order to select one, please add explicit type casts.
OSError: Date out of range
InternalError: Binder Error: No function matches the given name and argument types '+(DATE, DECIMAL(2,1))'. You might need to add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types '+(TIMESTAMP, INTEGER_LITERAL)'. You might need to add explicit type casts.
InternalError: Binder Error: Could not choose a best candidate function for the function call "+(STRING_LITERAL, INTERVAL)". In order to select one, please add explicit type casts.
InternalError: Binder Error: Macro function date_add(date, interval) requires 2 positional arguments, but 3 positional arguments were provided.
The month-end and leap-day cases from What it does, and adding a total against adding one month at a time:
run("SELECT date_add(DATE '2026-01-31', INTERVAL 1 MONTH), date_add(DATE '2024-01-31', INTERVAL 1 MONTH), "
"date_add(DATE '2026-05-31', INTERVAL 1 MONTH), date_add(DATE '2024-02-29', INTERVAL 1 YEAR), "
"date_add(DATE '2024-02-29', INTERVAL 4 YEAR), date_add(DATE '2026-01-31', INTERVAL '1 month 1 day')")
run("SELECT date_add(DATE '2026-01-31', INTERVAL 2 MONTH), date_add(DATE '2026-01-31', INTERVAL 1 MONTH + INTERVAL 1 MONTH), "
"DATE '2026-01-31' + INTERVAL 1 MONTH + INTERVAL 1 MONTH, date_add(TIME '23:30:00', INTERVAL 1 HOUR)")
[(datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2024, 2, 29, 0, 0), datetime.datetime(2026, 6, 30, 0, 0), datetime.datetime(2025, 2, 28, 0, 0), datetime.datetime(2028, 2, 29, 0, 0), datetime.datetime(2026, 3, 1, 0, 0))]
[(datetime.datetime(2026, 3, 31, 0, 0), datetime.datetime(2026, 3, 31, 0, 0), datetime.datetime(2026, 3, 28, 0, 0), datetime.time(0, 30))]
Examples
One month after each date, and n days after it:
run(f"SELECT id, date_add(d, INTERVAL 1 MONTH), date_add(d, n) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.datetime(2026, 3, 28, 0, 0), datetime.date(2026, 3, 1)), (2, datetime.datetime(2026, 4, 1, 0, 0), datetime.date(2026, 2, 27)), (3, datetime.datetime(2025, 1, 30, 0, 0), datetime.date(2024, 12, 30)), (4, None, None), (5, datetime.datetime(2024, 3, 29, 0, 0), datetime.date(2024, 3, 12))]
Ninety minutes after each timestamp and an hour after each time of day; then n days after each timestamp, with the interval built from the column:
run(f"SELECT id, date_add(ts, INTERVAL 90 MINUTE), date_add(t, INTERVAL 1 HOUR) FROM {TABLE} ORDER BY id LIMIT 10")
run(f"SELECT id, date_add(ts, to_days(n)) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.datetime(2026, 3, 1, 0, 45), datetime.time(0, 15)), (2, datetime.datetime(2026, 3, 1, 10, 0, 45, 123456), datetime.time(9, 30, 45, 123456)), (3, datetime.datetime(2024, 12, 30, 13, 30), datetime.time(13, 0)), (4, None, None), (5, datetime.datetime(2024, 2, 29, 1, 30), datetime.time(1, 0))]
[(1, datetime.datetime(2026, 3, 1, 23, 15)), (2, datetime.datetime(2026, 2, 27, 8, 30, 45, 123456)), (3, datetime.datetime(2024, 12, 30, 12, 0)), (4, None), (5, datetime.datetime(2024, 3, 12, 0, 0))]
Each date at its time of day, as one timestamp:
run(f"SELECT id, date_add(d, t) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, datetime.datetime(2026, 2, 28, 23, 15)), (2, datetime.datetime(2026, 3, 1, 8, 30, 45, 123456)), (3, datetime.datetime(2024, 12, 30, 12, 0)), (4, None), (5, datetime.datetime(2024, 2, 29, 0, 0))]
Keep only the events whose next day starts after 1 March 2026:
run(f"SELECT id FROM {TABLE} WHERE date_add(d, INTERVAL 1 DAY) > 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 an expression on a column, so date_add() on a column is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computesdate_add()for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine computesdate_add()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 date_add(d, INTERVAL 1 DAY) > TIMESTAMP '2026-03-01 00:00:00'")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ ((d + '1 day'::INTERVAL) >│
│ '2026-03-01 00:00:00': │
│ :TIMESTAMP) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ d │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Move the arithmetic to the constant side.
d > DATE '2026-03-01' - 1keeps the same rows asdate_add(d, INTERVAL 1 DAY) > TIMESTAMP '2026-03-01 00:00:00', and it's pushed down to VAST DataBase, so rows that don't match never leave storage. The constant side is computed once. Its plan has noFILTERstep:
run(f"SELECT id FROM {TABLE} WHERE d > DATE '2026-03-01' - 1 ORDER BY id LIMIT 10")
plan(f"SELECT id FROM {TABLE} WHERE d > DATE '2026-03-01' - 1")
[(2,)]
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- For timestamps, subtract the interval from the constant:
ts > TIMESTAMP '2026-03-01 00:00:00' - INTERVAL 1 HOURis pushed down,date_add(ts, INTERVAL 1 HOUR) > ...isn't (-).
Limitations and errors
- Write a negative interval in quotes, or negate it.
INTERVAL -1 DAYis a syntax error (syntax error at or near "DAY"). WriteINTERVAL '-1 day'or-INTERVAL 1 DAY. - Days only on a
DATE. A number of days works with aDATE; for aTIMESTAMPor a fraction of a day, add anINTERVAL. - No time-zone support. On a
TIMESTAMP WITH TIME ZONEvalue, such asnow(),date_addfails withNo function matches the given name and argument types '+(TIMESTAMP WITH TIME ZONE, INTERVAL)'. Cast the value toTIMESTAMPfirst; see time zones. - Other SQL dialects' names and forms aren't available.
dateaddfails withScalar Function with name dateadd does not exist!, anddate_add('day', 1, d)with three arguments fails too. Usedate_add(d, INTERVAL 1 DAY)ord + INTERVAL 1 DAY.
run("SELECT date_add(DATE '2026-03-01', INTERVAL -1 DAY)")
run("SELECT date_add(DATE '2026-03-01', INTERVAL '-1 day'), date_add(DATE '2026-03-01', -INTERVAL 1 DAY)")
run("SELECT date_add(now(), INTERVAL 1 DAY)")
run("SELECT date_add(CAST(now() AS TIMESTAMP), INTERVAL 1 DAY) > CAST(now() AS TIMESTAMP)")
run("SELECT dateadd('day', 1, DATE '2026-03-01')")
InternalError: Parse query: 'SELECT date_add(DATE '2026-03-01', INTERVAL -1 DAY)' failed with: Parser Error: syntax error at or near "DAY"
[(datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2026, 2, 28, 0, 0))]
InternalError: Binder Error: No function matches the given name and argument types '+(TIMESTAMP WITH TIME ZONE, INTERVAL)'. You might need to add explicit type casts.
[(True,)]
InternalError: Catalog Error: Scalar Function with name dateadd does not exist!
Similar functions
| Function or operator | Result for DATE '2026-01-31' and one month |
|---|---|
+, add() |
The same: d + INTERVAL 1 MONTH is 2026-02-28 00:00:00 |
-, subtract() |
Goes back: d - INTERVAL 1 MONTH; the same as date_add(d, -INTERVAL 1 MONTH) |
| to_months and the other interval constructors | Build the interval from a number or a column: d + to_months(1) |
| time_bucket, date_trunc | Round a value down instead of moving it |
run("SELECT date_add(DATE '2026-01-31', INTERVAL 1 MONTH), DATE '2026-01-31' + INTERVAL 1 MONTH, "
"add(DATE '2026-01-31', INTERVAL 1 MONTH), DATE '2026-01-31' + to_months(1), DATE '2026-01-31' + INTERVAL 30 DAY")
run("SELECT DATE '2026-03-31' - INTERVAL 1 MONTH, date_add(DATE '2026-03-31', -INTERVAL 1 MONTH)")
[(datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2026, 3, 2, 0, 0))]
[(datetime.datetime(2026, 2, 28, 0, 0), datetime.datetime(2026, 2, 28, 0, 0))]
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.