PlanetScale — Datetimes versus timestamps in MySQL¶
Summary¶
Short PlanetScale pedagogical post by Aaron Francis
(originally published 2023-06-22, re-surfaced via the
2026-04-21 feed snapshot) canonicalising the five
temporal column types MySQL offers
for storing dates, times, and combined points in time —
DATE, DATETIME, TIMESTAMP, YEAR, TIME — with
their storage costs, representable ranges, and
semantic trade-offs. The post's central comparison is
the DATETIME vs TIMESTAMP choice: both store a
date + time to 1-second resolution, but TIMESTAMP is
4 bytes (half the 8-byte DATETIME size) at the
cost of a narrow range (1970-01-01 00:00:00 to
2038-01-19 03:14:17 — the canonical
Year 2038 problem on
signed-32-bit-int Unix seconds) and implicit timezone
conversion ("MySQL converts TIMESTAMP values from the
current time zone to UTC for storage, and back from UTC
to the current time zone for retrieval. (This does not
occur for other types such as DATETIME.)"). A worked
example demonstrates the timezone asymmetry: a row
inserted as 2029-02-14 08:47 under
SET SESSION time_zone = '+00:00' returns verbatim on
read, but re-queried under SET SESSION time_zone =
'-05:00' yields 2029-02-14 03:47 in the TIMESTAMP
column (shifted 5 hours) while the DATETIME column
still returns 2029-02-14 08:47. Production guidance:
use TIMESTAMP only for application-populated
event-time columns (created_at, updated_at,
deleted_at, archived_at, posted_at) where the
value is always the server-current time and therefore
won't exceed 2038 in practice; use DATETIME for any
field where the date may be far-future (e.g.
user-entered birthdays, planned events beyond 2038) or
where you need timezone-neutral storage. DATE
(3 bytes, year 1000 to 9999) covers pure-date cases
like birthdays; TIME (3 bytes, -838:59:59 to
838:59:59 — ~35 days in either direction to
support interval arithmetic as well as wall-clock
time-of-day); YEAR (1 byte, 1901 to 2155) is the
rare niche. No marketing, no PlanetScale-specific
machinery — straight MySQL pedagogy.
Key takeaways¶
- Five temporal column types, each optimised for a different shape of temporal data. Summary table (byte sizes + legal ranges from the post):
| Column | Data | Bytes | Min | Max |
|---|---|---|---|---|
| DATE | Date only | 3 | 1000-01-01 | 9999-12-31 |
| DATETIME | Date + time | 8 | 1000-01-01 00:00:00 | 9999-12-31 23:59:59 |
| TIMESTAMP | Date + time | 4 | 1970-01-01 00:00:00 | 2038-01-19 03:14:17 |
| YEAR | Year only | 1 | 1901 | 2155 |
| TIME | Time only | 3 | -838:59:59 | 838:59:59 |
Schema-design decision tree: date-only → DATE;
year-only → YEAR; time-only or duration-interval
→ TIME; combined date + time → either
DATETIME or TIMESTAMP (see takeaways 2–4).
(Source: article body, "there are five column
types that you can use to store temporal data in
MySQL.")
-
TIMESTAMPis 4 bytes vsDATETIME's 8 bytes — exactly half the storage for combined date+time data. For high-cardinality event-log tables (billions of rows ×created_at+updated_at+deleted_atcolumns), the saving is non-trivial at scale. Canonical schema-design guidance from the post: "Storage space is cheap, but when designing table schemas, we always want to choose the smallest possible column type that fits the full range of our data. When creating your tables, there is no need to be generous!" -
TIMESTAMP's narrow range IS the Year 2038 Problem. The 4-byte size comes from MySQL representingTIMESTAMPinternally as a signed-32-bit-int count of Unix seconds since1970-01-01 00:00:00 UTC— the canonical Unix epoch. The2038-01-19 03:14:17ceiling is exactly2^31 - 1seconds after the epoch, at which point the counter overflows. "The 2038 issue arises in systems that calculate Unix time … These systems use a signed 32-bit integer to store this time, which can only hold integer values between-2^31and2^31 - 1." Also known as Y2038, Y2K38, or The Epochalypse. For anyTIMESTAMPcolumn that could ever hold a post-2038 value (user-entered future dates, planned-event dates, long-lived contracts, lifetime warranties, birthdays of children born after 2007 who will hit 30+ post-2038),TIMESTAMPis disqualified — useDATETIME(range extends to9999-12-31 23:59:59). -
TIMESTAMPperforms implicit timezone conversion;DATETIMEdoes not. From the MySQL 8.0 documentation (quoted in the post): "MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.) By default, the current time zone for each connection is the server's time." This makesTIMESTAMPthe correct choice for timezone-aware event logging on a UTC-server infrastructure (values land normalised to UTC regardless of client timezone; reads get localised to the client's zone automatically) — but a trap if any production or replica server has a non-UTC timezone: reads shift under the reader's timezone silently.DATETIMEis timezone-naïve — the bytes stored are the bytes returned, unchanged, regardless ofSET SESSION time_zone = .... -
Worked proof of the timezone asymmetry.
TheCREATE TABLE timezone_test ( `timestamp` TIMESTAMP, `datetime` DATETIME ); SET SESSION time_zone = '+00:00'; INSERT INTO timezone_test VALUES ('2029-02-14 08:47', '2029-02-14 08:47'); SELECT * FROM timezone_test; -- | timestamp | datetime | -- |---------------------|---------------------| -- | 2029-02-14 08:47:00 | 2029-02-14 08:47:00 | SET SESSION time_zone = '-05:00'; SELECT * FROM timezone_test; -- | timestamp | datetime | -- |---------------------|---------------------| -- | 2029-02-14 03:47:00 | 2029-02-14 08:47:00 |TIMESTAMPcolumn shifts 5 hours under the new session timezone (stored UTC value2029-02-14 08:47:00minus 5 hours =2029-02-14 03:47:00at-05:00); theDATETIMEcolumn is unchanged. (Source: article body, "Let's change our session timezone to-05:00now and see what happens.") -
Production rule of thumb: use
TIMESTAMPfor application-populated event-time columns —updated_at,created_at,deleted_at,archived_at,posted_at— because "as long as these columns record the current time at which an event happened, you don't have to worry about the 2038 problem for a long time." UseDATETIMEfor user-input date fields, future-planned events, anniversaries, or anything that may exceed 2038 — or alternatively validate at the application layer that the input falls in the 1970–2038 window before writing to aTIMESTAMP. -
TIMEcovers wall-clock AND interval storage. Legal range-838:59:59to838:59:59— roughly 35 days in either direction, far wider than the00:00:00–23:59:59wall-clock range. This is a deliberate design choice soTIMEcan store both "a time of day" and "an interval of time" in the same type, which is unusual among databases. Sign support enables subtraction semantics (TIMEDIFF('2029-02-14 08:00', '2029-02-15 08:00')=-24:00:00). -
DATE's massive range — year 1000 through 9999, in 3 bytes — makes it effectively unlimited for any real-world application. Use it without hesitation for birthdays, anniversaries, employee start dates, calendar days, any domain where only the date component matters. -
YEARis the least-used type — 1 byte, range 1901 to 2155. "Not widely used, but it serves its purpose exceedingly well when you need it." Outside the supported range, migrate to a signed or unsigned small integer for broader year storage.
Systems / concepts / patterns extracted¶
- Systems: MySQL (the relational database whose temporal-type catalogue the post canonicalises).
- Concepts:
MySQL temporal
types (the 5-type catalogue
DATE/DATETIME/TIMESTAMP/YEAR/TIMEwith sizes + ranges); DATETIME vs TIMESTAMP in MySQL (the canonical schema-design comparison: 8B vs 4B storage, broad vs narrow range, timezone-naïve vs timezone-converting); [[concepts/ year-2038-problem|Year 2038 Problem]] (Unix-epoch signed-32-bit-int rollover, the structural reason forTIMESTAMP's2038-01-19 03:14:17ceiling). - Patterns: None. The post is pedagogical
reference material — schema-design conventions
("choose the smallest possible column type that
fits the full range of your data", "use
TIMESTAMPfor application-populated event-time columns") are documented in-page on thedatetime-vs-timestamp-mysqlconcept rather than as reusable cross-system patterns.
Operational numbers¶
- 5 temporal column types in MySQL:
DATE,DATETIME,TIMESTAMP,YEAR,TIME. - Storage sizes:
YEAR1 byte;DATE3 bytes;TIME3 bytes;TIMESTAMP4 bytes;DATETIME8 bytes. 2× storage saving forTIMESTAMPvsDATETIMEat the cost of range. TIMESTAMPrange:1970-01-01 00:00:00to2038-01-19 03:14:17—2^31 - 1= 2,147,483,647 seconds after the Unix epoch.DATETIMErange:1000-01-01 00:00:00to9999-12-31 23:59:59— effectively unlimited for real-world applications.DATErange:1000-01-01to9999-12-31— same broad range asDATETIMEwithout the time component.YEARrange: 1901 to 2155 (255 years).TIMErange:-838:59:59to838:59:59— approximately ±35 days in either direction, supporting both wall-clock time and interval arithmetic.- Y2038 overflow:
2^31 - 1seconds = January 19, 2038 at 03:14:07 UTC.
Caveats¶
- Pedagogy voice, not a production retrospective — no incident narrative, no failure modes at scale, no PlanetScale-specific temporal-type handling disclosed. Same class as the already-ingested sources/2026-04-21-planetscale-character-sets-and-collations-in-mysql post (Aaron Francis pedagogy on MySQL schema primitives).
- No fractional-second precision discussion —
MySQL 5.6.4+ supports
DATETIME(6)/TIMESTAMP(6)for microsecond precision at the cost of extra storage (0 to +3 bytes depending on the precision chosen). The post ignores this entirely — all examples use 1-second resolution. - No
NOW()/CURRENT_TIMESTAMP/ON UPDATE CURRENT_TIMESTAMPdefault-expression coverage — theupdated_atpattern cited asTIMESTAMP's canonical use case typically relies onON UPDATE CURRENT_TIMESTAMPfor auto-update semantics, but the post doesn't describe the mechanism. - No index-size implications —
DATETIME's 8 bytes vsTIMESTAMP's 4 bytes affects B-tree index page density; at high cardinality (billion-row event tables), this materially changes the InnoDB buffer pool footprint of the index. The post doesn't quantify. - No migration guidance — converting
TIMESTAMP→DATETIMEon a production table is anALTER TABLEthat rewrites the table; the guidance thatTIMESTAMPmight be the wrong choice for far-future dates doesn't touch how to migrate an existingTIMESTAMPcolumn safely. - Server-timezone configuration elided — the
post notes "by default, the current time zone for
each connection is the server's time" but doesn't
walk through how to set
time_zone = 'UTC'at the server level (my.cnf,SET GLOBAL time_zone, or OS-levelTZ=UTC mysqld) to avoid the silent-shift trap in replicated / multi-region deployments. - No Y2038 solution framing — the post quips
"surely, by then, we'll have a solution for it.
(Surely, right?)" but doesn't mention that MySQL
has not yet committed to extending
TIMESTAMPbeyond 32-bit Unix seconds, nor that 64-bit Unix time (time64_t) is the standard Linux / glibc migration path for the broader Y2038 problem.TIMESTAMPremains a walking bug that will need an explicit migration before 2038. - PostgreSQL comparison omitted — the post is
MySQL-only; doesn't note that Postgres's
timestamp(without time zone) maps toDATETIME,timestamptz(with time zone) stores UTC internally with a timezone-aware read semantic similar to but not identical to MySQL'sTIMESTAMP, and Postgres'sintervaltype is a dedicated duration type avoiding MySQLTIME's overloaded role. - No discussion of how MySQL handles pre-1970
values on
TIMESTAMP— theTIMESTAMPrange begins at1970-01-01 00:00:00; historical dates (birthdays for anyone born before 1970, historical events) cannot be stored inTIMESTAMPat all — must useDATEorDATETIME.
Source¶
- Original: https://planetscale.com/blog/datetimes-vs-timestamps-in-mysql
- Raw markdown:
raw/planetscale/2026-04-21-datetimes-versus-timestamps-in-mysql-eee7901f.md