← 全部文章

在資料庫中儲存時間戳:DATETIME vs INT vs BIGINT

為時間戳選錯欄位類型會導致時區漂移、2038 年溢位、範圍查詢出錯和混亂的 API 輸出。比較 MySQL 和 PostgreSQL 中的原生 datetime 類型、BIGINT 紀元值和字串。

儲存時間戳的三種方式

大多數資料庫至少提供三種選擇:原生 datetime 型別(TIMESTAMP、DATETIME、TIMESTAMPTZ)、普通整數(INT 或 BIGINT),或字串(VARCHAR)。它們在儲存大小、查詢易用性、時區處理和面向未來方面各有取捨。對大多數產品資料庫,原生 datetime 欄位是最佳預設,因為資料庫可以把該值當作時間(而非匿名數字)來比較、索引、截斷、分組和格式化。

  • 原生 datetime 型別 —— 最適合日期算術、時區轉換和可讀性
  • BIGINT 整數 —— 適合高吞吐插入和簡單的數值範圍查詢
  • VARCHAR 字串 —— 幾乎總是錯的:日期的字串比較只對嚴格的 ISO 8601 格式有效
  • INT 整數 —— 除非已徹底核查 2038 年邊界,否則對未來時間戳應避免

MySQL:TIMESTAMP vs DATETIME vs INT

MySQL 有兩種看起來相似但行為差異很大的日期時間型別——其中一種有硬性的到期日。當你想要 UTC 與工作階段時區之間自動轉換時,TIMESTAMP 很方便,但它歷史上的 32 位範圍使其對面向未來的產品資料有風險。DATETIME 按你提供的字面日期時間儲存,當應用在寫入前統一使用 UTC 時通常更清晰。

  • TIMESTAMP:內部以 32 位 Unix 秒儲存 —— 限於 1970-01-01 至 2038-01-19
  • TIMESTAMP:在插入/讀取時自動在 UTC 與工作階段時區之間轉換
  • DATETIME:儲存字面日期時間,無時區。範圍 1000-01-01 至 9999-12-31。不受 Y2038 影響。
  • DATETIME:不轉換時區 —— 你在應用層控制 UTC
  • 建議:新表使用帶顯式 UTC 值的 DATETIME,以避開 2038 年限制

PostgreSQL:TIMESTAMPTZ 是正確選擇

PostgreSQL 的 TIMESTAMP WITH TIME ZONE(TIMESTAMPTZ)在內部以 UTC 微秒儲存時間戳,並在輸出時轉換為工作階段時區。對大多數用例而言,它是最安全、最正確的選擇,因為它表示時間中的真實瞬間。這個名字可能誤導:TIMESTAMPTZ 並不儲存像 America/New_York 這樣的原始時區標籤。它儲存瞬間,然後按當前工作階段時區顯示。

  • TIMESTAMPTZ:儲存 UTC,在輸出時轉換為工作階段時區 —— 可移植且對日光節約時間安全
  • TIMESTAMP(無時區):按字面值儲存且不轉換 —— 僅用於無時區資料
  • EXTRACT(EPOCH FROM col):從任意 TIMESTAMP 欄位以 float 回傳 Unix 秒
  • TO_TIMESTAMP(epoch):把 Unix 秒轉換回 TIMESTAMPTZ

索引與查詢效能

對普通應用表,原生 datetime 欄位與 BIGINT epoch 欄位之間的效能差異很少是決定因素。查詢形態、索引設計、分區和列數更重要。先選擇能正確保留含義的型別,再為應用實際執行的範圍查詢為其建立索引。

  • 三種型別都支援 B-tree 索引和高效的範圍查詢
  • 在超大表的等值和範圍掃描上,BIGINT 整數略快
  • 原生 datetime 型別允許按日期部分的索引查詢:WHERE created_at::date = '2024-01-01'
  • VARCHAR 時間戳的效能最差 —— 字串比較不識別日期

何時適合用 BIGINT 儲存 epoch

