🪟 第7章 SQL函数的使用
📄 一句话概括

SQLite 的 datetime('now') 给 UTC,SQL Server 的 GETDATE() 给本地时间——正好相反。 差 8 小时刚好跨零点,日期就变一天——所以「按天统计」用错函数会错一天。 DATEPART 的 mm 是月、mi 是分钟,dw 的 1 是周日不是周一。

📝 本章和前面几章的关系

日期是 SQL 里最麻烦的一块。 因为它涉及三个维度:格式、时区、精度。哪一个弄错,结果都不对,而且不一定报错。 教材 7.3 有八个小节:总览、DATEPART、GETDATE、GETUTCDATE、MONTH、YEAR、DAY、 综合应用。前七个小节讲函数,最后一个是把它们用起来。 演示里跑出了两组时间:UTC 是 30 号 18:50, 本地是 31 号 02:50——差 8 小时刚好跨过零点,日期就差一天。这是本节最值得看的一段。

一、常用日期函数对照

SQL Server 作用 SQLite 写法
GETDATE() 当前时间(本地) datetime('now','localtime')
GETUTCDATE() 当前时间(UTC) datetime('now')
YEAR(d) 取年 strftime('%Y', d)
MONTH(d) 取月 strftime('%m', d)
DAY(d) 取日 strftime('%d', d)
DATEADD(day,1,d) 加一天 date(d,'+1 day')
DATEDIFF(day,a,b) 相差几天 julianday(b)-julianday(a)

最后两行是本质差别。 SQL Server 有现成的 DATEADD / DATEDIFF, SQLite 得自己算——因为它没有日期类型,日期就是字符串。 「有没有日期类型」决定了能做什么。 有类型就能直接加减、直接算差; 没类型就只能当字符串处理,加减靠字符串修饰符('+1 day'),差值靠儒略日相减。 YEAR / MONTH / DAY 这三个其实是 DATEPART 的简写。YEAR(d) 等于 DATEPART(yy, d)—— 用简写更清楚,也少一个参数写错的机会。

二、UTC 与本地:一个反直觉的事实

这一段是本节的核心。先看实测:

datetime('now')             = 2026-10-30 18:50:28   (UTC)
datetime('now','localtime') = 2026-10-31 02:50:28   (本地)
两者相差 8.0 小时

注意两个日期不一样:一个是 30 号,一个是 31 号。因为凌晨 2:50 减 8 小时正好退到前一天。 这就是「按天统计会错一天」的由来。 如果你的报表用 UTC 分组,那么本地时间 31 号凌晨下的单, 会被算到 30 号头上——对账的时候怎么都对不上。 更麻烦的是两个产品的默认相反: SQLite 的 datetime('now') 给 UTC, SQL Server 的 GETDATE() 给 本地时间。同一句「取当前时间」,两边差 8 小时。 所以跨系统对时间,一定要说清「这是哪个时区」。工程上的做法是「存 UTC、 显示时转本地」——存的时候统一,就不会有歧义。

三、DATEPART 的缩写

缩写 含义 容易错在哪
yy / yyyy 年 两位还是四位看版本
mm 月 不是分钟
mi 分钟 不是月
dd 日 d 也行
hh 小时 24 小时制
ss 秒
ms 毫秒 精度看类型
dw 星期几 周日是 1
wk 一年中第几周 算法跟地区有关

mm 和 mi 是最容易混的一对。 两个都是 m 开头, 靠第二个字母区分——写成单个 m 会报错,写成 n 也不行(那是 Oracle 的写法)。 dw 的 1 是周日,不是周一。 这跟很多人的直觉相反—— 做周报的时候,如果按 dw 分组,一周是从周日开始的。 wk 的算法跟地区设置有关。 有的把 1 月 1 日所在周算第 1 周, 有的要求「第一个完整周」才算——跨年的时候两种算法差一周。 所以「按周统计」这件事,一定要先确认用哪种算法。最稳的做法是自己算: DATEDIFF(day, '2026-01-01', d) / 7,虽然粗糙,但至少每天的结果都一样。

四、按天分组:跨零点的坑

先看一个简单的例子:三笔订单,23:50、00:10、12:00。

时间 date() 取出的日期
2026-10-30 23:50 2026-10-30
2026-10-31 00:10 2026-10-31
2026-10-31 12:00 2026-10-31

