콘텐츠로 이동

[KO] CUBRID loaddb — 벌크 로더, 직접 경로 heap+B+Tree 삽입, 그리고 로드 후 통계 재구축

목차

벌크 로더(bulk loader) 는 시스템 바깥에서 흘러 들어온 N개 의 행 — 다른 데이터베이스에서 덤프된 평문 파일, 같은 엔진의 이전 버전이 내보낸 export, ETL 파이프라인의 출력물 — 을 받아 서, 저장 계층이 물리적으로 허용하는 한 가장 빠르게 독자에게 보이게 만드는 데이터베이스 엔진의 한 부속이다. 벌크 로더 와 INSERT를 루프로 도는 것 을 가르는 본질적 결정은, 보통의 INSERT … VALUES (…) 가 행 하나마다 돌리는 기계 장치를 로더가 우회할 의지가 있는가 다. Petrov의 Database Internals (4장 “Implementing B-Trees, 5장 Transaction Processing”) 는 이를 세 축으로 정리한다. write amplification, index maintenance, constraint checking. INSERT-루프 로더는 세 축 모두에서 행마다 전액의 세금을 내고, 직접 경로 로더는 그 비용 을 배치 전체에 분산시킬 권한을 받는다.

Write amplification 축이 가장 직접적이다. 트랜잭션 로깅을 하는 엔진에 일반 INSERT가 떨어지면 최소한 heap 레코드 1회 쓰기, 변경된 페이지마다 WAL 물리-논리 로그 1건, 인덱스 1개당 leaf 1회 쓰기, leaf마다 WAL 물리-논리 로그 1건이 나온다. 인덱스가 k 개 면 행당 페이지 접촉 수가 대략 1 + 2k 가 된다. 벌크 로더는 이 를 두 가지 방향에서 줄일 수 있다.

  1. 레코드 단위가 아닌 페이지 단위 로깅. 로더가 그 페이지의 유일한 writer라면 (로드 동안 클래스가 배타적으로 잠겨 있으면 참이다), 페이지 위에 몇 개의 레코드가 올라가 있든 “page-image redo” 한 건이 갓 만들어진 페이지 전체를 덮어 줄 수 있다. 수백 행이 로그 1건을 공유하게 되는 것이다.

  2. 인덱스 빌드 지연. 행이 도착할 때마다 k 개의 B+Tree 각각으로 들어가는 대신 — 여러 트리에 걸쳐 거의 무작위 페이지 접촉을 일으키는 패턴이다. 로더는 모든 (키, OID) 쌍을 모은 뒤 외부 정렬해서 트리마다 bottom-up으로 한 번에 만든다. 정렬 이 메모리를 묶어 주고, bottom-up 빌드는 leaf 페이지 하나하나 를 정확히 한 번씩만 만지며 순차로 쓴다.

Index maintenance 축은 constraint checking 축과 얽혀 있다. unique B+Tree는 중복 키를 거절해야 한다. 로더가 도는 동안 인덱스가 점진적으로 유지된다면 모든 행이 온라인에서 중복 검사 비용을 내고, 동시 inserter로부터 안전하기 위해 엔진은 여전히 key-range 락까지 잡아야 한다. 인덱스를 로드 후 빌드한다면 중복 검사는 정렬 단계에서 (인접한 동일 키가 위반 신호다) 자연 스럽게 일어나고, 절반만 적재된 테이블을 다른 트랜잭션이 만질 일이 없으니 — 로드가 클래스 단위 배타 락을 들고 있다는 전제가 있을 때 — key-range 락도 필요 없다.

Constraint checking 축은 더 넓다. 외래키, NOT NULL, CHECK, 트리거 발동 등이 모두 여기에 속한다. 벌크 로더는 거의 항상 외래 키 검사를 연기하거나 끈다 (CUBRID은 SA 모드에서 locator_Dont_check_foreign_key = true 로 둔다). 트리거는 항 상 끈다 (CUBRID은 로딩 직전에 db_disable_trigger () 를 부른 다). 비용 모델은 단순하다. 연기된 외래키 검사는 적재된 데이터 위에서 한 번 도는 anti-join이지만, 행마다 probe하면 행마다 비용 이 든다. 수천 행을 넘어가면 연기 쪽이 싸다.

세 번째 교과서적 관심사는 병렬 분할 이다. 입력 파일은 큰데 대상 테이블이 partitioned가 아니라면, 로더는 입력 파일을 배치 로 쪼개 워커 스레드에 나눠 주는 식으로 여전히 병렬화할 수 있 다. 함정은, 배치들이 같은 heap 파일과 같은 인덱스를 공유한다는 점이다. 그래서 워커들은 (배치 끝 → commit, 다음 배치 시작 → 계속) 의 순서를 맞춰야 한다. 일부 로더가 광고하는 OID 단조성과 WAL의 결정론적 복구가 그 위에 올라타 있기 때문이다. CUBRID의 워커 풀 모델 (load_worker_manager.cpp) 은 정확히 이 배치 방식을 — 순서가 보장된 commit까지 포함해 — 채택한다.

로드 후 통계 재구축 이 마지막 교과서적 부속이다. 비용 기반 옵티마이저는 컬럼 히스토그램, distinct-value 카운트, 클래스별 행 수에 의존하는데, 신규 로드는 셋 모두를 무효화시킨다. 따라서 로드 뒤에 평범한 UPDATE STATISTICS 를 도는 것은 선택이 아니 다. CUBRID의 loaddb는 이를 내장 단계로 만든다. SA 로더는 클래 스마다 sm_update_statistics 를 부르고, CS 로더는 loaddb_update_stats 를 부르며, 그 함수는 서버 위에서 xstats_update_statistics 를 구동한다. 이 단계가 없으면 옵티 마이저는 빈 통계 위에서 계획을 세우고, 갓 적재된 테이블을 향한 초기 몇 개 쿼리에서 파국적 플랜을 고르게 된다.

본 문서는 CUBRID loaddb 가 이 네 가지 — Bulk-Update 클래스 락 아래의 직접 경로 heap insert, 연기/배치 commit, 별도의 -i 인덱스 파일을 통한 인덱스 로드 지연, 로드 후 통계 재구축 — 를 src/loaddb/ 의 파일들과 그들이 호출하는 storage primitive들 위에서 어떻게 실현하는지 추적한다.

교과서가 모델을 주었다면, 이 절은 거의 모든 row-oriented 엔진 이 운영급 벌크 로더를 출하할 때 따르는 엔지니어링 관행을 이름 붙여 본다. CUBRID이 ## CUBRID의 구현 에서 내리는 구체적 선택은 이 공유 설계 공간 안의 한 다이얼 조합으로 읽는 편이 좋다.

모든 로더는 모호하지 않은 디스크 위 문법이 필요하다. 현장에서 보이는 관행은 이렇다.

  • 헤더 라인이 있는 CSV, 선택적으로 명시적 컬럼 리스트와 delimiter override (PostgreSQL COPY FROM, MySQL LOAD DATA INFILE, SQL Server BCP). 만들기 싸고, 파일 자체에 스키마는 없다.
  • 이진 테이블 덤프 포맷, 테이블을 인지하며 같은 엔진의 버전 사이에서 portable하다 (Oracle SQL*Loader의 직접 경로, PostgreSQL pg_dump custom 포맷, MySQL mysqldump --tab.txt/.sql 쌍). 파싱은 텍스트보다 빠르지만 엔진 종속적이 다.
  • CUBRID 포맷 객체 파일, 둘의 hybrid다. SQL 스타일 스키마 문장 뒤에 대상 테이블 이름을 잡아 주는 %class / %id 지시자가 따라오고, 그 뒤로 작은 lexer가 토큰의 타입을 정해 주는 인스턴스 라인 (string, integer, timestamp, 통화의 \KRW, set의 {…}, 객체 참조의 @oid, 줄 이음의 + 등) 이 이어진다. 이 포맷은 unloader (unloaddb) 에서 출발해 그것 과 대칭이다.

INSERT 식 로더는 행마다 다음을 통과시킨다.

parser → name-resolution → semantic-check → XASL-gen → executor →
locator_insert_force per row → trigger fire → constraint check

직접 경로 로더는 이를 다음으로 압축한다.

load-file lexer → load-file parser → DB_VALUE per attribute →
record_descriptor → locator_multi_insert_force on a vector →
(optional) deferred index entry → batch commit

이득은 SQL 파서를 거치지 않고, XASL을 만들지 않고, 행마다 옵티 마이저를 돌리지 않는 데서, 그리고 벡터로 묶인 레코드를 storage 계층에 넘김으로써 여러 레코드를 한 heap 페이지 위에 packing하고 페이지 전체를 WAL 1건만 발행할 수 있다는 데서 온다. Postgres 의 pg_bulkload 확장, Oracle SQL*Loader의 직접 경로, CUBRID의 loaddb가 모두 이 선 위에 앉는다. PostgreSQL 내장 COPY FROM 은 중간에 있다 (옵티마이저는 우회하지만 executor의 ExecInsert 은 거친다).

Drop-and-rebuild vs 점진적 인덱스 유지

섹션 제목: “Drop-and-rebuild vs 점진적 인덱스 유지”

