MySQL has five date and time types. Picking between them is mostly one decision — whether the value should move when the time zone changes — and getting that decision wrong is the kind of bug that shifts every timestamp in your database by a few hours without raising an error.
The five types
| Type | Range | Bytes | Zone-aware |
|---|---|---|---|
DATE | 1000-01-01 to 9999-12-31 | 3 | no |
TIME | -838:59:59 to 838:59:59 | 3 | no |
DATETIME | 1000-01-01 to 9999-12-31 | 5+ | no |
TIMESTAMP | 1970-01-01 to 2038-01-19 | 4+ | yes |
YEAR | 1901 to 2155 | 1 | no |
SELECT CAST('2024-03-15 14:30:45.123456' AS DATETIME(6)) AS dt,
CAST('2024-03-15' AS DATE) AS d,
CAST('14:30:45' AS TIME) AS t;+----------------------------+------------+----------+
| dt | d | t |
+----------------------------+------------+----------+
| 2024-03-15 14:30:45.123456 | 2024-03-15 | 14:30:45 |
+----------------------------+------------+----------+TIME is worth a second look: its range is ±838 hours, well beyond a clock. It
represents a duration as readily as a time of day.
DATETIME versus TIMESTAMP
This is the whole lesson. DATETIME stores the literal wall-clock value you gave it
and hands it back unchanged. TIMESTAMP converts to UTC on the way in and back to the
session's time zone on the way out.
CREATE TABLE tz_demo (dt DATETIME, ts TIMESTAMP NULL);
SET time_zone = '+00:00';
INSERT INTO tz_demo VALUES ('2024-06-01 12:00:00', '2024-06-01 12:00:00');
SELECT 'stored at +00:00' AS note, dt, ts FROM tz_demo;+------------------+---------------------+---------------------+
| note | dt | ts |
+------------------+---------------------+---------------------+
| stored at +00:00 | 2024-06-01 12:00:00 | 2024-06-01 12:00:00 |
+------------------+---------------------+---------------------+Identical so far. Now read the same row from a session in a different zone — nothing has been written, only the reader has moved:
SET time_zone = '-07:00';
SELECT 'read at -07:00' AS note, dt, ts FROM tz_demo;+----------------+---------------------+---------------------+
| note | dt | ts |
+----------------+---------------------+---------------------+
| read at -07:00 | 2024-06-01 12:00:00 | 2024-06-01 05:00:00 |
+----------------+---------------------+---------------------+DATETIME is still noon. TIMESTAMP is 05:00, because 12:00 UTC
is 05:00 at -07:00. Both are correct; they answer different questions:
- Use
TIMESTAMPfor a moment in time — when something happened. Two people in different countries should agree it was the same instant. - Use
DATETIMEfor a wall-clock value that should not move. A restaurant opening at 09:00 opens at 09:00 whoever is asking.
The pizza schema stores DATETIME(6) throughout. Its Java entities map to
LocalDateTime, which carries no zone either, so the pairing is consistent — and the
application explicitly turns off the driver's zone conversion to keep it that way. That story, and
the seven hours it cost, is in connections.
2038
TIMESTAMP is a 4-byte signed count of seconds since 1970, so it runs out on
19 January 2038. That is not far away for a column holding a subscription end date
or a scheduled job. DATETIME goes to the year 9999. If you need zone-awareness past
2038, store a DATETIME in UTC and convert in the application, or use
BIGINT epoch milliseconds.
Fractional seconds
Both types take a precision argument from 0 to 6: DATETIME(6) keeps microseconds,
DATETIME alone keeps none and rounds — 12:00:00.6 becomes 12:00:01,
which is a surprising place to lose a second.
The pizza schema uses DATETIME(6) for created/updated columns for a practical
reason: two rows written in the same second are otherwise indistinguishable, and "the most recent
one" stops being answerable. It costs three extra bytes.
Defaults that maintain themselves
CREATE TABLE audit_demo (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6)
);ON UPDATE CURRENT_TIMESTAMP refreshes the column on every modification, with no
trigger and no application code.
The pizza schema deliberately does not use it, and the reason is worth knowing: its timestamps are maintained by JPA lifecycle callbacks instead, so the behaviour lives in one place, in the Java, and works identically on every database. Splitting it — some columns maintained by the database, some by the application — is how you end up debugging why one of them did not update. Either mechanism is fine; pick one.
Writing dates
Use 'YYYY-MM-DD' and 'YYYY-MM-DD HH:MM:SS'. MySQL accepts a
remarkable variety of other formats and its guesses are not always yours. For anything else,
parse explicitly with STR_TO_DATE:
SELECT STR_TO_DATE('15/03/2024', '%d/%m/%Y') AS parsed;Under the default sql_mode an impossible date is an error rather than a silent
zero. Older MySQL accepted '0000-00-00', which is not a date and breaks every client
library that tries to read it; if you meet one in a legacy schema, it is
NO_ZERO_DATE being off.
The indexing trap
-- cannot use an index on created_at
SELECT COUNT(*) FROM customer_order WHERE DATE(created_at) = '2024-06-01';
-- can
SELECT COUNT(*) FROM customer_order
WHERE created_at >= '2024-06-01' AND created_at < '2024-06-02';Wrapping the column in a function makes the index unusable, because the index stores
created_at and the query asks about DATE(created_at). Half-open ranges
are the habit worth forming — and see BETWEEN for why
BETWEEN on a DATETIME quietly loses most of the last day.
Pulling a date apart
SELECT DATE('2024-03-15 14:30:45') AS date_part,
TIME('2024-03-15 14:30:45') AS time_part,
YEAR('2024-03-15') AS y, MONTH('2024-03-15') AS m, DAY('2024-03-15') AS d;+------------+-----------+------+------+------+
| date_part | time_part | y | m | d |
+------------+-----------+------+------+------+
| 2024-03-15 | 14:30:45 | 2024 | 3 | 15 |
+------------+-----------+------+------+------+Formatting and arithmetic — DATE_FORMAT, DATE_ADD,
DATEDIFF, TIMESTAMPDIFF — are covered in
DATE_FORMAT and date functions.
What to remember
DATETIMEdoes not move with the time zone;TIMESTAMPdoes.TIMESTAMPends in 2038.- Add
(6)unless you want your times rounded to the second. - Keep the column bare in
WHERE, and use half-open ranges.