一、TO_DATE函数到底是什么?
简单来说,TO_DATE函数的核心使命只有一个:将字符串转换为Oracle的DATE数据类型。
Oracle内部存储DATE数据时,使用的是一种内部格式,这意味着当你拿到一个字符串形式的日期,比如 '2024-12-11' 或者 'March 17, 2023',你必须通过TO_DATE函数将其转换为DATE类型,才能正确地存储到DATE列中,或者进行日期运算。
1.1 基本语法
TO_DATE(string, format_mask, [nls_language])
| 参数 | 说明 | 是否必填 |
|---|---|---|
| string | 要转换的日期字符串 | ✅ 必填 |
| format_mask | 指定字符串的日期格式 | ✅ 必填 |
| nls_language | 可选,指定语言环境 | ❌ 可选 |
重点来了:format_mask参数是整个TO_DATE函数的灵魂。 它告诉Oracle,你给的这个字符串到底长什么样。如果format_mask写错了,轻则返回NULL,重则直接报错。
二、format_mask格式掩码大全
这是本文最核心的部分,我把所有常用的格式掩码整理成表格,建议收藏。
2.1 年份相关
| 格式符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| YYYY | 四位年份 | 2024 | 2024 |
| YYY | 三位年份 | 024 | 024 |
| YY | 两位年份 | 24 | 24 |
| Y | 一位年份 | 4 | 4 |
| RRRR | 两位年份转四位(00-49→20xx,50-99→19xx) | 50 | 1950 |
| IYYY | ISO标准四位年份 | 2024 | 2024 |
特别注意RRRR的妙用: 当你只有两位年份时,使用RRRR可以智能判断世纪。比如输入'50',返回1950;输入'24',返回2024。这在处理老旧系统数据时非常实用。
2.2 月份相关
| 格式符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| MM | 两位数字月份(01-12) | 12 | 12月 |
| MON | 三位英文月份缩写 | DEC | DEC |
| MONTH | 完整月份名称(空格填充至9字符) | DECEMBER | DECEMBER |
| RM | 罗马数字月份(I-XII) | XII | XII |
踩坑预警: 如果你的字符串里写的是"December"而不是"DEC",那你必须用MONTH而不是MON。这是新手最常犯的错误之一。
2.3 日期相关
| 格式符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| DD | 当月第几天(01-31) | 15 | 15日 |
| DDD | 当年第几天(001-366) | 365 | 第365天 |
| DY | 星期缩写 | MON | MON |
| DAY | 星期全名(空格填充至9字符) | MONDAY | MONDAY |
| D | 星期几(1=周日,7=周六) | 2 | 周一 |
2.4 时间相关(重中之重)
| 格式符 | 含义 | 示例 | 结果 |
|---|---|---|---|
| HH 或 HH12 | 12小时制小时(01-12) | 02 | 02 |
| HH24 | 24小时制小时(00-23) | 14 | 14 |
| MI | 分钟(00-59) | 30 | 30分 |
| SS | 秒(00-59) | 45 | 45秒 |
| FF | 十亿分之一秒(纳秒级) | 123456789 | 纳秒 |
| AM / PM | 上午/下午标识 | PM | 下午 |
🚨 致命错误警告:分钟必须用MI,不能用MM!
这是我见过最多的错误。在Java里我们习惯写mm表示分钟,但在Oracle SQL中,MM是月份!如果你写成:
-- ❌ 错误写法,会报 ORA-01810 格式代码出现两次
SELECT TO_DATE('2024-12-11 14:30:45', 'YYYY-MM-DD HH24:mm:ss') FROM dual;
正确写法是:
-- ✅ 正确写法
SELECT TO_DATE('2024-12-11 14:30:45', 'YYYY-MM-DD HH24:MI:SS') FROM dual;
记住:Oracle里分钟是MI,不是MM!
三、实战案例:从入门到精通
3.1 基础转换——最常见的场景
-- 将 '2024-12-11' 转换为 DATE 类型
SELECT TO_DATE('2024-12-11', 'YYYY-MM-DD') FROM dual;
-- 结果:11-DEC-24(Oracle默认显示格式)
-- 带时间的转换
SELECT TO_DATE('2024-12-11 14:30:45', 'YYYY-MM-DD HH24:MI:SS') FROM dual;
-- 结果:11-DEC-24 下午 02:30:45
3.2 插入数据时使用TO_DATE
-- 假设有一张用户表 users(id, name, joined_date)
INSERT INTO users (id, name, joined_date)
VALUES (1, '张三', TO_DATE('2024-12-11', 'YYYY-MM-DD'));
-- 带时间的插入
INSERT INTO users (id, name, joined_date)
VALUES (2, '李四', TO_DATE('2024-12-11 09:30:00', 'YYYY-MM-DD HH24:MI:SS'));
3.3 处理不同语言的月份名称
当你的日期字符串中包含非英文月份名称时,需要使用第三个参数nls_language:
-- 法语月份 "Mai" = 五月
SELECT TO_DATE('05-Mai-2025', 'DD-Mon-YYYY', 'NLS_DATE_LANGUAGE=FRENCH') FROM dual;
-- 结果:05-MAY-25
-- 中文环境下
SELECT TO_DATE('2024年12月11日', 'YYYY"年"MM"月"DD"日"', 'NLS_DATE_LANGUAGE=SIMPLIFIED CHINESE') FROM dual;
你也可以在会话级别设置语言:
ALTER SESSION SET NLS_DATE_LANGUAGE = 'AMERICAN';
SELECT TO_CHAR(TO_DATE('2002-08-26', 'YYYY-MM-DD'), 'DAY') FROM dual;
-- 结果:MONDAY(而不是中文的"星期一")
3.4 求某天是星期几
-- 中文环境
SELECT TO_CHAR(TO_DATE('2024-12-11', 'YYYY-MM-DD'), 'DAY', 'NLS_DATE_LANGUAGE=SIMPLIFIED CHINESE') FROM dual;
-- 结果:星期三
-- 英文环境
SELECT TO_CHAR(TO_DATE('2024-12-11', 'YYYY-MM-DD'), 'DAY', 'NLS_DATE_LANGUAGE=AMERICAN') FROM dual;
-- 结果:WEDNESDAY
3.5 计算两个日期之间的天数
-- 当前日期减去指定日期
SELECT FLOOR(SYSDATE - TO_DATE('2024-01-01', 'YYYY-MM-DD')) AS days_diff FROM dual;
-- 两个指定日期之间的天数
SELECT TO_DATE('2024-12-31', 'YYYY-MM-DD') - TO_DATE('2024-01-01', 'YYYY-MM-DD') AS days_diff FROM dual;
-- 结果:365
3.6 计算月份差
-- 使用 MONTHS_BETWEEN 函数
SELECT MONTHS_BETWEEN(TO_DATE('2024-12-31', 'YYYY-MM-DD'),
TO_DATE('2024-01-01', 'YYYY-MM-DD')) AS months_diff
FROM dual;
-- 结果:12
-- 非整月的情况
SELECT MONTHS_BETWEEN(TO_DATE('2024-02-01', 'YYYY-MM-DD'),
TO_DATE('2024-01-01', 'YYYY-MM-DD')) AS months_diff
FROM dual;
-- 结果:1.03225806451613(约1.03个月)
3.7 查询某月内排除周末的天数
这是一个非常实用的业务场景——计算工作日天数:
SELECT COUNT(*) AS workdays
FROM (
SELECT ROWNUM - 1 AS rnum
FROM all_objects
WHERE ROWNUM <= TO_DATE('2024-02-28', 'YYYY-MM-DD') - TO_DATE('2024-02-01', 'YYYY-MM-DD') + 1
)
WHERE TO_CHAR(TO_DATE('2024-02-01', 'YYYY-MM-DD') + rnum - 1, 'D') NOT IN ('1', '7');
-- 结果:20(2024年2月1日到28日之间,排除周六周日的工作日天数)
3.8 处理NULL值
-- 查询时需要用 TO_DATE(NULL) 而不是 NULL
SELECT id, active_date FROM table1
UNION
SELECT 1, TO_DATE(NULL) FROM dual;
如果直接写NULL,Oracle可能会因为类型不匹配而报错。
四、TO_DATE与TO_CHAR的黄金搭档
在实际开发中,TO_DATE和TO_CHAR是一对形影不离的好搭档。一个负责"字符→日期",一个负责"日期→字符"。
| 操作 | 函数 | 示例 |
|---|---|---|
| 字符串转日期 | TO_DATE | TO_DATE('2024-12-11', 'YYYY-MM-DD') |
| 日期转字符串 | TO_CHAR | TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') |
| 提取年份 | TO_CHAR | TO_CHAR(SYSDATE, 'YYYY') |
| 提取月份 | TO_CHAR | TO_CHAR(SYSDATE, 'MM') |
| 提取日期 | TO_CHAR | TO_CHAR(SYSDATE, 'DD') |
| 提取小时 | TO_CHAR | TO_CHAR(SYSDATE, 'HH24') |
| 提取分钟 | TO_CHAR | TO_CHAR(SYSDATE, 'MI') |
| 提取秒 | TO_CHAR | TO_CHAR(SYSDATE, 'SS') |
实战:自定义格式化输出
SELECT TO_CHAR(SYSDATE, '"今天是" DD"日" MONTH", "YYYY"年"') AS custom_date FROM dual;
-- 结果:今天是 22日 MAY , 2026年
SELECT TO_CHAR(SYSDATE, 'YYYY"年"MM"月"DD"日" HH24"时"MI"分"SS"秒"') FROM dual;
-- 结果:2026年05月22日 15时30分45秒
五、常见坑点与最佳实践
作为一名老开发,我把这些年踩过的坑全部总结给你:
坑点1:格式掩码大小写不敏感,但必须匹配
Oracle的格式掩码不区分大小写,但你的字符串必须和掩码完全对应。比如掩码写的是MONTH,字符串就必须是完整的月份名;写的是MON,字符串就必须是三字母缩写。
坑点2:默认日期格式的陷阱
如果你不指定format_mask,Oracle会使用NLS_DATE_FORMAT参数的默认值,通常是DD-MON-RR。这意味着:
-- 以下写法依赖默认格式,不推荐!
INSERT INTO x VALUES (99, '31-may-98');
-- 推荐写法,显式指定格式
INSERT INTO x VALUES (99, TO_DATE('1998/05/31:12:00:00AM', 'yyyy/mm/dd:hh:mi:ssam'));
最佳实践:永远显式指定format_mask,不要依赖隐式转换!
坑点3:AM/PM与HH12/HH24的搭配
-- ✅ 正确:12小时制 + AM/PM
TO_DATE('02:30:00 PM', 'HH:MI:SS AM')
-- ✅ 正确:24小时制,不需要AM/PM
TO_DATE('14:30:00', 'HH24:MI:SS')
-- ❌ 错误:24小时制配了AM/PM,或者12小时制没配AM/PM
TO_DATE('14:30:00 PM', 'HH24:MI:SS AM') -- 逻辑矛盾!
坑点4:日期比较时的精度问题
当使用BETWEEN进行日期范围查询时,如果不包含时间部分,边界值可能会被意外排除:
-- 假设 joined_date 是 DATE 类型,包含时间
-- 以下查询可能不包含 12月31日 12:00:00 之后的记录
SELECT * FROM users
WHERE joined_date BETWEEN TO_DATE('2024-12-01', 'YYYY-MM-DD')
AND TO_DATE('2024-12-31', 'YYYY-MM-DD');
-- 更精确的写法
SELECT * FROM users
WHERE joined_date >= TO_DATE('2024-12-01', 'YYYY-MM-DD')
AND joined_date < TO_DATE('2025-01-01', 'YYYY-MM-DD');
坑点5:查询NLS参数
当遇到日期格式莫名其妙对不上时,先查NLS参数:
SELECT * FROM NLS_SESSION_PARAMETERS WHERE PARAMETER LIKE '%DATE%';
SELECT * FROM V$NLS_PARAMETERS WHERE PARAMETER LIKE '%DATE%';
六、高级用法:日期算术与相关函数
6.1 日期加减运算
Oracle中日期可以直接加减天数:
-- 当前时间加10天
SELECT SYSDATE + 10 AS future_date FROM dual;
-- 当前时间减5天
SELECT SYSDATE - 5 AS past_date FROM dual;
-- 加3个月
SELECT ADD_MONTHS(SYSDATE, 3) FROM dual;
-- 减7个月
SELECT ADD_MONTHS(SYSDATE, -7) FROM dual;
6.2 INTERVAL间隔运算
-- 当前时间减7分钟
SELECT SYSDATE, SYSDATE - INTERVAL '7' MINUTE FROM dual;
-- 当前时间减7小时
SELECT SYSDATE - INTERVAL '7' HOUR FROM dual;
-- 当前时间减7天
SELECT SYSDATE - INTERVAL '7' DAY FROM dual;
-- 当前时间减7月
SELECT SYSDATE - INTERVAL '7' MONTH FROM dual;
6.3 LAST_DAY与NEXT_DAY
-- 获取本月最后一天
SELECT LAST_DAY(SYSDATE) FROM dual;
-- 结果:2026-05-31
-- 获取下一个周五
SELECT NEXT_DAY(SYSDATE, 'FRIDAY') FROM dual;
-- 或使用中文
SELECT NEXT_DAY(SYSDATE, '星期五') FROM dual;
6.4 TRUNC截断函数
-- 截断到月份第一天
SELECT TRUNC(SYSDATE, 'MM') FROM dual;
-- 结果:2026-05-01
-- 截断到年份第一天
SELECT TRUNC(SYSDATE, 'YYYY') FROM dual;
-- 结果:2026-01-01
-- 截断到当天(去掉时分秒)
SELECT TRUNC(SYSDATE) FROM dual;
6.5 EXTRACT提取函数
SELECT EXTRACT(YEAR FROM SYSDATE) AS current_year,
EXTRACT(MONTH FROM SYSDATE) AS current_month,
EXTRACT(DAY FROM SYSDATE) AS current_day
FROM dual;
-- 结果:2026 | 5 | 22
七、在天翼云Oracle环境中的实践建议
在天翼云上部署Oracle数据库时,我有以下几条实战建议:
第一,统一日期格式标准。 建议在应用初始化时执行:
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS';
ALTER SESSION SET NLS_DATE_LANGUAGE = 'SIMPLIFIED CHINESE';
这样可以避免因字符集不同导致的日期解析异常。
第二,所有日期转换必须显式使用TO_DATE。 不要图省事依赖隐式转换,尤其是在多人协作的项目中,隐式转换是Bug的温床。
第三,时间字段一律使用TIMESTAMP而非DATE。 如果你的业务需要毫秒级精度,Oracle提供了TIMESTAMP类型:
SELECT TO_CHAR(CURRENT_TIMESTAMP(3), 'YYYY-MM-DD HH24:MI:SS.FF3') FROM dual;
-- 结果:2026-05-22 15:30:45.123
第四,善用绑定变量。 在应用程序中,不要把日期拼接成SQL字符串,而是使用绑定变量配合TO_DATE:
-- ✅ 推荐
SELECT * FROM orders WHERE order_date = TO_DATE(:date_str, 'YYYY-MM-DD');
-- ❌ 不推荐(有SQL注入风险)
SELECT * FROM orders WHERE order_date = '''' || :date_str || '''';
八、总结
TO_DATE函数看似简单,实则是Oracle日期处理的基石。掌握它,你就掌握了Oracle日期操作的半壁江山。
最后再帮你总结一张速查表:
| 需求 | 写法 |
|---|---|
| 字符串转日期 | TO_DATE('2024-12-11', 'YYYY-MM-DD') |
| 带时间转换 | TO_DATE('2024-12-11 14:30:45', 'YYYY-MM-DD HH24:MI:SS') |
| 12小时制 | TO_DATE('02:30:00 PM', 'HH:MI:SS AM') |
| 法语月份 | TO_DATE('05-Mai-2025', 'DD-Mon-YYYY', 'NLS_DATE_LANGUAGE=FRENCH') |
| 求星期几 | TO_CHAR(TO_DATE('2024-12-11','YYYY-MM-DD'), 'DAY') |
| 两日期天数差 | SYSDATE - TO_DATE('2024-01-01','YYYY-MM-DD') |
| 月份差 | MONTHS_BETWEEN(date1, date2) |