한 줄 요약: PK 없는 InnoDB 테이블이 일으키는 RBR 복제 지연·운영 도구 비호환을 MySQL 8.0 이 두 가지 정반대 방향의 변수로 풀어줌.

먼저 도입된 sql_require_primary_key (8.0.13) 는 PK 빠진 CREATE/ALTER TABLE 을 거부함. 그 뒤를 잇는 GIPK (sql_generate_invisible_primary_key, 8.0.30) 는 반대 방향으로 PK 없는 새 InnoDB 테이블에 my_row_idINVISIBLE·AUTO_INCREMENT 로 자동 부여함.

두 변수는 함께 켤 수 있고, 레플리카 측 적용 정책은 채널 옵션 REQUIRE_TABLE_PRIMARY_KEY_CHECK 가 따로 통제함.

개요

InnoDB 는 클러스터 인덱스 구조라 PK 가 없는 테이블이면 내부적으로 DB_ROW_ID (6바이트 hidden 컬럼)를 클러스터 인덱스 키로 씀. 이 값은 binlog 에 기록되지 않기 때문에 Row-Based Replication 환경에서 레플리카가 변경 대상 행을 찾을 때 PK·세컨더리 인덱스 대신 풀스캔·HASH_SCAN 경로로 빠짐. 이게 PK 없는 테이블에서 자주 발생하는 복제 지연의 근본 원인임.

MySQL 8.0 은 이 문제에 두 가지 정반대 방향의 옵션을 차례로 도입함 — 먼저 PK 없으면 DDL 자체를 거부 하는 sql_require_primary_key (8.0.13), 그 뒤에 PK 없으면 자동으로 붙여주는 GIPK (8.0.30). 두 변수는 충돌하지 않고 함께 켤 수 있음. GIPK 가 먼저 PK 를 붙이면 sql_require_primary_key 검사도 자연스럽게 통과함.

레플리카는 이 정책을 자동 상속하지 않음. 어느 쪽 정책을 받을지는 채널별 옵션 REQUIRE_TABLE_PRIMARY_KEY_CHECK 로 별도 통제함.

변수·옵션도입기본값정책·동작
sql_require_primary_key8.0.13OFFPK 빠진 CREATE/ALTER TABLE 을 에러 3750 으로 거부
sql_generate_invisible_primary_key8.0.30 (Release Notes)OFFPK 없는 새 InnoDB 테이블에 my_row_id (BIGINT UNSIGNED AUTO_INCREMENT INVISIBLE) 자동 PK 부여
REQUIRE_TABLE_PRIMARY_KEY_CHECKCHANGE REPLICATION SOURCE TO 채널 옵션STREAM레플리카가 위 두 정책을 어떻게 받을지 결정 — STREAM·ON·OFF·GENERATE

결론

두 옵션의 정책 비교

구분sql_require_primary_keyGIPK (sql_generate_invisible_primary_key)
도입 버전8.0.13+8.0.30+
정책 방향PK 없으면 거부 (에러 3750)PK 없으면 자동 부여 (서버가 보충)
적용 시점모든 CREATE/ALTER TABLE새 InnoDB 테이블 생성 시
기존 테이블변수 ON 직후엔 그대로, 이후 ALTER 부터 검사 적용소급 적용 X (ALTER TABLE 직접 필요)
부여되는 PK없음 (개발자가 명시)my_row_id BIGINT UNSIGNED AUTO_INCREMENT INVISIBLE
함께 켤 때동일GIPK 가 먼저 PK 부여 → sql_require_primary_key 검사 통과