k 개의 secondary 인덱스가 달린 테이블을 받았을 때 로더에는 두 가지 스케줄이 있다.

  1. 점진적 유지. 삽입된 heap 레코드마다 k 개의 인덱스 leaf 쓰기가 inline으로 발생한다. 구현이 간단하다 (heap insert가 이미 locator_attribute_info_force 를 구동하고 그것이 btree_insert 를 부른다). 그러나 인덱스 페이지가 무작위 키 순으로 만져지므로 buffer pool이 churn하고, WAL이 행당 k 건 씩 부풀어 오른다.

  2. Drop-and-rebuild. 로더가 secondary 인덱스를 drop하거나 애초에 만들지 않은 상태에서 heap을 적재한 다음, 외부 정렬과 순차 leaf 페이지 쓰기로 인덱스마다 bottom-up 재구축한다. 비용 은 인덱스 1개당 외부 정렬 1회와 heap 순차 스캔 1회이며, 이득 은 dense하고 잘 정렬된 leaf 페이지와 root → leaf 경로가 짧은 B+Tree다.

CUBRID의 loaddb 는 입력을 스키마 파일 (-s / --schema-file), 객체 파일 (-d / --data-file), 인덱스 파일 (-i / --index-file) 로 분리 함으로써 두 번째 스케줄을 지원한다. unloader가 인덱스를 보통의 CREATE INDEX 문장으로 인덱스 파일에 내보내고, loaddb는 heap 로드 ldr_exec_query_from_file 로 그 문장을 실행한다. 빌드 자체는 엔진의 평범한 인덱스 생성 경로 — btree_load_index + external_sort 의 정렬 기반 bottom-up 구축 — 가 된다.

엔진들 사이에서 보이는 패턴은 셋이다.

  • 단일 프로세스, 다중 스레드 배치 (CUBRID CS 모드 loaddb, pg_bulkload). 한 reader가 파일을 배치로 쪼개고, 워커 풀이 배치를 소비한다. commit은 순서가 보장된다.
  • 다중 프로세스, partition-aware (Oracle SQL*Loader의 parallel-direct, Greenplum gpload). 워커마다 자기 segment / partition에 쓴다. 싼 병렬성이지만 테이블이 partitioned일 때만 쓸 수 있다.
  • 외부 sharding 후 shard별 로드 (Vertica, ClickHouse). 일은 데이터를 sharding한 무엇에게 떠넘기고, 엔진 자체는 N 회의 순차 벌크 로드를 돌릴 뿐이다.

CUBRID은 첫 번째 진영이다. 워커들은 단일 transaction-per-batch 모델 아래 하나의 heap 파일을 공유한다. 세션은 로드 전체에 걸쳐 클래스 위에 Bulk-Update (BU_LOCK) 락을 들고 있고, 워커마다 서브 트랜잭션 (logtb_assign_tran_index) 을 열어 자기 배치를 삽입하고, batch-id 순서로 commit한다 (session::wait_for_previous_batch).

세 가지 패턴.

  • 로드 뒤 별도 명령으로 돌린다 (PostgreSQL ANALYZE, MySQL ANALYZE TABLE, Oracle DBMS_STATS.GATHER_TABLE_STATS). 구현 비용이 싸고, 잊어버리기 쉽다.
  • commit 시 암묵적 (auto-analyze 데몬이 행 수 변화 임계를 넘으면 트리거).
  • 로더 안에 내장 (CUBRID loaddb, Oracle SQL*Loader의 STATISTICS 절). 로더는 어떤 클래스가 만져졌는지 정확히 알 고 있으니 분석을 직접 돌린다.

CUBRID은 세 번째를 고른다. SA 모드는 ldr_update_statistics 안에서 클래스별 sm_update_statistics 를 부르고, CS 모드는 loaddb_update_stats 를 불러 로드 세션에서 클래스 OID 리스트를 받아 클라이언트가 클래스마다 stats_update_statistics(STATS_WITH_SAMPLING) 로 순회한다.

수백만 행 단위로 측정되는 로더는 부분 실패에서 살아남아야 한다. 관행은 이렇다.

  • 주기적 commit. N 행마다 (--periodic-commit N / commit-period) 로더가 commit하고, 도달한 라인 번호를 기록 한다. 재시작은 그 라인부터 건너뛰고 시작한다. CUBRID의 기본값 은 PERIODIC_COMMIT_DEFAULT_VALUE = 10240.
  • 에러 제어 파일. 로더가 무시해도 좋다고 선언한 에러 코드 목록 (CUBRID --error-control-file). 그 코드를 트리거한 행 은 배치를 abort하는 대신 로깅하고 건너뛴다.
  • 문법 검사 모드. 파일을 파싱하지만 insert는 하지 않는 dry-run (CUBRID --check-only / args.syntax_check). 갓 만들어진 unload 파일을 데이터베이스에 commit하기 전에 검증 하는 데 쓴다.

CUBRID은 셋 다 지원한다. 주기적 commit과 문법 검사의 조합이야 말로 운영 ETL 담당자가 원하는 정확한 그림이다. 일단 dry-run 하고, 그 dry-run이 통과하면 10K 행마다 체크포인트를 찍으며 commit한다.

CUBRID은 벌크 로드를 CS 모드에서는 두 프로세스 파이프라인, SA 모드에서는 단일 프로세스 직접 경로 로 구현한다. 둘은 같 은 로드 파일 문법 (load_grammar.yy + load_lexer.l), 같은 배치 알고리즘 (load_common.cppcubload::split), 같은 cubload::driver 매개자 클래스를 공유한다. 갈라지는 자리는 object_loader / class_installer 인터페이스 (load_common.hpp) 다. SA 모드는 sa_object_loader (legacy ldr_* 콜백을 감싼 load_sa_loader.cpp 의 wrapper) 를 쓰고, CS 모드는 server_object_loader (load_server_loader.cpp 의 직접 경로 구현) 를 쓴다.

flowchart TD
    subgraph CLIENT["loaddb client process (load_db.c)"]
        A1["main: loaddb_user → loaddb_internal"] --> A2["ldr_validate_object_file<br/>· get_loaddb_args"]
        A2 --> A3["db_login + db_restart<br/>(서버 접속)"]
        A3 --> A4["ldr_load_schema_file<br/>(-s 가 주어졌을 때)"]
        A4 --> A5{SA_MODE?}
        A5 -- yes --> SA["ldr_sa_load<br/>load_sa_loader.cpp"]
        A5 -- no --> CS["ldr_server_load<br/>· split + loaddb_load_batch"]
        SA --> A6["ldr_exec_query_from_file<br/>(-i 인덱스 파일)"]
        CS --> A6
        A6 --> A7["sm_update_catalog_statistics"]
        A7 --> A8["db_shutdown"]
    end
    subgraph SERVER["cub_server process (CS 모드 한정)"]
        B1["sloaddb_init<br/>→ new cubload::session"]
        B2["sloaddb_install_class<br/>→ session::install_class"]
        B3["sloaddb_load_batch<br/>→ session::load_batch<br/>→ worker_manager_try_task"]
        B4["load_task::execute<br/>→ driver::parse<br/>→ server_object_loader<br/>→ locator_multi_insert_force"]
        B5["sloaddb_update_stats<br/>→ xstats_update_statistics"]
        B6["sloaddb_destroy"]
    end
    CS -.NET_SERVER_LD_INIT.-> B1
    CS -.NET_SERVER_LD_INSTALL_CLASS.-> B2
    CS -.NET_SERVER_LD_LOAD_BATCH.-> B3
    B3 --> B4
    A7 -.NET_SERVER_LD_UPDATE_STATS.-> B5
    A8 -.NET_SERVER_LD_DESTROY.-> B6

CUBRID 로드 파일은 세 종류의 라인을 섞은 라인 단위 텍스트 파일 이다.

%id mytable 1
%class mytable (id, name, salary, dept)
1 'Alice' 50000.00 @dept|2
2 'Bob' 60000.00 NULL
  • %id <class-name> <numeric-id> 라인은 클래스 이름을 잡아 주 고, 배치의 나머지에서 쓸 numeric class-id를 부여한다. load_grammar.yyid_command 규칙에서 파싱되어 class_installer::check_class 로 dispatch된다.
  • %class <class-name> ( <attr-list> ) 라인은 기능적으로 동일 하되 추가로 어트리뷰트를 순서대로 나열한다. class_command 규칙과 class_installer::install_class 로 매핑된다.
  • 빈 줄이 아닌 그 외의 라인은 모두 인스턴스 라인 이다. 선택 적인 <int>: OID prefix 뒤에 공백으로 구분된 타입 있는 상수 들이 따라온다. instance_line 규칙과 object_loader::start_lineprocess_line 으로 dispatch된 다.

