MySQL Date Arithmetic: INTERVAL, DATEDIFF and TIMESTAMPDIFF
Add and subtract time in MySQL with INTERVAL, count days with DATEDIFF, months and years with TIMESTAMPDIFF, and find the first and last day of a month.
- Course: SQL study plan
- Module: Functions
- Kind: Lesson
- Reading time: 14 min
- Runtime: MySQL 8 syntax on SQLite 3.49
How do you find the difference between two dates in MySQL?
DATEDIFF(later, earlier) returns the number of days between two dates, ignoring any time part, and is negative when the first date is earlier. For other units use TIMESTAMPDIFF(unit, start, end) — note the reversed order — with MINUTE, HOUR, DAY, MONTH or YEAR; it counts only whole units, so it is the right tool for ages and tenure.
Lesson
Reading parts out of a date answers "when". Arithmetic answers "how long" and "by when": the due date of a loan, the number of days an invoice is overdue, a subscription's renewal, a customer's tenure in months, the users who came back within seven days. MySQL gives you three tools — INTERVAL to move a date, DATEDIFF to count days, and TIMESTAMPDIFF to count any unit — and one helper, LAST_DAY, for the month edges that make calendar arithmetic awkward.
Moving a date: DATE_ADD and INTERVAL
DATE_ADD(d, INTERVAL n unit) returns the date moved forward by n units; DATE_SUB moves it back. The units are SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER and YEAR.
SELECT DATE_ADD('2024-03-15', INTERVAL 10 DAY) AS plus_10_days, -- '2024-03-25'
DATE_SUB('2024-03-15', INTERVAL 2 WEEK) AS minus_2_weeks, -- '2024-03-01'
DATE_ADD('2024-03-15', INTERVAL 1 YEAR) AS next_year, -- '2025-03-15'
DATE_ADD('2024-03-15 23:30:00', INTERVAL 45 MINUTE) AS later; -- '2024-03-16 00:15:00'
MySQL also accepts the operator form: order_date + INTERVAL 30 DAY and order_date - INTERVAL 1 MONTH mean the same thing and read well inside a WHERE. A negative amount moves the other way, so DATE_ADD(d, INTERVAL -7 DAY) is a week earlier.
Adding to a DATE gives a DATE; adding hours or minutes, or adding to a DATETIME, gives a DATETIME. Never add a bare number to a date: '2024-03-15' + 1 is not the 16th — MySQL turns the string into the number 2024 and adds 1. Always say INTERVAL.
Months are not all the same length
Adding days is simple arithmetic. Adding months is calendar arithmetic, because a month has 28 to 31 days. MySQL's rule: keep the day number, and if the target month is too short, clamp to its last day.
SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH) AS a, -- '2024-02-29' (leap year)
DATE_ADD('2023-01-31', INTERVAL 1 MONTH) AS b, -- '2023-02-28'
DATE_ADD('2024-02-29', INTERVAL 1 YEAR) AS c, -- '2025-02-28'
DATE_ADD('2024-03-31', INTERVAL -1 MONTH) AS d; -- '2024-02-29'
A consequence worth remembering: adding one month twice is not always adding two months. '2024-01-31' plus one month is '2024-02-29', plus another is '2024-03-29' — but plus two months at once is '2024-03-31'. Billing systems compute every renewal from the original start date for this reason, not from the previous renewal.
Counting days: DATEDIFF
DATEDIFF(a, b) returns a − b in days. The later date goes first for a positive answer, and any time part is ignored.
SELECT DATEDIFF('2024-03-15', '2024-03-01') AS a, -- 14
DATEDIFF('2024-03-01', '2024-03-15') AS b, -- -14
DATEDIFF('2024-03-01', '2024-02-01') AS c, -- 29 (leap February)
DATEDIFF('2024-03-02 00:01:00', '2024-03-01 23:59:00') AS d; -- 1
The last line shows what "ignores the time" means: two minutes apart, but on different days, so the answer is 1. DATEDIFF counts calendar days, which is usually what a business question means by "days late".
A typical use is an overdue report:
SELECT id, due_date, returned_on,
DATEDIFF(returned_on, due_date) AS days_late
FROM loans
WHERE returned_on > due_date
ORDER BY days_late DESC, id;
Counting any unit: TIMESTAMPDIFF
TIMESTAMPDIFF(unit, start, end) returns end − start in the given unit, counting whole units only. Note the argument order is the opposite of DATEDIFF: start first, end second.
SELECT TIMESTAMPDIFF(MINUTE, '2024-03-15 09:00:00', '2024-03-15 10:45:00') AS mins, -- 105
TIMESTAMPDIFF(HOUR, '2024-03-15 09:00:00', '2024-03-15 10:45:00') AS hrs, -- 1
TIMESTAMPDIFF(MONTH, '2024-01-15', '2024-03-14') AS months_a, -- 1
TIMESTAMPDIFF(MONTH, '2024-01-15', '2024-03-15') AS months_b, -- 2
TIMESTAMPDIFF(YEAR, '2000-06-20', '2024-06-19') AS age; -- 23
"Whole units" is exactly the rule for ages and tenure: someone born on 20 June 2000 is still 23 on 19 June 2024. Dividing DATEDIFF by 365 gets birthdays wrong and drifts with leap years; TIMESTAMPDIFF(YEAR, …) does not.
Month edges: LAST_DAY and the first of the month
LAST_DAY(d) returns the last day of d's month, leap years included. The first day of a month has no dedicated MySQL function, but two idioms give it:
SELECT LAST_DAY('2024-02-10') AS month_end, -- '2024-02-29'
DATE_FORMAT('2024-02-10', '%Y-%m-01') AS month_start, -- '2024-02-01'
DATE_SUB('2024-02-10', INTERVAL DAY('2024-02-10') - 1 DAY) AS also_start,
DATE_ADD(LAST_DAY('2024-02-10'), INTERVAL 1 DAY) AS next_month; -- '2024-03-01'
DAY(LAST_DAY(d)) is the number of days in the month — a common building block for "average per day this month" and for checking leap years.
Rolling windows
Arithmetic with INTERVAL is how relative periods are written. Against a fixed reporting date:
-- orders in the 7 days up to and including 2024-03-15
SELECT id, order_date
FROM orders
WHERE order_date > DATE_SUB('2024-03-15', INTERVAL 7 DAY)
AND order_date <= '2024-03-15'
ORDER BY order_date, id;
In production the fixed date is usually CURDATE(). Notice the filter keeps the column bare and moves the arithmetic to the constant side — order_date > CURDATE() - INTERVAL 7 DAY can use an index, while DATEDIFF(CURDATE(), order_date) < 7 cannot.
Other databases
Interviewers often ask how this looks elsewhere:
- PostgreSQL:
d + INTERVAL '10 days',d2 - d1gives an integer number of days forDATEvalues, andAGE(end, start)returns an interval of years, months and days. - SQL Server:
DATEADD(day, 10, d)andDATEDIFF(day, start, end)— the unit first, then start, then end. It counts boundaries crossed, soDATEDIFF(year, '2023-12-31', '2024-01-01')is 1, unlike MySQL'sTIMESTAMPDIFF(YEAR, …), which is 0. - Standard SQL has
INTERVALliterals too; the MySQLINTERVAL n UNITform is close to it.
Pitfalls
DATEDIFF(later, earlier)butTIMESTAMPDIFF(unit, earlier, later)— opposite orders.- Adding a bare number to a date instead of an
INTERVAL. - Expecting
'2024-01-31' + INTERVAL 1 MONTHto be in March. - Ages from
DATEDIFF / 365. - Putting the column inside the function in a filter when the arithmetic could move to the constant.
Key takeaways
DATE_ADD(d, INTERVAL n unit),DATE_SUB, ord ± INTERVAL n unitmove a date.- Month arithmetic clamps to the end of a shorter month.
DATEDIFF(a, b)is calendar daysa − b;TIMESTAMPDIFF(unit, start, end)counts whole units.LAST_DAY(d)is the month's end;DATE_FORMAT(d, '%Y-%m-01')its start.- Keep the column bare in filters and do the arithmetic on the constant.
Common questions
How do I add days or months to a date in MySQL?
Use DATE_ADD(d, INTERVAL n unit) or the shorter d + INTERVAL n unit, where the unit is DAY, WEEK, MONTH, YEAR, HOUR or MINUTE. DATE_SUB and - INTERVAL subtract. Adding a month to 31 January gives the last day of February, because MySQL clamps the day to the end of a shorter month.
How do I calculate someone's age in years in SQL?
In MySQL use TIMESTAMPDIFF(YEAR, birth_date, on_date), which counts only completed years, so a birthday later in the year is not counted early. Dividing DATEDIFF by 365 is wrong around birthdays and leap years. PostgreSQL uses EXTRACT(YEAR FROM AGE(on_date, birth_date)).
How do I get the first and last day of the month in MySQL?
LAST_DAY(d) returns the last day of the date's month, taking leap years into account. For the first day, use DATE_FORMAT(d, '%Y-%m-01'), or subtract the day number with DATE_SUB(d, INTERVAL DAY(d) - 1 DAY). The first day of the next month is LAST_DAY(d) + INTERVAL 1 DAY.
What is the difference between DATEDIFF in MySQL and SQL Server?
MySQL's DATEDIFF(end, start) takes two dates and always returns days. SQL Server's DATEDIFF(unit, start, end) takes the unit first and the dates in the opposite order, and counts boundaries crossed, so DATEDIFF(year, '2023-12-31', '2024-01-01') is 1 there. MySQL's TIMESTAMPDIFF is the closer match to SQL Server's version.
Exercises
Library loans: due dates and days late
Books are lent for 14 days. returned_on is NULL while a book is still out.
| id | borrowed_on | returned_on |
|---|---|---|
| 1 | 2024-03-01 | 2024-03-10 |
| 2 | 2024-03-01 | 2024-03-20 |
| 3 | 2024-02-20 | NULL |
| 4 | 2024-02-25 | 2024-03-15 |
Return id, due_on (borrowed date plus 14 days) and days_late — days between the due date and the return, never below 0 (an early return is 0 days late), and NULL for a book not yet returned. Order by id.
id | due_on | days_late
1 | 2024-03-15 | 0
2 | 2024-03-15 | 5
3 | 2024-03-05 | NULL
4 | 2024-03-10 | 5Subscriptions: renewal, tenure and month end
Each subscription has a start and an end date.
| id | started_on | ended_on |
|---|---|---|
| 1 | 2024-01-31 | 2024-07-15 |
| 2 | 2023-11-15 | 2024-02-14 |
| 3 | 2024-02-10 | 2024-02-29 |
Return id, renewal (the start date plus one month, MySQL-style — a 31st clamps to the end of a shorter month), months_active (whole months from start to end) and month_end (the last day of the starting month). Order by months_active descending, then id.
id | renewal | months_active | month_end
1 | 2024-02-29 | 5 | 2024-01-31
2 | 2023-12-15 | 2 | 2023-11-30
3 | 2024-03-10 | 0 | 2024-02-29In this module: Functions
- String functions — CONCAT, SUBSTRING, TRIM and friends
- Numeric functions — ROUND, CEIL, FLOOR, MOD and division
- Dates and times — types, parts and DATE_FORMAT
- Date arithmetic — INTERVAL, DATEDIFF and TIMESTAMPDIFF (this lesson)
- Conditional functions — IF, IFNULL, NULLIF, GREATEST and FIELD
- Checkpoint — Functions
← Dates and times — types, parts and DATE_FORMAT · Conditional functions — IF, IFNULL, NULLIF, GREATEST and FIELD →