콘텐츠로 이동

[KO] CUBRID 병렬 질의 — Heap Scan, Hash Join, Query Execute에 걸친 Intra-Query 병렬성

목차

관계형 실행기는 질의 계획을 튜플의 스트림으로 변환한다. 계획 자체나 그 입력이 큰 경우 그 일을 한 CPU에서만 한다는 것은 다른 코어를 놀게 두면서 응답 지연을 늘린다는 뜻이다. Intra-query 병렬성 은 계획 안의 어떤 연산자 (또는 그 leaf) 의 사본 여러 개를 동시에 돌리고, 결과를 다시 모아서 그 위의 소비자가 직렬 계획에서 보았을 동일한 출력을 보게 한다.

이 분야의 출발점은 Graefe의 Encapsulation of Parallelism in the Volcano Query Processing System (SIGMOD 1990) 이다. Graefe는 직렬 iterator (open/next/close) 가 exchange 라는 연산자 하나만 추가되면 병렬성에 충분한 계약이 된다고 관찰했다. exchange가 생산자 thread와 소비자 thread 사이의 큐, 입력을 생산자에 매핑하는 분할 정책, flow control 을 모두 소유한다. 거기 서 네 가지 변종이 떨어져 나온다 — Gather (N→1), Scatter (1→N hash 또는 range 분할), Repartition (N→N 새 함수), Replicate (1→N broadcast). 대부분의 엔진은 이 중 일부만 고른다.

그 안에서 의미 있는 개념이 셋이다. 병렬도 (DOP, degree of parallelism) 는 정수 N으로, 하드웨어 (코어 수), 소프트웨어 (전역 thread 예산), 데이터 (페이지 4개를 스캔하는데 worker 8개를 띄우는 것은 의미 없음) 에 의해 위로 잘린다. 분할 은 block- range (heap의 N개의 연속 영역), hash (조인 키로 N개의 버킷), round-robin 중 하나다. 이 선택이 위쪽 exchange가 무엇을 할 수 있는지를 제약한다 — hash는 partition-aware 라 위의 join이 파티션 별로 돌 수 있지만 block-range는 그렇지 않다. 스케줄링 은 오래 사는 worker (Postgres BackgroundWorker, Oracle PX server) 와 연산자 사이를 오가며 재활용되는 짧게 사는 task (DuckDB, CUBRID) 의 두 갈래다.

이 그림을 두 가지 정련이 마무리한다. shared-build 패턴 (Anatomy of a Database System, Red Book 4장) 은 병렬 hash join 을 위한 것이다 — inner 측을 한 번만 공유 구조로 build하고 N개의 probe worker가 동시에 읽게 한다. CUBRID은 약화된 형태를 구현한다 — 공유되는 것은 hash table 자체가 아니라 파티션 풀 이다. 2단계 parallel sort (Database Systems: The Complete Book §15.4) 는 N개의 worker가 각자 한 슬라이스를 정렬한 뒤 merge한다. CUBRID의 parallel sort가 정확히 그 slice-and-merge 다.

이 계층이 어디에 앉느냐가 책임을 결정한다. 위로는 qexec_execute_mainblock, scan_next_scan, qexec_hash_join, sort_listfile_internal 이 직렬 경로가 만들어 내던 것과 같은 도메인 (DB_VALUE, XASL_NODE, QFILE_LIST_ID) 에서 튜플을 요구하므로 소비자는 차이를 알 수 없다. 아래로는 access method가 페이지 단위 인터페이스를 노출하므로, 병렬 계층은 직렬 latch 프로토콜을 깨지 않으면서도 N개의 thread에 걸쳐 fixing, latch, pinning 을 조율해야 한다. thread pool, 에러 컨텍스트, perf 모니터, 인터럽트 기계 모두가 fan-out 되었다가 완료 시 fan-in 되어야 한다. CUBRID 설계의 흥미로운 부분은 정확히 그 fan-out / fan-in 배관이다.

한 코어 너머로 확장하는 모든 현대 DBMS는 구현 전략 하나를 선택한다. 추상 수준에서는 일관되지만 (Graefe exchange는 어디나 있다) thread 모델, 스케줄링, 적용 범위 에서 갈라진다.

PostgreSQL 은 parallel-aware 노드BackgroundWorker 풀을 쓴다. 플래너가 위에 Gather / Gather Merge 를 내고, 그 아래로는 각 병렬 가능 연산자를 별도의 Parallel* 노드 (ParallelSeqScan, ParallelIndexScan, ParallelBitmapHeapScan, ParallelHashJoin, ParallelAppend, ParallelAggregate) 로 표현한다. 리더가 gather마다 N개의 짧게 사는 프로세스를 fork 하고, 조정은 공유 메모리로 한다. DOP는 max_parallel_workers_per_gather 로 정해지고 min_parallel_table_scan_size 로 잘린다. thread 모델은 process -per-worker.

Oracle 은 오래 사는 PX 서버의 공유 풀에서 끌어 오는 parallel execution slave 를 쓴다. 계획에는 Px Block Iterator / Px Partition IteratorPx Send/Px Receive 쌍이 박혀 있다. DOP는 parallel_max_servers 로 막히고 11gR2 이후 Auto DOP가 질의별로 조정한다. 적용 범위는 주류 엔진 중 가장 넓다 — scan, join, sort, group-by, top-N, parallel DML, parallel index build. shared-build 는 broadcast distribution 또는 hash-hash distribution 으로 구현된다.

SQL Server 는 SQLOS 사용자 모드 스케줄러 위에 parallel-aware 연산자를 얹는다. DOP는 max degree of parallelismMAXDOP 로 막힌다. SQLOS는 컨텍스트 스위치 비용을 낮게 유지해 thread 과 배정도 견디게 한다.

MySQL/InnoDB 는 역사적으로 intra-query 병렬성을 갖지 않았다. 8.0.14부터 InnoDB가 parallel read 를 출하한다 — multi-thread로 clustered-index를 스캔하며 CHECK TABLE, SELECT COUNT(*), 일부 DDL 경로에 쓰인다. 그 위 SQL 계층은 여전히 단일 thread다 — parallel join, sort, aggregation 은 8.4 mainline에 없다.

DuckDB 는 morsel-driven 모델 (Leis et al., SIGMOD 2014) 을 받아들인다. 스케줄러가 소스를 morsel로 쪼개고 고정 크기 풀의 thread에 할당하며, 연산자들은 데이터 청크의 push 기반 emission 으로 사슬을 이룬다.

CUBRID은 네 번째 점을 고른다 — 공유된 고정 크기 worker 풀 위에 얹은 연산자별 병렬 오케스트레이터 다. 전역 exchange 연산자 도 없고, 플래너 출력에 parallel-aware 표시도 없다. 세 연산자 — heap scan, hash join, top-level query execute — 가 각자 자기 manager 클래스를 가지고 open / execute 시점에 병렬로 갈지 결정하고, 일을 분할하고, 전역 풀에서 N개의 worker를 예약하고, N 개의 task를 push하고, join한다. 풀은 — parallel-query 라고 이름 붙은 단일 cubthread::worker_pool 이 — 모두에게 공유된다. 오늘날의 적용 범위는 parallel sequential heap scan, parallel hash join (Grace 식 2단계 build/probe), parallel uncorrelated subquery (BUILDLIST_PROC / BUILDVALUE_PROC / UNION_PROC / HASHJOIN_PROC / MERGELIST_PROC), parallel slice-and-merge sort 이다. 절충은 분명하다 — Postgres나 Oracle에 비해 적용 범위는 좁지만 (parallel index scan 없음, heap scan 위 parallel aggregate 없음, parallel append 없음) 조정 계층은 훨씬 얇다 — 오케스트레이터 셋, 풀 하나, DOP 함수 하나, S_PARALLEL_HEAP_SCAN 가지 하나.

