SQL日期处理要点总结:
- 日期数据类型特性:
- 内部存储为数字(天数/秒数)
- 支持加减运算和比较操作
- 日期与字符串转换:
- TO_DATE()将字符串转为日期
- TO_CHAR()将日期转为字符串
- 需注意格式符(YYYY/MM/DD等)
- 日期比较注意事项:
- 避免直接比较字符串和日期
- 范围查询应使用左闭右开区间
- 注意边界值问题(BETWEEN可能丢失数据)
- 日期运算函数:
- 支持加减天数、月份计算等
- Oracle和MySQL函数存在差异
- 最佳实践:
- 使用显式类型转换
- 避免在WHERE左侧使用转换函数(影响索引)
- 注意时区问题
特别提醒:日期处理时要特别注意格式转换和边界条件,不当操作可能导致查询结果不准确或性能问题。
SQL中日期的特殊性总结
在SQL中,日期是一种特殊的数据类型,既有数值的特性,又有字符串的表现形式,使用时有诸多需要注意的地方。
一、日期数据类型的特点
| 特性 | 说明 | 示例 |
|---|---|---|
| 存储格式 | 内部存储为数字(从某个基准日期开始的天数/秒数) | Oracle: 4712-01-01 起的天数 |
| 显示格式 | 由数据库参数控制,不一定是输入时的格式 | Oracle: 17-12月-80 |
| 运算能力 | 支持加减运算(天数/月数/年数) | HIREDATE + 30(30天后) |
| 比较能力 | 支持 <, >, =, BETWEEN 等比较操作 |
HIREDATE > TO_DATE('1981-01-01') |
二、日期与字符串的转换(最重要)
核心函数
| 函数 | 方向 | 用途 |
|---|---|---|
TO_DATE(字符串, 格式) |
字符串 → 日期 | 将字符串按指定格式解析为日期类型 |
TO_CHAR(日期, 格式) |
日期 → 字符串 | 将日期按指定格式转换为字符串 |
常用日期格式元素
| 格式符 | 含义 | 示例 |
|---|---|---|
YYYY |
四位年份 | 1981 |
YY |
两位年份 | 81 |
MM |
两位月份 | 05 |
MON |
月份缩写(中文环境为'5月') | '5月' |
MONTH |
月份全称 | '5月' |
DD |
两位日期 | 01 |
DAY |
星期几 | '星期三' |
HH24 |
24小时制 | 14 |
MI |
分钟 | 30 |
SS |
秒钟 | 45 |
在SQL中,日期格式符是区分大小写的,这是一个非常重要的细节,写错了会导致转换失败或结果错误。
核心规则:格式符严格区分大小写
| 格式符 | 含义 | 正确示例 | 错误示例(大小写错误) |
|---|---|---|---|
MM |
月份(01-12) | TO_CHAR(date, 'MM') → 04 |
mm → ❌ 报错或无效 |
MI |
分钟(00-59) | TO_CHAR(date, 'MI') → 30 |
Mi / mi → ❌ |
HH24 |
24小时制(00-23) | TO_CHAR(date, 'HH24') → 14 |
hh24 → ❌ |
HH12 / HH |
12小时制(01-12) | TO_CHAR(date, 'HH12') → 02 |
hh12 → ❌ |
YYYY |
四位年份 | TO_CHAR(date, 'YYYY') → 2026 |
yyyy → ❌ |
YY |
两位年份 | TO_CHAR(date, 'YY') → 26 |
yy → ❌ |
MON |
月份缩写(如'4月') | TO_CHAR(date, 'MON') → 4月 |
Mon / mon → ❌ |
MONTH |
月份全称(如'4月') | TO_CHAR(date, 'MONTH') → 4月 |
Month → ❌ |
DD |
日期(01-31) | TO_CHAR(date, 'DD') → 23 |
dd → ❌ |
DY |
星期缩写(如'周三') | TO_CHAR(date, 'DY') → 周三 |
dy → ❌ |
DAY |
星期全称(如'星期三') | TO_CHAR(date, 'DAY') → 星期三 |
Day → ❌ |
示例代码
sql
-- TO_DATE:字符串转日期
TO_DATE('1981-05-01', 'YYYY-MM-DD') -- 返回日期:1981年5月1日
TO_DATE('19810501', 'YYYYMMDD') -- 返回日期:1981年5月1日
TO_DATE('1981-05', 'YYYY-MM') -- 返回日期:1981年5月1日(默认当月1号)
-- TO_CHAR:日期转字符串
TO_CHAR(HIREDATE, 'YYYY-MM-DD') -- '1981-05-01'
TO_CHAR(HIREDATE, 'YYYYMM') -- '198105'
TO_CHAR(HIREDATE, 'MON DD, YYYY') -- '5月 01, 1981'
三、日期比较的特殊性
1. 不能直接用字符串比较日期
sql
-- ❌ 错误:字符串 '1981' 和日期类型不能直接比较
SELECT * FROM EMP WHERE HIREDATE = '1981';
-- ✅ 正确方式1:转换日期为字符串比较
SELECT * FROM EMP WHERE TO_CHAR(HIREDATE, 'YYYY') = '1981';
-- ✅ 正确方式2:字符串转日期比较
SELECT * FROM EMP WHERE HIREDATE >= TO_DATE('1981-01-01', 'YYYY-MM-DD')
AND HIREDATE < TO_DATE('1982-01-01', 'YYYY-MM-DD');
2. 日期比较的边界问题(重要⚠️)
sql
-- 查询1981年入职的员工(错误写法)
SELECT * FROM EMP
WHERE TO_CHAR(HIREDATE, 'YYYY') = 1981; -- ✅ 可行,但效率低
-- 查询1981年入职的员工(正确写法 - 使用范围)
SELECT * FROM EMP
WHERE HIREDATE >= TO_DATE('1981-01-01', 'YYYY-MM-DD')
AND HIREDATE < TO_DATE('1982-01-01', 'YYYY-MM-DD');
-- 查询1981年5月入职(错误写法)
WHERE HIREDATE BETWEEN TO_DATE('1981-05-01', 'YYYY-MM-DD')
AND TO_DATE('1981-05-31', 'YYYY-MM-DD'); -- ⚠️ 漏掉了5月31日23:59:59之后的数据
-- 查询1981年5月入职(正确写法)
WHERE HIREDATE >= TO_DATE('1981-05-01', 'YYYY-MM-DD')
AND HIREDATE < TO_DATE('1981-06-01', 'YYYY-MM-DD');
边界写法参考
| 需求 | TO_CHAR 写法 | TO_DATE 范围写法 |
|---|---|---|
| 年份 = 1981 | TO_CHAR(HIREDATE,'YYYY') = 1981 |
HIREDATE >= TO_DATE('1981-01-01','YYYY-MM-DD') AND HIREDATE < TO_DATE('1982-01-01','YYYY-MM-DD') |
| 年份 < 1982 | TO_CHAR(HIREDATE,'YYYY') < 1982 |
HIREDATE < TO_DATE('1982-01-01','YYYY-MM-DD') |
| 年份 <= 1982 | TO_CHAR(HIREDATE,'YYYY') <= 1982 |
HIREDATE < TO_DATE('1983-01-01','YYYY-MM-DD') |
| 年份 > 1981 | TO_CHAR(HIREDATE,'YYYY') > 1981 |
HIREDATE >= TO_DATE('1982-01-01','YYYY-MM-DD') |
| 年份 >= 1982 | TO_CHAR(HIREDATE,'YYYY') >= 1982 |
HIREDATE >= TO_DATE('1982-01-01','YYYY-MM-DD') |
四、日期的加减运算
| 运算 | 含义 | 示例 |
|---|---|---|
日期 + 数字 |
增加天数 | HIREDATE + 30(30天后) |
日期 - 数字 |
减少天数 | HIREDATE - 7(7天前) |
日期1 - 日期2 |
相差天数 | SYSDATE - HIREDATE(入职天数) |
ADD_MONTHS(日期, 数字) |
增加月份 | ADD_MONTHS(HIREDATE, 6)(6个月后) |
MONTHS_BETWEEN(日期1, 日期2) |
相差月数 | MONTHS_BETWEEN(SYSDATE, HIREDATE) |
示例代码
sql
-- 计算员工入职天数
SELECT ENAME, SYSDATE - HIREDATE AS 工作天数 FROM EMP;
-- 计算员工入职月数
SELECT ENAME, MONTHS_BETWEEN(SYSDATE, HIREDATE) AS 工作月数 FROM EMP;
-- 查询入职超过30年的员工
SELECT * FROM EMP
WHERE ADD_MONTHS(HIREDATE, 30*12) < SYSDATE;
五、日期函数对比(Oracle vs MySQL)
| 功能 | Oracle | MySQL |
|---|---|---|
| 当前日期时间 | SYSDATE |
NOW() / CURDATE() |
| 提取年份 | TO_CHAR(date, 'YYYY') |
YEAR(date) |
| 提取月份 | TO_CHAR(date, 'MM') |
MONTH(date) |
| 日期加减天数 | date + 10 |
DATE_ADD(date, INTERVAL 10 DAY) |
| 日期差(天数) | date1 - date2 |
DATEDIFF(date1, date2) |
| 增加月份 | ADD_MONTHS(date, 6) |
DATE_ADD(date, INTERVAL 6 MONTH) |
六、常见陷阱与最佳实践
❌ 常见错误
sql
-- 1. 直接比较字符串和日期
WHERE HIREDATE = '1981-05-01' -- 隐式转换可能失败
-- 2. 使用 BETWEEN 包含结束日期(会丢失当天23:59:59后的数据)
WHERE HIREDATE BETWEEN '1981-05-01' AND '1981-05-31'
-- 3. TO_CHAR 写在 WHERE 条件的左边(无法使用索引)
WHERE TO_CHAR(HIREDATE, 'YYYY') = '1981'
-- 4. 忽略时区问题
WHERE CREATE_TIME = '2026-04-23' -- 可能漏掉带时分秒的记录
✅ 最佳实践
sql
-- 1. 始终使用显式转换
WHERE HIREDATE >= TO_DATE('1981-05-01', 'YYYY-MM-DD')
AND HIREDATE < TO_DATE('1981-06-01', 'YYYY-MM-DD')
-- 2. 范围查询使用左闭右开区间
WHERE HIREDATE >= TRUNC(SYSDATE - 30) -- 30天前零点
AND HIREDATE < TRUNC(SYSDATE) -- 今天零点
-- 3. 让函数作用在常量上,保持索引有效
WHERE HIREDATE >= TO_DATE('1981-01-01', 'YYYY-MM-DD') -- ✅ 索引有效
WHERE TO_CHAR(HIREDATE, 'YYYY') = '1981' -- ❌ 索引失效
-- 4. 使用 TRUNC 去掉时间部分
WHERE TRUNC(HIREDATE) = TO_DATE('1981-05-01', 'YYYY-MM-DD')
使用 TRUNC 去掉时间部分是什么意思
TRUNC是一个用于截断 日期或数字的函数。在日期处理中,"去掉时间部分"是指将日期中的时、分、秒清零,只保留年、月、日。
为什么需要"去掉时间部分"?
问题场景:日期比较的陷阱
sql-- 假设表中有一条记录,HIREDATE = 1981-05-01 14:30:00 -- ❌ 错误:这条记录会被漏掉! SELECT * FROM EMP WHERE HIREDATE = TO_DATE('1981-05-01', 'YYYY-MM-DD'); -- 因为左边有 14:30:00,右边是 00:00:00,不相等 -- ✅ 解法1:使用 TRUNC 去掉时间部分 SELECT * FROM EMP WHERE TRUNC(HIREDATE) = TO_DATE('1981-05-01', 'YYYY-MM-DD'); -- ✅ 解法2:使用范围查询(更推荐,索引友好) SELECT * FROM EMP WHERE HIREDATE >= TO_DATE('1981-05-01', 'YYYY-MM-DD') AND HIREDATE < TO_DATE('1981-05-02', 'YYYY-MM-DD');
TRUNC 的常用格式
用法 结果 说明 TRUNC(SYSDATE)2026-04-24 00:00:00 截断到当天开始(默认) TRUNC(SYSDATE, 'DD')2026-04-24 00:00:00 同上,DD表示天 TRUNC(SYSDATE, 'MM')2026-04-01 00:00:00 截断到当月第一天 TRUNC(SYSDATE, 'Q')2026-04-01 00:00:00 截断到当季第一天 TRUNC(SYSDATE, 'YYYY')2026-01-01 00:00:00 截断到当年第一天 TRUNC(SYSDATE, 'HH24')2026-04-24 14:00:00 截断到当前小时开始
对比总结
| 函数 | 作用 | 示例输入 | 示例输出 |
|---|---|---|---|
TRUNC(date) |
去掉时间部分(归零) | 2026-04-24 14:35:28 | 2026-04-24 00:00:00 |
TO_CHAR(date, 'YYYY-MM-DD') |
转为字符串(丢失时间) | 2026-04-24 14:35:28 | '2026-04-24' |
ROUND(date) |
四舍五入到天 | 2026-04-24 14:35:28 | 2026-04-25 00:00:00 |
TRUNC 会让索引失效(类似 TO_CHAR)
sql
-- ❌ 索引失效
WHERE TRUNC(HIREDATE) = TO_DATE('1981-05-01', 'YYYY-MM-DD')
-- ✅ 推荐:范围查询(索引有效)
WHERE HIREDATE >= TO_DATE('1981-05-01', 'YYYY-MM-DD')
AND HIREDATE < TO_DATE('1981-05-02', 'YYYY-MM-DD')
原则 :能不用 TRUNC 在 WHERE 条件中就不用,除非数据量很小或没有时间精度要求。
TRUNC 常用于 GROUP BY 分组
sql
-- 按天统计(即使数据库存了时分秒)
SELECT TRUNC(HIREDATE) AS 入职日期, COUNT(*) AS 人数
FROM EMP
GROUP BY TRUNC(HIREDATE);
一句话总结
TRUNC去掉时间部分 = 把14:35:28变成00:00:00用于忽略时分秒的干扰 ,让日期比较只看年月日。
但在
WHERE中要小心使用,因为它和TO_CHAR一样会让索引失效,大数据量时建议用范围查询替代。
七、快速参考卡片
| 需求 | SQL写法 |
|---|---|
| 当前系统日期 | SYSDATE(Oracle)/ CURDATE()(MySQL) |
| 年月日格式 | 'YYYY-MM-DD' |
| 字符串→日期 | TO_DATE('1981-05-01', 'YYYY-MM-DD') |
| 日期→字符串 | TO_CHAR(HIREDATE, 'YYYY-MM-DD') |
| 提取年份 | TO_CHAR(HIREDATE, 'YYYY') |
| 提取年月 | TO_CHAR(HIREDATE, 'YYYYMM') |
| 某月第一天 | TRUNC(HIREDATE, 'MM') |
| 某年第一天 | TRUNC(HIREDATE, 'YYYY') |
| 月底最后一天 | LAST_DAY(HIREDATE) |
| 下个月同一天 | ADD_MONTHS(HIREDATE, 1) |
八、你在作业中的日期问题总结
sql
-- 第3题 ✅ 正确
WHERE TO_CHAR(HIREDATE, 'YYYYMM') < 198210
-- 第5题 ⚠️ 缺少括号(结果正确但不规范)
WHERE DEPTNO=20 AND TO_CHAR(HIREDATE,'YYYY')<1982
OR DEPTNO=30 AND TO_CHAR(HIREDATE,'YYYY')<1985
-- 第11题 ❌ 完全遗漏WHERE条件
-- 应该加:WHERE TO_CHAR(HIREDATE, 'YYYY') > 1981
核心要点:
-
日期比较时,优先使用范围查询(左闭右开)
-
TO_CHAR会让索引失效,大数据量时慎用 -
始终用 显式转型,不要依赖隐式转换
-
注意边界值,BETWEEN 可能丢失最后一天的末尾时间
TO_CHAR 会让索引失效,大数据量时慎用。是什么意思?
索引就像一本书的"目录",它能帮你快速翻到需要的页码。但如果对"目录"里的文字做了修改(比如加了格式),那原来的目录就失效了,你只能一页一页地翻完整本书来找内容。
1. 为什么 TO_CHAR 会让索引失效?
SQL的执行顺序决定了索引的生效机制。
当你执行 WHERE TO_CHAR(HIREDATE, 'YYYY') = '1981' 时,数据库的处理过程是这样的:
-
读取一条数据:数据库从硬盘或内存中取出第一行员工的数据。
-
执行函数 :对这行数据的
HIREDATE列执行TO_CHAR函数,把日期类型(如1981-05-01的内部存储值)转换成字符串类型(如'1981')。 -
条件比对 :判断这个转换后的字符串
'1981'是否等于你指定的'1981'。 -
重复 :对表中的每一行重复第1到第3步。
索引为什么没起作用?
因为索引里存的是原始的、未经过任何处理的 HIREDATE 值,而你在查询时使用的是 TO_CHAR(HIREDATE) 这个函数的返回值。数据库没法用原始的日期值去匹配一个函数的返回值,所以只能放弃索引,从头到尾把整张表的数据都处理一遍。
这正是你在笔记中看到的高级查询优化问题。
2. 另一种写法:为什么索引能生效?
如果你换一种写法,比如 WHERE HIREDATE >= TO_DATE('1981-01-01', 'YYYY-MM-DD'),过程完全不同:
-
函数执行一次 :数据库首先执行
TO_DATE('1981-01-01', 'YYYY-MM-DD'),把字符串'1981-01-01'转换成一个日期值(比如1981-01-01的内部存储数字)。 -
索引快速定位 :数据库拿着这个日期值,直接去索引(书的目录)里查找。
-
直接找到数据:通过索引快速定位到满足条件的数据在磁盘上的物理位置,然后直接读取。
索引生效的关键在于:
列本身 (HIREDATE)没有被任何函数、计算所改变,数据库可以直接用你在 WHERE 里给的值去跟索引里的值做比对。
3. "大数据量时慎用"是什么意思?
| 数据量 | 影响程度 | 说明 |
|---|---|---|
| 小数据量 (几十、几百条) | 影响极微 | 即便没有索引,逐行扫描也快如闪电,用户完全感受不到差异。 |
| 中等数据量 (几万、几十万条) | 影响显著 | 逐行扫描开始变慢,可能需要几秒甚至更久,用户能明显感觉到"卡"。 |
| 大数据量 (百万、千万条以上) | 灾难性影响 | 逐行扫描会让查询耗时从毫秒级 (有索引)变成分钟甚至小时级(无索引,全表扫描) |
举个生活化的例子:
-
小数据量:在一个五六个座位的家庭餐桌上找一个人,扫一眼就行,不需要名片索引。
-
大数据量:在鸟巢(容纳9万人)里找一个座位号是 '1981' 的人。如果不用座位索引,每个座位都去核对,会累到崩溃。
4. 优化建议与最佳实践
核心原则 :永远不要让函数去修饰被筛选的列本身。
| 要避免的写法 (❌ 索引失效) | 推荐写法 (✅ 索引生效) | 说明 |
|---|---|---|
WHERE TO_CHAR(HIREDATE, 'YYYY') = '1981' |
WHERE HIREDATE >= TO_DATE('1981-01-01', 'YYYY-MM-DD') AND HIREDATE < TO_DATE('1982-01-01', 'YYYY-MM-DD') |
用范围查询代替函数 |
WHERE TO_CHAR(HIREDATE, 'YYYYMM') = '198105' |
WHERE HIREDATE >= TO_DATE('1981-05-01', 'YYYY-MM-DD') AND HIREDATE < TO_DATE('1981-06-01', 'YYYY-MM-DD') |
同上 |
WHERE SAL * 12 > 50000 |
WHERE SAL > 50000 / 12 |
让函数作用在常量上 |
WHERE SUBSTR(ENAME, 1, 1) = 'S' |
WHERE ENAME LIKE 'S%' |
使用 LIKE 前缀匹配(也能利用索引) |
总结一句话:
写 WHERE 条件时,让"列"自己待着,别碰它。如果需要处理,去处理等号右边的"值"。 这样你的查询才能在大数据量下保持高效。
你之前写的 WHERE TO_CHAR(HIREDATE, 'YYYY') = 1981 在小数据量练习时完全没问题,但在真正的企业生产环境中(数据量可能上千万),这种写法几乎是被明令禁止的。
初学阶段掌握规则即可,但养成好习惯会很有帮助。