한 줄 요약: --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-nowmy.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 포맷(기본값)에서의 영향없음 — 실제 값이 이미 기록됨없음 — 동일없음

핵심은 세 가지임.

  1. SYSDATE()NOW() 와 달리 문장 스코프의 동결된 시각이 아니라 호출 순간의 실제 시각을 돌려줌.
  2. 그래서 SET TIMESTAMP 를 기반으로 소스 시각을 재현하려는 statement 복제와 호환되지 않음.
  3. --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 이면 OK

3단계 — 복제 토폴로지 전체에 동일 설정

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.ccItem_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() 는 이 우회로를 무시하고 레플리카의 실제 벽시계를 읽음. 결과는 두 가지로 갈림.

  1. 소스에서 INSERT ... VALUES (SYSDATE()) 실행 시각 = T1
  2. 레플리카에서 같은 문장 replay 시각 = T1 + 복제 지연 (보통 밀리초~수초, 재시작 상황에선 분 단위)
  3. 두 노드 테이블에 찍히는 시각이 서로 다름 → 논리적 불일치

MySQL 이 STATEMENT 포맷에서 이 문장을 기록할 때 Note 1592 — Unsafe statement written to the binary log using statement format 경고를 남기는 이유임. 실측으로 확인한 결과는 테스트 섹션에서 다룸.


비결정성 함수 — SYSDATE() 하나가 아님

statement 복제 관점에서 “unsafe” 로 분류되는 함수·구문은 sql/parse_tree_items.ccsql/item_*.cc 곳곳에서 lex->set_stmt_unsafe() (또는 thd->lex->set_stmt_unsafe()) 호출로 표시됨. SYSDATE 는 그중 대표 사례이고, 같은 계열로 자주 언급되는 것들.

함수·구문unsafe 여부비고
NOW(), CURRENT_TIMESTAMP, CURDATE()safeSET 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.ccPTI_function_call_nonkeyword_sysdate::do_itemize() 임. 파서가 SYSDATE() 토큰을 만나 Item 트리를 만들 때 다음 순서로 동작함.

  1. 무조건 lex->set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_SYSTEM_FUNCTION) 을 호출해 statement-unsafe 마킹을 켬.
  2. 그 다음에 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 1592
Level 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:10826sysdate-is-nowmy_long_options[]MyOption 으로 등록 (GET_BOOL, NO_ARG, 기본 0). 시스템 변수 테이블이 아니라 명령행 옵션 테이블에 들어가 있어 @@sysdate_is_now 조회·SET GLOBAL 모두 불가
    • sql/system_variables.h:339bool sysdate_is_now;global_system_variables 구조체 멤버로 잡혀 옵션값을 보관
    • sql/parse_tree_items.cc:188~210PTI_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, 1223class Item_func_now_local : public Item_func_now, class Item_func_sysdate_local final : public Item_datetime_func 선언
    • sql/item_timefunc.cc:2100Item_func_now_local::store_in() 등 NOW 계열은 thd->query_start_timeval_trunc(decimals) 로 캐시된 query-start 타임스탬프 사용
    • sql/item_timefunc.cc:2160Item_func_sysdate_local::get_date()my_micro_time() 을 직접 호출해 OS 시계를 그때그때 다시 읽음