ログの 1790550000 は何時か:UNIX時間とミリ秒、2038年問題
UNIXタイムスタンプが何を数えているのか、10桁と13桁の違い、タイムスタンプにタイムゾーンがないという意味、うるう秒と2038年問題、データベースへの保存方法を例とともにまとめました。
障害対応でログを開くと、こんな行があります。
{"event":"payment_failed","created_at":1790550000}
「これ、何時のことですか」と聞かれたら、答えは日本時間の2026年9月28日午前8時ちょうどです。ところが別のサービスは同じ出来事を 1790550000000 と記録していて、これを秒のつもりで読むと、答えは5万年以上先の日時になります。UNIX時間は単純なぶん、取り違えも起きやすいのです。
1970年1月1日0時から数えた秒
UNIX時間(エポック秒)は、1970年1月1日 00:00:00 UTC から経過した秒数です。この基準の瞬間をエポックと呼びます。1,790,550,000 秒たつと 2026年9月27日 23:00:00 UTC になり、日本時間は UTC より9時間進んでいるので、9月28日の午前8時です。
日時を1つの数で扱うと便利なことがたくさんあります。2つの時刻の差は引き算1回で求まり、並べ替えは数値の並べ替えで済み、国ごとに違う日付の書き方を気にする必要もありません。OS やデータベース、多くのプログラミング言語が内部でこの方式を使っているのはそのためです。
桁数で単位の見当をつける
同じ瞬間でも、秒で書くかミリ秒で書くかで数は1000倍違います。
| 値 | 単位 | UTC |
|---|---|---|
1790550000 |
秒(10桁) | 2026-09-27 23:00:00 |
1790550000000 |
ミリ秒(13桁) | 2026-09-27 23:00:00 |
1790550000 をミリ秒と読み違えると |
1970-01-21 17:22:30 |
C の time()、Python の time.time()、シェルの date +%s は秒を返します。JavaScript の Date.now() と Java の System.currentTimeMillis() はミリ秒です。サーバーは Python、画面は JavaScript というサービスでは、この2つが混ざりやすくなります。
見分ける手がかりは桁数です。現在に近い時刻なら、秒は10桁、ミリ秒は13桁です。日付が1970年1月になったら秒をミリ秒として読んでいないか、数万年後になったらその逆ではないかを、まず疑ってみる価値があります。
数値にはタイムゾーンがない
1790550000 は、東京で見てもロンドンで見ても同じ瞬間です。タイムスタンプそのものには「日本時間」や「UTC」という情報は含まれていません。タイムゾーンが登場するのは、この数を人が読む日時に変換するときです。
const d = new Date(1790550000 * 1000);
d.toISOString(); // "2026-09-27T23:00:00.000Z"
d.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" }); // "2026/9/28 8:00:00"
「日本時間のタイムスタンプに直してください」という依頼が成り立たないのはこのためです。9時間分を足した値を別に保存すると、その数はもう UNIX時間ではなくなり、あとでどちらの値なのか誰にもわからなくなります。
うるう秒は数えない
地球の自転はわずかに不規則なので、協定世界時(UTC)にはときどき1秒を挿入するうるう秒がありました。最後は2017年1月1日 8:59:60(日本時間)、つまり 2016年12月31日 23:59:60 UTC です。ところが POSIX は、UNIX時間では1日を正確に86,400秒として計算すると定めています。うるう秒は数えないということです。
そのため UNIX時間は、厳密には1972年以降に挿入された27回のうるう秒の分だけ、実際に経過した秒数より少なくなっています。うるう秒の瞬間には同じタイムスタンプが2回現れるか、Google のようにその日1日をかけて1秒を少しずつ吸収する方式(リープスミア)がとられます。ほとんどのアプリケーションには影響しませんが、タイムスタンプの差で厳密な時間間隔を測るなら知っておきたい点です。
2038年1月19日 12時14分7秒(日本時間)
古いシステムは UNIX時間を符号付き32ビット整数に入れていました。この整数に入る最大の値は 2,147,483,647 で、その瞬間は 2038年1月19日 03:14:07 UTC、日本時間では同じ日の12時14分7秒です。さらに1秒たつと値があふれて最小の −2,147,483,648 に戻り、日付としては1901年12月13日になります。
64ビットの OS や最近の言語はほとんどが64ビットで時刻を扱うので、この限界からは抜け出しています。残るリスクは目立たないところにあります。32ビットの組み込み機器、古いファイル形式、そしてデータベースの型です。たとえば MySQL の TIMESTAMP 型は、範囲が 2038-01-19 03:14:07 UTC で終わります。いま契約する35年の住宅ローンの完済日は、とっくにこの日を越えています。2038年は遠い未来の話ではなく、今日保存するデータの問題です。
データベースに保存するとき
時刻を保存するときの原則は、おおむね1つにまとまります。UTC の瞬間として保存し、表示するときに利用者のタイムゾーンに変換することです。
PostgreSQL なら timestamp with time zone(timestamptz)がこの方式です。名前に反してタイムゾーンを保存するわけではなく、入ってきた値を UTC に変換して保存し、取り出すときにセッションのタイムゾーンで表示します。タイムゾーンのない timestamp に日本時間をそのまま入れると、サーバーを移したり海外の利用者が増えたりした時点で、その値がどのタイムゾーンだったのかわからなくなります。
文字列でやり取りするなら、オフセット付きの ISO 8601 が安全です。2026-09-28T08:00:00+09:00 と 2026-09-27T23:00:00Z は同じ瞬間で、どちらもオフセットが書かれているので誤解の余地がありません。2026-09-28 08:00:00 のようにオフセットのない文字列は、受け取る側が好きなように解釈します。
例外が1つあります。「毎週月曜の午前9時の会議」のような未来の壁時計の時刻は、瞬間ではありません。日本にはサマータイムがありませんが、ニューヨークやロンドンの参加者がいれば、同じ9時が季節によって UTC では違う時刻になります。こうした値は、現地の時刻とタイムゾーン名(America/New_York など)をセットで保存します。
その場で確かめる
UNIX時間変換ツールに秒かミリ秒を入れると、東京・ソウル・UTC などの日時をまとめて表示し、逆に日時をタイムスタンプに変換することもできます。この記事の 1790550000 と 2147483647 を入れてみてください。現在の UNIX時間もリアルタイムで表示されます。
参考
- IEEE Std 1003.1(POSIX)Base Definitions「Seconds Since the Epoch」
- ECMAScript 仕様(ECMA-262)Time Values and Time Range
- ISO 8601『Date and time — Representations for information interchange』、RFC 3339
- MySQL 8.0 リファレンスマニュアル「The DATE, DATETIME, and TIMESTAMP Types」
- PostgreSQL 文書「日付/時刻データ型」