前两笔只差 20 分钟,却分到了两天。 这个结果没错——「按天分组」看的就是日期部分, 23:50 和 00:10 确实不在同一天。 但要小心「日结」类业务。 如果业务规则是「20:00 之后的算第二天」, 那么直接按日期分组就错了——跨零点的订单会算到前一天的账上。 解法是先把时间平移,再取日期: date(otime, '+2 hours')。 加两小时之后,20:00 就变成了 22:00(还在当天)、 23:50 变成 01:50(第二天)——平移之后按天分组,就符合业务规则了。 这个技巧叫「业务日偏移」,零售、餐饮、物流里很常见。关键在于: 别让「自然日」和「业务日」混着用——两者的边界不一样,混着用一定出错。

五、动手 Demo:UTC 与本地差 8 小时

这一段先列函数对照,再真跑 UTC 和本地两个时间看它们差多少、日期有没有跨天, 然后列 DATEPART 的缩写,最后用三笔订单演示「按天分组」怎么把 20 分钟拆成两天。

import sqlite3

c = sqlite3.connect(u":memory:")

utc = c.execute(u"SELECT datetime('now')").fetchone()[0]
loc = c.execute(u"SELECT datetime('now','localtime')").fetchone()[0]
print(u"UTC     ", utc)
print(u"本地    ", loc)
diff = c.execute(u"SELECT (julianday('now','localtime')"
                 u" - julianday('now')) * 24").fetchone()[0]
print(u"相差 %.1f 小时" % diff)

# 按天分组:23:50 和 00:10 分到两天
c.execute(u"CREATE TABLE tb (otime TEXT)")
c.executemany(u"INSERT INTO tb VALUES (?)",
              [('2026-10-30 23:50:00',), ('2026-10-31 00:10:00',),
               ('2026-10-31 12:00:00',)])
for r in c.execute(u"SELECT date(otime), COUNT(*) FROM tb GROUP BY 1"):
    print(r)

实跑输出:

--------------------------------------------------------------------
日期函数:UTC 与本地的时差
--------------------------------------------------------------------


  ① 常用日期函数对照
  --------------------------------------------------------------------
| SQL Server | 作用 | SQLite 写法 |
|---|---|---|
| GETDATE() | 当前时间(本地) | datetime('now','localtime') |
| GETUTCDATE() | 当前时间(UTC) | datetime('now') |
| YEAR(d) | 取年 | strftime('%Y', d) |
| MONTH(d) | 取月 | strftime('%m', d) |
| DAY(d) | 取日 | strftime('%d', d) |
| DATEPART(dd,d) | 取日(同上) | strftime('%d', d) |
| DATENAME(dw,d) | 取星期几(英文名) | strftime('%w', d) |
| DATEADD(day,1,d) | 加一天 | date(d,'+1 day') |
| DATEDIFF(day,a,b) | 相差几天 | julianday(b)-julianday(a) |

  最后两行是重点:SQL Server 有现成的 DATEADD / DATEDIFF,SQLite 得自己算。
  SQLite 把日期当字符串存,加减天数用字符串修饰符 '+1 day',差值要靠儒略日相减。
  这就是「有没有日期类型」的差别:有类型就能直接算,没类型就只能当数字算。

  ② 同一个时刻,两个函数差 8 小时
  --------------------------------------------------------------------
  datetime('now')            = 2026-10-30 18:50:49   (UTC)
  datetime('now','localtime')= 2026-10-31 02:50:49   (本地)
  两者相差 8.0 小时
  注意上面那两个时间:UTC 那个是 30 号,本地那个已经是 31 号了。
  差 8 小时刚好跨过零点,日期就变了一天——这就是「按天统计会错一天」的由来。
  UTC 是本初子午线的时间,中国是 UTC+8,所以本地时间比 UTC 快 8 小时。
  SQL Server 的 GETDATE() 返回本地时间,GETUTCDATE() 返回 UTC——
  这一点跟 SQLite 的 datetime('now') 正好相反:SQLite 默认给 UTC。
  所以「按天统计」这件事,用错一个函数就会差一天——跨零点的数据会跑到前一天的账上。

  ③ DATEPART 的缩写:yy 和 yyyy 是两回事
  --------------------------------------------------------------------