언제 쓰는가

  • sql_require_primary_key 가 어울리는 상황 — 정책적으로 PK 가 빠진 DDL 을 아예 받지 말아야 하는 환경 (감사·컴플라이언스, 또는 PK 누락이 운영에 들어오는 것 자체를 휴먼 게이트로 막고 싶은 팀). 자동 보충 없이 책임을 개발자에게 명시적으로 떠넘김.
  • GIPK 가 어울리는 상황 — 레거시·서드파티 툴이 종종 PK 없이 테이블을 만들고, 그게 RBR 복제 지연이나 pt-online-schema-change/gh-ost 호환 문제로 번지는 환경. 개발자 부담 없이 서버가 자동으로 메워주는 쪽. 신규 테이블에만 적용되므로 일괄 재생성이 전제.
  • 둘 다 켤 때 (가장 안전) — 기본 정책은 PK 명시 강제 로 두되, 깜빡한 케이스는 GIPK 가 자동 보충. 단 GIPK 가 추가한 my_row_idSHOW CREATE TABLE·mysqldump 결과에 나타나는 점은 팀 합의 필요. 레플리카에는 REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE 와 조합 가능.

운영 시 체크

  1. GIPK 는 새로 생성되는 테이블에만 적용됨. 기존 테이블에는 별도 ALTER TABLE ... ADD COLUMN ... PRIMARY KEY 가 필요.
  2. 스키마 관리 도구(Liquibase, Flyway 등) 의 diff 가 달라질 수 있음. SHOW CREATE TABLE 결과에 my_row_id 가 추가되므로 팀 마이그레이션 전에 도구 동작 테스트 필요.
  3. 레플리카에서도 GIPK 를 자동 생성하려면 CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE 가 별도로 필요함. 소스에서 ON 이어도 레플리카는 자동 상속하지 않음.
  4. GIPK ON 상태에서 CREATE TABLE ... SELECT 의 statement-based 복제는 미지원. 8.4 공식 docs 가 명시하는 제약으로, binlog_format=STATEMENT/MIXED 환경에서 CTAS 가 들어오면 복제가 깨짐. ROW 포맷이면 무관하나, SBR 잔존 환경에서 GIPK 를 켤 때는 사전 점검 필요.

GIPK 설정 확인 및 활성화

(sql_require_primary_key 도 SET 패턴은 동일함 — 변수명만 바꿔서 적용. 아래는 GIPK 기준.)

현재 설정 확인

SHOW VARIABLES LIKE 'sql_generate_invisible_primary_key';
+------------------------------------+-------+
| Variable_name                      | Value |
+------------------------------------+-------+
| sql_generate_invisible_primary_key | OFF   |
+------------------------------------+-------+

활성화

-- 세션 단위 (테스트·임시 적용)
SET sql_generate_invisible_primary_key = ON;
 
-- 전역 (온라인, 재시작 시 원복)
SET GLOBAL sql_generate_invisible_primary_key = ON;

영구 반영은 my.cnf 에 기재:

[mysqld]
sql_generate_invisible_primary_key = ON

MySQL 8.4.8 에서 직접 확인 — SET GLOBAL 은 즉시 반영되나 my.cnf 수정이 없으면 재시작 시 원복됨.


GIPK 동작 확인

아래 모든 출력은 MySQL 8.4.8 컨테이너에서 직접 실행한 결과임. sql_require_primary_key 의 동작(에러 3750 거부)은 아래 레플리카 설정ON 시나리오에서 같은 형태로 재현됨.

대조군 — GIPK OFF 에서는 PK 없이 생성

SET sql_generate_invisible_primary_key = OFF;
CREATE TABLE without_gipk (c1 INT, c2 VARCHAR(100));
SHOW CREATE TABLE without_gipk\G
*************************** 1. row ***************************
       Table: without_gipk
