日期时间函数
本教程共 50 篇 · 第 38 篇 · 更新于 2026-07-31
38. 日期时间函数
本节目标:学完本章你能正确地在 SQLite 里存储和计算日期时间,并理解为什么”日期函数只负责解析和格式化,真正的日期是按 TEXT/REAL/INTEGER 存进去的”。
先说一个最根本、也最容易踩坑的事实:SQLite 没有 DATE、TIME、DATETIME、TIMESTAMP 这几种类型。很多从 MySQL、PostgreSQL 转过来的同学会下意识写 CREATE TABLE t(created_at DATETIME),以为这样就定义了一个”日期列”。但在 SQLite 里,这只是一种”类型亲和性(Type Affinity)“的声明——DATETIME 会被归为 NUMERIC 亲和性,而真正落盘时,值是什么存储类就存什么存储类。SQLite 只有 5 种存储类:NULL、INTEGER、REAL、TEXT、BLOB。日期时间就是用其中三种来存的。
为什么这么设计?因为 SQLite 是 serverless、零配置的嵌入式库,追求”简单即可靠”。它不内置一个复杂的日期类型系统,而是复用已有的文本和数值存储类,再配套一组函数来做解析与格式化。换句话说:函数不负责”存日期类型”,只负责”把字符串/数字解析成日期、或把日期格式化成字符串/数字”。
一、三种存储日期的姿势
官方和社区都推荐三种存法,按场景选:
- TEXT 存 ISO-8601 字符串,例如
'2026-07-31 14:16:00'。最直观、可读、排序也正确(因为格式是”大单位在前”)。这是大多数应用的首选。对应我们的orders表,created_at TEXT就是这种用法。 - REAL 存儒略日数(Julian Day):从公元前 4714-11-24 12:00 起算的天数,含小数。适合做天文或高精度天数计算,但人看不懂。
- INTEGER 存 Unix 时间戳:从 1970-01-01 00:00:00 UTC 起的秒数。跨语言通用,很多系统接口直接给这个。
存进去之后,用函数来”读”和”算”。
二、五个核心函数
这里有必要和 MySQL/PostgreSQL 做个客观对比(只说差异,不贬低):那些数据库有原生的 DATETIME/TIMESTAMP 类型,列本身带”日期语义”,数据库会替你校验格式、提供日期专属运算符。SQLite 选择不这么做,换取的是极简——没有额外的类型系统要维护,单文件、零配置、易嵌入。代价是日期的”正确性”更多落在你写 SQL 的习惯上:要更自觉地用 ISO-8601 文本或标准数值去存,并始终通过日期函数去读和算。理解了这个取舍,你就不会把”没有日期类型”当成缺点,而会把它看作 SQLite 设计哲学的一致体现。
SQLite 提供七个日期时间函数(本章讲最常用的五个,外加 unixepoch、timediff 会在示例里顺带出现)。它们都接受”时间值 + 零个或多个修饰符”的形式。
date(...) 返回 YYYY-MM-DD 格式的日期文本:
SELECT date('now');
time(...) 返回 HH:MM:SS(加 subsec 修饰符可得毫秒)格式的时间文本。
datetime(...) 返回 YYYY-MM-DD HH:MM:SS 格式,最常用:
SELECT datetime('now');
SELECT datetime('now', 'localtime'); -- 转成本地时区
julianday(...) 返回儒略日数(REAL)。用来算”两个日期相差多少天”最方便:
SELECT julianday('now') - julianday('1776-07-04');
strftime(格式, ...) 是最通用的格式化函数,第一个参数是格式串。它几乎能表达上面所有函数:
SELECT strftime('%Y-%m-%d', 'now'); -- 等价于 date('now')
SELECT strftime('%s', 'now'); -- 等价于 unixepoch(),返回 Unix 秒
SELECT strftime('%Y年%m月%d日 %H时%M分', '2026-07-31 14:16:00');
常用占位符:%Y 年、%m 月、%d 日、%H 时(00-24)、%M 分、%S 秒、%s 自 1970 起的秒数、%j 年中第几天、%w 星期几(0=周日)、%f 带小数的秒。
重点提示
date()、time()、datetime()返回的永远是文本,所以可以直接当字符串比较和排序。而julianday()和unixepoch()(3.46+ 提供,等价于strftime('%s')的数值版)返回的是数值,适合做减法求差值。需要”可读的时段描述”用timediff(A,B),需要”精确的相差天数/秒数”就用两个julianday()或unixepoch()相减。
三、修饰符:让时间前后移动
函数后面可以跟任意数量的”修饰符(modifier)“,从左到右依次作用。最实用的是时间偏移和”取整到某起点”:
-- 当前月份的最后一天
SELECT date('now', 'start of month', '+1 month', '-1 day');
-- 给定 unix 秒,转成日期(必须加 'unixepoch')
SELECT datetime(1092941466, 'unixepoch', 'localtime');
-- 90 天后是几号
SELECT date('now', '+90 days');
可用的修饰符包括 NNN days/hours/minutes/seconds/months/years、start of month/year/day、weekday N、unixepoch、localtime、utc 等。带月份/年份的偏移遇到月底歧义时,默认按”ceiling”(取较晚日期)处理,也可以用 floor 修饰符改策略。
常见坑
第一,千万记住 SQLite 没有日期类型,
created_at列声明成DATETIME并不会让它变成真正的日期列,它只是文本或数值。所有”日期计算”都必须经过日期函数,直接对文本列做加减是无效的。第二,'unixepoch'修饰符必须紧跟在数字形式的时间值后面,否则行为未定义。第三,时区:函数内部统一用 UTC,想显示本地时间务必加'localtime'修饰符。
四、什么时候用哪种存法
- 要人能直接看懂、要和别的系统交换文本日志 → 用 TEXT + ISO-8601,配合
datetime()读写。 - 要高频做”相差多少天/秒”的数学运算 → 用 REAL(儒略日) 或 INTEGER(Unix 秒),用
julianday()/unixepoch()相减,展示时再datetime()格式化。 - 从网页/接口拿到的是 Unix 时间戳 → 直接存 INTEGER,读时
datetime(秒, 'unixepoch')。
把这三点落到一个具体例子上。我们的 orders(id INTEGER PRIMARY KEY, user_id INTEGER, amount REAL, created_at TEXT) 表,created_at 就是 TEXT 存 ISO-8601。插入时用函数生成当前时间,查询时再用函数加工:
INSERT INTO orders(user_id, amount, created_at)
VALUES (1, 99.5, datetime('now'));
-- 查 2026 年 7 月下的所有订单
SELECT id, amount
FROM orders
WHERE date(created_at) BETWEEN '2026-07-01' AND '2026-07-31';
-- 查每笔订单距现在多少天
SELECT id, julianday('now') - julianday(created_at) AS days_ago
FROM orders;
这种写法的好处是”存和读都用同一套函数约定”,无论以后把 created_at 改成 REAL 还是 INTEGER,只要读写两侧同步改函数,业务逻辑几乎不动。
再给一组”常用日期配方”,照抄即可:
SELECT date('now', '-7 days'); -- 7 天前
SELECT strftime('%Y-%m', 'now'); -- 当前年月
SELECT date('now', 'weekday 1'); -- 本周(周一)日期
SELECT timediff('2026-12-31', 'now'); -- 到年底还差多久(可读文本)
实用技巧
排序时直接
ORDER BY created_at就行,因为 ISO-8601 是”大单位在前”的字符串,字典序恰等于时间顺序,无需先转成数字。这是 TEXT 存法相对 REAL/INTEGER 的一个隐性优势:既能读又能直接排。
实用技巧
把”写入”和”读取”统一用函数包一层最省心:插入时用
datetime('now'),查询展示时也用datetime(created_at)。这样即使将来改存储格式,SQL 改动也最小。注意julianday()与unixepoch()的适用范围是 0000-01-01 到 9999-12-31,超出此范围的日期结果是未定义的。
五、类比小结
把 SQLite 的日期处理想成”拍照片”:你用 TEXT/REAL/INTEGER 三种方式把”时间点”记到本子上(存储),date/time/datetime/julianday/strftime 这些函数就是”放大镜和翻译机”——负责把本子上的数字读出来、翻成你能看懂的年月日时分秒,或者帮你算两张照片之间隔了多久。本子本身没有”日期”这一格,只是你约定好怎么写、怎么读。
记住一句话:在 SQLite 里,日期是”存成普通值 + 用函数解析”的组合拳,而不是一种独立的列类型。理解了这点,你就不会再被”为什么我写了 DATETIME 却存成了字符串”这类问题困扰了。