Dates, times and time zones
DATETIME versus TIMESTAMP, month arithmetic that clamps, fractional seconds that round, and the half-open range that is both correct and index-friendly.
After this lesson you can
- Choose between DATETIME and TIMESTAMP and explain the time-zone difference
- Predict the result of adding months to an end-of-month date
- Write a date-range filter that misses nothing and can use an index
MySQL has DATE, TIME, DATETIME, TIMESTAMP and YEAR. The two that
cause the interview questions are DATETIME and TIMESTAMP.
DATETIME versus TIMESTAMP
DATETIMEstores the calendar date and clock time you give it, unchanged. It knows nothing about time zones. Range: years 1000 to 9999.TIMESTAMPconverts the value from the sessiontime_zoneto UTC on the way in, and from UTC to the sessiontime_zoneon the way out. Its range ends at2038-01-19 03:14:07UTC.
CREATE TABLE events (id INT PRIMARY KEY, dt DATETIME NULL, ts TIMESTAMP NULL);-- A helper that runs a statement and records the error instead of stopping.CREATE TABLE attempts (n INT AUTO_INCREMENT PRIMARY KEY, statement TEXT, outcome TEXT);CREATE PROCEDURE try_sql(IN stmt TEXT)BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN GET DIAGNOSTICS CONDITION 1 @err_no = MYSQL_ERRNO, @err_msg = MESSAGE_TEXT; INSERT INTO attempts (statement, outcome) VALUES (stmt, CONCAT('error ', @err_no, ': ', REPLACE(@err_msg, CONCAT(DATABASE(), '.'), ''))); END; SET @sql = stmt; PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; INSERT INTO attempts (statement, outcome) VALUES (stmt, 'ok');END;SET time_zone = '+00:00';INSERT INTO events VALUES (1, '2026-03-01 12:00:00', '2026-03-01 12:00:00');-- One second past the TIMESTAMP range, into each column type.CALL try_sql('INSERT INTO events (id, dt) VALUES (2, ''2038-01-19 03:14:08'')');CALL try_sql('INSERT INTO events (id, ts) VALUES (3, ''2038-01-19 03:14:08'')'); SET time_zone = '+04:00';SELECT id, dt, ts, (SELECT GROUP_CONCAT(CONCAT(IF(n = 1, 'DATETIME: ', 'TIMESTAMP: '), outcome) ORDER BY n SEPARATOR ' / ') FROM attempts) AS one_second_past_2038FROM eventsWHERE id = 1;The same text went into both columns at UTC. Read back under +04:00, the
DATETIME is unchanged and the TIMESTAMP has moved four hours. One
second past the 2038 limit, the DATETIME insert succeeds and the
TIMESTAMP insert fails with error 1292. Neither
is wrong; they answer different questions. TIMESTAMP means "this instant",
DATETIME means "this wall-clock reading". A common convention is to keep
every server and session on UTC and store instants in DATETIME, which
avoids the 2038 limit.
Both types take an optional precision for fractional seconds,
DATETIME(3) for milliseconds up to DATETIME(6). Without it, fractions
are rounded, not cut off.
Arithmetic
SELECT '2026-01-31' + INTERVAL 1 MONTH AS jan31_plus_1m, '2024-01-31' + INTERVAL 1 MONTH AS leap_year, TIMESTAMPDIFF(MONTH, '2026-01-31', '2026-02-28') AS whole_months, TIMESTAMPDIFF(YEAR, '2000-02-29', '2026-02-28') AS age, STR_TO_DATE('03/04/2026', '%d/%m/%Y') AS parsed, CAST('2026-03-31 23:59:59.5' AS DATETIME) AS rounded;- Adding a month to 31 January gives the last day of February: MySQL clamps to the end of the shorter month (28th in 2026, 29th in 2024).
TIMESTAMPDIFF(MONTH, ...)counts complete months, so 31 January to 28 February is 0 months.TIMESTAMPDIFF(YEAR, birth, today)is the correct way to compute an age; someone born on 29 February 2000 is still 25 on 28 February 2026.STR_TO_DATEparses with an explicit format, so'03/04/2026'with%d/%m/%Yis 3 April, not 4 March.
The half-open range
To select a month, compare the column with two boundaries: inclusive start, exclusive end of the following period.
WHERE placed_at >= '2026-03-01' AND placed_at < '2026-04-01'
This has no edge cases: it does not care whether the column has fractional
seconds, and it leaves the column bare, so an index on placed_at can be
searched as a range. Wrapping the column, as in WHERE MONTH(placed_at) = 3,
forces MySQL to compute the function for every row. The alternatives with
BETWEEN look equivalent and are not:
CREATE TABLE orders (id INT PRIMARY KEY, placed_at DATETIME(3));INSERT INTO orders VALUES (1, '2026-03-31 09:00:00.000'), (2, '2026-03-31 23:59:59.500'), (3, '2026-04-01 00:00:00.000'), (4, '2026-03-01 00:00:00.000');SELECT (SELECT GROUP_CONCAT(id ORDER BY id) FROM orders WHERE placed_at BETWEEN '2026-03-01' AND '2026-03-31') AS between_dates, (SELECT GROUP_CONCAT(id ORDER BY id) FROM orders WHERE placed_at BETWEEN '2026-03-01' AND '2026-03-31 23:59:59') AS between_235959, (SELECT GROUP_CONCAT(id ORDER BY id) FROM orders WHERE placed_at >= '2026-03-01' AND placed_at < '2026-04-01') AS half_open;BETWEEN '2026-03-01' AND '2026-03-31' means "up to 31 March at midnight"
and drops the whole last day. Ending at '23:59:59' still drops an order
at 23:59:59.500. Only the half-open range finds all three March orders.
Common mistakes
BETWEENwith a date as the upper bound on aDATETIME. It stops at midnight at the start of that day, as order 1 shows.- Ending a range at
23:59:59. Fractional seconds slip past, as order 2 shows. - Casting to a lower precision and expecting truncation.
CAST('2026-03-31 23:59:59.5' AS DATETIME)rounds up to the next day. - Using
TIMESTAMPfor dates far in the future (contract end dates, expiry dates past 2038). The insert fails with error 1292, as the first example shows.
What interviewers ask
DATETIMEorTIMESTAMP?TIMESTAMPconverts through UTC using the session time zone and ends in 2038;DATETIMEstores the value as given.- How do you select all rows from March efficiently? A half-open range on the bare column, which is correct for any precision and can use an index.
Exercise · Pro
Daily totals for March 2026
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade