Skip to content
Functions

SQL Date Functions: YEAR, MONTH, DATEDIFF & Ranges

10 min readUpdated September 24, 2026Every example verified
On this page 12 sections ▾
  1. Pulling a date apart: YEAR, MONTH, DAY
  2. The monthly report: grouping by a date part
  3. Filtering by date range — the safe pattern
  4. Today’s date, and the last 30 days
  5. Shifting a date: adding days and month starts
  6. The gap between two dates: DATEDIFF and julianday
  7. Earliest and latest: MIN and MAX
  8. Cohorts: grouping by year
  9. Two habits that prevent date bugs
  10. Practice this topic
  11. Frequently asked questions
  12. Related reading

SQL date functions let a query take a date apart into its year, month or day, measure the gap between two dates, shift a date forward or back, and ask for today’s date. Nearly every real question has a time dimension: how many orders this month, who was hired before 2020, how long has each employee been here. This guide covers the small set that appears in almost every query, with the dialect differences flagged where they bite, because date functions are where databases disagree most.

Which database runs these examples

The playground runs SQLite, which has no separate date type: the sample database keeps its dates as ISO text such as '2024-03-15', and SQLite’s own date functions, date(), strftime() and julianday(), work on that text. SQLite has no YEAR(), MONTH() or DAY(), so the playground adds those three as helpers, spelled the way MySQL and SQL Server spell them. Blocks marked not in SQLite show another database’s spelling and will not run here.

Pulling a date apart: YEAR, MONTH, DAY

SELECT order_id, order_date,
       YEAR(order_date) AS yr, MONTH(order_date) AS mo, DAY(order_date) AS dy
FROM Orders
ORDER BY order_id
LIMIT 4;
order_idorder_dateyrmody
50012024-01-142024114
50022024-01-222024122
50032024-02-02202422
50042024-02-112024211

● 4 rows · produced by running this query on the sample database

These three extract one component of a date as a plain number. That number then behaves like any other value — you can filter on it, sort by it, and, most usefully, group by it. YEAR, MONTH and DAY are the MySQL and SQL Server names; here they are the playground’s helpers. SQLite’s own tool is strftime, which formats a date with a pattern and returns text:

SELECT order_id, order_date,
       strftime('%Y', order_date) AS yr_text,
       CAST(strftime('%m', order_date) AS INTEGER) AS mo
FROM Orders
ORDER BY order_id
LIMIT 4;
order_idorder_dateyr_textmo
50012024-01-1420241
50022024-01-2220241
50032024-02-0220242
50042024-02-1120242

● 4 rows · produced by running this query on the sample database

strftime('%Y', ...) gives the text '2024', not the number 2024, so wrap it in CAST(... AS INTEGER) whenever you compare it with a number or do arithmetic on it. The concept is the same everywhere; only the spelling moves:

The year of a date
DatabaseExpression
MySQLYEAR(order_date) (EXTRACT(YEAR FROM order_date) also works)
SQL ServerYEAR(order_date) or DATEPART(year, order_date)
PostgreSQLEXTRACT(YEAR FROM order_date)
SQLiteCAST(strftime('%Y', order_date) AS INTEGER)
This playgroundthe SQLite form, or the added YEAR(order_date)

The monthly report: grouping by a date part

This is the single most common date-function pattern in working SQL — turn each date into its month, then aggregate per month:

SELECT MONTH(order_date) AS order_month, COUNT(*) AS orders, SUM(order_total) AS revenue
FROM Orders
GROUP BY MONTH(order_date)
ORDER BY order_month;
order_monthordersrevenue
1218285
2311202.42
3416444.89
4316776
5315943

● 5 rows · produced by running this query on the sample database

One honest caveat: months with no orders simply do not appear, because GROUP BY can only summarise rows that exist. Reports that must show empty months as zero rows need a calendar table or a generated series to join against — a technique worth knowing exists, even before you need it.

