用TRUNC和MOD可高效计算工作日天数:先算总天数,再减周末天数(基于TRUNC(date,'IW')确保ISO标准),最后校正起止日是否为周末;节假日须通过holidays表显式过滤,不可硬编码。

直接用 TRUNC 和 MOD 算出工作日天数,别写循环
Oracle 没有内置的“工作日差”函数,但用 TRUNC 和 MOD 组合就能避开游标或递归,避免性能崩盘。核心思路是:先算总天数,再减去周末天数(含跨周末的边界情况),最后手动校正起止日是否为周末。
常见错误是直接用 TO_CHAR(date, 'D') 判断星期几——它依赖 NLS_TERRITORY 设置,德国返回周日=1、美国可能周一=1,结果不可靠。必须用 TRUNC(date) - TRUNC(date, 'IW') 或固定偏移法。
-
TRUNC(date, 'IW')返回本周一(ISO 标准,稳定不随 NLS 变) - 用
(TRUNC(end_date) - TRUNC(start_date))得总自然日 - 用
FLOOR((TRUNC(end_date, 'IW') - TRUNC(start_date, 'IW')) / 7) * 2算完整周末天数(每个整周扣 2 天) - 再检查 start_date 和 end_date 所在周的周一到周五之间是否包含它们,用
LEAST/GREATEST处理跨周边界
处理节假日必须显式传入表,不能硬编码
业务系统里“工作日”必然包含法定假日,PL/SQL 无法自动识别国庆/春节。硬编码 CASE WHEN date IN (DATE'2024-01-28', ...) 会随时间失效,且难以维护。
正确做法是建一张 holidays 表(字段:hol_date DATE PRIMARY KEY),然后在计算逻辑里 LEFT JOIN 或用 NOT EXISTS 过滤:
SELECT COUNT(*)
FROM (
SELECT TRUNC(start_date) + LEVEL - 1 AS dt
FROM DUAL
CONNECT BY TRUNC(start_date) + LEVEL - 1 <= TRUNC(end_date)
) days
WHERE TO_CHAR(dt, 'D', 'NLS_DATE_LANGUAGE=AMERICAN') NOT IN ('1', '7')
AND NOT EXISTS (SELECT 1 FROM holidays h WHERE h.hol_date = days.dt)
注意:CONNECT BY 在大数据区间(如跨年)会慢,仅适合小范围(
TO_CHAR(date, 'D') 的坑:NLS 设置让结果飘忽不定
很多人用 TO_CHAR(my_date, 'D') 判断周几,结果开发环境返回 1=周日,测试环境变成 1=周一,上线就错乱。根本原因是 NLS_TERRITORY 控制该格式符行为,而它常被会话级设置覆盖。
安全写法只有两种:
- 强制指定语言:
TO_CHAR(my_date, 'D', 'NLS_DATE_LANGUAGE=AMERICAN')(此时周日=1,周六=7) - 用 ISO 周计算:
TRUNC(my_date) - TRUNC(my_date, 'IW') + 1(返回 1=周一,7=周日,完全不受 NLS 影响)
如果业务要求“周一至周五为工作日”,必须统一用第二种,否则节假日脚本在不同数据库实例上跑出不同结果。
函数封装时务必声明 DETERMINISTIC
把工作日计算封装成函数(比如 workdays_between)后,若没加 DETERMINISTIC,Oracle 无法在函数索引、物化视图或查询重写中复用结果,性能损失明显。
但要注意:只要函数内部查了 holidays 表,就不能标 DETERMINISTIC——因为表数据会变。这时得拆成两层:
- 底层纯计算函数(只依赖输入日期,加
DETERMINISTIC) - 上层包装函数(JOIN holidays 表,不加该关键字)
否则优化器可能缓存过期结果,导致某天突然多算/少算 1 天,排查极难。


