parallel_query 네임스페이스와 전역 워커 풀

섹션 제목: “parallel_query 네임스페이스와 전역 워커 풀”

모든 것이 세 형제 네임스페이스 (parallel_query, parallel_query_execute, parallel_heap_scan) 아래 산다. 닻이 되는 것은 단일 클래스 — worker_manager_global 이라는 Meyers 싱글턴인데, 모든 병렬 기능이 공유하는 OS-thread 풀을 소유한다. 필드는 cubthread::worker_pool_type *m_worker_pool, std::once_flag m_init_flag, std::atomic<int> m_available, int m_capacity 이고, 공개 API는 init / destroy, 내부 API는 try_reserve_workers / release_workers / push_task 다.

초기화는 프로젝트 전반의 워커 풀 레지스트리에 끼어든다 — REGISTER_WORKERPOOL(parallel_query, ...) 매크로로. cubthread 의 모든 이름 붙은 풀은 서버 시작 시 그 레지스트리를 걷는 방식으로 생성되고, parallel-query 풀은 PRM_ID_MAX_PARALLEL_WORKERS 크기 의 한 항목이다. init() 안에서 std::call_once 가드가 파라미터를 읽어 기능을 비활성화하거나 (max_parallel_workers < 2) thread_create_worker_pool(max_parallel_workers, 1, parallel-query, thread_get_entry_manager()) 로 풀을 만든다. 풀은 고정 크기 (autoscaling 없음), 단일 용도 (worker당 task_max_count = 1), 그리고 이름이 붙어 있어서 CPU 덤프가 그 이름으로 귀속된다.

흥미로운 필드는 m_available 이다 — 현재 예약되지 않은 worker 수의 단일 atomic 카운터다. 모든 연산자는 task를 push하기 전에 예약하고 (N만큼 감소시키고), release 시 다시 증가시킨다. 이 예약이 엔진 전반의 admission control 이다.

// worker_manager_global::try_reserve_workers — src/query/parallel/px_worker_manager_global.cpp
int worker_manager_global::try_reserve_workers (const int num_workers)
{
int requested = MIN (num_workers, PRM_MAX_PARALLELISM);
const int min_degree = (requested == 1) ? 1 : 2;
int available = m_available.load ();
while (true) {
if (available < min_degree) return 0;
int reserved = (requested <= available) ? requested : available;
if (m_available.compare_exchange_weak (available, available - reserved))
return reserved;
std::this_thread::yield (); // CAS lost, retry
}
}

여기에서 두 가지 설계 선택이 두드러진다. 첫째, CAS는 부분 예약 을 허용한다 — 8개를 요청했는데 5개만 남아 있다면 5를 반환하고, 연산자 측에서 5로 충분한지 또는 직렬로 fallback할지 결정한다. 둘째, 최소 차수가 2 다 — parallel execution (heap scan, hash join, sort) 의 경우 1 worker는 0과 같이 취급된다. main + 1 worker 는 직렬과 다를 바 없는데 오버헤드만 더하기 때문이다 — 하지만 parallel subquery (uncorrelated aptr 하나는 worker에서 돌고 main은 계속 진행) 의 경우 1 이다.

flowchart TB
    subgraph "process-wide"
        REG["REGISTER_WORKERPOOL(parallel_query, ...)"]
        POOL["cubthread::worker_pool 'parallel-query'<br/>capacity = PRM_ID_MAX_PARALLEL_WORKERS"]
        AVAIL["std::atomic&lt;int&gt; m_available"]
        REG --> POOL --> AVAIL
    end
    subgraph "per-operator"
        WMP1["worker_manager (heap scan)"]
        WMP2["worker_manager (hash join)"]
        WMP3["worker_manager (query exec)"]
        WMP4["worker_manager (sort)"]
    end
    AVAIL -. "try_reserve_workers(N)" .-> WMP1
    AVAIL -. " " .-> WMP2
    AVAIL -. " " .-> WMP3
    AVAIL -. " " .-> WMP4
    WMP1 -. "release_workers" .-> AVAIL
    WMP2 -. " " .-> AVAIL
    WMP3 -. " " .-> AVAIL
    WMP4 -. " " .-> AVAIL
    POOL --> TASKQ["task 큐 (cubthread)"]
    WMP1 -- "push_task" --> TASKQ
    WMP2 -- " " --> TASKQ
    WMP3 -- " " --> TASKQ
    WMP4 -- " " --> TASKQ
    TASKQ --> W1[worker 1]
    TASKQ --> W2[worker 2]
    TASKQ --> Wn[worker N]

compute_parallel_degree — 임계 페이지 수에 대한 log2

섹션 제목: “compute_parallel_degree — 임계 페이지 수에 대한 log2”

DOP는 단일 함수 compute_parallel_degree (parallel_type type, UINT64 num_pages, int hint_degree) 가 결정한다. 네 개의 parallel_type 값 — HEAP_SCAN, HASH_JOIN, SORT, SUBQUERY — 각각이 자기 페이지 수 임계값을 가진다. 임계는 PRM_ID_PARALLEL_HEAP_SCAN_PAGE_THRESHOLD, PRM_ID_PARALLEL_HASH_JOIN_PAGE_THRESHOLD, PRM_ID_PARALLEL_SORT_PAGE_THRESHOLD 에서 끌어 온다. num_pages < page_threshold 이면 0을 반환해 비활성화한다. 그 외에는 다음과 같다.

// compute_parallel_degree — src/query/parallel/px_parallel.cpp
UINT64 x = num_pages / page_threshold;
auto_degree = (63 - __builtin_clzll (x)) + start_degree; // log2(x) + 2
return MIN (auto_degree, (UINT32) parallelism);

공식은 log2(num_pages / threshold) + 2 이고, 전역 parallelism 파라미터와 머신의 코어 수로 위에서 잘린다. 임계 위로 입력이 두 배가 될 때마다 worker 한 명이 더 붙는다. 8 코어 머신에서 기본 임계값을 쓰면 페이지가 다하기 훨씬 전에 코어 수 근처에서 DOP가 포화한다. SUBQUERY 의 경우는 항상 1을 반환하는데, subquery 실행기는 aptr마다 N-way fan-out이 아니라 aptr마다 main + 1- worker fan-out을 하기 때문이다. 힌트 처리는 — hint_degree == -1 → auto-compute, >= 2system_core_count 로 잘라서 사용, 0 이나 1 → 비활성화.

worker_manager — 연산자별 예약 핸들

섹션 제목: “worker_manager — 연산자별 예약 핸들”

전역 풀과 연산자 오케스트레이터 사이에 작은 RAII 핸들 (parallel_query::worker_manager) 이 끼어 있다. std::atomic<int> m_active_tasksint m_reserved_workers 를 들고 다닌다. 수명 주기는 — try_reserve_workers(N) 이 전역 CAS를 감싸고 (실패 시 nullptr, 연산자는 직렬로 fallback); push_taskm_active_tasks 를 올리고 전역 풀에 push하며 (task 는 retire 시 pop_task() 를 불러야 한다); wait_workersm_active_tasks 가 0이 될 때까지 busy-yield (std::this_thread:: yield()); release_workers 가 먼저 wait_workers 를 부르고 예약 한 worker를 전역 풀로 돌려준다. yield 루프는 의도적이다 — worker는 보통 여전히 유용한 일을 하고 있으므로 부모는 condvar에 막히면 안 된다.

