콘텐츠로 이동

(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 하더라도 회수되지 않는다” 고 명시한다.

마지막 절이 설계의 하중을 받치는 부분이다. 여기서 세 가지 교과서적 절충이 따라 나오고, 그 절충이 본 문서의 골격을 만든다.

  1. gapless vs. gap-permissive 의미론. gapless 시퀀스는 호출 트랜잭션 의 전체 수명 동안 잠겨야 한다. 값 N 을 소비한 트랜잭션이 rollback 되면 N 을 되돌려 놔야 하기 때문이다. 이는 동시성과 양립할 수 없다. 매 nextval 이 직렬화 지점이 된다. 모든 주요 엔진 — Oracle, PostgreSQL, MySQL, CUBRID — 이 gap-permissive 의미론을 고른다. 시퀀스는 짧게 유지되는 lock 아래서 전진하고, 증가는 둘러싼 사용자 트랜잭션과 독립적 으로 commit 되며, rollback 된 값은 그저 건너뛴다.

  2. 캐시된 vs. 캐시되지 않은 할당. gap-permissive 시퀀스도 순진하게 구현하면 nextval 마다 heap 페이지 갱신 비용이 든다. 표준 최적화는 캐싱 이다. 한 번의 nextval 이 메모리 안에 cached_num 개의 값 블록을 예약하고, 그 블록의 high water mark 만 카탈로그에 기록한다. 같은 블록 내의 후속 호출은 순수한 인메모리 연산이다. 대가는 크래시 시점과 캐시 축출 시점의 더 큰 gap 이다 (마지막으로 소비한 값과 high water mark 사이의 모든 값이 사라진다).

  3. 카탈로그 쓰기의 트랜잭션성. 시퀀스가 전진하면 영속 행이 변형된다. 그 변형이 사용자 트랜잭션을 함께 탔다면, 사용자가 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 에 걸린 채로 반환된다. 본 문서의 나머지는 이 결정들이 소스에서 어떻게 드러나는지를 추적한다.

동시성 환경에서 대리키를 만들고 싶은 모든 관계형 엔진은 비슷한 패턴 묶음 에 손을 뻗는다.

시퀀스는 원시 타입이 아니다. 모든 엔진이 시퀀스를 어떤 카탈로그 비슷한 관계 안의 한 행 테이블 로 구현하고, 그 행을 읽고 갱신하는 SQL 레벨 함수 몇 개를 노출한다.

  • PostgreSQL 은 시퀀스를 위한 별도의 관계 종류 (relkind = 'S') 를 통째로 할당한다. 매 CREATE SEQUENCE s 가 실제 heap 을 만들고 그 안에 last_value, log_cnt, is_called 를 담은 튜플 하나가 들어간다. nextval('s') 는 특수 access path 를 사용한다 (commands/sequence.cSequenceNextval). 페이지를 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 은 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 든 — 같은 저장 모양과 같은 코드 경로를 가진다.

_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 에서 행으로 곧장 걸어간다.

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 을 만지지도 않는다.

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 독자의 직관과 다르게 보이는 두 가지가 있다.

  1. mutex 가 보호하는 것은 해시 테이블이지, 값이 아니다. 보호된 임계 영역은 mht_get / mht_put 짝의 lookup-and-update 와 엔트리 위의 산술 연산이다. heap 위의 serial 은 OID 위의 보통의 CUBRID X_LOCK 으로 보호된다. 다른 어떤 heap 객체와 똑같이. 시퀀스는 행이 타는 같은 lock 매니저를 탄다.
  2. 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_serialclass_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.c
    struct 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_serialentry->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_decachesserial_decache 요청을 서버로 보내고, 서버는 xserial_decache 를 호출해 엔트리를 제거하며 또한 그 serial 을 참조한 query 의 캐시된 XASL 도 함께 정리한다 (xcache_remove_by_oid).

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_val attribute 만 읽어 결과로 공유한다. 캐시 경로는 캐시 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 로 채워진 fresh SERIAL_CACHE_ENTRY 를 등록한다. 최초의 started == 0 경로는 별도의 num_alloc-- 로 처리되어 첫 반환 값이 start_val 그 자체가 된다.

  • serial_get_next_cached_value / serial_update_cur_val_of_serial. 순수 인메모리 전진 vs. heap 재충전. 첫 함수는 cur_vallast_cached_val 을 비교하고, 같으면 (또는 num_alloc > 1 일 때 사영된 next_vallast_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_NUMERIC precision-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_IDserial_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 가 사용자-qualified unique_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 으로 구현되어, 다음 nextvalmin_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_serialALTER 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_numSERIAL_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_ATTRIBUTEauto_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_VALUET_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_SERIAL vs GENERATE_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 시퀀스가 조용히 고장 난 시퀀스가 될 것이다.

심볼파일라인
SR_ATTRIBUTES enumsrc/query/serial.c56
SERIAL_CACHE_ENTRY (struct serial_entry)src/query/serial.c78
SERIAL_CACHE_POOL (struct serial_cache_pool)src/query/serial.c106
serial_Cache_pool (싱글톤)src/query/serial.c119
serial_Cached_btidsrc/query/serial.c124
xserial_get_current_valuesrc/query/serial.c158
xserial_get_current_value_internalsrc/query/serial.c200
xserial_get_next_valuesrc/query/serial.c284
serial_get_next_cached_valuesrc/query/serial.c418
serial_update_cur_val_of_serialsrc/query/serial.c507
xserial_get_next_value_internalsrc/query/serial.c635
serial_update_serial_objectsrc/query/serial.c917
serial_get_nth_valuesrc/query/serial.c1019
serial_initialize_cache_poolsrc/query/serial.c1117
serial_finalize_cache_poolsrc/query/serial.c1155
serial_get_attridsrc/query/serial.c1189
serial_load_attribute_info_of_db_serialsrc/query/serial.c1216
serial_set_cache_entrysrc/query/serial.c1349
xserial_decachesrc/query/serial.c1414
serial_cache_index_btidsrc/query/serial.c1486
SERIAL_ATTR_* 매크로src/storage/storage_common.h1127
oid_Serial_class_oidsrc/storage/oid.c80
oid_get_serial_oidsrc/storage/oid.c171
SET_AUTO_INCREMENT_SERIAL_NAMEsrc/object/transform.h120
do_evaluate_default_exprsrc/query/execute_statement.c430
do_create_serial_internalsrc/query/execute_statement.c672
do_update_auto_increment_serial_on_renamesrc/query/execute_statement.c877
do_reset_auto_increment_serialsrc/query/execute_statement.c1023
do_change_auto_increment_serialsrc/query/execute_statement.c1110
do_get_obj_id / do_get_serial_obj_idsrc/query/execute_statement.c1262 / 1334
do_get_serial_cached_numsrc/query/execute_statement.c1378
do_create_serialsrc/query/execute_statement.c1405
do_create_auto_increment_serialsrc/query/execute_statement.c1872
do_update_maxvalue_of_auto_increment_serialsrc/query/execute_statement.c2128
do_alter_serialsrc/query/execute_statement.c2330
do_drop_serialsrc/query/execute_statement.c2982
T_NEXT_VALUE / T_CURRENT_VALUE 케이스src/query/fetch.c2422 / 2445
serial_decache (클라이언트 wrapper)src/communication/network_interface_cl.c7698
sserial_decache (서버 stub)src/communication/network_interface_sr.cpp6688
  • 캐시 상태의 소유권. 캐시는 서버 프로세스마다 하나의 싱글톤이다. 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 된다. 첫 nextvalstarted=0→1 을 뒤집고 같은 serial_update_serial_object 호출 안에서 current_valstarted 를 함께 쓴다. 사용자 트랜잭션이 첫 값을 소비한 후 abort 하면, 행은 started=1 을 유지하고 두 번째 nextvalstart_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_serialdo_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.cT_NEXT_VALUE / T_CURRENT_VALUE 산술 노드의 XASL 평가 (xserial_get_next_value / xserial_get_current_value 호출).
  • src/storage/storage_common.hSERIAL_ATTR_* 컬럼 이름 매크로, DB_SERIAL_MAX / DB_SERIAL_MIN numeric 경계.
  • src/storage/oid.c / oid.h_db_serial 의 예약 OID oid_Serial_class_oid, oid_get_serial_oid.
  • src/object/transform.hSET_AUTO_INCREMENT_SERIAL_NAME 매크로, AUTO_INCREMENT_SERIAL_NAME_EXTRA_LENGTH.
  • src/communication/network_interface_cl.c / network_interface_sr.cpp — DDL 무효화를 모든 서버 프로세스로 전파하는 serial_decache 클라이언트 / 서버 stub.