當資料呈事件型、以追加為主、且已由另一個系統產生為 Unix 時間時,BIGINT 是合理的。分析管線、遙測流、佇列和精簡的二進位協定常用 epoch 毫秒,因為數值比較快且與語言無關。代價是可讀性:人需要轉換器,而 SQL 的日期算術會更冗長。

  • 如果 JavaScript 用戶端直接產生事件,Unix 毫秒使用 BIGINT
  • 如果來源系統是 Unix 風格且秒精度足夠,Unix 秒使用 BIGINT
  • 在欄位名中記錄單位:created_at_ms 比 created_at_epoch 更清晰
  • 如果分析師需要可讀的 SQL 查詢,加入一個產生的 datetime 欄位
  • 因 32 位範圍限制,面向未來的現代時間戳應避免使用 INT

推薦的綱要設計

對大多數 Web 應用,把瞬間以 UTC 儲存,只有在需要重建本地掛鐘意圖時才單獨儲存使用者偏好的時區。安排在 America/New_York 上午 9:00 的會議,不同於在精確 UTC 瞬間建立的事件日誌;請對這些情況分別建模。

  • 事件日誌:PostgreSQL 用 created_at TIMESTAMPTZ,MySQL 用 UTC 的 created_at DATETIME
  • JavaScript 事件擷取:created_at_ms BIGINT 加上清晰的 API 文件
  • 重複的本地排程:儲存 local_date、local_time 和 timezone_id,再計算下一個瞬間
  • 過期時間戳:expires_at 用原生 datetime,或 expires_at_seconds 用顯式 Unix 秒
  • 稽核表:把 created_at 和 updated_at 都保留為原生 datetime 欄位,以便可讀地偵錯

資料庫時間戳常見問題

MongoDB stores dates natively as the BSON Date type — a signed 64-bit integer of milliseconds since the Unix epoch. The driver maps it to a Date object in JavaScript, datetime in Python, ISODate in the mongo shell.

  • BSON Date: 8 bytes, signed 64-bit, milliseconds since 1970 UTC
  • Indexes natively — range queries on Date are fast and idiomatic
  • Use ISODate("2024-01-01T00:00:00Z") in the shell to create comparable values
  • For multi-precision needs, use Decimal128 or a sub-document with seconds + nanos; Date itself is millisecond precision
  • TTL indexes operate on Date fields specifically — integer epoch fields will not work with expireAfterSeconds

DynamoDB: epoch attributes and TTL

DynamoDB has no dedicated date type. Two patterns dominate: ISO 8601 strings for human readability, or epoch numbers for compactness and TTL support.

  • TTL requires the attribute to be a Number representing Unix seconds (not milliseconds)
  • Strings store ISO 8601 with full timezone info but cost more bytes and sort lexicographically (so include the Z suffix consistently)
  • Range queries on a sort-key epoch number are extremely cheap and align with DynamoDB’s query model
  • Avoid mixing seconds and milliseconds across tables — DynamoDB will not protect you from the unit drift
  • DynamoDB Streams emit an ApproximateCreationDateTime in ISO 8601 regardless of how you stored the original value

FAQ

Should I store UTC or local time in a database?
Store UTC for event timestamps and convert to local time when displaying. Store a timezone identifier separately when the user's local wall-clock intent matters, such as recurring meetings or business hours.
Is BIGINT better than TIMESTAMP?
Not generally. BIGINT is useful for numeric epoch pipelines, but native datetime types are easier for SQL date arithmetic, readable debugging, and timezone-aware output.
Should MySQL use TIMESTAMP or DATETIME?
For new application tables, DATETIME with UTC values is often safer because it avoids the 2038 range limit and does not silently depend on session timezone conversion.
Should I store timestamps as UTC or with a timezone?
Store the instant in UTC (TIMESTAMPTZ in PostgreSQL, or DATETIME with UTC values in MySQL) and convert to local time on display. Keep a separate IANA timezone column only when you must reconstruct a user's local wall-clock intent, such as recurring meetings.