공통 task 배관 — callable_task, interrupt, err_messages_with_lock

섹션 제목: “공통 task 배관 — callable_task, interrupt, err_messages_with_lock”

callable_taskstd::function<void(cubthread::entry &)>cubthread::task<entry> 와 retire functor로 어댑팅한다. 대부분의 heap-scan 과 hash-join task는 cubthread::entry_task 를 직접 상속하지만, sort 모듈은 본체가 편리한 std::bind 표현이라서 callable_task 를 쓴다. retire 시 task는 pop_task() 로 부모 worker_manager 에 위임한 뒤 사용자 retire functor (보통 delete this) 를 실행한다.

interrupt 는 thread 사이에 왜 멈추는지 를 전파하는 단일 공유 atomic enum 이다. 일곱 상태 (NO_INTERRUPT, USER_INTERRUPTED_FROM_MAIN_THREAD, USER_INTERRUPTED_FROM_WORKER_THREAD, ERROR_INTERRUPTED_FROM_MAIN_THREAD, ERROR_INTERRUPTED_FROM_WORKER_THREAD, INST_NUM_SATISFIED, JOB_ENDED) 가 방향 (수신자가 원격 er_message 를 swap-in 할지 그냥 break 할지 알 수 있도록) 과 이유 (사용자 vs 에러 vs limit 충족, 결과 핸들러가 S_END 를 반환할지 S_ERROR 를 반환할지 알 수 있도록) 를 모두 담는다. 오래 도는 worker 루프는 매 iteration 마다 한 번 m_interrupt->get_code() 를 검사한다.

err_messages_with_lockcuberr::er_message 의 vector를 들고 다닌다. 에러를 만난 worker는 mutex 아래에서 자기 thread-local 에러 컨텍스트를 vector로 swap한다. main thread는 join 시 처음 나오는 non-ER_INTERRUPTED 메시지를 골라 자기 에러 컨텍스트로 다시 swap-in 한다. 짝꿍인 atomic_instnumLIMIT N 을 처리한다 — N번째 튜플을 push한 worker가 동료들에게 INST_NUM_SATISFIED 인터럽트를 fan한다.

직렬 heap-scan iterator (HEAP_SCAN_ID 위의 scan_open_heap_scan / scan_next_scan) 가 병렬 버전 (PARALLEL_HEAP_SCAN_ID 위의 scan_open_parallel_heap_scan / scan_next_parallel_heap_scan) 으로 대체된다. 스캔 매니저는 표준 SCAN_TYPE switch로 dispatch 한다 — S_PARALLEL_HEAP_SCANS_HEAP_SCAN, S_INDX_SCAN 등과 나란한 가지 중 하나다.

scan_open_parallel_heap_scan 이 진입점이다. 옵티마이저는 TARGET_CLASS 의 access-spec에서 병렬이 그럴듯하다고 판단되면 ACCESS_SPEC_FLAG_NUM_PARALLEL_THREADS 를 세팅한다. 이 함수는 지금 병렬로 갈지를 결정한다. 먼저 scan_id->type = S_HEAP_SCAN (낙관적 fallback) 으로 두고 다섯 개의 게이트를 닫는다 — (1) 시스템 클래스 — 너무 작고 내부에서 너무 자주 읽힘; (2) MVCC 비활성화 클래스 (catalog, transient) — 병렬 스캔은 worker별 가시성 검사가 직렬과 동일하다고 가정함; (3) select-lock-needed (serializable / FOR UPDATE) — 병렬 스캔은 row lock을 전파하지 않음; (4) private_heap_id == 0 으로 main-thread 가 아닌 스캔 배제 (중첩 병렬 금지); (5) HFID empty. 게이트 통과 후 file_get_num_user_pagescompute_parallel_degree (parallel_type::HEAP_SCAN, ...) 로 페이지 수 임계를 검사하고, worker_manager::try_reserve_workers 로 전역 예약을 시도한다. 어느 단계에서 실패하면 scan_id->typeS_HEAP_SCAN 으로 둔 채 직렬 fallback. 성공하면 세 결과 타입 (MERGEABLE_LIST / BUILDVALUE_OPT / XASL_SNAPSHOT) 중 하나를 골라서 manager<RESULT_TYPE> 을 placement-new로 짓고, 그 open() 을 부르고, scan_id->type = S_PARALLEL_HEAP_SCAN 으로 갈아끼운다. 주의 — xasl->topn_items 또는 XASL_TO_BE_CACHED 가 세팅되어 있으면 MERGEABLE_LIST 플래그는 해제된다. 이런 기능은 private- heap shuttle 너머로 안정된 주소가 필요해서 worker 출력이 대신 XASL_SNAPSHOT 으로 가야 하기 때문이다.

세 결과 타입 이 세 가지 호출자 형태를 포착한다. MERGEABLE_LIST 가 일반 케이스 — 각 worker가 QFILE_LIST_ID 를 쓰고 main이 읽으면서 merge한다. XASL_SNAPSHOT 은 row 단위 — 부모가 중간 list 파일 없이 한 번에 한 튜플씩 원할 때 사용 (예: XASL_TO_BE_CACHED 또는 topn_items). BUILDVALUE_OPT 는 집계 fast path — 각 worker가 부분 집계를 만들고 main이 끝에서 부분들을 merge한다. 결과 타입은 PARALLEL_HEAP_SCAN_ID 안에 인코딩되어 있고 manager<RESULT_TYPE> / task<RESULT_TYPE> 클래스를 template-specialise 한다 (공개 표면은 open, start_tasks, next, reset, merge_stats, end, close).

분할 단위는 heap-file sector 다. heap manager는 데이터 페이지를 소유하는 sector의 파일별 FILE_FTAB_COLLECTOR 를 유지한다. input_handler_ftabs::init_on_main 이 main thread에서 file_get_all_data_sectors 를 부르고, 결과를 m_ftab_set 으로 복사하고 (ftab_set::convert 로), split (n_sets) 로 N개의 같은 크기 슬라이스로 자른다. 그 이후 페이지 수준 조정은 전적으로 worker별이다 — 공유 비트맵도 work-stealing도 없다.

각 worker는 initialize()m_splited_ftab_set_idxfetch_add 로 슬라이스 하나를 claim한다. worker별 상태는 input_handler_ftabs 의 정적 thread_local 멤버 (m_tl_scan_cache, m_tl_old_page_watcher, m_tl_ftab_set, m_tl_vpid, m_tl_pgoffset, m_tl_ftab) 에 산다. 페이지-fix watcher 패턴 (m_tl_old_page_watcherm_tl_scan_cache->page_watcher 를 번갈아 쓰는 식) 이 buffer pool 의 ordered-fix 프로토콜로 페이지 단위 스캔을 구현한다 — 페이지는 다음 페이지가 fix된 뒤에야 unfix된다. input_handler_ftabs::get_next_vpid_with_fix 이 worker hot path 다 — 다음 sector를 pop하고, 그 64-bit page_bitmap 을 걸어서 데이터 페이지를 찾고, 각 페이지를 OLD_PAGE_MAYBE_DEALLOCATED 로 ordered-fix 한다 (bitmap 빌드와 페이지 fix 사이에 deallocation과 경합하는 것은 예상되는 일이다 — 조용히 skip).

Tasks: worker별 XASL clone과 페이지별 slot iteration

섹션 제목: “Tasks: worker별 XASL clone과 페이지별 slot iteration”

