
同一个时刻,在 JavaScript 里是一个 Date 对象,写进 JSON 变成一段字符串,落到 SQLite 里又只是某列的一串字符。三个地方都叫时间,形态却不一样。要定的那个东西,是存下来之后还能不能直接拿去比大小。
先从入口看。同样的日期,字面量写法不同,时区处理就不同。
new Date("2026-10-08") // 纯日期:按 UTC 午夜解析
new Date("2026-10-08 10:00:00") // 带空格的时间:按本机时区解析
new Date("2026-10-08T10:00:00") // 带 T 的时间:也按本机时区解析
== 1. 同一串日期,两种字面量的时区处理不一样 ==
new Date("2026-10-08")
toISOString 2026-10-08T00:00:00.000Z
getTime() 1791417600000
本机展示 2026-10-08 08:00:00
new Date("2026-10-08 10:00:00")
toISOString 2026-10-08T02:00:00.000Z
getTime() 1791424800000
本机展示 2026-10-08 10:00:00
new Date("2026-10-08T10:00:00")
toISOString 2026-10-08T02:00:00.000Z
getTime() 1791424800000
本机展示 2026-10-08 10:00:00
2026-10-08 里没有时间部分,规范把它当成 UTC 的零点,本机在 +08:00,展示出来就是当天早上八点。后两行带时间,按本机时区解释,换算出的 getTime 才对得上。纯日期和带时间走的是两条路,中间差出整整八小时。
再往里一层,Date 只要过一次 JSON.parse,出来就不是 Date 了。
const d = new Date("2026-10-08T02:00:00Z");
JSON.stringify(d);
JSON.parse(JSON.stringify({ at: d })).at;
== 2. Date 过一遍 JSON,回来的是字符串 ==
JSON.stringify(new Date('2026-10-08T02:00:00Z'))
"2026-10-08T02:00:00.000Z"
JSON.parse(JSON.stringify({ at: d })).at
值 "2026-10-08T02:00:00.000Z"
typeof string
是不是 Date false
toString 2026-10-08T02:00:00.000Z <- 字符串没有 getTime
序列化那一步走的是 toJSON,而 Date 的 toJSON 就是 toISOString,结果是一条带 Z 的 UTC 串。反序列化不会反查类型,JSON 里没有 Date 这个概念,回来还是字符串。两端都用 JSON 收发的系统,时间字段的真实形态就是字符串,谁要拿它当时间用,谁就得自己再 new Date 一次。
字符串化顺带带出另一个细节,精度。Date 内部只到毫秒。
new Date("2026-10-08T02:00:00.123456Z").toISOString();
new Date("2026-10-08T02:00:00.123456Z").getTime();
== 3. Date 只有毫秒,微秒进来就被截掉 ==
输入 2026-10-08T02:00:00.123456Z
toISOString 2026-10-08T02:00:00.123Z
getTime() 1791424800123
微秒 123000 之后的数字没了
输入里毫秒后面还有 456 微秒,toISOString 只留三位。SQLite 的文本列能把 .123456 原样存住;一旦被 JS 读出来转成 Date,微秒就没了。毫秒级够用的场景无所谓,日志或流水按微秒排序的,这一截是真的丢。
SQLite 这边最容易被名字骗。把列类型写成 DATETIME,它不会变成日期类型,只是个列名。
CREATE TABLE event (id INTEGER PRIMARY KEY, dt DATETIME, ts INTEGER);
INSERT INTO event (dt, ts) VALUES ('2026-10-08 10:00:00', 1791424800000);
SELECT typeof(dt), typeof(ts) FROM event;
== 4. SQLite 的 DATETIME 不是类型,只是列名 ==
建表 CREATE TABLE event (dt DATETIME, ts INTEGER)
写入 dt='2026-10-08 10:00:00'
typeof(dt) text
typeof(ts) integer
== 9. sqlite3 命令行里,DATETIME 列同样退回 TEXT ==
typeof(dt) 与值:
text|2026-10-08 10:00:00
PRAGMA table_info 的 type 只是声明字面量:
0|dt|DATETIME|0||0
typeof 回来是 text。SQLite 的动态类型按写进去的值定,列上的 DATETIME 只是给读代码的人看的声明,命令行里 PRAGMA 打出来照样是那个字面量。用 DATETIME 列存时间这句话,落到 SQLite 里等于用一列文本存时间。
存文本不是不行,代价是能不能排序全押在两个前提上。第一个前提是宽度要一致。
const db = new DatabaseSync(":memory:"); // node:sqlite
db.exec("CREATE TABLE padded (v TEXT); CREATE TABLE unpadded (v TEXT)");
for (const v of ["2026-10-08 09:00:00", "2026-10-08 10:00:00", "2026-10-08 18:00:00"]) db.prepare("INSERT INTO padded VALUES (?)").run(v);
for (const v of ["2026-10-08 9:00:00", "2026-10-08 10:00:00", "2026-10-08 18:00:00"]) db.prepare("INSERT INTO unpadded VALUES (?)").run(v);
== 5. TEXT 存时间,没零填充排序就乱 ==
零填充 ORDER BY v -> 2026-10-08 09:00:00 2026-10-08 10:00:00 2026-10-08 18:00:00
差一位 ORDER BY v -> 2026-10-08 10:00:00 2026-10-08 18:00:00 2026-10-08 9:00:00
零填充那一列按字典序排出来正好是时间顺序。把 09 写成 9,九点的记录就跑到最后:字符串比大小是逐字符的,第一位是 9,直接压过了 1。这类数据一般来自写入端省了前导零,或者时间被拼接时漏了补位。
第二个前提是时区必须统一。同一列里混进 +08:00 和 Z 两种写法,字符串排序就不再等于时间排序。
const mixed = ["2026-10-08T02:00:00Z", "2026-10-08T10:00:00+08:00", "2026-10-08T03:00:00Z"];
[...mixed].sort();
[...mixed].sort((a, b) => Date.parse(a) - Date.parse(b));
== 6. TEXT 混了时区偏移,字符串排序不等于时间排序 ==
三条其实是三个时刻,其中前两条是同一瞬间
2026-10-08T02:00:00Z getTime=1791424800000
2026-10-08T10:00:00+08:00 getTime=1791424800000
2026-10-08T03:00:00Z getTime=1791428400000
按字符串排 2026-10-08T02:00:00Z 2026-10-08T03:00:00Z 2026-10-08T10:00:00+08:00
按 getTime 2026-10-08T02:00:00Z 2026-10-08T10:00:00+08:00 2026-10-08T03:00:00Z
头两条指向同一瞬间,Date.parse 出来的数一样。字符串排序把 2026-10-08T10:00:00+08:00 排到末尾,因为它以 1 开头;按时刻排,它和 02:00:00Z 并列在最前。一列里两种偏移并存时,任何不转成数字的直接比较都会给错序。
换成整数就把这些前提都省掉了。epoch 毫秒是一个整数,排序和判等都在比数字本身。
const idb = new DatabaseSync(":memory:"); // node:sqlite
idb.exec("CREATE TABLE ev (id INTEGER PRIMARY KEY, ms INTEGER)");
const ins = idb.prepare("INSERT INTO ev (ms) VALUES (?)");
for (const s of mixed) ins.run(Date.parse(s));
idb.prepare("SELECT ms FROM ev ORDER BY ms").all();
Date.parse("2026-10-08T02:00:00Z") === Date.parse("2026-10-08T10:00:00+08:00");
new Date(Date.parse("2026-10-08T02:00:00Z")).toISOString();
== 7. INTEGER 存 epoch,排序与比较都稳 ==
写入 1791424800000 1791424800000 1791428400000
ORDER BY ms 1791424800000 1791424800000 1791428400000
同一瞬间相等 true
取回要自己换 new Date(ms).toISOString() -> 2026-10-08T02:00:00.000Z
同一瞬间的两条记录在数字上相等,顺序固定,也不在意它们当初是用哪种偏移写进来的。换来的是读的时候要自己换算,先 new Date(ms) 再格式化成给人看的字符串。整数列还有一处容易混,单位。
const sdb = new DatabaseSync(":memory:");
sdb.exec("CREATE TABLE mixed_unit (v INTEGER)");
sdb.prepare("INSERT INTO mixed_unit VALUES (?)").run(1791424800);
sdb.prepare("INSERT INTO mixed_unit VALUES (?)").run(1791428400000);
== 8. 秒和毫秒混存,同一列里差一千倍 ==
存 1791424800 -> 当秒看 1970-01-21 17:37:04 / 当毫秒看 2026-10-08 02:00:00
存 1791428400000 -> 当秒看 2026-10-08 03:00:00 / 当毫秒看 null
十位的当秒正好,十三位的当毫秒正好,混在同一列里,光按数值排还看不出错,一旦按秒去换算,十位那条被读成 1970 年,十三位那条溢出成空。整数方案的头一条约定,是把单位写进列名或注释,秒就是秒,毫秒就是毫秒,不接受两者混放。
回到最开始那个要定的东西。两端只用 JSON 收发的系统,时间字段就是字符串,想让排序稳定,文本得零填充,还得统一成带 Z 的 UTC;数据落在本地又要频繁按时间取范围的,我更愿意存成 epoch 毫秒整数,把格式化留到最后一层。两种做法没有绝对的对错,取舍的标准是这条数据会不会被拿去比大小:会,就别留一堆字符串形态在中间环节。