A second caveat: every sample order falls in 2024, so grouping by the month alone is safe here. On data that spans years, group by year and month — GROUP BY YEAR(order_date), MONTH(order_date), or the month-start date shown further down — or January 2024 and January 2025 land in the same row.

Filtering by date range — the safe pattern

SELECT order_id, order_date, status
FROM Orders
WHERE order_date >= '2024-03-01' AND order_date < '2024-04-01';

Note the shape: inclusive start, exclusive end. Asking for >= March 1 and < April 1 captures all of March no matter how the column stores time. The tempting alternative, BETWEEN '2024-03-01' AND '2024-03-31', quietly drops rows stamped 2024-03-31 14:30 the day the column gains a time component — because '2024-03-31' means midnight, and 14:30 is after midnight. The half-open range never has this problem, which is why it is the professional habit.

Plain comparisons work on dates too, exactly as they do on numbers. In SQLite, where a date is ISO text, that works because year-month-day text sorts in date order:

SELECT first_name, hire_date
FROM Employees
WHERE hire_date < '2020-01-01'
ORDER BY hire_date;

Today’s date, and the last 30 days

Every database can tell you the current date, and each spells it differently. In SQLite:

SELECT date('now') AS today, CURRENT_DATE AS also_today;

The answer changes every day, which is why this page does not print it. For the time as well, use datetime('now') or CURRENT_TIMESTAMP. SQLite reports 'now' in UTC, so for part of the day, depending on your time zone, its “today” is not the date on your calendar; date('now', 'localtime') gives the local date.

Today’s date and the current moment
DatabaseTodayDate and time now
SQLite (this playground)date('now') or CURRENT_DATEdatetime('now') or CURRENT_TIMESTAMP
PostgreSQLCURRENT_DATENOW() or CURRENT_TIMESTAMP
MySQLCURDATE() or CURRENT_DATENOW() or CURRENT_TIMESTAMP
SQL ServerCAST(GETDATE() AS date)GETDATE() or SYSDATETIME()

The everyday use is a rolling window, such as orders from the last 30 days: WHERE order_date >= date('now', '-30 days'). On the sample data that finds nothing, because every order is from 2024, so the runnable version anchors the window to the latest order instead of to today:

SELECT order_id, order_date, order_total
FROM Orders
WHERE order_date >= date((SELECT MAX(order_date) FROM Orders), '-30 days')
ORDER BY order_date;
order_idorder_dateorder_total
50132024-05-065800
50142024-05-181050
50152024-05-299093

● 3 rows · produced by running this query on the sample database

The latest order is dated 29 May 2024, so the window opens on 29 April and three orders fall inside it. The same filter against today is order_date >= CURRENT_DATE - INTERVAL '30 days' in PostgreSQL, order_date >= CURDATE() - INTERVAL 30 DAY in MySQL and order_date >= DATEADD(day, -30, CAST(GETDATE() AS date)) in SQL Server.

Shifting a date: adding days and month starts

SQLite moves a date by passing date() a modifier: a signed number and a unit.

SELECT order_id, order_date,
       date(order_date, '+7 days') AS follow_up,
       date(order_date, '-1 day') AS day_before
FROM Orders
ORDER BY order_id
LIMIT 4;
order_idorder_datefollow_upday_before
50012024-01-142024-01-212024-01-13
50022024-01-222024-01-292024-01-21
50032024-02-022024-02-092024-02-01
50042024-02-112024-02-182024-02-10

● 4 rows · produced by running this query on the sample database

Add 7 days to a date
DatabaseExpression
SQLite (this playground)date(order_date, '+7 days')
PostgreSQLorder_date + INTERVAL '7 days'
MySQLDATE_ADD(order_date, INTERVAL 7 DAY)
SQL ServerDATEADD(day, 7, order_date)

Other units work the same way ('+1 month', '-1 year'), but month arithmetic is where databases disagree. Ask SQLite for 31 January plus one month and it rolls past the short month:

