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 秒实验
- 跑
SELECT GETDATE(), GETUTCDATE()(SQL Server),看两个时间差多少。 在中国是差 8 小时——但服务器时区设错的话,可能差 0 或者差别的数。 - 在你的库上跑
SELECT DATEPART(dw, GETDATE()),看今天返回几。 周日返回 1——如果今天是周一,它会返回 2。 这个起点跟直觉不同。 - 想一想:为什么工程上建议「存 UTC、显示时转本地」? 因为存的时候要统一。 如果有的存 UTC、有的存本地, 跨系统对时间永远对不齐——统一存 UTC,至少在数据层面没有歧义。
常见误区
- 「
datetime('now')给的是本地时间」。SQLite 给的是 UTC。 要本地时间得加'localtime'。SQL Server 的GETDATE()才是本地时间—— 两家的默认正好相反。 - 「日期差 8 小时无所谓,反正只差一点点」。跨零点就差一天。 本地 31 号凌晨的单,按 UTC 分组会算到 30 号——日结报表就是这么错的。
- 「
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 做不到。
- ☐ (本篇完,下一篇 7.4 转换函数)