(KO) CUBRID SERIAL — 카탈로그 상태와 캐시 값을 가진 시퀀스 / AUTO_INCREMENT 서브시스템
목차:
학술적 배경
섹션 제목: “학술적 배경”시퀀스 (또는 serial) 은 데이터베이스의 gap을 허용하는 대리키 생성기
다. nextval 호출마다 엄격히 단조 증가하지만 — 반드시 연속적이지는 않은 숫자를 돌려주는 stateful 객체다. 시퀀스 객체는 관계 모델이 식 안의 부수
효과를 금지한다는 원칙에서 빠져나가는 교과서적 탈출구다. nextval('s') 를
읽는 SELECT는 s 를 변형시키므로 idempotent 하지 않다. Database System
Concepts (Silberschatz/Korth/Sudarshan, 7판, 5장 §5.3) 가 시퀀스를 정확히
이 언어로 도입한다. “유일한 숫자 값을 분배하는 것이 유일한 목적인 특수
객체 이며, 설계상 시퀀스가 분배한 값은 둘러싼 트랜잭션이 abort 하더라도
회수되지 않는다” 고 명시한다.
마지막 절이 설계의 하중을 받치는 부분이다. 여기서 세 가지 교과서적 절충이 따라 나오고, 그 절충이 본 문서의 골격을 만든다.
-
gapless vs. gap-permissive 의미론. gapless 시퀀스는 호출 트랜잭션 의 전체 수명 동안 잠겨야 한다. 값
N을 소비한 트랜잭션이 rollback 되면N을 되돌려 놔야 하기 때문이다. 이는 동시성과 양립할 수 없다. 매nextval이 직렬화 지점이 된다. 모든 주요 엔진 — Oracle, PostgreSQL, MySQL, CUBRID — 이 gap-permissive 의미론을 고른다. 시퀀스는 짧게 유지되는 lock 아래서 전진하고, 증가는 둘러싼 사용자 트랜잭션과 독립적 으로 commit 되며, rollback 된 값은 그저 건너뛴다. -
캐시된 vs. 캐시되지 않은 할당. gap-permissive 시퀀스도 순진하게 구현하면
nextval마다 heap 페이지 갱신 비용이 든다. 표준 최적화는 캐싱 이다. 한 번의nextval이 메모리 안에cached_num개의 값 블록을 예약하고, 그 블록의 high water mark 만 카탈로그에 기록한다. 같은 블록 내의 후속 호출은 순수한 인메모리 연산이다. 대가는 크래시 시점과 캐시 축출 시점의 더 큰 gap 이다 (마지막으로 소비한 값과 high water mark 사이의 모든 값이 사라진다). -
카탈로그 쓰기의 트랜잭션성. 시퀀스가 전진하면 영속 행이 변형된다. 그 변형이 사용자 트랜잭션을 함께 탔다면, 사용자가 rollback 할 때 시퀀스도 함께 rollback 된다. gap-permissive 의미론을 무력화한다. 교과서적 답은 카탈로그 쓰기를 system top operation 안에서 수행 하는 것이다 (Oracle 의 autonomous transaction, PostgreSQL 의 system transaction). 사용자가 아직 진행 중일 때 commit 되는 중첩 트랜잭션 으로, 시퀀스의 내구성을 사용자 내구성에서 떼어 낸다.
CUBRID 은 세 절충을 모두 명시적으로 구현한다. cached_num 컬럼은 serial
별 선택이고, 카탈로그 쓰기는 log_sysop_start / log_sysop_commit 안에서
일어나며, nextval 은 트랜잭션 길이의 lock 이 아닌 짧은 X_LOCK 아래에서
serial OID 에 걸린 채로 반환된다. 본 문서의 나머지는 이 결정들이 소스에서
어떻게 드러나는지를 추적한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”동시성 환경에서 대리키를 만들고 싶은 모든 관계형 엔진은 비슷한 패턴 묶음 에 손을 뻗는다.
한 행 테이블로서의 시퀀스
섹션 제목: “한 행 테이블로서의 시퀀스”시퀀스는 원시 타입이 아니다. 모든 엔진이 시퀀스를 어떤 카탈로그 비슷한 관계 안의 한 행 테이블 로 구현하고, 그 행을 읽고 갱신하는 SQL 레벨 함수 몇 개를 노출한다.
- PostgreSQL 은 시퀀스를 위한 별도의 관계 종류 (
relkind = 'S') 를 통째로 할당한다. 매CREATE SEQUENCE s가 실제 heap 을 만들고 그 안에last_value,log_cnt,is_called를 담은 튜플 하나가 들어간다.nextval('s')는 특수 access path 를 사용한다 (commands/sequence.c의SequenceNextval). 페이지를 pin 하고 값을 bump 하고 WAL-log 한 뒤 반환한다. 호출 트랜잭션의 스냅샷 바깥에서. - Oracle 은 시퀀스 상태를 데이터 사전의
seq$에 두고,s.NEXTVAL,s.CURRVAL을 어떤 식에서나 해석되는 의사 컬럼으로 노출한다. 캐시 크기 (CACHE n) 는 크래시 안전성과 속도를 절충하고,NOCACHE는 매NEXTVAL마다 사전을 갱신한다. 행 변형은 별도 access path 가 아니라 재귀 SQL 이 한다. - MySQL/InnoDB 은 시퀀스를 일급 객체로 노출하지 않는다. AUTO_INCREMENT
카운터는
dict_table_t안의 테이블별 인메모리 카운터로 살고, 재시작 후 최초 사용 시점에MAX(col) + 1스캔으로 재구성된 파생 값으로 산다 (innodb_autoinc_lock_mode로 구성 가능). lock-mode 파라미터는 정확히 gapless-vs-gap-permissive 손잡이가 사용자에게 노출된 형태다.
CUBRID 의 위치
섹션 제목: “CUBRID 의 위치”CUBRID 은 Oracle 에 가장 가깝다. 단일 시스템 클래스 _db_serial 이
시퀀스마다 한 행과 AUTO_INCREMENT 컬럼마다 한 행을 들고 있고,
unique_name 위의 unique B-tree 로 SQL 식별자로 주소 가능하다.
nextval(s) / currval(s) 함수는 진짜 SQL 식이며, XASL T_NEXT_VALUE /
T_CURRENT_VALUE 연산자로 컴파일된다 (fetch.c 참조). 이 연산자가 서버
측의 xserial_get_next_value / xserial_get_current_value 를 호출한다.
Oracle 대비 결정적인 차이는, CUBRID 이 별도의 사설 시퀀스 캐시 구조를
만들지 않는다는 점이다. 인메모리 상태는 serial OID 로 키된 SERIAL_CACHE_ENTRY
객체의 작은 해시 테이블이고, 권위 있는 상태는 항상 _db_serial heap 안에
산다. 즉 모든 serial — 명시적이든 AUTO_INCREMENT 든 — 같은 저장 모양과
같은 코드 경로를 가진다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”_db_serial — 카탈로그 한 행, 12개 이상의 컬럼
섹션 제목: “_db_serial — 카탈로그 한 행, 12개 이상의 컬럼”_db_serial 시스템 클래스는 데이터베이스 생성 시점에 고정된 컬럼 목록과
함께 부트스트랩된다. 컬럼 이름은 storage_common.h 안의 매크로로 살아
있어 클라이언트 (DDL) 와 서버 (executor) 가 같은 문자열을 참조한다.
// SERIAL_ATTR_* — src/storage/storage_common.h#define SERIAL_ATTR_UNIQUE_NAME "unique_name"#define SERIAL_ATTR_NAME "name"#define SERIAL_ATTR_OWNER "owner"#define SERIAL_ATTR_CURRENT_VAL "current_val"#define SERIAL_ATTR_INCREMENT_VAL "increment_val"#define SERIAL_ATTR_MAX_VAL "max_val"#define SERIAL_ATTR_MIN_VAL "min_val"#define SERIAL_ATTR_START_VAL "start_val"#define SERIAL_ATTR_CYCLIC "cyclic"#define SERIAL_ATTR_STARTED "started"#define SERIAL_ATTR_CLASS_NAME "class_name"#define SERIAL_ATTR_ATTR_NAME "attr_name"#define SERIAL_ATTR_CACHED_NUM "cached_num"#define SERIAL_ATTR_COMMENT "comment"이 컬럼 중 몇 개는 즉시 설명할 만하다. serial.c 의 알고리즘이 이 컬럼들
에 직접 키를 맞추기 때문이다.
current_val. serial 이 캐시되어 있을 때는 commit 된 high water mark, 캐시되어 있지 않을 때는 마지막으로 반환된 값 이다. 캐시 경계를 넘은 매nextval호출 (또는 serial 이 캐시되어 있지 않을 때마다) 후, 새current_val이 이 컬럼에 다시 쓰인다. 결정적인 점 — serial 이cached_num = N으로 캐시되어 있을 때, 디스크 위에 있는 값은 캐시 엔트리의 인메모리last_cached_val이다. 즉 가장 최근에 예약한 블록의 마지막 값 이다. 따라서 행을 새로 읽는 캐시 미스는 이전 캐시가 첫 번째 값만 쓰고 끝났더라도 이전 캐시가 예약한 모든 값 너머 에서 시작한다.started. 첫 nextval 과 그 이후의 모든 호출을 구분하는 0/1 sentinel 이다. 첫 호출에서 CUBRID 은current_val그 자체 를 반환하고 그 후에야 전진한다. 이 방식 덕분에START WITH 1이 1 + increment 가 아닌 1 을 먼저 돌려 준다. 첫 호출 후started는 1 로 뒤집히고 영속 된다.cached_num. 0 또는 1 은 캐시 없음, 매 호출이 heap 을 친다 를 뜻한다. 2 이상은 그 만큼의 값 블록을 가진 인메모리 캐시 풀을 활성화한다. DDL 은(max_val - min_val) / |inc_val|안에 들어가지 않는 값을 거부하므로, 완전히 소진된 캐시는 항상 cycle / overflow 경계와 정렬된다.class_name/attr_name.CREATE TABLE t (id INT AUTO_INCREMENT)로 부터 합성된 AUTO_INCREMENT serial 의 경우에 한해 NULL 이 아니다. ALTER / RENAME 이 사용하는 back-link 이며, 사용자가 AUTO_INCREMENT serial 을 직접 삭제하는 것을 막는 DROP 가드의 근거다.
두 싱글톤이 SQL 세계와 행을 잇는다. 클래스 자체는 oid_Serial_class_oid
(storage/oid.c 안의 부트 시 oid_Reserved_class_table) 에 저장된 고정
OID 를 가지고, unique_name 위의 unique B-tree 는 pk_db_serial_unique_name
이라 명명되어 서버 시작 시 BTID serial_Cached_btid
(serial_cache_index_btid) 에 한 번 캐시된다. 따라서 DDL 경로는
db_find_unique 로 식별자를 OID 로 번역하고, 런타임 경로는 캐시된 BTID
로 unique_name 에서 행으로 곧장 걸어간다.
단일 serial 의 상태 기계
섹션 제목: “단일 serial 의 상태 기계”stateDiagram-v2
[*] --> Created: CREATE SERIAL / AUTO_INCREMENT 컬럼
Created --> NotStarted: started=0\ncurrent_val=start_val 로 행 삽입
NotStarted --> Started: 첫 nextval()\ncurrent_val 반환\nstarted=1 로 뒤집기
Started --> Cached: nextval(), cached_num>1\ncached_num 만큼의 블록 예약
Started --> Persisted: nextval(), cached_num<=1\n매 호출마다 current_val 갱신
Cached --> Cached: 블록 안의 nextval()\n순수 인메모리 연산
Cached --> CacheMiss: 블록 소진\nheap 의 high-water 전진\n캐시 재충전
CacheMiss --> Cached
Cached --> Persisted: xserial_decache()\nDDL 또는 축출
Persisted --> Persisted: nextval()\n매번 current_val 갱신
Persisted --> Created: ALTER ... RESTART\n또는 rename 시 reset
Cached --> [*]: DROP SERIAL\n또는 AI 의 경우 테이블 drop
Persisted --> [*]: DROP SERIAL
Cached → CacheMiss → Cached 전이가 nextval 을 싸게 유지하는 최적화다.
대부분의 호출은 인메모리 경로를 떠나지 않으며 heap 을 만지지도 않는다.
서버 측 nextval 흐름
섹션 제목: “서버 측 nextval 흐름”flowchart TD
A["fetch_peek_dbval()<br/>case T_NEXT_VALUE<br/>(query/fetch.c)"] --> B["xserial_get_next_value(thread_p, oid, cached_num, num_alloc, GENERATE_SERIAL)"]
B --> C{cached_num <= 1?}
C -->|예| D["xserial_get_next_value_internal()<br/>행 읽기, N번째 값 계산,<br/>current_val 갱신,<br/>started 뒤집기"]
C -->|아니오| E["cache_pool_mutex 잠그기<br/>mht_get(oid)"]
E --> F{캐시 hit?}
F -->|예| G["serial_get_next_cached_value()<br/>cur_val 과 last_cached_val 비교"]
G --> H{블록 소진?}
H -->|아니오| I["인메모리에서 다음 값 계산<br/>결과로 복사"]
H -->|예| J["serial_update_cur_val_of_serial():<br/>log_sysop_start 아래에서<br/>heap 갱신으로 블록 재충전"]
J --> I
F -->|아니오| K["lock_object(oid, X_LOCK, COND)"]
K --> L{허용?}
L -->|아니오| M["mutex 해제,<br/>UNCOND lock_object,<br/>try_again 으로 재시도"]
M --> E
L -->|예| N["xserial_get_next_value_internal()"]
N --> O["캐시 엔트리 할당,<br/>mht 에 등록,<br/>X_LOCK 해제"]
I --> P["mutex 해제"]
D --> P
O --> P
P --> Q{is_auto_increment?}
Q -->|예| R["xsession_set_cur_insert_id()"]
Q -->|아니오| S["반환"]
R --> S
이 흐름에서 Postgres 독자의 직관과 다르게 보이는 두 가지가 있다.
- mutex 가 보호하는 것은 해시 테이블이지, 값이 아니다. 보호된
임계 영역은
mht_get/mht_put짝의 lookup-and-update 와 엔트리 위의 산술 연산이다. heap 위의 serial 행 은 OID 위의 보통의 CUBRIDX_LOCK으로 보호된다. 다른 어떤 heap 객체와 똑같이. 시퀀스는 행이 타는 같은 lock 매니저를 탄다. try_again재시도 패턴. 조건부lock_object가 실패하면 (지금 다른 워커가 이 serial 을 전진시키고 있는 중) 워커는 lock 을 무조건적으로 잡기 전에 캐시 mutex 를 떨어뜨리고, 그러고 나서 캐시 를 다시 검사하기 위해 루프로 돌아간다. 행 lock 을 기다리는 동안 캐시 풀 mutex 를 들고 있으면 데이터베이스의 다른 모든 시퀀스를 굶주리게 만들 것이다. 이 패턴이 그것을 막는다.
AUTO_INCREMENT 가 serial 위에 어떻게 매핑되는가
섹션 제목: “AUTO_INCREMENT 가 serial 위에 어떻게 매핑되는가”CUBRID 은 별도의 auto-increment 서브시스템을 가지지 않는다. DDL 이
AUTO_INCREMENT 컬럼을 만들 때, 스키마 레이어는 unique_name 이 매크로
하나로 구성된 serial 을 합성한다.
// SET_AUTO_INCREMENT_SERIAL_NAME — src/object/transform.h#define SET_AUTO_INCREMENT_SERIAL_NAME(SR_NAME, CL_NAME, AT_NAME) \ sprintf ((SR_NAME), "%s_ai_%s", (CL_NAME), (AT_NAME))#define AUTO_INCREMENT_SERIAL_NAME_EXTRA_LENGTH (4) /* "_ai_" */do_create_auto_increment_serial (execute_statement.c) 가 그러고 나서
적절한 기본값을 고른다. inc_val = 1, start_val = 1, cached_num = 0,
cyclic = 0 — 그리고 SQL 타입 도메인으로부터 max_val 을 계산한다
(INTEGER 의 경우 INT32_MAX, BIGINT 의 경우 INT64_MAX,
NUMERIC(p,0) 의 경우 모두 9 인 numeric 등). 컬럼 디스크립터의
auto_increment 필드가 결과 MOP 으로 설정된다. 이 시점부터 AUTO_INCREMENT
경로는 명시적 CREATE SERIAL 과 기계적으로 동일하다. 같은 _db_serial
행, 같은 xserial_get_next_value 서버 호출. 유일한 분기는 executor 에
있다. INSERT 경로가 넘기는 is_auto_increment 플래그가
xsession_set_cur_insert_id 로 하여금 세션의 LAST_INSERT_ID() 등가
상태를 갱신하게 만든다.
_db_serial.class_name 안의 클래스 이름 back-link 가 이 구조를 DDL 아래
에서 안전하게 만든다. do_drop_serial 이 class_name 을 읽어 — NULL 이
아니면 명시적 DROP SERIAL 은 ER_QPROC_CANNOT_UPDATE_SERIAL 로 거부된다.
오직 DROP TABLE 만이 AI serial 을 풀어 줄 수 있다. ALTER TABLE … RENAME
COLUMN 은 do_update_auto_increment_serial_on_rename 으로 흐른다.
이 함수가 같은 행 위에서 unique_name, name, class_name, attr_name
을 다시 쓴다 (workspace MOP 캐시를 무효화하기 위해 ws_decache 후).
캐시 메커니즘
섹션 제목: “캐시 메커니즘”캐시는 서버 측에서 프로세스-로컬이다. 구성은 다음과 같다.
-
풀 (
SERIAL_CACHE_POOL serial_Cache_pool) 이 해시 테이블ht, 엔트리 의 free list, 미리 할당된SERIAL_CACHE_AREA블록의 단일 연결 리스트 를 들고 있다. 각 area 는 한 번에NCACHE_OBJECTS = 100개의 엔트리를 할당해 free list 로 엮는다. 새 area 는 필요할 때 끝에 추가된다. -
활성 serial 마다
SERIAL_CACHE_ENTRY하나:// SERIAL_CACHE_ENTRY — src/query/serial.cstruct serial_entry{OID oid; /* serial object identifier */DB_VALUE cur_val; /* last value handed to a caller */DB_VALUE inc_val;DB_VALUE max_val;DB_VALUE min_val;DB_VALUE cyclic;DB_VALUE started;int cached_num;DB_VALUE last_cached_val; /* high water mark of current block */struct serial_entry *next; /* free-list pointer when free */};
두 값이 모든 일을 한다. cur_val 은 캐시된 매 nextval 마다 전진하고,
last_cached_val 은 heap 안에 예약된 가장 높은 값이다. 블록 소진은 정확히
cur_val == last_cached_val 이다. 소진 시 serial_update_cur_val_of_serial
이 entry->last_cached_val := serial_get_nth_value(..., nturns * cached_num, ...) 을 _db_serial.current_val 로 다시 쓴다. 즉 새 경계를
포함해 cached_num 만큼의 다음 블록을 예약하는 효과다. 모두 top
operation 으로 commit 된 heap-page 로그 레코드 아래에서 일어나므로 사용자
트랜잭션이 abort 되더라도 살아남는다.
DDL 은 xserial_decache 로 이 캐시를 무효화한다. serial 의 행을
변형시키는 execute_statement.c 의 모든 코드 경로 (CREATE, ALTER, DROP,
RENAME, AUTO_INCREMENT max-val 갱신) 가 그 후 serial_decache 를 호출
한다. network_interface_cl.c 안의 클라이언트 측 wrapper serial_decache
가 sserial_decache 요청을 서버로 보내고, 서버는 xserial_decache 를
호출해 엔트리를 제거하며 또한 그 serial 을 참조한 query 의 캐시된 XASL
도 함께 정리한다 (xcache_remove_by_oid).
DDL 흐름
섹션 제목: “DDL 흐름”sequenceDiagram
participant C as CSQL Client
participant P as Parser
participant S as do_create_serial<br/>(execute_statement.c)
participant H as Heap (_db_serial)
participant O as Server (xserial_*)
Note over C,O: CREATE SERIAL s START WITH 1 INCREMENT BY 1 CACHE 100
C->>P: SQL 텍스트
P->>S: PT_CREATE_SERIAL
S->>H: sm_find_class("db_serial")
S->>S: 불변식 검증<br/>(SERIAL_INVARIANT[])
S->>S: do_create_serial_internal()<br/>14개 attr 모두로 OBJTMPL 구축
S->>H: dbt_finish_object()<br/>행 삽입, OID 반환
Note over C,O: SELECT s.NEXTVAL FROM dual
C->>P: SQL 텍스트
P->>O: T_NEXT_VALUE arith 노드 포함 XASL<br/>(serial_oid, cached_num, num_alloc=1)
O->>O: fetch.c → xserial_get_next_value
O->>H: heap_get_visible_version (peek)<br/>serial OID 의 X_LOCK 아래
O->>H: spage_update + log_sysop_commit
O->>O: 캐시 엔트리 등록
O-->>C: 반환된 값
Note over C,O: DROP SERIAL s
C->>P: SQL 텍스트
P->>S: do_drop_serial
S->>H: db_drop(serial_object)
S->>O: serial_decache(oid)<br/>(클라이언트→서버 메시지)
O->>O: xserial_decache: 캐시에서 제거, XASL 정리
모든 DDL 경로가 같은 관용구를 반복한다. do_get_serial_obj_id (단지
unique_name 위의 db_find_unique) 로 serial MOP 을 찾고,
au_check_serial_authorization 으로 권한 검사를 하고, OBJTMPL API 로
변형하고, 마지막으로 serial_decache 로 모든 서버의 인메모리 사본을
무효화한다. ALTER SERIAL 은 추가로 check_serial_invariants 를 8 슬롯
짜리 SERIAL_INVARIANT 술어 배열을 돌린다. min_val ≤ max_val,
current_val between min and max, cached_num × |inc| ≤ range 등 — 변경
이 자리잡기 전에.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”서버 측 런타임 — src/query/serial.c
섹션 제목: “서버 측 런타임 — src/query/serial.c”이 파일이 serial 의 서버 측 런타임 전부다. serial.h 로 네 개의
함수를 export 하며, unique-name BTID 를 캐시하는 SERVER_MODE 전용 helper
두 개를 추가로 가진다.
-
xserial_get_current_value/xserial_get_current_value_internal. 비캐시 경로는_db_serial클래스 위에 빠른 scan 캐시를 열고,heap_get_visible_version으로 serial OID 의 행을 peek 하며,current_valattribute 만 읽어 결과로 공유한다. 캐시 경로는 캐시 mutex 아래에서entry->cur_val로 단락된다.mht_get이 미스이면 비캐시 경로로 폴백한다. serial OID 위에는 lock 이 잡히지 않는다.currval은 read-only 이며 어떤 가시 버전이든 허용한다. -
xserial_get_next_value/xserial_get_next_value_internal. 드라 이버 함수와 일꾼. 드라이버는num_alloc ≥ 1을 강제하고, 캐시 풀 mutex 를 잡고, 엔트리를 찾아 인메모리에서 전진하거나 escalate 한다. Escalation 경로가 위의try_again루프다. serial OID 위의 조건부X_LOCK, 경합 시 drop-and-retry, 그러고 나서 일꾼 호출, 그 다음 X_LOCK 해제. 일꾼은 행의 모든 컬럼 (current_val,increment_val,max_val,min_val,cyclic,started,cached_num, 복제 키용unique_name) 을 읽고,serial_get_nth_value로 새 값을 계산하고,current_val을 다시 쓰고 — serial 이 캐시되어 있으면 — 새 값에서(cached_num - 1)increment 만큼 떨어진last_cached_val과 함께cur_val = next로 채워진 freshSERIAL_CACHE_ENTRY를 등록한다. 최초의started == 0경로는 별도의num_alloc--로 처리되어 첫 반환 값이start_val그 자체가 된다. -
serial_get_next_cached_value/serial_update_cur_val_of_serial. 순수 인메모리 전진 vs. heap 재충전. 첫 함수는cur_val과last_cached_val을 비교하고, 같으면 (또는num_alloc > 1일 때 사영된next_val이last_cached_val에 닿거나 넘어서면) 두 번째 함수를 호출해 디스크 위 high water mark 를 정수 개의 블록만큼 전진 시킨다. 블록 크기는cached_num이고 필요한 블록 수는CEIL_PTVDIV(num_alloc, cached_num)이다. 따라서nextval(s, 1000)도 그것을 덮는 가장 작은 정렬된 super-block 만 예약한다. -
serial_update_serial_object. 캐시 재충전 경로와 비캐시 갱신 경로 가 공유하는 내부 페이지-그리고-로그 primitive. CUBRID 의 top-operation 패턴의 교과서적 예시다.log_sysop_start,heap_attrinfo_transform_to_disk로 stack 할당된 레코드로 변환,log_append_redo_recdes로 redo 로그,spage_update실행, 해당되면 복제 레코드 emit, 마지막으로log_sysop_commit.if (lock_mode != X_LOCK)분기는 둘러싼 트랜잭션이 이미 serial 위에 X_LOCK 을 들고 있을 때 top operation 을 생략한다. 이 경우는 호출자가 변경이 사용자 트랜잭션 자신의 commit 위에서만 보이기를 명시적으로 원하는 CREATE / ALTER 경로에서만 닿는다. 따라서 별도 sysop 은 옳지 않다. -
serial_get_nth_value. 산술의 핵심.cur_val + nth × inc_val를 계산한다. 양수 / 음수 increment 분기,max_val/min_val에 대한 범위 검사, 그리고 cyclic 일 때 반대쪽 경계로의 wrap-around. 비-cyclic 이고 범위 밖이면ER_QPROC_SERIAL_RANGE_OVERFLOW로 에러. 모든 산술 은numeric_db_value_*를 쓴다. serial 이 내부적으로DB_TYPE_NUMERICprecision-38 을 쓰기 때문이다. 매크로DB_SERIAL_MAX = 99...9(38 개의 9) 와DB_SERIAL_MIN(음수 짝) 이 묵시적 상한을 정의한다. -
serial_initialize_cache_pool/serial_finalize_cache_pool/serial_alloc_cache_entry/serial_alloc_cache_area. 부트 시점 bring-up 과 free-list 관리. 풀은 100 개의 엔트리를 가진 area 하나로 시작한다. 새 area 는 필요할 때 할당되어serial_Cache_pool.area에 엮인다. -
serial_load_attribute_info_of_db_serial/serial_get_attrid. attribute-id 표의 lazy 부트스트랩. 서버가 처음으로_db_serial행을 읽거나 쓸 때, 클래스 레코드를 한 번 걸으면서 각 attribute 의 이름 (SERIAL_ATTR_*문자열) 을 인덱스의 enum (SR_ATTRIBUTES) 에 매칭 하고attribute_index → ATTR_ID를serial_Attrs_id[]에 캐시한다. 이후의 모든 호출은 O(1) lookup 을 위해serial_get_attrid를 거친다. -
serial_set_cache_entry/serial_clear_value. 엔트리 안에 들어 있는DB_VALUE들을 복제하거나 풀어 주는 보일러플레이트 helper. NUMERIC 같은 가변 길이 값에 대한 CUBRID 의 deep-copy 관습을 존중하기 위해pr_clone_value/pr_clear_value와 짝을 이룬다. -
xserial_decache. 모든 DDL 완료 시점에 클라이언트-서버 프로토콜 로부터 호출된다. 엔트리를 해시에서 제거하고, 값을 정리하고, 엔트리를 free list 로 다시 엮는다. 이 serial 을 참조한 컴파일된 XASL 이 버려 지도록xcache_remove_by_oid도 호출한다 (오래된 cached_num 을 inline 한 query plan 은 이제 무효다). -
serial_cache_index_btid/serial_get_index_btid(SERVER_MODE 전용). 서버 시작 시 lookup 되어serial_Cached_btid에 캐시된다. B-tree 의 이름은pk_db_serial_unique_name으로 하드코딩되어 있다. 이 BTID 는 locator 가 카탈로그를 매번 다시 해석하지 않고unique_name을 OID 로 번역할 때 쓴다.
클라이언트 측 DDL — src/query/execute_statement.c
섹션 제목: “클라이언트 측 DDL — src/query/execute_statement.c”클라이언트 경로가 모든 정책을 들고 있다. executor 파일의 serial 섹션은 다음을 호스팅한다.
- 식별자 해석.
do_get_serial_obj_id가 표준 진입점이다.unique_name위의db_find_unique.DB_CLIENT_TYPE_ADMIN_LOADDB_COMPAT_UNDER_11_2아래의loaddb호환 shim 은 qualified lookup 이 미스했을 때do_find_serial_by_query(legacy 비-qualified 이름으로_db_serial에 대한 SELECT) 로 폴백한다. 11.2 이전의 unload 는 bare 이름을 저장 했고, 11.2+ 의 loader 가 사용자-qualifiedunique_name을 즉석에서 재구축한다. - CREATE.
do_create_serial이 AST 를 파싱하고, 네 개의DB_VALUE슬롯 (start,inc,min,max) 을 채우고,NUMERIC(38,0)으로 도메인 변환을 돌리고, 일관성 검사용SERIAL_INVARIANT[]배열을 구축하고, 마지막으로do_create_serial_internal을 호출한다. 후자가 매SERIAL_ATTR_*attribute 를 설정해 OBJTMPL 을 구축하고 객체를 마무리한다. 권한 검사는 단순한AU_DISABLE괄호다. serial DDL 은 항상 인증을 잠시 끈 채로_db_serial을 만지고, 그 자체의 owner 검사를 적용한다 (!ws_is_same_object(owner, Au_user) && !au_is_dba_group_member). - ALTER.
do_alter_serial이 현재 행을 읽고, AST 의 델타를 적용하고 (current_val,inc,min,max,cyclic,cached_num각각이 독립적으로 선택적), 병합된 이미지를 불변식을 다시 돌리고, OBJTMPL 으로 쓰고,serial_decache로 마무리한다. 같은 루틴이ALTER SERIAL ... RESTART WITH n을 처리한다. CURRENT_VAL 갱신과 함께STARTED = 0으로 구현되어, 다음nextval이min_val + inc가 아닌 새 값을 반환하게 만든다. - DROP.
do_drop_serial이 AUTO_INCREMENT 가드를 강제하고 (class_name IS NOT NULL⇒ 거부), MOP 위의db_drop을 호출하고,serial_decache한다. - AUTO_INCREMENT.
do_create_auto_increment_serial은 컬럼 레벨 진입점이며do_add_attribute(execute_schema.c안) 가auto_increment != NULL인 PT_NODE 가 추가될 때마다 호출한다. 매크로로 serial 이름을 조립하 고, 기본값을 모으고,do_create_serial_internal에 위임한다.do_update_maxvalue_of_auto_increment_serial이ALTER TABLE ... CHANGE COLUMN type의 짝이다. 어떤 상태도 reset 하지 않고 더 넓은 정수 타입에 맞도록 serial 의max_val만 넓힌다.do_reset_auto_increment_serial은 (TRUNCATE TABLE에서 호출됨) serial 을MIN_VAL로 되감고started를 정리한다.do_change_auto_increment_serial(ALTER TABLE ... AUTO_INCREMENT = n) 은current_val,min_val,started를 재설정한다.do_update_auto_increment_serial_on_rename은 네 back-link 컬럼을ws_decache아래에서 다시 써 workspace 가 오래된 행을 서비스하지 못하게 한다. - Helper.
do_get_serial_cached_num은SERIAL_ATTR_CACHED_NUM을 가져오는 한 줄짜리이며, executor 가nextval호출 사이트를 컴파일할 때cached_num을 XASL 에 박아 넣기 위해 호출하는 것이다. 그 XASL 상수는 서버에서xserial_get_next_value에 먹이는 같은cached_num이며 (xserial_decache가 XASL 캐시를 정리해 무효화하는 그 값이다).
CREATE TABLE 배선 — src/query/execute_schema.c
섹션 제목: “CREATE TABLE 배선 — src/query/execute_schema.c”CREATE TABLE t (id INTEGER AUTO_INCREMENT, …) 가 돌면,
do_add_attribute 가 각 PT_NODE 컬럼을 걷는다. 컬럼이 auto_increment != NULL 을 들고 있으면, 스키마 레이어가 execute_statement.c 에서
do_create_auto_increment_serial 을 끌어와 _db_serial 행을 만들고,
결과 MOP 을 진행 중인 SM_TEMPLATE 의 attribute 에
smt_set_attribute_auto_increment 로 저장한다. 그 후 템플릿이 그
MOP 을 attribute 에 들고 있는 클래스 객체를 flush 하고, SM_ATTRIBUTE 의
auto_increment 필드가 영속 클라이언트 측 빵 부스러기가 된다. INSERT
시점에 executor 가 attribute 에서 auto_increment.serial_obj 를 읽고,
do_get_serial_cached_num 으로 cached_num 을 박아 넣은 T_NEXT_VALUE
arith 노드를 XASL 에 emit 한다. RENAME COLUMN 은
smt_change_attribute_w_dflt_w_order 를 거쳐 라우팅되며, 이는 결국
do_update_auto_increment_serial_on_rename 을 호출한다. RENAME TABLE 은
같은 back-link 재기록을 테이블 레벨에서 처리한다. DROP TABLE 은 자기가
소유한 모든 AUTO_INCREMENT serial 을 삭제한다.
nextval / currval 평가 — src/query/fetch.c
섹션 제목: “nextval / currval 평가 — src/query/fetch.c”호출 사슬의 끝은 fetch_peek_dbval 에 있다. serial.next_value() 와
serial.current_value() 를 위한 XASL 산술 연산자 노드가 T_NEXT_VALUE
와 T_CURRENT_VALUE 로 디코드된다.
// fetch_peek_dbval — src/query/fetch.c (case T_NEXT_VALUE)serial_oid = db_get_oid (peek_left);cached_num = db_get_int (peek_right);num_alloc = db_get_int (peek_third);
if (xserial_get_next_value (thread_p, arithptr->value, serial_oid, cached_num, num_alloc, GENERATE_SERIAL, false) != NO_ERROR) { goto error; }파서에서 XASL 을 거쳐 호출 사이트로 값으로 넘어가는 두 플래그가 있다.
GENERATE_SERIALvsGENERATE_AUTO_INCREMENT. 후자만이xserial_get_next_value로 하여금 세션의LAST_INSERT_ID()를 갱신 하게 만든다. 명시적s.NEXTVAL은 그렇지 않다.force_set_last_insert_id. 세션에 이미 하나가 설정되어 있어도 LAST_INSERT_ID 갱신을 강제한다. INSERT 경로는 한 statement 안에서 두 AI 컬럼을 한꺼번에 타게팅하는 multi-row insert 의 첫 행에 대해서는false, 나머지에 대해서는true를 넘긴다.
이 플래그는 또한 산술 노드를 REGU_VARIABLE_FETCH_NOT_CONST 로 도장
찍어 옵티마이저가 그것을 fold 하지 못하게 한다. 이 플래그가 없다면,
첫 호출이 상수로 캐시되어 재사용될 것이고 — gap-permissive 시퀀스가
조용히 고장 난 시퀀스가 될 것이다.
위치 힌트 (2026-05-01 기준)
섹션 제목: “위치 힌트 (2026-05-01 기준)”| 심볼 | 파일 | 라인 |
|---|---|---|
SR_ATTRIBUTES enum | src/query/serial.c | 56 |
SERIAL_CACHE_ENTRY (struct serial_entry) | src/query/serial.c | 78 |
SERIAL_CACHE_POOL (struct serial_cache_pool) | src/query/serial.c | 106 |
serial_Cache_pool (싱글톤) | src/query/serial.c | 119 |
serial_Cached_btid | src/query/serial.c | 124 |
xserial_get_current_value | src/query/serial.c | 158 |
xserial_get_current_value_internal | src/query/serial.c | 200 |
xserial_get_next_value | src/query/serial.c | 284 |
serial_get_next_cached_value | src/query/serial.c | 418 |
serial_update_cur_val_of_serial | src/query/serial.c | 507 |
xserial_get_next_value_internal | src/query/serial.c | 635 |
serial_update_serial_object | src/query/serial.c | 917 |
serial_get_nth_value | src/query/serial.c | 1019 |
serial_initialize_cache_pool | src/query/serial.c | 1117 |
serial_finalize_cache_pool | src/query/serial.c | 1155 |
serial_get_attrid | src/query/serial.c | 1189 |
serial_load_attribute_info_of_db_serial | src/query/serial.c | 1216 |
serial_set_cache_entry | src/query/serial.c | 1349 |
xserial_decache | src/query/serial.c | 1414 |
serial_cache_index_btid | src/query/serial.c | 1486 |
SERIAL_ATTR_* 매크로 | src/storage/storage_common.h | 1127 |
oid_Serial_class_oid | src/storage/oid.c | 80 |
oid_get_serial_oid | src/storage/oid.c | 171 |
SET_AUTO_INCREMENT_SERIAL_NAME | src/object/transform.h | 120 |
do_evaluate_default_expr | src/query/execute_statement.c | 430 |
do_create_serial_internal | src/query/execute_statement.c | 672 |
do_update_auto_increment_serial_on_rename | src/query/execute_statement.c | 877 |
do_reset_auto_increment_serial | src/query/execute_statement.c | 1023 |
do_change_auto_increment_serial | src/query/execute_statement.c | 1110 |
do_get_obj_id / do_get_serial_obj_id | src/query/execute_statement.c | 1262 / 1334 |
do_get_serial_cached_num | src/query/execute_statement.c | 1378 |
do_create_serial | src/query/execute_statement.c | 1405 |
do_create_auto_increment_serial | src/query/execute_statement.c | 1872 |
do_update_maxvalue_of_auto_increment_serial | src/query/execute_statement.c | 2128 |
do_alter_serial | src/query/execute_statement.c | 2330 |
do_drop_serial | src/query/execute_statement.c | 2982 |
T_NEXT_VALUE / T_CURRENT_VALUE 케이스 | src/query/fetch.c | 2422 / 2445 |
serial_decache (클라이언트 wrapper) | src/communication/network_interface_cl.c | 7698 |
sserial_decache (서버 stub) | src/communication/network_interface_sr.cpp | 6688 |
소스 검증
섹션 제목: “소스 검증”- 캐시 상태의 소유권. 캐시는 서버 프로세스마다 하나의 싱글톤이다.
HA 복제에서는 각 replica 가 자기 자신의 풀을 가지지만, master 에서
생성된 값은
serial_update_cur_val_of_serial(log_append_supplemental_serial) 이 emit 하는 supplemental 로그 레코드로 복제된다. replica 는 값을 독립적으로 만들지 않는다. replica 의 캐시는 그것이 나중에 promote 될 때만 의미를 가지고, 그 시점에serial_initialize_cache_pool이 cold 로 시작한다. 이 불변 식은 묵시적이며 문서화되어 있지 않다. started는 값이 반환되기 전에 commit 된다. 첫nextval이started=0→1을 뒤집고 같은serial_update_serial_object호출 안에서current_val과started를 함께 쓴다. 사용자 트랜잭션이 첫 값을 소비한 후 abort 하면, 행은started=1을 유지하고 두 번째nextval은start_val이 아닌start_val + inc를 반환한다. 이는 올바른 gap-permissive 의미론이지만, 사용자가 가끔 CUBRID 의started플래그 를 읽고 다른 가정을 하므로 짚어 둘 만하다.- 캐시 재충전 후의
current_val. 캐시된 serial 위에서 SQL 로 직접_db_serial.current_val을 읽으면, 마지막으로 실제로nextval이 반환한 값이 아니라 가장 최근에 예약된 블록의 high water mark 가 돌아온다. 권위 있는 마지막으로 반환된 값 은serial_Cache_pool.ht[oid].cur_val안에 살며, 프로세스 사설이다. 따라서UPDATE _db_serial SET current_val = ...로 시퀀스를 reset 하려는 도구는 그 변경이 다음nextval에 의해 관찰되기 전에serial_decache도 함께 보내야 한다 (또는 서버를 재시작해야 한다). 프로덕션 코드는 정확히 이 함정을 피하기 위해 ALTER SERIAL DDL 을 쓴다. 그것이serial_decache로 끝난다. - 캐시 mutex 는 글로벌이다.
cache_pool_mutex는 모든 serial 의 해시 엔트리를 보호하는 것이지, serial 별 상태를 보호하는 것이 아니다. 서로 다른 serial 이 많은 워크로드는 기저 serial 이 독립적이어도 이 하나의 mutex 위에서 경합을 본다. retry-on-X_LOCK-failure 패턴이 lock 매니저가 관여되어 있는 동안 mutex 를 떨어뜨리므로, 최악 경우는 인메모리 산술로 한정된다. - 레코드 조립을 위한
stack_block.serial_update_serial_object는 heap 할당 없이 redo-image 레코드를 구축하기 위해cubmem::stack_block<IO_MAX_PAGE_SIZE>를 쓴다. 그 함수의 redo 경로 는 zero-malloc 이다. 캐시 풀 mutex 임계 영역 안에서 실행되기 때문에 이 점이 중요하다. - 복제와
lock_mode != X_LOCK. 호출자가 이미 serial 위의 X_LOCK 을 들고 있을 때log_sysop_start를 생략하는 분기는nextval에서가 아닌 CREATE 와 ALTER 에서만 닿는다.nextval은 항상 sysop 을 연다. 복제 flush 마크도lock_mode != X_LOCK에 조건부다. 진행 중인 CREATE / ALTER 는 정상적인 RBR 레코드를 emit 해서는 안 된다. 행 자체 가 사용자 트랜잭션의 writeset 의 일부이며 사용자 commit 시점에 정상적인 복제 스트림으로 실릴 것이기 때문이다. - AUTO_INCREMENT 이름 충돌. serial 이름 형식이
<class>_ai_<attr>이므로, 그 이름과 정확히 같은 사용자 정의 serial 이 기술적으로 가능 하다. CREATE SERIAL 은 underscore 를 금지하지 않는다. 충돌이 감지될 때do_create_auto_increment_serial이do_get_serial_obj_id로부터ER_QPROC_SERIAL_ALREADY_EXIST를 반환받는 것에 의존한다.
미해결 질문
섹션 제목: “미해결 질문”- 캐시 축출 정책. 캐시 풀은 크기 제한도 없고 축출도 없다. 엔트리는
DDL 이
xserial_decache로 축출하거나 서버가 재시작될 때까지 누적 된다. 짧게 많은 서로 다른 serial 을 만지는 워크로드 (multi-tenant 스키마 안의 tenant 별 AUTO_INCREMENT) 의 경우 무한정 자랄 수 있다. 메모리 압박이 보이기 전의 실용적 상한은 검증되지 않았다. - hot standby 아래의
nextval. read replica 는nextval을 거부 해야 한다.xserial_get_next_value안의CHECK_MODIFICATION_NO_RETURN이 read-only replica 에서ER_DB_NO_MODIFICATIONS를 반환하지만, 같은 검사가 완전히 수동적인 HA standby 에서 동작하는지는boot_sr.c의 복구 상태 기계를 읽어 봐야 알 수 있다. - 38자리 경계의 numeric overflow.
serial_get_nth_value는 cyclic 이 아닐 때만 경계를 검사하고ER_QPROC_SERIAL_RANGE_OVERFLOW를 올린다.cached_num이 병적이라면 비교가 돌기 전에 중간 계산inc_val * nturns * cached_num이 38 자리를 초과할 수 있다.numeric_db_value_mul의 saturation 동작은 이 파일에서 명백하지 않다. - 캐시된 값의 statement 간 격리. 캐시는 프로세스-글로벌이지 세션-
글로벌이 아니다. 한 statement 에서 두 번 평가된
s.NEXTVAL은 SQL 표준의 same-expression-same-value 규칙과 무관하게 두 개의 다른 값을 반환한다. PostgreSQL 과 일치하고, Oracle 과는 다르다. 코드베이스에 의도를 확인해 주는 주석은 없다. - 11.2 unload/load 호환 shim.
do_get_serial_obj_id안의DB_CLIENT_TYPE_ADMIN_LOADDB_COMPAT_UNDER_11_2폴백은 11.2 이전의 덤프가 사용자 qualifier 없이 serial 이름을 저장했고 11.2+ loader 가 그 gap 을 메운다는 것을 시사한다. 이것이 qualifier 마이그레이션이 새는 유일한 자리인지는 검증되지 않았다.
src/query/serial.c(39 KB) — 서버 측 런타임 전체, 캐시 풀, attribute-id 부트스트랩, BTID 캐싱.src/query/serial.h(1 KB) — export 된 함수 집합.src/query/execute_statement.c— 모든 DDL 진입점 (do_create_serial,do_alter_serial,do_drop_serial,do_create_auto_increment_serial,do_get_serial_obj_id,do_get_serial_cached_num, 그리고 rename / reset / max-val 갱신 / change 를 위한 AUTO_INCREMENT 동반자들).src/query/execute_schema.c— 컬럼별 AUTO_INCREMENT serial 을 만들고 RENAME COLUMN 시 back-link 를 다시 쓰는 테이블 레벨 배선.src/query/fetch.c—T_NEXT_VALUE/T_CURRENT_VALUE산술 노드의 XASL 평가 (xserial_get_next_value/xserial_get_current_value호출).src/storage/storage_common.h—SERIAL_ATTR_*컬럼 이름 매크로,DB_SERIAL_MAX/DB_SERIAL_MINnumeric 경계.src/storage/oid.c/oid.h—_db_serial의 예약 OIDoid_Serial_class_oid,oid_get_serial_oid.src/object/transform.h—SET_AUTO_INCREMENT_SERIAL_NAME매크로,AUTO_INCREMENT_SERIAL_NAME_EXTRA_LENGTH.src/communication/network_interface_cl.c/network_interface_sr.cpp— DDL 무효화를 모든 서버 프로세스로 전파하는serial_decache클라이언트 / 서버 stub.