[오라클 운영] DBMS_SCHEDULER 캘린더 표현식 완벽 정리 - FREQ, BYHOUR, BYDAY 모든 활용
테스트 환경: Oracle 11g / 12c / 19c / 21c
DBMS_SCHEDULER로 작업을 등록할 때 가장 헷갈리는 부분이 repeat_interval 표현식입니다. 단순한 "매일 새벽 2시"는 쉽지만, "매월 마지막 평일 18시", "매분기 첫 번째 월요일", "공휴일 제외 평일 9시" 같은 요구사항을 받으면 막막해집니다.
이 글은 이전 글 (DBMS_SCHEDULER 6시간 배치 예제)의 후속편입니다. 거기서는 한 가지 시나리오만 다뤘다면, 이번 글은 운영 환경에서 자주 만나는 모든 스케줄 표현식을 카탈로그처럼 정리했습니다. 책갈피해 두고 필요할 때마다 꺼내 쓰는 레퍼런스 글로 활용하세요.
급하신 분은 자주 쓰는 표현식 모음부터 보세요.
기본 문법 - 한 번에 정리
캘린더 표현식의 전체 구조입니다.
repeat_interval =
FREQ=빈도
[; INTERVAL=숫자]
[; BYMONTH=월목록]
[; BYWEEKNO=주차]
[; BYYEARDAY=일자]
[; BYMONTHDAY=일목록]
[; BYDAY=요일목록]
[; BYHOUR=시목록]
[; BYMINUTE=분목록]
[; BYSECOND=초목록]
규칙:
- FREQ는 필수, 나머지는 선택
- 각 절은 세미콜론(;)으로 구분
- 절 안의 여러 값은 콤마(,)로 구분 (예: BYHOUR=4,10,16,22)
- 대소문자 구분 없음 (관습적으로 대문자)
FREQ - 7가지 빈도
FREQ 값 의미
| YEARLY | 매년 |
| MONTHLY | 매월 |
| WEEKLY | 매주 |
| DAILY | 매일 |
| HOURLY | 매시간 |
| MINUTELY | 매분 |
| SECONDLY | 매초 |
BY 절 핵심 9가지
절 가능한 값 음수 의미
| BYMONTH | 1~12 또는 JAN~DEC | - |
| BYMONTHDAY | 1~31, -1~-31 | -1 = 마지막 날 |
| BYDAY | MON,TUE,WED,THU,FRI,SAT,SUN | -1FRI = 마지막 금요일 |
| BYHOUR | 0~23 | - |
| BYMINUTE | 0~59 | - |
| BYSECOND | 0~59 | - |
| BYYEARDAY | 1~366, -1~-366 | -1 = 연중 마지막 날 |
| BYWEEKNO | 1~53 | -1 = 마지막 주 |
| BYSETPOS | -366~366 | -1 = 매치 중 마지막 |
INTERVAL - 빈도 간격
INTERVAL=N은 "FREQ N번마다 한 번"입니다.
FREQ=DAILY; INTERVAL=10 -- 10일마다
FREQ=HOURLY; INTERVAL=6 -- 6시간마다
FREQ=WEEKLY; INTERVAL=2 -- 격주
FREQ=MONTHLY; INTERVAL=3 -- 분기마다
주의: FREQ=HOURLY; INTERVAL=6은 "6시간 간격"이지 "04, 10, 16, 22시 정각"이 아닙니다. 정확한 시각을 원하면 BYHOUR를 쓰세요. 이전 글의 함정 참고.
자주 쓰는 표현식 모음 - 케이스별
운영 환경에서 가장 많이 만나는 시나리오들입니다. 그대로 복붙해서 활용하세요.
📌 일 단위
원하는 동작 표현식
| 매일 자정 | FREQ=DAILY;BYHOUR=0;BYMINUTE=0;BYSECOND=0 |
| 매일 새벽 2시 30분 | FREQ=DAILY;BYHOUR=2;BYMINUTE=30;BYSECOND=0 |
| 매일 4번 (04, 10, 16, 22시) | FREQ=DAILY;BYHOUR=4,10,16,22;BYMINUTE=0;BYSECOND=0 |
| 매일 영업시간만 (9~18시, 매시간 정각) | FREQ=HOURLY;BYHOUR=9,10,11,12,13,14,15,16,17,18 |
| 10일에 한 번 | FREQ=DAILY;INTERVAL=10 |
📌 주 단위
원하는 동작 표현식
| 매주 월요일 9시 | FREQ=WEEKLY;BYDAY=MON;BYHOUR=9;BYMINUTE=0;BYSECOND=0 |
| 평일만 (월~금) 9시 | FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI;BYHOUR=9 |
| 주말만 (토~일) 자정 | FREQ=DAILY;BYDAY=SAT,SUN;BYHOUR=0 |
| 격주 금요일 18시 | FREQ=WEEKLY;INTERVAL=2;BYDAY=FRI;BYHOUR=18 |
| 매주 월수금 새벽 6시 | FREQ=WEEKLY;BYDAY=MON,WED,FRI;BYHOUR=6 |
📌 월 단위
원하는 동작 표현식
| 매월 1일 자정 | FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=0 |
| 매월 15일 새벽 3시 | FREQ=MONTHLY;BYMONTHDAY=15;BYHOUR=3 |
| 매월 마지막 날 23시 | FREQ=MONTHLY;BYMONTHDAY=-1;BYHOUR=23 |
| 매월 마지막 평일 18시 | FREQ=MONTHLY;BYDAY=MON,TUE,WED,THU,FRI;BYSETPOS=-1;BYHOUR=18 |
| 매월 첫째 월요일 | FREQ=MONTHLY;BYDAY=1MON |
| 매월 둘째 수요일 | FREQ=MONTHLY;BYDAY=2WED |
| 매월 마지막 금요일 | FREQ=MONTHLY;BYDAY=-1FRI |
| 분기마다 (1, 4, 7, 10월 1일) | FREQ=MONTHLY;BYMONTH=1,4,7,10;BYMONTHDAY=1 |
📌 연 단위
원하는 동작 표현식
| 매년 1월 1일 자정 | FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1;BYHOUR=0 |
| 매년 12월 31일 23:59 | FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=31;BYHOUR=23;BYMINUTE=59 |
| 매년 마지막 평일 | FREQ=YEARLY;BYDAY=MON,TUE,WED,THU,FRI;BYSETPOS=-1 |
| 매년 첫째 월요일 | FREQ=YEARLY;BYDAY=1MON |
📌 시간/분/초 단위
원하는 동작 표현식
| 매시간 정각 | FREQ=HOURLY;BYMINUTE=0;BYSECOND=0 |
| 매시간 30분 | FREQ=HOURLY;BYMINUTE=30;BYSECOND=0 |
| 5분마다 | FREQ=MINUTELY;INTERVAL=5 |
| 15분마다 (정각, 15, 30, 45) | FREQ=HOURLY;BYMINUTE=0,15,30,45 |
| 매분 정각 | FREQ=MINUTELY;BYSECOND=0 |
| 30초마다 | FREQ=SECONDLY;INTERVAL=30 |
★ BYDAY 음수 / N번째 표현 - 가장 강력한 기능
BYDAY는 단순 요일 외에도 "N번째 요일" 또는 "마지막 요일" 을 표현할 수 있습니다. 이걸 알면 복잡한 스케줄도 한 줄로 해결됩니다.
표기 규칙
[숫자]요일
표현 의미
| MON | 매주 월요일 |
| 1MON | 첫째 월요일 |
| 2MON | 둘째 월요일 |
| -1MON | 마지막 월요일 |
| -2MON | 마지막에서 두 번째 월요일 |
실전 예시
-- 매월 두 번째 월요일 10시 (이사회 같은 정기 일정)
'FREQ=MONTHLY;BYDAY=2MON;BYHOUR=10'
-- 매월 마지막 토요일 자정 (월말 백업)
'FREQ=MONTHLY;BYDAY=-1SAT;BYHOUR=0'
-- 매분기 첫째 수요일 (분기 정산)
'FREQ=MONTHLY;BYMONTH=1,4,7,10;BYDAY=1WED'
★ BYSETPOS - 매치 결과 중 N번째 선택
BYSETPOS는 다른 BY 절의 결과 집합 중에서 N번째만 선택합니다.
가장 유용한 예시: "매월 마지막 평일"
-- BYDAY는 평일 전부를 매치, BYSETPOS=-1로 마지막만
FREQ=MONTHLY; BYDAY=MON,TUE,WED,THU,FRI; BYSETPOS=-1
이 표현식은 매월 마지막 날이 토요일이라도 그 전 금요일을 선택합니다. 즉, 달력상 마지막 평일을 정확히 잡습니다.
더 활용 예시
-- 매월 첫 번째 평일 9시 (월초 정산 시작)
FREQ=MONTHLY; BYDAY=MON,TUE,WED,THU,FRI; BYSETPOS=1; BYHOUR=9
-- 매월 마지막에서 두 번째 평일
FREQ=MONTHLY; BYDAY=MON,TUE,WED,THU,FRI; BYSETPOS=-2
-- 분기 마지막 평일 18시 (분기 마감)
FREQ=MONTHLY; BYMONTH=3,6,9,12; BYDAY=MON,TUE,WED,THU,FRI; BYSETPOS=-1; BYHOUR=18
★ INCLUDE / EXCLUDE - 공휴일 처리 (실무 핵심)
한국어 자료에 거의 없는 강력한 기능입니다. 다른 schedule을 참조해서 포함/제외할 수 있습니다.
시나리오: 공휴일 제외하고 평일에만 실행
1단계: 공휴일 schedule 정의
-- 2026년 공휴일 schedule 생성
BEGIN
DBMS_SCHEDULER.CREATE_SCHEDULE(
schedule_name => 'KOREA_HOLIDAYS_2026',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=YEARLY;BYDATE=20260101,20260301,20260505,' ||
'20260606,20260815,20261003,20261009,20261225'
);
END;
/
2단계: 작업 schedule에서 EXCLUDE 사용
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'JOB_WORKDAY_BATCH',
job_type => 'STORED_PROCEDURE',
job_action => 'PKG_BATCH.RUN_DAILY',
repeat_interval => 'FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI;' ||
'EXCLUDE=KOREA_HOLIDAYS_2026;BYHOUR=2',
enabled => TRUE
);
END;
/
이렇게 하면 평일이라도 공휴일이면 실행되지 않습니다.
INCLUDE / INTERSECT의 차이
키워드 의미
| INCLUDE | 참조 schedule의 날짜를 추가 |
| EXCLUDE | 참조 schedule의 날짜를 제외 |
| INTERSECT | 참조 schedule과 공통된 날짜만 |
-- 평일 + 공휴일에도 실행 (휴일에도 점검)
'FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI;INCLUDE=KOREA_HOLIDAYS_2026'
-- 공휴일에만 실행
'FREQ=DAILY;INTERSECT=KOREA_HOLIDAYS_2026'
BYDATE - 특정 날짜 직접 지정
BYDATE는 MMDD 또는 YYYYMMDD 형식으로 특정 날짜를 직접 지정합니다.
-- 매년 1월 1일과 12월 25일
'FREQ=YEARLY;BYDATE=0101,1225'
-- 특정 연도의 특정 날짜
'FREQ=YEARLY;BYDATE=20260315,20260920'
-- 분기 마지막 날 (3, 6, 9, 12월의 마지막 날)
'FREQ=MONTHLY;BYMONTH=3,6,9,12;BYMONTHDAY=-1'
공휴일이나 특별 이벤트일을 한꺼번에 처리할 때 유용합니다.
★ 검증 방법 - EVALUATE_CALENDAR_STRING
복잡한 표현식을 만들었다면 실제 등록 전에 다음 N번의 실행 시각을 미리 확인해야 합니다. 그렇지 않으면 의도와 다른 시각에 실행되는 사고가 납니다.
SET SERVEROUTPUT ON
DECLARE
v_start TIMESTAMP WITH TIME ZONE := SYSTIMESTAMP;
v_next TIMESTAMP WITH TIME ZONE;
BEGIN
DBMS_OUTPUT.PUT_LINE('검증할 표현식: FREQ=MONTHLY;BYDAY=MON,TUE,WED,THU,FRI;BYSETPOS=-1;BYHOUR=18');
DBMS_OUTPUT.PUT_LINE('-----------------------------------');
FOR i IN 1..10 LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING(
calendar_string => 'FREQ=MONTHLY;BYDAY=MON,TUE,WED,THU,FRI;BYSETPOS=-1;BYHOUR=18',
start_date => SYSTIMESTAMP,
return_date_after => v_start,
next_run_date => v_next
);
DBMS_OUTPUT.PUT_LINE(i || '회차: ' ||
TO_CHAR(v_next, 'YYYY-MM-DD (DY) HH24:MI:SS'));
v_start := v_next;
END LOOP;
END;
/
출력 예시:
1회차: 2026-05-29 (FRI) 18:00:00
2회차: 2026-06-30 (TUE) 18:00:00
3회차: 2026-07-31 (FRI) 18:00:00
4회차: 2026-08-31 (MON) 18:00:00
...
이 검증 단계를 생략하지 마세요. 실수의 95%가 여기서 잡힙니다.
자주 발생하는 함정 5가지
함정 1: BY 절 생략 시 start_date 값이 적용됨
-- start_date가 2026-06-01 09:23:17 인데
-- BYHOUR을 생략
'FREQ=DAILY'
-- → 매일 09:23:17에 실행됨 (09:00 아님!)
해결: 정확한 시각을 원하면 항상 BYHOUR, BYMINUTE, BYSECOND를 명시.
함정 2: HOURLY INTERVAL의 시작점
-- "6시간마다"라고 등록했는데
'FREQ=HOURLY;INTERVAL=6'
-- → start_date에 따라 09, 15, 21, 03시 식으로 어긋날 수 있음
해결: 정확한 시각을 원하면 BYHOUR=4,10,16,22 같이 명시.
함정 3: 31일 없는 달의 처리
'FREQ=MONTHLY;BYMONTHDAY=31'
-- → 2월은 매치 없음 → 실행 안 됨
-- → 4월, 6월, 9월, 11월도 매치 없음
해결: "매월 말일"이 목적이면 BYMONTHDAY=-1 사용.
함정 4: 타임존 불일치
19c부터 DBMS_SCHEDULER가 세션 타임존을 사용합니다. UTC 환경 서버에서 한국 시간 9시를 의도했는데 새벽 6시(KST)에 실행되는 사고가 자주 발생합니다.
해결: start_date에 명시적 타임존 지정.
start_date => TO_TIMESTAMP_TZ('2026-06-01 09:00:00 Asia/Seoul',
'YYYY-MM-DD HH24:MI:SS TZR')
함정 5: 음수 BYDAY 표기 오류
-- ❌ 잘못된 표기 (공백 들어감)
'FREQ=MONTHLY;BYDAY=-1 FRI'
-- ✅ 올바른 표기 (붙여 씀)
'FREQ=MONTHLY;BYDAY=-1FRI'
음수와 요일 사이에 공백을 넣으면 파싱 오류가 발생합니다.
운영 환경 권장 표준
작업 등록 표준 패턴
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'JOB_명칭',
job_type => 'PLSQL_BLOCK',
job_action => '...',
-- start_date에 명시적 타임존
start_date => TO_TIMESTAMP_TZ(
'2026-06-01 02:00:00 Asia/Seoul',
'YYYY-MM-DD HH24:MI:SS TZR'),
-- 정확한 시각 위해 BYHOUR/BYMINUTE/BYSECOND 모두 명시
repeat_interval => 'FREQ=DAILY;BYHOUR=2;BYMINUTE=0;BYSECOND=0',
enabled => TRUE,
comments => '명확한 작업 설명'
);
END;
/
체크리스트
- [ ] BY 절 누락 없는가? (HOUR, MINUTE, SECOND)
- [ ] EVALUATE_CALENDAR_STRING으로 검증했는가?
- [ ] start_date에 타임존을 명시했는가?
- [ ] 공휴일 처리가 필요한가? (EXCLUDE 활용)
- [ ] 31일/말일 처리에 함정이 없는가?
마무리
DBMS_SCHEDULER의 캘린더 표현식은 처음엔 복잡해 보이지만, 빈도(FREQ) + BY 절 조합 이라는 단순한 구조입니다. 자주 쓰는 패턴 10여 개만 익혀 두면 운영 환경의 거의 모든 스케줄을 한 줄로 표현할 수 있습니다.
특히 BYSETPOS와 EXCLUDE 같은 고급 기능은 운영 현장에서 정말 강력합니다. "마지막 평일"이나 "공휴일 제외" 같은 요구사항을 트리거나 별도 스크립트로 우회하지 마세요. 캘린더 표현식 한 줄로 깔끔하게 해결됩니다.
가장 중요한 건 등록 전 EVALUATE_CALENDAR_STRING으로 검증하는 습관입니다. 운영에 들어간 후 의도와 다르게 동작하는 스케줄을 잡는 것보다 100배 빠릅니다.
비슷한 스케줄링 요구사항이나 더 좋은 패턴이 있다면 댓글로 공유해 주세요.