Date Functions
2026-04-02
Almost every real business question is, at its core, a story over time.
A single timestamp can represent many analytical dimensions at once.
Without explicitly extracting and structuring these dimensions:
Dates turn events into patterns but only if we ask the right questions.
Throughout this session, we will use the sales_analysis table created earlier.
Each row represents:
Our goal is to answer progressively deeper analytical questions:
Each new date function will add structure to this story.
Sales Analysis Context
Remember to create or recreate the table using the following reference:
https://hovhannisyan91.github.io/aca/materials/sql/session4.html#creating-sales_analysis-table
Before using any date function, we must answer a fundamental question:
What kind of time do we have?
2024-05-122024-05-12 14:37:22Most analytical mistakes with dates begin right here.
In Our Dataset
In sales_analysis:
order_date_date → DATEorder_date → TIMESTAMPWe will intentionally use both to understand their analytical implications.
EXTRACT()EXTRACT()Dates contain multiple dimensions inside a single value \(\rightarrow\) Pulling Meaning from Dates
EXTRACT() allows us to isolate these components.
\[\downarrow\]
| order_date_date | year | month | day | weekday |
|---|---|---|---|---|
| 2021-02-11 | 2021 | 2 | 11 | 4 |
| 2022-12-10 | 2022 | 12 | 10 | 6 |
| 2021-02-22 | 2021 | 2 | 22 | 1 |
| 2022-07-12 | 2022 | 7 | 12 | 2 |
| 2021-04-19 | 2021 | 4 | 19 | 1 |
DATE_PART()DATE_PART() is functionally equivalent to EXTRACT().
Both return numeric components of a date.
EXTRACT() → ANSI-standard, portable SQLDATE_PART() → PostgreSQL-native, readableIn this course, we prefer EXTRACT() for consistency.
DATE_TRUNC()DATE_TRUNC()In analytics, grain matters.
The same data can tell very different stories depending on how time is grouped.
\[\downarrow\]
| month | total_revenue |
|---|---|
| 2020-01-01 00:00:00+00 | 38621.59 |
| 2020-02-01 00:00:00+00 | 30460.16 |
| 2020-04-01 00:00:00+00 | 18096.39 |
| 2020-05-01 00:00:00+00 | 23722.28 |
| 2020-06-01 00:00:00+00 | 42599.07 |
\[\downarrow\]
| month | total_revenue |
|---|---|
| 2022-12-01 00:00:00+00 | 52510.67 |
| 2022-07-01 00:00:00+00 | 49011.10 |
| 2023-12-01 00:00:00+00 | 46700.47 |
| 2021-09-01 00:00:00+00 | 46433.23 |
| 2020-06-01 00:00:00+00 | 42599.07 |
\[\downarrow\]
| quarter | total_revenue |
|---|---|
| 2020-01-01 00:00:00+00 | 69081.75 |
| 2020-04-01 00:00:00+00 | 84417.74 |
| 2020-07-01 00:00:00+00 | 62003.53 |
| 2020-10-01 00:00:00+00 | 69127.16 |
| 2021-01-01 00:00:00+00 | 74184.68 |
| 2021-04-01 00:00:00+00 | 68431.42 |
| 2021-07-01 00:00:00+00 | 84194.98 |
| 2021-10-01 00:00:00+00 | 63543.31 |
\[\downarrow\]
| year | total_revenue |
|---|---|
| 2020-01-01 00:00:00+00 | 284630.18 |
| 2021-01-01 00:00:00+00 | 290354.39 |
| 2022-01-01 00:00:00+00 | 345714.03 |
| 2023-01-01 00:00:00+00 | 350319.25 |
DATE_TRUNC() does not change the data.
It changes how you see the data.
CURRENT_DATE, NOW()In analytics, we often need a reference point called now.
This allows us to answer questions such as:
NOW()If you try:
TIMESTAMP WITH TIME ZONECURRENT_DATEIf you try:
DATEDates become analytical only when we compare them.
PostgreSQL allows direct subtraction between dates.
\[\downarrow\]
| order_date_date | days_since_order |
|---|---|
| 2021-02-11 | 1797 |
| 2022-12-10 | 1130 |
| 2021-02-22 | 1786 |
| 2022-07-12 | 1281 |
| 2021-04-19 | 1730 |
Tip
DATEs returns an integer\[\downarrow\]
| order_date_date | months_estimate |
|---|---|
| 2022-08-03 | 43 |
| 2020-10-10 | 65 |
| 2023-11-17 | 27 |
| 2023-05-25 | 33 |
| 2023-05-25 | 33 |
Warning
This introduces systematic bias.
Assumes every month has 30 days.
\[ 30.4375 = \frac{365.25}{12} \]
\[\downarrow\]
| approx_months |
|---|
| 42.8090349075975359 |
| 64.5585215605749487 |
| 27.3347022587268994 |
| 33.1170431211498973 |
| 33.1170431211498973 |
SELECT
order_date_date,
(DATE_PART('year', CURRENT_DATE) - DATE_PART('year', order_date_date)) * 12
+ (DATE_PART('month', CURRENT_DATE) - DATE_PART('month', order_date_date))
- CASE
WHEN DATE_PART('day', CURRENT_DATE)
< DATE_PART('day', order_date_date)
THEN 1 ELSE 0
END AS full_months
FROM sales_analysis
ORDER BY order_date_date DESC
LIMIT 10;\[\downarrow\]
| order_date_date | full_months |
|---|---|
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
| 2023-12-24 | 26 |
An INTERVAL represents a span of time, not a point.
\[\downarrow\]
| recent_orders |
|---|
| 636 |
INTERVAL '7 days'INTERVAL '1 month'INTERVAL '1 year'\[\downarrow\]
| order_date_date | follow_up_date |
|---|---|
| 2022-08-03 | 2022-08-17 00:00:00 |
| 2020-10-10 | 2020-10-24 00:00:00 |
| 2023-11-17 | 2023-12-01 00:00:00 |
| 2023-05-25 | 2023-06-08 00:00:00 |
| 2023-05-25 | 2023-06-08 00:00:00 |
AGE() returns an INTERVAL representing true calendar distance.
\[\downarrow\]
| order_date_date | order_age |
|---|---|
| 2021-02-11 | 4 years 11 mons 2 days |
| 2022-12-10 | 3 years 1 mon 3 days |
| 2021-02-22 | 4 years 10 mons 19 days |
\[\downarrow\]
| order_date_date | years | months | days |
|---|---|---|---|
| 2022-08-03 | 3 | 6 | 23 |
| 2020-10-10 | 5 | 4 | 16 |
| 2023-11-17 | 2 | 3 | 9 |
| 2023-05-25 | 2 | 9 | 1 |
| 2023-05-25 | 2 | 9 | 1 |
AGE() Interpretation
Use AGE() for human interpretation,
numeric arithmetic for features and modeling.