worker마다 자기 parallel_heap_scan::task<RESULT_TYPE> 를 worker 풀 위에서 돌린다. execute 는 세 단계다 — initialize (실패 시 에러 메시지를 m_err_messages 로 swap하고 interrupt_code:: ERROR_INTERRUPTED_FROM_WORKER_THREAD 를 세팅), loop, finalize.

initialize 는 부모의 connection / transaction 컨텍스트를 복사하고, XASL 트리를 클론하고 (캐시가 clonable XASL을 가지면 xcache_find_xasl_id_for_execute 로, 아니면 stx_map_stream_to_xasl 로), 클론한 spec 위에 worker별 HEAP_SCAN_ID 를 열고, 더 깊은 XASL 레벨이 필요로 하는 중첩 list / index scan을 열고 (subquery, 위쪽 join 등), input handler의 sector slice 와 result handler의 writer 상태를 세팅한다. m_px_orig_thread_entry 는 부모 포인터로 세팅된다 — 엔진 전반의 이 thread는 병렬 worker인가? 후크다. worker 안에서 다르게 동작해야 하는 코드 (perfmon, log retry, lock-wait timeout) 가 이 값을 검사한다.

loop 의 외부 iteration 마다 — m_interrupt->get_code() 를 검사하고, logtb_is_interrupted_tran 을 검사하고, input handler에 다음 VPID를 묻고, 성공하면 그 페이지의 모든 자격 있는 slot을 slot iterator로 훑는다. 자격 있는 slot마다 if_pred 를 평가하고, XASL이 scan_ptr 를 가지면 (위쪽 join에 참여하는 list 또는 index scan 같은 중첩 레벨), 자격 있는 outer row마다 qexec_execute_scan_ptr 로 재귀한 뒤 result_handler_p->write(...) 를 부른다. 결과 타입 specialisation 이 m_xasl->outptr_list (mergeable list / buildvalue) 와 m_xasl->val_list (xasl snapshot) 사이에서 고른다. 매 row 후에는 clear_xasl_dptr_list 가 row별 동적 sub-XASL 상태를 청소한다. 에러는 ERROR_INTERRUPTED_FROM_WORKER_THREAD 를, 사용자 인터럽트는 USER_INTERRUPTED_FROM_WORKER_THREAD 를 세팅한다.

finalize 는 그 역순이다 — result handler를 write-finalise하고, input handler와 slot iterator를 finalise하고, join 정보를 기록하고, 클론한 XASL 상태를 청소하고, XASL 클론을 retire (xcache_retire_clone) 하거나 unpacked 트리를 free한다.

worker가 fan-out 하는 동안, 부모의 scan_next_parallel_heap_scan 은 계속해서 manager->next() 를 부른다. 첫 호출은 lazy하게 worker를 띄우고 (start_tasks()), 뒤 이은 호출들은 result handler의 read() 에 위임한다. mergeable-list는 m_xasl->list_id 에서 읽어 private-heap 경계 너머로 fetch_val_list 로 값을 다시 클론한다. xasl-snapshot은 m_xasl->val_list 로 곧장 읽고, buildvalue-opt 는 m_xasl->proc.buildvalue.agg_list 로 읽는다. 인터럽트 fan-in은 끝에서 — 어떤 worker가 에러를 raise했다면 그 er_message 를 main thread의 컨텍스트로 swap하고 S_ERROR 를 반환한다.

MERGEABLE_LIST 경로가 가장 정교하다 — worker는 private-heap에 할당된 QFILE_LIST_ID 에 쓰지만 reader는 main thread의 private heap으로 값을 돌려주어야 한다. shuttle은 두 개의 db_change_private_heap 경계 사이에서 일어나는 pr_clone_value 호출이다 — worker heap에서 클론 아웃, worker 측 정리, heap 스위치, main 측으로 클론 인. 이것이 MERGEABLE_LISTtopn_items 또는 XASL_TO_BE_CACHED 와 결합될 수 없는 이유다 — 그 기능들은 경계 너머에서 안정된 주소를 요구한다.

sequenceDiagram
    participant Main as Main thread<br/>(scan_next_scan)
    participant Mgr as parallel_heap_scan::<br/>manager&lt;result_type&gt;
    participant InH as input_handler_ftabs<br/>(thread_local sectors)
    participant W1 as Worker 1
    participant Wn as Worker N
    participant ResH as result_handler

    Main->>Mgr: scan_open_parallel_heap_scan
    Mgr->>InH: init_on_main (sector 분할)
    Mgr->>Mgr: try_reserve_workers (N)
    Main->>Mgr: scan_next_parallel_heap_scan (1차)
    Mgr->>Mgr: start_tasks() — N개 task push
    par fan-out
        Mgr-->>W1: push_task (XASL clone)
        Mgr-->>Wn: push_task (XASL clone)
    end
    W1->>InH: 슬라이스 claim
    Wn->>InH: 슬라이스 claim
    loop VPID마다
        W1->>InH: get_next_vpid_with_fix
        W1->>W1: slot iterator + if_pred + write
        Wn->>InH: get_next_vpid_with_fix
        Wn->>Wn: slot iterator + if_pred + write
    end
    W1-->>ResH: writer_result_p / aggregate
    Wn-->>ResH: writer_result_p / aggregate
    Main->>ResH: read (merge)
    Main->>Main: fetch_val_list (private-heap shuttle)
    Main-->>Mgr: 호출별 SCAN_CODE
    Main->>Mgr: scan_close — release_workers, free

병렬 hash join은 qexec_hash_join 안에 — 상태 switch의 새 HASHJOIN_STATUS_PARALLEL 가지에 — 자리 잡는다. 직렬 경로는 비 병렬 hash join (cubrid-hash-join.md 참조) 과 같다 — manager init, empty-side 검사, 분할 결정, 그리고 single-pass classic build/probe 또는 partitioned build/probe. 병렬 가지는 분할이 선택되었고 and 파티션당 페이지 수가 충분히 클 때 직렬 partitioned build/probe를 worker fan-out 버전으로 대체한다.

hjoin_try_parallel — 게이트와 예약

섹션 제목: “hjoin_try_parallel — 게이트와 예약”

hjoin_try_parallelhjoin_try_partition 이 join에 분할이 필요하다고 결정한 다음에 돌아간다. min_page_cnt = min(outer_list_id->page_cnt, inner_list_id->page_cnt) 를 계산하고, compute_parallel_degree(parallel_type::HASH_JOIN, min_page_cnt, manager->num_parallel_threads) 를 부르고, degree < 2 면 HASHJOIN_STATUS_PARTITION (직렬 fallback) 을 반환하고, num_parallel_threadsmanager->context_cnt 로 클램프한 뒤 (파티션보다 worker가 더 많을 이유는 없다) worker_manager::try_reserve_workers 를 부른다. 성공하면 핸들을 manager->px_worker_manager 에 저장하고 HASHJOIN_STATUS_PARALLEL 을 반환한다. qexec_hash_join 의 dispatcher가 그 다음 parallel_query::hash_join::execute_partitions(*thread_p, &manager) 를 부른다.

build_partitions 가 phase 1 이다 — hjoin_init_shared_split_info 를 부르고, outer 와 inner 양쪽qfile_collect_list_sector_info 로 데이터 sector를 열거하고, 연산자의 task_manager 위에 N개의 split_task 인스턴스를 push하고, task_manager.join() 으로 barrier 한다. 각 split_task 는 자기에게 할당된 입력 list 청크를 읽고, join key로 hash해서 파티션별 출력 파일에 쓴다 (공유 membuf claim 으로 접근을 조정).

