🛢️ 第10章 常用函数
📄 一句话概括

沙箱的 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 秒实验

  1. 把 datetime('now') 和 datetime('now', 'localtime') 各跑一次,算出差几小时。
  2. 把 julianday 那一句换成 date(a) - date(b),看它给几。
  3. 试 date('2026-03-31', '+1 month'),看它是 4 月 30 日还是 5 月 1 日。

常见误区

  1. 用 date('now') 算「今天」,跨零点那几小时的数据跑到前一天。
  2. 以为两个日期能直接减,得到 0 还以为是同一天。
  3. 以为「加一个月」一定是压到月末,没测过就写了账期逻辑。
自测三问(点开对答案) 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 分」这种口径问题。

相关笔记