写SQL查数据,十有八九都会碰到日期。无论是看报表、跑统计,还是做数据清洗,日期过滤和日期聚合几乎逃不掉。我最早用SQL Server,后来切到MySQL,又接触过PostgreSQL和达梦,最大的感受是:日期函数的名字看起来差不多,可真到换库的时候,坑是一个接一个,写法也是五花八门。这篇就把我平时写查询时最常用的日期函数完整梳理一遍,从取当前时间、拆年/月/日,到日期加减、算差值、格式化,再到报表场景里的实战用法,每一步都配上可以直接跑的SQL示例。不管你是刚学SQL的新手,还是写了几年查询想查漏补缺的同学,这篇都值得先收藏再慢慢看。
1. 先搞清楚日期函数帮我们解决什么问题
1.1 业务场景:你什么时候会用到日期函数
日期函数在SQL查询里属于“高频中的高频”,因为业务数据绝大多数都带时间戳。我随手列几个最常见的场景:
- 报表统计:日报、周报、月报、季报,几乎每个分组汇总都离不开日期维度。
- 数据清洗:找出异常时间、补齐缺失日期、判断记录是否在有效时间范围内。
- 业务规则判断:比如订单是否超时未付款、会员是否到期、订阅是否需要续费提醒。
- 数据分析:计算用户年龄、统计日活跃/月活跃、做同比环比对比。
这些场景表面看是不同业务,其实核心都是同一类问题:拿到一个日期时间值,然后对它做提取、计算、比较、格式化。所以我把日期函数单独拎出来讲,就是因为它们在太多场景里都会复用。
1.2 日期函数的分类与学习路线
对于写查询的人来说,日期函数不需要死记硬背,但心里要有一张分类地图。我一般把常用日期函数分成五类:
| 分类 | 典型函数 | 要解决什么问题 |
|---|---|---|
| 获取当前日期时间 | GETDATE / CURRENT_TIMESTAMP / NOW / SYSDATE | 拿到“现在”这个时间点 |
| 提取日期部分 | YEAR / MONTH / DAY / DATEPART / EXTRACT / QUARTER | 从完整日期中拆出年、月、日、季度等 |
| 日期加减计算 | DATEADD / DATE_SUB / INTERVAL / EOMONTH | 计算N天前、下个月、月末等 |
| 日期差值计算 | DATEDIFF / TIMESTAMPDIFF | 算两个日期之间差了多少天/月/年 |
| 格式化与转换 | CONVERT / FORMAT / DATE_FORMAT / TO_CHAR / STR_TO_DATE | 把日期变成指定字符串,或把字符串变成日期 |
学习的时候不需要一上来全记,先把第一类到第四类用熟,格式化那类用到再查。接下来我按这个顺序一个一个拆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 取当前时间和拆日期部分:最基础也最常用
2.1 获取当前日期时间,各数据库怎么取
获取当前时间是最基础的日期函数,但不同数据库的写法差异很大。我自己跨库写查询的时候,经常看到同事把SQL Server的写法直接扔到MySQL里跑,然后报错半天找不出原因。这里整理一个对照表:
| 数据库 | 获取当前日期时间 | 获取当前日期(不含时间) |
|---|---|---|
| SQL Server | GETDATE()、SYSDATETIME()、CURRENT_TIMESTAMP | CAST(GETDATE() AS date) |
| MySQL | NOW()、SYSDATE() | CURDATE()、CURRENT_DATE |
| PostgreSQL | NOW()、CURRENT_TIMESTAMP | CURRENT_DATE |
| Oracle | SYSDATE | TRUNC(SYSDATE) |
这里有几个实用细节:
- CURRENT_TIMESTAMP是标准SQL写法,在SQL Server、MySQL、PostgreSQL里都能用。如果你写通用工具或存储过程,优先用它,迁移成本最低。
- SQL Server里GETDATE()返回datetime类型,精度大约3.33毫秒;SYSDATETIME()返回datetime2类型,精度更高。普通业务用GETDATE()就够了,不需要追求高精度。
- 如果只需要日期,MySQL直接写CURDATE(),SQL Server也可以CAST(GETDATE() AS date),不要先取完整时间再做字符串截取,那样既绕又容易出时区问题。
顺手给个例子:
sql复制-- SQL Server
SELECT GETDATE() AS now_dt, CURRENT_TIMESTAMP AS now_ts, CAST(GETDATE() AS date) AS today;
-- MySQL
SELECT NOW() AS now_dt, CURRENT_TIMESTAMP AS now_ts, CURDATE() AS today;
2.2 提取年、月、日、季度、星期几
取到完整日期之后,下一步经常是拆出年、月、日。SQL Server和MySQL都有专门的函数:
sql复制-- SQL Server
SELECT
YEAR(order_date) AS year_no,
MONTH(order_date) AS month_no,
DAY(order_date) AS day_no,
DATEPART(quarter, order_date) AS quarter_no,
DATEPART(weekday, order_date) AS weekday_no
FROM orders;
-- MySQL
SELECT
YEAR(order_date) AS year_no,
MONTH(order_date) AS month_no,
DAY(order_date) AS day_no,
QUARTER(order_date) AS quarter_no,
DAYOFWEEK(order_date) AS weekday_no
FROM orders;
如果你用的是PostgreSQL或者Oracle,标准SQL写法是EXTRACT:
sql复制-- PostgreSQL / Oracle
SELECT
EXTRACT(YEAR FROM order_date) AS year_no,
EXTRACT(MONTH FROM order_date) AS month_no,
EXTRACT(DAY FROM order_date) AS day_no,
EXTRACT(QUARTER FROM order_date) AS quarter_no
FROM orders;
MySQL也支持EXTRACT写法,SQL Server则没有EXTRACT,只能用DATEPART或者YEAR/MONTH/DAY。这就是典型的多库兼容问题,写之前先搞清楚目标数据库是哪家。
2.3 取日期部分的几个细节坑
拆日期部分看着简单,实际使用时容易在“星期”上翻车。
- SQL Server的DATEPART(weekday, ...)返回值受@@DATEFIRST影响。默认情况下周日是1,但如果会话改了DATEFIRST,结果就不一样了。
- MySQL的DAYOFWEEK()周日是1,周六是7;而WEEKDAY()周一是0,周日是6。这两个函数很容易记混。
所以我的建议是:跨周统计尽量用日期范围过滤,不要依赖星期函数做核心逻辑。如果确实要算“今天是周几”,先在测试环境跑一下确认返回值,再写进正式代码。
3. 日期加减、差值计算与格式化转换
3.1 日期加减:DATEADD / DATE_SUB / INTERVAL
业务里最常写的日期计算就是“N天前”“三个月前”“下月初”这类。
SQL Server用DATEADD:
sql复制-- 最近7天
SELECT DATEADD(day, -7, GETDATE()) AS last_7_days;
-- 下个月同一天
SELECT DATEADD(month, 1, GETDATE()) AS next_month;
-- 去年今天
SELECT DATEADD(year, -1, GETDATE()) AS last_year_today;
MySQL用DATE_ADD和DATE_SUB配合INTERVAL:
sql复制-- 最近7天
SELECT DATE_SUB(NOW(), INTERVAL 7 DAY) AS last_7_days;
-- 下个月同一天
SELECT DATE_ADD(NOW(), INTERVAL 1 MONTH) AS next_month;
-- 去年今天
SELECT DATE_SUB(NOW(), INTERVAL 1 YEAR) AS last_year_today;
PostgreSQL更直接,可以直接在日期上加减interval:
sql复制SELECT NOW() - INTERVAL '7 days' AS last_7_days;
这里有一个很重要的特性:DATEADD这类函数会自动处理跨月进位。比如SQL Server执行DATEADD(month, 1, '2024-01-31'),会得到2024-02-29,因为2月没有31号。这个自动进位在业务上可能不是你想要的,如果你的逻辑要求“必须保留当月末”,要单独做边界处理。
3.2 日期差值:DATEDIFF / TIMESTAMPDIFF
算两个日期之间差了多少天、多少月、多少年,是统计年龄、计算活跃周期的核心。
SQL Server写法:
sql复制-- 两个日期相差天数
SELECT DATEDIFF(day, '2024-01-01', '2024-01-31') AS diff_days;
-- 结果:30
MySQL写法有点不一样,DATEDIFF在MySQL里只能算天数,而且参数顺序是相反的:
sql复制-- MySQL: DATEDIFF(结束日期, 开始日期)
SELECT DATEDIFF('2024-01-31', '2024-01-01') AS diff_days;
-- 结果:30
-- 按指定单位计算,用TIMESTAMPDIFF
SELECT TIMESTAMPDIFF(MONTH, '2024-01-01', '2024-06-15') AS diff_months;
-- 结果:5
PostgreSQL用法:
sql复制SELECT DATE_PART('day', '2024-01-31'::timestamp - '2024-01-01'::timestamp) AS diff_days;
这里必须提醒一个经典坑:SQL Server的DATEDIFF是“跨越边界计数”,不是整天数整除。比如:
sql复制SELECT DATEDIFF(year, '2024-12-31', '2025-01-01') AS diff_years;
-- 结果:1
明明只隔了一天,但年份边界跨过去了,结果就是1。同样的,DATEDIFF(month, '2024-01-31', '2024-02-01')会得到1个月,而不是“不足一个月算0”。写代码之前先想清楚这个语义,否则年龄计算和月份统计非常容易出偏差。
3.3 格式化与字符串互转
日期格式化是另一个高频操作,尤其在做报表输出的时候。不同数据库的格式占位符风格完全不一样,我经常看到有人把MySQL的%Y写进SQL Server的FORMAT里,结果输出一堆没用的字符。
SQL Server常用两种方式:
sql复制-- CONVERT + 样式码,数字样式码要记忆
SELECT CONVERT(varchar(10), GETDATE(), 120) AS date_str;
-- 结果:2024-06-01
-- FORMAT 使用.NET格式,可读性好但性能差
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd') AS date_str;
MySQL用DATE_FORMAT:
sql复制SELECT DATE_FORMAT(NOW(), '%Y-%m-%d') AS date_str;
-- 结果:2024-06-01
PostgreSQL用TO_CHAR:
sql复制SELECT TO_CHAR(NOW(), 'YYYY-MM-DD') AS date_str;
-- 结果:2024-06-01
字符串转日期也是反向操作:
sql复制-- SQL Server
SELECT CONVERT(datetime, '2024-06-01', 120) AS dt;
-- MySQL
SELECT STR_TO_DATE('2024-06-01', '%Y-%m-%d') AS dt;
-- PostgreSQL
SELECT TO_DATE('2024-06-01', 'YYYY-MM-DD') AS dt;
关于性能,我在SQL Server里测过,FORMAT(GETDATE(), 'yyyy-MM-dd')在百万级数据上比CONVERT(varchar(10), GETDATE(), 120)慢好几倍。原因很简单,FORMAT走的是.NET运行时,开销大。所以数据量大、对性能敏感的场景,能不用FORMAT就不用,SQL Server里首选CONVERT+样式码。
4. 实战:用日期函数做统计报表
4.1 按天、周、月、季度分组统计
报表里最常见的写法就是“按天统计订单数”。核心思路是:把日期时间字段格式化到指定粒度,然后GROUP BY。
MySQL写法:
sql复制SELECT
DATE_FORMAT(order_date, '%Y-%m-%d') AS stat_date,
COUNT(*) AS order_cnt,
SUM(amount) AS total_amount
FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2024-04-01'
GROUP BY DATE_FORMAT(order_date, '%Y-%m-%d')
ORDER BY stat_date;
SQL Server写法:
sql复制SELECT
CONVERT(varchar(10), order_date, 23) AS stat_date,
COUNT(*) AS order_cnt,
SUM(amount) AS total_amount
FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2024-04-01'
GROUP BY CONVERT(varchar(10), order_date, 23)
ORDER BY stat_date;
按月、按季度类似:
sql复制-- MySQL 按月
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
-- MySQL 按季度
GROUP BY YEAR(order_date), QUARTER(order_date)
-- SQL Server 按月
GROUP BY CONVERT(varchar(7), order_date, 23)
-- SQL Server 按季度
GROUP BY YEAR(order_date), DATEPART(quarter, order_date)
有一个很容易踩的雷:GROUP BY后面不能直接写别名,至少MySQL和SQL Server在这块行为不完全一样。ORDER BY可以用别名,GROUP BY最好用原表达式,避免不同数据库行为不一致导致报错或结果错误。
4.2 常用时间窗口:本月、上月、最近30天
报表过滤条件里,时间窗口几乎是必写的。我整理了几个高频窗口的写法:
本月第一天
sql复制-- SQL Server
SELECT DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0) AS first_day_of_month;
-- MySQL
SELECT DATE_FORMAT(CURDATE(), '%Y-%m-01') AS first_day_of_month;
-- PostgreSQL
SELECT DATE_TRUNC('month', CURRENT_DATE) AS first_day_of_month;
SQL Server这个写法看起来绕,但很好用。DATEDIFF(month, 0, GETDATE())是算“从1900-01-01到当前日期跨越了多少个月”,再把这个月数加回1900-01-01,结果就是月初的零点零分零秒。
上月最后一天
sql复制-- SQL Server 2012+
SELECT EOMONTH(GETDATE(), -1) AS last_day_of_last_month;
-- SQL Server 2008 R2及更老版本没有EOMONTH
SELECT DATEADD(day, -1, DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)) AS last_day_of_last_month;
-- MySQL
SELECT LAST_DAY(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) AS last_day_of_last_month;
最近30天
sql复制-- SQL Server
WHERE order_date >= DATEADD(day, -30, GETDATE())
-- MySQL
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
这里要提醒一下:如果“最近30天”业务上指的是“当天往前推30个自然日”,用这个写法没问题。但如果你想要的是“本月1号到昨天”,那就得用上面的月初和EOMONTH组合,别混用。
4.3 计算年龄和同比环比
年龄计算是日期函数的经典考题。MySQL的TIMESTAMPDIFF直接能用:
sql复制SELECT TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age FROM users;
SQL Server的DATEDIFF(YEAR, ...)会按“跨越了几个元旦”算,直接用在年龄上会偏大。标准修正是这样:
sql复制SELECT
DATEDIFF(YEAR, birth_date, GETDATE())
- CASE
WHEN DATEADD(YEAR, DATEDIFF(YEAR, birth_date, GETDATE()), birth_date) > GETDATE()
THEN 1 ELSE 0
END AS age
FROM users;
逻辑是:先按年份边界算出一个年龄,然后看“在算出的年龄上过生日的那天”是不是还没到,如果没到就减一岁。这个修正我在生产环境跑过,能正确处理绝大多数生日逻辑。唯一要注意的是2月29日出生的人在非闰年的生日处理,各数据库行为会有细微差异,建议在需求评审阶段和业务方确认清楚。
同比环比在报表里也经常用到。比如“去年同月的订单量”:
sql复制-- SQL Server
WHERE order_date >= DATEADD(year, -1, '2024-06-01')
AND order_date < DATEADD(year, -1, '2024-07-01')
-- MySQL
WHERE order_date >= DATE_SUB('2024-06-01', INTERVAL 1 YEAR)
AND order_date < DATE_SUB('2024-07-01', INTERVAL 1 YEAR)
这种写法比“把年份字段减1再比较”要稳,因为直接用函数改整个日期范围,不会因为日期格式问题出错。
5. 常见问题与排查技巧实录
5.1 日期比较:用 >= 和 <,少用 BETWEEN
日期时间字段做范围过滤时,最经典的问题就是漏数据。比如要查2024年1月的数据:
sql复制-- 错误示范
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31'
这条SQL在有时间部分的datetime字段下,会漏掉1月31日00:00:00之后到下一天之前的记录。因为BETWEEN是包含边界的,等价于<= '2024-01-31 00:00:00'。正确写法是:
sql复制WHERE order_date >= '2024-01-01'
AND order_date < '2024-02-01'
这个习惯我在团队里反复强调过,几乎所有线上日期范围漏数问题,最后排查下来都跟BETWEEN直接相关。当成一个强制规范:日期范围过滤统一用 >= 和 <,不用BETWEEN。
5.2 DATEDIFF的单位边界怎么排查
前面提到SQL Server的DATEDIFF是“跨越边界计数”,这会导致一些反直觉的结果。我排查过最典型的一个就是算月薪对应天数:
sql复制SELECT DATEDIFF(month, '2024-01-31', '2024-02-01') AS diff_month;
-- 结果:1
业务上从1月31日到2月1日肯定不算一个月,但DATEDIFF返回1。这种场景不应该用DATEDIFF(month),要么用DAYS先算出精确天数再除,要么用TIMESTAMPDIFF(MySQL)这种更贴近自然差的函数。
排查技巧:遇到DATEDIFF结果异常时,先把起止日期往小范围打印出来,人工数一遍边界,再去套SQL,基本一眼就能看出是不是跨边界计数的问题。
5.3 WHERE条件里别用函数包列
这个坑的代价通常是索引失效。比如:
sql复制-- 错误示范:这条SQL无法正常走order_date上的索引
WHERE YEAR(order_date) = 2024
为了让查询能走索引,应该改成范围条件:
sql复制WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
MySQL和SQL Server在这件事上的原理一样:对列套函数后,数据库优化器无法直接利用B+树的顺序性,只能全表扫描。我自己在线上排查过一条慢SQL,就是把DATE_FORMAT(order_date, '%Y-%m-%d') = '2024-06-01'这种写法换成了范围条件,查询时间从几百毫秒降到了个位数毫秒。
5.4 NULL、闰年和时区的边界处理
日期字段经常有空值,统计时如果不处理,结果会莫名其妙变少。建议在聚合前用COALESCE或ISNULL兜底:
sql复制-- SQL Server
SELECT ISNULL(CONVERT(varchar(10), order_date, 23), '未知日期') AS stat_date, COUNT(*) AS cnt
FROM orders
GROUP BY CONVERT(varchar(10), order_date, 23);
-- MySQL
SELECT COALESCE(DATE_FORMAT(order_date, '%Y-%m-%d'), '未知日期') AS stat_date, COUNT(*) AS cnt
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m-%d');
闰年问题主要出现在2月29日出生的人、月末加减、跨年周统计这几个场景。月末最好用EOMONTH或者LAST_DAY,不要自己用“固定30天/31天”去推算。时区问题则是老生常谈:能用UTC存储就不要存本地时间,展示时再做转换。
5.5 日期函数速查表
我把最常用的日期操作整理成一张对照表,放在团队wiki里反复用,现在贴出来:
| 功能 | SQL Server | MySQL | PostgreSQL |
|---|---|---|---|
| 当前日期时间 | GETDATE() | NOW() | NOW() |
| 当前日期 | CAST(GETDATE() AS date) | CURDATE() | CURRENT_DATE |
| 提取年 | YEAR(d) | YEAR(d) | EXTRACT(YEAR FROM d) |
| 提取月 | MONTH(d) | MONTH(d) | EXTRACT(MONTH FROM d) |
| 提取日 | DAY(d) | DAY(d) | EXTRACT(DAY FROM d) |
| 季度 | DATEPART(quarter, d) | QUARTER(d) | EXTRACT(QUARTER FROM d) |
| 加N天 | DATEADD(day, N, d) | DATE_ADD(d, INTERVAL N DAY) | d + INTERVAL 'N days' |
| 减N天 | DATEADD(day, -N, d) | DATE_SUB(d, INTERVAL N DAY) | d - INTERVAL 'N days' |
| 日期差天数 | DATEDIFF(day, start, end) | DATEDIFF(end, start) | end::date - start::date |
| 格式化 | CONVERT(varchar(10), d, 23) | DATE_FORMAT(d, '%Y-%m-%d') | TO_CHAR(d, 'YYYY-MM-DD') |
| 字符串转日期 | CONVERT(datetime, '2024-01-01', 120) | STR_TO_DATE('2024-01-01', '%Y-%m-%d') | TO_DATE('2024-01-01', 'YYYY-MM-DD') |
| 月末 | EOMONTH(d) | LAST_DAY(d) | (DATE_TRUNC('month', d) + INTERVAL '1 month - 1 day')::date |
写日期函数这几年,我最大的心得是:先把“取当前时间、提取部分、加减计算、差值计算”这四类用熟,格式化那类用到再翻文档。最怕的不是不会写,而是只背了某一个数据库的写法,然后换库不换脑。如果你也经常在多个数据库之间切换,建议把上面这张速查表存成自己的笔记,能省下大量时间。
另外还有一个习惯值得坚持:日期字段尽量存成标准日期类型,不要用varchar拼字符串。字符串日期在排序、范围查询、聚合统计上问题一堆,真到了要用日期函数的时候,各种隐式转换会让你怀疑人生。先保证数据类型干净,再谈函数熟练度,这是写SQL最扎实的路线。