execute_partitions 가 phase 2 + 3 이다 — N개의 join_task 인스턴스 를 push하고 (각각 한 파티션의 inner 측을 인메모리 hash로 만들고 대응하는 outer 측 파티션을 probe), join하고, 그 다음 파티션별 list id를 hjoin_merge_qlist 로 merge한다.

split_taskjoin_task 모두 base_task : cubthread::entry_task 를 상속하고 연산자 수준 worker_manager 위에 push/wait/join을 감싸는 task_manager 를 공유한다. task_managerpush_taskm_worker_manager->push_task 를 부르기 전에 mutex 아래에서 active -task counter를 올린다. end_task 는 그것을 내리고, pop_task 를 부르고, 0에 도달하면 m_all_tasks_done_cv.notify_all 을 보낸다. join 은 condvar에서 기다렸다가 m_worker_manager->wait_workers() 를 부른다.

hash-join task_manager 가 heap-scan 패턴과 다른 점은 두 가지다 — yield 루프 대신 condvar 대기를 쓴다 (heap-scan 부모와 달리 여기 서 부모는 대기 동안 할 일이 없기 때문이다 — heap-scan 부모는 worker write와 병렬로 결과를 읽는다); 그리고 명시적 handle_error 가 worker의 er_messagemain thread의 cuberr::context 로 직접 swap한다 — err_messages_with_lock 의 중간 단계를 우회한다 — hash join은 단 하나의 에러만 표면화하면 되고 그것을 즉시 main thread에 두기를 원하기 때문이다.

task_execution_guard — thread 컨텍스트 fan-out 의 RAII

섹션 제목: “task_execution_guard — thread 컨텍스트 fan-out 의 RAII”

모든 worker는 부모 thread를 흉내내는 것으로 시작한다. task_execution_guard 의 생성자가 main thread의 entry에서 m_thread_ref.m_px_orig_thread_entry, conn_entry, tran_index, on_trace 를 세팅하고 push_resource_tracks() 를 부른다. 소멸자는 conn_entryon_trace 를 비우고 pop_resource_tracks() 를 부른다. 이게 없으면 worker마다 transaction 컨텍스트 없이 (access-method 계층의 모든 assertion이 터질 것이다) 또는 이전 task의 stale 컨텍스트로 돌게 된다.

spawn_manager — thread별 XASL 하부구조 클로닝

섹션 제목: “spawn_manager — thread별 XASL 하부구조 클로닝”

hash join은 thread-local spawner 를 쓰는데, worker마다 join 시점 하부구조 (val_descr, during-join predicate, outer/inner regu list) 를 on-demand 로 클론한다. get_* 호출 (get_val_descr, get_during_join_pred, get_outer_regu_list_pred, get_inner_regu_list_pred) 마다 lazy하게 worker의 db_private_alloc heap에 클론한 하부구조를 할당-캐시하고, 다음 호출에는 캐시된 포인터를 반환한다. 이는 heap-scan task의 clone_xasl() 의 hash-join 대응물이지만 더 가는 단위에서 — worker가 실제로 필요로 하는 하부구조만 클론한다.

flowchart TB
    QHJ["qexec_hash_join (status switch)"]
    HTP["hjoin_try_parallel:<br/>compute_parallel_degree(HASH_JOIN)<br/>· try_reserve_workers"]
    QHJ -->|HASHJOIN_STATUS_TRY| HTP
    HTP -->|< 2 workers| Single[직렬: hjoin_execute]
    HTP -->|>= 2 workers| EP[parallel_query::hash_join::execute_partitions]
    QHJ -->|HASHJOIN_STATUS_PARTITION| BP1[hjoin_execute_partitions 직렬]
    EP --> Phase1[build_partitions]
    Phase1 --> SplitO["N x split_task (outer)"]
    SplitO --> JoinO[task_manager::join]
    JoinO --> SplitI["N x split_task (inner)"]
    SplitI --> JoinI[task_manager::join]
    JoinI --> Phase2[execute_partitions phase 2]
    Phase2 --> JT["N x join_task (파티션별 build+probe)"]
    JT --> JoinJ[task_manager::join]
    JoinJ --> MergeR["파티션별 hjoin_merge_qlist"]
    MergeR --> Done[xasl-&gt;list_id]

세 번째 오케스트레이터는 XASL 부분-트리 전체를 병렬로 돌린다. 표준적인 사용처는 일부 proc 타입의 aptr_list 안의 비상관 subquery 다 — 실행기가 그렇지 않으면 main 블록에 앞서 직렬로 prelude로 돌렸을 subquery들 (UNION_PROC 이 고전적인 예 — 각 가지가 독립적이다).

make_parallel_query_executor_recursively — main thread 에서의 wiring

섹션 제목: “make_parallel_query_executor_recursively — main thread 에서의 wiring”

C 호출 가능 진입점은 qexec_execute_mainblock 이 호출되기 전에 병렬 구조를 wire-up 한다. !xcache_uses_clones() 면 early-out 한다 (parallel query execute는 clonable XASL을 요구한다). thread_p->m_px_orig_thread_entry = thread_p 로 세팅하고, 선택적으로 perf-monitor 병렬 통계를 초기화하고, XASL 트리를 cubxasl::iterate_xasl_tree 로 두 번 걷는다. 첫 번째 walk는 parallel-eligible XASL 타입별로 비상관 aptr 노드를 센다. 두 번째 walk는 non-link aptr이 둘 이상인 모든 parallel-eligible XASL 노드에 query_executor 를 붙인다 — 그런 첫 번째 노드가 root executor (m_is_root_executor == true, 큐와 worker 풀을 소유) 를 받고, 그 이후의 노드들은 root의 큐와 worker 풀을 공유하는 child executor를 받는다.

parallel-eligible XASL 타입은 aptr_list 가 보통 독립 subquery를 담는 것들이다 — BUILDLIST_PROC / BUILDVALUE_PROC (list나 scalar 를 만드는 SELECT), UNION_PROC / INTERSECTION_PROC / DIFFERENCE_PROC (독립 가지의 집합 연산), HASHJOIN_PROC (sub- build 측이 그 자체로 독립일 수 있음), 그리고 MERGELIST_PROC (파티션 프루닝 질의가 쓰는 list 파일의 parallel union-all). 따라서 질의 트리 전체가 단 하나의 작업 큐와 단 하나의 worker 집합을 공유 하고, 병렬성은 중첩된 parallel-eligible 연산자 사이에 amortise 된다.

query_executor::run_jobs — 단일 공유 큐와 main의 work-loop

섹션 제목: “query_executor::run_jobs — 단일 공유 큐와 main의 work-loop”

run_jobs 는 전역 풀에 정확히 하나의 parallel-task가 있도록 보장하고 (첫 호출에서 단일 parallel_query_execute::task 를 lazy-spawn), main thread가 pre-pop된 첫 job을 돌리게 하고 (add_job 이 작은 fan-out에서 큐가 비지 않도록 m_job 으로 stash해 둔 것), m_join_context.get_running_jobs() == 0 이 될 때까지 큐에서 work-steal한다. join은 join_context::join_jobs 의 condvar 대기다. root에서는 join 후 함수가 sentinel을 push (push_last()) 하고, worker를 release하고, 인터럽트 fan-in (heap scan과 같은 패턴) 을 수행하고, perf 통계를 merge한다.

