ADDDATE · SUBDATE — 날짜 더하기·빼기 완전 정리
날짜에 기간을 더하거나 빼는 계산은 회원가입일 + 30일 = 쿠폰 만료일, 오늘 - 1개월 = 지난달 통계 기준일처럼 실무에서 정말 자주 나온다. 아래 코드는 ADDDATE와 SUBDATE에 INTERVAL 표현식을 넘겨 "며칠 뒤"와 "몇 달 뒤"를 동시에 구하는 가장 기본적인 형태다.
| 계산식 | 기준일 | 연산 | 결과 |
|---|---|---|---|
| ADDDATE(..., INTERVAL 31 DAY) | 2025-01-01 | +31일 | 2025-02-01 |
| ADDDATE(..., INTERVAL 1 MONTH) | 2025-01-01 | +1개월 | 2025-02-01 |
| SUBDATE(..., INTERVAL 31 DAY) | 2025-01-01 | -31일 | 2024-12-01 |
| SUBDATE(..., INTERVAL 1 MONTH) | 2025-01-01 | -1개월 | 2024-12-01 |
ADDDATE(날짜, 기간) 형태로 항상 두 개의 값을 받는다. 첫 번째는 계산의 출발점이 되는 날짜, 두 번째는 거기에 더할 양이다. 이 구조가 중요한 이유는 "1월 1일에서 31일을 더한다"는 계산이 단순히 숫자를 더하는 것이 아니라 달력 규칙(월별 일수, 윤년 등)을 따라야 하기 때문이다. MySQL은 이 규칙을 함수 내부에서 대신 처리해준다.
즉 개발자가 직접 "이번 달은 31일까지인지 28일까지인지"를 계산할 필요가 없다. 그 계산을 함수에 위임하는 것이 ADDDATE를 쓰는 핵심 이유다.
이렇게 계산 규칙을 함수 안으로 감추면, 애플리케이션 코드(PHP·자바 등)에서 날짜 라이브러리를 따로 불러오지 않고도 DB 레벨에서 바로 정확한 날짜를 얻을 수 있다는 실무적 이점도 있다.
'2025-01-01'뿐 아니라 CURDATE(), 다른 컬럼(예: created_at)도 그대로 들어갈 수 있다.두 번째 인자를 "기간"이라고 부르는 것도 의도적이다. 단순한 숫자가 아니라 INTERVAL이라는 별도의 표현식 문법을 쓰기 때문에, 이 함수는 "숫자 하나를 더하는 계산기"가 아니라 "날짜 전용 계산 규칙을 아는 함수"로 설계되어 있다.
SUBDATE(날짜, 기간)는 ADDDATE와 정확히 반대 방향으로 계산한다. 사실 ADDDATE(date, INTERVAL -31 DAY)처럼 음수를 넣으면 SUBDATE와 같은 결과를 얻을 수 있지만, 실무 코드에서는 "빼기"라는 의도를 명시적으로 드러내는 것이 가독성 면에서 훨씬 유리하다. 코드를 읽는 사람이 부호를 뒤집어 계산할 필요 없이 함수 이름만 보고 방향을 알 수 있다.
결국 ADDDATE와 SUBDATE 중 무엇을 쓰느냐는 계산 결과에는 영향이 없고, 코드의 의도를 얼마나 분명하게 전달하느냐의 문제다. 협업 환경에서는 이런 작은 명시성의 차이가 리뷰 시간을 크게 줄여준다.
만약 ADDDATE(date, 31)처럼 숫자만 넘긴다면 MySQL은 그 31을 "일(day)"로 해석한다. 하지만 사람이 "31 뒤"라고 말할 때는 31일일 수도, 31개월일 수도, 31시간일 수도 있다. INTERVAL 31 DAY, INTERVAL 1 MONTH처럼 단위를 명시하면 이 모호함이 완전히 사라진다. 즉 INTERVAL은 "이 숫자의 단위가 무엇인지"를 SQL 문 안에 고정하는 장치다.
이 표현식은 INTERVAL 숫자 단위 순서를 반드시 지켜야 하며, 단위는 예약어이므로 따옴표 없이 그대로 쓴다.
INTERVAL -7 DAY는 "7일 전"과 같은 의미이며, 이 성질 덕분에 ADDDATE 하나만으로도 SUBDATE와 같은 계산을 표현할 수 있다.
실무에서 자주 쓰는 단위만 추려도 아래와 같다. 단위를 잘못 고르면 "하루 뒤"를 구하려다 "한 시간 뒤"가 나오는 실수로 이어지므로, 표를 보고 의도한 단위인지 꼭 확인하는 습관이 필요하다.
| 단위 | 의미 | 예시 |
|---|---|---|
| DAY | 일 단위 | INTERVAL 7 DAY → 7일 뒤 |
| WEEK | 주 단위 (7일과 동일 계산) | INTERVAL 2 WEEK → 14일 뒤 |
| MONTH | 월 단위, 같은 "일"을 다음 달로 이동 | INTERVAL 1 MONTH |
| YEAR | 연 단위 | INTERVAL 1 YEAR |
| HOUR / MINUTE / SECOND | 시간 단위, 결과가 DATETIME으로 승격 | INTERVAL 3 HOUR |
ADDDATE는 ADDDATE(date, days)처럼 INTERVAL 없이 정수만 넘기는 축약형도 지원한다. 이때 days는 항상 "일(day)"로 해석된다. 짧게 쓸 수 있어 편리하지만, 단위가 코드에 드러나지 않아 나중에 코드를 읽는 사람이 "이 숫자가 일인지 다른 단위인지" 헷갈릴 수 있다는 단점이 있다.
SUBDATE(date, days)를 지원하지만, 팀 코드 컨벤션에서는 의미를 명확히 하기 위해 INTERVAL 표기를 권장하는 경우가 많다.결국 두 형태 중 어느 쪽이 "더 옳다"는 없다. 짧게 쓰고 싶다면 축약형을, 여러 사람이 함께 읽는 코드라면 INTERVAL 표기를 선택하면 된다.
MySQL에는 ADDDATE·SUBDATE 외에 DATE_ADD·DATE_SUB라는 이름의 함수도 존재하는데, 이 둘은 동작이 완전히 동일하다. ADDDATE/SUBDATE는 표준 SQL 호환을 위한 별칭(alias)이고, DATE_ADD/DATE_SUB는 MySQL이 먼저 제공하던 이름이다. 어느 쪽을 써도 결과는 같으므로, 팀 컨벤션이나 개인 취향에 맞춰 하나를 골라 일관되게 쓰면 된다.
표준 SQL 문법과 이름이 비슷해 익숙함
MySQL 공식 문서 예제에 더 자주 등장
MONTH 단위 계산에서 자주 놓치는 부분이다. 예를 들어 ADDDATE('2025-01-31', INTERVAL 1 MONTH)를 계산하면 2월에는 31일이 없으므로 MySQL은 결과를 2월의 마지막 날인 2025-02-28로 자동 보정한다. 이는 버그가 아니라 달력상 존재하지 않는 날짜를 만들지 않기 위한 의도된 동작이다.
이 특성을 모르고 "매달 같은 일자에 알림을 보낸다"는 로직을 짜면, 31일에 가입한 회원의 알림일이 짝수 달마다 슬쩍 당겨지는 현상이 발생할 수 있다. 날짜 계산 로직을 만들 때는 이런 월말 보정을 반드시 염두에 둬야 한다.
| 기준일 | +1 MONTH 결과 | 비고 |
|---|---|---|
| 2025-01-31 | 2025-02-28 | 2월은 28일까지라 마지막 날로 보정 |
| 2024-01-31 | 2024-02-29 | 윤년이라 29일까지 보정 |
| 2025-01-15 | 2025-02-15 | 대상 달에 15일이 존재하므로 그대로 이동 |
실무에서는 고정된 문자열 날짜보다 "오늘"을 기준으로 계산하는 경우가 압도적으로 많다. 회원가입 즉시 발급되는 쿠폰의 만료일, 최근 N일 데이터 조회, 지난달 매출 집계 등이 대표적이다. 아래 단계는 이런 패턴이 실제로 만들어지는 흐름을 보여준다.
- 비교용 조건절(WHERE)에서는 계산 결과가 상수라 인덱스를 그대로 탈 수 있어 성능에 유리하다.
- 컬럼에 함수를 씌우는 것(예: ADDDATE(order_date, ...) = ...)은 인덱스를 못 타므로 피한다.
- 기준일 자리에 다른 테이블의 컬럼을 넣어도 동일하게 동작한다.
| 실수 | 증상 | 해결 |
|---|---|---|
| INTERVAL 없이 문자열로 기간을 씀 (예: ADDDATE(date, '1 MONTH')) | 문법 오류 또는 의도와 다른 결과 | INTERVAL 숫자 단위 형태를 정확히 지켜 작성 |
| 단위를 DAY로 착각하고 MONTH를 씀 | 기대보다 훨씬 먼 미래·과거 날짜가 나옴 | 4번 섹션의 단위 표를 보고 의도한 단위인지 재확인 |
| 31일에 매달 반복되는 이벤트를 MONTH 단위로 계산 | 2월 등 짧은 달에서 일자가 당겨짐(28·29일) | 정확한 일수가 필요하면 DAY 단위로 계산하거나 LAST_DAY 등으로 별도 보정 |
| WHERE절 컬럼에 ADDDATE를 씌워 비교 | 인덱스를 타지 못해 쿼리가 느려짐 | 계산은 기준값(우변) 쪽에서만 하고 컬럼은 그대로 비교 |
| ADDDATE와 SUBDATE의 부호 방향을 혼동 | 미래를 구해야 하는데 과거 날짜가 나옴 | 더할 땐 ADDDATE, 뺄 땐 SUBDATE로 함수 이름 자체로 방향을 고정 |
| NOW()를 CURDATE() 대신 사용 | 결과에 시:분:초까지 붙어 날짜만 비교할 때 예상과 다르게 동작 | 순수 날짜 계산이 목적이면 CURDATE()를 사용 |
핵심 한 줄 요약
Tags
'php' 카테고리의 다른 글
| MySQL REPEAT · REPLACE · REVERSE — 문자열 변형 완전 정리 (0) | 2026.06.28 |
|---|---|
| MySQL DATEDIFF · TIMEDIFF — 날짜·시간 차이 완전 정리 (0) | 2026.06.28 |
| MySQL CONCAT_WS — 구분자 지정 문자열 연결 완전 정리 (0) | 2026.06.27 |
| MySQL ELT · LOCATE · INSTR — 위치·값 찾기 완전 정리 (0) | 2026.06.27 |
| MySQL SUBSTRING · SPACE · SUBSTRING_INDEX — 문자열 추출 완전 정리 (0) | 2026.06.27 |
왕진 블로그

댓글