我们的文章会在微信公众号IT民工的龙马人生和博客网站( www.htz.pw )同步更新 ,欢迎关注收藏,也欢迎大家转载,但是请在文章开始地方标注文章出处,谢谢!
由于博客中有大量代码,通过页面浏览效果更佳。
本文为个人学习《Expert Oracle Database Architecture Techniques and Solutions for High Performance and Productivity(第四版本》一书过程中的笔记与理解分享,仅用于学习与交流,部分内容参考原书观点并结合>实际经验进行整理。若涉及版权问题,请联系删除或沟通处理。也请大家支持购买原版书籍。
深入理解 Oracle 的 DATE 类型:存储、计算与最佳实践
Oracle 数据库中的 DATE 类型是一种功能强大且高效的日期时间数据类型。它不仅用于存储日期,还精确到秒级时间。本文将深入探讨其内部存储格式、如何进行日期运算,并提供实用的最佳实践建议。
1. DATE 类型的内部存储机制
DATE 是一种固定宽度、占用 7 个字节的数据类型。每个字节分别存储以下七个属性:
字节 1:世纪 (Century)
字节 2:年份 (Year)
字节 3:月份 (Month)
字节 4:日 (Day)
字节 5:小时 (Hour)
字节 6:分钟 (Minute)
字节 7:秒 (Second)
Oracle 使用一种特殊的内部格式存储这些值,并非直接存储我们看到的数字。我们可以使用 DUMP 函数来查看其内部实际存储的值。
示例:查看内部存储
SQL> CREATE TABLE t (x DATE);
SQL> INSERT INTO t (x) VALUES (TO_DATE('25-jun-2005 12:01:00', 'dd-mon-yyyy hh24:mi:ss'));
SQL> SELECT x, DUMP(x, 10) AS d FROM t;
X D
--------- -----------------------------------
25-JUN-05 Typ=12 Len=7: 120,105,6,25,13,2,1
解码存储格式:
世纪和年份 (120,105):采用“excess-100”表示法。计算方式为 字节值 - 100。
世纪: 120 - 100 = 20 (20世纪)
年份: 105 - 100 = 5 (05年)
如果结果为负数,则表示公元前(BC)日期。
月份和日期 (6,25):直接存储,无需转换。6 代表六月,25 代表25日。
时间部分 (13,2,1):采用“excess-1”表示法。计算方式为 字节值 - 1。
小时: 13 - 1 = 12
分钟: 2 - 1 = 1
秒: 1 - 1 = 0
因此,1,1,1 代表午夜 00:00:00。
这种精巧的存储格式使得日期能够自然地按二进制顺序排序,并且截断操作(如截取到天或月)非常高效,只需将相应字节设置为 1 即可。
2. 给 DATE 类型加减时间
为 DATE 类型增加或减少时间单位主要有三种方法:
直接加数字:DATE + n。n 代表天数,因此 1 是一天,1/24 是一小时,1/24/60 是一分钟,依此类推。
使用 INTERVAL 数据类型:例如 NUMTODSINTERVAL(n, 'unit') 用于天/秒级单位,NUMTOYMINTERVAL(n, 'unit') 用于年月级单位。
使用内置函数:ADD_MONTHS(date, n) 专门用于增加月份,能智能处理月末日期。
推荐的最佳实践:
秒、分钟、小时:使用 NUMTODSINTERVAL(n, 'second'/'minute'/'hour')。代码更清晰易读。SELECT SYSDATE + NUMTODSINTERVAL(1, 'HOUR') FROM dual; -- 加1小时
天、周:直接加数字。SELECT SYSDATE + 7 FROM dual; -- 加1周
月、年:强烈推荐使用 ADD_MONTHS(date, n)。它能正确处理月末等特殊情况,避免错误。
为什么不推荐用 INTERVAL 加月/年?
ADD_MONTHS 会对月末日期进行智能舍入,而直接加 INTERVAL 可能导致错误。
-- ADD_MONTHS 能正确处理月末
SELECT ADD_MONTHS(TO_DATE('29-FEB-2000', 'DD-MON-YYYY'), 1) FROM dual;
-- 结果: 31-MAR-2000 (返回下个月的最后一天)
-- 使用 INTERVAL 可能出错
SELECT TO_DATE('30-JAN-2001', 'DD-MON-YYYY') + NUMTOYMINTERVAL(1, 'MONTH') FROM dual;
-- 错误: ORA-01839: 日期对指定的月份无效 (因为二月没有30号)
3. 计算两个 DATE 之间的差异
直接相减即可得到两个日期之间相差的天数(包括小数部分)。
SELECT (SYSDATE - hire_date) AS days_diff FROM employees;
要获取更直观的“年-月-日 时:分:秒”格式的差异,可以结合使用函数:
SELECT
NUMTOYMINTERVAL(TRUNC(MONTHS_BETWEEN(dt2, dt1)), 'month') AS years_months,
NUMTODSINTERVAL(
dt2 - ADD_MONTHS(dt1, TRUNC(MONTHS_BETWEEN(dt2, dt1))),
'day'
) AS days_hours
FROM (
SELECT
TO_DATE('29-feb-2000 01:02:03', 'dd-mon-yyyy hh24:mi:ss') AS dt1,
TO_DATE('15-mar-2001 11:22:33', 'dd-mon-yyyy hh24:mi:ss') AS dt2
FROM dual
);
-- 结果示例:
-- YEARS_MONTHS: +000000001-00 (1年0个月)
-- DAYS_HOURS: +000000015 10:20:30.000000000 (15天10小时20分30秒)
解释:
MONTHS_BETWEEN(dt2, dt1) 计算两个日期之间总月数(含小数)。
TRUNC(...) 取整,得到完整的月数。
NUMTOYMINTERVAL(...) 将整月数转换为“年-月”间隔。
dt2 - ADD_MONTHS(dt1, 整月数) 计算扣除整月后剩余的时间差。
NUMTODSINTERVAL(...) 将剩余的时间差转换为“天 时:分:秒”间隔。
总结
存储:Oracle DATE 使用 7 字节高效存储,格式精巧,支持自然排序和快速截断。
运算:
加/减时间:使用 NUMTODSINTERVAL 处理小时/分钟/秒,直接加数字处理天,使用 ADD_MONTHS 处理月/年。
避免使用 NUMTOYMINTERVAL 直接给日期加月份或年份,以免因月末日期导致错误。
差异:日期相减得天数。组合 MONTHS_BETWEEN, ADD_MONTHS, NUMTOYMINTERVAL 和 NUMTODSINTERVAL 函数可以计算出精确的年、月、日、时、分、秒差。
遵循这些最佳实践,您将能更加准确和高效地在 Oracle 中处理日期和时间数据。
------------------作者介绍-----------------------
姓名:黄廷忠
个人博客: (http://www.htz.pw)
CSDN地址: (https://blog.csdn.net/wwwhtzpw)
博客园地址: (https://www.cnblogs.com/www-htz-pw)