여기서 의미 있는 설계 선택이 — main thread도 worker로 참여한다 이다. worker 풀에 task를 하나 스케줄한 뒤 main이 큐에서 job을 pop해서 돌리기 시작한다. worker(들) 와 main이 try_pop 에서 경합 하고, job을 본 쪽이 그것을 돌린다. 이는 XASL 부분-트리 단위의 work-stealing 이고, worker 풀이 굶주릴 때조차 (worker가 없어 worker_manager 가 0을 반환할 때조차) main이 같은 코드 경로로 모든 job을 직렬로 돌리는 보너스를 가진다.

join_context 는 작은 condvar 기반 barrier 다 — add_running_jobs / sub_running_jobs 가 mutex 아래에서 increment / decrement (후자 는 0에 도달하면 알림); join_jobs 가 0이 될 때까지 대기. 실제 job은 execute_job_internal 이 돌리는데, XASL 상태를 클론하고, worker thread의 컨텍스트 (conn_entry, tran_index, on_trace, m_px_orig_thread_entry) 를 부모로 swap하고, qexec_execute_mainblock 을 부르고, 부분 질의의 list_id 를 복사해 내고, 복원한다.

Job 큐: lock-free MPMC ring (thread_safe_queue)

섹션 제목: “Job 큐: lock-free MPMC ring (thread_safe_queue)”

job 큐는 슬롯별 sequence 번호를 가진 교과서적인 bounded MPMC ring 이다. 각 슬롯은 data, std::atomic<uint64_t> sequence, std::atomic<bool> ready 를 가진다. fast path (try_push_fast / try_pop_fast) 는 표준적인 Vyukov 시퀀스를 실행한다 — position load, 슬롯의 sequence를 CAS (pospos + m_capacity), data write, ready = true release-store, position fetch_add. slow path 는 mutex를 잡고 condvar에서 공간 / 작업을 기다리며 iteration마다 인터럽트를 검사한다. parallel-query 한정 정련이 둘이다 — push_completed 는 sticky shutdown 플래그, reset_queue 는 절대 position counter가 UINT64_MAX 에 가까워질 때 도는 wraparound 핸들러 (편집증적 가드 — 사실상 발화하지 않음). 더 깊은 배경은 cubrid-thread-worker-pool.md 를 보라.

sequenceDiagram
    participant Main as Main thread
    participant QE as query_executor
    participant Q as thread_safe_queue&lt;job&gt;
    participant Pool as parallel-query 풀
    participant W as Worker

    Main->>QE: make_parallel_query_executor_recursively
    QE->>QE: XASL walk, px_executor 부착
    Main->>QE: add_job (job_1) — m_job 으로 stash
    Main->>QE: add_job (job_2..n)
    QE->>Q: push job_2..n
    Main->>QE: run_jobs()
    QE->>Pool: push_task (query_task)
    Pool->>W: dispatch
    Main->>QE: execute_job_internal (m_job)  /* main이 worker로 */
    par main이 Q에서 worker와 경합
        Main->>Q: try_pop -> job_k
        Main->>QE: execute_job_internal (job_k)
        W->>Q: pop -> job_m
        W->>QE: execute_job_internal (job_m)
    end
    QE->>QE: join_context.join_jobs()
    Main->>Pool: release_workers()
    Main-->>QE: 통계 집계 + 인터럽트 fan-in

parallel sort는 넷 중 가장 오래된 것이다 — px_sort.h 의 매크로 DSL은 cubthread::entry_task / worker_manager 배관보다 먼저 있었기에 통합 방식이 parallel_query::callable_task 인스턴스를 직접 push하는 식이다. 패턴은 slice-and-merge — 입력 임시 파일을 N개의 sub-file로 쪼개고, worker 안에서 각각 정렬한 뒤, merge. 드라이버 매크로 둘은 SORT_EXECUTE_PARALLEL(num, px_sort_param, function) (각 &px_sort_param[i] 와 함께 function 에 bind된 N개 의 callable_task 를 할당해 sort_param->px_worker_manager 에 push) 와 SORT_WAIT_PARALLEL(parallel_num, sort_param, px_sort_param) (sort_param->px_mtx 아래에서 모든 px_sort_param[i].px_status 가 더 이상 PX_PROGRESS 가 아닐 때까지 condvar에서 polling하고, 그 후 wait_workers()) 이다.

DOP 게이트는 sort_check_parallelism 이 공유 compute_parallel_degree(parallel_type::SORT, ...) 를 써서 결정 한다. parallel-sort 호출자는 둘이다 — SORT_ORDER_BY (실행기의 ORDER BY 정렬, 임시 파일 위, 질의별 parallelism 힌트와 함께) 와 SORT_INDEX_LEAF (B+Tree 구축의 parallel index-leaf builder, 질의별 힌트 없음). 게이트가 통과하면 try_reserve_workers 를 부르고 실제 예약된 수를 반환한다. 어떤 실패에도 1을 반환해 호출자 가 single-thread 정렬로 fallback하게 한다.

상태 enum (PX_PROGRESS, PX_DONE, PX_ERR_FAILED) 은 parallel- sort 한정의 interrupt_code 등가물이다. parallel sort의 독특한 부분은 merge phase 자체가 병렬 이라는 것이다 — sort_merge_nruns_parallel 이 worker 풀에서 log2(N)-tournament 형 태의 merge task를 돌려 단일 정렬 출력만 남을 때까지 진행한다.

안정적인 심볼들 — 서브시스템별로 묶음. 절 끝의 position-hint 표는 가장 자주 인용되는 심볼을 (file, line) 으로 매핑하며, 행 수치는 updated: 시점 기준이다.

기반 — src/query/parallel/

  • parallel_query::compute_parallel_degree — DOP 휴리스틱.
  • parallel_query::parallel_typeHEAP_SCAN, HASH_JOIN, SORT, SUBQUERY.
  • parallel_query::worker_manager — 연산자별 핸들. try_reserve_workers, release_workers, wait_workers, push_task, pop_task, get_reserved_workers.
  • parallel_query::worker_manager_global — Meyers 싱글턴. m_worker_pool (이름 붙은 parallel-query 풀) 과 std::atomic<int> m_available 을 소유. init, destroy, 내부 try_reserve_workers, release_workers, push_task.
  • parallel_query::interrupt — 7-상태 atomic enum.
  • parallel_query::atomic_instnumis_instnum_satisfies_after_1tuple_insert.
  • parallel_query::err_messages_with_lockmove_top_error_message_to_this.
  • parallel_query::callable_taskstd::function / std::bind 본체용 cubthread::task<entry> 어댑터 (sort 매크로가 사용).
  • parallel_query::ftab_setconvert, split (n_sets), get_next, append, move_from, clear.
  • parallel_query::thread_safe_queue<T> — Vyukov MPMC ring. push, pop, try_push, try_pop, push_last, is_empty, is_full, size, capacity, reset_queue.

Heap scan — src/query/parallel/px_heap_scan/

  • parallel_heap_scan::manager<RESULT_TYPE> — 스캔당 최상위 오케스트레이터. open, start_tasks, next, reset, merge_stats, end, close. RESULT_TYPE::{MERGEABLE_LIST, XASL_SNAPSHOT, BUILDVALUE_OPT} 로 templated.
  • parallel_heap_scan::task<RESULT_TYPE>cubthread::entry_task 본체. execute, retire, initialize, finalize, clone_xasl, loop.
  • parallel_heap_scan::input_handler_ftabs — sector-slice dispatcher. init_on_main, initialize, finalize, get_next_vpid_with_fix. thread-local 페이지 watcher.
  • parallel_heap_scan::result_handler<RESULT_TYPE>read_initialize, read, read_finalize, write_initialize, write, write_finalize.
  • parallel_heap_scan::slot_iteratorinitialize, finalize, set_page, next_qualified_slot_with_peek.
  • parallel_heap_scan::join_infocapture_join_info, record_join_info, apply_join_info, get_scan_info.
  • parallel_heap_scan::trace_handler, accumulative_trace_storage — perf 카운터 집계.
  • C 진입점 — scan_open_parallel_heap_scan, scan_start_parallel_heap_scan, scan_next_parallel_heap_scan, scan_reset_scan_block_parallel_heap_scan, scan_end_parallel_heap_scan, scan_close_parallel_heap_scan. 검사기 — scan_check_parallel_heap_scan_possible.

