--- title: "date_sub" slug: "dev-database-reference-sql-functions-date-time-date-sub" updated: 2026-10-30T18:19:01Z published: 2026-10-30T18:19:01Z canonical: "docs.vastdata.com/dev-database-reference-sql-functions-date-time-date-sub" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.vastdata.com/llms.txt > Use this file to discover all available pages before exploring further. # date_sub *Applies to VAST Cluster 5.5.* *[Functions](/develop/docs/dev-database-reference-sql-functions-index) › [Date and time functions](/develop/docs/dev-database-reference-sql-functions-date-time) › Differences.* Returns the number of complete parts, such as whole months or whole days, between two dates, timestamps or times: from 31 January to 1 February is zero months. `datesub` is the same function. Despite its name, it doesn't subtract an interval from a date: for that, use [-](/develop/docs/dev-database-reference-sql-functions-date-time-minus). ## Syntax ``` date_sub(part, start, end) datesub(part, start, end) ``` | Argument | Type | Description | | --- | --- | --- | | `part` | `VARCHAR` | The unit to count, such as `'month'`, `'day'` or `'hour'`. Names aren't case-sensitive, and they're the same as for [date_diff](/develop/docs/dev-database-reference-sql-functions-date-time-date-diff#what-it-does). | | `start`, `end` | Both `DATE`, both `TIMESTAMP` (also `TIMESTAMP_NS` and `TIMESTAMP_MS`), or both `TIME` | The two values. A `DATE` with a `TIMESTAMP` works: the date is read as midnight. | **Returns:** `BIGINT`: positive when `end` is after `start`, negative when it's before. **Aliases:** `datesub`. ## Set up the examples The examples connect with the ADBC driver, as in [VAST Query Engine (ADBC)](/develop/docs/dev-database-integrations-adbc), and work in a schema called `docs_sql_fn_date_time_date_sub`. Set `QE_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `VASTDB_BUCKET` ([SQL reference](/develop/docs/dev-database-reference-sql-index#example-connect-and-use-a-transaction)), 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`) and a time of day (`t`). 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_sub"' 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, "") 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)") run(f"INSERT INTO {TABLE} VALUES " "(1, DATE '2026-02-28', TIMESTAMP '2026-02-28 23:15:00', TIME '23:15:00'), " "(2, DATE '2026-03-01', TIMESTAMP '2026-03-01 08:30:45.123456', TIME '08:30:45.123456'), " "(3, DATE '2024-12-30', TIMESTAMP '2024-12-30 12:00:00', TIME '12:00:00'), " "(4, NULL, NULL, NULL), " "(5, DATE '2024-02-29', TIMESTAMP '2024-02-29 00:00:00', TIME '00:00:00')") ``` ``` [] [] [(5,)] ``` ## What it does `date_sub` counts how many whole parts fit between `start` and `end`, and drops the remainder. From `2026-01-31` to `2026-02-01` is zero months and one day; from `2025-12-31` to `2026-01-01` is zero years. For timestamps the time counts too: from `TIMESTAMP '2026-01-31 12:00:00'` to `TIMESTAMP '2026-02-28 11:59:59'` is 27 days, one second short of 28. A month counts as complete when adding it lands on or before `end`, and adding a month to the end of a month gives the end of the shorter month. So from 31 January to 28 February is one complete month, and from 29 February 2024 to 28 February 2025 is one complete year. [date_diff](/develop/docs/dev-database-reference-sql-functions-date-time-date-diff) counts boundaries instead. The two differ whenever a period is incomplete: | From | To | `date_sub` | `date_diff` | | --- | --- | --- | --- | | `DATE '2026-01-31'` | `DATE '2026-02-01'`, months | `0` | `1` | | `DATE '2026-01-31'` | `DATE '2026-03-01'`, months | `1` | `2` | | `DATE '2025-12-31'` | `DATE '2026-01-01'`, years | `0` | `1` | | `TIMESTAMP '2026-03-01 08:59:00'` | `TIMESTAMP '2026-03-01 09:01:00'`, hours | `0` | `1` | | `DATE '2024-02-29'` | `DATE '2025-02-28'`, years | `1` | `1` | On `TIME` values, `date_sub` counts within one day, with the time parts only: from `TIME '08:00:00'` to `TIME '09:30:00'` is one hour. ## NULLs and edge cases | Input | Result | | --- | --- | | A NULL value (row 4) | `NULL` | | A NULL part: `date_sub(NULL, DATE '2026-01-01', DATE '2026-02-01')` | `NULL` | | `DATE 'infinity'` as either value | `NULL` | | A `DATE` and a string: `date_sub('day', DATE '2026-01-01', '2026-02-01')` | `31`: the string is read as a date | | Two `TIMESTAMP_NS` values less than a second apart: `.999999999` to the next whole second | `0` seconds | | `' year'`, `'yyyy'`, `'mm'`, `'q'`, `'fortnight'` | Error: `extract specifier " year" not recognized` (with the name you gave) | | `'nanosecond'` and its other spellings | Error: `extract specifier "nanosecond" not recognized` | | `'era'`, `'timezone'`, `'timezone_hour'`, `'timezone_minute'` | Error: `Specifier type not implemented for DATESUB` | | `TIME` values with a date part such as `'day'` | Error: `"time" units "day" not recognized` | | A `TIME` and a `TIMESTAMP` | Error: `No function matches the given name and argument types 'date_sub(STRING_LITERAL, TIME, TIMESTAMP)'` | | Two strings, or two untyped `NULL`s | Error: `Could not choose a best candidate function`; cast the values | | A number, boolean, `BLOB`, list or `INTERVAL` | Error: `No function matches the given name and argument types`, for example `'date_sub(STRING_LITERAL, INTEGER_LITERAL, DATE)'` | The same cases, as the cluster returns them. They use the table and helpers from [Set up the examples](/develop/docs/dev-database-reference-sql-functions-date-time-date-sub#set-up-the-examples): ``` run(f"SELECT id, date_sub('day', d, DATE '2026-12-31') FROM {TABLE} WHERE id = 4") run("SELECT date_sub(NULL, DATE '2026-01-01', DATE '2026-02-01'), date_sub('day', DATE '2026-01-01', DATE 'infinity'), " "date_sub('day', DATE '2026-01-01', '2026-02-01'), " "date_sub('second', TIMESTAMP_NS '2026-03-01 00:00:00.999999999', TIMESTAMP_NS '2026-03-01 00:00:01')") run("SELECT date_sub('yyyy', DATE '2024-02-29', DATE '2026-03-01')") run("SELECT date_sub('nanosecond', DATE '2024-02-29', DATE '2026-03-01')") run("SELECT date_sub('era', DATE '2024-02-29', DATE '2026-03-01')") run("SELECT date_sub('day', TIME '08:00:00', TIME '09:00:00')") run("SELECT date_sub('hour', TIME '08:00:00', TIMESTAMP '2026-03-01 09:00:00')") run("SELECT date_sub('day', '2026-01-01', '2026-02-01')") run("SELECT date_sub('day', 123, DATE '2026-12-31')") ``` ``` [(4, None)] [(None, None, 31, 0)] OSError: extract specifier "yyyy" not recognized OSError: extract specifier "nanosecond" not recognized OSError: Specifier type not implemented for DATESUB OSError: "time" units "day" not recognized InternalError: Binder Error: No function matches the given name and argument types 'date_sub(STRING_LITERAL, TIME, TIMESTAMP)'. You might need to add explicit type casts. InternalError: Binder Error: Could not choose a best candidate function for the function call "date_sub(STRING_LITERAL, STRING_LITERAL, STRING_LITERAL)". In order to select one, please add explicit type casts. InternalError: Binder Error: No function matches the given name and argument types 'date_sub(STRING_LITERAL, INTEGER_LITERAL, DATE)'. You might need to add explicit type casts. ``` The comparison with `date_diff` from [What it does](/develop/docs/dev-database-reference-sql-functions-date-time-date-sub#what-it-does), the month-end and leap-day cases, and a `TIME` difference: ``` run("SELECT date_sub('month', DATE '2026-01-31', DATE '2026-02-01'), date_diff('month', DATE '2026-01-31', DATE '2026-02-01'), " "date_sub('month', DATE '2026-01-31', DATE '2026-03-01'), date_diff('month', DATE '2026-01-31', DATE '2026-03-01'), " "date_sub('year', DATE '2025-12-31', DATE '2026-01-01'), date_diff('year', DATE '2025-12-31', DATE '2026-01-01')") run("SELECT date_sub('hour', TIMESTAMP '2026-03-01 08:59:00', TIMESTAMP '2026-03-01 09:01:00'), " "date_diff('hour', TIMESTAMP '2026-03-01 08:59:00', TIMESTAMP '2026-03-01 09:01:00'), " "date_sub('year', DATE '2024-02-29', DATE '2025-02-28'), date_diff('year', DATE '2024-02-29', DATE '2025-02-28')") run("SELECT date_sub('month', DATE '2026-01-31', DATE '2026-02-28'), " "date_sub('day', TIMESTAMP '2026-01-31 12:00:00', TIMESTAMP '2026-02-28 11:59:59'), " "datesub('hour', TIME '08:00:00', TIME '09:30:00')") ``` ``` [(0, 1, 1, 2, 0, 1)] [(0, 1, 1, 1)] [(1, 27, 1)] ``` ## Examples The whole days from each event to the end of 2026, the whole hours from each timestamp to 1 March 2026, and the whole months to the end of 2026: ``` run(f"SELECT id, date_sub('day', d, DATE '2026-12-31'), datesub('hour', ts, TIMESTAMP '2026-03-01 00:00:00'), " f"date_sub('month', ts, TIMESTAMP '2026-12-31 00:00:00') FROM {TABLE} ORDER BY id LIMIT 10") ``` ``` [(1, 306, 0, 10), (2, 305, -8, 9), (3, 731, 10212, 24), (4, None, None, None), (5, 1036, 17544, 34)] ``` The whole minutes from each time of day to noon: ``` run(f"SELECT id, date_sub('minute', t, TIME '12:00:00') FROM {TABLE} ORDER BY id LIMIT 10") ``` ``` [(1, -675), (2, 209), (3, 0), (4, None), (5, 720)] ``` Keep only the events less than 10 whole months before the end of 2026: ``` run(f"SELECT id FROM {TABLE} WHERE date_sub('month', d, DATE '2026-12-31') < 10 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 `date_sub()` is always computed by the Query Engine: - **In the `SELECT` list,** the scan reads the columns and the Query Engine computes `date_sub()` for each row it returns. - **In `WHERE`,** the scan sends the columns for every row, and a `FILTER` step in the Query Engine computes `date_sub()` 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_sub('month', d, DATE '2026-12-31') < 10") ``` ``` ┌───────────────────────────┐ │ PROJECTION │ │ ──────────────────── │ │ #1 │ │ │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ FILTER │ │ ──────────────────── │ │ (date_sub('month', d, │ │ '2026-12-31'::DATE) < 10) │ │ │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ VAST_SCAN │ │ ──────────────────── │ │ Function: VAST_SCAN │ │ │ │ Projections: │ │ d │ │ id │ │ │ └───────────────────────────┘ ``` ## Make it faster - **Compare the column with a computed constant.** "Less than one whole year before the end of 2026" is `ts > TIMESTAMP '2026-12-31 00:00:00' - INTERVAL 1 YEAR`. That condition is pushed down to VAST DataBase, so rows that don't match never leave storage; `date_sub('year', ts, ...) < 1` isn't. Both return the same rows, and the plan of the first has no `FILTER` step: ``` run(f"SELECT id FROM {TABLE} WHERE date_sub('year', ts, TIMESTAMP '2026-12-31 00:00:00') < 1 ORDER BY id LIMIT 10") run(f"SELECT id FROM {TABLE} WHERE ts > TIMESTAMP '2026-12-31 00:00:00' - INTERVAL 1 YEAR ORDER BY id LIMIT 10") plan(f"SELECT id FROM {TABLE} WHERE ts > TIMESTAMP '2026-12-31 00:00:00' - INTERVAL 1 YEAR") ``` ``` [(1,), (2,)] [(1,), (2,)] ┌───────────────────────────┐ │ VAST_SCAN │ │ ──────────────────── │ │ Function: VAST_SCAN │ │ Projections: id │ │ │ └───────────────────────────┘ ``` - **For whole days between dates, subtract the dates.** `DATE '2026-12-31' - d` gives the same number as `date_sub('day', d, DATE '2026-12-31')`; to filter on it, write `d > DATE '2026-12-31' - 300` ([-](/develop/docs/dev-database-reference-sql-functions-date-time-minus#make-it-faster)). ## Limitations and errors - **It doesn't subtract an interval.** A number in place of the start date, as in `date_sub('day', 1, DATE '2026-03-01')`, fails with `No function matches the given name and argument types`. To go back in time, use [-](/develop/docs/dev-database-reference-sql-functions-date-time-minus): `DATE '2026-03-01' - 1` or `ts - INTERVAL 1 MONTH`. - **No time-zone support.** On `TIMESTAMP WITH TIME ZONE` values, such as `now()`, `date_sub` fails with an `Extension Autoloading Error`. Cast the values to `TIMESTAMP` first; see [time zones](/develop/docs/dev-database-reference-sql-functions-date-time-time-zones). - **No nanoseconds.** Use `'microseconds'`. ``` run("SELECT date_sub('day', 1, DATE '2026-03-01')") run("SELECT DATE '2026-03-01' - 1, TIMESTAMP '2026-03-31 08:00:00' - INTERVAL 1 MONTH") ``` ``` InternalError: Binder Error: No function matches the given name and argument types 'date_sub(STRING_LITERAL, INTEGER_LITERAL, DATE)'. You might need to add explicit type casts. [(datetime.date(2026, 2, 28), datetime.datetime(2026, 2, 28, 8, 0))] ``` ## Similar functions | Function | Returns | From `DATE '2026-01-31'` to `DATE '2026-03-01'` (`date_sub('month', ...)` gives `1`) | | --- | --- | --- | | [date_diff](/develop/docs/dev-database-reference-sql-functions-date-time-date-diff) | The number of part boundaries crossed | `date_diff('month', ...)` is `2` | | [age](/develop/docs/dev-database-reference-sql-functions-date-time-age) | An `INTERVAL` in months, days and time | `1 month 1 day` | | [-](/develop/docs/dev-database-reference-sql-functions-date-time-minus) | `DATE - DATE` is the days between them | `29` | ``` run("SELECT date_sub('month', DATE '2026-01-31', DATE '2026-03-01'), date_diff('month', DATE '2026-01-31', DATE '2026-03-01'), " "age(DATE '2026-03-01', DATE '2026-01-31'), DATE '2026-03-01' - DATE '2026-01-31'") ``` ``` [(1, 2, MonthDayNano(months=1, days=1, nanoseconds=0), 29)] ``` Clean up: ``` run(f"DROP TABLE IF EXISTS {TABLE}") run(f"DROP SCHEMA IF EXISTS {SCHEMA}") conn.close() ``` ``` [] [] ``` ## See also - [Date and time functions](/develop/docs/dev-database-reference-sql-functions-date-time): all the date and time functions. - [Where filters run](/develop/docs/dev-database-reference-sql-select#where-filters-run) and [pushdowns](/develop/docs/dev-database-reference-pushdowns). - [EXPLAIN](/develop/docs/dev-database-reference-sql-explain): how to read a plan.