| 缩写 | 含义 |
|---|---|
| yy | 年(两位还是四位看版本) |
| yyyy | 年,四位 |
| mm | 月 |
| dd | 日 |
| hh | 小时 |
| mi | 分钟 |
| ss | 秒 |
| ms | 毫秒 |
| dw | 星期几(周日是 1) |
| wk | 一年中第几周 |

  mm 是月、mi 是分钟——两个都是 m 开头,靠第二个字母区分。
  写成 m 会报错;写成 n 也不行(那是 Oracle 的写法)。
  dw 的 1 是周日不是周一,这一点跟很多人的直觉相反,做周报的时候要注意。

  ④ 按天分组:跨零点的坑
  --------------------------------------------------------------------
  日期          单数
  2026-10-30  1
  2026-10-31  2
  23:50 和 00:10 只差 20 分钟,但分到了两天——因为「按天分组」看的是日期部分。
  这个结果是对的,但要小心「日结」类报表:如果业务要求「20:00 之后的算第二天」,
  那就得先把时间平移两小时再分组,否则跨零点的订单会算错账。
  写法是 date(otime, '+2 hours')——加两小时再取日期,就把 20:00 之后挪到第二天了。

第一段那两行时间就是本节的核心。 差 8 小时, 而且其中一个已经跨到第二天了——本地 31 号凌晨,UTC 还是 30 号。 这个差别在「按天统计」上是致命的。 本地 31 号下的单, 如果用 UTC 分组会被算到 30 号——对账时怎么都对不上,而且查不出原因。 第二段三笔订单分成了两组:30 号一笔、31 号两笔。23:50 和 00:10 只差 20 分钟, 但确实不是同一天——这个结果本身没错。 错的是「直接按自然日分组」这个假设。 如果业务规则是「20:00 之后算第二天」, 那就得先 date(otime, '+2 hours') 平移再分组——这一步不做,日结就会错。

30 秒实验

  1. 跑 SELECT GETDATE(), GETUTCDATE()(SQL Server),看两个时间差多少。 在中国是差 8 小时——但服务器时区设错的话,可能差 0 或者差别的数。
  2. 在你的库上跑 SELECT DATEPART(dw, GETDATE()),看今天返回几。 周日返回 1——如果今天是周一,它会返回 2。 这个起点跟直觉不同。
  3. 想一想:为什么工程上建议「存 UTC、显示时转本地」? 因为存的时候要统一。 如果有的存 UTC、有的存本地, 跨系统对时间永远对不齐——统一存 UTC,至少在数据层面没有歧义。

常见误区

  1. 「datetime('now') 给的是本地时间」。SQLite 给的是 UTC。 要本地时间得加 'localtime'。SQL Server 的 GETDATE() 才是本地时间—— 两家的默认正好相反。
  2. 「日期差 8 小时无所谓,反正只差一点点」。跨零点就差一天。 本地 31 号凌晨的单,按 UTC 分组会算到 30 号——日结报表就是这么错的。
  3. 「DATEPART(dw, ...) 返回 1 是周一」。 1 是周日。 做周报按 dw 分组,一周是从周日开始的——跟国内「周一开始」的习惯不一样。
自测三问(点开对答案) 1. SQLite 的 `datetime('now')` 和 SQL Server 的 `GETDATE()` 分别给什么时区? —— **SQLite 给 UTC,SQL Server 给本地时间——正好相反。**所以同一句「取当前时间」, 两边可能差 8 小时,跨零点还差一天。 2. 为什么按天分组会把只差 20 分钟的两笔订单分到两天? —— **因为「按天分组」看的是日期部分,不看时间差。**23:50 是 30 号、 00:10 是 31 号——**结果没错,错的是「按自然日分组」这个假设。** 3. `DATEPART(dw, 日期)` 返回 1 表示星期几? —— **周日。** 这个起点跟「周一是一周开始」的直觉不同。 **做周报时如果按 `dw` 分组,一周是从周日算起的。**

下一篇 7.4 讲转换函数。 CAST 是标准写法,CONVERT 是 SQL Server 扩展——后者多一个样式码参数, 能控制日期转成字符串的格式,这一点 CAST 做不到。

相关笔记