1. 动手写日期函数前,先搞清楚列里装的是“时间点”还是“日历日”
写 SQL 查询时,很多看起来一模一样的日期条件,最后跑出来的数据却对不上,根子往往不在函数本身,而在表里的字段到底是什么类型、存的到底是哪个时区的时间点。日期函数只是工具,先看清数据表的“时钟表盘”,再去选函数,才不会越写越乱。
1.1 不同数据库里的日期时间类型,名字相似但容量不同
我最早用的 MySQL,后来切到 PostgreSQL,中间还维护过 SQL Server 和 Oracle 的报表查询。最大的感受是:同样的“日期”,在不同数据库里根本不是同一个概念。你写 DATE 列,MySQL 里只存年月日;Oracle 的 DATE 类型却能把时分秒全装进去。这直接决定了你用截断函数之后的结果长什么样。
| 数据库 | 常见日期时间类型 | 是否自带时区 | 关键注意事项 |
|---|---|---|---|
| MySQL | DATE、DATETIME、TIMESTAMP |
TIMESTAMP 有会话时区转换 |
DATETIME 不带时区,存进去是多少就返回多少 |
| SQL Server | date、datetime、datetime2、datetimeoffset |
datetimeoffset 带时区 |
老 datetime 精度和范围都有限,新库建议用 datetime2 |
| PostgreSQL | date、timestamp without time zone、timestamp with time zone |
timestamptz 带时区 |
不推荐用字符串保存日期,timestamptz 底层是 UTC 存储 |
| Oracle | DATE、TIMESTAMP、TIMESTAMP WITH TIME ZONE |
极少用自带类型存时区 | Oracle 的 DATE 等于其他数据库的 DATETIME,里面包含时间部分 |
为什么这段看起来像基础课的内容值得先说?因为不少同事跑来问我“为什么我 WHERE date = '2024-01-01' 查不到数”,结果一查,字段是 timestamp,库里那条数据是 2024-01-01 08:30:00。字符串和日期比较时数据库会隐式转换,但时间部分还在,自然匹配不上。这不是日期函数的锅,是“字段真身”没看清。
1.2 时区问题:同一个函数,不同区域跑出的“今天”不一致
做国内业务可能感受不深,一旦涉及海外用户、跨境订单,或者数据仓库里的时间统一存成 UTC,日期函数就会开始作妖。比如订单表里的 created_at 存的是 UTC 时间,你直接写 DATE(created_at) 去按自然日分组,北京时间早上 8 点之前产生的订单,会被归到前一天。
这类问题的处理方式有两种。一种是入库和查询统一约定用 UTC,展示层再去转换;另一种是在 SQL 里显式转换,比如 MySQL 环境:
sql复制-- 将 UTC 时间转换为东八区时间
SELECT DATE(CONVERT_TZ(created_at, '+00:00', '+08:00')) AS biz_date
FROM orders;
但要注意,CONVERT_TZ 依赖 MySQL 的时区表是否初始化,不是所有环境都能直接用。更稳妥的做法是应用层在写入前就把时间转成业务时区。SQL Server 里可以用 AT TIME ZONE,PostgreSQL 里则是比较规矩的 AT TIME ZONE 表达式。不要依赖“先取字符串再改时间”的土办法,那会让后续排序、比较、分组全部变得不可控。
1.3 “今天”到底是自然日,还是最近 24 小时
这是我在需求评审里反复确认的问题。你说“查今天的数据”,对方可能想要的是从今天 00:00:00 到今天 23:59:59 之间的自然日数据,也可能只是“最近 24 小时滚动数据”。这两种写法完全不同。
自然日是左闭右开区间:
sql复制WHERE created_at >= '2025-01-06 00:00:00'
AND created_at < '2025-01-07 00:00:00';
滚动 24 小时是:
sql复制WHERE created_at >= NOW() - INTERVAL 24 HOUR;
如果需求文档只写了“最近一天”,我一般会再问一句:按自然日算还是按滚动时距算。这种语义确认比背函数更省事,因为只要语义错了,后面所有日期函数组合都救不回来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 取当前时间不要只背一个 now(),还要知道它“卡”在哪个时刻
“取当前时间”是日期函数里最常用的一类。大家张口就能说 NOW()、GETDATE()、SYSDATE,但这些函数返回值的含义是有细微差异的。尤其在定时任务、数据回刷场景里,如果没搞清楚取值时点,很容易把日期边界算错。
2.1 几个常用取当前时间的函数对照
先放一份我平时查得最多的对照表:
| 数据库 | 当前日期时间 | 当前日期 | 当前 UTC 时间 | 说明 |
|---|---|---|---|---|
| MySQL | NOW() |
CURDATE() |
UTC_TIMESTAMP() |
NOW() 是语句开始执行时的时间 |
| SQL Server | GETDATE() |
CAST(GETDATE() AS date) |
GETUTCDATE() |
新环境也可以看 SYSDATETIME() |
| PostgreSQL | NOW() 或 CURRENT_TIMESTAMP |
CURRENT_DATE |
CURRENT_TIMESTAMP AT TIME ZONE 'UTC' |
事务中多次调用 NOW() 值不变 |
| Oracle | SYSDATE |
TRUNC(SYSDATE) |
SYS_EXTRACT_UTC(SYSTIMESTAMP) |
CURRENT_DATE 返回会话时区当前日期 |
很多初学者以为 NOW() 会“实时变化”,其实 MySQL 和 PostgreSQL 里的 NOW() 在很大程度上是稳定的。MySQL 中,同一条 SQL 里多处使用 NOW(),返回的是同一条语句开始时的时间;PostgreSQL 里更严格,NOW() 返回的是当前事务开始时间,同一个事务里反复取也一致。如果真要拿一个实时时钟,PostgreSQL 使用 clock_timestamp()。
2.2 为什么批处理脚本最容易在“当前时间”上翻车
假设有一个每天凌晨 1 点执行的报表任务,要统计“昨天全天的订单”。最粗暴的写法是:
sql复制WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)
AND created_at < CURDATE();
这个写法在当天凌晨跑没问题,但如果任务因为故障被延迟到 23:50 补跑,或者不小心在跨天后重试,结果就会偏差。更稳的写法是把统计窗口参数化,由调度平台传入“业务日期”,比如任务日期是 2025-01-07,窗口就是 2025-01-06 00:00:00 到 2025-01-07 00:00:00。
我并不反对在 SQL 里用当前日期函数,它们非常适合临时查询和快速对账。但长期定时任务里,日期窗口应当由外部参数控制,核心原因是“当前时间”是一个执行期环境状态,而报表窗口是一个业务定义。把这两个概念混在一起写,哪天数据对不上,排查成本会很高。
2.3 用当前时间生成日期边界,再去做范围条件
临时写当日数据时,我会先构造出自然日的边界,而不是对带时间戳的列反复调用 DATE()。
以 PostgreSQL 为例:
sql复制SELECT *
FROM orders
WHERE created_at >= CURRENT_DATE
AND created_at < CURRENT_DATE + 1;
这里的 CURRENT_DATE + 1 是 PostgreSQL 的日期加整数写法,表示下一天的凌晨。MySQL 里可以写成:
sql复制WHERE created_at >= CURRENT_DATE
AND created_at < DATE_ADD(CURRENT_DATE, INTERVAL 1 DAY);
一句话总结:对列做函数处理会导致索引难以生效,先算出边界值再和列直接比较,是更稳定的做法。这一条经验在接下来的章节还会反复用到。
3. 把日期拆成年、月、日:取数时最顺手的几组函数
做报表的人最熟悉的一类日期函数是“拆分函数”,把一整个时间戳拆成年、月、日、小时、星期、季度。它让一行数据可以被规整到不同的日历维度里,再配合 GROUP BY 做聚合。
3.1 通用取法:EXTRACT 和 DATEPART 的“分工”
如果只记一个接近 SQL 标准的函数,我推荐 EXTRACT。PostgreSQL、Oracle、MySQL 都支持这种写法:
sql复制SELECT
EXTRACT(YEAR FROM created_at) AS year_no,
EXTRACT(MONTH FROM created_at) AS month_no,
EXTRACT(DAY FROM created_at) AS day_no,
EXTRACT(HOUR FROM created_at) AS hour_no
FROM orders;
MySQL 里也常用简短写法:
sql复制SELECT YEAR(created_at),
MONTH(created_at),
DAY(created_at),
HOUR(created_at)
FROM orders;
SQL Server 则有自己的一套 DATEPART 写法:
sql复制SELECT
DATEPART(year, created_at),
DATEPART(month, created_at),
DATEPART(day, created_at),
DATEPART(hour, created_at)
FROM orders;
Oracle 的 EXTRACT 可以从 DATE 里取年、月、日,但取小时、分钟更推荐 TO_CHAR,因为 EXTRACT(HOUR FROM SYSDATE) 的使用限制比较多。这也是实战中的常见小坑:别指望一种数据库的函数能 100% 平移。
3.2 星期和季度:比月日更容易踩坑的拆分维度
季度挺好处理。MySQL 可以直接 QUARTER(date),PostgreSQL 可以用 EXTRACT(QUARTER FROM date),Oracle 可以用 TO_CHAR(date, 'Q')。星期相关的语义则复杂得多,因为不同地区对“一周的第一天”定义不一样。
MySQL 里的 WEEKDAY(date) 返回 0-6,0 表示周一;DAYOFWEEK(date) 返回 1-7,1 表示周日。SQL Server 的 DATEPART(weekday, date) 返回值受 @@DATEFIRST 影响,默认设置下 1 是周日,但如果有人执行了 SET DATEFIRST 1,同样的函数会变成 1 是周一。PostgreSQL 则推荐用 EXTRACT(ISODOW FROM date),它固定返回 1-7,1 是周一,不会受会话设置影响。
所以我在跨数据库写周统计时,口径一定是在需求阶段就明确好的:周一是本周开始还是周日是本周开始,要不要按 ISO 周计算,跨年那周算给去年还是今年。日期拆分本身不难,难的是口径统一。
3.3 把拆分结果拼成统计键
实际统计里,比起单独取 YEAR、MONTH,更常见的是把这些数拼成“统计键”。比如按订单年月统计,就要生成 202501 这种维度。
MySQL:
sql复制SELECT
DATE_FORMAT(created_at, '%Y-%m') AS order_month,
COUNT(*)
FROM orders
GROUP BY DATE_FORMAT(created_at, '%Y-%m');
PostgreSQL:
sql复制SELECT
TO_CHAR(created_at, 'YYYY-MM') AS order_month,
COUNT(*)
FROM orders
GROUP BY TO_CHAR(created_at, 'YYYY-MM');
SQL Server 可以用 FORMAT(created_at, 'yyyy-MM'),但 FORMAT 在大数据量上很慢;追求性能时我用:
sql复制SELECT
CONVERT(VARCHAR(7), created_at, 120) AS order_month,
COUNT(*)
FROM orders
GROUP BY CONVERT(VARCHAR(7), created_at, 120);
生成月首日期而不是月字符串,做后续关联时往往更好用。MySQL 中可以直接把日期截到当月 1 号:
sql复制SELECT DATE_FORMAT(created_at, '%Y-%m-01') AS month_start
FROM orders;
PostgreSQL 用 DATE_TRUNC('month', created_at),返回的仍是时间戳类型,适合继续做日期加减和排序。
4. 日期加减与日期差:跨月、跨年、月末的坑一次说清楚
日期加减是我觉得最需要“警惕性”的内容。因为直觉上“加一个月”很简单,但 1 月 31 日加一个月到底是什么?不同数据库的处理方式基本一致,都是把结果矫正到目标月的最后一天。但很多新手第一次遇到时都会觉得反直觉。
4.1 日期加减的常见写法
MySQL 里标准写法是 DATE_ADD 和 DATE_SUB:
sql复制SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH);
-- 结果:2024-02-29
SELECT DATE_ADD('2024-01-31', INTERVAL -1 DAY);
-- 结果:2024-01-30
PostgreSQL 里习惯使用 INTERVAL 表达式:
sql复制SELECT DATE '2024-01-31' + INTERVAL '1 month';
-- 结果:2024-02-29 00:00:00
SELECT TIMESTAMP '2024-01-31 10:00:00' + INTERVAL '1 day';
-- 结果:2024-02-01 10:00:00
SQL Server:
sql复制SELECT DATEADD(month, 1, '2024-01-31');
-- 结果:2024-02-29
SELECT DATEADD(day, -1, '2024-01-01');
-- 结果:2023-12-31
Oracle:
sql复制SELECT ADD_MONTHS(DATE '2024-01-31', 1) FROM dual;
-- 结果:2024-02-29
SELECT SYSDATE + 1 FROM dual;
-- Oracle 里 DATE + 数字,数字单位是“天”
Oracle 的 DATE + 1 是加一天,不是加一秒。这个和 PostgreSQL 里 date + integer 类似,都是按“天”来计算的。如果你要加 8 小时,Oracle 里写 SYSDATE + 8/24,PostgreSQL 里最好用 INTERVAL '8 hours',MySQL 也统一用 INTERVAL 8 HOUR。
4.2 日期差:DATEDIFF 的方向不要凭记忆
不同数据库的 DATEDIFF 参数顺序差异太大,我吃过亏。
MySQL:
sql复制SELECT DATEDIFF('2024-03-01', '2024-02-01');
-- 返回 29,是“前一个日期减后一个日期”
SQL Server:
sql复制SELECT DATEDIFF(day, '2024-02-01', '2024-03-01');
-- 返回 29,是“第二个日期减第一个日期”
PostgreSQL 里直接用日期相减,返回天数:
sql复制SELECT DATE '2024-03-01' - DATE '2024-02-01';
-- 返回 29
Oracle 里日期直接相减,结果是小数天:
sql复制SELECT DATE '2024-03-01 10:00:00' - DATE '2024-02-01 00:00:00'
FROM dual;
-- 返回 29.4166667
所以我有个习惯:写 DATEDIFF 前先看一眼上一个参数是日期还是日期部分,再确认是“前减后”还是“后减前”。否则代码评审时一眼扫过去,符号反了很难发现。
4.3 精确到小时、分钟的差值,别直接拿天数凑
如果两个时间点相隔 23 小时,直接做日期差会得到 0 天,但你想知道的“超过多少小时”会有不同的答案。要精确算小时差,需要按数据库区分。
MySQL:
sql复制SELECT TIMESTAMPDIFF(HOUR, '2024-01-01 22:00:00', '2024-01-02 20:00:00');
-- 返回 22
SQL Server:
sql复制SELECT DATEDIFF(HOUR, '2024-01-01 22:00:00', '2024-01-02 20:00:00');
-- 跨越小时边界,结果可能返回 22
但要注意,SQL Server 的 DATEDIFF 是按“边界跨越次数”计算,不是严格的时间差。比如从 23:59 到次日 00:01,按小时算只要 2 分钟,但 DATEDIFF(HOUR, ...) 返回 1,因为它跨越了一个小时的边界。这是很多人排查很久都找不到原因的经典坑。
PostgreSQL 里更推荐用 EXTRACT(EPOCH FROM ...) 得到秒数,再做换算:
sql复制SELECT EXTRACT(EPOCH FROM (TIMESTAMP '2024-01-02 20:00:00'
- TIMESTAMP '2024-01-01 22:00:00')) / 3600.0 AS hour_diff;
4.4 用日期差算月末、月初边界
按月加减函数能自动处理月末逻辑,这算是最省心的部分。MySQL 里可以直接拿月末:
sql复制SELECT LAST_DAY('2024-02-10');
-- 返回 2024-02-29
拿月初则配合日期格式化:
sql复制SELECT DATE_FORMAT('2024-02-10', '%Y-%m-01');
SQL Server 没有 MySQL 这么好记的 LAST_DAY,但 2012+ 提供了 EOMONTH:
sql复制SELECT EOMONTH('2024-02-10');
PostgreSQL 里拿月末更常见的是用“下个月第一天减一天”的方式:
sql复制SELECT DATE_TRUNC('month', DATE '2024-02-10') + INTERVAL '1 month' - INTERVAL '1 day';
这类写法在生成账期、还款日、周期报表时特别有用。不要手写 28、29、30、31 的 switch,数据库已经帮你处理了闰年和大小月。
5. 日期格式化与字符串互转:格式串写错一个,数据面目全非
日期函数用多了,我发现很多问题不是不会取日期,而是日期和字符串转换时格式符号写混了。每个数据库对格式符号的约定不一样,最坑的是 MySQL 用 %Y 表示四位年份,而 SQL Server 的 FORMAT 用 yyyy,PostgreSQL 和 Oracle 用 YYYY。同一份 SQL 换库跑,报错不一定马上出现,但结果会悄悄偏掉。
5.1 日期转字符串的常用例子
MySQL:
sql复制SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
-- 2025-02-18 10:15:30
PostgreSQL:
sql复制SELECT TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI:SS');
-- 2025-02-18 10:15:30
SQL Server:
sql复制SELECT FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss');
-- 语法最接近 .NET 的习惯
Oracle:
sql复制SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') FROM dual;
这里要特别提醒:MySQL 里 %H 表示 00-23 的小时,%h 表示 01-12;PostgreSQL 和 Oracle 里必须写 HH24 才是 24 小时制,只写 HH 的话,默认是 12 小时制。如果你在凌晨报表里看到下午的数据被拼成了上午的字符串,不用怀疑,就是 HH 惹的祸。
5.2 字符串转日期:能用具体格式就不要让数据库猜
数据库本身有能力把常见格式的字符串自动转成日期,但依赖隐式转换的代码在边界情况下特别脆弱。建议显式指定格式。
MySQL:
sql复制SELECT STR_TO_DATE('2025-02-18 10:15:30', '%Y-%m-%d %H:%i:%s');
PostgreSQL:
sql复制SELECT TO_DATE('2025-02-18', 'YYYY-MM-DD');
SELECT TO_TIMESTAMP('2025-02-18 10:15:30', 'YYYY-MM-DD HH24:MI:SS');
SQL Server:
sql复制SELECT CONVERT(date, '2025-02-18', 120);
SELECT TRY_CONVERT(datetime2, '2025-02-18 10:15:30', 120);
Oracle:
sql复制SELECT TO_DATE('2025-02-18 10:15:30', 'YYYY-MM-DD HH24:MI:SS') FROM dual;
TRY_CONVERT 是 SQL Server 的默认容错利器,遇到无法转换的数据不会让整个查询报错,而是返回 NULL,很适合在数据清洗阶段使用。PostgreSQL 和 MySQL 则没那么宽容,数据源有脏值时,建议先用正则或者 CASE 分支把明显不合法的字符串过滤掉。
5.3 格式化字符串在统计关联里的作用
格式化最常见的用途不是给人看,而是生成统计键。我在做前端图表接口时,会直接在 SQL 里把时间转成前端想要的 YYYY-MM-DD 字符串,分组返回。但如果在后端还要做二次聚合,我更愿意返回标准日期类型而不是字符串,因为字符串的数字顺序并不总是等于日历顺序。2025-02-01 比 2025-1-1 排序正确,但手工拼接的 2025-1-1 格式则会排错,这是团队协作里容易反复出现的低级事故。
6. 日期统计分桶实战:按天、按周、按自然月、最近 N 天
有了拆分、加减、格式化这些基础,最后把它们组合起来处理“分桶统计”。分桶指的是把一条条明细数据按照时间粒度归入不同的桶里,再计个数、求和或者算均值。这是报表系统里最典型的应用场景。
6.1 按天和按自然月分组的推荐写法
按天分组,最直观的是把时间戳转换为日期。MySQL 里可以直接 DATE(created_at):
sql复制SELECT
DATE(created_at) AS stat_date,
COUNT(*) AS order_cnt
FROM orders
GROUP BY DATE(created_at)
ORDER BY stat_date;
PostgreSQL 里同样支持 created_at::date 或者 DATE(created_at)。SQL Server 里则是 CAST(created_at AS date)。如果时间字段本身只精确到日期,那直接 GROUP BY created_at 也可以。
按自然月分组时,不要用年、月两个字段去做两个桶,而应直接用月首日期或年月字符串:
sql复制SELECT
DATE_FORMAT(created_at, '%Y-%m-01') AS month_start,
SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(created_at, '%Y-%m-01')
ORDER BY month_start;
PostgreSQL 版本:
sql复制SELECT
DATE_TRUNC('month', created_at) AS month_start,
SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month_start;
为什么要用月首日期而不是年月字符串?因为后续如果要和下个月的月首日期做日期差,或者 join 一张日历维度表,日期类型比字符串类型更省事。
6.2 按周分组的两个完整思路
按周分桶是最难统一口径的地方。MySQL 中可以用 YEARWEEK,但要指定模式。想按周一分界,用模式 1:
sql复制SELECT
YEARWEEK(created_at, 1) AS year_week,
COUNT(*) AS order_cnt
FROM orders
GROUP BY YEARWEEK(created_at, 1);
但 YEARWEEK 返回的是整数,比如 202506 表示 2025 年第 6 周。这种结构适合排序,但不适合直接展示“本周起始日”。更直观的是计算本周周一日期:
sql复制SELECT
DATE_SUB(DATE(created_at), INTERVAL WEEKDAY(created_at) DAY) AS week_start,
COUNT(*) AS order_cnt
FROM orders
GROUP BY DATE_SUB(DATE(created_at), INTERVAL WEEKDAY(created_at) DAY);
PostgreSQL 里直接使用 DATE_TRUNC('week', created_at),默认就是从周一开始:
sql复制SELECT
DATE_TRUNC('week', created_at) AS week_start,
COUNT(*) AS order_cnt
FROM orders
GROUP BY DATE_TRUNC('week', created_at);
SQL Server 里不好用“我脑海里记得住的函数”一句话解决,因为 DATEPART(weekday, ...) 的结果受 @@DATEFIRST 影响。推荐 2022 版及以上使用 DATETRUNC(week, created_at),并在查询前设置 SET DATEFIRST 1。老版本里可以用:
sql复制SELECT
DATEADD(day, 1 - DATEPART(weekday, created_at), CAST(created_at AS date)) AS week_start,
COUNT(*)
FROM orders
GROUP BY DATEADD(day, 1 - DATEPART(weekday, created_at), CAST(created_at AS date));
这个公式只在 DATEFIRST = 1 时可靠。所以我在团队规范里通常建议:能靠日期维度表 join 就不要在 SQL 里手写周逻辑,月、周、季度的口径都先在一张统计日历表里定义好,查询时只需要关联,不同报表的口径自然不会打架。
6.3 最常用的“最近 N 天”范围条件
“最近 7 天”“最近 30 天”“上个月”“今年”这几类需求,最好转化成日期范围条件。
MySQL 里最近 30 天:
sql复制WHERE created_at >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
AND created_at < CURRENT_DATE + 1;
如果需求是“最近 30 个自然日,包含今天且滚动更新”,上述写法是可用的。如果需求是“最近 30 天整,不含今天”,就改成 < CURRENT_DATE 而不是 < CURRENT_DATE + 1。范围边界差一天,对业务结果的影响往往是决定性的。
PostgreSQL 里更常见的写法:
sql复制WHERE created_at >= CURRENT_DATE - 30
AND created_at < CURRENT_DATE + 1;
6.4 反例:在字段上套函数让你丢失索引
最后必须提醒一个高频问题:有的人为了按日期过滤,会在条件里对时间戳字段做函数处理,写成了这样:
sql复制-- 不推荐的写法,效率低
WHERE DATE(created_at) = CURRENT_DATE;
这个写法并不是不能用,它能跑,但在数据量大时会让索引失效。数据库必须先把每一行的 created_at 算成日期,再去比较。正确做法是把条件改成范围:
sql复制WHERE created_at >= CURRENT_DATE
AND created_at < CURRENT_DATE + INTERVAL 1 DAY;
前者可读性很好,适合小表临时查询;后者更适合线上核心表和每日跑批。如果有一天你要优化慢查询,优先检查的就是这类在索引列上包装了日期函数的问题。我曾经接手过一个订单报表,状态一直正常,就是每月盘点时查询二十秒都跑不出来。最后排查发现 WHERE 里全是 DATE(create_time) = xxx 和 MONTH(create_time) = xxx,改成日期范围后,查询从十几秒降到了几百毫秒。
另外,处理 NULL 日期字段也要养成习惯。日期函数遇到 NULL 大多返回 NULL,聚合时 COUNT(列名) 不会统计 NULL,但 COUNT(*) 会统计。要判断“某个时间字段是否为空”,直接写 created_at IS NULL 比用日期格式函数去套更清楚。
从我自己的实践经验看,日期函数的坑通常不在函数不会写,而在三件事:字段类型没确认、日期字段上套函数导致性能下降、周和月份的统计口径没和业务对齐。所以我现在写到时间条件时,会先把边界条件写在注释里,比如“开始时间含当天 00:00:00,结束时间不含次日零点”,这样即使过三个月回来看这段 SQL,也能猜出当初的意图。如果你也想把日期查询这块彻底理顺,建议把上面这些代码片段放进自己的笔记里,在本地库里逐条跑一遍,跑完后再把口径不同的几个数据库结果放在一起对比,记忆会深很多。