Hash join — src/query/parallel/px_hash_join/

  • parallel_query::hash_join::build_partitions — phase 1 (outer split, 그 다음 inner split).
  • parallel_query::hash_join::execute_partitions — phase 2 + 3 (파티션별 build/probe, 그 다음 merge).
  • parallel_query::hash_join::task_managerpush_task, end_task, join, handle_error, notify_stop, check_interrupt, clear_interrupt.
  • parallel_query::hash_join::task_execution_guard — 컨텍스트 fan-out RAII.
  • parallel_query::hash_join::base_task, split_task, join_task — task 계층.
  • parallel_query::hash_join::spawn_manager — TLS 하부구조 cloner. get_val_descr, get_during_join_pred, get_outer_regu_list_pred, get_inner_regu_list_pred, get_instance, destroy_instance.
  • query_hash_join.c 의 C 측 — hjoin_try_parallel (게이트), qexec_hash_joinHASHJOIN_STATUS_PARALLEL 가지, hjoin_init_shared_split_info, hjoin_clear_shared_split_info, hjoin_trace_*, hjoin_merge_qlist.

Query execute — src/query/parallel/px_query_execute/

  • parallel_query_execute::query_executor — 최상위 (root + child 생성자). add_job, run_jobs, get_parallelism, get_stats.
  • parallel_query_execute::taskcubthread::entry_task 본체. execute, retire, init, get_job, execute_job, end.
  • parallel_query_execute::execute_job_internal — XASL 상태를 클론하고, thread 컨텍스트를 swap하고, qexec_execute_mainblock 을 호출, list_id를 복사해 내고, 복원.
  • parallel_query_execute::job(xasl_node*, xasl_state*, join_context*, trace_context*) 큐 페이로드.
  • parallel_query_execute::join_context — 도는-job barrier (add_running_jobs, sub_running_jobs, get_running_jobs, join_jobs).
  • parallel_query_execute::trace_context — job별 perf-stat vector.
  • make_parallel_query_executor_recursively — 모든 parallel- eligible XASL 노드의 xasl_p->px_executor 를 wire.
  • 검사기 — check_parallel_subquery_possible.

Sort — src/query/parallel/px_sort.{h,c}src/storage/external_sort.c

  • 매크로 — SORT_IS_PARALLEL, SORT_EXECUTE_PARALLEL, SORT_WAIT_PARALLEL.
  • 상태 enum — PX_ERR_FAILED, PX_DONE, PX_PROGRESS. 타입 enum — PARALLEL_TYPE::{PX_SINGLE, PX_MAIN_IN_PARALLEL, PX_THREAD_IN_PARALLEL}.
  • 함수 — sort_check_parallelism, sort_start_parallelism, sort_end_parallelism, sort_listfile_execute, sort_copy_sort_param, sort_copy_sort_info, sort_split_input_temp_file, sort_merge_run_for_parallel, sort_merge_nruns, sort_put_result_for_parallel, sort_merge_nruns_parallel, sort_split_last_run, sort_put_result_from_tmpfile.

모듈 간 통합

  • src/query/scan_manager.c — 모든 공개 스캔 진입점이 S_PARALLEL_HEAP_SCAN switch 가지가 있어 px_heap_scan.cpp 의 C 래퍼 (scan_start_scan, scan_reset_scan_block, scan_end_scan, scan_close_scan, scan_next_scan_local) 로 forward한다.
  • src/query/query_hash_join.cqexec_hash_joinparallel_query::hash_join::execute_partitions 를 부르는 HASHJOIN_STATUS_PARALLEL 가지를 추가한다. 상태는 hjoin_try_parallel 이 결정한다.
  • src/storage/external_sort.csort_listfile_internalsort_check_parallelism 을 부르고 ≥ 2 일 때 매크로로 dispatch.
  • src/thread/thread_worker_pool*.{cpp,hpp} — 전역 parallel- query 풀이 인스턴스화하는 worker_pool 템플릿.
  • 사용된 THREAD_ENTRY 필드 — m_px_orig_thread_entry (부모 포인터), m_uses_px_stats, m_px_stats (thread별 perf 버퍼), m_px_stats_mutex, m_px_lock_mutex (heap-scan task의 initialise/finalise 동안 클론된 XASL을 lock).