SELECT date('2024-01-31', '+1 month') AS one_month_later;
one_month_later
2024-03-02

● 1 row · produced by running this query on the sample database

MySQL, PostgreSQL and SQL Server stop at the last day of February (2024-02-29) instead. Neither rule is wrong, but they are different, so test month arithmetic on the 29th, 30th and 31st before trusting it.

The first day of the month

The other everyday shift is back to the first of the month. It turns every date into a month label that sorts correctly and never merges two years:

SELECT strftime('%Y-%m-01', order_date) AS month_start, COUNT(*) AS orders
FROM Orders
GROUP BY strftime('%Y-%m-01', order_date)
ORDER BY month_start;
month_startorders
2024-01-012
2024-02-013
2024-03-014
2024-04-013
2024-05-013

● 5 rows · produced by running this query on the sample database

strftime('%Y-%m-01', order_date) keeps the year and month and pins the day to 01; date(order_date, 'start of month') does the same. Elsewhere it is DATE_TRUNC('month', order_date) in PostgreSQL, DATE_FORMAT(order_date, '%Y-%m-01') in MySQL, and DATEFROMPARTS(YEAR(order_date), MONTH(order_date), 1) in SQL Server, which also offers DATETRUNC(month, order_date) from SQL Server 2022.

The gap between two dates: DATEDIFF and julianday

How many days has each employee been here? SQLite has no DATEDIFF. It turns each date into a day number with julianday(), and the difference between two day numbers is the gap in days:

SELECT first_name, hire_date,
       julianday('2026-08-29') - julianday(hire_date) AS days_employed
FROM Employees
ORDER BY days_employed DESC
LIMIT 5;
first_namehire_datedays_employed
Chen2016-05-203753
Tomas2017-11-053219
Ananya2018-03-123092
Marcus2019-07-012616
Priya2019-09-302525

● 5 rows · produced by running this query on the sample database

julianday() counts days from a fixed starting point in 4714 BC and returns a real number, because it can carry a time of day as a fraction. For plain dates the difference is a whole number of days; if the columns hold times as well, wrap it in ROUND() or CAST(... AS INTEGER). MySQL and SQL Server call this DATEDIFF, and they put its arguments in opposite orders:

-- SQL Server: DATEDIFF(unit, start, end)
SELECT TOP 5 first_name, hire_date,
       DATEDIFF(day, hire_date, '2026-08-29') AS days_employed
FROM Employees
ORDER BY days_employed DESC;
-- MySQL: DATEDIFF(end, start), always in days
SELECT first_name, hire_date,
       DATEDIFF('2026-08-29', hire_date) AS days_employed
FROM Employees
ORDER BY days_employed DESC
LIMIT 5;

Same name, opposite order: MySQL subtracts its second argument from its first, SQL Server subtracts its second from its third. PostgreSQL has no DATEDIFF and subtracts one date from the other directly (DATE '2026-08-29' - hire_date). If a “days since” report comes out negative after a move between databases, check the argument order first.

Earliest and latest: MIN and MAX

MIN and MAX work on dates as well as numbers: the earliest date is the smallest and the latest is the largest. Per customer, that gives a first-and-latest-order report in one query:

SELECT c.company, MIN(o.order_date) AS first_order, MAX(o.order_date) AS latest_order
FROM Customers c
JOIN Orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.company
ORDER BY latest_order DESC, c.company;
companyfirst_orderlatest_order
Cedar Health2024-03-272024-05-29
Meridian Trading Co.2024-01-142024-05-18
Fjord Analytics2024-04-042024-05-06
Andes Textiles2024-03-012024-04-25
Sakura Logistics2024-02-022024-04-12
Baltic Systems2024-03-152024-03-15
Acme Robotics2024-01-222024-03-08
Kilimanjaro Foods2024-02-192024-02-19

● 8 rows · produced by running this query on the sample database