Create Table: CREATE TABLE `without_gipk` (
  `c1` int DEFAULT NULL,
  `c2` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

PK 없이 생성됨 — InnoDB 내부적으로 DB_ROW_ID 를 씀.

GIPK ON — my_row_id PK 자동 추가

SET sql_generate_invisible_primary_key = ON;
CREATE TABLE with_gipk (c1 INT, c2 VARCHAR(100));
SHOW CREATE TABLE with_gipk\G
*************************** 1. row ***************************
       Table: with_gipk
Create Table: CREATE TABLE `with_gipk` (
  `my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT /*!80023 INVISIBLE */,
  `c1` int DEFAULT NULL,
  `c2` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`my_row_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

my_row_idBIGINT UNSIGNED NOT NULL AUTO_INCREMENT 이면서 INVISIBLE 이고 PK 로 지정됨. 주석 /*!80023 INVISIBLE */ 는 MySQL 8.0.23 이상에서만 해석되는 버전 조건부 힌트임.

INVISIBLE 컬럼 동작 — SELECT * 에서는 숨겨짐

INSERT INTO with_gipk (c1, c2) VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma');
SELECT * FROM with_gipk;
+------+-------+
| c1   | c2    |
+------+-------+
|    1 | alpha |
|    2 | beta  |
|    3 | gamma |
+------+-------+

my_row_id 값은 지정하지 않았으나 AUTO_INCREMENT 로 채워졌고, SELECT * 에는 나타나지 않음. 기존 애플리케이션 코드가 그대로 동작하는 이유임.

명시 조회 — INVISIBLE 컬럼도 볼 수 있음

SELECT my_row_id, c1, c2 FROM with_gipk;
+-----------+------+-------+
| my_row_id | c1   | c2    |
+-----------+------+-------+
|         1 |    1 | alpha |
|         2 |    2 | beta  |
|         3 |    3 | gamma |
+-----------+------+-------+

컬럼명을 명시하면 조회 가능. 1부터 순차 증가하며, 이후 RBR 환경에서 binlog 에도 이 값이 실리므로 레플리카가 행을 O(log n) 으로 바로 찾을 수 있음.


레플리카 설정 (REQUIRE_TABLE_PRIMARY_KEY_CHECK)

먼저 한 짝인 sql_require_primary_key 부터 — GIPK 가 PK 없으면 자동으로 붙임 정책이라면 이쪽은 PK 없으면 DDL 자체를 막음 정책임. MySQL 8.0.13 도입, 기본값 OFF, Global·Session 동적 변경 가능.

ON 이면 PK 빠진 CREATE TABLE/ALTER TABLE 을 에러 3750 으로 거부함. GIPK 와 함께 ON 으로 켜도 GIPK 가 먼저 PK 를 붙여 통과시키므로 충돌 없음.

REQUIRE_TABLE_PRIMARY_KEY_CHECK레플리카가 소스의 sql_require_primary_key 를 어떻게 받을지 결정하는 채널별 옵션임. 소스에서 GIPK 가 활성화돼 있어도 레플리카는 이 옵션을 자동 상속하지 않음.

아래 결과는 모두 MySQL 8.4.8 × 2 (소스·레플리카, GTID, Row-Based Replication) 토폴로지에서 직접 실측한 값임.

값과 동작

동작실측 결과
STREAM기본값. 트랜잭션별로 소스의 sql_require_primary_key 값을 그대로 따름소스 기본값(sql_require_primary_key=OFF)에서는 PK 없는 테이블이 그대로 복제됨
OFF소스 설정 무시, PK 없는 DDL 허용레플리카에 PK 없는 테이블로 생성
ON소스 설정 무시, PK 없는 DDL 은 에러에러 3750 으로 SQL thread 중단, 테이블 생성되지 않음
GENERATEPK 없는 DDL 을 받으면 레플리카에서 my_row_id GIPK 를 붙여 생성소스엔 PK 없고 레플리카에만 my_row_id — 토폴로지 간 구조가 달라짐

현재 값은 performance_schema 에서 조회:

SELECT REQUIRE_TABLE_PRIMARY_KEY_CHECK
FROM performance_schema.replication_applier_configuration;

값 변경:

STOP REPLICA;
CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE;
START REPLICA;

ON — SQL thread 중단

레플리카를 ON 으로 걸어둔 뒤 소스에서 PK 없는 테이블을 만들면 레플리카는 에러 3750 으로 SQL thread 를 멈춤 (performance_schema.replication_applier_status_by_worker.LAST_ERROR_MESSAGE).

Worker 1 failed executing transaction '...:14' at source log mysql-bin.000003,
end_log_pos 2050; Error 'Unable to create or change a table without a primary
key, when the system variable 'sql_require_primary_key' is set. Add a primary
key to the table or unset this variable to avoid this message. ...' on query.
Default database: 'testdb'. Query: 'CREATE TABLE t_on (c1 INT, c2 VARCHAR(20))'

복구는 문제 GTID 를 빈 트랜잭션으로 덮어 건너뛰는 방식:

STOP REPLICA;
SET GTID_NEXT='<문제 GTID>';
BEGIN; COMMIT;
SET GTID_NEXT=AUTOMATIC;
START REPLICA;

건너뛴 DDL 은 복구 후 수동으로 PK 를 붙여 재실행해야 함.

GENERATE — 소스·레플리카 구조가 달라짐

소스가 GIPK OFF 인 상태에서 PK 없이 테이블을 만들어도, GENERATE 를 건 레플리카는 자동으로 my_row_id 를 붙여 생성함.

소스의 SHOW CREATE TABLE t_gen:

CREATE TABLE `t_gen` (
  `c1` int DEFAULT NULL,
  `c2` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

레플리카:

CREATE TABLE `t_gen` (
  `my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT /*!80023 INVISIBLE */,
  `c1` int DEFAULT NULL,
  `c2` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`my_row_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

레플리카가 자체 AUTO_INCREMENT 로 채운 값:

+-----------+----+----+
| my_row_id | c1 | c2 |
+-----------+----+----+
|         1 | 10 | x  |
|         2 | 20 | y  |
|         3 | 30 | z  |
+-----------+----+----+

GENERATE 운영 시 주의:

  1. 소스·레플리카 스키마 분기 — 소스엔 my_row_id 가 없고 레플리카에만 있음. 이 상태가 두 가지 경로로 문제를 만듦.

    • 레플리카가 새 소스로 승격되면 binlog 에 my_row_id 가 실려 하위 레플리카 RBR 이 깨질 수 있음.
    • Liquibase/Flyway 같은 diff 도구를 레플리카에 돌리면 차이를 오탐함.

    안전한 길은 토폴로지 전체를 동일하게 GENERATE 로 맞추거나, 차라리 소스에서 sql_generate_invisible_primary_key = ON 을 켜 스키마를 통일하는 쪽임.

  2. my_row_id 값은 노드 간에 일치하지 않음 — 각 레플리카가 독립적으로 AUTO_INCREMENT 를 채우므로 같은 행이라도 값이 다름. 애플리케이션이 my_row_id 를 식별자로 쓰면 안 됨.

언제 어느 값을 쓰나

  • STREAM (기본) — 토폴로지 전체가 동일한 sql_require_primary_key 설정을 공유한다는 확신이 있을 때. 일반적인 운영 구성.
  • ON — 소스 설정과 무관하게 레플리카에서 독립적으로 PK 강제 정책을 걸고, 위반 시 에러로 막고 DBA 가 수동 개입하는 운영을 원할 때.
  • GENERATE — PK 없는 테이블이 섞여 들어오는 환경에서 복제 지연만은 해소하고 싶을 때. 위 세 가지 주의점을 감당 가능한 경우에만.
  • OFF — 소스 설정을 무시하고 PK 없는 DDL 을 무조건 허용. 일반 운영에서는 권장되지 않음.

MySQL 8.4.8 × 2 (GTID, Row-Based Replication) 실측. 기본값이 STREAM 이며 OFF 가 아니라는 점, ON 의 에러 코드가 3750 이라는 점, GENERATE 는 레플리카에만 my_row_id 를 붙여 소스·레플리카 스키마가 의도적으로 어긋난다는 점을 확인함.


참고