심볼파일라인
compute_parallel_degreesrc/query/parallel/px_parallel.cpp36
worker_manager::try_reserve_workerssrc/query/parallel/px_worker_manager.cpp49
worker_manager::wait_workerssrc/query/parallel/px_worker_manager.cpp97
worker_manager_global::initsrc/query/parallel/px_worker_manager_global.cpp53
worker_manager_global::try_reserve_workerssrc/query/parallel/px_worker_manager_global.cpp97
REGISTER_WORKERPOOL(parallel_query)src/query/parallel/px_worker_manager_global.cpp48
interrupt::interrupt_codesrc/query/parallel/px_interrupt.hpp31
err_messages_with_lock::move_top_error_message_to_thissrc/query/parallel/px_interrupt.hpp100
ftab_set::splitsrc/query/parallel/px_ftab_set.hpp106
thread_safe_queue<T>::try_push_fastsrc/query/parallel/px_thread_safe_queue.cpp172
thread_safe_queue<T>::try_pop_fastsrc/query/parallel/px_thread_safe_queue.cpp221
scan_open_parallel_heap_scanpx_heap_scan/px_heap_scan.cpp349
scan_next_parallel_heap_scanpx_heap_scan/px_heap_scan.cpp44
scan_close_parallel_heap_scanpx_heap_scan/px_heap_scan.cpp242
parallel_heap_scan::manager<>::openpx_heap_scan/px_heap_scan.cpp632
parallel_heap_scan::manager<>::start_taskspx_heap_scan/px_heap_scan.cpp795
parallel_heap_scan::manager<>::nextpx_heap_scan/px_heap_scan.cpp817
parallel_heap_scan::task<>::executepx_heap_scan/px_heap_scan_task.cpp44
parallel_heap_scan::task<>::initializepx_heap_scan/px_heap_scan_task.cpp71
parallel_heap_scan::task<>::clone_xaslpx_heap_scan/px_heap_scan_task.cpp439
parallel_heap_scan::task<>::looppx_heap_scan/px_heap_scan_task.cpp510
input_handler_ftabs::init_on_mainpx_heap_scan/px_heap_scan_input_handler_ftabs.cpp60
input_handler_ftabs::get_next_vpid_with_fixpx_heap_scan/px_heap_scan_input_handler_ftabs.cpp87
hash_join::build_partitionspx_hash_join/px_hash_join.cpp43
hash_join::execute_partitionspx_hash_join/px_hash_join.cpp157
hash_join::task_manager::push_taskpx_hash_join/px_hash_join_task_manager.cpp58
hash_join::task_manager::joinpx_hash_join/px_hash_join_task_manager.cpp81
hash_join::task_execution_guardpx_hash_join/px_hash_join_task_manager.hpp105
hash_join::spawn_manager::spawnpx_hash_join/px_hash_join_spawn_manager.hpp85
make_parallel_query_executor_recursivelypx_query_execute/px_query_executor.cpp279
query_executor::run_jobspx_query_execute/px_query_executor.cpp115
execute_job_internalpx_query_execute/px_query_task.cpp91
join_context::join_jobspx_query_execute/px_query_job.hpp70
hjoin_try_parallelsrc/query/query_hash_join.c1965
qexec_hash_join HASHJOIN_STATUS_PARALLEL 가지src/query/query_hash_join.c230
sort_check_parallelismsrc/storage/external_sort.c4936
SORT_EXECUTE_PARALLEL / SORT_WAIT_PARALLELsrc/query/parallel/px_sort.h41, 52
  • parallel_heap_scan::managerRESULT_TYPE 으로 templated 되어 있지만 C API는 non-template 이고 런타임 dispatch는 PARALLEL_HEAP_SCAN_IDresult_type 필드로 한다. 네 번째 결과 타입을 추가하려면 다섯 곳을 손대야 한다 — enum, manager 인스턴스화, task 인스턴스화, scan_next_parallel_heap_scan / scan_reset_scan_block_parallel_heap_scan / scan_close_parallel_heap_scan 의 런타임 switch, 그리고 scan_open_parallel_heap_scan 의 결과 타입 선택.
  • DOP 휴리스틱 auto_degree = log2(num_pages / page_threshold) + 2compute_parallel_degree 의 주석 외에는 문서화된 곳이 없다. Postgres 식 선형 ramp가 아니다. 매우 큰 테이블에서는 num_pages 가 다하기 훨씬 전에 DOP가 parallelism 에서 포화 한다 — 거대 스캔에는 보수적, 중간 스캔에는 공격적.
  • scan_open_parallel_heap_scan 안의 다섯 no parallel 게이트 (system class, MVCC-disabled, select-lock-needed, private_heap_id == 0, partitioned-class) 는 open 시 한 번만 검사된다. 주석의 “DB_PARTITION_CLASS will be parallel-heap- scanned, not DB_PARTITIONED_CLASS” 가 함의하는 단위는 부모가 아니라 leaf 파티션이다.
  • make_parallel_query_executor_recursively!xcache_uses_clones() 에 early-out 한다는 사실 — parallel query execute는 clonable XASL을 요구한다. heap scan도 이득을 얻지만 그쪽 clone_xasl 은 두 가지 경로 (cache 와 stx_map_stream_to_xasl 를 통한 non-cache) 를 모두 가지므로 cache 없이도 동작한다. query executor는 그렇지 않다.
  • hash-join task_managercondvar 대기를 쓰지만 heap-scan worker_manageryield 루프 대기를 쓴다. heap-scan main thread는 fan-out 동안 idle이 아니다 — worker write와 병렬로 결과를 읽고 있다 — 그래서 막힐 수 없다. hash-join phase 1 main thread는 진짜로 idle 이고 condvar의 이득을 본다. query- executor main thread는 worker로 참여하다가 큐를 비운 뒤에야 join_context::join_jobs 에서 대기한다.
  • 인터럽트 enum이 일곱 상태 (binary on/off 가 아닌) 인 이유는 병렬 계층이 방향이유 둘 다를 필요로 하기 때문이다.
  • THREAD_ENTRY::m_px_lock_mutex 는 특이하다 — thread별 mutex 인데 호출자가 다른 thread다 (main thread의 m_px_lock_mutexclone_xasl()finalize() 안에서 worker가 잠근다). 이는 XASL 클론 할당 / 회수를 main thread의 xasl-cache 작업을 직렬화한다. 부모 측에 둔 자식 thread 접근용 lock 이지, thread 자체용 self-lock 이 아니다.
  • m_px_orig_thread_entry 는 엔진 전반의 “이 thread는 병렬 worker인가?” 후크다. worker 안에서 다르게 동작해야 하는 코드 (perfmon, log-acquisition retry, lock-wait timeout) 가 이를 검사하고 부모의 컨텍스트를 사용한다. 가로지르는 새 기능 (예: 질의별 메모리 한도) 이 worker 경계마다 새 필드를 빼는 대신 이 같은 후크에 꽂아야 한다.
  • 적응형 DOP. compute_parallel_degree 는 scan open / hash- join admission / sort start 에 한 번만 돌고, DOP는 그 이후 조정 될 수 없다. 질의 도중 경합이나 페이지 버퍼 압력이 치솟으면 엔진은 축소할 수 없다. Postgres에도 같은 한계가 있고, Oracle의 in-memory parallel execution 은 동적 re-sharding을 한다.
  • Parallel index scan. 카탈로그에 S_INDX_SCAN 은 있지만 병렬 형제는 없다. S_PARALLEL_INDX_SCAN 을 추가하려면 B+Tree leaf 레벨 분할과 공유 cursor 상태를 가진 worker별 BTREE_SCAN 클론이 필요하다.
  • heap scan 위 parallel aggregation. BUILDVALUE_OPT 의 부분들이 main thread의 read() 에서 merge된다 — 병렬이지만 연산자 수준은 아니다. cardinality가 높은 GROUP BY 에서는 merge가 병목이 된다. parallel merge (부분 집계의 tournament tree, 또는 hash-partitioned merge) 는 문헌이 알려진 승리지만 구현되어 있지 않다.
  • 중첩 병렬성. 오케스트레이터마다 같은 풀에서 예약하므로, hash join이 또 병렬로 가고 싶어 하는 heap scan을 부르면 안쪽 호출이 고갈된 풀을 보고 조용히 직렬로 fallback한다. private_heap_id == 0 게이트가 명시적 가드다. 통제된 중첩 (예: 레벨별로 더 적은 worker 예약) 을 허용할지는 열린 선택이다.
  • 메모리 회계. 각 worker가 scratch (XASL 클론, 클론된 val_descr, 부분 집계) 에 db_private_alloc 을 쓰지만, 예산은 thread별이지 질의별이 아니다. 병렬도가 높은 질의는 자기 메모리 발자국을 N배로 늘린다. 오늘날 병렬 계층에는 질의별 메모리 한도 가 없다.
  • 소스 파일 (/data/hgryoo/references/cubrid/ 아래) — src/query/parallel/px_parallel.{hpp,cpp}, px_worker_manager{,_global}.{hpp,cpp}, px_callable_task.{hpp,cpp}, px_thread_safe_queue.{hpp,cpp}, px_interrupt.hpp, px_ftab_set.hpp, px_sort.{h,c}, px_heap_scan/*.{hpp,cpp}, px_hash_join/*.{hpp,cpp}, px_query_execute/*.{hpp,cpp}, src/thread/thread_worker_pool*.{hpp,cpp}, src/query/scan_manager.c, src/query/query_hash_join.c, src/storage/external_sort.c.
  • 관련 큐레이션 문서 — cubrid-scan-manager.md, cubrid-hash-join.md, cubrid-thread-worker-pool.md, cubrid-external-sort.md.
  • 이론 참고문헌 — Graefe, Encapsulation of Parallelism in the Volcano Query Processing System (SIGMOD 1990); Graefe, Query Evaluation Techniques for Large Databases (CSUR 1993) §3; Leis et al., Morsel-Driven Parallelism (SIGMOD 2014); Anatomy of a Database System (Red Book 4장) — shared build; Garcia-Molina/Ullman/Widom, Database Systems: The Complete Book §15.4 (2단계 parallel sort); Vyukov, Bounded MPMC queue.