Cedar Health ordered most recently, on 29 May 2024. Baltic Systems and Kilimanjaro Foods ordered only once, so their first and latest orders are the same day. In SQLite these dates are text, so MIN and MAX are comparing text here; that gives the right answer only because year-month-day text sorts in date order, which is one more reason to keep dates in that format.

Cohorts: grouping by year

SELECT YEAR(hire_date) AS hire_year, COUNT(*) AS hires
FROM Employees
GROUP BY YEAR(hire_date)
ORDER BY hire_year;
hire_yearhires
20161
20171
20181
20192
20202
20212
20222
20231

● 8 rows · produced by running this query on the sample database

The same monthly-report pattern at a coarser grain — one row per hiring year. Swap in any date column and any aggregate and this shape produces most of the "X per year" tables you have ever seen in a slide deck.

Try it yourself

Every query marked runs in the playground works on the sample database as-is. Change a range, swap MONTH for YEAR, or try a different date modifier, and watch the report reshape itself.

Open the Playground

Two habits that prevent date bugs

Store dates as dates, write them as ISO

Use a real DATE or TIMESTAMP column where the database has one. SQLite has no date type at all, which is why the sample database keeps its dates as text in one fixed format. Either way, write dates as '2024-03-01' (year-month-day): it is the ISO 8601 standard, every database accepts it, and it sorts correctly even when compared as text. Regional formats like 03/01/2024 mean different dates in different countries and have no place inside a query.

Prefer half-open ranges over BETWEEN

>= start AND < next-start keeps working when a date column gains a time component, when months have 28, 30 or 31 days, and when ranges need to chain without overlap. BETWEEN breaks quietly on all three.

Practice this topic

Reading explains it; solving it is what makes it stick. This exercise uses the monthly-report pattern from this page, and your answer is checked by running it against the same database:

See all 42 exercises →

Frequently asked questions

How do I get the year or month out of a date in SQL?

In MySQL and SQL Server, YEAR(column) and MONTH(column) return plain numbers. PostgreSQL spells it EXTRACT(YEAR FROM column). SQLite has neither and uses strftime('%Y', column), which returns text, so wrap it in CAST(... AS INTEGER) when you need a number. This site's playground runs SQLite and adds YEAR, MONTH and DAY as helpers so the MySQL-style examples run.

How do I get today's date in SQL?

CURRENT_DATE works in PostgreSQL, MySQL and SQLite; in SQL Server write CAST(GETDATE() AS date). For the date and time, use NOW() in PostgreSQL and MySQL, GETDATE() in SQL Server, and datetime('now') in SQLite, which reports it in UTC.

Why should I avoid BETWEEN for date ranges?

BETWEEN is inclusive on both ends, and a date literal like '2024-03-31' means midnight at the start of that day. The moment the column carries a time, rows from later that day fall outside the range. The half-open pattern - greater-or-equal to the start, strictly less than the next period's start - never has this problem.

How do I calculate the number of days between two dates?

It depends on the database. SQL Server uses DATEDIFF(day, start, end). MySQL uses DATEDIFF(end, start), which always counts days. PostgreSQL subtracts one date from the other directly. SQLite, which runs this site's playground, has no DATEDIFF: write julianday(end) - julianday(start).

Can I GROUP BY a date function?

Yes. Grouping by a date part, such as MONTH(order_date), YEAR(hire_date) or strftime('%Y-%m', order_date) in SQLite, is how monthly and yearly reports are built. When the data spans several years, group by year and month together, or January of one year merges with January of the next. Groups only appear for values that exist in the data; empty months return no row at all.

What date format should I write in SQL queries?

ISO 8601: 'YYYY-MM-DD', like '2024-03-01'. Every database accepts it, it is unambiguous across countries, and it sorts correctly even as text. Regional formats such as 03/01/2024 are ambiguous and should never appear inside a query.

Do these functions change the stored dates?

No. Like all functions in a SELECT, they only shape the output of the query. The stored values change only if you use the expressions inside an UPDATE.

About the author

SQL Practice

SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.