SQL 中日期的特殊性总结(格式符严格要求全大写)

SQL日期处理要点总结:

  1. 日期数据类型特性:
  • 内部存储为数字(天数/秒数)
  • 支持加减运算和比较操作
  1. 日期与字符串转换:
  • TO_DATE()将字符串转为日期
  • TO_CHAR()将日期转为字符串
  • 需注意格式符(YYYY/MM/DD等)
  1. 日期比较注意事项:
  • 避免直接比较字符串和日期
  • 范围查询应使用左闭右开区间
  • 注意边界值问题(BETWEEN可能丢失数据)
  1. 日期运算函数:
  • 支持加减天数、月份计算等
  • Oracle和MySQL函数存在差异
  1. 最佳实践:
  • 使用显式类型转换
  • 避免在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')

原则 :能不用 TRUNCWHERE 条件中就不用,除非数据量很小或没有时间精度要求。

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

核心要点

  1. 日期比较时,优先使用范围查询(左闭右开)

  2. TO_CHAR让索引失效,大数据量时慎用

  3. 始终用 显式转型,不要依赖隐式转换

  4. 注意边界值,BETWEEN 可能丢失最后一天的末尾时间


TO_CHAR 会让索引失效,大数据量时慎用。是什么意思?


索引就像一本书的"目录",它能帮你快速翻到需要的页码。但如果对"目录"里的文字做了修改(比如加了格式),那原来的目录就失效了,你只能一页一页地翻完整本书来找内容。


1. 为什么 TO_CHAR 会让索引失效?

SQL的执行顺序决定了索引的生效机制。

当你执行 WHERE TO_CHAR(HIREDATE, 'YYYY') = '1981' 时,数据库的处理过程是这样的:

  1. 读取一条数据:数据库从硬盘或内存中取出第一行员工的数据。

  2. 执行函数 :对这行数据的 HIREDATE 列执行 TO_CHAR 函数,把日期类型(如 1981-05-01 的内部存储值)转换成字符串类型(如 '1981')。

  3. 条件比对 :判断这个转换后的字符串 '1981' 是否等于你指定的 '1981'

  4. 重复 :对表中的每一行重复第1到第3步。


索引为什么没起作用?

因为索引里存的是原始的、未经过任何处理的 HIREDATE 值,而你在查询时使用的是 TO_CHAR(HIREDATE) 这个函数的返回值。数据库没法用原始的日期值去匹配一个函数的返回值,所以只能放弃索引,从头到尾把整张表的数据都处理一遍。


这正是你在笔记中看到的高级查询优化问题。


2. 另一种写法:为什么索引能生效?

如果你换一种写法,比如 WHERE HIREDATE >= TO_DATE('1981-01-01', 'YYYY-MM-DD'),过程完全不同:

  1. 函数执行一次 :数据库首先执行 TO_DATE('1981-01-01', 'YYYY-MM-DD'),把字符串 '1981-01-01' 转换成一个日期值(比如 1981-01-01 的内部存储数字)。

  2. 索引快速定位 :数据库拿着这个日期值,直接去索引(书的目录)里查找。

  3. 直接找到数据:通过索引快速定位到满足条件的数据在磁盘上的物理位置,然后直接读取。

索引生效的关键在于:
列本身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 在小数据量练习时完全没问题,但在真正的企业生产环境中(数据量可能上千万),这种写法几乎是被明令禁止的。


初学阶段掌握规则即可,但养成好习惯会很有帮助。

相关推荐
花椒技术3 小时前
从区间锁到行锁:一次高并发写入死锁治理实战
后端·sql
你觉得脆皮鸡好吃吗9 小时前
SQL注入 高权限注入(引入概念)
网络·数据库·sql·oracle·网络安全学习
untE EADO9 小时前
MySQL错误-this is incompatible with sql_mode=only_full_group_by完美解决方案
android·sql·mysql
Irene19919 小时前
(课堂笔记)SQL基础语法分类总结表
数据库·sql
你觉得脆皮鸡好吃吗10 小时前
SQL注入 手工注入
网络·数据库·sql·安全·web安全·网络安全学习
m0_7375393710 小时前
SQL语言续2
数据库·sql
在屏幕前出油11 小时前
08. ORM——快速开始
数据库·后端·python·sql·pycharm·orm
lzhdim11 小时前
SQL 入门 11:日期时间格式化、IF、CASE的使用
数据库·sql
上海云盾-小余12 小时前
Web 业务常见 SQL 注入攻击原理详解及 WAF 防护部署实战教程
前端·数据库·sql