SQL Date Functions: Syntax, Examples & Practice
SQL date functions let a query work with dates. Use them to get today's date, pull the year or month out of a date, add or subtract days, and count the days between two dates. The function names differ by database, so every example is labelled with the one it runs on.
What Are SQL Date Functions?
A date function takes a date and returns something useful about it: its year, the date 30 days later, the number of days since it happened. Here are two of them on real data — the year of each order, and the date 30 days after it:
SELECT id, order_date,
strftime('%Y', order_date) AS order_year,
date(order_date, '+30 days') AS plus_30_days
FROM orders
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| id | order_date | order_year | plus_30_days |
|---|---|---|---|
| 1 | 2024-04-24 | 2024 | 2024-05-24 |
| 2 | 2024-03-15 | 2024 | 2024-04-14 |
| 3 | 2023-02-15 | 2023 | 2023-03-17 |
| 4 | 2022-01-10 | 2022 | 2022-02-09 |
| 5 | 2023-06-16 | 2023 | 2023-07-16 |
For every order, strftime('%Y', …) extracts the year and date(…, '+30 days') returns the date 30 days later. The second function does real calendar arithmetic: 30 days after 15 March is 14 April, and 30 days after 15 February 2023 is 17 March, because February has 28 days. That query is written for SQLite. In SQL Server the same two columns are YEAR(order_date) and DATEADD(day, 30, order_date); the comparison table below gives the MySQL and PostgreSQL versions too.
SQL Date Functions Syntax in MySQL, PostgreSQL, SQL Server and SQLite
Every operation this lesson covers, with the function each database uses for it. d is a date column; a is the earlier date and b the later one.
| Operation | SQLite | MySQL | PostgreSQL | SQL Server |
|---|---|---|---|---|
| Current date | date('now') | CURDATE() | CURRENT_DATE | CAST(GETDATE() AS date) |
| Current date and time | datetime('now') | NOW() | NOW() | GETDATE() |
| Extract the year | strftime('%Y', d) | YEAR(d) | EXTRACT(YEAR FROM d) | YEAR(d) |
| Extract the month | strftime('%m', d) | MONTH(d) | EXTRACT(MONTH FROM d) | MONTH(d) |
| Add 30 days | date(d, '+30 days') | DATE_ADD(d, INTERVAL 30 DAY) | d + INTERVAL '30 days' | DATEADD(day, 30, d) |
| Days between two dates | julianday(b) - julianday(a) | DATEDIFF(b, a) | b - a | DATEDIFF(day, a, b) |
| Format as DD-MM-YYYY | strftime('%d-%m-%Y', d) | DATE_FORMAT(d, '%d-%m-%Y') | TO_CHAR(d, 'DD-MM-YYYY') | FORMAT(d, 'dd-MM-yyyy') |
| First day of the month | date(d, 'start of month') | DATE_FORMAT(d, '%Y-%m-01') | DATE_TRUNC('month', d) | DATEFROMPARTS(YEAR(d), MONTH(d), 1) |
EXTRACT and TO_CHAR, and uses SYSDATE and ADD_MONTHS().What Date Functions Do
Date functions are the part of SQL that understands calendars. A column such as order_date is only a value until you ask a question about it — which year? how long ago? what is the date 30 days later? — and date functions answer those questions inside the query. They cover five everyday operations:
- Get the current date or timestamp — today's date, or the date and time right now.
- Extract date parts — pull the year, month, day or weekday out of a date.
- Date arithmetic — add or subtract days, months or years.
- Date difference — count the days, months or years between two dates.
- Date filtering, grouping and formatting — select a date range, build monthly reports, change how a date is displayed.
Basic Date Function Syntax
A date function takes a date (and usually an instruction) and returns a new value:
function_name(date_value, instruction)
strftime('%Y', order_date) -- extract: the year of order_date
date(order_date, '+30 days') -- arithmetic: 30 days after order_date
The Four SQLite Date Functions
The SQLabHub Playground runs SQLite, which does everything with four functions and a list of modifiers:
| Function | Returns | Example | Result |
|---|---|---|---|
date(value, modifiers…) | A date as YYYY-MM-DD | date('2024-04-24', '+1 month') | 2024-05-24 |
datetime(value, modifiers…) | A date and time | datetime('2024-04-24 14:30:25', '+2 hours') | 2024-04-24 16:30:25 |
strftime(format, value) | Any part or format, as text | strftime('%Y-%m', '2024-04-24') | 2024-04 |
julianday(value) | A day number, for subtraction | julianday('2024-04-24') - julianday('2024-04-01') | 23.0 |
Modifiers are short strings such as '+30 days', '-1 year', 'start of month' and 'localtime'. Several can be chained and they are applied left to right.
Date and Time Data Types
Before using date functions, know what kind of value you are working with.
| Type | Stores | Example |
|---|---|---|
| DATE | Calendar date only — no time component | 2026-07-12 |
| DATETIME | Date plus a time of day, down to the second | 2026-07-12 14:30:25 |
| TIMESTAMP | Date and time, often time-zone aware, typically used for "record created" columns | 2026-07-12 14:30:25 UTC |
| TIME | Time of day only — no date at all | 14:30:25 |
CREATE TABLE deliveries (
order_id INT PRIMARY KEY,
order_date DATE,
placed_at TIMESTAMP,
delivery_date DATE
);
'2024-04-24') and the date functions interpret that text. This is why every date in the Playground looks like YYYY-MM-DD, and why that exact, zero-padded format matters for comparisons and sorting.'12/07/2026', 'July 12' — is one of the most common causes of broken date logic. See SQL data types for choosing column types, and the data cleaning section below for fixing it.Current Date and Time
The anchor for every "today", "this month" and "last 30 days" query.
SQL CURRENT_DATE
Definition — CURRENT_DATE
CURRENT_DATE returns today's calendar date only — no hours, minutes or seconds. It is a standard SQL keyword (written without parentheses) available in PostgreSQL, MySQL, SQLite and Oracle. SQL Server only added it in SQL Server 2025 (and Azure SQL Database); on SQL Server 2022 and earlier use CAST(GETDATE() AS date), which works on every version.
SELECT CURRENT_DATE;
Daily reports
Comparing order_date with today's date in a WHERE clause selects exactly today's orders.
Age and tenure calculations
The current date is the anchor point for every "how long since" calculation.
Renewal reminders
Comparing a renewal_date with today and with today plus seven days flags upcoming renewals.
SQL NOW() and CURRENT_TIMESTAMP
Definition — NOW()
NOW() returns the current date and time together, including hours, minutes and seconds. It exists in MySQL and PostgreSQL. The standard keyword is CURRENT_TIMESTAMP, SQL Server uses GETDATE(), and SQLite uses datetime('now').
SELECT NOW();
| Aspect | CURRENT_DATE | NOW() |
|---|---|---|
| Returns | Date only | Date + time |
| Example | 2026-07-12 | 2026-07-12 14:30:25 |
| Typical use | Filtering "today", age and tenure maths | Audit logs, created_at timestamps, precise event ordering |
Current Date in the Playground (SQLite)
SELECT date('now') AS today_utc,
datetime('now') AS right_now_utc,
datetime('now', 'localtime') AS right_now_local;
▶ Run this query in the SQL Playground
The result changes every time you run it, so no fixed output is shown here. Note the column names: SQLite's 'now' — and its CURRENT_DATE — are in UTC. Add the 'localtime' modifier to convert to the machine's local time zone.
Extracting Date Parts
Pulling the year, month or day out of a date — the first step of almost every report.
SQL YEAR(), MONTH() and DAY()
Definition — YEAR, MONTH and DAY
YEAR(date) returns the four-digit year, MONTH(date) the month number from 1 to 12, and DAY(date) the day of the month from 1 to 31. These three functions exist in MySQL and SQL Server.
SELECT order_date,
YEAR(order_date) AS yr,
MONTH(order_date) AS mo,
DAY(order_date) AS dy
FROM orders
ORDER BY id;
| order_date | yr | mo | dy |
|---|---|---|---|
| 2024-04-24 | 2024 | 4 | 24 |
| 2024-03-15 | 2024 | 3 | 15 |
| 2023-02-15 | 2023 | 2 | 15 |
| 2022-01-10 | 2022 | 1 | 10 |
| Function | Returns | Example (2026-07-12) | Typical use |
|---|---|---|---|
| YEAR() | Four-digit year | 2026 | Annual reports, year-over-year comparisons |
| MONTH() | Month number 1–12 | 7 | Monthly reports, seasonality |
| DAY() | Day of month 1–31 | 12 | Building custom date ranges |
DAY() on its own is rare.
SQL EXTRACT()
Definition — EXTRACT
EXTRACT(part FROM date) returns one component of a date or time value — YEAR, MONTH, DAY, HOUR and more. It is the standard SQL syntax, supported by PostgreSQL, MySQL and Oracle.
SELECT id, order_date,
EXTRACT(YEAR FROM order_date) AS order_year,
EXTRACT(MONTH FROM order_date) AS order_month
FROM orders;
| Part of the call | What it does |
|---|---|
EXTRACT(...) | The function call itself |
YEAR | The date part being requested — could also be MONTH, DAY, HOUR and so on |
FROM order_date | The source column to extract the part from |
strftime() in SQLite
SQLite has neither YEAR() nor EXTRACT(). It uses strftime(format, date), where the format string names the part you want:
SELECT id, order_date,
strftime('%Y', order_date) AS order_year,
strftime('%m', order_date) AS order_month,
strftime('%d', order_date) AS order_day
FROM orders
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| id | order_date | order_year | order_month | order_day |
|---|---|---|---|---|
| 1 | 2024-04-24 | 2024 | 04 | 24 |
| 2 | 2024-03-15 | 2024 | 03 | 15 |
| 3 | 2023-02-15 | 2023 | 02 | 15 |
| 4 | 2022-01-10 | 2022 | 01 | 10 |
| 5 | 2023-06-16 | 2023 | 06 | 16 |
strftime() returns text, not a number. WHERE strftime('%Y', order_date) = 2024 matches nothing in the Playground; = '2024' (with quotes) matches 66 orders. To get a number, wrap it: CAST(strftime('%m', order_date) AS INTEGER).EXTRACT vs YEAR / MONTH / DAY: Which Database Supports What
| Database | YEAR() / MONTH() / DAY() | EXTRACT() | Other |
|---|---|---|---|
| MySQL | Yes | Yes | — |
| SQL Server | Yes | No | DATEPART(year, d) |
| PostgreSQL | No | Yes | date_part('year', d) |
| Oracle | No | Yes | — |
| SQLite | No | No | strftime('%Y', d) |
EXTRACT() for PostgreSQL will not run on SQL Server, and YEAR() will not run on PostgreSQL. The full comparison is in SQL Date Functions by Dialect.Date Arithmetic: Adding and Subtracting Time
Shifting a date forward or backward — expiry dates, deadlines, "30 days ago".
SQL DATEADD() — SQL Server
Definition — DATEADD
DATEADD(unit, number, date) adds number units of time (day, month, year, hour…) to date. A negative number subtracts instead. DATEADD is a SQL Server function.
SELECT id, order_date,
DATEADD(day, 30, order_date) AS thirty_days_later
FROM orders
ORDER BY id;
| id | order_date | thirty_days_later |
|---|---|---|
| 1 | 2024-04-24 | 2024-05-24 |
| 2 | 2024-03-15 | 2024-04-14 |
| 3 | 2023-02-15 | 2023-03-17 |
| 4 | 2022-01-10 | 2022-02-09 |
plus_30_days in the SQLite example below| Argument | Meaning | Value in example |
|---|---|---|
| unit | The interval type — day, month, year, hour | day |
| number | How many units to add (negative to subtract) | 30 |
| date | The starting date | order_date |
SELECT DATEADD(month, 6, start_date) AS six_months_later,
DATEADD(year, 1, start_date) AS one_year_later
FROM subscriptions;
DATEADD(day, -30, GETDATE()) means "30 days ago", the building block behind every "last 30 days" report.DATE_ADD() in MySQL and INTERVAL in PostgreSQL
SELECT DATE_ADD(order_date, INTERVAL 30 DAY) AS renewal
FROM orders;
SELECT order_date + INTERVAL '30 days' AS renewal
FROM orders;
MySQL also has DATE_SUB() for going backward. In PostgreSQL, adding a whole number to a date (order_date + 30) adds that many days and returns a date; adding an INTERVAL returns a timestamp.
Date Modifiers in SQLite
SELECT id, order_date,
date(order_date, '+30 days') AS plus_30_days,
date(order_date, '+1 month') AS plus_1_month,
date(order_date, '-1 year') AS minus_1_year
FROM orders
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| id | order_date | plus_30_days | plus_1_month | minus_1_year |
|---|---|---|---|---|
| 1 | 2024-04-24 | 2024-05-24 | 2024-05-24 | 2023-04-24 |
| 2 | 2024-03-15 | 2024-04-14 | 2024-04-15 | 2023-03-15 |
| 3 | 2023-02-15 | 2023-03-17 | 2023-03-15 | 2022-02-15 |
| 4 | 2022-01-10 | 2022-02-09 | 2022-02-10 | 2021-01-10 |
| 5 | 2023-06-16 | 2023-07-16 | 2023-07-16 | 2022-06-16 |
"30 days" and "1 month" are not the same thing: for the order on 15 March, 30 days later is 14 April but one month later is 15 April.
date('2024-01-31', '+1 month') returns 2024-03-02.DATEADD Business Examples
Five real scenarios that are all the same pattern underneath. These use SQL Server syntax on illustrative tables.
SELECT customer, plan, start_date,
DATEADD(month, 12, start_date) AS expiry_date
FROM subscriptions;
| customer | plan | start_date | expiry_date |
|---|---|---|---|
| Neha Kapoor | Pro | 2026-01-10 | 2027-01-10 |
| Vikram Singh | Basic | 2026-02-15 | 2027-02-15 |
| Aditi Sharma | Enterprise | 2025-08-01 | 2026-08-01 |
| Rohan Mehta | Pro | 2026-06-20 | 2027-06-20 |
SELECT id, order_date,
DATEADD(day, 6, order_date) AS estimated_delivery
FROM orders;
SELECT customer, renewal_date
FROM subscriptions
WHERE renewal_date <= DATEADD(day, 7, CAST(GETDATE() AS date));
SELECT id, order_date,
DATEADD(year, 1, order_date) AS warranty_expiry
FROM orders;
SELECT first_name, last_name, hire_date,
DATEADD(day, 90, hire_date) AS probation_end
FROM employees;
date(hire_date, '+90 days') on the real employees table.
Calculating Date Differences
Measuring the gap between two dates — one of the most common SQL interview topics.
SQL DATEDIFF() — SQL Server
Definition — DATEDIFF
DATEDIFF(unit, start_date, end_date) returns the difference between two dates in the given unit — most often days. It computes end_date minus start_date. This three-argument form is SQL Server syntax.
SELECT order_id, order_date, delivery_date,
DATEDIFF(day, order_date, delivery_date) AS delivery_days
FROM deliveries;
| order_id | order_date | delivery_date | delivery_days |
|---|---|---|---|
| 101 | 2026-01-05 | 2026-01-09 | 4 |
| 102 | 2026-02-14 | 2026-02-20 | 6 |
| 103 | 2026-03-02 | 2026-03-07 | 5 |
| 104 | 2026-04-18 | 2026-04-25 | 7 |
| 105 | 2026-05-09 | 2026-05-14 | 5 |
deliveries table (defined in the data types section), not a Playground table. The Playground's shipped_date values are random test data, so they are not used for delivery times.| Argument | Meaning | Value in example |
|---|---|---|
| unit | The unit of the result — day, month, year | day |
| start_date | The earlier date | order_date |
| end_date | The later date | delivery_date |
DATEDIFF(day, delivery_date, order_date) flips the sign and produces a negative result. Always pass the earlier date first.DATEDIFF(year, '2025-12-31', '2026-01-01') is 1, even though only one day has passed.DATEDIFF in MySQL and Date Subtraction in PostgreSQL
-- end date first, result always in days
SELECT DATEDIFF(delivery_date, order_date) AS delivery_days
FROM deliveries;
-- subtracting two dates gives days
SELECT delivery_date - order_date AS delivery_days
FROM deliveries;
MySQL's DATEDIFF has the same name as SQL Server's but a different signature: two arguments, end date first, days only. For months or years in MySQL use TIMESTAMPDIFF(MONTH, start_date, end_date).
Date Differences in SQLite: julianday()
julianday() converts a date into a continuous day number, so subtracting two of them gives the days between the dates. This example measures each employee's tenure as of 1 January 2025:
SELECT id, first_name, hire_date,
CAST(julianday('2025-01-01') - julianday(hire_date) AS INTEGER) AS tenure_days
FROM employees
ORDER BY id
LIMIT 5;
▶ Run this query in the SQL Playground
| id | first_name | hire_date | tenure_days |
|---|---|---|---|
| 1 | William | 2022-07-24 | 892 |
| 2 | John | 2021-08-09 | 1241 |
| 3 | Daniel | 2016-06-06 | 3131 |
| 4 | Melissa | 2022-04-13 | 994 |
| 5 | Jason | 2019-10-17 | 1903 |
William was hired on 24 July 2022, which is 892 days before 1 January 2025. Replace '2025-01-01' with 'now' to measure against today instead.
DATEDIFF Practical Examples
Five business questions built on a date difference. SQL Server syntax on illustrative tables; GETDATE() is the current date and time.
SELECT user_id, MAX(order_date) AS last_order,
DATEDIFF(day, MAX(order_date), GETDATE()) AS days_since_last_order
FROM orders
GROUP BY user_id;
SELECT first_name, last_name, hire_date,
DATEDIFF(day, hire_date, GETDATE()) AS tenure_days
FROM employees;
SELECT user_id,
DATEDIFF(day, MIN(order_date), MAX(order_date)) AS retention_days
FROM orders
GROUP BY user_id;
SELECT project_id,
DATEDIFF(day, start_date, end_date) AS project_duration_days
FROM projects;
SELECT order_id,
DATEDIFF(day, order_date, delivery_date) AS delivery_days
FROM deliveries
WHERE DATEDIFF(day, order_date, delivery_date) > 7;
Calculating Age in SQL
Customer age, employee age, years of service — all the same calculation: count the full years between a start date and today. The first two queries assume a members table with a dob (date of birth) column.
SELECT first_name, last_name, dob,
DATEDIFF(day, dob, GETDATE()) / 365.25 AS approx_age
FROM members;
SELECT first_name, last_name, dob,
DATEDIFF(year, dob, GETDATE())
- CASE WHEN DATEADD(year, DATEDIFF(year, dob, GETDATE()), dob) > GETDATE()
THEN 1 ELSE 0 END AS exact_age
FROM members;
YEAR(today) - YEAR(dob) overstates age by one whenever this year's birthday is still to come. The CASE WHEN adjustment above is the correct version. MySQL does it in one call: TIMESTAMPDIFF(YEAR, dob, CURDATE()); PostgreSQL uses EXTRACT(YEAR FROM AGE(dob)).The Playground's employees table has no date of birth, but the same "full years" logic applies to hire_date — completed years of service as of 1 January 2025:
SELECT first_name, hire_date,
CAST(strftime('%Y', '2025-01-01') AS INTEGER)
- CAST(strftime('%Y', hire_date) AS INTEGER)
- (strftime('%m-%d', '2025-01-01') < strftime('%m-%d', hire_date)) AS full_years
FROM employees
ORDER BY id
LIMIT 4;
▶ Run this query in the SQL Playground
| first_name | hire_date | full_years |
|---|---|---|
| William | 2022-07-24 | 2 |
| John | 2021-08-09 | 3 |
| Daniel | 2016-06-06 | 8 |
| Melissa | 2022-04-13 | 2 |
The comparison in the last line is 1 when the anniversary (month and day) has not been reached yet, which subtracts the incomplete year. William's 2025 − 2022 = 3 becomes 2 because 24 July comes after 1 January.
Filtering Rows by Date
Selecting a day, a month, a year or a moving window.
To filter records by date, compare the date column in a WHERE clause. The most reliable pattern is a half-open range: from the first day you want, up to but not including the first day you don't.
SELECT COUNT(*) AS orders_in_2024
FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01';
▶ Run this query in the SQL Playground
| orders_in_2024 |
|---|
| 66 |
This works unchanged in MySQL, PostgreSQL and SQL Server. It also stays correct when the column holds a time of day: BETWEEN '2024-01-01' AND '2024-12-31' would miss anything timestamped after midnight on 31 December. The SQL BETWEEN lesson explains why its upper bound is inclusive.
Filtering for One Exact Date
For a single day, compare the column with a date written as 'YYYY-MM-DD':
SELECT id, user_id, order_date, total
FROM orders
WHERE order_date = '2024-04-24'
ORDER BY id;
▶ Run this query in the SQL Playground
| id | user_id | order_date | total |
|---|---|---|---|
| 1 | 43 | 2024-04-24 | 2638.11 |
| 134 | 55 | 2024-04-24 | 2007.49 |
| 157 | 92 | 2024-04-24 | 4882 |
= only finds the whole day when the column holds a date and nothing else. If it also stores a time, such as 2024-04-24 13:23:44, the same filter returns no rows in SQLite, and in MySQL, PostgreSQL and SQL Server it matches only rows stamped exactly at midnight. Use a one-day range instead — order_date >= '2024-04-24' AND order_date < '2024-04-25' — which works for both kinds of column in every database.Filtering With a Function on the Column
SELECT *
FROM orders
WHERE YEAR(order_date) = 2026;
Rolling 30-Day Window
A rolling window moves with the calendar instead of using fixed months. In production the anchor is today; in the Playground we anchor on the newest order date, 2024-12-24:
SELECT COUNT(*) AS orders, ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE order_date > date('2024-12-24', '-30 days')
AND order_date <= '2024-12-24';
▶ Run this query in the SQL Playground
| orders | revenue |
|---|---|
| 6 | 10621.36 |
SELECT SUM(amount) AS rolling_30_day_sales
FROM sales
WHERE sale_date > DATEADD(day, -30, CAST(GETDATE() AS date))
AND sale_date <= CAST(GETDATE() AS date);
Current Month Sales (Month to Date)
Month-to-date revenue is "everything since the first day of the current month". Here the anchor is the newest order in the table, found with a subquery:
SELECT COUNT(*) AS orders, ROUND(SUM(total), 2) AS mtd_revenue
FROM orders
WHERE order_date >= date((SELECT MAX(order_date) FROM orders), 'start of month');
▶ Run this query in the SQL Playground
| orders | mtd_revenue |
|---|---|
| 6 | 10621.36 |
SELECT SUM(amount) AS mtd_revenue
FROM sales
WHERE sale_date >= DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1);
Grouping Data by Date
The monthly report — the single most common date query in business SQL.
Extract the part of the date you want to report on, then GROUP BY it and summarise each group with an aggregate function.
SELECT strftime('%Y-%m', order_date) AS month,
COUNT(*) AS orders,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE order_date >= '2024-07-01' AND order_date < '2025-01-01'
GROUP BY strftime('%Y-%m', order_date)
ORDER BY month;
▶ Run this query in the SQL Playground
| month | orders | revenue |
|---|---|---|
| 2024-07 | 6 | 10296.67 |
| 2024-08 | 8 | 25429.09 |
| 2024-09 | 1 | 4319.46 |
| 2024-10 | 3 | 7996.72 |
| 2024-11 | 6 | 17380.72 |
| 2024-12 | 6 | 10621.36 |
Each order is labelled with its year and month ('2024-07'), rows with the same label are grouped, and SUM and COUNT run once per group. Sorting with ORDER BY works because YYYY-MM text sorts chronologically.
GROUP BY strftime('%m', order_date) reports 18 orders for month 01 — that is January 2022, 2023 and 2024 added together. Always include the year.SELECT YEAR(sale_date) AS yr, MONTH(sale_date) AS mo, SUM(amount) AS total_sales
FROM sales
GROUP BY YEAR(sale_date), MONTH(sale_date)
ORDER BY yr, mo;
Date Functions with Aggregate Functions
SELECT strftime('%Y', order_date) AS year,
COUNT(*) AS orders,
ROUND(SUM(total), 2) AS revenue
FROM orders
GROUP BY strftime('%Y', order_date)
ORDER BY year;
▶ Run this query in the SQL Playground
| year | orders | revenue |
|---|---|---|
| 2022 | 62 | 165062.86 |
| 2023 | 72 | 184615.11 |
| 2024 | 66 | 161292.74 |
Month-over-Month (MoM) Analysis
MoM compares each month with the one before it. Build the monthly totals first, then read the previous row with the window function LAG(), covered step by step in SQL LAG and LEAD:
SELECT month, revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month
FROM (
SELECT strftime('%Y-%m', order_date) AS month,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE order_date >= '2024-09-01'
GROUP BY strftime('%Y-%m', order_date)
) AS monthly;
▶ Run this query in the SQL Playground
| month | revenue | prev_month |
|---|---|---|
| 2024-09 | 4319.46 | NULL |
| 2024-10 | 7996.72 | 4319.46 |
| 2024-11 | 17380.72 | 7996.72 |
| 2024-12 | 10621.36 | 17380.72 |
Year-over-Year (YoY) Analysis
YoY compares a period with the same period one year earlier, which removes seasonal effects. Conditional aggregation puts both numbers in one row:
SELECT ROUND(SUM(CASE WHEN strftime('%Y-%m', order_date) = '2024-12' THEN total ELSE 0 END), 2) AS dec_2024,
ROUND(SUM(CASE WHEN strftime('%Y-%m', order_date) = '2023-12' THEN total ELSE 0 END), 2) AS dec_2023
FROM orders;
▶ Run this query in the SQL Playground
| dec_2024 | dec_2023 |
|---|---|
| 10621.36 | 29761.96 |
SELECT
SUM(CASE WHEN YEAR(sale_date) = 2026 AND MONTH(sale_date) = 7 THEN amount ELSE 0 END) AS july_2026,
SUM(CASE WHEN YEAR(sale_date) = 2025 AND MONTH(sale_date) = 7 THEN amount ELSE 0 END) AS july_2025
FROM sales;
SUM(CASE WHEN …) expressions compared with each other.
Formatting Dates
Presenting a date the way a report or region expects to see it.
| Format | Example | Common region |
|---|---|---|
| YYYY-MM-DD | 2026-07-12 | ISO 8601 — unambiguous, safest for storage |
| DD-MM-YYYY | 12-07-2026 | India, UK, most of Europe |
| MM-DD-YYYY | 07-12-2026 | United States |
SELECT id, order_date,
strftime('%d-%m-%Y', order_date) AS dd_mm_yyyy,
strftime('%m/%d/%Y', order_date) AS us_style
FROM orders
ORDER BY id
LIMIT 3;
▶ Run this query in the SQL Playground
| id | order_date | dd_mm_yyyy | us_style |
|---|---|---|---|
| 1 | 2024-04-24 | 24-04-2024 | 04/24/2024 |
| 2 | 2024-03-15 | 15-03-2024 | 03/15/2024 |
| 3 | 2023-02-15 | 15-02-2023 | 02/15/2023 |
SELECT DATE_FORMAT(order_date, '%d-%m-%Y') AS formatted_date
FROM orders;
SELECT TO_CHAR(order_date, 'DD-MM-YYYY') AS formatted_date
FROM orders;
SELECT FORMAT(order_date, 'dd-MM-yyyy') AS formatted_date
FROM orders;
YYYY-MM-DD text in SQLite — because a formatted string can no longer be sorted, compared or used in date arithmetic. For more text manipulation, see SQL string functions.First Day and Last Day of the Month
Clean month boundaries are essential for billing cycles and month-end reports.
SELECT date('2024-12-24', 'start of month') AS month_start,
date('2024-12-24', 'start of month', '+1 month', '-1 day') AS month_end;
▶ Run this query in the SQL Playground
| month_start | month_end |
|---|---|
| 2024-12-01 | 2024-12-31 |
The modifiers run left to right: jump to the 1st, add a month, step back one day. The same chain gives 2024-02-29 for a February 2024 date, so leap years are handled for you.
SELECT DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1) AS month_start,
EOMONTH(GETDATE()) AS month_end;
LAST_DAY(d); PostgreSQL uses DATE_TRUNC('month', d) for the start of the month.
Cleaning Date Data
Real-world dates are messy — the five problems to check for first.
Invalid dates
Values like '2026-02-30' or '2026-13-01' that do not correspond to a real calendar day — validate or reject them before analysis.
Missing dates
NULL dates make date arithmetic and differences return NULL silently — check for them first, or supply a fallback with COALESCE.
Format issues
Mixed formats like '12-07-2026' and '2026-07-12' in one text column silently misparse without explicit standardisation.
Time zone problems
Current-timestamp functions resolve in the server (or UTC) time zone, which can differ from the business's local time.
Standardisation
Convert every date column to a real date type — or one agreed ISO text format — as early as possible in the pipeline.
Detecting Values That Are Not Real Dates
-- rows whose text does not parse as a date
SELECT raw_date_text
FROM legacy_orders
WHERE TRY_CAST(raw_date_text AS date) IS NULL;
In SQLite, date(value) returns NULL for text it cannot read (date('12-07-2026') and date('2026-13-01') are both NULL), so WHERE date(col) IS NULL does the same job. Be careful with impossible days: date('2026-02-30') is not rejected — it rolls forward to 2026-03-02.
A Real Data-Quality Check in the Playground
Dates can be perfectly formatted and still be wrong. An order cannot ship before it was placed, so this query looks for exactly that:
SELECT id, order_date, shipped_date
FROM orders
WHERE shipped_date < order_date
ORDER BY id;
▶ Run this query in the SQL Playground
| id | order_date | shipped_date |
|---|---|---|
| 2 | 2024-03-15 | 2023-04-09 |
| 17 | 2023-11-27 | 2023-04-02 |
| 19 | 2024-08-11 | 2023-11-27 |
The Playground's practice data has 38 such orders — its shipped_date values are randomly generated, so do not read shipping times from them. Finding rows like these before building a delivery-time report is precisely what date cleaning is for.
Practical Date Function Examples
35 real business scenarios across sales, retention, subscriptions, banking, insurance, healthcare, SaaS and more.
@today stands for the current date, declared once as DECLARE @today date = CAST(GETDATE() AS date);. To translate a query to another database, swap the functions using the dialect table.Daily Sales Report
Store operations wants total sales broken down by exact calendar day.
SELECT sale_date, SUM(amount) AS daily_total
FROM sales
GROUP BY sale_date
ORDER BY sale_date;
Weekly Sales Report
Regional management wants sales totals rolled up by week instead of by day.
SELECT DATEADD(day, 1 - DATEPART(weekday, sale_date), sale_date) AS week_start,
SUM(amount) AS weekly_total
FROM sales
GROUP BY DATEADD(day, 1 - DATEPART(weekday, sale_date), sale_date)
ORDER BY week_start;
-- the first day of the week follows the server's DATEFIRST setting
Monthly Revenue Report
Finance wants total revenue for every calendar month across all years of sales data.
SELECT YEAR(sale_date) AS yr, MONTH(sale_date) AS mo, SUM(amount) AS monthly_revenue
FROM sales
GROUP BY YEAR(sale_date), MONTH(sale_date)
ORDER BY yr, mo;
Quarterly Revenue Report
The CFO wants revenue rolled up into fiscal quarters instead of months.
SELECT YEAR(sale_date) AS yr,
DATEPART(quarter, sale_date) AS quarter,
SUM(amount) AS quarterly_revenue
FROM sales
GROUP BY YEAR(sale_date), DATEPART(quarter, sale_date)
ORDER BY yr, quarter;
Annual Revenue Report
The board wants one total revenue number per calendar year for the last 5 years.
SELECT YEAR(sale_date) AS yr, SUM(amount) AS annual_revenue
FROM sales
GROUP BY YEAR(sale_date)
ORDER BY yr;
Customer Retention by Signup Month
Growth wants to see how many customers from each signup month are still active today.
SELECT YEAR(signup_date) AS yr, MONTH(signup_date) AS mo,
COUNT(*) AS signups,
SUM(CASE WHEN last_active_date >= DATEADD(day, -30, @today) THEN 1 ELSE 0 END) AS still_active
FROM customers
GROUP BY YEAR(signup_date), MONTH(signup_date);
Churned Subscriptions This Month
The subscriptions team wants every subscriber whose renewal_date has already passed without renewing.
SELECT customer, plan, renewal_date
FROM subscriptions
WHERE renewal_date < @today;
Subscriptions Renewing in the Next 7 Days
The billing team wants a list of subscriptions due to renew within the next week, to trigger reminder emails.
SELECT customer, plan, renewal_date
FROM subscriptions
WHERE renewal_date BETWEEN @today AND DATEADD(day, 7, @today);
Products Still Under Warranty
Support wants to check whether a product's 1-year warranty is still active based on its purchase date.
SELECT oi.order_id, p.name AS product_name, o.order_date,
DATEADD(year, 1, o.order_date) AS warranty_expiry,
CASE WHEN @today <= DATEADD(year, 1, o.order_date) THEN 'Active' ELSE 'Expired' END AS warranty_status
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id;
Inventory Aging Report
Warehouse management wants to see how many days each batch of stock has been sitting in inventory.
SELECT batch_id, received_date,
DATEDIFF(day, received_date, @today) AS days_in_stock
FROM inventory_batches
ORDER BY days_in_stock DESC;
Employee Tenure Report
HR wants every employee's tenure with the company expressed in full years.
SELECT first_name, last_name, hire_date,
DATEDIFF(year, hire_date, @today)
- CASE WHEN DATEADD(year, DATEDIFF(year, hire_date, @today), hire_date) > @today
THEN 1 ELSE 0 END AS tenure_years
FROM employees;
-- DATEDIFF(year, …) counts year boundaries, so subtract 1 until the anniversary has passed
Employees Due for a Payroll Milestone Bonus
Payroll wants to flag employees who are hitting exactly 5 years of service this month.
SELECT first_name, last_name, hire_date
FROM employees
WHERE YEAR(DATEADD(year, 5, hire_date)) = YEAR(@today)
AND MONTH(DATEADD(year, 5, hire_date)) = MONTH(@today);
Customers Inactive for 90+ Days
A CRM analyst wants to flag customers whose last order was more than 90 days ago as at-risk.
SELECT user_id, MAX(order_date) AS last_order_date,
DATEDIFF(day, MAX(order_date), @today) AS days_since_last_order
FROM orders
GROUP BY user_id
HAVING DATEDIFF(day, MAX(order_date), @today) > 90;
Orders Placed During a Campaign Window
Marketing wants every order placed during a campaign that ran from 15 October to 5 November 2024.
SELECT id, user_id, order_date
FROM orders
WHERE order_date BETWEEN '2024-10-15' AND '2024-11-05';
Transactions in the Current Financial Year
A bank's finance team wants every transaction within the current Indian financial year (April 1 - March 31).
SELECT transaction_id, transaction_date, amount
FROM transactions
WHERE transaction_date >= '2026-04-01' AND transaction_date < '2027-04-01';
Claims Filed Within 30 Days of Policy Start
An insurer wants to flag claims that were filed suspiciously soon after the policy began.
SELECT claim_id, policy_start_date, claim_date,
DATEDIFF(day, policy_start_date, claim_date) AS days_since_policy_start
FROM claims
WHERE DATEDIFF(day, policy_start_date, claim_date) <= 30;
Upcoming Appointments This Week
A hospital's front desk wants every appointment scheduled in the next 7 days.
SELECT patient_name, doctor_name, appointment_date
FROM appointments
WHERE appointment_date BETWEEN @today AND DATEADD(day, 7, @today);
Orders Delivered Late
Operations wants every order where the delivery took longer than 7 days.
SELECT order_id, order_date, delivery_date,
DATEDIFF(day, order_date, delivery_date) AS delivery_days
FROM deliveries
WHERE DATEDIFF(day, order_date, delivery_date) > 7;
Trailing 12-Month Revenue for Forecasting
An analyst wants total revenue from the trailing 12 months, up to today, to feed a simple forecasting model.
SELECT SUM(amount) AS trailing_12mo_revenue
FROM sales
WHERE sale_date >= DATEADD(month, -12, @today);
Current Month-to-Date Revenue Tile
An executive dashboard tile needs revenue for the current month so far, updating daily.
SELECT SUM(amount) AS mtd_revenue
FROM sales
WHERE YEAR(sale_date) = YEAR(@today)
AND MONTH(sale_date) = MONTH(@today);
Days Remaining in the Current Month
A KPI dashboard shows how many days are left in the current month to hit target.
SELECT DATEDIFF(day, @today, EOMONTH(@today)) AS days_remaining_this_month;
Newest Products Launched This Quarter
Product management wants every product launched in the current quarter, from a product_launches table.
SELECT product_name, launch_date
FROM product_launches
WHERE YEAR(launch_date) = YEAR(@today)
AND DATEPART(quarter, launch_date) = DATEPART(quarter, @today);
Store Sales for the Last Complete Month
Regional managers want each store's total sales for last month, not the current in-progress month.
SELECT store_id, SUM(amount) AS last_month_sales
FROM sales
WHERE sale_date >= DATEADD(month, -1, DATEFROMPARTS(YEAR(@today), MONTH(@today), 1))
AND sale_date < DATEFROMPARTS(YEAR(@today), MONTH(@today), 1)
GROUP BY store_id;
Branch Revenue Growth This Year vs Last
Branch performance needs this year's revenue compared to the same period last year.
SELECT branch_id,
SUM(CASE WHEN YEAR(sale_date) = YEAR(@today)
AND sale_date <= @today THEN amount ELSE 0 END) AS this_year,
SUM(CASE WHEN YEAR(sale_date) = YEAR(@today) - 1
AND sale_date <= DATEADD(year, -1, @today) THEN amount ELSE 0 END) AS last_year
FROM sales
GROUP BY branch_id;
Franchise Locations Opened in the Last 2 Years
Franchise operations wants every location opened within the last 24 months.
SELECT location_name, opened_date
FROM franchise_locations
WHERE opened_date >= DATEADD(month, -24, @today);
Average Supplier Lead Time
Procurement wants the average number of days between placing a purchase order and receiving the goods.
SELECT supplier_id, AVG(1.0 * DATEDIFF(day, po_date, received_date)) AS avg_lead_time_days
FROM purchase_orders
GROUP BY supplier_id;
Open Orders Older Than a Week
Logistics wants every order that is still pending or processing, has no shipped date and was placed more than 7 days ago.
SELECT id, user_id, status, order_date
FROM orders
WHERE shipped_date IS NULL
AND status IN ('pending', 'processing')
AND order_date < DATEADD(day, -7, @today);
Customer Lifespan in Days
An analyst wants each customer's active lifespan, from first order to most recent order.
SELECT user_id,
DATEDIFF(day, MIN(order_date), MAX(order_date)) AS lifespan_days,
SUM(total) AS lifetime_value
FROM orders
GROUP BY user_id;
Monthly Signup Cohorts
Growth wants customers grouped into cohorts based on the month they first signed up.
SELECT YEAR(first_order_date) AS cohort_year, MONTH(first_order_date) AS cohort_month,
COUNT(DISTINCT user_id) AS cohort_size
FROM (SELECT user_id, MIN(order_date) AS first_order_date FROM orders GROUP BY user_id) t
GROUP BY YEAR(first_order_date), MONTH(first_order_date);
Daily Active Users
A SaaS product team wants a count of distinct users active on each calendar day.
SELECT activity_date, COUNT(DISTINCT user_id) AS daily_active_users
FROM user_activity_log
GROUP BY activity_date
ORDER BY activity_date;
Month-End Close Snapshot
Finance wants account balances exactly as of the last day of last month.
SELECT account_id, balance
FROM account_snapshots
WHERE snapshot_date = EOMONTH(@today, -1);
Spend vs Budget for the Current Quarter
FP&A wants total spend so far in the current quarter, to compare against the quarterly budget.
SELECT SUM(amount) AS qtd_spend
FROM expenses
WHERE YEAR(expense_date) = YEAR(@today)
AND DATEPART(quarter, expense_date) = DATEPART(quarter, @today);
Upcoming Payables Due This Week
Treasury wants every payable due within the next 7 days for cash flow planning.
SELECT vendor_name, invoice_id, due_date, amount
FROM payables
WHERE due_date BETWEEN @today AND DATEADD(day, 7, @today);
Average Order-to-Fulfillment Time by Month
Operations wants the average number of days from order to delivery, tracked month by month.
SELECT YEAR(order_date) AS yr, MONTH(order_date) AS mo,
AVG(1.0 * DATEDIFF(day, order_date, delivery_date)) AS avg_fulfillment_days
FROM deliveries
GROUP BY YEAR(order_date), MONTH(order_date);
Monthly Recurring Revenue by Renewal Month
A SaaS finance analyst wants MRR grouped by the month subscriptions are set to renew.
SELECT YEAR(renewal_date) AS yr, MONTH(renewal_date) AS mo, COUNT(*) AS renewals_due,
SUM(monthly_price) AS mrr_at_risk
FROM subscriptions
GROUP BY YEAR(renewal_date), MONTH(renewal_date);
Why Do We Need Date Functions?
Manual calculations in a spreadsheet, exporting raw dates and subtracting by hand, hours of copy-paste work for a report that changes every day.
One reusable query returns live monthly revenue, retention, and trend data — refreshed automatically every time it runs.
Easy reporting
Monthly, quarterly, and yearly rollups become a GROUP BY away instead of a manual export.
Trend analysis
Month-over-month and year-over-year growth, rolling averages, and seasonality all depend on date arithmetic.
Business intelligence
Every BI dashboard — Power BI, Tableau, Looker — is built on date functions running underneath the visuals.
Date Functions for Data Analysts
Where date arithmetic and date differences show up in day-to-day BI work.
Power BI
DAX time-intelligence functions such as SAMEPERIODLASTYEAR and DATEADD mirror the date arithmetic in this lesson.
Tableau
DATEDIFF and DATEADD calculated fields in Tableau take the same arguments and follow the same logic as SQL Server.
Excel Power Query
"Date.AddDays" and "Duration.Days" are Power Query's equivalents of adding days and measuring a date difference.
Dashboard development
Every "Last 30 Days" filter, month-to-date tile and trend line on a dashboard is powered by a date query underneath.
Time series analysis
Forecasting and trend detection are built on date-bucketed aggregates — the group-by-year-and-month pattern from this lesson.
Revenue analysis
Monthly, quarterly and annual revenue rollups are the most requested date reports in any analyst role.
Customer retention analysis
The difference between signup and last activity is the core metric behind retention and churn dashboards.
Cohort analysis
Grouping customers by the year and month of their first order is the foundation of every cohort retention curve.
KPI reporting
MTD, QTD and YTD tiles all filter on date ranges built from the current date.
Executive reporting
Board decks lead with YoY and MoM growth numbers — both are date comparisons dressed up as headlines.
Common Date Function Mistakes
7 mistakes that trip up almost every SQL learner working with dates — the wrong version, the fix, and why it matters.
#1Wrong Date Formats (Ambiguous DD-MM vs MM-DD)
SELECT * FROM orders
WHERE order_date = '03-04-2026';
-- is this March 4th or April 3rd? Ambiguous without knowing the format
SELECT * FROM orders
WHERE order_date = '2026-04-03'; -- YYYY-MM-DD is unambiguous
#2Comparing Dates as Badly Formatted Text
SELECT COUNT(*) FROM orders
WHERE order_date > '2024-3-15';
-- returns 0 rows in the Playground: '2024-3-15' is not zero-padded,
-- so the text comparison sorts it after every real date in 2024
SELECT COUNT(*) FROM orders
WHERE order_date > '2024-03-15';
-- returns 47: zero-padded YYYY-MM-DD text sorts chronologically
#3Time Zone Problems With "Today"
SELECT * FROM orders
WHERE order_date = date('now');
-- date('now') is the current date in UTC, which can be
-- yesterday or tomorrow in the business's local time zone
SELECT * FROM orders
WHERE order_date = date('now', 'localtime');
-- convert to local time before taking the date
#4DATEDIFF Parameter Order Issues
SELECT DATEDIFF(day, delivery_date, order_date) AS delivery_days
FROM deliveries;
-- arguments reversed — produces a negative delivery time
SELECT DATEDIFF(day, order_date, delivery_date) AS delivery_days
FROM deliveries;
#5DATEADD Logic Errors (Adding Instead of Subtracting)
SELECT DATEADD(day, 30, GETDATE()) AS orders_last_30_days;
-- meant to look BACK 30 days, but added 30 days INTO THE FUTURE instead
SELECT DATEADD(day, -30, GETDATE()) AS thirty_days_ago;
#6Incorrect Grouping by Date (Losing the Year)
SELECT MONTH(order_date) AS mo, SUM(total) AS revenue
FROM orders
GROUP BY MONTH(order_date);
-- merges January 2022, 2023 and 2024 into a single "month 1" row
SELECT YEAR(order_date) AS yr, MONTH(order_date) AS mo, SUM(total) AS revenue
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date);
#7Poor Reporting Logic — Using a Function-Wrapped Filter Instead of a Range
SELECT * FROM sales
WHERE YEAR(sale_date) = 2026;
-- correct result, but cannot use a standard index on sale_date
SELECT * FROM sales
WHERE sale_date >= '2026-01-01' AND sale_date < '2027-01-01';
SQL Date Functions by Dialect
SQLite vs MySQL vs PostgreSQL vs SQL Server — exactly where the syntax diverges.
| Task | SQLite | MySQL | SQL Server | PostgreSQL |
|---|---|---|---|---|
| Current date | date('now') | CURDATE() | CAST(GETDATE() AS date) | CURRENT_DATE |
| Current timestamp | datetime('now') | NOW() | GETDATE() | NOW() / CURRENT_TIMESTAMP |
| Date difference (days) | julianday(b) - julianday(a) | DATEDIFF(b, a) | DATEDIFF(day, a, b) | b - a |
| Date add | date(d, '+n days') | DATE_ADD(d, INTERVAL n DAY) | DATEADD(day, n, d) | d + INTERVAL 'n days' |
| Extract year | strftime('%Y', d) | YEAR(d) | YEAR(d) | EXTRACT(YEAR FROM d) |
| Extract month | strftime('%m', d) | MONTH(d) | MONTH(d) | EXTRACT(MONTH FROM d) |
| Format a date | strftime(fmt, d) | DATE_FORMAT(d, fmt) | FORMAT(d, fmt) | TO_CHAR(d, fmt) |
| Last day of month | date(d, 'start of month', '+1 month', '-1 day') | LAST_DAY(d) | EOMONTH(d) | (DATE_TRUNC('month', d) + INTERVAL '1 month - 1 day')::date |
The same task — a renewal date 30 days after an order — in each database:
SELECT date(order_date, '+30 days') AS renewal
FROM orders;
SELECT DATE_ADD(order_date, INTERVAL 30 DAY) AS renewal
FROM orders;
SELECT DATEADD(day, 30, order_date) AS renewal
FROM orders;
SELECT order_date + INTERVAL '30 days' AS renewal
FROM orders;
SYSDATE, ADD_MONTHS(), EXTRACT() and plain date subtraction for days.Performance Tips
Keeping date-heavy queries fast, even at millions of rows.
Avoid wrapping filtered date columns in functions
WHERE YEAR(order_date) = 2026 usually prevents the database from using a plain index on order_date, forcing a full scan.
Use range filters instead
WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01' returns identical rows while staying index-friendly.
Index the raw date column
A standard B-tree index on order_date helps range-filtered queries, not function-wrapped ones.
Partitioning
Partitioning a large fact table by month or year lets the database skip irrelevant partitions for range-filtered date queries.
Measure before optimising
Never assume a date-heavy query is slow — confirm with the query plan and real timings first.
Checking the Query Plan
EXPLAIN QUERY PLAN
SELECT *
FROM orders
WHERE order_date >= '2024-01-01';
A query plan shows whether the database can jump straight to matching rows through an index (a search or index range scan) or must read every row (a scan). MySQL and PostgreSQL use EXPLAIN; SQL Server shows an execution plan. The Playground's small tables have no index on order_date, so both forms of the filter scan there — the difference appears on large, indexed tables, and it grows with table size.
Date Functions Cheat Sheet
Quick revision before an interview — and the format codes you will reuse constantly.
📋 Quick revision — date functions
SQLite strftime() Format Codes
| Code | Meaning | Result for '2024-12-24 14:30:25' |
|---|---|---|
%Y | Four-digit year | 2024 |
%m | Month, 01–12 | 12 |
%d | Day of month, 01–31 | 24 |
%H | Hour, 00–23 | 14 |
%M | Minute, 00–59 | 30 |
%S | Second, 00–59 | 25 |
%w | Day of week, 0 (Sunday) – 6 | 2 |
%j | Day of year, 001–366 | 359 |
MySQL's DATE_FORMAT() uses similar percent codes; PostgreSQL's TO_CHAR() and SQL Server's FORMAT() use letter patterns such as YYYY-MM-DD and yyyy-MM-dd.
Practice SQL Date Queries
Learn → Practice — 8 exercises, easy to challenging, on the real Playground tables.
Every exercise uses a table and columns that exist in the SQLabHub SQL Playground, and every solution has been run against that database. Write your own query first, run it, then compare.
-
1Easy For every employee, show the name, the hire date, and the hire year and hire month as separate columns. Sort by hire date. Table:
employeesShow solution
SELECT first_name, last_name, hire_date, strftime('%Y', hire_date) AS hire_year, strftime('%m', hire_date) AS hire_month FROM employees ORDER BY hire_date;▶ Run this query in the SQL Playground
strftime() formats a date using a pattern: %Y is the four-digit year and %m the two-digit month. The first row is Steven Nguyen, hired 2015-01-14 → 2015, 01.
-
2Easy List every order placed in March 2024, oldest first. Table:
ordersShow solution
SELECT id, order_date, total FROM orders WHERE order_date >= '2024-03-01' AND order_date < '2024-04-01' ORDER BY order_date;▶ Run this query in the SQL Playground
A half-open range (from the 1st, up to but not including the 1st of the next month) is the safest way to select a month. It returns 6 orders.
-
3Medium Each order can be returned within 30 days. Show each order with its return deadline, newest orders first. Table:
ordersShow solution
SELECT id, order_date, date(order_date, '+30 days') AS return_deadline FROM orders ORDER BY order_date DESC;▶ Run this query in the SQL Playground
date() accepts modifiers that shift the value. The newest order (2024-12-24) gets a deadline of 2025-01-23 — the function rolls over the month and year for you.
-
4Medium Build a monthly revenue report for 2023: one row per month with the number of orders and total revenue. Table:
ordersShow solution
SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders, ROUND(SUM(total), 2) AS revenue FROM orders WHERE order_date >= '2023-01-01' AND order_date < '2024-01-01' GROUP BY strftime('%Y-%m', order_date) ORDER BY month;▶ Run this query in the SQL Playground
Grouping by '%Y-%m' keeps the year and month together, so months from different years can never merge. You should get 12 rows, starting with 2023-01 (3 orders, 10518.82).
-
5Medium Find the five longest-serving employees and their tenure in years (one decimal) as of 1 January 2025. Table:
employeesShow solution
SELECT first_name, last_name, hire_date, ROUND((julianday('2025-01-01') - julianday(hire_date)) / 365.25, 1) AS tenure_years FROM employees ORDER BY hire_date LIMIT 5;▶ Run this query in the SQL Playground
julianday() turns a date into a day number, so subtracting two of them gives the days between the dates. Dividing by 365.25 converts days to years. Steven Nguyen comes first with 10 years.
-
6Medium Which day of the week gets the most orders? Show each weekday name with its order count, busiest first. Table:
ordersShow solution
SELECT CASE strftime('%w', order_date) WHEN '0' THEN 'Sunday' WHEN '1' THEN 'Monday' WHEN '2' THEN 'Tuesday' WHEN '3' THEN 'Wednesday' WHEN '4' THEN 'Thursday' WHEN '5' THEN 'Friday' ELSE 'Saturday' END AS weekday, COUNT(*) AS orders FROM orders GROUP BY strftime('%w', order_date) ORDER BY orders DESC;▶ Run this query in the SQL Playground
%w returns the day of the week as text from '0' (Sunday) to '6' (Saturday), and CASE WHEN turns it into a name. Friday is the busiest day with 37 orders.
-
7Challenging The newest order in the table is dated 2024-12-24. Treating that as “today”, find the customers (user_id) whose last order was more than 365 days earlier, most inactive first. Table:
ordersShow solution
SELECT user_id, MAX(order_date) AS last_order, CAST(julianday('2024-12-24') - julianday(MAX(order_date)) AS INTEGER) AS days_inactive FROM orders GROUP BY user_id HAVING days_inactive > 365 ORDER BY days_inactive DESC;▶ Run this query in the SQL Playground
MAX(order_date) finds each customer’s latest order, julianday() measures the gap, and HAVING filters on the aggregated result. 57 customers qualify; the most inactive is user 15 (last order 2022-01-15, 1074 days).
-
8Challenging For each month of 2024, show revenue, the previous month’s revenue, and the month-over-month change as a percentage. Table:
ordersShow solution
SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_revenue, ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) * 100.0 / LAG(revenue) OVER (ORDER BY month), 1) AS mom_change_pct FROM ( SELECT strftime('%Y-%m', order_date) AS month, ROUND(SUM(total), 2) AS revenue FROM orders WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01' GROUP BY strftime('%Y-%m', order_date) ) AS monthly ORDER BY month;▶ Run this query in the SQL Playground
The inner query builds monthly totals; LAG() then reads the previous row’s revenue. January has no previous month, so its change is NULL; February is +4.2% (16998.61 vs 16318.57).
📝 Quiz — SQL Date Functions
30 questions · instant feedback · 0/30 answered
What is the primary purpose of SQL Date Functions?
Date functions operate specifically on DATE, DATETIME, and TIMESTAMP data — extracting parts of a date, calculating differences, and shifting dates forward or backward.
What does CURRENT_DATE return?
CURRENT_DATE returns just today's date (e.g. 2026-07-12), without any time-of-day component.
What is the key difference between CURRENT_DATE and NOW()?
CURRENT_DATE returns a date only (2026-07-12), while NOW() returns a full timestamp including hours, minutes, and seconds (2026-07-12 14:30:25).
What does YEAR(order_date) return for an order placed on 2026-03-02?
YEAR() extracts just the four-digit year portion of a date value — 2026.
What does MONTH(order_date) return for an order placed on 2026-03-02?
MONTH() returns the month as a number from 1 to 12 — here 3, for March. (SQLite's strftime('%m', ...) returns the text '03' instead.)
What does DAY(order_date) return for an order placed on 2026-03-02?
DAY() returns the day of the month as a number from 1 to 31 — here 2.
In SQL Server, what does DATEADD(day, 30, order_date) calculate?
DATEADD() shifts a date forward (or backward with a negative number) by a specified interval — here, 30 days into the future from order_date.
In SQL Server, what does DATEDIFF(day, start_date, end_date) calculate?
DATEDIFF() returns the difference between two dates in the specified unit — here, the count of days between them.
In MySQL's DATEDIFF(end_date, start_date), what happens if the argument order is reversed?
Getting the argument order backwards is a classic DATEDIFF mistake — MySQL's DATEDIFF(a,b) computes a - b in days, so swapping the order simply flips the sign.
What does EXTRACT(YEAR FROM order_date) do?
EXTRACT(YEAR FROM date) is the ANSI-SQL-standard way to pull out a date part — functionally equivalent to YEAR(date) in engines that support both.
Which engines commonly support EXTRACT()?
EXTRACT() is part of the ANSI SQL standard and is supported by PostgreSQL, Oracle, MySQL, and others, alongside their own YEAR()/MONTH()/DAY() shortcuts.
Why can WHERE YEAR(order_date) = 2026 be slow on a large orders table?
Applying a function to an indexed column typically disables that index, since the index stores raw date values, not the YEAR()-transformed ones.
What is the recommended index-friendly rewrite of WHERE YEAR(order_date) = 2026?
A range condition directly on the raw order_date column returns the exact same rows while still allowing the database to use a standard index.
What does SELECT MONTH(order_date), SUM(sales_amount) FROM sales GROUP BY MONTH(order_date); produce?
GROUP BY MONTH(order_date) buckets every row by its month number, then SUM() totals sales_amount within each bucket — a classic monthly reporting pattern.
Why should YEAR() often be added alongside MONTH() in a GROUP BY for multi-year data?
GROUP BY MONTH(order_date) alone ignores the year, so if your data spans multiple years, every January across every year collapses into one row unless YEAR() is also included in the GROUP BY.
How would you typically calculate a person's age in SQL?
A common SQL Server pattern computes DATEDIFF(day, date_of_birth, GETDATE()) and divides by ~365.25, or uses DATEDIFF(year, ...) adjusted with a CASE WHEN check for whether the birthday has occurred yet this year.
Why is a naive YEAR(CURRENT_DATE) - YEAR(date_of_birth) age calculation sometimes wrong?
Simply subtracting birth year from the current year ignores whether the birthday has actually occurred yet this year, which can overstate age by one for anyone whose birthday is still upcoming.
What does 'first day of the month' typically mean in a SQL reporting query?
Computing or filtering by the first day of the month (e.g. 2026-07-01) is common for month-start reports, subscription billing cycles, and building clean date ranges.
What is Month-over-Month (MoM) analysis?
MoM analysis compares a metric (like revenue) in the current month against the immediately preceding month, commonly expressed as a percentage change.
What is Year-over-Year (YoY) analysis?
YoY analysis compares a metric during a period this year against the same period exactly one year earlier, filtering out normal seasonal month-to-month noise.
What is Rolling 30-Day analysis used for?
A rolling 30-day window recalculates a metric (like total sales) for the trailing 30 days as of every day, smoothing out day-to-day noise better than looking at fixed monthly buckets alone.
Which of these is a common SQL date data cleaning problem?
Inconsistent text-based date formats, invalid/impossible dates, missing dates, and time zone mismatches are all common real-world date data quality issues that must be cleaned before analysis.
What is a practical reason to standardize date formats before analysis?
Ambiguous date formats are a real source of silent data errors — 03-04-2026 means completely different dates depending on whether the format is day-first or month-first, which can corrupt calculations if not standardized.
What does SELECT YEAR(order_date), SUM(sales_amount) FROM sales GROUP BY YEAR(order_date); return?
Grouping by YEAR(order_date) and summing sales_amount produces one row of total revenue per calendar year — a standard annual revenue report.
Why is DATEDIFF considered a major SQL interview topic?
DATEDIFF-based problems (employee tenure, delivery delay, customer retention, project duration) are extremely common in real SQL interviews because they mirror everyday business reporting needs.
Which function returns the current date and time in SQL Server?
SQL Server uses GETDATE() (or CURRENT_TIMESTAMP). MySQL uses NOW() and CURDATE(), PostgreSQL uses NOW() and CURRENT_DATE, and SQLite uses datetime('now') and date('now').
How does PostgreSQL typically add 7 days to a date?
PostgreSQL commonly uses date arithmetic with INTERVAL, e.g. date_column + INTERVAL '7 days', whereas SQL Server uses DATEADD(day, 7, date_column) and MySQL uses DATE_ADD(date_column, INTERVAL 7 DAY).
Why are Date Functions considered essential for Data Analysts?
Nearly every business dashboard reports on some time dimension — daily, monthly, quarterly, or yearly — and date functions are the toolkit that makes that kind of time-based analysis possible directly in SQL.
What does an EXPLAIN plan typically reveal about WHERE YEAR(order_date) = 2026 versus a range filter?
EXPLAIN commonly shows a full scan for the function-wrapped filter and an index range scan for the equivalent raw-column range filter, illustrating why the rewrite matters for performance at scale.
In SQLite (the SQLabHub Playground), which expression adds 30 days to order_date?
SQLite shifts dates with modifiers: date(order_date, '+30 days'). DATEADD is SQL Server and DATE_ADD is MySQL; order_date + 30 would just do arithmetic on the leading year number.
SQL Date Functions Interview Questions
50 questions grouped by level — select any question to reveal the answer.
DATEADD or DATEDIFF(unit, start, end), it is SQL Server syntax, with GETDATE() as the current date and time. Interviewers usually accept any dialect as long as you say which one you are writing. More topics are covered in the SQL interview questions guide.Beginner
Q1What are Date Functions in SQL?
Q2What is the difference between DATE and DATETIME?
Q3What does DATEDIFF do?
Q4What does DATEADD do?
Q5How do you calculate age in SQL?
Q6How do you extract the month from a date?
Q7What is the difference between CURRENT_DATE and NOW()?
Q8What does YEAR() return?
Q9What does DAY() return?
Q10What is EXTRACT() used for?
Q11What data types store dates in SQL?
Q12Can date functions be used inside a WHERE clause?
Q13What does CURRENT_TIMESTAMP return?
Q14Why would a business need Date Functions?
Q15What does DATEADD(month, 1, hire_date) calculate?
Q16Is TIME the same as DATETIME?
Q17What is the simplest way to get "today" in a query?
Q18Can you subtract one date from another directly?
Intermediate
Q19How would you calculate the number of days between two dates?
Q20How would you find all orders placed in the current year?
Q21How would you calculate a subscription's renewal date, 6 months after start?
Q22How would you find employees hired more than 5 years ago?
Q23How would you build a monthly sales report grouped by year and month?
Q24What is the difference between EXTRACT(YEAR FROM date) and YEAR(date)?
Q25How would you calculate an employee's age given their date_of_birth?
Q26How would you find the first and last day of the current month?
Q27How would you calculate Month-over-Month revenue growth?
Q28How would you calculate Year-over-Year revenue growth for July?
Q29How would you calculate a rolling 30-day sales total as of today?
Q30How would you find customers who have not ordered in over 90 days?
Q31What does DATE_TRUNC do, and which engines support it?
Q32How would you standardize a date stored as inconsistent text like "12-07-2026"?
Q33How would you find subscriptions renewing in the next 7 days?
Q34How would you calculate average delivery time per month?
Advanced
Q35Why can WHERE YEAR(order_date) = 2026 hurt performance on a large table?
Q36How can you keep year-based filtering fast on a large table?
Q37How would you design a query to detect Month-over-Month growth exceeding 20%?
Q38How would you correctly calculate age accounting for whether the birthday has passed this year?
Q39What is the risk of comparing dates stored as VARCHAR instead of a real DATE type?
Q40How would you build a Rolling 30-Day trend line for a dashboard, one point per day?
Q41What is the interview significance of the Date Functions topic?
Q42How do time zones complicate NOW()/CURRENT_TIMESTAMP in a global business?
Q43How would you calculate Year-over-Year growth for a rolling 12-month period rather than a fixed calendar year?
Q44Why might DATEDIFF(year, start_date, end_date) give a misleading result compared to DATEDIFF(day, ...)/365?
Q45How would you detect and flag invalid or impossible dates during data cleaning?
Q46How would you build a cohort retention query showing what % of each signup month's customers were still active N months later?
Q47Why is understanding Date Functions considered foundational for a Data Analyst role and for Time Series Analysis?
Q48How would you write a single query that reports both MTD and the prior month-to-date figure for a like-for-like comparison?
Q49What are the trade-offs of partitioning a large fact table by date?
Q50How would you safely handle DATEADD(month, n, date) when the resulting day doesn't exist in the target month (e.g. Jan 31 + 1 month)?
Summary
Key takeaways from this lesson.
Frequently Asked Questions
What are SQL date functions?
SQL date functions are built-in functions that work with date and time values. They return the current date, extract parts such as the year or month, add or subtract time, calculate the difference between two dates, and format dates for display. They are the basis of time-based reporting such as monthly revenue, tenure and retention.
How do I get the current date in SQL?
Use CURRENT_DATE in PostgreSQL, MySQL and SQLite. MySQL also offers CURDATE(), SQLite offers date('now'), and SQL Server uses CAST(GETDATE() AS date). For the date and time together use NOW() in MySQL and PostgreSQL, GETDATE() in SQL Server, or datetime('now') in SQLite.
How do I extract the year from a date in SQL?
Use YEAR(date_column) in MySQL and SQL Server, EXTRACT(YEAR FROM date_column) in PostgreSQL, MySQL and Oracle, or strftime('%Y', date_column) in SQLite. All of them return the four-digit year, although SQLite returns it as text.
What is DATEDIFF in SQL?
DATEDIFF calculates the difference between two dates. In SQL Server the syntax is DATEDIFF(day, start_date, end_date) and the unit can be day, month, year and more. In MySQL it is DATEDIFF(end_date, start_date) and always returns days. PostgreSQL subtracts dates directly (end_date - start_date), and SQLite uses julianday(end_date) - julianday(start_date).
What is DATEADD in SQL?
DATEADD is the SQL Server function that adds an interval to a date: DATEADD(day, 30, order_date) returns the date 30 days later, and a negative number subtracts. The equivalents are DATE_ADD(order_date, INTERVAL 30 DAY) in MySQL, order_date + INTERVAL '30 days' in PostgreSQL, and date(order_date, '+30 days') in SQLite.
How do I calculate age in SQL?
Count the full years between the date of birth and today, subtracting one if this year's birthday has not happened yet. MySQL does this with TIMESTAMPDIFF(YEAR, date_of_birth, CURDATE()) and PostgreSQL with EXTRACT(YEAR FROM AGE(date_of_birth)). In SQL Server, DATEDIFF(year, ...) only counts year boundaries, so it needs a CASE WHEN adjustment for the birthday.
What is the difference between DATE, DATETIME and TIMESTAMP?
DATE stores only a calendar date such as 2026-07-12. DATETIME and TIMESTAMP store a date together with a time of day, such as 2026-07-12 14:30:25, and TIMESTAMP is often time-zone aware. SQLite has no dedicated date types and stores these values as text, which is why the YYYY-MM-DD format matters there.
Why can WHERE YEAR(order_date) = 2026 be slow?
Wrapping a date column in a function such as YEAR() usually stops the database from using a standard index on that column, so it scans every row. A range filter — WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01' — returns the same rows and can use the index.
Are SQL date functions the same in every database?
No. The operations are the same everywhere, but the function names differ: SQL Server uses GETDATE(), DATEADD() and DATEDIFF(unit, start, end); MySQL uses CURDATE(), DATE_ADD() and DATEDIFF(end, start); PostgreSQL uses CURRENT_DATE, INTERVAL arithmetic and date subtraction; SQLite uses date(), strftime() and julianday(). Always check which database you are writing for.
Which date functions work in SQLite?
SQLite has date(), time(), datetime(), julianday(), unixepoch() and strftime(), plus the keywords CURRENT_DATE, CURRENT_TIME and CURRENT_TIMESTAMP. It does not have YEAR(), MONTH(), DATEADD(), DATEDIFF() or EXTRACT(). The SQLabHub SQL Playground runs SQLite, so the practice examples in this lesson use those functions.
How are date functions used in data analytics?
Analysts use date functions for monthly and yearly revenue rollups, customer retention and churn analysis, cohort analysis, rolling 30-day trends, and for building the date fields behind dashboards in tools such as Power BI and Tableau.
What is time series analysis in SQL?
Time series analysis studies how a metric changes over time. In SQL it combines date functions with GROUP BY and aggregate functions to build trends such as month-over-month growth, year-over-year comparisons and rolling averages.
How can I practice SQL date queries?
Work through the practice exercises in this lesson, then run them in the free SQL Playground, which has orders, employees, users and payments tables with real date columns. The SQL date challenges add graded problems.