한 줄 요약:
--sysdate-is-now는 기동 시점에만 지정 가능한 서버 옵션(동적 변수 아님)으로, 문장 내부에서도 값이 바뀌고SET TIMESTAMP를 무시하는 비결정성 함수SYSDATE()를NOW()로 동치시켜 statement 기반 복제에서 소스·레플리카 값이 어긋나지 않게 만듦. ROW 포맷이 기본값이 된 8.0+ 에서는 실효성이 크게 떨어졌지만, 비결정성 함수가 복제 안전성에 어떻게 영향을 주는지 이해하는 표본 케이스임.
개요
MySQL 의 날짜·시간 함수 중 NOW() 와 SYSDATE() 는 이름은 비슷하지만 결정성이 정반대임. NOW() 는 문장이 시작된 시점의 타임스탬프를 고정해서 같은 문장 안에서는 항상 같은 값을 돌려주는 반면, SYSDATE() 는 호출되는 바로 그 순간의 실제 시각을 돌려주므로 한 문장 안에서도 호출마다 값이 달라짐. 이 “문장 내 불변성” 유무가 statement 기반 복제의 재현 가능성을 좌우함.
--sysdate-is-now 는 이 문제를 해결하려고 MySQL 5.0 때부터 존재해 온 서버 기동 옵션임. 켜두면 서버 전체에서 SYSDATE() 가 NOW() 로 동치(alias)됨. 시스템 변수가 아니라서 SHOW VARIABLES LIKE 'sysdate_is_now' 나 @@sysdate_is_now 로는 조회되지 않고, SET GLOBAL 로 바꿀 수도 없음. 오로지 mysqld --sysdate-is-now 나 my.cnf 의 [mysqld] 섹션에서 지정하고 재기동해야 반영됨.
| 항목 | 내용 |
|---|---|
| 유형 | 서버 기동 옵션 (command-line/my.cnf 전용, 동적 변수 아님) |
| 기본값 | FALSE (OFF) |
| 변경 방법 | mysqld --sysdate-is-now 또는 my.cnf sysdate-is-now 후 재기동 |
| 도입 | MySQL 5.0 |
| 의존 관계 | statement/mixed 기반 복제에서 의미 있음. ROW 포맷에서는 사실상 무의미함. binlog_format 자체가 8.0.34+ deprecated |
| 관련 비결정성 | SYSDATE(), (비교 대상) NOW(), UUID(), RAND() |
MySQL 8.4.8 에서 직접 확인 —
SELECT @@sysdate_is_now·SHOW VARIABLES LIKE 'sysdate%'모두 시스템 변수 테이블에 없어 조회 불가. 가동 중인 인스턴스의 활성 여부는 my.cnf 텍스트와 SQL probe 로 확인함 (아래 적용 절차 1단계 참조).
결론
NOW() · SYSDATE() · —sysdate-is-now 세 경로 비교
| 관찰 지점 | NOW() | SYSDATE() (기본) | SYSDATE() (—sysdate-is-now) |
|---|---|---|---|
| 같은 문장 내 호출마다 값 | 동일 (문장 시작 시각 고정) | 호출 시점마다 달라짐 | 동일 (NOW() 로 동치) |
SET TIMESTAMP=... 반영 | 반영됨 | 무시됨 — 실제 시스템 시계 사용 | 반영됨 |
| STATEMENT 복제에서 replay 시 정합성 | 안전 (양쪽 SET TIMESTAMP 적용) | 깨짐 (replica 시계로 재평가) | 안전 — 단, replica 에도 동일 옵션 필요 |
| 바이너리 로그에 남는 문자열 | NOW() | SYSDATE() | SYSDATE() (치환은 파서 단계 Item 매핑만) |
| STATEMENT 포맷 unsafe 경고 (Note 1592) | 없음 | 발생 | 여전히 발생 |
| ROW 포맷(기본값)에서의 영향 | 없음 — 실제 값이 이미 기록됨 | 없음 — 동일 | 없음 |
핵심은 세 가지임.
SYSDATE()는NOW()와 달리 문장 스코프의 동결된 시각이 아니라 호출 순간의 실제 시각을 돌려줌.- 그래서
SET TIMESTAMP를 기반으로 소스 시각을 재현하려는 statement 복제와 호환되지 않음. --sysdate-is-now는 이 비결정성을 런타임 동치로 무력화하지만, ROW 가 기본값이 된 8.0+ 위에binlog_format시스템 변수 자체가 8.0.34+ 에서 deprecated 되면서 STATEMENT/MIXED 라인의 미래가 끊겼음. 옵션 자체는 8.4 에서도 살아 있지만 실효 타깃이 매우 좁아졌음.
언제 켜야 하는가
| 환경 | 권장 |
|---|---|
binlog_format=ROW (8.0+ 기본값) | 켜지 않아도 됨 — ROW 가 이미 최종 값을 그대로 전파함 |
| STATEMENT/MIXED 포맷을 유지하는 레거시 클러스터 | 소스·레플리카 모두 에 켜야 의미 있음 |
애플리케이션이 SYSDATE() 를 의존하지 않는 경우 | 굳이 켜지 않고 NOW() 로 통일하는 편이 더 단순함 |
DST·시간 조작 테스트에서 SYSDATE() 를 쓰는 경우 | 옵션 켜면 SET TIMESTAMP 가 먹히므로 테스트가 쉬워짐 |
가장 간단한 결론: 8.0 이후 새 클러스터에서는 binlog_format 을 ROW 로 쓰므로 이 옵션을 켤 이유가 거의 없음. SYSDATE() 자체의 “실시간성” 이 필요한 코드가 아니라면, 애플리케이션 쪽에서 NOW() 로 바꾸는 것이 근본적 해결임.
적용 절차 (실제로 켜야 할 때)
1단계 — 현재 상태 확인
시스템 변수로는 조회 불가이므로 config 파일과 실제 동작 두 경로를 봄.
# my.cnf 에 이미 들어있는지
grep -r 'sysdate[-_]is[-_]now' /etc/mysql /etc/my.cnf /etc/my.cnf.d 2>/dev/null가동 중인 인스턴스의 실제 동작은 SQL probe 로 확인.
SET TIMESTAMP = 1000000000;
SELECT NOW() = SYSDATE() AS sysdate_aliased;sysdate_aliased = 1 이면 --sysdate-is-now 가 켜진 상태, 0 이면 꺼진 상태임 (꺼져 있으면 NOW() 만 SET TIMESTAMP 를 따라가고 SYSDATE() 는 OS 시계라 두 값이 다름).
2단계 — my.cnf 반영 후 재기동
[mysqld] 섹션에 추가. 재기동 없이 동적으로 바꿀 방법은 없음.
[mysqld]
sysdate-is-now재기동 후 위 SQL probe 로 실제 동작이 NOW() 동치 모드인지 확인.
SET TIMESTAMP = 1000000000;
SELECT NOW() = SYSDATE() AS sysdate_aliased;
-- sysdate_aliased = 1 이면 OK3단계 — 복제 토폴로지 전체에 동일 설정
statement 기반 복제에서 효과를 보려면 모든 노드가 같은 값을 가져야 함. 소스만 켜고 레플리카를 두면 binlog 에는 SYSDATE() 문자열이 그대로 남으므로, 레플리카가 옵션 없이 replay 하면 그 순간의 레플리카 시계가 찍혀 분기가 벌어짐. 순차 재기동 시 레플리카를 먼저 적용하는 편이 안전함.
상세
SYSDATE() 가 “동적(dynamic)” 이라는 것의 의미
mysqld --verbose --help 가 옵션 설명에 적어둔 문구 — “Since 5.0, SYSDATE() returns a `dynamic’ value different for different invocations, even within the same statement.” — 의 실제 코드 위치는 sql/item_timefunc.cc 의 Item_func_sysdate_local::get_date() 임. NOW() 가 Item_func_now_local 으로 구현되어 문장 시작 시점에 캐시된 query-start 타임스탬프를 가져오는 것과 달리, SYSDATE() 는 호출 때마다 OS 시계(my_micro_time())를 그때그때 다시 읽음.
| 함수 | 값 고정 시점 | SET TIMESTAMP 반영 | 내부 구현 (MySQL 8.4.8) |
|---|---|---|---|
NOW() | 문장 시작 시 1회 | 반영 (thd->query_start_timeval_trunc()) | Item_func_now_local (item_timefunc.h:1183) |
SYSDATE() | 매 호출 | 무시 (my_micro_time() 직호출) | Item_func_sysdate_local::get_date (item_timefunc.cc:2160) |
이 설계는 원래 “정말로 그 순간의 wall-clock 이 필요한 디버깅·벤치마크용” 으로 의도된 것임. 하지만 존재 자체가 비결정성 함수 카테고리를 만들어버렸고, 복제 안전성 논의에서 대표적인 미끄러짐 지점이 됨.
왜 statement 복제에서 위험한가
statement 기반 복제(SBR)는 소스에서 실행된 문자열 그대로 레플리카가 다시 실행하는 모델임. 시간 의존 연산을 재현 가능하게 만드는 트릭은 binlog 의 SET TIMESTAMP=... 이벤트로, 레플리카가 이 값을 THD::user_time 에 넣고 NOW() 류가 이를 참조함으로써 소스와 동일한 시간을 재현하는 방식임.
SYSDATE() 는 이 우회로를 무시하고 레플리카의 실제 벽시계를 읽음. 결과는 두 가지로 갈림.
- 소스에서
INSERT ... VALUES (SYSDATE())실행 시각 = T1 - 레플리카에서 같은 문장 replay 시각 = T1 + 복제 지연 (보통 밀리초~수초, 재시작 상황에선 분 단위)
- 두 노드 테이블에 찍히는 시각이 서로 다름 → 논리적 불일치
MySQL 이 STATEMENT 포맷에서 이 문장을 기록할 때 Note 1592 — Unsafe statement written to the binary log using statement format 경고를 남기는 이유임. 실측으로 확인한 결과는 테스트 섹션에서 다룸.
비결정성 함수 — SYSDATE() 하나가 아님
statement 복제 관점에서 “unsafe” 로 분류되는 함수·구문은 sql/parse_tree_items.cc 와 sql/item_*.cc 곳곳에서 lex->set_stmt_unsafe() (또는 thd->lex->set_stmt_unsafe()) 호출로 표시됨. SYSDATE 는 그중 대표 사례이고, 같은 계열로 자주 언급되는 것들.
| 함수·구문 | unsafe 여부 | 비고 |
|---|---|---|
NOW(), CURRENT_TIMESTAMP, CURDATE() | safe | SET TIMESTAMP 경로로 소스 값 재현 |
SYSDATE() | unsafe | 문장 내 비결정성 + SET TIMESTAMP 무시 |
UUID(), UUID_SHORT() | unsafe | 호출 시점마다 완전히 다른 값 |
RAND() (seed 없음) | unsafe | 서버마다 PRNG 상태가 다름 |
USER(), CURRENT_USER() | unsafe | 레플리카 측 복제 계정으로 바뀜 |
LOAD_FILE() | unsafe | 파일시스템 상태에 의존 |
@@version, @@hostname 류 시스템 변수 | 대부분 unsafe | 노드별로 값이 다름 |
비결정성 함수 관점에서 보면 --sysdate-is-now 는 이 목록에서 단 하나 항목을 수동으로 끄는 스위치임. UUID 나 RAND 에는 대응 옵션이 없고, 사용자가 애플리케이션 단에서 처리하거나 binlog 포맷을 ROW 로 바꾸는 수밖에 없음. 이 비대칭이 --sysdate-is-now 가 “모든 비결정성 문제의 해결책” 이 될 수 없는 이유임.
흥미로운 한계 — unsafe 경고는 옵션과 무관하게 찍힘
테스트 섹션의 실측이지만 결론에 해당하므로 여기서도 짚어둠. --sysdate-is-now 를 켠 서버에서 SYSDATE() 를 STATEMENT 포맷으로 INSERT 해도 Note 1592 unsafe 경고가 그대로 발생함. 이유는 옵션이 실제로 어디서 작동하는지를 보면 명확해짐.
핵심 코드는 sql/parse_tree_items.cc 의 PTI_function_call_nonkeyword_sysdate::do_itemize() 임. 파서가 SYSDATE() 토큰을 만나 Item 트리를 만들 때 다음 순서로 동작함.
- 무조건
lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_SYSTEM_FUNCTION)을 호출해 statement-unsafe 마킹을 켬. - 그 다음에
global_system_variables.sysdate_is_now를 검사해0이면Item_func_sysdate_local을,1이면Item_func_now_local을 만들어 트리에 끼워 넣음.
순서가 핵심임. 옵션 검사는 unsafe 마킹 이후이므로, 옵션 ON/OFF 와 상관없이 SYSDATE 토큰만 보면 unsafe 가 켜짐. 그리고 같은 함수 안의 공식 코멘트가 그 이유를 직접 적어둠 (parse_tree_items.cc:192~198):
Unlike other time-related functions, SYSDATE() is replication-unsafe because it is not affected by the TIMESTAMP variable. It is unsafe even if sysdate_is_now=1, because the slave may have sysdate_is_now=0.
소스가 sysdate_is_now=1 이어도 레플리카가 0 일 수 있으니 보수적으로 unsafe 로 마킹한다는 뜻임. 또한 binlog 에 기록되는 SQL 문자열은 옵션과 무관하게 SYSDATE() 그대로임 — 옵션은 소스 측 파서가 만드는 Item 트리만 Item_func_now_local 로 갈아끼우지, 텍스트나 binlog 페이로드를 건드리지 않기 때문임. 따라서 레플리카가 옵션 없이 돌리면 평소의 Item_func_sysdate_local 로 replay 되어 실제로 값이 어긋남.
이 두 관찰이 묶이면, “옵션을 소스에만 켜서는 안전하지 않다”는 운영 규칙과 경고 동작이 정합적으로 설명됨. 옵션은 파서 단계의 Item 매핑 스위치 이지 binlog 텍스트 치환이 아니라는 점이 포인트.
테스트
테스트 환경: MySQL 8.4.8, 두 인스턴스 — 옵션 미지정 기본 서버(대조군) 와
--sysdate-is-now만 추가한 서버. 기동 명령:docker run -d -e MYSQL_ROOT_PASSWORD=... -e MYSQL_DATABASE=testdb mysql:8.4 --sysdate-is-now
1. 문장 내 비결정성 — SLEEP 뒤 값이 밀림
SELECT NOW(), SYSDATE(), SLEEP(2), NOW(), SYSDATE();기본 서버:
+---------------------+---------------------+----------+---------------------+---------------------+
| NOW() | SYSDATE() | SLEEP(2) | NOW() | SYSDATE() |
+---------------------+---------------------+----------+---------------------+---------------------+
| 2026-04-26 03:15:51 | 2026-04-26 03:15:51 | 0 | 2026-04-26 03:15:51 | 2026-04-26 03:15:53 |
+---------------------+---------------------+----------+---------------------+---------------------+--sysdate-is-now 서버:
+---------------------+---------------------+----------+---------------------+---------------------+
| NOW() | SYSDATE() | SLEEP(2) | NOW() | SYSDATE() |
+---------------------+---------------------+----------+---------------------+---------------------+
| 2026-04-26 03:15:53 | 2026-04-26 03:15:53 | 0 | 2026-04-26 03:15:53 | 2026-04-26 03:15:53 |
+---------------------+---------------------+----------+---------------------+---------------------+기본 서버에서 두 번째 SYSDATE() 가 2초 건너뛰었지만 NOW() 는 고정된 것이 보임. 옵션을 켠 서버에서는 SYSDATE() 가 NOW() 와 한 몸으로 고정됨.
2. SET TIMESTAMP 반영 — statement replication 의 핵심
SET TIMESTAMP = 1000000000; -- 2001-09-09 01:46:40 UTC
SELECT NOW() AS now_val, SYSDATE() AS sysdate_val,
UNIX_TIMESTAMP(NOW()) AS now_unix,
UNIX_TIMESTAMP(SYSDATE()) AS sysdate_unix;기본 서버:
+---------------------+---------------------+------------+--------------+
| now_val | sysdate_val | now_unix | sysdate_unix |
+---------------------+---------------------+------------+--------------+
| 2001-09-09 01:46:40 | 2026-04-26 03:15:56 | 1000000000 | 1777173356 |
+---------------------+---------------------+------------+--------------+--sysdate-is-now 서버:
+---------------------+---------------------+------------+--------------+
| now_val | sysdate_val | now_unix | sysdate_unix |
+---------------------+---------------------+------------+--------------+
| 2001-09-09 01:46:40 | 2001-09-09 01:46:40 | 1000000000 | 1000000000 |
+---------------------+---------------------+------------+--------------+기본 서버에서 SYSDATE() 는 SET TIMESTAMP 를 완전히 무시하고 실제 시스템 시각(2026-04-26)을 돌려줬음. SET TIMESTAMP 는 레플리카가 binlog 를 replay 할 때 소스 시각을 재현하는 핵심 장치이므로, 이 한 장의 결과가 곧 “statement 복제에서 SYSDATE 가 왜 위험한가” 의 증거임. 옵션을 켜면 두 함수 모두 같은 시각을 반환함.
3. STATEMENT 포맷 binlog 경고와 실제 기록 내용
세션에서 binlog_format='STATEMENT' 를 켜고 각각 NOW() / SYSDATE() 로 한 건씩 INSERT 한 후 SHOW WARNINGS.
SET SESSION binlog_format='STATEMENT';
INSERT INTO t_sd (t) VALUES (NOW());
SHOW WARNINGS; -- NOW(): 경고 없음
INSERT INTO t_sd (t) VALUES (SYSDATE());
SHOW WARNINGS; -- SYSDATE(): Note 1592Level Code Message
Note 1592 Unsafe statement written to the binary log using statement format
since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it
uses a system function that may return a different value on the replica.--sysdate-is-now 서버에서도 정확히 같은 Note 1592 가 발생함 — 즉 옵션은 경고를 끄지 않음.
실제 binlog 에 기록된 문장을 SHOW BINLOG EVENTS 로 확인하면 두 서버 모두 INSERT INTO t_sd (t) VALUES (SYSDATE()) 문자열이 그대로 남음.
-- 기본 서버
binlog.000009 2856 Intvar 1 2888 INSERT_ID=2
binlog.000009 2888 Query 1 3016 use `testdb`; INSERT INTO t_sd (t) VALUES (SYSDATE())
-- --sysdate-is-now 서버
binlog.000002 1136 Intvar 1 1168 INSERT_ID=2
binlog.000002 1168 Query 1 1296 use `testdb`; INSERT INTO t_sd (t) VALUES (SYSDATE())바이너리 로그에는 SYSDATE 문자열이 그대로 살아 있으므로, 레플리카가 이 이벤트를 받아 replay 할 때 레플리카의 --sysdate-is-now 설정이 그 replay 결과를 결정함. 소스만 켜고 레플리카를 끄면 replay 시점 레플리카 시계가 찍혀 값이 어긋남.
4. ROW 포맷에서의 동작 — 옵션은 사실상 중립
binlog_format=ROW 에서는 INSERT ... VALUES (SYSDATE()) 가 실행될 때 소스에서 확정된 DATETIME 값이 그대로 Write_rows 이벤트에 실려 레플리카로 감. SYSDATE 의 비결정성은 이미 소스에서 해소되고, 레플리카는 값을 받기만 함. 따라서 --sysdate-is-now 는 ROW 포맷 환경에서 동작에 영향을 주지 않음 — 비결정성 함수의 값은 소스에서 단 한 번 평가되어 결과만 전파되기 때문임.
참고
- MySQL 8.4 — Server Command Options (
--sysdate-is-now) — 옵션의 공식 정의, statement 복제 안전성 표기 - MySQL 8.4 — Date and Time Functions:
NOW(),SYSDATE()— 두 함수의 “dynamic” vs “statement-time” 차이 원문 - MySQL 8.4 — Replication and System Functions (determinism) —
SET TIMESTAMP기반 시간 함수 재현 모델, SYSDATE 가 예외인 이유 - MySQL 8.4 — Determination of Safe and Unsafe Statements in Binary Logging — Note 1592 분류표,
UUID()·RAND()등 비결정성 함수 전체 목록 - MySQL 8.4 —
binlog_format— 8.0.34+ 에서 변수 자체 deprecated, ROW 가 유일한 미래 - MySQL 8.4.8 소스 코드 (
mysql-server태그mysql-8.4.8)sql/mysqld.cc:10826—sysdate-is-now가my_long_options[]의MyOption으로 등록 (GET_BOOL,NO_ARG, 기본0). 시스템 변수 테이블이 아니라 명령행 옵션 테이블에 들어가 있어@@sysdate_is_now조회·SET GLOBAL모두 불가sql/system_variables.h:339—bool sysdate_is_now;가global_system_variables구조체 멤버로 잡혀 옵션값을 보관sql/parse_tree_items.cc:188~210—PTI_function_call_nonkeyword_sysdate::do_itemize(). unsafe 마킹을 무조건 켠 다음 옵션값에 따라Item_func_sysdate_local또는Item_func_now_local을 생성. 192~198 코멘트에 “It is unsafe even if sysdate_is_now=1, because the slave may have sysdate_is_now=0.” 명시sql/item_timefunc.h:1183, 1223—class Item_func_now_local : public Item_func_now,class Item_func_sysdate_local final : public Item_datetime_func선언sql/item_timefunc.cc:2100—Item_func_now_local::store_in()등 NOW 계열은thd->query_start_timeval_trunc(decimals)로 캐시된 query-start 타임스탬프 사용sql/item_timefunc.cc:2160—Item_func_sysdate_local::get_date()는my_micro_time()을 직접 호출해 OS 시계를 그때그때 다시 읽음