MySQL Date and Time Functions
Date and time manipulation is one of the most common tasks in SQL — reporting periods, calculating age, scheduling jobs, or filtering by date ranges. MySQL provides a comprehensive set of functions that make this straightforward once you understand the key concepts.
TIMESTAMP vs DATETIME — Timezone Handling
The most important distinction in MySQL date handling is between TIMESTAMP and DATETIME:
-
TIMESTAMP stores the value as UTC internally. When you insert a value, MySQL converts from the current session timezone to UTC for storage. When you read it back, MySQL converts from UTC to the session timezone. TIMESTAMP is the right type for "when did this event happen" — it stays correct across timezone changes.
-
DATETIME stores the value literally as entered, with no timezone conversion. If you store '2024-07-15 14:30:00' it reads back as '2024-07-15 14:30:00' regardless of timezone settings. Use DATETIME for "calendar" values where timezone semantics do not apply (e.g., a scheduled appointment, a birth date with time).
-- TIMESTAMP: UTC storage, timezone-converted display CREATE TABLE events_ts ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ); -- DATETIME: literal storage, no conversion CREATE TABLE appointments ( id INT NOT NULL AUTO_INCREMENT, patient_id INT NOT NULL, appt_time DATETIME NOT NULL, -- stored as-is, no UTC conversion PRIMARY KEY (id) ); -- Check session timezone SELECT @@session.time_zone; -- Change session timezone (affects TIMESTAMP display, not DATETIME) SET time_zone = '+05:30'; SELECT created_at FROM events_ts; -- shows time in IST now
SET time_zone = '+00:00' at connection time to avoid timezone confusion.NOW() vs SYSDATE()
NOW() and SYSDATE() both return the current datetime, but they behave differently inside
stored procedures and long-running queries:
- NOW() is evaluated once when the statement starts executing. All calls to
NOW()within the same statement return the same value. - SYSDATE() calls the system clock at the exact moment the function is evaluated — so two calls within the same statement may return different values.
This difference matters for replication: statement-based replication replays SQL statements.
NOW() is deterministic per-statement (safe). SYSDATE() is non-deterministic and requires
row-based replication or the --sysdate-is-now server option.
-- NOW() is fixed per statement SELECT NOW(), SLEEP(2), NOW(); -- Both NOW() calls return the same time -- SYSDATE() evaluates at call time SELECT SYSDATE(), SLEEP(2), SYSDATE(); -- Second SYSDATE() is 2 seconds later -- Practical: use NOW() for created_at defaults (deterministic) INSERT INTO orders (customer_id, created_at) VALUES (42, NOW());
NOW() over SYSDATE() in production. It is deterministic per statement, safer for replication, and produces consistent results within a transaction.Getting the Current Date and Time
SELECT NOW(); -- '2024-07-15 14:30:00' (date and time) SELECT CURDATE(); -- '2024-07-15' (date only) SELECT CURRENT_DATE(); -- '2024-07-15' (alias for CURDATE) SELECT CURTIME(); -- '14:30:00' (time only) SELECT CURRENT_TIME(); -- '14:30:00' (alias for CURTIME) SELECT UTC_DATE(); -- '2024-07-15' (UTC date) SELECT UTC_TIMESTAMP(); -- '2024-07-15 18:30:00' (UTC datetime)
DATE_FORMAT — Format Specifier Reference
DATE_FORMAT(date, format) converts a date/datetime to a formatted string using % specifiers.
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d'); -- '2024-07-15' SELECT DATE_FORMAT(NOW(), '%d/%m/%Y'); -- '15/07/2024' SELECT DATE_FORMAT(NOW(), '%M %d, %Y'); -- 'July 15, 2024' SELECT DATE_FORMAT(NOW(), '%W, %M %e, %Y'); -- 'Monday, July 15, 2024' SELECT DATE_FORMAT(NOW(), '%h:%i %p'); -- '02:30 PM' SELECT DATE_FORMAT(NOW(), '%Y-%m'); -- '2024-07' (year-month key) SELECT DATE_FORMAT(NOW(), '%Y-Q%Q'); -- '2024-Q3' (fiscal quarter label) -- Group sales by month for reporting SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, COUNT(*) AS order_count, SUM(total) AS revenue FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY month;
Specifier | Description | Example |
|---|---|---|
%Y | Four-digit year | 2024 |
%y | Two-digit year | 24 |
%m | Month 01-12 | 07 |
%M | Month name | July |
%b | Month abbreviation | Jul |
%d | Day of month 01-31 | 15 |
%e | Day of month 1-31 (no padding) | 15 |
%W | Weekday name | Monday |
%a | Weekday abbreviation | Mon |
%w | Day of week (0=Sunday) | 1 |
%H | Hour 00-23 | 14 |
%h | Hour 01-12 | 02 |
%i | Minutes 00-59 | 30 |
%s | Seconds 00-59 | 00 |
%p | AM or PM | PM |
%Q | Quarter 1-4 | 3 |
%U | Week number (Sunday start) | 28 |
%V | ISO week number | 29 |
STR_TO_DATE — Parsing User Input
STR_TO_DATE(string, format) parses a string into a DATE or DATETIME using the same format specifiers as DATE_FORMAT(). Essential when importing data with non-standard date formats.
SELECT STR_TO_DATE('15/07/2024', '%d/%m/%Y');
-- '2024-07-15'
SELECT STR_TO_DATE('July 15 2024', '%M %d %Y');
-- '2024-07-15'
SELECT STR_TO_DATE('07-15-2024 2:30 PM', '%m-%d-%Y %h:%i %p');
-- '2024-07-15 14:30:00'
-- Fix imported data with wrong date format
UPDATE orders
SET order_date = STR_TO_DATE(order_date_raw, '%d-%m-%Y')
WHERE order_date IS NULL AND order_date_raw IS NOT NULL;DATE_ADD and DATE_SUB — INTERVAL Units
These functions add or subtract a time interval from a date or datetime value. MySQL supports all the following INTERVAL units:
SELECT DATE_ADD('2024-07-15', INTERVAL 30 DAY); -- '2024-08-14'
SELECT DATE_ADD('2024-07-15', INTERVAL 3 MONTH); -- '2024-10-15'
SELECT DATE_ADD('2024-07-15', INTERVAL 1 YEAR); -- '2025-07-15'
SELECT DATE_ADD(NOW(), INTERVAL 2 HOUR); -- 2 hours from now
SELECT DATE_ADD(NOW(), INTERVAL 30 MINUTE);
SELECT DATE_ADD(NOW(), INTERVAL 45 SECOND);
SELECT DATE_ADD(NOW(), INTERVAL '1 30' HOUR_MINUTE); -- 1h 30m
SELECT DATE_ADD(NOW(), INTERVAL '2 12:30:00' DAY_SECOND);
-- Find subscriptions expiring in the next 7 days
SELECT user_id, expires_at
FROM subscriptions
WHERE expires_at BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 7 DAY);TIMESTAMPDIFF — Age and Duration Calculations
TIMESTAMPDIFF(unit, start, end) is more versatile than DATEDIFF() — it handles month and year
boundaries correctly and supports YEAR, MONTH, WEEK, DAY, HOUR, MINUTE, and SECOND.
-- Age in years
SELECT TIMESTAMPDIFF(YEAR, '1990-03-15', CURDATE()) AS age_years;
-- Duration in months
SELECT TIMESTAMPDIFF(MONTH, '2024-01-01', '2024-07-15') AS months; -- 6
-- Days until New Year
SELECT TIMESTAMPDIFF(DAY, NOW(), '2025-01-01 00:00:00') AS days_to_ny;
-- Subscription duration
SELECT
user_id,
TIMESTAMPDIFF(DAY, start_date, COALESCE(end_date, CURDATE())) AS active_days
FROM subscriptions;
-- Age verification with category
SELECT
name,
birth_date,
TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age,
CASE
WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) >= 18 THEN 'Adult'
ELSE 'Minor'
END AS category
FROM customers;Business Day and Fiscal Quarter Calculations
MySQL has no built-in business-day function, but you can calculate working days by subtracting weekends. For fiscal year quarters (often not aligned with calendar quarters), use CASE expressions.
-- Rough business day count between two dates (excludes weekends)
-- Note: does not account for holidays
SELECT
start_dt,
end_dt,
FLOOR(DATEDIFF(end_dt, start_dt) / 7) * 5
+ MID('0123444401233334012222340111123400001234000123440',
WEEKDAY(start_dt) * 7 + WEEKDAY(end_dt) + 1, 1) AS business_days
FROM date_ranges;
-- Fiscal year quarter (Q1 = April-June, common in UK/India)
SELECT
order_date,
CASE
WHEN MONTH(order_date) IN (4,5,6) THEN 'Q1'
WHEN MONTH(order_date) IN (7,8,9) THEN 'Q2'
WHEN MONTH(order_date) IN (10,11,12) THEN 'Q3'
ELSE 'Q4'
END AS fiscal_quarter
FROM orders;Common Date Query Patterns
This week's orders:
SELECT * FROM orders WHERE order_date >= DATE(NOW() - INTERVAL WEEKDAY(NOW()) DAY) AND order_date < DATE(NOW() - INTERVAL WEEKDAY(NOW()) DAY) + INTERVAL 7 DAY;
Last month's revenue:
SELECT SUM(total) AS last_month_revenue FROM orders WHERE order_date >= DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 MONTH), '%Y-%m-01') AND order_date < DATE_FORMAT(NOW(), '%Y-%m-01');
Rolling 30-day active users:
SELECT COUNT(DISTINCT user_id) AS active_users_30d FROM user_activity WHERE activity_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);
Month-over-month revenue comparison:
WITH monthly AS (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(total) AS revenue
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
)
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month) * 100, 2
) AS pct_change
FROM monthly;UNIX_TIMESTAMP and FROM_UNIXTIME
SELECT UNIX_TIMESTAMP(); -- e.g. 1721051400
SELECT UNIX_TIMESTAMP('2024-07-15 14:30:00'); -- specific datetime to Unix
SELECT FROM_UNIXTIME(1721051400); -- '2024-07-15 14:30:00'
SELECT FROM_UNIXTIME(1721051400, '%Y-%m-%d'); -- '2024-07-15' (formatted)
-- Convert legacy INT timestamps to readable dates
SELECT name, FROM_UNIXTIME(created_ts) AS created_at FROM legacy_events;DATETIME or TIMESTAMP column types over storing Unix timestamps as integers. The 32-bit Unix timestamp rolls over in 2038, and MySQL's TIMESTAMP type handles timezone conversion automatically.Quick Reference
Function | Returns | Notes |
|---|---|---|
NOW() | Current datetime | Evaluated once per statement; use in production |
SYSDATE() | Current datetime | Evaluated at call time; avoid in replicated setups |
CURDATE() | Current date | Alias: CURRENT_DATE() |
CURTIME() | Current time | Alias: CURRENT_TIME() |
DATE_ADD(d, INTERVAL n u) | Date plus interval | Units: MICROSECOND to YEAR |
DATEDIFF(d1, d2) | Days between (d1 - d2) | Integer result; use TIMESTAMPDIFF for other units |
TIMESTAMPDIFF(u, d1, d2) | Difference in unit | Handles month/year boundaries correctly |
DATE_FORMAT(d, fmt) | Formatted string | Uses % specifiers like %Y-%m-%d |
STR_TO_DATE(s, fmt) | String to DATE/DATETIME | For parsing non-standard date imports |
EXTRACT(unit FROM d) | Integer component | YEAR, MONTH, DAY, HOUR... |
LAST_DAY(d) | Last day of the month | Handles leap years automatically |
UNIX_TIMESTAMP(d) | Seconds since epoch | 32-bit; use TIMESTAMP column type instead |
FROM_UNIXTIME(n) | Datetime from epoch | Optional format string argument |