[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 가 된다. 벌크 로더는 이
를 두 가지 방향에서 줄일 수 있다.
-
레코드 단위가 아닌 페이지 단위 로깅. 로더가 그 페이지의 유일한 writer라면 (로드 동안 클래스가 배타적으로 잠겨 있으면 참이다), 페이지 위에 몇 개의 레코드가 올라가 있든 “page-image redo” 한 건이 갓 만들어진 페이지 전체를 덮어 줄 수 있다. 수백 행이 로그 1건을 공유하게 되는 것이다.
-
인덱스 빌드 지연. 행이 도착할 때마다 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들
위에서 어떻게 실현하는지 추적한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서가 모델을 주었다면, 이 절은 거의 모든 row-oriented 엔진
이 운영급 벌크 로더를 출하할 때 따르는 엔지니어링 관행을
이름 붙여 본다. CUBRID이 ## CUBRID의 구현 에서 내리는 구체적
선택은 이 공유 설계 공간 안의 한 다이얼 조합으로 읽는 편이
좋다.
벌크 로드 파일 포맷
섹션 제목: “벌크 로드 파일 포맷”모든 로더는 모호하지 않은 디스크 위 문법이 필요하다. 현장에서 보이는 관행은 이렇다.
- 헤더 라인이 있는 CSV, 선택적으로 명시적 컬럼 리스트와
delimiter override (PostgreSQL
COPY FROM, MySQLLOAD DATA INFILE, SQL Server BCP). 만들기 싸고, 파일 자체에 스키마는 없다. - 이진 테이블 덤프 포맷, 테이블을 인지하며 같은 엔진의 버전
사이에서 portable하다 (Oracle SQL*Loader의 직접 경로,
PostgreSQL
pg_dumpcustom 포맷, MySQLmysqldump --tab의.txt/.sql쌍). 파싱은 텍스트보다 빠르지만 엔진 종속적이 다. - CUBRID 포맷 객체 파일, 둘의 hybrid다. SQL 스타일 스키마
문장 뒤에 대상 테이블 이름을 잡아 주는
%class/%id지시자가 따라오고, 그 뒤로 작은 lexer가 토큰의 타입을 정해 주는 인스턴스 라인 (string, integer, timestamp, 통화의\KRW, set의{…}, 객체 참조의@oid, 줄 이음의+등) 이 이어진다. 이 포맷은 unloader (unloaddb) 에서 출발해 그것 과 대칭이다.
직접 경로 vs SQL-INSERT 경로
섹션 제목: “직접 경로 vs SQL-INSERT 경로”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 인덱스가 달린 테이블을 받았을 때 로더에는 두 가지 스케줄이 있다.
-
점진적 유지. 삽입된 heap 레코드마다 k 개의 인덱스 leaf 쓰기가 inline으로 발생한다. 구현이 간단하다 (heap insert가 이미
locator_attribute_info_force를 구동하고 그것이btree_insert를 부른다). 그러나 인덱스 페이지가 무작위 키 순으로 만져지므로 buffer pool이 churn하고, WAL이 행당 k 건 씩 부풀어 오른다. -
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 구축 — 가 된다.
병렬 partition 로딩
섹션 제목: “병렬 partition 로딩”엔진들 사이에서 보이는 패턴은 셋이다.
- 단일 프로세스, 다중 스레드 배치 (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, MySQLANALYZE TABLE, OracleDBMS_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의 구현
섹션 제목: “CUBRID의 구현”CUBRID은 벌크 로드를 CS 모드에서는 두 프로세스 파이프라인,
SA 모드에서는 단일 프로세스 직접 경로 로 구현한다. 둘은 같
은 로드 파일 문법 (load_grammar.yy + load_lexer.l), 같은
배치 알고리즘 (load_common.cpp 의 cubload::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|22 'Bob' 60000.00 NULL%id <class-name> <numeric-id>라인은 클래스 이름을 잡아 주 고, 배치의 나머지에서 쓸 numeric class-id를 부여한다.load_grammar.yy의id_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_line→process_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.hpp 의 cubload::load_args 위로
매핑되며, load_db.c 의 get_loaddb_args 가 풀어 준다.
| 플래그 (long / short) | load_args 필드 | 기본값 |
|---|---|---|
--user / -u | user_name | AU_PUBLIC_USER_NAME |
--password / -p | password | empty (ER_AU_INVALID_PASSWORD 시 prompt) |
--schema-file / -s | schema_file | empty |
--index-file / -i | index_file | empty |
--data-file / -d | object_file | empty |
--check-only / -c | syntax_check | false |
--load-only / -l | load_only | false |
--periodic-commit | periodic_commit | 10240 (PERIODIC_COMMIT_DEFAULT_VALUE) |
--no-statistics | disable_statistics | false |
--ignore-logging | ignore_logging | false |
--error-control-file | error_file | empty |
--ignore-classes | ignore_class_file | empty |
--table / -t | table_name | empty (전체 파일 로드) |
--CS-mode / -C | cs_mode | false |
--no-user-specified-name | no_user_specified_name | false |
이 구조체가 cubpacking::packable_object 에서 파생되는 이유는
CS 모드에서 load_args 통째로가 sloaddb_init 요청 본문에
실려 서버로 전송되기 때문이다.
Lexer / parser 파이프라인
섹션 제목: “Lexer / parser 파이프라인”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.cppintdriver::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가 살아 있는 driver 와
semantic_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만을
일으킨다.
Splitter와 배치화
섹션 제목: “Splitter와 배치화”load_common.cpp 의 cubload::split 가 I/O 프론트엔드다. 객체 파일을 열고, 라인 단위로 걸으며, 라인을 batch_buffer 에
누적하다가, 다음 세 조건 중 하나가 트리거되면 버퍼를 비운다.
- 클래스 경계.
%class또는%CLASS로 시작하는 라인 (splitter는 lexer가 보기도 전에 prefix를 텍스트로 검사한다). 현재 배치를b_handler로 비우고, class-id 카운터를 증가시킨 뒤, 새%class또는%id라인을 혼자만c_handler에 보낸다. - 행 수. 누적 행 수가
args.periodic_commit(기본값 10240) 에 도달. 버퍼를b_handler로 비우고 새 배치를 시작 한다. - 버퍼 크기. 버퍼가
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:1510batch_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_batch 는 is_batch_accepted == false 로 돌아 올
수 있다. 클라이언트는 이미 보낸 배치 버퍼를 서버 측이
m_temp_task 에 들고 있도록 하고, 다음 호출에서
use_temp_batch = true 로 표시해 네트워크가 같은 바이트를 다시
실어 보내지 않게 한다.
네트워크 배관
섹션 제목: “네트워크 배관”클라이언트 측 stub은 network_interface_cl.c 의
loaddb_load_batch 다. 이 함수는 packed cubload::batch 를
본문으로 한 NET_SERVER_LD_LOAD_BATCH 요청을 보낸다. 서버는
network_sr.c 에서 demultiplex한다.
// network_sr.c — request-table excerptreq_p->processing_function = sloaddb_init; // NET_SERVER_LD_INITreq_p->processing_function = sloaddb_install_class; // NET_SERVER_LD_INSTALL_CLASSreq_p->processing_function = sloaddb_load_batch; // NET_SERVER_LD_LOAD_BATCHreq_p->processing_function = sloaddb_fetch_status; // NET_SERVER_LD_FETCH_STATUSreq_p->processing_function = sloaddb_destroy; // NET_SERVER_LD_DESTROYreq_p->processing_function = sloaddb_interrupt; // NET_SERVER_LD_INTERRUPTreq_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
Class installer와 Bulk-Update 락
섹션 제목: “Class installer와 Bulk-Update 락”splitter가 %class 또는 %id 라인을 만나면 c_handler 를
부르고, 이는 CS 모드에서
session::install_class → invoke_parser → server_class_installer::install_class
로 들어간다. installer가 하는 일은 다음과 같다.
- 클래스 이름을 소문자로 변환 (
to_lowercase_identifier→intl_identifier_lower). - 사용자가 지정한
--ignore-classes집합에 속하는지 (is_class_ignored), 또는 legacy GLO 클래스인지 (IS_OLD_GLO_CLASS) 검사. 그렇다면is_ignored = true의class_entry를 등록하고 일찍 반환. xlocator_find_class_oid로 클래스를 이름으로 조회하되BU_LOCK을 요청 (server_class_installer::locate_class).- 찾았다면 클래스 레코드를 가져와 attribute representation을
걸으며,
%class라인의 명시적 attribute 리스트로 선택적 필터/재배치를 한 뒤 session의class_registry에class_entry를 등록.
BU_LOCK 요청은 로드의 가장 결정적인 선택이다. BU_LOCK 은
lock_table.h 에서 Bulk-Update Lock 으로 정의되며, 락 계층
구조에서 IX_LOCK 과 X_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:13779bool 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_batch 가
cubload::load_task 를 만들어 worker_manager_try_task 에 넘
긴 뒤 즉시 클라이언트로 돌아간다.
// session::load_batch — load_session.cpptask = 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:106REGISTER_WORKERPOOL (loaddb, []() { return prm_get_integer_value (PRM_ID_LOADDB_WORKER_COUNT);});이 풀은 서버 안의 동시 로드 세션 모두가 공유한다. 워커 스레드
마다 cubload::driver 가 worker_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.cppvoid 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 복구에 중요하다.
직접 경로 heap insert
섹션 제목: “직접 경로 heap insert”워커 안에서 invoke_parser 가 배치 content 문자열을 Bison
파서를 돌린다. 파서는 인스턴스 라인마다 constant_type * 연결
리스트로 reduce하고 server_object_loader::process_line(cons)
를 부른다.
// server_object_loader::process_line — load_server_loader.cpp:632for (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.cpp 의 conv_func 행렬을 거치는데, 이
행렬은 [DB_TYPE][LDR_TYPE] 로 인덱싱된다. 행렬은 프로세스
시작 시점에 한 번만 초기화되고 (init_setters), 이후 행마다
의 dispatch는 단일 2차원 배열 lookup이다.
process_line 이 성공하면 grammar action이 finish_line 을
부른다. 이 함수는 메모리 안의 db_value 배열을
heap_attrinfo_transform_to_disk_except_lob 로 record_descriptor
에 직렬화한 뒤, 워커의 m_recdes_collected 벡터에 push한다.
// server_object_loader::finish_line — load_server_loader.cpp:689record_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.yy 의
loader_start) 이
m_driver.get_object_loader ().flush_records () 를 부르고, 이것
이 벡터 전체를 storage 계층으로 dispatch한다.
// server_object_loader::flush_records — load_server_loader.cpp:728log_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:766if (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]
제약과 unique 처리
섹션 제목: “제약과 unique 처리”외래키는 두 가지 방식으로 명시적으로 비활성화된다. 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:1097if (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 이면 설치를 거절한다.
인덱스 로딩 (-i 파일)
섹션 제목: “인덱스 로딩 (-i 파일)”인덱스는 데이터 파일 로더가 만들지 않는다. 대신 unloader가 인덱
스 하나하나를 SQL CREATE INDEX 문장으로 별도의 인덱스 파일
에 내보내고, loaddb_internal 이 데이터 파일 다음에 그 파일을
실행한다.
// loaddb_internal — load_db.cif (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]
트리거 (--trigger-file 파일)
섹션 제목: “트리거 (--trigger-file 파일)”트리거와 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.cif (!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 으로 다시 통계 잡을 때만 쓰인다.
에러 처리, 재시작, 문법 검사
섹션 제목: “에러 처리, 재시작, 문법 검사”에러는 세 계층에서 발생한다.
- 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 라인을 출력한다. - 행 단위 삽입 에러 (타입 변환, NOT NULL, FK 위반 등) 는
error_handler::on_failure가 보고한다. 에러 코드가args.m_ignored_errors안에 있으면 행을 건너뛰고 배치를 계속 진행한다. 그렇지 않으면 배치의 트랜잭션은 abort되고 session 은 실패로 표시된다. - 세션 단위 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_records 는
args.syntax_check 일 때 short-circuit해 recdes 벡터가 비어
있는지 assert하고, process_line 은 행 수만 센다. 모든 에러는
여전히 모이고 출력되지만, 사용자는 끝에서 깨끗한 DB를 얻는다.
병렬성과 partition
섹션 제목: “병렬성과 partition”병렬성은 배치 단위 이지 행 단위가 아니다. 배치 하나는 한
워커 스레드 위에서 끝까지 돈다. PRM_ID_LOADDB_WORKER_COUNT = 8
과 periodic_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_force 의 pruning_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: 날짜 시점의 것이다.
진입점과 CLI
섹션 제목: “진입점과 CLI”| 심볼 | 역할 |
|---|---|
loaddb_user | 공개 유틸리티 진입점 — loaddb_internal(arg, 0) 으로 forward |
loaddb_internal | 인자 검증, 로그인, 스키마/데이터/인덱스/트리거 드라이버 |
get_loaddb_args | UTIL_ARG_MAP 을 cubload::load_args 에 매핑 |
ldr_validate_object_file | 인자 정합성 검사 (volume, 파일, HA-mode 제약) |
ldr_check_file | 파일 열고 에러 코드와 함께 FILE * 반환 |
ldr_get_start_line_no | 파일 경로에서 :N suffix 파싱 |
register_signal_handlers | SIGINT/SIGQUIT → load_interrupted = true 핸들러 설치 |
파일 드라이버와 람다 핸들러
섹션 제목: “파일 드라이버와 람다 핸들러”| 심볼 | 역할 |
|---|---|
ldr_server_load | CS 모드 드라이버 — loaddb_init, load_object_file, loaddb_update_stats, loaddb_destroy |
ldr_sa_load | SA 모드 드라이버 — ldr_init, ldr_Driver->parse, ldr_update_statistics |
load_object_file | b_handler/c_handler 람다 구성 후 cubload::split 호출 |
cubload::split | 라인 단위 splitter — %class/%id 감지, 배치, 줄 이음, single-quote 추적 |
cubload::handle_batch | batch_buffer 를 cubload::batch 로 감싸 b_handler 호출 |
append_incomplete_row | 버퍼 오버플로 핸들러 — flush 후 계속 |
Lexer / parser / driver
섹션 제목: “Lexer / parser / driver”| 심볼 | 역할 |
|---|---|
cubload::driver | mediator — scanner + class_installer + object_loader + error_handler + semantic_helper |
driver::initialize | 4개 컴포넌트의 소유권을 driver에 넘김 |
driver::parse | 스캐너 stream 전환 후 Bison 파서 실행 |
cubload::scanner | load_lexer.l 에서 생성된 Flex C++ 스캐너 |
cubload::parser | load_grammar.yy 에서 생성된 Bison C++ 파서 |
cubload::semantic_helper | string_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_args | CLI 플래그의 packable 구조체 |
cubload::batch | packable 구조체 — batch_id, class_id, content, 라인 오프셋, 행 수 |
cubload::stats | packable 구조체 — rows_committed, current_line, last_committed_line, rows_failed, error/log 메시지 |
cubload::load_status | packable 구조체 — client_type, completed, failed, vector |
cubload::data_type | LDR_NULL / INT / STR / NUMERIC / DOUBLE / FLOAT / OID / DATE / TIME / TIMESTAMP / DATETIME / COLLECTION / MONETARY / BSTR / XSTR / JSON … |
cubload::class_installer | 클래스 등록을 위한 순수 가상 인터페이스 |
cubload::object_loader | 행 삽입을 위한 순수 가상 인터페이스 |
서버 측 session
섹션 제목: “서버 측 session”| 심볼 | 역할 |
|---|---|
cubload::session | 서버 위의 연결당 로드 상태 |
session::install_class | 파서로 클래스 등록 |
session::load_batch | 워커 풀에 load_task 큐잉, 재시도 의미론 포함 |
session::wait_for_previous_batch | batch_id 순서로 commit 정렬 |
session::wait_for_completion | sloaddb_destroy 가 사용 |
session::fail / session::interrupt | session 실패 표시, tran-index interrupt 플래그 세팅 |
session::stats_update_* | 살아 있는 stats를 atomic하게 갱신 |
session::fetch_status | stats와 최근 수집된 배치별 stats의 스냅샷 |
init_driver | 워커의 driver를 server_class_installer + server_object_loader 로 lazy init |
invoke_parser | 배치 content를 Bison 파서 실행 |
cubload::class_registry | class_id → class_entry 맵 — mutex 보호 |
cubload::class_entry | resolve된 클래스 — OID + 이름 + 순서 있는 어트리뷰트 + is_ignored |
cubload::attribute | resolve된 어트리뷰트 — 이름 + 인덱스 + or_attribute * repr |
서버 측 직접 경로 구현
섹션 제목: “서버 측 직접 경로 구현”| 심볼 | 역할 |
|---|---|
server_class_installer | 서버용 class_installer 구현 |
server_class_installer::locate_class | xlocator_find_class_oid 를 BU_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::init | start_scancache + start_attrinfo, BU_LOCK 보유 assert |
server_object_loader::process_line | 행 단위 — 상수 타입 변환 + heap_attrinfo_set |
server_object_loader::finish_line | heap_attrinfo_transform_to_disk_except_lob → m_recdes_collected.push_back |
server_object_loader::flush_records | 배치 끝 — locator_multi_insert_force (HA에서는 행 단위 루프) |
server_object_loader::stop_scancache | unique 인덱스 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_values 와 m_attrinfo 리셋 |
워커 풀
섹션 제목: “워커 풀”| 심볼 | 역할 |
|---|---|
cubload::load_task | invoke_parser + commit을 돌리는 cubthread::entry_task |
load_task::execute | 워커 본체 — tran 할당, 파싱, flush, 순서 commit, notify |
worker_entry_manager | cubload::driver 인스턴스를 claim/retire하는 cubthread::entry_manager |
worker_manager_register_session / worker_manager_unregister_session | session을 글로벌 활성 set에 추가/제거 |
worker_manager_try_task | worker_pool_task_capper 로 task를 풀에 넘김 |
worker_manager_stop_all | shutdown 시 활성 session을 모두 interrupt |
변환 행렬
섹션 제목: “변환 행렬”| 심볼 | 역할 |
|---|---|
cubload::conv_func | int (*) (const char *, size_t, const attribute *, db_value *) |
cubload::get_conv_func | 2차원 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_class → sloaddb_install_class | %class / %id 라인 파싱 |
loaddb_load_batch → sloaddb_load_batch | 배치 제출 (use_temp_batch 재전송 플래그 포함) |
loaddb_fetch_status → sloaddb_fetch_status | session stats 폴링 |
loaddb_update_stats → sloaddb_update_stats | 클래스별 OID 리스트 수령, 클래스마다 stats_update_statistics 호출 |
loaddb_destroy → sloaddb_destroy | wait_for_completion 후 session 해제 |
loaddb_interrupt → sloaddb_interrupt | 활성 워커 모두에 tran-index interrupt 세팅 |
SA 모드 드라이버
섹션 제목: “SA 모드 드라이버”| 심볼 | 역할 |
|---|---|
sa_class_installer / sa_object_loader | legacy ldr_* 콜백을 감싼 SA 모드 어댑터 |
ldr_init_driver | ldr_Driver 와 SA installer/loader 쌍을 lazily 생성 |
ldr_init / ldr_start / ldr_final | load_sa_loader.cpp 안의 로드별 lifecycle |
ldr_register_post_commit_handler / ldr_register_post_interrupt_handler | setjmp/longjmp commit-or-abort 배관 |
ldr_update_statistics | Classes 를 걸으며 sm_update_statistics(STATS_WITH_SAMPLING) 호출 |
ldr_signal_handler | ldr_Load_interrupted 세팅 |
ldr_stats | Total_objects, Total_fails, Last_committed_line 의 스냅샷 |
이 개정 시점의 위치 힌트
섹션 제목: “이 개정 시점의 위치 힌트”| 심볼 | 파일 | 라인 |
|---|---|---|
loaddb_user | src/loaddb/load_db.c | 957 |
loaddb_internal | src/loaddb/load_db.c | 530 |
ldr_server_load | src/loaddb/load_db.c | 1305 |
load_object_file | src/loaddb/load_db.c | 1510 |
get_loaddb_args | src/loaddb/load_db.c | 1251 |
ldr_exec_query_from_file | src/loaddb/load_db.c | 1018 |
cubload::split | src/loaddb/load_common.cpp | 678 |
cubload::handle_batch | src/loaddb/load_common.cpp | 882 |
append_incomplete_row | src/loaddb/load_common.cpp | 649 |
cubload::load_args | src/loaddb/load_common.hpp | 83 |
cubload::batch | src/loaddb/load_common.hpp | 47 |
cubload::stats | src/loaddb/load_common.hpp | 255 |
cubload::data_type enum | src/loaddb/load_common.hpp | 132 |
cubload::class_installer | src/loaddb/load_common.hpp | 313 |
cubload::object_loader | src/loaddb/load_common.hpp | 372 |
cubload::session | src/loaddb/load_session.hpp | 71 |
cubload::session::load_batch | src/loaddb/load_session.cpp | 582 |
cubload::load_task::execute | src/loaddb/load_session.cpp | 120 |
init_driver (server) | src/loaddb/load_session.cpp | 47 |
invoke_parser | src/loaddb/load_session.cpp | 70 |
server_class_installer::locate_class | src/loaddb/load_server_loader.cpp | 118 |
server_class_installer::register_class_with_attributes | src/loaddb/load_server_loader.cpp | 329 |
server_object_loader::init | src/loaddb/load_server_loader.cpp | 591 |
server_object_loader::process_line | src/loaddb/load_server_loader.cpp | 632 |
server_object_loader::finish_line | src/loaddb/load_server_loader.cpp | 689 |
server_object_loader::flush_records | src/loaddb/load_server_loader.cpp | 728 |
server_object_loader::stop_scancache | src/loaddb/load_server_loader.cpp | 1097 |
worker_entry_manager | src/loaddb/load_worker_manager.cpp | 50 |
REGISTER_WORKERPOOL (loaddb, …) | src/loaddb/load_worker_manager.cpp | 106 |
worker_manager_register_session | src/loaddb/load_worker_manager.cpp | 112 |
worker_manager_try_task | src/loaddb/load_worker_manager.cpp | 100 |
cubload::class_registry | src/loaddb/load_class_registry.hpp | 87 |
class_registry::register_class | src/loaddb/load_class_registry.cpp | 146 |
init_setters (conv 행렬) | src/loaddb/load_db_value_converter.cpp | 86 |
get_conv_func | src/loaddb/load_db_value_converter.cpp | 195 |
driver::parse | src/loaddb/load_driver.cpp | 85 |
loader_start (Bison action) | src/loaddb/load_grammar.yy | 205 |
class_command (Bison rule) | src/loaddb/load_grammar.yy | 279 |
instance_line (Bison rule) | src/loaddb/load_grammar.yy | 419 |
ldr_sa_load | src/loaddb/load_sa_loader.cpp | 6330 |
ldr_update_statistics (SA) | src/loaddb/load_sa_loader.cpp | 6627 |
sloaddb_init | src/communication/network_interface_sr.cpp | 10559 |
sloaddb_install_class | src/communication/network_interface_sr.cpp | 10583 |
sloaddb_load_batch | src/communication/network_interface_sr.cpp | 10639 |
sloaddb_fetch_status | src/communication/network_interface_sr.cpp | 10710 |
sloaddb_destroy | src/communication/network_interface_sr.cpp | 10763 |
sloaddb_update_stats | src/communication/network_interface_sr.cpp | 10806 |
loaddb_update_stats (client) | src/communication/network_interface_cl.c | 10717 |
locator_multi_insert_force | src/transaction/locator_sr.c | 13779 |
locator_insert_force | src/transaction/locator_sr.c | 4938 |
BU_LOCK enum value | src/transaction/lock_table.h | 46 |
xstats_update_statistics | src/storage/statistics_sr.c | 109 |
교차 검증 노트
섹션 제목: “교차 검증 노트”본 문서는 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.md가xstats_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_internal이sysprm_set_force로 강제로 올린다.src/loaddb/의AGENTS.md가 로드 시점 파일로load_object.c와load_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부터의 재시작은 지원되지 만,N은 commit된 배치 경계 (기본값 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인터페이스, 그리고 파일 splittercubload::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, legacyldr_*콜백 체인.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.c—locator_multi_insert_force,locator_insert_force.src/transaction/lock_table.h,src/transaction/lock_table.c,src/transaction/lock_manager.c—BU_LOCK값과 호환성 행렬.src/storage/heap_file.c—heap_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.c—xstats_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 FROM과pg_bulkloadextension 매뉴얼. - Oracle SQL*Loader, Direct Path Load 챕터 — 직접 경로 기법의 정본급 출처.
- MySQL Reference Manual,
LOAD DATA INFILE과 bulk-insert 최적화 노트.