SQL Date Functions Guide: Practical Examples (2026)
Master sql date functions with practical examples. Learn DATE_TRUNC, EXTRACT, interval rolling windows, and how to avoid the timestamp BETWEEN trap.
Mastering sql date functions is essential for tracking time-series metrics, calculating month-over-month growth, and building accurate retention cohorts without silent timestamp bugs. Whether aggregating revenue by month using DATE_TRUNC or isolating seasonal cycles with EXTRACT, applying correct date logic ensures analytical integrity.
This sql date functions practical guide provides production-tested sql date functions examples to teach you how to use sql date functions effectively. We examine real-world queries against our verified 2,000-order e-commerce database, demonstrate interval math, avoid the silent BETWEEN date trap, and build reproducible cohort queries. For complementary SQL techniques, explore our SQL CTE Guide, SQL Window Functions Guide, and execution logic in WHERE vs HAVING in SQL.
Quick Dialect Syntax Matrix
| Operation | PostgreSQL / Redshift | Snowflake | BigQuery | MySQL |
|---|---|---|---|---|
| Truncate Month | DATE_TRUNC('month', dt) | DATE_TRUNC('month', dt) | DATE_TRUNC(dt, MONTH) | DATE_FORMAT(dt, '%Y-%m-01') |
| Extract Year | EXTRACT(YEAR FROM dt) | EXTRACT(YEAR FROM dt) | EXTRACT(YEAR FROM dt) | YEAR(dt) |
| Rolling 30 Days | dt + INTERVAL '30 days' | DATEADD('day', 30, dt) | DATE_ADD(dt, INTERVAL 30 DAY) | DATE_ADD(dt, INTERVAL 30 DAY) |
1. EXTRACT: Isolating Date Components for Seasonality
The EXTRACT() function pulls out a specific numeric piece (year, month, day of week, hour) from a date or timestamp.
Syntax
EXTRACT(field FROM source)Orders Per Calendar Year
Let's group our 2,000 orders by year:
SELECT EXTRACT(YEAR FROM order_date)::int AS order_year,
COUNT(*) AS total_orders
FROM orders
GROUP BY order_year
ORDER BY order_year;The Output
| order_year | total_orders |
|---|---|
| 2022 | 625 |
| 2023 | 563 |
| 2024 | 549 |
| 2025 | 263 |
Notice that 2025 has fewer orders (263) because the dataset ends on June 1, 2025.
Analyzing Day-of-Week Seasonality
EXTRACT(DOW FROM order_date) returns the day of the week as an integer (0 for Sunday, 6 for Saturday):
SELECT EXTRACT(DOW FROM order_date)::int AS day_of_week,
COUNT(*) AS order_volume
FROM orders
WHERE status = 'completed'
GROUP BY day_of_week
ORDER BY day_of_week;EXTRACT is perfect for answering questions like: "Which day of the week has the highest purchase volume across all historical data?" However, because it discards the year, you cannot use EXTRACT alone to build a chronological timeline.
2. DATE_TRUNC: Building Continuous Trendlines
To track monthly or weekly trends over time, you must keep the year and month bound together. DATE_TRUNC() rounds a timestamp down to the beginning of the specified interval.
Truncation Examples
DATE_TRUNC('month', '2024-03-24 15:30:00'::timestamp)→2024-03-01 00:00:00DATE_TRUNC('year', '2024-03-24 15:30:00'::timestamp)→2024-01-01 00:00:00DATE_TRUNC('day', '2024-03-24 15:30:00'::timestamp)→2024-03-24 00:00:00
Monthly Revenue Timeline Query
SELECT DATE_TRUNC('month', order_date)::date AS order_month,
COUNT(*) AS completed_orders,
ROUND(SUM(total_amount), 2) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY order_month
ORDER BY order_month;The Output (First 4 Months)
| order_month | completed_orders | monthly_revenue |
|---|---|---|
| 2022-01-01 | 26 | ₹13,675.76 |
| 2022-02-01 | 24 | ₹10,370.43 |
| 2022-03-01 | 31 | ₹15,820.10 |
| 2022-04-01 | 28 | ₹12,190.50 |
Why We Add ::date
DATE_TRUNC returns a TIMESTAMP with 00:00:00. Appending ::date in PostgreSQL strips off the unnecessary midnight timestamp, returning a clean YYYY-MM-DD date formatted for reporting.
Trap vs. Fix: Multi-Year Seasonality Collapse
A frequent analytical pitfall is grouping multi-year transactions by EXTRACT(MONTH FROM col) when attempting to report business trendlines.
⚠️ The Seasonality Trap Query
-- TRAP: Groups all Januarys (2022, 2023, 2024, 2025) into a single bucket
SELECT EXTRACT(MONTH FROM order_date)::int AS month_num,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS gross_revenue
FROM orders
WHERE status = 'completed'
GROUP BY month_num
ORDER BY month_num;Sandbox Output: 12 Rows (Multi-Year Data Merged)
| month_num | order_count | gross_revenue |
|---|---|---|
| 1 | 172 | ₹89,450.20 |
| 2 | 158 | ₹82,110.40 |
| 3 | 165 | ₹86,340.10 |
This query collapses four distinct years into 12 rows, completely masking growth or churn between 2022 and 2025.
✅ The Continuous Timeline Fix Query
-- FIX: Preserves chronological order across 42 distinct calendar months
SELECT DATE_TRUNC('month', order_date)::date AS order_month,
COUNT(*) AS order_count,
ROUND(SUM(total_amount), 2) AS gross_revenue
FROM orders
WHERE status = 'completed'
GROUP BY order_month
ORDER BY order_month;Sandbox Output: 42 Distinct Monthly Cohorts
| order_month | order_count | gross_revenue |
|---|---|---|
| 2022-01-01 | 26 | ₹13,675.76 |
| 2022-02-01 | 24 | ₹10,370.43 |
| 2022-03-01 | 31 | ₹15,820.10 |
Grouping by DATE_TRUNC('month', order_date)::date correctly preserves each calendar month across multi-year history for accurate trend charts.
3. The BETWEEN Trap on Dates: Why Production Queries Drop Data
Filtering date ranges is where many analysts accidentally lose data. Look at this query designed to pull all orders in January 2024:
-- ⚠️ THE TEMPTING (BUT DANGEROUS) WAY
SELECT COUNT(*) AS total_orders
FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';Result in our dataset: 47 orders.
Now look at the safe half-open range:
-- ✅ THE BULLETPROOF WAY: Half-Open Range
SELECT COUNT(*) AS total_orders
FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2024-02-01';Result in our dataset: 47 orders.
Why They Match Here — And Why It Breaks in Production
In our synthetic educational dataset, every order_date is stored exactly at midnight (00:00:00). Therefore, the 31st at midnight matches both queries.
However, in production systems, timestamps contain hours, minutes, and seconds (e.g. 2024-01-31 16:45:12).
BETWEEN '2024-01-01' AND '2024-01-31'
is expanded by SQL to:
>= '2024-01-01 00:00:00' AND <= '2024-01-31 00:00:00'
Any order placed at 10:00 AM on January 31st is strictly greater than 2024-01-31 00:00:00. BETWEEN silently drops the entire last day of the month without any warning or error.
Always Use Half-Open Ranges
For date ranges on timestamps, always write:
WHERE timestamp_col >= '2024-01-01' AND timestamp_col < '2024-02-01'
Never use BETWEEN for dates.
Master Production SQL & Time-Series Modeling
Practice date filtering, rolling intervals, and window functions with interactive database drills in our full career track.
Explore Data Analyst Track4. Date Math with INTERVAL
SQL allows dynamic arithmetic using the INTERVAL keyword, enabling flexible rolling windows without hardcoded dates.
Rolling 90-Day Window Filter
SELECT COUNT(*) AS orders_last_90d,
ROUND(SUM(total_amount), 2) AS revenue_last_90d
FROM orders
WHERE order_date >= DATE '2025-06-01' - INTERVAL '90 days'
AND order_date < DATE '2025-06-01'
AND status = 'completed';In a production dashboard, replace the static '2025-06-01' with CURRENT_DATE:
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
AND order_date < CURRENT_DATECommon INTERVAL Expressions
INTERVAL '7 days'/INTERVAL '1 week'INTERVAL '1 month'/INTERVAL '3 months'INTERVAL '1 year'INTERVAL '2 hours 30 minutes'
5. Month-over-Month (MoM) Growth Analysis
Combining DATE_TRUNC with the LAG() window function allows you to calculate month-over-month growth in a clean, reproducible query.
WITH monthly_metrics AS (
SELECT DATE_TRUNC('month', order_date)::date AS order_month,
ROUND(SUM(total_amount), 2) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY order_month
)
SELECT order_month,
revenue,
LAG(revenue, 1) OVER (ORDER BY order_month) AS prev_month_revenue,
ROUND(revenue - LAG(revenue, 1) OVER (ORDER BY order_month), 2) AS mom_change,
ROUND(
((revenue - LAG(revenue, 1) OVER (ORDER BY order_month)) /
NULLIF(LAG(revenue, 1) OVER (ORDER BY order_month), 0)) * 100,
1
) AS mom_growth_pct
FROM monthly_metrics
ORDER BY order_month;The Output (First 3 Months)
| order_month | revenue | prev_month_revenue | mom_change | mom_growth_pct |
|---|---|---|---|---|
| 2022-01-01 | ₹13,675.76 | NULL | NULL | NULL |
| 2022-02-01 | ₹10,370.43 | ₹13,675.76 | -₹3,305.33 | -24.2% |
| 2022-03-01 | ₹15,820.10 | ₹10,370.43 | +₹5,449.67 | +52.5% |
6. Cohort Analysis Foundation: First-Order Month
In product analytics, cohort retention tracks groups of customers who signed up or made their first purchase in the same month.
Finding each customer's cohort acquisition month requires MIN(DATE_TRUNC('month', order_date)):
SELECT customer_id,
MIN(DATE_TRUNC('month', order_date))::date AS first_order_month
FROM orders
WHERE customer_id IS NOT NULL
GROUP BY customer_id
ORDER BY first_order_month, customer_id
LIMIT 10;In our dataset, the first-order cohorts start in January 2022 with 47 customers, followed by 43 customers in February 2022, forming the baseline for cohort retention curves.
7. Date Function Decision Guide
| Feature / Criteria |
|---|
8. Defensive Coding Best Practices for SQL Dates
When writing production SQL date queries, adhere to these five defensive engineering rules:
- Always Use Half-Open Ranges for Timestamps: Replace
BETWEEN '2024-01-01' AND '2024-01-31'with>= '2024-01-01' AND < '2024-02-01'. This eliminates boundary cutoff bugs regardless of millisecond precision. - Never Group by EXTRACT for Trends: Use
DATE_TRUNC('month', col)::datefor continuous trendlines. ReserveEXTRACT(MONTH FROM col)exclusively for multi-year seasonality indexes. - Explicitly Cast Truncations with
::date: In PostgreSQL,DATE_TRUNCreturns a timestamp with00:00:00. Appending::datestrips the time component and ensures clean joins and BI dashboard formatting. - Standardize on UTC with Explicit Conversion: Store all event records in
TIMESTAMPTZ(UTC). Convert timestamps to business timezones at report runtime usingcol AT TIME ZONE 'America/New_York'. - Enforce ISO 8601 String Literals: Always write string dates as
'YYYY-MM-DD'or'YYYY-MM-DD HH24:MI:SS'to avoid locale-specific parsing errors (MM/DD/YYYYvsDD/MM/YYYY).
9. Summary & Hands-on Practice
- Use DATE_TRUNC() when preserving continuous timelines for charts, dashboards, and MoM calculations.
- Use EXTRACT() when isolating seasonal cycles across multiple years.
- Avoid BETWEEN for timestamp filtering—always use
>= start AND < next_periodto prevent losing the final day's transactions. - Cast truncated timestamps with
::datefor clean, standards-compliant date outputs.
Practice live date queries, MoM growth calculations, and cohort analyses in the Interactive SQL Practice Sandbox or build end-to-end data pipelines in our Data Analyst Career Track. To master query structure and readability, explore our SQL CTE Guide.
Practice SQL Date & Time Queries Live
Master DATE_TRUNC, EXTRACT, and rolling intervals with hands-on practice problems in our browser-based PostgreSQL sandbox.
Practice Date Functions FreeFrequently Asked Questions
What is the difference between DATE_TRUNC and EXTRACT in SQL?
DATE_TRUNC rounds a timestamp down to the start of a specified interval (e.g., month start 2024-03-01 00:00:00), preserving chronological timeline for trend analysis. EXTRACT pulls a single numeric component (e.g., month number 3 or year 2024), pooling all years together for seasonality analysis.
Why is BETWEEN dangerous for filtering timestamp ranges?
BETWEEN '2024-01-01' AND '2024-01-31' evaluates up to '2024-01-31 00:00:00'. Any transaction occurring after midnight on January 31st (e.g. 2024-01-31 14:30:00) is silently omitted. Always use the half-open range >= '2024-01-01' AND < '2024-02-01'.
Why does DATE_TRUNC need a ::date cast in PostgreSQL?
DATE_TRUNC returns a TIMESTAMP with time set to 00:00:00 (e.g., 2022-01-01 00:00:00). Appending ::date casts the result to a clean DATE type (2022-01-01), which formats cleaner in tables and BI exports.
How do you calculate rolling 30, 60, or 90 day windows in SQL?
Use INTERVAL arithmetic: WHERE order_date >= CURRENT_DATE - INTERVAL '90 days' AND order_date < CURRENT_DATE. This creates dynamic, rolling date windows without manual date recalculation.
How do you handle timezones when querying timestamps across regions?
Store timestamps in UTC using TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ), and convert to the reporting timezone at query time using the AT TIME ZONE operator (e.g., order_date AT TIME ZONE 'America/New_York').
What are the most common SQL date functions used in data analysis?
The most essential SQL date functions are DATE_TRUNC for timeline bucketing, EXTRACT for isolating calendar parts like year or day of week, INTERVAL arithmetic for rolling time windows, and TO_CHAR or DATE_FORMAT for string display.

Written by
Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.
Related Articles
Window Functions in SQL: Practical Guide & Examples
Master window functions in sql with practical examples. Learn OVER, PARTITION BY, running totals, rankings, and lead lag calculations step by step.
Understanding Null Semantics In Sql: 2026 Guide & Examples
Master three-valued logic and traps by understanding null semantics in sql. Learn NOT IN pitfalls, WHERE vs HAVING filtering, and safe COALESCE math.
Full Outer Join In Sql: 2026 Guide & Examples
Master the full outer join in sql with practical examples, billing reconciliation queries, syntax rules, and NULL handling for data analysts.