需求:計算起始日期與結束日期之間的**工作日天數** (不含雙方日期、不含週末)。
SELECT
((TO_NUMBER(TRUNC(TO_DATE('2015-04-22','YYYY-MM-DD'), 'D') -
TRUNC(TO_DATE('2015-04-21','YYYY-MM-DD') + 6, 'D'))) / 7 * 5)
+ MOD(7 - TO_NUMBER(TO_CHAR(TO_DATE('2015-04-21','YYYY-MM-DD'), 'D')), 6)
+ LEAST(TO_NUMBER(TO_CHAR(TO_DATE('2015-04-22','YYYY-MM-DD'), 'D')) - 2, 5)
AS work_days
FROM dual;
(TRUNC(end,'D') - TRUNC(start+6,'D')) / 7 * 5MOD(7 - TO_NUMBER(TO_CHAR(start,'D')), 6)LEAST(TO_NUMBER(TO_CHAR(end,'D')) - 2, 5)| 起始日期 | 結束日期 | 說明 | SQL 結果 |
|---|---|---|---|
| 2015-04-21 (二) | 2015-04-22 (三) | 相鄰工作日 | 1 |
| 2015-04-24 (五) | 2015-04-27 (一) | 跨週末 | 1 |
| 2015-04-20 (一) | 2015-04-24 (五) | 同週完整工作週 | 3 |
注意: 此公式**將起始/結束日期若為工作日則計入**。若需「不含雙方日期」,需調整邊界條件。
CREATE OR REPLACE FUNCTION num_business_days (
p_start_date IN DATE,
p_end_date IN DATE
) RETURN NUMBER
IS
v_curr_date DATE := p_start_date;
v_day_name VARCHAR2(10);
v_count NUMBER := 0;
BEGIN
-- 邊界檢查
IF p_end_date - p_start_date <= 0 THEN
RETURN 0;
END IF;
LOOP
v_curr_date := v_curr_date + 1; -- 移至下一天
EXIT WHEN v_curr_date = p_end_date; -- 到達結束日期停止 (不含結束日)
-- 取得星期名稱 (需注意 NLS_TERRITORY 影響)
SELECT TO_CHAR(v_curr_date, 'fmDay') INTO v_day_name FROM dual;
-- 非週末則計數
IF v_day_name NOT IN ('Saturday', 'Sunday') THEN
v_count := v_count + 1;
END IF;
END LOOP;
RETURN v_count;
END num_business_days;
/
兩方法關鍵差異對比
| 維度 | SQL 公式法 | PL/SQL 迴圈法 |
|---|---|---|
| 效能 | 極高 (單語句、無上下文切換) | 較低 (逐日迴圈、Context Switch) |
| 邊界語義 | 含起始/結束工作日 | **不含**起始/結束日期 (可精確控制) |
| NLS 敏感度 | 低 (使用數字格式 'D') | 高 (依賴 'fmDay' 文字,受 NLS_TERRITORY 影響) |
| 可維護性 | 低 (公式晦澀難改) | 高 (邏輯直觀易修改) |
| 適用場景 | 大量資料集運算、報表查詢 | 業務邏輯封裝、精確邊界控制 |
-- 使用數字格式 'D' (1=週日, 7=週六,美式標準) v_day_num := TO_NUMBER(TO_CHAR(v_curr_date, 'D')); IF v_day_num NOT IN (1, 7) THEN -- 排除週日(1)與週六(7) v_count := v_count + 1; END IF;
DETERMINISTIC 函數並建立函數索引加速查詢