lexer (load_lexer.l) 는 타입 있는 상수를 글자 모양으로 인식 한다. [+\-]?[0-9]+INT_LIT, [Ee] 와 선택적 [fFlL] 이 들어간 더 큰 정규식은 REAL_LIT, [0-9]+/[0-9]+/[0-9]+DATE_LIT2, [0-9]+:[0-9]+(:[0-9]+)? 가 여섯 종의 TIME_LIT 을 덮는다. '…' 은 SQL 문자열 본문 (state <SQS>), m_semantic_helper.in_instance_line () 의 결과에 따라 delimited 식별자 (state <DELIMITED_ID>) 또는 double-quoted 문자열 (state <DQS>) 이다. 통화 기호는 자기 토큰을 받는다. \$ → DOLLAR_SYMBOL, \\KRW → WON_SYMBOL, \\EUR → EURO_SYMBOL 등 — 통화가 일급 DB 타입이기 때문이다. + 줄 이음 표시는 lexer (<SQS> 안의 '\+[ \t]*\r?\n[ \t]*\' 가 두 SQS 문자열을 잇는다) 와 splitter (cubload::split 안의 ends_with (line, +) 가 다음 반복까지 행을 one_row_buffer 에 보관) 양쪽에서 처리된다.

명령행 플래그는 load_common.hppcubload::load_args 위로 매핑되며, load_db.cget_loaddb_args 가 풀어 준다.

플래그 (long / short)load_args 필드기본값
--user / -uuser_nameAU_PUBLIC_USER_NAME
--password / -ppasswordempty (ER_AU_INVALID_PASSWORD 시 prompt)
--schema-file / -sschema_fileempty
--index-file / -iindex_fileempty
--data-file / -dobject_fileempty
--check-only / -csyntax_checkfalse
--load-only / -lload_onlyfalse
--periodic-commitperiodic_commit10240 (PERIODIC_COMMIT_DEFAULT_VALUE)
--no-statisticsdisable_statisticsfalse
--ignore-loggingignore_loggingfalse
--error-control-fileerror_fileempty
--ignore-classesignore_class_fileempty
--table / -ttable_nameempty (전체 파일 로드)
--CS-mode / -Ccs_modefalse
--no-user-specified-nameno_user_specified_namefalse

이 구조체가 cubpacking::packable_object 에서 파생되는 이유는 CS 모드에서 load_args 통째로가 sloaddb_init 요청 본문에 실려 서버로 전송되기 때문이다.

lexer는 load_lexer.l 에서 생성된 Flex C++ 스캐너다. 문법은 load_grammar.yy 에서 생성된 Bison LALR(1) C++ 파서다 (skeleton lalr1.cc, namespace cubload, 클래스 parser). 둘은 cubload::driver 클래스 (load_driver.cpp) 가 함께 꿰맨다.

// driver::parse — load_driver.cpp
int
driver::parse (std::istream &iss, int line_offset)
{
m_scanner->switch_streams (&iss);
m_scanner->set_lineno (line_offset + 1);
m_semantic_helper.reset_after_batch ();
assert (m_class_installer != NULL && m_object_loader != NULL);
parser parser (*this);
return parser.parse ();
}

driver는 cubload::scanner, cubload::class_installer, cubload::object_loader, error_handler, semantic_helper 를 소유한다. 파서의 액션 (load_grammar.yy 안) 은 %class / %id 지시자에서는 m_driver.get_class_installer () 를, 인스턴 스 라인에서는 m_driver.get_object_loader () 를, 원시 lexer 문자열에서 타입 있는 constant_type 값을 만들 때는 m_driver.get_semantic_helper () 를 부른다. 스캐너는 in-place 로 생성되므로 (%option yyclass="cubload::scanner", load_scanner.hpp), lexer state가 살아 있는 driversemantic_helper 를 본다.

semantic_helper (load_semantic_helper.hpp) 는 파서 내부에서 의미 있는 메모리를 할당하는 유일한 부속이다. 1024개의 재사용 가능한 string_type 슬롯을 담은 string_pool, 1024개의 constant_type 슬롯을 담은 constant_pool, 512 × 32 KiB의 문자열 본문 버퍼인 qstr_buf_pool, 그리고 풀을 넘치는 문자열 을 위한 fallback cubmem::extensible_block. 풀은 배치 사이 (reset_after_batch) 와 행 사이 (reset_after_line) 에 리셋 되므로, 한 번의 파싱이 hot path 위에서 사실상 0회의 malloc만을 일으킨다.

load_common.cppcubload::split 가 I/O 프론트엔드다. 객체 파일을 열고, 라인 단위로 걸으며, 라인을 batch_buffer 에 누적하다가, 다음 세 조건 중 하나가 트리거되면 버퍼를 비운다.

  1. 클래스 경계. %class 또는 %CLASS 로 시작하는 라인 (splitter는 lexer가 보기도 전에 prefix를 텍스트로 검사한다). 현재 배치를 b_handler 로 비우고, class-id 카운터를 증가시킨 뒤, 새 %class 또는 %id 라인을 혼자만 c_handler 에 보낸다.
  2. 행 수. 누적 행 수가 args.periodic_commit (기본값 10240) 에 도달. 버퍼를 b_handler 로 비우고 새 배치를 시작 한다.
  3. 버퍼 크기. 버퍼가 LOADDB_BUFFER_SIZE_LIMIT ((2 GiB - 1 KiB)) 를 넘으려 함. splitter는 가장 마지막의 완전한 행까지 잘라 비우고, 남은 행을 one_row_buffer 에 넣은 채로 계속 간다.

splitter를 까다롭게 만드는 두 가지 cross-line 조건이 있다.

  • 줄 이음. + 로 끝나는 라인은 “행이 다음 라인까지 이어 진다” 는 의미다. splitter는 그런 라인을 + 로 끝나지 않는 라인을 만날 때까지 one_row_buffer 에 보관해 두었다가 이어 붙인 결과를 한 행으로 센다.
  • 열려 있는 문자열 리터럴. 행 중간의 single quote가 여러 물리적 라인에 걸칠 수 있다. splitter는 single_quote_checker (' 가 나올 때마다 XOR로 토글되는 0/1 플래그) 를 추적하며, quote가 열린 동안에는 행 수 조건 이 비우게 했을 상황이라도 — 비우기를 거절한다.

배치가 다 차면 splitter는 handle_batch 를 부른다. 이 함수가 auto-증가하는 batch_id, 현재 class_id, 내용물, 시작 라인 오프셋, 행 수와 함께 cubload::batch 를 만들어 b_handler (batch) 를 호출한다. 두 핸들러는 load_db.c::load_object_file 안의 람다다.

// load_object_file — load_db.c:1510
batch_handler b_handler = [&] (const batch &batch) -> int {
bool use_temp_batch = false;
bool is_batch_accepted = false;
int error_code;
do {
load_status status;
error_code = loaddb_load_batch (batch, use_temp_batch,
is_batch_accepted, status);
if (error_code != NO_ERROR) return error_code;
use_temp_batch = true; // 재시도 중에는 다시 업로드하지 않는다
print_stats (status.get_load_stats (), *args, exit_status);
} while (!is_batch_accepted);
return error_code;
};
class_handler c_handler = [] (const batch &batch, bool &is_ignored) -> int {
std::string class_name;
int error_code = loaddb_install_class (batch, is_ignored, class_name);
if (error_code == NO_ERROR && !is_ignored && !class_name.empty ())
error_code = load_has_authorization (class_name, AU_INSERT);
return error_code;
};

재시도 루프가 본질이다. 서버의 워커 풀이 가득 차 있으면 loaddb_load_batchis_batch_accepted == false 로 돌아 올 수 있다. 클라이언트는 이미 보낸 배치 버퍼를 서버 측이 m_temp_task 에 들고 있도록 하고, 다음 호출에서 use_temp_batch = true 로 표시해 네트워크가 같은 바이트를 다시 실어 보내지 않게 한다.

클라이언트 측 stub은 network_interface_cl.cloaddb_load_batch 다. 이 함수는 packed cubload::batch 를 본문으로 한 NET_SERVER_LD_LOAD_BATCH 요청을 보낸다. 서버는 network_sr.c 에서 demultiplex한다.

// network_sr.c — request-table excerpt
req_p->processing_function = sloaddb_init; // NET_SERVER_LD_INIT
req_p->processing_function = sloaddb_install_class; // NET_SERVER_LD_INSTALL_CLASS
req_p->processing_function = sloaddb_load_batch; // NET_SERVER_LD_LOAD_BATCH
req_p->processing_function = sloaddb_fetch_status; // NET_SERVER_LD_FETCH_STATUS
req_p->processing_function = sloaddb_destroy; // NET_SERVER_LD_DESTROY
req_p->processing_function = sloaddb_interrupt; // NET_SERVER_LD_INTERRUPT
req_p->processing_function = sloaddb_update_stats; // NET_SERVER_LD_UPDATE_STATS

서버 측 핸들러 (network_interface_sr.cpp) 는 얇다. 요청을 풀고, session_get_load_session 으로 연결당 cubload::session 을 찾아내고, 매칭되는 메서드 (session::install_class, session::load_batch, session::fetch_status 등) 를 부르고, 응답을 packing해 돌려보낸다. session은 클라이언트의 session_state 구조체에 저장되므로 단일 TCP 연결이 로드 수명 전체에 걸쳐 단 하나의 cubload::session 을 본다.

sequenceDiagram
    participant L as loaddb client
    participant S as cub_server
    participant W as worker pool
    L->>S: NET_SERVER_LD_INIT (load_args)
    S->>S: new cubload::session(args)<br/>worker_manager_register_session
    S-->>L: NO_ERROR
    L->>S: NET_SERVER_LD_INSTALL_CLASS (%class 라인)
    S->>S: server_class_installer<br/>locate_class + BU_LOCK<br/>register_class_with_attributes
    S-->>L: class_name + is_ignored
    loop 배치마다 (size = periodic_commit)
        L->>S: NET_SERVER_LD_LOAD_BATCH (배치 바이트)
        S->>W: worker_manager_try_task(load_task)
        W->>W: logtb_assign_tran_index<br/>driver::parse<br/>server_object_loader::flush_records<br/>locator_multi_insert_force<br/>wait_for_previous_batch<br/>xtran_server_commit
        S-->>L: status + is_batch_accepted
    end
    L->>S: NET_SERVER_LD_UPDATE_STATS
    S->>S: enumerate class_registry<br/>pack OIDs back
    S-->>L: vector<OID>
    L->>L: 클래스마다 stats_update_statistics(STATS_WITH_SAMPLING)
    L->>S: NET_SERVER_LD_DESTROY
    S->>S: session.wait_for_completion<br/>worker_manager_unregister_session
    S-->>L: NO_ERROR

splitter가 %class 또는 %id 라인을 만나면 c_handler 를 부르고, 이는 CS 모드에서 session::install_class → invoke_parser → server_class_installer::install_class 로 들어간다. installer가 하는 일은 다음과 같다.

  1. 클래스 이름을 소문자로 변환 (to_lowercase_identifierintl_identifier_lower).
  2. 사용자가 지정한 --ignore-classes 집합에 속하는지 (is_class_ignored), 또는 legacy GLO 클래스인지 (IS_OLD_GLO_CLASS) 검사. 그렇다면 is_ignored = trueclass_entry 를 등록하고 일찍 반환.
  3. xlocator_find_class_oid 로 클래스를 이름으로 조회하되 BU_LOCK 을 요청 (server_class_installer::locate_class).
  4. 찾았다면 클래스 레코드를 가져와 attribute representation을 걸으며, %class 라인의 명시적 attribute 리스트로 선택적 필터/재배치를 한 뒤 session의 class_registryclass_entry 를 등록.

BU_LOCK 요청은 로드의 가장 결정적인 선택이다. BU_LOCKlock_table.h 에서 Bulk-Update Lock 으로 정의되며, 락 계층 구조에서 IX_LOCKX_LOCK 사이에 앉는다. 자기 자신과 같은 클래스 위의 다른 BU_LOCK 과는 호환된다 (즉 동일 클래스를 두 개의 동시 로더가 돌 수 있다). 그러나 보통의 writer는 모두 차단한다. X_LOCK, S_LOCK, IS_LOCK, SIX_LOCK 모두 같은 자원에 대한 BU_LOCK 과 호환되지 않는다 (호환성 행렬은 lock_table.c 에 있다). 결정적으로, BU_LOCK 을 들고 있다는 사실이 heap 매니저로 하여금 locator_multi_insert_force 안에서 페이지 레벨 로그 단축을 사용할 수 있게 해 준다.

// locator_multi_insert_force — locator_sr.c:13779
bool has_BU_lock =
lock_has_lock_on_object (class_oid, oid_Root_class_oid, BU_LOCK);
// ...
heap_max_page_size =
heap_nonheader_page_capacity () * (1.0f - PRM_HF_UNFILL_FACTOR);
// fresh page에 들어갈 수 있을 만큼 채운 다음:
pgbuf_log_redo_new_page (thread_p, home_hint_p.pgptr,
DB_PAGESIZE, PAGE_HEAP);

pgbuf_log_redo_new_page 가 벌크 로드 WAL 단축이다. 페이지 가 몇 개의 heap 레코드를 들고 있든 갓 찍은 페이지 전체를 로그 1건이 덮는다. BU_LOCK 이 없다면 다른 트랜잭션이 같은 페이지 의 부분 뷰를 들고 있을 수 있으므로, 엔진은 레코드 단위 physical -undo 로깅으로 fall back해야 한다.

CS 모드 session (load_session.cpp) 은 네트워크 핸들러 안에서 배치를 파싱하지 않는다. 대신 session::load_batchcubload::load_task 를 만들어 worker_manager_try_task 에 넘 긴 뒤 즉시 클라이언트로 돌아간다.

// session::load_batch — load_session.cpp
task = new load_task (*batch, *this, *thread_ref.conn_entry);
auto pred = [&] () -> bool {
is_batch_accepted = worker_manager_try_task (task);
if (is_batch_accepted) ++m_active_task_count;
else if (!use_temp_batch) {
m_temp_task = task; // 서버가 배치를 들고 있는다
use_temp_batch = true; // 클라이언트가 재전송을 건너뛰게
}
return !m_collected_stats.empty () || is_batch_accepted;
};

워커 풀 자체 (load_worker_manager.cpp) 는 프로세스 글로벌 풀이며 크기는 PRM_ID_LOADDB_WORKER_COUNT 가 정한다.

// REGISTER_WORKERPOOL — load_worker_manager.cpp:106
REGISTER_WORKERPOOL (loaddb, []() {
return prm_get_integer_value (PRM_ID_LOADDB_WORKER_COUNT);
});

이 풀은 서버 안의 동시 로드 세션 모두가 공유한다. 워커 스레드 마다 cubload::driverworker_entry_manager::on_create 에서 lazily 부착된다 (resource_shared_pool<driver> 에서 claim). driver는 on_retire 에서 driver::clear 로 리셋되고 풀로 돌 아간다. cubthread::worker_pool_task_capper 가 한 세션이 동시 에 풀에 띄울 수 있는 task 수를 제한한다.

load_task::execute (load_session.cpp:120) 가 워커 본체다.

// load_task::execute — load_session.cpp
void execute (cubthread::entry &thread_ref) final {
if (m_session.is_failed ()) return;
thread_ref.conn_entry = &m_conn_entry;
driver *driver = thread_ref.m_loaddb_driver;
init_driver (driver, m_session);
const class_entry *cls_entry =
m_session.get_class_registry ().get_class_entry (m_batch.get_class_id ());
// ...
logtb_assign_tran_index (&thread_ref, NULL_TRANID, TRAN_ACTIVE,
NULL, NULL, TRAN_LOCK_INFINITE_WAIT,
TRAN_DEFAULT_ISOLATION_LEVEL ());
int tran_index = thread_ref.tran_index;
m_session.register_tran_start (tran_index);
// session의 client id를 worker tdes로 복사
LOG_TDES *session_tdes =
log_Gl.trantable.all_tdes[m_conn_entry.get_tran_index ()];
LOG_TDES *worker_tdes = log_Gl.trantable.all_tdes[tran_index];
worker_tdes->client.set_ids (session_tdes->client);
bool parser_result = invoke_parser (driver, m_batch);
std::size_t rows_number =
driver->get_object_loader ().get_rows_number ();
driver->clear ();
if (m_session.is_failed () ||
(!is_syntax_check_only && (!parser_result || er_has_error ())))
{
m_session.fail ();
xtran_server_abort (&thread_ref);
}
else
{
m_session.wait_for_previous_batch (m_batch.get_id ());
xtran_server_commit (&thread_ref, false);
m_session.stats_update_rows_committed (rows_number);
m_session.stats_update_last_committed_line (line_no + 1);
}
notify_done_and_tran_end (tran_index);
}

여기서 중요한 두 가지 불변식.

  • 워커마다 자기 트랜잭션 위에서 돈다 (logtb_assign_tran_index → 배치마다 xtran_server_commit). 클래스 위의 BU_LOCK 은 외부 session으로부터 상속된다. 모든 워커가 동일 session에 자 기 트랜잭션을 등록하고, session의 트랜잭션이 install_class 시점에 그 락을 취득했기 때문이다.
  • commit은 wait_for_previous_batch (m_batch.get_id ()) 로 batch-id 순서가 보장된다. 배치 N 은 배치 N-1 이 commit하기 전에는 commit하지 못한다. 이렇게 해야 WAL의 외관 적 commit 순서가 파일의 라인 순서와 일치하고, 이는 HA 복제 와 crash 복구에 중요하다.

워커 안에서 invoke_parser 가 배치 content 문자열을 Bison 파서를 돌린다. 파서는 인스턴스 라인마다 constant_type * 연결 리스트로 reduce하고 server_object_loader::process_line(cons) 를 부른다.

// server_object_loader::process_line — load_server_loader.cpp:632
for (constant_type *c = cons; c != NULL; c = c->next, attr_index++) {
const attribute &attr = m_class_entry->get_attribute (attr_index);
int error_code = process_constant (c, attr);
if (error_code != NO_ERROR) {
m_error_handler.on_syntax_failure ();
return;
}
db_value &db_val = get_attribute_db_value (attr_index);
error_code = heap_attrinfo_set (
&m_class_entry->get_class_oid (),
attr.get_repr ().id, &db_val, &m_attrinfo);
// ...
}

process_constant 는 lexer가 붙인 LDR_* 타입 코드로 dispatch 한다. collection이나 monetary가 아닌 모든 것은 process_generic_constant, {…} set 리터럴은 process_collection_constant, 통화 값은 process_monetary_constant. 일반 변환은 load_db_value_converter.cppconv_func 행렬을 거치는데, 이 행렬은 [DB_TYPE][LDR_TYPE] 로 인덱싱된다. 행렬은 프로세스 시작 시점에 한 번만 초기화되고 (init_setters), 이후 행마다 의 dispatch는 단일 2차원 배열 lookup이다.

process_line 이 성공하면 grammar action이 finish_line 을 부른다. 이 함수는 메모리 안의 db_value 배열을 heap_attrinfo_transform_to_disk_except_lobrecord_descriptor 에 직렬화한 뒤, 워커의 m_recdes_collected 벡터에 push한다.

// server_object_loader::finish_line — load_server_loader.cpp:689
record_descriptor new_recdes (cubmem::STANDARD_BLOCK_ALLOCATOR);
RECDES *old_recdes = NULL;
if (heap_attrinfo_transform_to_disk_except_lob (
m_thread_ref, &m_attrinfo, old_recdes, &new_recdes) != S_SUCCESS)
{
m_error_handler.on_failure ();
return;
}
if (!m_error_handler.current_line_has_error ())
m_recdes_collected.push_back (std::move (new_recdes));

핵심은 레코드가 누적된다는 것 이다. 한 건씩 삽입되지 않는 다. 배치의 끝에서, 문법의 시작 규칙 (load_grammar.yyloader_start) 이 m_driver.get_object_loader ().flush_records () 를 부르고, 이것 이 벡터 전체를 storage 계층으로 dispatch한다.

// server_object_loader::flush_records — load_server_loader.cpp:728
log_sysop_start (m_thread_ref);
int error_code = locator_multi_insert_force (
m_thread_ref,
&m_scancache.node.hfid, &m_scancache.node.class_oid,
m_recdes_collected,
/*has_index=*/true,
/*op_type=*/MULTI_ROW_INSERT,
&m_scancache, &force_count, /*pruning_type=*/0,
NULL, NULL, UPDATE_INPLACE_NONE, /*dont_check_fk=*/true);
if (error_code == NO_ERROR) {
log_sysop_attach_to_outer (m_thread_ref);
m_rows += m_recdes_collected.size ();
}

locator_multi_insert_force (locator_sr.c:13779) 는 heap_alloc_new_page → heap_insert_logical(in_place) 로 레코드 를 fresh heap 페이지에 packing한 뒤, 채워진 페이지마다 한 건의 pgbuf_log_redo_new_page 로그 레코드를 발행한다. 위에서 말한 벌크 쓰기 단축이다. 인덱스는 여전히 inline으로 유지된다 (has_index=true) — CUBRID이 primary key 테이블의 secondary 인덱스를 아직 bulk build하지 않기 때문이다. 대신 heap insert 경로는 행 단위 INSERT가 쓰는 같은 btree_insert 를 쓰되, heap 페이지의 page-fix 수명에 걸쳐 amortise한다.

여기에 fallback 경로가 하나 있다. HA가 비활성이 아닐 때, “page -image-redo” 로그 레코드는 복제될 수 없다 (slave는 행마다 record-level LSA를 필요로 한다). 로더는 HA_DISABLED () 로 이 를 감지하고, 행마다 log_sysop_start / log_sysop_attach_to_outer 를 두른 locator_insert_force 의 per-record 루프로 fall back한다.

// flush_records HA 경로 — load_server_loader.cpp:766
if (insert_errors_filtered || !HA_DISABLED ()) {
for (size_t i = 0; i < m_recdes_collected.size (); i++) {
log_sysop_start (m_thread_ref);
RECDES local_record = m_recdes_collected[i].get_recdes ();
int error_code = locator_insert_force (m_thread_ref, &hfid,
&class_oid, &dummy_oid, &local_record, /*has_index=*/true,
op_type, &m_scancache, &force_count, /*pruning=*/0, NULL, NULL,
UPDATE_INPLACE_NONE, NULL, has_BU_lock,
/*dont_check_fk=*/true, /*ignore_serializable=*/false);
// ... per-record commit-or-skip with log_sysop_attach/abort
}
}

요컨대 CUBRID의 직접 경로 는 log-free 가 아니라 page-batched 다. 로그 레코드를 행 단위가 아닌 페이지 단위로 묶을 뿐, 페이지마다 redo 1건은 여전히 발행한다. 그 선택이 crash 복구를, 그리고 비-HA 모드에서는 복제 정합성을 보존하면서도 행 단위 WAL 대비 1~2자리 수의 차이로 그것을 이긴다.

flowchart LR
    subgraph WORKER["load_task::execute (worker thread)"]
      P1[bison parse batch] --> P2[행 단위<br/>process_line]
      P2 --> P3[heap_attrinfo_set]
      P3 --> P4[finish_line]
      P4 --> P5[heap_attrinfo_transform_to_disk_except_lob]
      P5 --> P6[m_recdes_collected.push_back]
      P6 -.다음 행.-> P2
      P6 --> P7[배치 끝:<br/>flush_records]
    end
    subgraph FLUSH["flush_records 분기"]
      P7 --> Q1{HA 비활성<br/>이고 에러 필터 없음?}
      Q1 -- yes --> Q2[locator_multi_insert_force<br/>page-image redo log]
      Q1 -- no --> Q3[행 단위<br/>locator_insert_force 루프]
    end
    Q2 --> R1[xtran_server_commit]
    Q3 --> R1
    R1 --> R2[wait_for_previous_batch<br/>그리고 commit]

외래키는 두 가지 방식으로 명시적으로 비활성화된다. SA 모드는 locator_Dont_check_foreign_key = true 를 직접 세팅한다. CS 모드는 locator_(multi_)insert_force 호출마다 dont_check_fk = true 를 넘긴다. 그래서 로드가 들여 놓은 FK 위반은 로드 시점에 잡히지 *않는다. 그 테이블이 다음에 참조될 때야 드러난다.

unique 제약 검사는 로드를 살아남되 연기된다. 워커가 행을 삽입할 때 heap 매니저는 인덱스 엔트리마다 btree_insert 를 부른다. 기존 leaf 위에서 unique 최적화는 단순히 클래스별 index_stats 카운트 (m_scancache.m_index_stats) 를 기록할 뿐이다. 로드 끝 에서 server_object_loader::stop_scancache 가 돌 때 m_scancache.m_index_stats->get_map() 를 걸어 unique를 assert하고, !is_unique() 인 인덱스가 있으면 BTREE_SET_UNIQUE_VIOLATION_ERROR 를 올리며 session을 실패 처리한다.

// stop_scancache — load_server_loader.cpp:1097
if (m_scancache.m_index_stats != NULL) {
for (const auto &it : m_scancache.m_index_stats->get_map ()) {
if (!it.second.is_unique ()) {
BTREE_SET_UNIQUE_VIOLATION_ERROR (
thread_get_thread_entry_info (), NULL, NULL,
&m_class_entry->get_class_oid (), &it.first, NULL);
m_error_handler.on_failure ();
break;
}
int error = logtb_tran_update_unique_stats (
thread_get_thread_entry_info (),
it.first, it.second, true);
}
}

트리거는 loaddb_internal 안에서 데이터 파일을 열기도 전에 db_disable_trigger () 로 무조건 비활성화된다.

NOT NULL은 inline으로 강제된다. load_db_value_converter.cpp 의 변환 함수는 LDR_NULL 상수가 not-null 어트리뷰트로 떨어지면 ER_OBJ_ATTRIBUTE_CANT_BE_NULL 을 올리고, class installer (register_class_with_attributes) 는 %class 라인이 생략한 어트리뷰트들을 걸으며 그중 하나라도 is_notnull 이면 설치를 거절한다.

인덱스는 데이터 파일 로더가 만들지 않는다. 대신 unloader가 인덱 스 하나하나를 SQL CREATE INDEX 문장으로 별도의 인덱스 파일 에 내보내고, loaddb_internal 이 데이터 파일 다음에 그 파일을 실행한다.

// loaddb_internal — load_db.c
if (index_file != NULL) {
print_log_msg (1, "\nStart index loading.\n");
// ldr_exec_query_from_file 가 문장을 파싱하고 실행
if (ldr_exec_query_from_file (args.index_file.c_str (),
index_file,
&index_file_start_line, &args)
!= NO_ERROR) {
// 에러 출력, restart 힌트, abort
}
sm_update_catalog_statistics (CT_INDEX_NAME, STATS_WITH_FULLSCAN);
sm_update_catalog_statistics (CT_INDEXKEY_NAME, STATS_WITH_FULLSCAN);
db_commit_transaction ();
}

각각의 CREATE INDEX 는 엔진의 평범한 인덱스 생성 경로를 탄다. 그 경로가 정렬 기반 bottom-up B+Tree 구축을 위한 btree_load_index + external_sort 다. 인덱스 파일이 돌 무렵 heap은 이미 채워져 있으니, 빌드는 행 집합 전체를 한 번의 패스 로 본다. 정확히 ## DBMS 공통 설계 패턴 의 drop-and-rebuild 스케줄이다. loaddb 고유의 튜닝은 LOAD_INDEX_MIN_SORT_BUFFER_PAGES (8192) 다. PRM_ID_SR_NBUFFERS 가 그 아래라면 loaddb가 sysprm_set_force 로 강제로 밀어 올려 서, 외부 정렬이 최소 64 MiB의 작업 메모리를 갖도록 보장한다.

flowchart LR
    A[unloaddb 출력<br/>schema.sql / data.obj / index.sql] --> B
    B[loaddb -s schema.sql] --> C
    C[loaddb -d data.obj<br/>BU_LOCK 위 직접 경로 heap insert<br/>· page-image redo] --> D
    D[loaddb -i index.sql<br/>라인마다 CREATE INDEX<br/>btree_load_index + external_sort] --> E
    E[loaddb_update_stats /<br/>로드된 클래스마다<br/>sm_update_statistics] --> F
    F[db_commit + db_shutdown]

트리거와 stored procedure는 자기 파일 (-t / --trigger-file) 에 산다. 인덱스 파일과 마찬가지로 데이터 로드 이후 ldr_exec_query_from_file 로 실행된다. 인덱스 파일과 다른 점은 sort-buffer override가 없다는 것이다. 문장은 그저 한 번에 하나씩 컴파일되고 실행되며, periodic_commit 이 실행된 문장 수에 적용된다.

CS 모드에서 로드 후 통계는 벌크 로드 워커 풀 바깥에서 산다. wait_for_completion 이 로드 완료를 알리고 나면, load_db.c::ldr_server_load 가 갱신 여부를 결정한다.

// ldr_server_load — load_db.c
if (!load_interrupted && !status.is_load_failed ()
&& !args->syntax_check && error_code == NO_ERROR
&& !args->disable_statistics)
{
error_code = loaddb_update_stats (args->verbose);
// 통계와 마지막 fetch 출력
}

loaddb_update_stats (network_interface_cl.c:10717) 의 모양은 독특하다. NET_SERVER_LD_UPDATE_STATS 요청을 보내면, 서버가 로드가 만진 클래스 OID 리스트 를 응답으로 돌려준다 (출처는 session.get_class_registry ()). 그러면 클라이언트가 그 OID를 순회하며 클래스마다 stats_update_statistics(STATS_WITH_SAMPLING) 를 부른다. 실제 히스토그램 빌드는 서버 측의 statistics_sr.c 안의 xstats_update_statistics 에서 돈다. 이를 단일 bulk 서버 호출 이 아닌 클라이언트 측 클래스별 순회로 두는 것은 의도적이다. 클라이언트가 클래스별 진행률 (LOADDB_MSG_CLASS_TITLE) 을 출력 하고 사용자의 verbose 선호를 존중할 수 있게 해 주기 때문이다.

SA 모드에서는 ldr_update_statistics (load_sa_loader.cpp:6627) 가 Classes 연결 리스트 (SA 로더의 자체 class registry) 를 걸으며 직접 sm_update_statistics(class_, STATS_WITH_SAMPLING) 를 부른다.

두 경로 모두 기본값은 샘플링 (STATS_WITH_SAMPLING) 이고, full scan이 아니다. full scan은 인덱스 파일 단계 뒤에 catalog 테이블 db_index / db_index_key 자체를 STATS_WITH_FULLSCAN 으로 다시 통계 잡을 때만 쓰인다.

에러는 세 계층에서 발생한다.

  1. Lexer / parser 에러 는 error_handler::on_error 를 거쳐 LOADDB_MSG_SYNTAX_ERR 를 올린다. CS 모드에서는 이 에러가 session의 m_stats.error_message 에 append된다. 클라이언트 는 (ldr_server_load 의 do-while 루프에서) 100 ms마다 loaddb_fetch_status 를 폴링해 도착하는 새 error_message 라인을 출력한다.
  2. 행 단위 삽입 에러 (타입 변환, NOT NULL, FK 위반 등) 는 error_handler::on_failure 가 보고한다. 에러 코드가 args.m_ignored_errors 안에 있으면 행을 건너뛰고 배치를 계속 진행한다. 그렇지 않으면 배치의 트랜잭션은 abort되고 session 은 실패로 표시된다.
  3. 세션 단위 interrupt (Ctrl-C, 시그널). 클라이언트의 register_signal_handlers 가 핸들러를 설치한다. 핸들러는 load_interrupted = true 를 세팅하고 loaddb_interrupt 를 부르는데, 이 함수가 NET_SERVER_LD_INTERRUPT 를 서버로 실어 보낸다. 서버 측의 session::interrupt 는 session의 활성 트랜잭션 인덱스 집합 을 걸으며 각각의 interrupt 플래그를 logtb_set_tran_index_interrupt 로 켜 준 뒤 session을 실패 로 표시한다.

재시작은 라인 기반이다. commit된 배치마다 stats.last_committed_line 이 마지막으로 성공적으로 commit된 행의 파일 오프셋으로 갱신된 다. interrupt 시 클라이언트는 LOADDB_MSG_LAST_COMMITTED_LINE 을 출력하고, 사용자는 데이터 파일 경로 뒤에 :line suffix를 붙여 loaddb를 다시 돌릴 수 있다. 라인 skip은 ldr_get_start_line_no 이 파일 경로에서 :N suffix를 떼어내고, ldr_exec_query_from_file (또는 새 파싱 후의 cubload::split) 이 거기로 점프하는 식으로 이루어진다.

문법 검사 (--check-only) 는 heap insert 직전까지의 파이프 라인 전체를 돈다. server_object_loader::flush_recordsargs.syntax_check 일 때 short-circuit해 recdes 벡터가 비어 있는지 assert하고, process_line 은 행 수만 센다. 모든 에러는 여전히 모이고 출력되지만, 사용자는 끝에서 깨끗한 DB를 얻는다.

병렬성은 배치 단위 이지 행 단위가 아니다. 배치 하나는 한 워커 스레드 위에서 끝까지 돈다. PRM_ID_LOADDB_WORKER_COUNT = 8periodic_commit = 10240 이라면, 같은 BU_LOCK 아래에서 8개의 워커가 각자 1만 행짜리 배치를 동시에 돌릴 수 있다. 클라 이언트의 배치 재시도 루프가 backpressure를 보장한다. 풀이 가득 차 있으면 worker_manager_try_task 가 false를 돌리고, 서버는 is_batch_accepted = false 로 응답하며, 클라이언트는 다시 폴링한다.

CUBRID은 loaddb에 명시적인 병렬 partition 로드 모드를 두지 않는다. partition된 클래스의 로드 파일에는 모든 partition의 행이 섞여 있고, inserter는 엔진의 평범한 partition pruning 기계 (locator_multi_insert_forcepruning_type 인자, flush_records 에서는 0 — 즉 DB_NOT_PARTITIONED_CLASS) 에 의존해 행마다 heap 매니저가 lazily 정확한 partition으로 dispatch하게 한다.

flowchart TD
    SPLIT["cubload::split (클라이언트 측)"]
    SPLIT --> B1["배치 1: 행 1..10240<br/>class_id 1"]
    SPLIT --> B2["배치 2: 행 10241..20480<br/>class_id 1"]
    SPLIT --> B3["배치 3: 행 20481..30720<br/>class_id 1"]
    SPLIT --> B4["배치 4: 행 30721..40960<br/>class_id 1"]
    B1 --> WP{worker_pool size<br/>= PRM_LOADDB_WORKER_COUNT}
    B2 --> WP
    B3 --> WP
    B4 --> WP
    WP --> W1[worker 1<br/>tran_index = T1<br/>commit ordered]
    WP --> W2[worker 2<br/>tran_index = T2<br/>commit ordered]
    WP --> W3[worker 3<br/>tran_index = T3<br/>commit ordered]
    W1 -. wait_for_previous_batch .-> W2
    W2 -. wait_for_previous_batch .-> W3
    W3 --> COMMIT[xtran_server_commit<br/>batch_id 순서로]

이 절은 위의 본문에 나오는 안정적 심볼 이름을 나열한다. 라인 번호는 updated: 날짜 시점의 것이다.

심볼역할
loaddb_user공개 유틸리티 진입점 — loaddb_internal(arg, 0) 으로 forward
loaddb_internal인자 검증, 로그인, 스키마/데이터/인덱스/트리거 드라이버
get_loaddb_argsUTIL_ARG_MAPcubload::load_args 에 매핑
ldr_validate_object_file인자 정합성 검사 (volume, 파일, HA-mode 제약)
ldr_check_file파일 열고 에러 코드와 함께 FILE * 반환
ldr_get_start_line_no파일 경로에서 :N suffix 파싱
register_signal_handlersSIGINT/SIGQUITload_interrupted = true 핸들러 설치
심볼역할
ldr_server_loadCS 모드 드라이버 — loaddb_init, load_object_file, loaddb_update_stats, loaddb_destroy
ldr_sa_loadSA 모드 드라이버 — ldr_init, ldr_Driver->parse, ldr_update_statistics
load_object_fileb_handler/c_handler 람다 구성 후 cubload::split 호출
cubload::split라인 단위 splitter — %class/%id 감지, 배치, 줄 이음, single-quote 추적
cubload::handle_batchbatch_buffercubload::batch 로 감싸 b_handler 호출
append_incomplete_row버퍼 오버플로 핸들러 — flush 후 계속
심볼역할
cubload::drivermediator — scanner + class_installer + object_loader + error_handler + semantic_helper
driver::initialize4개 컴포넌트의 소유권을 driver에 넘김
driver::parse스캐너 stream 전환 후 Bison 파서 실행
cubload::scannerload_lexer.l 에서 생성된 Flex C++ 스캐너
cubload::parserload_grammar.yy 에서 생성된 Bison C++ 파서
cubload::semantic_helperstring_type / constant_type / qstr_buf 의 풀 할당자
make_string_by_buffer / make_string_by_yytext풀에서 string_type 할당
make_constant / make_real / make_monetary_constant풀에서 constant_type 할당
reset_after_line / reset_after_batch풀 인덱스 리셋
심볼역할
cubload::load_argsCLI 플래그의 packable 구조체
cubload::batchpackable 구조체 — batch_id, class_id, content, 라인 오프셋, 행 수
cubload::statspackable 구조체 — rows_committed, current_line, last_committed_line, rows_failed, error/log 메시지
cubload::load_statuspackable 구조체 — client_type, completed, failed, vector
cubload::data_typeLDR_NULL / INT / STR / NUMERIC / DOUBLE / FLOAT / OID / DATE / TIME / TIMESTAMP / DATETIME / COLLECTION / MONETARY / BSTR / XSTR / JSON …
cubload::class_installer클래스 등록을 위한 순수 가상 인터페이스
cubload::object_loader행 삽입을 위한 순수 가상 인터페이스
심볼역할
cubload::session서버 위의 연결당 로드 상태
session::install_class파서로 클래스 등록
session::load_batch워커 풀에 load_task 큐잉, 재시도 의미론 포함
session::wait_for_previous_batchbatch_id 순서로 commit 정렬
session::wait_for_completionsloaddb_destroy 가 사용
session::fail / session::interruptsession 실패 표시, tran-index interrupt 플래그 세팅
session::stats_update_*살아 있는 stats를 atomic하게 갱신
session::fetch_statusstats와 최근 수집된 배치별 stats의 스냅샷
init_driver워커의 driver를 server_class_installer + server_object_loader 로 lazy init
invoke_parser배치 content를 Bison 파서 실행
cubload::class_registryclass_id → class_entry 맵 — mutex 보호
cubload::class_entryresolve된 클래스 — OID + 이름 + 순서 있는 어트리뷰트 + is_ignored
cubload::attributeresolve된 어트리뷰트 — 이름 + 인덱스 + or_attribute * repr
심볼역할
server_class_installer서버용 class_installer 구현
server_class_installer::locate_classxlocator_find_class_oidBU_LOCK 으로 호출
server_class_installer::locate_class_for_all_users모든 사용자 대상의 스키마 lookup (legacy 11.2 호환)
server_class_installer::register_class_with_attributes클래스의 or_attribute[] 로부터 class_entry 구성
server_object_loader직접 경로 heap insert를 위한 object_loader 구현
server_object_loader::initstart_scancache + start_attrinfo, BU_LOCK 보유 assert
server_object_loader::process_line행 단위 — 상수 타입 변환 + heap_attrinfo_set
server_object_loader::finish_lineheap_attrinfo_transform_to_disk_except_lobm_recdes_collected.push_back
server_object_loader::flush_records배치 끝 — locator_multi_insert_force (HA에서는 행 단위 루프)
server_object_loader::stop_scancacheunique 인덱스 stats 검증, 위반 시 BTREE_SET_UNIQUE_VIOLATION_ERROR
server_object_loader::process_constant / process_generic_constant / process_monetary_constant / process_collection_constant타입별 dispatch 상수 변환
server_object_loader::clear_db_values행 사이의 m_db_valuesm_attrinfo 리셋
심볼역할
cubload::load_taskinvoke_parser + commit을 돌리는 cubthread::entry_task
load_task::execute워커 본체 — tran 할당, 파싱, flush, 순서 commit, notify
worker_entry_managercubload::driver 인스턴스를 claim/retire하는 cubthread::entry_manager
worker_manager_register_session / worker_manager_unregister_sessionsession을 글로벌 활성 set에 추가/제거
worker_manager_try_taskworker_pool_task_capper 로 task를 풀에 넘김
worker_manager_stop_allshutdown 시 활성 session을 모두 interrupt
심볼역할
cubload::conv_funcint (*) (const char *, size_t, const attribute *, db_value *)
cubload::get_conv_func2차원 lookup setters[db_type][ldr_type]
cubload::init_setters행렬을 한 번에 채움
to_db_int / to_db_bigint / to_db_string / to_db_date / to_db_monetary / to_db_json(db_type, ldr_type) 별 변환
mismatch기본 동작 — ER_LDR_DOMAIN_MISMATCH 발행
심볼역할
loaddb_init (client) → sloaddb_init (server)cubload::session
loaddb_install_classsloaddb_install_class%class / %id 라인 파싱
loaddb_load_batchsloaddb_load_batch배치 제출 (use_temp_batch 재전송 플래그 포함)
loaddb_fetch_statussloaddb_fetch_statussession stats 폴링
loaddb_update_statssloaddb_update_stats클래스별 OID 리스트 수령, 클래스마다 stats_update_statistics 호출
loaddb_destroysloaddb_destroywait_for_completion 후 session 해제
loaddb_interruptsloaddb_interrupt활성 워커 모두에 tran-index interrupt 세팅
심볼역할
sa_class_installer / sa_object_loaderlegacy ldr_* 콜백을 감싼 SA 모드 어댑터
ldr_init_driverldr_Driver 와 SA installer/loader 쌍을 lazily 생성
ldr_init / ldr_start / ldr_finalload_sa_loader.cpp 안의 로드별 lifecycle
ldr_register_post_commit_handler / ldr_register_post_interrupt_handlersetjmp/longjmp commit-or-abort 배관
ldr_update_statisticsClasses 를 걸으며 sm_update_statistics(STATS_WITH_SAMPLING) 호출
ldr_signal_handlerldr_Load_interrupted 세팅
ldr_statsTotal_objects, Total_fails, Last_committed_line 의 스냅샷
심볼파일라인
loaddb_usersrc/loaddb/load_db.c957
loaddb_internalsrc/loaddb/load_db.c530
ldr_server_loadsrc/loaddb/load_db.c1305
load_object_filesrc/loaddb/load_db.c1510
get_loaddb_argssrc/loaddb/load_db.c1251
ldr_exec_query_from_filesrc/loaddb/load_db.c1018
cubload::splitsrc/loaddb/load_common.cpp678
cubload::handle_batchsrc/loaddb/load_common.cpp882
append_incomplete_rowsrc/loaddb/load_common.cpp649
cubload::load_argssrc/loaddb/load_common.hpp83
cubload::batchsrc/loaddb/load_common.hpp47
cubload::statssrc/loaddb/load_common.hpp255
cubload::data_type enumsrc/loaddb/load_common.hpp132
cubload::class_installersrc/loaddb/load_common.hpp313
cubload::object_loadersrc/loaddb/load_common.hpp372
cubload::sessionsrc/loaddb/load_session.hpp71
cubload::session::load_batchsrc/loaddb/load_session.cpp582
cubload::load_task::executesrc/loaddb/load_session.cpp120
init_driver (server)src/loaddb/load_session.cpp47
invoke_parsersrc/loaddb/load_session.cpp70
server_class_installer::locate_classsrc/loaddb/load_server_loader.cpp118
server_class_installer::register_class_with_attributessrc/loaddb/load_server_loader.cpp329
server_object_loader::initsrc/loaddb/load_server_loader.cpp591
server_object_loader::process_linesrc/loaddb/load_server_loader.cpp632
server_object_loader::finish_linesrc/loaddb/load_server_loader.cpp689
server_object_loader::flush_recordssrc/loaddb/load_server_loader.cpp728
server_object_loader::stop_scancachesrc/loaddb/load_server_loader.cpp1097
worker_entry_managersrc/loaddb/load_worker_manager.cpp50
REGISTER_WORKERPOOL (loaddb, …)src/loaddb/load_worker_manager.cpp106
worker_manager_register_sessionsrc/loaddb/load_worker_manager.cpp112
worker_manager_try_tasksrc/loaddb/load_worker_manager.cpp100
cubload::class_registrysrc/loaddb/load_class_registry.hpp87
class_registry::register_classsrc/loaddb/load_class_registry.cpp146
init_setters (conv 행렬)src/loaddb/load_db_value_converter.cpp86
get_conv_funcsrc/loaddb/load_db_value_converter.cpp195
driver::parsesrc/loaddb/load_driver.cpp85
loader_start (Bison action)src/loaddb/load_grammar.yy205
class_command (Bison rule)src/loaddb/load_grammar.yy279
instance_line (Bison rule)src/loaddb/load_grammar.yy419
ldr_sa_loadsrc/loaddb/load_sa_loader.cpp6330
ldr_update_statistics (SA)src/loaddb/load_sa_loader.cpp6627
sloaddb_initsrc/communication/network_interface_sr.cpp10559
sloaddb_install_classsrc/communication/network_interface_sr.cpp10583
sloaddb_load_batchsrc/communication/network_interface_sr.cpp10639
sloaddb_fetch_statussrc/communication/network_interface_sr.cpp10710
sloaddb_destroysrc/communication/network_interface_sr.cpp10763
sloaddb_update_statssrc/communication/network_interface_sr.cpp10806
loaddb_update_stats (client)src/communication/network_interface_cl.c10717
locator_multi_insert_forcesrc/transaction/locator_sr.c13779
locator_insert_forcesrc/transaction/locator_sr.c4938
BU_LOCK enum valuesrc/transaction/lock_table.h46
xstats_update_statisticssrc/storage/statistics_sr.c109

본 문서는 src/loaddb/, src/transaction/locator_sr.c, src/storage/statistics_sr.c, 그리고 network-interface 파일들 의 살아 있는 소스를 읽으며 작성되었다. 별도의 PDF raw 출처는 없다. 아래 항목은 이 지식 트리의 자매 분석 문서들과의 정합성을 맞춘 것이다.

  • cubrid-heap-manager.md 가 슬롯 페이지 레이아웃과 레코드 별 헤더를 다룬다. 벌크 로더는 새로운 페이지 레이아웃을 도입 하지 않는다. 평범한 INSERT가 쓰는 동일한 디스크 위 레코드 포맷을 만들어 내는 heap_attrinfo_transform_to_disk_except_lob 에 의존하고, WAL 절약을 위해 page-image-redo 경로를 탄다. 삽입 된 레코드의 MVCC 헤더는 평소 그대로 heap_insert_logical 가 찍는다 (로더의 트랜잭션 id가 레코드의 insert-MVCCID가 된다).
  • cubrid-btree.md 가 갓 만들어진 인덱스의 bulk-build 경로 로 btree_load_index 를 문서화한다. loaddb는 그 경로를 데이터 파일 로더가 아니라 인덱스 파일 (-i) 의 CREATE INDEX 문으로 호출한다. 그래서 데이터 로드 동안 btree.c와의 상호작용은 heap-page 수명에 걸쳐 amortise되는 행 단위 btree_insert 이 며, bottom-up bulk build는 데이터 이후 인덱스 파일 단계에서만 일어난다.
  • cubrid-locator.md 가 locator의 locator_(multi_)insert_force 를 다룬다. 로더의 사용은 순수한 직접 경로다. dont_check_fk = true, op_type = MULTI_ROW_INSERT, pruning_type = 0. BU_LOCK 요건은 locator_multi_insert_force 자신의 안에서 (lock_has_lock_on_object (class_oid, oid_Root_class_oid, BU_LOCK)) 검사되며, 이것이 page-image-redo 로그 단축의 잠금을 풀어 주는 열쇠다.
  • cubrid-statistics.mdxstats_update_statistics 를 문서화한다. 로더는 사용자가 발행하는 UPDATE STATISTICS 와 같은 진입점을 부른다. loaddb 고유의 점은, 클래스 OID별로 순회 하고 사용자 데이터에는 STATS_WITH_SAMPLING 을, 인덱스 파일 뒤의 catalog 테이블 갱신에는 STATS_WITH_FULLSCAN 을 선호 한다는 것뿐이다.
  • cubrid-external-sort.md-i 인덱스 파일 경로로 추이적으로 호출된다. 로더 고유의 다이얼은 LOAD_INDEX_MIN_SORT_BUFFER_PAGES = 8192 다. 인덱스 파일이 주어지면 loaddb_internalsysprm_set_force 로 강제로 올린다.
  • src/loaddb/AGENTS.md 가 로드 시점 파일로 load_object.cload_object_table.c 를 함께 나열한다. 살아 있는 소스를 읽어 보면 이 둘은 오직 SA 모드 로더 (load_sa_loader.cpp) 에서만 쓰인다. CS 모드는 이 파일들을 포함하지 않는다. SA 로더는 fast loaddb prototype 의 lineage를 그대로 유지하고 있어 CS 모드의 server_object_loader 보다 상당히 크다. 6.8K 라인 vs 1.2K 라인. 그러나 워커 풀과의 조율이 필요 없으므로 기능적 폭은 더 좁다.
  • 로드 도중의 온라인 스키마 변경. 로드 session이 들고 있는 BU_LOCK 은 같은 클래스에 대한 X_LOCK 을 차단하므로, 로드 대상 클래스에 대한 DDL은 로드 동안 배제된다. 그렇다면 그 클래 스가 FK로 참조하는 다른 클래스에 대한 DDL은? 로더는 FK 검사 를 건너뛰므로 (dont_check_fk), 참조되는 클래스에 평행으로 돌리는 ALTER TABLE 은 조용히 안전하다. 그것이 허용되어야 하는지는 정책의 문제다.
  • 배치 중간에서의 재개. 라인 N 부터의 재시작은 지원되지 만, Ncommit된 배치 경계 (기본값 10240 행) 다. 배치 중간에 끊긴 로드는 그 배치 전체를 잃는다. 배치 중간에서 재개 를 시작하게 해 주는 행 단위 WAL 마커는 없다. 그것을 추가 해야 할지 (그리고 page-image redo 단축에 어떤 비용을 치르는 지) 는 열려 있다.
  • on-wire 배치의 압축. 배치는 그 안의 행들의 raw 텍스트 내용 그대로 cubpacking::packer::pack_string 으로 packing 되어 실려 간다. 전송 계층 압축 (zstd / lz4) 은 없고, 배치는 최대 LOADDB_BUFFER_SIZE_LIMIT ≈ 2 GiB 까지 갈 수 있다. 대규모 WAN 로드에서 이 비용은 만만치 않다. batch::pack / batch::unpack 경로에 opt-in 압축기를 더할지는 열려 있는 최적화 문제다.
  • 로드 시점 의 bulk B+Tree 빌드. 오늘은 인덱스가 두 가지 중 하나다. heap 로드 동안 행마다 유지되거나 (로드 시작 시점에 인덱스가 이미 있을 때), 별도 파일의 CREATE INDEX 가 bottom-up으로 빌드하거나. 데이터 파일 전에 인덱스를 자동으로 drop했다가 끝나면 다시 만들어 주는 경로는 없다. 그 일은 unloader의 책임이다. loaddb —rebuild-indexes 플래그가 생긴다면 운영 워크플로를 단순화할 수 있겠지만 schema-loader 단계와의 조율이 필요하다.
  • partition을 인지하는 병렬 로드. 오늘 partition된 테이블 은 자기 행이 로드 파일 안에 섞여 있고, 행마다 heap 매니저의 pruning 로직이 dispatch한다. unloader가 partition별로 미리 분류해 둔 로드 파일이 있다면 워커마다 자기 partition의 HFID 로 cross-worker 조율 없이 쓸 수 있을 것이다. Oracle의 parallel-direct에 더 가까운 모양이다. 그 처리량 이득이 unloader와 grammar 변경을 정당화하는지는 열려 있는 설계 질문이다.
  • HA 모드 page-image redo. 오늘 HA는 flush_records 의 행 단위 루프를 강제한다. 복제 로그는 slave에서 replay 가능하기 위해 행마다 record-level LSA를 요구한다. page-image redo 레코 드를 이해하는 slave replay를 새로 설계하면 HA 클러스터 위에 서도 fast path를 풀 수 있다. 이것은 R&D 영역이지 현재 로더 기능이 아니다.

본 문서는 코드 only 다. raw/ 의 PDF나 PPTX 입력은 붙어 있지 않다. 읽기는 다음 CUBRID 소스 파일들 (references/cubrid/ 기 준 경로) 에 anchor를 두었다.

  • src/loaddb/load_db.c — 유틸리티 진입점, 스키마/인덱스/ 트리거 드라이버, ldr_server_load, ldr_exec_query_from_file.
  • src/loaddb/load_common.{hpp,cpp}load_args, batch, stats, load_status, class_installer / object_loader 인터페이스, 그리고 파일 splitter cubload::split.
  • src/loaddb/load_session.{hpp,cpp}cubload::session, load_task, init_driver, invoke_parser, wait_for_previous_batch.
  • src/loaddb/load_server_loader.{hpp,cpp}server_class_installer, server_object_loader, flush_records 의 page-image-redo 경로와 행 단위 HA 경로.
  • src/loaddb/load_sa_loader.{hpp,cpp}ldr_sa_load, ldr_update_statistics, legacy ldr_* 콜백 체인.
  • src/loaddb/load_worker_manager.{hpp,cpp} — 글로벌 loaddb 워커 풀, REGISTER_WORKERPOOL, worker_manager_try_task.
  • src/loaddb/load_class_registry.{hpp,cpp} — class_id 키의 resolve된 클래스 registry와 어트리뷰트 리스트.
  • src/loaddb/load_db_value_converter.{hpp,cpp}[DB_TYPE][LDR_TYPE] 변환 행렬.
  • src/loaddb/load_driver.{hpp,cpp} — driver mediator 클래스.
  • src/loaddb/load_grammar.yy, src/loaddb/load_lexer.l, src/loaddb/load_semantic_helper.hpp — Bison/Flex 문법과 풀 할당 semantic helper.
  • src/loaddb/load_error_handler.hpp — 템플릿 에러 포매팅과 라인별 부기.
  • src/communication/network_interface_cl.c, src/communication/network_interface_sr.cpp, src/communication/network_sr.c — 7개 메시지로 이루어진 loaddb_*/sloaddb_* 프로토콜.
  • src/transaction/locator_sr.clocator_multi_insert_force, locator_insert_force.
  • src/transaction/lock_table.h, src/transaction/lock_table.c, src/transaction/lock_manager.cBU_LOCK 값과 호환성 행렬.
  • src/storage/heap_file.cheap_attrinfo_*, heap_alloc_new_page, heap_insert_logical, heap_get_class_info, heap_scancache_*.
  • src/storage/btree_load.c, src/storage/external_sort.c — 인덱스 파일 경로에서 추이적으로 호출됨.
  • src/storage/statistics_sr.cxstats_update_statistics.

이론적 출처:

  • Petrov, Database Internals, 4장 Implementing B-Trees (bottom-up B+Tree 빌드) 와 5장 Transaction Processing (write amplification, 연기된 제약 검사).
  • Garcia-Molina / Ullman / Widom, Database Systems: The Complete Book, §16 Logging and Recovery (page-image vs record-level redo) 와 §13.7 “Variable-Length Data and Records”.
  • PostgreSQL 문서, COPY FROMpg_bulkload extension 매뉴얼.
  • Oracle SQL*Loader, Direct Path Load 챕터 — 직접 경로 기법의 정본급 출처.
  • MySQL Reference Manual, LOAD DATA INFILE 과 bulk-insert 최적화 노트.