沙箱的 now 是 UTC,不是本地时间;这台机器差 8 小时。
两个日期相减必须走 julianday,直接减字符串会给 0 而且不报错。
「加一个月」两个引擎给的答案不一样:一个顺延到下月初,一个压到月末。
10.4 讲 CURDATE、CURTIME、NOW、DATEDIFF、ADDDATE、SUBDATE。
这些名字沙箱一个都没有,对应的是 date()、time()、datetime() 加修饰符。
这一篇的时区、月末、不补零三件事,做业务系统时都会撞上,不只是语法问题。
一、取当前时间:UTC 与本地差多少
date('now')、time('now')、datetime('now') 全是 UTC。
加 'localtime' 修饰符才是本地时间。
| 写法 | 结果 |
|---|---|
date('now') |
2026-10-30 |
time('now') |
13:58 |
datetime('now') |
2026-10-30 13:58 |
datetime('now', 'localtime') |
2026-10-30 21:58 |
第三行和第四行差的就是时区。这台机器在 UTC+8,所以差 8 小时。
写日志、算「今天」的时候用错一个,跨零点那几小时的数据就会跑到前一天。
MySQL 的 NOW() 给的是会话时区的时间,默认就是服务器本地时间。
所以教材里 NOW() 和 CURDATE() 是「同一时刻的两种精度」,沙箱里不是。
要「和 MySQL 一样」就把 localtime 加上。
但更稳的做法是在应用层算好时间再传进来,因为时区不该由 SQL 猜。
二、DATEDIFF:两个日期差几天
julianday 把每一天编成一个连续的小数,所以两个日期能直接相减。
从 10 月 1 日到 10 月 30 日是 29 天,它给 29.0。
直接写 date(a) - date(b) 给 0,这个 0 是个陷阱。
因为 date() 返回的是字符串,字符串相减会被当成数字 0 处理,不报错。
所以「两个日期相减」在沙箱里必须走 julianday,直接减等于什么都没算。
MySQL 有 DATEDIFF(d1, d2),算的是 d1 - d2,被减数写在前面。
顺序写反了不报错,只会得到一个负数。
这一点和别处一样:SQL 里错得最安静的,往往是「结果是个合法数字」。
三、ADDDATE 与 SUBDATE:加一个月到底加多少
date(d, '+5 day')、date(d, '-3 month') 是标准写法,单位单复数都认。
day、month、year、hour 这些修饰符直接写在字符串里。
| 写法 | 结果 |
|---|---|
'+5 day' |
2026-11-04 |
'-3 month' |
2026-07-30 |
'2026-01-31' +1 month |
2026-03-03 |
'start of month' |
2026-10-01 |
第三行最值得停一下:1 月 31 日加一个月给的是 3 月 3 日,不是 2 月 28 日。
它的做法是「月份加一、日子不变」:1 月 31 日变成 2 月 31 日,这个日子不存在,
顺延成 3 月 3 日。
MySQL 的 ADDDATE 给的是 2 月 28 日——它把日子压到目标月的最后一天。
所以「加一个月」这件事,两个引擎给的答案不一样。
做账期、账单日、会员到期日的时候,这一处必须实测。
不能凭「一个月就是加一个月」去推,因为「月末加一个月」本身就有两种解释。
第四行那个 start of month 是修饰符,把日期归到月初。
做「本月统计」时很省事,不用自己拼年月。
四、日期存成字符串时,比较还准不准
ISO 格式(YYYY-MM-DD)的字符串比较等价于时间比较,因为高位在前。
逐字符比就是逐时间单位比,这是这个格式被选作标准的原因。
| 写法 | 结果 |
|---|---|
'2026-10-30' > '2026-10-09' |
1 |
'2026-9-30' < '2026-10-09' |
0 |
date('2026-9-30') |
NULL |
date('2026-09-30') |
2026-09-30 |
第二行本该是 1(9 月在 10 月之前),它给了 0。
因为逐字符比到第 6 位时,'9' 和 '1' 比,9 更大,整串就被判成「更晚」。
第三行更狠:不补零的日期 date() 都不认,直接给 NULL。
第四行补上零就正常了。
所以「存的时候补零」比「查的时候补救」可靠。
一旦库里混进不补零的日期,比较和转换会同时出错,而且都不报错。
五、时间戳与 2038 那个上限
32 位有符号整数的上限是 2147483647,对应的时间是 2038-01-19 03:14:07 UTC。
业内叫「2038 问题」,和当年的「千年虫」是同一类事。
MySQL 的 TIMESTAMP 类型就卡在这个上限上,DATETIME 没有这个问题。
所以「存时间用 DATETIME 还是 TIMESTAMP」不是风格问题。
TIMESTAMP 只占 4 字节、自带时区转换(存 UTC、按会话时区显示),但只能到 2038 年。
DATETIME 占 5 到 8 字节、不带时区转换,但能到 9999 年。
选哪个看业务:跨时区、只存近期用 TIMESTAMP;
要存远期、要「原样记下这个时刻」用 DATETIME。
六、动手 Demo:日期时间函数,和 UTC 那一小时的差
这一段把「时区、减法、加一个月、字符串比较、2038」五件事各跑一次。 其中加一个月和字符串比较那两段,结果和直觉都不一致。
import sqlite3
c = sqlite3.connect(u":memory:")
# ① UTC 与本地
for s in [u"date('now')", u"time('now')", u"datetime('now')",
u"datetime('now', 'localtime')"]:
print(s, c.execute(u"SELECT " + s).fetchone()[0])
# ② 两个日期相减
print(c.execute(u"SELECT julianday('2026-10-30') - "
u"julianday('2026-10-01')").fetchone()) # 29.0
print(c.execute(u"SELECT date('2026-10-30') - "
u"date('2026-10-01')").fetchone()) # 0
# ③ 加一个月
print(c.execute(u"SELECT date('2026-01-31', '+1 month')").fetchone())
# ④ 不补零的日期
print(c.execute(u"SELECT '2026-9-30' < '2026-10-09'").fetchone()) # 0
print(c.execute(u"SELECT date('2026-9-30')").fetchone()) # None
实跑输出:
--------------------------------------------------------------------
第四十七课:日期时间函数,和 UTC 那一小时的差
--------------------------------------------------------------------
**教材 10.4 讲 `CURDATE`、`CURTIME`、`NOW`、`DATEDIFF`、`ADDDATE`、`SUBDATE`。**
**沙箱里这些名字一个都没有,对应的是 `date()`、`time()`、`datetime()` 加修饰符。**
**这一课最要紧的一件事:`now` 在沙箱里是 UTC,不是本地时间。**
一、取当前时间:UTC 与本地差多少
--------------------------------------------------------------------
写法 结果
---------------------------------------------------
`date('now')` 2026-10-30
`time('now')` 14:00:19
`datetime('now')` 2026-10-30 14:00:19
`datetime('now', 'localtime')` 2026-10-30 22:00:19
**第三行和第四行差的就是时区。这台机器在 UTC+8,所以差 8 小时。**
**写日志、算「今天」的时候用错一个,跨零点那几小时的数据就会跑到前一天。**
**MySQL 的 `NOW()` 给的是会话时区的时间,默认就是服务器本地时间。**
**所以教材里 `NOW()` 和 `CURDATE()` 是「同一时刻的两种精度」,沙箱里不是。**
**要「和 MySQL 一样」,就把 `localtime` 加上,或者干脆在应用层算好时间再传进来。**
**后者更稳,因为时区这种东西不该由 SQL 猜。**
二、DATEDIFF:两个日期差几天
--------------------------------------------------------------------
写法 结果
-----------------------------------------------------------------
`julianday('2026-10-30') - julianday('2026-10-01')` 29.0
`date('2026-10-30') - date('2026-10-01')` 0
`datediff('2026-10-30', '2026-10-01')` 沙箱无此函数
**第一行给 29.0,是对的:从 10 月 1 日到 10 月 30 日是 29 天。**
**它算的是「儒略日」之差,儒略日是把每一天编成一个连续的小数,所以能直接相减。**
**第二行给 0,这个 0 是个陷阱。**
**因为 `date()` 返回的是字符串,字符串相减会被当成数字 0 处理,不报错。**
**所以「两个日期相减」在沙箱里必须走 `julianday`,直接减等于什么都没算。**
**MySQL 有 `DATEDIFF(d1, d2)`,算的是 `d1 - d2`,所以被减数要写在前面。**
**顺序写反了不报错,只会得到一个负数——这一点和别处一样,错得很安静。**
三、ADDDATE 与 SUBDATE:加减日期
--------------------------------------------------------------------
写法 结果
--------------------------------------------------
`date('2026-10-30', '+5 day')` 2026-11-04
`date('2026-10-30', '-3 month')` 2026-07-30
`date('2026-01-31', '+1 month')` 2026-03-03
`date('2026-10-30', 'start of month')` 2026-10-01
**前两行是标准的加减,单位和单复数都认:`day`、`month`、`year`、`hour`。**
**第三行值得停一下:1 月 31 日加一个月给的是 3 月 3 日,不是 2 月 28 日。**
**它的做法是「月份加一、日子不变」:1 月 31 日变成 2 月 31 日。**
**这个日子不存在,于是顺延成 3 月 3 日。**
**MySQL 的 `ADDDATE` 给的是 2 月 28 日——它把日子压到目标月的最后一天。**
**所以「加一个月」这件事,两个引擎给的答案不一样。**
**做账期、账单日、会员到期日的时候,这一处必须实测,不能凭「一个月就是加一个月」去推。**
**第四行那个 `start of month` 是修饰符,把日期归到月初。**
**做「本月统计」时这一招很省事,不用自己拼年月。**
四、日期存成字符串时,比较还准不准
--------------------------------------------------------------------
写法 结果
-----------------------------------------
`'2026-10-30' > '2026-10-09'` 1
`'2026-9-30' < '2026-10-09'` 0
`date('2026-9-30')` NULL
`date('2026-09-30')` 2026-09-30
**第一行给 1,说明 ISO 格式(`YYYY-MM-DD`)的字符串比较等价于时间比较。**
**因为高位在前,逐字符比就是逐时间单位比。**
**第二行本该是 1(9 月在 10 月之前),它给了 0。**
**因为逐字符比到第 6 位时,`'9'` 和 `'1'` 比,`9` 更大,整串就被判成「更晚」。**
**第三行更狠:不补零的日期 `date()` 都不认,直接给 NULL。**
**第四行补上零就正常了,返回 `2026-09-30`。**
**所以「存的时候补零」比「查的时候补救」可靠。**
**一旦库里混进不补零的日期,比较和转换会同时出错,而且都不报错。**
五、时间戳与 2038 那个上限
--------------------------------------------------------------------
项 值
------------------------------------------
当前 UTC 时间戳(秒) 1793368819
能表示的最大值 2147483647
对应的时间(UTC) 2038-01-19 03:14:07
**第三行那个时间是 32 位有符号整数的上限,业内叫「2038 问题」。**
**MySQL 的 `TIMESTAMP` 类型就卡在这个上限上,`DATETIME` 没有这个问题。**
**所以「存时间用 `DATETIME` 还是 `TIMESTAMP`」不是风格问题。**
**`TIMESTAMP` 只占 4 字节、自带时区转换,但只能到 2038 年。**
前四行里,第三和第四行差 8 小时,这就是 UTC 与本地时间。
沙箱默认 UTC,MySQL 默认本地,这个差别足以让「今天的订单」少算一截。
两个日期相减那两行是 29.0 和 0。
0 那一行最危险:它看起来像个正常结果,其实是「字符串相减」的产物。
加一个月那行给 3 月 3 日,说明它不做「压到月末」的处理。
MySQL 那边给 2 月 28 日,所以同一句 SQL 两个答案。
最后两行是不补零的后果:比较给 0(错),date() 给 NULL(不认)。
这两个错都不报异常,只能靠「结果不合常理」去发现。
30 秒实验
- 把
datetime('now')和datetime('now', 'localtime')各跑一次,算出差几小时。 - 把
julianday那一句换成date(a) - date(b),看它给几。 - 试
date('2026-03-31', '+1 month'),看它是 4 月 30 日还是 5 月 1 日。
常见误区
- 用
date('now')算「今天」,跨零点那几小时的数据跑到前一天。 - 以为两个日期能直接减,得到 0 还以为是同一天。
- 以为「加一个月」一定是压到月末,没测过就写了账期逻辑。
自测三问(点开对答案)
1. 沙箱里 `date('now')` 和 `date('now', 'localtime')` 有什么区别? —— 前者是 UTC 日期,后者是本地日期。这台机器在 UTC+8,跨零点时两者会差一天。 2. `julianday(a) - julianday(b)` 为什么能算出天数? —— 儒略日把每一天编成一个连续的小数,两个日期相减就是相隔的天数。直接减字符串只会得到 0。 3. 「1 月 31 日加一个月」在沙箱和 MySQL 里分别是几号? —— 沙箱给 3 月 3 日(月份加一、日子不变,2 月 31 日顺延), MySQL 给 2 月 28 日(压到目标月最后一天)。下一篇 10.5 进条件判断函数,重点是「缺考算不算 0 分」这种口径问题。
- ☐ (本篇完,下一篇 10.5 条件判断函数)