Date and Time Functions
Extract parts, calculate differences, truncate to periods, format for display, handle timezones — every temporal operation for time-series analytics and reporting
Quick answer
Date functions extract parts of a date, add or subtract time, and group by periods. PostgreSQL uses EXTRACT, DATE_TRUNC and INTERVAL; SQLite, used in the playground, uses strftime, date() and julianday().
- Group time series by a truncated date (month start), not by month number alone.
- Subtracting dates gives days in PostgreSQL; in SQLite use julianday(a) - julianday(b).
- Store and compare dates as YYYY-MM-DD so they sort correctly.
SELECT strftime('%Y-%m', order_date) AS month,
COUNT(*) AS orders
FROM orders
GROUP BY month
ORDER BY month;Result · 3 rows
| month | orders |
|---|---|
| 2024-01 | 10 |
| 2024-02 | 11 |
| 2024-03 | 9 |
// Part 01
Date and Time Types — What You Are Working With
SQL has four primary temporal data types. Understanding which type a column uses determines which functions apply and how arithmetic works.
| Type | Stores | Example | Use for |
|---|---|---|---|
| DATE | Calendar date only — no time | 2024-01-15 | Order dates, birthdays, hire dates |
| TIME | Time of day only — no date | 14:30:00 | Store hours, shift times |
| TIMESTAMP | Date + time, no timezone | 2024-01-15 14:30:00 | Event logs, created_at, updated_at |
| TIMESTAMPTZ | Date + time + timezone offset | 2024-01-15 14:30:00-05:00 | Multi-region events, audit logs |
// Part 02
Current Date and Time — Getting Now
// Part 03
EXTRACT — Pulling Out Date Parts
EXTRACT pulls a single numeric component out of a date or timestamp. It is the most commonly used date function in analytics — grouping by month, filtering by year, or computing the day of the week.
// Part 04
DATE_TRUNC — Rounding Down to a Period
DATE_TRUNC truncates a date or timestamp to the start of a specified period — the first day of the month, the first day of the week, the first hour of the day. It is the foundation of time-series grouping: every date in the same month truncates to the same value, making GROUP BY work cleanly across all dates in that period.
// Part 05
Date Arithmetic — Adding and Subtracting Time
SQL dates support arithmetic — you can add or subtract intervals to compute future dates, calculate differences, and define rolling time windows.
// Part 06
AGE — Human-Readable Time Differences
AGE computes the difference between two dates as an interval — expressed in years, months, and days rather than a raw number of days. It is the right tool when you want to display tenure, age, or time elapsed in a human-readable format.
// Part 07
Date Formatting — TO_CHAR and strftime
Date formatting converts a date or timestamp into a human-readable string for display. PostgreSQL uses TO_CHAR. SQLite (this playground) uses strftime. Both follow format codes where letters represent date parts.
// Part 08
Date Construction — MAKE_DATE and Casting
Sometimes you need to build a date from parts — constructing the first day of a month, creating a date from year and month numbers, or parsing a date from a string.
// Part 09
Time-Series Analysis Patterns
Date functions are the engine of time-series analytics. The patterns in this part — period-over-period comparison, rolling windows, cohort dating, and gap detection — appear in almost every analytical dashboard.
Month-over-month growth
Cohort analysis — when did customers first order?
Rolling 7-day average
// Part 10
Timezone Handling
Timezones matter for any platform with users or stores in multiple regions. The US alone spans six timezones — a platform with stores in Seattle and Boston already handles a 3-hour offset within one country. Global platforms need to store timestamps in UTC and convert for display.
// Part 11
Complete Date Function Reference
CURRENT_DATEToday's dateCURRENT_DATE → 2024-02-15CURRENT_TIMESTAMP / NOW()Current date + timeNOW() → 2024-02-15 09:30:00EXTRACT(part FROM date)Pull one numeric componentEXTRACT(MONTH FROM order_date) → 2DATE_TRUNC('period', date)Round down to period startDATE_TRUNC('month', date) → 2024-02-01date + INTERVAL 'n unit'Add time to a date'2024-01-15' + INTERVAL '7 days' → 2024-01-22date1 - date2Days between two dates'2024-02-15'::DATE - '2024-01-15'::DATE → 31AGE(date2, date1)Human-readable intervalAGE('2024-04-01', '2022-01-15') → 2 years 2 mons 17 daysstrftime(date, fmt)Format date as string (DuckDB)strftime(date, '%d %b %Y') → 15 Jan 2024TO_CHAR(date, fmt)Format date as string (PostgreSQL)TO_CHAR(date, 'DD Mon YYYY') → 15 Jan 2024MAKE_DATE(y, m, d)Build date from integersMAKE_DATE(2024, 1, 15) → 2024-01-15date::DATECast to date type'2024-01-15'::DATEDATEADD(unit, n, date)Add n units (MySQL/SQL Server)DATEADD(day, 7, order_date)DATEDIFF(unit, d1, d2)Difference in units (MySQL)DATEDIFF(day, '2024-01-01', '2024-02-01') → 31// Part 12
What This Looks Like at Work
You are a data analyst at Instacart. The growth team runs a weekly business review and needs three reports every Monday morning: (1) last week's revenue by day of week, (2) month-to-date revenue versus the same period last month, and (3) a 30-day delivery time trend. All three are date function queries.
🎯 Pro Tip
Save commonly used date boundaries as variables or CTEs at the top of your reporting queries: current month start, prior month start, MTD cutoff. Then reference them throughout rather than recomputing DATE_TRUNC('month', CURRENT_DATE) ten times. A single CTE named report_dates with all your period boundaries makes the query readable and ensures consistency — one change at the top updates every reference.
// Part 13
Interview Prep — 5 Questions With Complete Answers
EXTRACT pulls a single numeric component from a date — EXTRACT(MONTH FROM date) returns the number 1 through 12. DATE_TRUNC rounds a date down to the start of a specified period — DATE_TRUNC('month', date) returns a complete date value representing the first day of that month.
For time-series grouping, this distinction is critical. EXTRACT(MONTH FROM date) loses the year — January 2023 and January 2024 both return 1 from EXTRACT(MONTH). If you GROUP BY EXTRACT(MONTH FROM order_date), orders from different years collapse into the same group, producing incorrect multi-year totals. DATE_TRUNC('month', date) returns 2023-01-01 and 2024-01-01 respectively — distinct values that sort correctly and do not merge across years.
Always use DATE_TRUNC for time-series GROUP BY. Use EXTRACT only when you specifically want just the numeric component — for example, filtering by month number across all years (WHERE EXTRACT(MONTH FROM order_date) = 1 to get all Januaries), or for day-of-week analysis where you want to aggregate all Mondays together regardless of the specific date. The rule: if ordering and uniqueness across years matters, use DATE_TRUNC. If you want to aggregate across multiple years intentionally, use EXTRACT.
In PostgreSQL and DuckDB, subtract two DATE values directly — the result is an integer representing the number of days: delivery_date - order_date returns the number of days between them. For TIMESTAMP values, subtraction returns an INTERVAL — extract the day component with EXTRACT(DAY FROM (timestamp2 - timestamp1)) or convert to epoch seconds and divide: EXTRACT(EPOCH FROM (ts2 - ts1)) / 86400.
In MySQL, use DATEDIFF(end_date, start_date) — note the argument order is end then start, which returns positive values for end > start. DATEDIFF('2024-02-15', '2024-01-15') returns 31. In SQL Server, DATEDIFF(day, start_date, end_date) — note the unit is first and the argument order is reversed from MySQL.
For business days (excluding weekends), there is no standard SQL function — you need a calendar table or a more complex expression. A simple approximation: total_days - (total_weeks * 2) where total_weeks is FLOOR(total_days / 7) — this works for approximate business day counts. For precise business day calculations accounting for holidays, a calendar dimension table with an is_business_day flag is the correct approach. The AGE function provides a human-readable interval (years, months, days) rather than a raw day count — use AGE for display purposes (tenure display, customer age) and direct date subtraction for arithmetic calculations.
The standard pattern: WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'. This filters orders where the order_date is within the last 30 days from today. CURRENT_DATE gives today's date; subtracting INTERVAL '30 days' gives the cutoff date. The >= operator includes the cutoff date itself.
Variations for different databases: MySQL uses DATE_SUB(CURDATE(), INTERVAL 30 DAY) or NOW() - INTERVAL 30 DAY. SQL Server uses DATEADD(day, -30, GETDATE()). The PostgreSQL/DuckDB interval syntax (INTERVAL '30 days') is the most readable and closest to the SQL standard.
Important nuances: CURRENT_DATE - INTERVAL '30 days' is not the same as "one calendar month ago" — 30 days is always 30 days regardless of month length. For "same period last calendar month", use WHERE order_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' AND order_date < DATE_TRUNC('month', CURRENT_DATE). For "last N months", use INTERVAL 'N months' — this correctly handles month-length differences: CURRENT_DATE - INTERVAL '3 months' gives the date 3 calendar months ago, not 90 days ago. For performance, never apply functions to the indexed column — WHERE order_date >= CURRENT_DATE - INTERVAL '30 days' allows index range scan; WHERE EXTRACT(MONTH FROM order_date) = ... requires a full table scan.
DATE_TRUNC truncates a date or timestamp to the beginning of a specified time period — 'month' gives the first day of the month, 'year' gives January 1st, 'week' gives Monday of that week, 'hour' gives the top of the hour. The key behaviour: all dates within the same period truncate to the same value. Every day in January 2024 truncates to 2024-01-01. This makes DATE_TRUNC the foundation of time-series GROUP BY.
For time-series reporting: GROUP BY DATE_TRUNC('month', order_date) groups all January orders together, all February orders together, and produces one row per calendar month. The truncated date is a complete, sortable, comparable value — ORDER BY DATE_TRUNC('month', order_date) correctly sorts months chronologically. You can also join the truncated date to a calendar dimension table to fill in months with zero activity.
The practical reporting pattern: SELECT DATE_TRUNC('month', order_date)::DATE AS month, SUM(revenue) AS monthly_revenue FROM orders GROUP BY DATE_TRUNC('month', order_date) ORDER BY month. The ::DATE cast converts the TIMESTAMP result of DATE_TRUNC to a clean DATE for display. This pattern — DATE_TRUNC in both SELECT and GROUP BY — is the correct, idiomatic time-series aggregation in PostgreSQL and DuckDB. In MySQL, use DATE_FORMAT(date, '%Y-%m-01') to achieve the same effect: '%Y-%m-01' formats the date as the first day of its month, grouping all days in the same month to the same string value.
The foundational rule: store all timestamps in UTC. Use the TIMESTAMPTZ (timestamp with time zone) column type in PostgreSQL. When data is inserted, the database converts the local time to UTC for storage. When data is read, it can be converted back to any local timezone for display. Storing in UTC means the stored values are unambiguous — there is no confusion about which timezone an event occurred in, and daylight saving time transitions do not create duplicates or gaps.
To convert for display: SELECT created_at AT TIME ZONE 'America/Los_Angeles' AS pst_time FROM events. This converts the UTC-stored timestamp to Pacific time for display without modifying the stored value. In DuckDB: timezone('America/Los_Angeles', created_at). The AT TIME ZONE expression handles DST transitions automatically when using named timezone identifiers ('America/Los_Angeles') rather than fixed offsets ('-08:00') — named identifiers know about DST rules, fixed offsets do not.
For filtering: never apply timezone conversion to the stored column in WHERE — this prevents index usage. Instead, convert the filter boundary values to UTC: WHERE created_at >= '2024-01-15 00:00:00-08:00' AND created_at < '2024-01-16 00:00:00-08:00'. The database converts the literal values to UTC internally and compares against the UTC-stored timestamps — the index can be used for a range scan. For reports aggregated by local day: convert to local timezone first (in a CTE or derived table), then truncate and group. A platform operating out of a single US timezone can simplify this handling somewhat — the real complexity arises in platforms serving users across multiple timezones and DST rules, which is the norm for any US-wide platform.
// Part 14
Errors You Will Hit — And Exactly Why They Happen
🎯 Key Takeaways
- ✓SQL has four temporal types: DATE (date only), TIME (time only), TIMESTAMP (date+time, no timezone), TIMESTAMPTZ (date+time+timezone). Use TIMESTAMPTZ for audit trails and multi-region event logging.
- ✓CURRENT_DATE gives today's date. NOW() / CURRENT_TIMESTAMP gives the current date and time. Use CURRENT_DATE for date comparisons, NOW() for timestamps.
- ✓EXTRACT(part FROM date) returns a single numeric component. Use it to filter by month number or day-of-week — not for time-series grouping.
- ✓DATE_TRUNC('period', date) rounds down to the period start and preserves the year. Always use DATE_TRUNC for time-series GROUP BY — EXTRACT loses the year, collapsing different years into the same group.
- ✓Date arithmetic: date + INTERVAL 'n days' adds time. date1 - date2 returns integer days (PostgreSQL/DuckDB). INTERVAL '1 month' is one calendar month, not 30 days.
- ✓AGE(end_date, start_date) returns a human-readable interval in years, months, days. Always put the later date first to get a positive result.
- ✓strftime(date, format) in DuckDB and TO_CHAR(date, format) in PostgreSQL format dates as strings for display. ISO format codes (%Y-%m-%d) are locale-independent.
- ✓Never apply functions to indexed date columns in WHERE — WHERE EXTRACT(YEAR FROM date) = 2024 prevents index use. Use range comparisons: WHERE date >= '2024-01-01' AND date < '2025-01-01'.
- ✓Store timestamps in UTC using TIMESTAMPTZ. Convert to local timezone at display time only (AT TIME ZONE 'America/Los_Angeles'). Convert filter boundaries to UTC rather than applying timezone functions to stored columns.
- ✓For rolling windows: combine DATE_TRUNC for period grouping with LAG() window function for period-over-period comparisons. The date arithmetic gives the offset; the window function gives the prior period value.
What comes next
In Lesson 43, you learn math and numeric functions — ROUND, FLOOR, CEIL, ABS, MOD, POWER, SQRT, LOG, and every numeric operation for financial calculations, statistical summaries, and data transformation.
Lesson 43 → Math FunctionsQuick check
Why is grouping by EXTRACT(MONTH FROM order_date) risky for multi-year data?
HELP US IMPROVE
How was this lesson?
Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.
Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.
