SQL 里算两个日期差几天,看起来是个小到不能再小的问题,但我见过太多人在这上面翻车。有人用DATEDIFF写得好好的,换了个数据库直接报错;有人减法一减,结果差了 8 小时;还有人拿字符串直接相减,跑出来一个莫名其妙的大数字。这类问题在业务里出现的频率极高——算账期、算账龄、算会员天数、算逾期天数、算留存间隔,几乎每个系统都会碰到。所以这篇我不打算只给你一句DATEDIFF(day, a, b)就完事,而是把主流数据库的写法差异、边界情况、时区陷阱、以及我实际踩过的坑,一次性讲透。不管你是刚写 SQL 的新手,还是写了好几年但一直靠试错的人,看完都能对"日期差"这件事有个清晰、可复现的判断标准。
1. 先搞清楚"差几天"到底在问什么
在动手写函数之前,有个问题必须先定下来:你说的"差几天",是自然日跨度,还是经过的完整 24 小时数?这两个答案在很多场景下是不一样的,而且一旦选错,业务逻辑就会悄悄出错。
1.1 自然日差与时间跨度差是两回事
举个具体例子。2024-03-01 23:00:00和2024-03-02 01:00:00,这两个时间点之间只隔了 2 个小时。如果你问"差几天",按自然日算,答案是 1 天(跨了一天);按完整 24 小时算,答案是 0 天(不足一天)。
这两种口径没有对错,只有场景适配:
- 账期、会员到期、逾期天数、留存天数这类业务,通常要的是自然日差。用户 3 月 1 日下单,3 月 2 日付款,业务上就是隔了 1 天,哪怕只隔了几分钟。
- 耗时统计、任务执行时长、缓存过期这类场景,要的是真实时间跨度,这时候用小时或秒来算更准确,硬套"天"反而会丢精度。
我见过一个真实的坑:某系统算"逾期天数"用的是时间戳相减再除以 86400 取整,结果用户晚上 11 点 50 分还款,系统算出来逾期 0 天,但按合同口径已经逾期 1 天了,对账时扯了半天。所以第一步永远是跟业务方确认口径,而不是急着敲函数。
1.2 日期类型和字符串类型必须先分清
第二个高频错误是拿字符串当日期算。很多表里日期存的是VARCHAR,形如'2024-03-01',直接相减在某些数据库里会隐式转换,在另一些里会报错或者给出诡异结果。
判断标准很简单:参与计算的字段,类型必须是 DATE、DATETIME、TIMESTAMP 之一。如果存的是字符串,先显式转换,别指望数据库帮你猜。转换这一步看似多余,实际上是给后续所有计算兜底,尤其是数据源不规范的时候。
提示:转换前先确认字符串格式统一。如果同一列里混着
2024-03-01和2024/03/01两种写法,转换会失败或得到 NULL,务必先清洗。
1.3 空值和非法日期是隐藏炸弹
NULL参与任何日期运算,结果都是NULL,这个大多数人都知道。但真正容易忽略的是非法日期,比如'2024-02-30'、'0000-00-00'。某些数据库对这类值宽容,会给你转成一个奇怪的日子;某些直接报错中断查询。
我的习惯是,在计算前先做一次过滤或兜底:
SELECT CASE WHEN date_a IS NULL OR date_b IS NULL THEN NULL ELSE DATEDIFF(day, date_a, date_b) END AS diff_days FROM your_table;这样至少保证结果集不会因为个别脏数据整批挂掉。数据质量差的环境里,这一步能省掉大量排查时间。
2. 主流数据库的写法差异,一张表说清
这是最容易出问题的地方。同样一句"算日期差",MySQL、SQL Server、PostgreSQL、Oracle、SQLite 的写法各不相同,函数名、参数顺序、返回口径都有区别。我按实际使用频率整理了一张对照表,建议收藏。
2.1 各数据库函数对照
| 数据库 | 常用写法 | 返回口径 | 备注 |
|---|---|---|---|
| MySQL | DATEDIFF(date1, date2) | 自然日差,date1 - date2 | 只取日期部分,忽略时分秒 |
| SQL Server | DATEDIFF(day, start, end) | 按指定单位差 | 第一个参数是单位,顺序是 end - start |
| PostgreSQL | date1 - date2 | 整数天 | 两个 DATE 相减直接得天数 |
| PostgreSQL | DATE_PART('day', ts1 - ts2) | 时间戳差值的天部分 | 注意是"天部分"不是总天数 |
| Oracle | date1 - date2 | 天数(带小数) | 两个 DATE 相减得天数差 |
| SQLite | JULIANDAY(date1) - JULIANDAY(date2) | 天数(带小数) | 没有原生 DATEDIFF |
| 通用近似 | TIMESTAMPDIFF(DAY, a, b) | 自然日差 | MySQL 支持,部分库兼容 |
这张表里最坑的两处,我单独拎出来说。
2.2 MySQL 的 DATEDIFF 只认日期部分
MySQL 的DATEDIFF(expr1, expr2)返回的是expr1 - expr2的天数,而且只取日期部分。也就是说DATEDIFF('2024-03-02 01:00', '2024-03-01 23:00')返回 1,而不是 0。这跟很多人直觉里的"时间跨度"不一样。
如果你要的是精确到小时的时间跨度,得用TIMESTAMPDIFF(HOUR, a, b) / 24或者TIMESTAMPDIFF(SECOND, a, b) / 86400。这里有个细节:TIMESTAMPDIFF是截断而不是四舍五入,TIMESTAMPDIFF(DAY, '2024-03-01 23:00', '2024-03-02 22:00')返回 0,因为不足完整一天。要四舍五入得自己加逻辑。
2.3 SQL Server 的参数顺序反直觉
SQL Server 的DATEDIFF(datepart, startdate, enddate)返回的是enddate - startdate,注意是后者减前者,跟 MySQL 的DATEDIFF(a, b)表示a - b正好相反。这个顺序差异是跨库迁移时最常见的 bug 来源。
我个人的做法是,写的时候在注释里明确标一句"end - start",避免几个月后自己都记不清。另外 SQL Server 的DATEDIFF是按"跨越的边界数"算的,DATEDIFF(day, '2024-03-01 23:59', '2024-03-02 00:01')返回 1,因为它跨过了一个日期边界,这点跟 MySQL 的自然日口径一致。
2.4 PostgreSQL 的减法最直观,但有个陷阱
PostgreSQL 里两个DATE类型直接相减,得到的就是整数天数,非常干净。但如果字段是TIMESTAMP,相减得到的是INTERVAL类型,这时候DATE_PART('day', ...)取的是间隔里的天部分,不是总天数。
比如两个时间戳差 50 小时,DATE_PART('day', interval)返回 2(因为 50 小时 = 2 天 2 小时),但如果你想要"总共跨了几天",得用EXTRACT(EPOCH FROM (ts1 - ts2)) / 86400再取整。这个区别我在做留存分析时踩过,当时数据怎么算都对不上,最后发现是DATE_PART只取了天部分,把小时数丢了。
3. 时区与时间戳:那个让你差 8 小时的元凶
日期差算错,除了函数用错,第二大原因就是时区。这个问题在跨时区业务、日志分析、国际化产品里尤其突出,而且往往很隐蔽——数据看着对,一算就差几个小时,甚至差一整天。
3.1 TIMESTAMP 和 DATETIME 的语义差别
很多数据库里TIMESTAMP和DATETIME是两种不同的类型,语义不一样:
DATETIME通常存的是"墙上时间",不带时区信息,你存什么读什么。TIMESTAMP在很多数据库里会做时区转换,存储时转成 UTC,读取时按会话时区转回来。
这意味着,如果你用TIMESTAMP字段做日期差,而会话时区设置不一致,同一个查询在不同连接里可能算出不同结果。我遇到过最典型的情况:应用服务器时区是东八区,数据库会话时区是 UTC,两边对同一个TIMESTAMP的理解差了 8 小时,日期差自然就偏了。
3.2 统一时区是根治办法
处理这类问题的原则只有一条:在计算前,把所有时间统一到同一个时区。具体做法有两种:
- 存储层统一用 UTC,展示层再转本地时区。这是最推荐的做法,计算时不用操心时区。
- 计算时显式转换,比如 MySQL 用
CONVERT_TZ(ts, '+00:00', '+08:00'),PostgreSQL 用ts AT TIME ZONE 'Asia/Shanghai'。
-- MySQL:先统一到东八区再算日期差 SELECT DATEDIFF( DATE(CONVERT_TZ(ts_end, '+00:00', '+08:00')), DATE(CONVERT_TZ(ts_start, '+00:00', '+08:00')) ) AS diff_days FROM events;注意:
CONVERT_TZ依赖数据库的时区表是否加载完整,某些环境里没配时区表会返回 NULL,上线前务必在目标环境验证一次。
3.3 夏令时地区的额外坑
如果业务涉及有夏令时的地区,日期差还可能因为"某天只有 23 小时"而出错。比如某地春季调时间那天,从 0 点到次日 0 点实际只有 23 小时,如果你用"秒数除以 86400"算天数,会得到 0.958 天,取整后变成 0,但自然日明明跨了 1 天。
这种场景下,用自然日口径(按日期部分算)比用时间跨度口径更稳,因为自然日不受夏令时影响。这也是我前面强调先确认口径的原因之一——口径选对了,很多坑自动就绕开了。
4. 边界情况实战:那些让结果偏离预期的细节
函数会用只是及格线,真正拉开差距的是对边界情况的处理。下面这几个场景,我在实际项目里都遇到过,每一个都值得单独拿出来说。
4.1 同一天返回 0 还是 1
DATEDIFF(day, '2024-03-01', '2024-03-01')返回 0,这个没争议。但业务上"同一天"有时候要算作 1 天,比如会员"当天开通当天到期"算不算一天?这属于业务口径,不是 SQL 能决定的。我的建议是 SQL 层老老实实返回 0,业务层需要 +1 的时候自己加,别在 SQL 里偷偷改语义,否则后面维护的人会被绕晕。
4.2 跨月、跨年、闰年
自然日差的好处是跨月跨年自动正确,DATEDIFF('2024-03-01', '2024-02-01')返回 29(2024 是闰年),不需要你操心月份天数。但如果用"月数 × 30"这种土办法估算,就会错得离谱。我见过有人为了"算月份差"自己写(year1 - year2) * 12 + (month1 - month2),这个在纯月份差上是对的,但一旦要精确到天就完全不能用。
闰年 2 月 29 日这个点,用自然日函数是安全的,用时间戳除法也是安全的,唯一会出问题的是那些手写的、假设"每月 30 天"的逻辑。所以能用内置函数就别自己造轮子。
4.3 负数结果的方向问题
DATEDIFF返回负数表示前一个日期早于后一个日期,方向取决于参数顺序。跨库时这个方向特别容易搞反,因为 MySQL 是a - b,SQL Server 是end - start。我的习惯是,在业务代码里对结果取绝对值之前,先想清楚"我到底要哪个方向",因为方向本身往往携带业务含义(比如"提前几天"和"延后几天"是相反的)。
如果只是要"间隔天数"不关心方向,用ABS()包一层最省事:
SELECT ABS(DATEDIFF(day, date_a, date_b)) AS gap_days FROM your_table;但要注意,ABS之后你就丢失了先后信息,如果后续还要判断"谁在前谁在后",得另外保留原始差值。
4.4 大数据量下的性能考量
日期差计算本身很轻,但如果写在WHERE里做过滤,就可能让索引失效。典型反例:
-- 不推荐:对字段做函数运算,索引可能用不上 SELECT * FROM orders WHERE DATEDIFF(day, created_at, NOW()) > 30;更好的写法是把计算挪到常量侧:
-- 推荐:字段保持裸奔,常量侧计算 SELECT * FROM orders WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);这两种写法结果等价,但后者能走created_at上的索引,数据量大时性能差距可能是几十倍。这个技巧不只适用于日期差,所有"对列做函数运算"的过滤条件都该警惕。
5. 从"能算"到"算得稳":我的几条实操心得
写到这里,函数、口径、时区、边界都覆盖了,最后分享几条纯经验层面的东西,都是踩过坑之后总结出来的,文档里一般不会写。
5.1 把口径写进字段名或注释
我最推荐的一个习惯是:凡是涉及日期差的字段,命名或注释里带上口径。比如overdue_days_natural(自然日)、duration_hours(小时跨度),一眼就能看出算的是什么。团队里新人接手时不用猜,也避免了"两个人对同一个字段理解不同"的扯皮。
5.2 单元测试要覆盖跨天、跨月、闰年、时区
日期逻辑的测试用例,光测"正常情况"是不够的。我一般至少覆盖这几类:同一天、跨一天、跨月、跨年、闰年 2 月 29 日、时区边界(比如 UTC 23 点和本地 7 点)、以及 NULL 输入。这几类过了,基本能挡住 90% 的线上问题。
5.3 别在应用层和数据库层重复算
有的系统在 SQL 里算一遍日期差,应用层又用代码算一遍,两边口径不一致时就会打架。我的原则是只在一处算,要么全在 SQL,要么全在应用层,另一处直接用结果。这样口径唯一,排查问题时也有明确的落点。
5.4 迁移数据库前先跑一遍对照
跨库迁移时,日期函数是最容易出问题的地方之一。我的做法是,迁移前写一组对照查询,把同一批数据在新旧库各跑一遍,逐行比对结果。差异行单独分析,是函数语义不同还是时区设置不同,定位清楚再改代码。这一步花的时间,远比上线后救火少。
5.5 记住"自然日"和"时间跨度"是两套工具
最后再强调一次这个核心区分。自然日差用DATEDIFF这类按日期部分算的函数,时间跨度用时间戳相减再换算。两套工具各有适用场景,混用就是 bug 的温床。每次写之前先问自己一句"我要的是哪种",能省掉后面大量返工。
日期差这个需求小归小,但它牵扯到类型、时区、口径、性能、跨库兼容好几个维度,任何一个没处理好都会在业务里放大成对账错误或者数据偏差。把上面这些点过一遍,再动手写,基本就能做到"算得对、算得稳、换库也不慌"。