(KO) CUBRID 파티셔닝 — Range/Hash/List 전략, 파티션 가지치기, 파티션별 실행
목차
학술적 배경
섹션 제목: “학술적 배경”관계형 테이블은 의미상 한 다발의 튜플 집합이다. 이 집합이 워킹 메모리에 안정적으로 들어 가는 한도, 또는 단일 디바이스에 안정 적으로 얹히는 한도를 넘어가면, 물리적 수평 분할이 필요해진 다. 행마다 결정적인 함수를 적용해 disjoint 한 부분 집합 — 즉 파티션 — 으로 쪼개고, 논리 테이블은 그 파티션들의 합집합으로 구현한다.
Database System Concepts (Silberschatz, Korth, Sudarshan, 7판
22장 “Parallel and Distributed Storage”) 가 정본 처리를 한다.
파티셔닝 함수 P(tuple) → {0, 1, …, N-1} 은 DBA 또는 시스템이
정한다. 모든 튜플은 정확히 하나의 파티션으로 매핑된다. 교과서
가 세 가지 정본 전략을 나열한다.
- Range 파티셔닝.
P(t) = i이고t.k ∈ [low_i, high_i). 파티션 키 컬럼이 전순서를 가지며, 파티션은 그 순서 위의 겹 치지 않는 구간을 덮는다. 시계열 데이터처럼 query 가 최근 slice 만 두드리는 패턴에 좋다. - Hash 파티셔닝.
P(t) = h(t.k) mod N. 파티션 키를 해시 하고, modulus 로 파티션을 고른다. 파티션 분포를 균등하게 만들고 싶거나 활용할 수 있는 자연스러운 순서가 없을 때 (혹 은 자연 순서가 skew 되어 있을 때) 적합하다. - List 파티셔닝. 각 파티션이 자기가 소유하는 키 값의 명시
적 집합을 선언하고,
P(t) = i이면t.k ∈ values_i다. 국가 코드나 테넌트 ID 처럼 categorical 데이터에 좋다.
Petrov 의 Database Internals (11장 Distributed Systems) 가 네 번째 설계 차원을 강조한다. 옵티마이저와 실행기에서의 파티 션 인지. 파티션 테이블이 자기 비용을 갚으려면 엔진이 다음을 할 수 있어야 한다.
- plan time 가지치기. Range 로 파티션된 테이블에
WHERE k = 42술어가 걸리면, 옵티마이저는 어떤 scan 도 만들기 전에, 42 를 담는 파티션만 스캔하면 된다는 점을 계산해야 한다. 나머 지 N-1 개 파티션은 제거된다. 이게 파티션 pruning 이다. - execute time 디스패치. 가지치기 이후에도 살아남은 파티션
들은 독립된 heap 과 독립된 인덱스를 가진 별개 객체다. 실행기
는 살아남은 각 파티션의
SCAN_ID를 차례로 (또는 병렬로) 돌려 가며 단일 튜플 스트림을 만들어 내야 한다. - DML 시점의 행별 결정.
INSERT INTO t VALUES (...)에서 엔진은P(row)를 계산해야만 어느 heap 에 쓸지 안다. UPDATE 는 post-image 에 같은 계산을 한 번 더 해야 한다. 그 결과 파티 션 할당이 바뀌었다면, 옛 heap 에서 행을 옮겨 새 heap 으로 옮겨 심어야 한다.
교과서가 더 공격적인 엔진들이 지원하는 두 가지 정제를 짚는다.
- Partition-wise join.
T1과T2가 같은 키와 같은 파티 셔닝 함수로 분할되어 있다면,T1 ⨝ T2 ON T1.k = T2.k는T1.partition_i ⨝ T2.partition_i의 N 개 병렬 join 으로 분 해된다.i ≠ j인 cross-partition 매칭은 발생할 수 없으므로, 거기서 오는 부담이 사라진다. - Local 인덱스 vs Global 인덱스. Local 인덱스는 테이블과 같이 분할된다. 파티션마다 자기 B-tree 를 가진다. Global 인덱스는 모든 파티션을 가로지른다. leaf 엔트리에 행 포인터 외에도 파티션 식별자가 들어 간다. Local 인덱스는 유지비가 싸 다 (한 파티션에 대한 DDL 이 그 파티션의 인덱스만 건드린다). 대신 도움 없이는 테이블 전역 unique 제약을 지킬 수 없다. Global 인덱스는 유지비가 비싸지만 테이블 전역 unique 를 직접 강제한다.
가지치기 자체는 단순한 대수 질문으로 환원된다. 파티션 키 컬럼
위의 술어 Φ 와 파티션 기술자 P_i (range, hash modulus 위치,
또는 값 list) 가 주어졌을 때, Φ(v) ∧ v ∈ P_i 가 성립하는 값
v 가 존재하는가? 존재하지 않으면 그 파티션은 가지치기된다.
가지치기 알고리즘이 술어 트리를 내려 가면서, 각 leaf 비교마다
모든 파티션 기술자에게 증인 값을 줄 수 있느냐? 를 묻는다.
AND 노드는 결과를 교집합 한다. OR 노드는 합집합 한다. NOT 과
모르는 함수는 보수적으로 모든 파티션을 살린다.
이 문서의 나머지는 CUBRID 가 이 개념을 어떻게 실현했는지를 따 라간다. 파티션 기술자를 카탈로그 메모리에 어떻게 표현하는지, 옵티마이저가 access spec 준비 단계에 가지치기를 어떻게 끼워 넣는지, 실행기가 파티션별 scan 을 어떻게 디스패치하는지, locator 가 DML 을 어떻게 올바른 파티션 heap 으로 라우팅하는지.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”파티셔닝을 지원하는 모든 관계형 엔진은 같은 결정 다발을 약간씩 다른 모양으로 처리한다. 변형들을 훑고 나면 CUBRID 의 선택이 선명해진다.
PostgreSQL — declarative 파티셔닝, 네이티브 가지치기, partition-wise join
섹션 제목: “PostgreSQL — declarative 파티셔닝, 네이티브 가지치기, partition-wise join”PostgreSQL 은 10 (2017, range/list), 11 (2018, hash), 12 (partition-wise join) 을 거치며 declarative 파티셔닝을 추가했고, 이후 릴리스에서 가지치기를 다듬어 왔다. 모델은 다음과 같다.
- 카탈로그. 파티션 테이블은 부모
pg_class행 하나, 각 파티션은 자식pg_class행이며pg_inherits가 둘을 잇는 다. 부모는 자기 heap 이 없다 (logical parent). 파티션 기술 자는pg_partitioned_table+pg_class.relpartbound에 산다. - 가지치기. 두 단계다. Plan-time pruning
(
partprune.c::prune_append_rel_partitions) 은 술어가 plan 시점에 완전히 알려진 경우 파티션을 제거한다. Execution-time pruning 은 실행기가Append노드를 열 때마다 가지치기 로직 을 다시 돌린다. execute 시점에 결정되는 파라미터 (예 — generic plan 이나 파라미터화된 nested-loop) 에서도 파티션을 떨어 낼 수 있게. - Plan 모양. 부모 테이블이 plan 에서는 파티션별 scan 의
Append(또는MergeAppend) 로 대체된다. 옵티마이저는 비용 계산을 위해 각 파티션을 독립 relation 으로 취급한다. 통계가 파티션별로 유지된다. - Partition-wise join. 옵션 (
enable_partitionwise_join). 두 입력의 파티션 모양이 같으면, planner 는 join 을 파티션별 join 들의 다발로 분해해 단일Append아래에 둔다. - 인덱스. Local. 부모 테이블에 heap 이 없으므로 global 인덱스라는 것이 인덱싱할 부모 heap 을 가지지 못한다. 각 파티 션이 자기 인덱스를 소유한다. 모든 파티션을 가로지르는 unique 를 강제하려면 unique 제약에 파티션 키가 포함되어 있어야 한다.
MySQL — 파티셔닝 문법, plan time 의 type-pruning
섹션 제목: “MySQL — 파티셔닝 문법, plan time 의 type-pruning”MySQL 은 5.1 부터 파티셔닝을 지원했다. 모델은 다음과 같다.
- 카탈로그. 단일
.frm(지금은mysql.tables행) 이 파티 션별 정의를 포함한 파티션 테이블 전체를 기술한다. 파티션은 별개 테이블이 아니다. 스토리지 엔진 (역사적으로ha_partition, 5.7 이후 InnoDB 네이티브 파티셔닝) 이 fan-out 을 책임진다. - 가지치기.
sql/sql_partition.c가 WHERE 절을 걸어 다니 며 사용된 파티션 비트맵을 채운다. 가지치기 엔진이 range, hash, key, list 분할과 composite 분할 (range 안의 hash subpartition) 까지 안다. - Plan 모양. 옵티마이저는 파티션 비트맵을 스토리지 엔진에
넘긴다. 스토리지 엔진이 살아남은 파티션을 내부적으로 순회한
다. 옵티마이저 레벨에서는
Append같은 노드가 보이지 않는다. - 인덱스. Local 만. 파티션 키를 포함하지 않는 unique 인덱 스는 DDL 시점에 거부된다.
Oracle — range/hash/list/composite 와 interval 파티셔닝
섹션 제목: “Oracle — range/hash/list/composite 와 interval 파티셔닝”Oracle 은 상용 영역에서 가장 성숙한 파티셔닝 기능을 가진다.
- 전략. Range, hash, list, 더해서 composite 분할 (range- hash, range-list, list-hash, list-range, list-list) 과 데이터가 도착할 때 새 파티션이 자동 생성되는 interval 분할.
- 가지치기. Static (plan-time) 와 dynamic (execute-time, 바인드 변수와 join 키 대상) 둘 다. Oracle Database VLDB and Partitioning Guide 에 정리되어 있다.
- 인덱스. Local 과 global 둘 다. Global 인덱스는
(key, ROWID, partition_no)를 담아서 파티션을 가로질러 행을 풀어 낸다. Index-organized table 은 별도 처리. - Partition-wise join. 지원한다. Full (양쪽이 동일하게 파티션됨) 과 partial (한쪽만 파티션, 반대편을 serial broadcast 또는 hash 재분배) 둘 다.
CUBRID 의 위치
섹션 제목: “CUBRID 의 위치”CUBRID 의 파티셔닝은 정신적으로 MySQL 에 가장 가깝다. range,
hash, list 전략. plan time 가지치기만. local 인덱스만.
partition-wise join 없음. composite 분할 없음. 다만 구현 으로
는 PostgreSQL 에 더 가깝다 — 각 파티션이 진짜 CUBRID 클래스다
(자기 OID, HFID, 부모에서 상속받은 attribute, 자기 카탈로그
엔트리를 모두 가진다). 그리고 파티션 가지치기는 파서 레벨 재작
성이 아니라 실행기가 access spec 을 열 때 호출하는 서버 측
헬퍼다.
CUBRID 만의 비틀기는 마스터 클래스 — 파티션된 클래스 — 가
Postgres 의 logical parent 같은 게 아니라, 파티션 규칙을 자기
SM_PARTITION 멤버에 들고 있는 진짜 클래스 객체다.
파티션 발견은 클래스-슈퍼클래스 사슬 (heap_get_class_supers)
을 따라간다. 각 파티션의 클래스가 자기 단일 슈퍼클래스로 마스
터 클래스를 가지기 때문이다. 이 방식은 cubrid-class-object.md
에서 다룬 OODB 상속 기계를 그대로 재사용한다 — 별도 파티션 카
탈로그 테이블을 새로 도입하지 않는다. 비용은 파티션 테이블의
한 파티션이 schema layer 의 시선에서는 그냥 subclass 로 보인다
는 점이다. 그래서 CUBRID 가 파티션 테이블에 통상적인 sub-
classing 을 금지한다 (ER_SM_NO_PARTITION_ON_HIERARCHIES).
CUBRID의 구현
섹션 제목: “CUBRID의 구현”CUBRID 는 사용자가 SQL 로 참조하는 마스터 클래스 (즉 파티션
된 테이블) 와 그 아래 파티션마다 하나씩 만들어지는 N 개의 자
식 클래스 를 가지고 테이블을 분할한다. 마스터 클래스가 파티션
규칙 (range 경계 / hash 개수 / list 값) 을 자기 SM_PARTITION
멤버에 들고 있다. 각 자식 클래스도 자기 SM_PARTITION 을 가지
며, 나는 클래스 M 의 i 번 파티션이고 V_i 값을 다룬다 라는 정
보를 적어 둔다. execute_schema.c 의 DDL 경로가 둘 모두를 실
체화한다. 런타임 경로 (partition.c) 가 그 정보를 읽어 와
PRUNING_CONTEXT 를 만들고, 그것으로 (a) 스캔 생성 전에 파티션
을 제거하고, (b) 삽입되는 각 행의 목적 파티션을 계산하고, (c)
주어진 인덱스 키가 어느 파티션에 속하는지를 계산한다.
서브시스템 전체가 서버 측이다. partition.c 는 #if !defined (SERVER_MODE) && !defined (SA_MODE) 가드로 감싸져 있다 (
partition_sr.h:26-28). query_executor.c, locator_sr.c,
btree.c, btree_load.c 가 호출자다. 클라이언트 측 DDL
(execute_schema.c) 은 클라이언트 모드에서 동작하며, 일반 schema-
template 기계로 파티션 카탈로그를 쓴다.
카탈로그 모델 — 마스터 클래스 위의 SM_PARTITION
섹션 제목: “카탈로그 모델 — 마스터 클래스 위의 SM_PARTITION”영속 파티션 기술자는 SM_PARTITION 이고, class_object.h 에
정의되어 있다.
// sm_partition — class_object.hstruct sm_partition{ struct sm_partition *next; /* currently not used, always NULL */ const char *pname; /* partition name */ int partition_type; /* partition type (range, list, hash) */ DB_CLASS_PARTITION_TYPE class_partition_type; /* partitioned class | partition class */ DB_SEQ *values; /* values for range and list partition types */ const char *expr; /* partition expression */ const char *comment;};SM_CLASS 는 이를 SM_PARTITION *partition
(class_object.h:798) 으로 들고 다닌다. 마스터 클래스의 경우,
partition 은 단 하나의 기술자다 — class_partition_type = DB_PARTITIONED_CLASS, 전략은 partition_type (DB_PARTITION_TYPE
enum 의 셋 중 하나), 사용자의 파티션 expression 은 expr,
파티션 술어 sequence 가 values. 파티션 술어는 직렬화된
FUNC_PRED (regu-variable expression) 다. 행을 평가하면
파티션 할당에 쓰이는 값이 나온다. 자식 클래스의 경우
partition->class_partition_type 은 DB_PARTITION_CLASS 이고
values 는 그 파티션의 데이터를 담는다 — range 라면 [min, max), list 라면 명시적인 값 집합, hash 는 사용하지 않는다.
DB_PARTITION_TYPE 과 DB_CLASS_PARTITION_TYPE enum 은
storage_common.h 에 있다.
// DB_PARTITION_TYPE — storage_common.htypedef enum{ DB_PARTITION_HASH = 0, DB_PARTITION_RANGE, DB_PARTITION_LIST} DB_PARTITION_TYPE;
typedef enum{ DB_NOT_PARTITIONED_CLASS = 0, DB_PARTITIONED_CLASS = 1, DB_PARTITION_CLASS = 2} DB_CLASS_PARTITION_TYPE;디스크 위에서는 같은 정보가 두 구조로 갈라진다. 파티션된 클래
스의 클래스별 요약은 OR_CLASSREP::has_partition_info (단일
비트) 에 들어 가고, 서버 시작 시점에 만들어지는 파티션별 튜플
배열은 object_representation_sr.h 의 OR_PARTITION 이다.
// or_partition — object_representation_sr.htypedef struct or_partition OR_PARTITION;struct or_partition{ OID class_oid; /* class OID */ HFID class_hfid; /* class HFID */ int partition_type; /* partition type (range, list, hash) */ REPR_ID rep_id; /* class representation id */ DB_SEQ *values; /* values for range and list partition types */};이게 서버가 partition.c 에 넘기는 런타임 형태다. 개념적으로
OR_PARTITION[0] 은 마스터 (파티션 테이블 자체) 고
OR_PARTITION[1..N] 이 파티션들이다. 마스터 엔트리는 자기
values 에 직렬화된 파티션 술어를 담는다 (
partition_load_partition_predicate 참조). 파티션 엔트리는
파티션의 데이터 (range 경계나 list 값) 를 values 에 담는다.
SM 형식 (클라이언트 워크스페이스) 과 OR 형식 (서버) 사이의 다
리는 partition_load_pruning_context 에서 호출되는
heap_get_class_partitions 헬퍼다.
classDiagram
class SM_CLASS {
SM_PARTITION* partition
DB_OBJLIST* users
}
class SM_PARTITION {
const char* pname
int partition_type
DB_CLASS_PARTITION_TYPE class_partition_type
DB_SEQ* values
const char* expr
}
class OR_PARTITION {
OID class_oid
HFID class_hfid
int partition_type
REPR_ID rep_id
DB_SEQ* values
}
class PRUNING_CONTEXT {
OID root_oid
OR_PARTITION* partitions
int count
DB_PARTITION_TYPE partition_type
ATTR_ID attr_id
func_pred* partition_pred
}
SM_CLASS *-- SM_PARTITION
SM_PARTITION ..> OR_PARTITION : 디스크 직렬화 후 reload
OR_PARTITION ..> PRUNING_CONTEXT : 배열, [0]=마스터, [1..N]=파티션
두 가지 설계 결정이 두드러진다. 첫째, 파티션은 진짜 CUBRID
클래스다. 각 자식 클래스가 자기 OID, 자기 HFID, 자기
attribute representation, 자기 인덱스를 가진다. 마스터 클래스
와의 연결은 단일 상속 으로 이루어진다 (마스터 쪽에서는
SM_CLASS::users, 파티션 쪽에서는 SM_CLASS::inheritance).
가지치기 발견 단계가 이를 활용한다 —
partition_find_root_class_oid 가 heap_get_class_supers 를
부르고, 단 하나뿐인 super 를 파티션된 클래스의 root 로 본다.
둘째, 파티션 술어는 컬럼 id + 연산자가 아니라 직렬화된
regu-variable expression 으로 저장된다. 더 유연하다 — PARTITION BY HASH (YEAR(birthdate)) 같은 형태가 가능하고, 술어가 함수
호출이 된다. 대신 가지치기는 stx_map_stream_to_func_pred 와
fetch_peek_dbval 을 거쳐 deserialize 하고 평가해야 한다. 빠
른 attribute 비교가 아니다.
DDL 경로 — do_create_partition 과 친구들
섹션 제목: “DDL 경로 — do_create_partition 과 친구들”사용자가 CREATE TABLE t (...) PARTITION BY HASH (id) PARTITIONS 8 이라 쓰면, 파서는 PT_CREATE_ENTITY 를 만든다. 그 안의
partition_info 가 전략과 (HASH 의 경우) 파티션 개수를 기술한
다. 스키마 실행기가 do_create_partition (execute_schema.c:4036+)
을 부르고, 이 함수가 다음 일을 한다.
- 마스터 클래스를
au_fetch_class로 가져 온다. attribute 리스트와 OID 가 손에 들어 온다. - 마스터 클래스가 상속 hierarchy 의 일부라면 파티셔닝 DDL 을
거부한다 —
if (smclass->users != NULL || smclass->inheritance != NULL) { … ER_SM_NO_PARTITION_ON_HIERARCHIES }. 이 제약 이 파티션 테이블을 subclass 로 두지 못하게, 그리고 subclass 를 파티션 테이블로 만들지 못하게 막는다. - HASH 의 경우 파티션 이름
<class>__p__p<i>(i ∈[0, hashsize)) 을 자동 생성하고, 각각을dbt_create_class와do_create_local을 부른다. 파티션 클래스가 실체화된다.PARTITIONED_SUB_CLASS_TAG매크로가 이름 가운데의 구분자__p__다. 나중 코드가 파티션 이름을 grep 으로 잡을 수 있게 한다. - RANGE/LIST 의 경우 파스 트리의 명시적
parts리스트를 따라 가며 range 경계가 monotonic 하고 disjoint 한지 (또는 list 값이 파티션 사이에서 pairwise disjoint 한지) 검증한 다음, 각 파티션 엔트리당 클래스 하나씩을 만든다. - 만들어진 각 파티션 클래스가 단일 상속으로 마스터에 연결된다
(
partition_parent_atts = smclass->attributes). 자기SM_PARTITION레코드 (class_partition_type = DB_PARTITION_CLASS) 를 받고, 마지막으로 schema-template flow 로 commit 된다.
동행하는 DDL 헬퍼들.
do_alter_partitioning_pre/do_alter_partitioning_post— 실제ALTER TABLE본체 앞뒤로 실행되는 pre/post 훅 한 쌍.ADD PARTITION,DROP PARTITION,COALESCE PARTITION,REORGANIZE PARTITION같은 변형들에 쓰인다.do_remove_partition_pre/_post— 한 파티션을 떨어내고 마스터로부터 분리한다.do_coalesce_partition_pre/_post— N 개의 HASH 파티션을 N-K 개로 합친다 (기존 데이터를 rehash).do_reorganize_partition_pre/_post— RANGE 나 LIST 파티 션을 재구성하며 데이터를 재분배한다.do_create_partition_constraints와do_create_partition_constraint— 마스터의 인덱스와 제약을 각 파티션에 복제한다 (CUBRID 가 local 인덱스를 쓰기 때문이 다. 아래 참조).
DDL 은 파티션 정보를 마스터 클래스의 SM_PARTITION 멤버와 각
자식 클래스의 자기 멤버 양쪽에 쓴다. 둘 다 표준 schema-template
flush 를 따라 흐르므로 카탈로그 레코드 (_db_class, 있다면
_db_partition, 그리고 _db_class_repr 의 representation 사
슬) 가 atomic 하게 갱신된다. 별도의 파티션 전용 카탈로그 테이
블은 없다 — 데이터는 모두 _db_class.partition (클래스 레코드
에 박힌 DB_SEQ 리터럴) 와 상속 edge 에 들어 있다.
런타임 모델 — PRUNING_CONTEXT
섹션 제목: “런타임 모델 — PRUNING_CONTEXT”실행기가 파티션 테이블을 스캔/삽입/갱신하려 할 때, 서버 측
PRUNING_CONTEXT 를 만든다. 구조체는 partition_sr.h 에 있다.
// pruning_context — partition_sr.hstruct pruning_context{ THREAD_ENTRY *thread_p; OID root_oid; /* OID of the root class */ REPR_ID root_repr_id; access_spec_node *spec; /* class to prune */ val_descr *vd; DB_PARTITION_TYPE partition_type; /* hash, range, list */
OR_PARTITION *partitions; /* partitions array, [0] is master */ OR_PARTITION *selected_partition; SCANCACHE_LIST *scan_cache_list; int count; /* number of entries (master+children) */
xasl_unpack_info *fp_cache_context; func_pred *partition_pred; /* deserialized partition expression */ int attr_position; /* attribute position in index key */ ATTR_ID attr_id; /* attribute id of partition key */ HEAP_CACHE_ATTRINFO attr_info; int error_code;
int pruning_type; /* DB_PARTITIONED_CLASS | DB_PARTITION_CLASS */ bool is_attr_info_inited; bool is_from_cache;};partitions[0] 은 마스터, partitions[1..count-1] 은 파티션이
다. partition_pred 는 파티션 키를 계산하는 deserialize 된
regu-variable. attr_id 는 파티션 키의 컬럼 id (regu 트리를
걸어 다니며 파티션 술어에서 추출된다). attr_info 는 술어를
RECDES 위에서 평가할 때 쓰는 heap attribute 캐시다.
라이프사이클은 명료하다.
partition_init_pruning_context가 구조체를 0 으로 초기화한다.partition_load_pruning_context가 채운다. 먼저 서버별 hash 캐시 (db_Partition_Ht, root 클래스 OID 키, 크기는PARTITION_CACHE_SIZE = 200) 에서 로드를 시도한다. miss 면heap_get_class_partitions로 디스크에서OR_PARTITION배 열을 읽고,partition_load_partition_predicate로partitions[0].values[2](직렬화된FUNC_PRED스트림) 에 서 regu 트리를 deserialize 한 다음, 결과를 캐시에 넣는다.- 호출자가 가지치기를 한다 (spec 단위, insert 단위, update 단 위, 인덱스 lookup 단위).
partition_clear_pruning_context가 scan 캐시들과 deserialize 된 술어를 해제한다.
이 캐시는 INSERT INTO t SELECT ... FROM t2 같은 대량 DML 에
필수적이다. 캐시가 없다면 행마다 카탈로그를 다시 읽어야 했을
것이다. 캐시 hash 테이블은 서버 부팅 시 partition_cache_init
이 만들고, 스키마 무효화 시점에 partition_decache_class 가 걸
어 다닌다.
옵티마이즈 시점의 가지치기 — partition_prune_spec
섹션 제목: “옵티마이즈 시점의 가지치기 — partition_prune_spec”옵티마이저 측 핵심 진입점은 partition.c:3313 의
partition_prune_spec 이다. query_executor.c:8656 (일반 scan
준비) 과 query_executor.c:13361 (parallel-query 클래스 lookup)
에서 호출되며, parallel 의 px_heap_scan task 에서도 부른다.
계약은 다음과 같다 — pruning_type == DB_PARTITIONED_CLASS 인
access_spec_node 가 주어지면, spec->where_pred,
spec->where_key, spec->where_range 를 파티션 기술자를
걸어 다니며, 살아남은 파티션의 PARTITION_SPEC_TYPE 연결 리스
트를 spec->parts 에 만든다.
// partition_prune_spec — partition.cintpartition_prune_spec (THREAD_ENTRY * thread_p, val_descr * vd, access_spec_node * spec){ ... if (spec->pruning_type != DB_PARTITIONED_CLASS) { spec->pruned = true; return NO_ERROR; } ... partition_load_pruning_context (thread_p, &ACCESS_SPEC_CLS_OID (spec), spec->pruning_type, &pinfo); ... pruningset_init (&pruned, PARTITIONS_COUNT (&pinfo)); pruningset_set_all (&pruned);
if (pinfo.spec->where_pred != NULL) { pruningset_init (&pruned_pred, PARTITIONS_COUNT (&pinfo)); status = partition_match_pred_expr (&pinfo, pinfo.spec->where_pred, &pruned_pred); if (status != MATCH_NOT_FOUND) pruningset_intersect (&pruned, &pruned_pred); } if (pinfo.spec->where_key != NULL) { /* same idea, intersect */ } if (pinfo.spec->where_range != NULL) { /* same idea, intersect */ }
error = pruningset_to_spec_list (&pinfo, &pruned); ... spec->pruned = true;}비트셋 pruned 는 모든 파티션 살림 으로 시작한다. 술어 슬롯
세 개 — where_pred (일반 WHERE), where_key (인덱스 스캔의
key-range 필터), where_range (UPDATE/DELETE 의 MVCC 재평가
술어) — 가 각각 비트셋을 기여하고, 결과는 그 셋의 교집합이다.
어떤 슬롯이 MATCH_NOT_FOUND 를 돌려 주면 (술어가 파티션 키
를 참조하지 않으면) 교집합에 끼지 않는다 — 비트 패턴이 “정보
없음” 으로 취급된다. 이게 안전한 기본값이다.
pruningset_to_spec_list 가 비트셋을 걸어 다니며 살아남은 파
티션마다 (class_oid, class_hfid, btid) 를 복사해 spec->parts
에 PARTITION_SPEC_TYPE * 사슬을 만든다. 인덱스 스캔의 경우,
heap_get_index_with_name 으로 각 파티션의 같은 이름의 local
인덱스를 찾기까지 한다.
비트셋 자체 (PRUNING_BITSET) 는 partition.c:70-83 에서 직
접 작성된 것으로, MAX_ELEMENTS = 1024 파티션 크기로 잡혀 있
다 (partition.h 의 MAX_PARTITIONS = 1024 매크로와 맞춘다).
연산은 평범하다 — pruningset_init, _set_all, _add,
_remove, _intersect, _union, _is_set, _popcount, 그리
고 iterator. 인코딩은 파티션당 1 비트. 파티션 i 가 비트 i
에 대응하지만, OR_PARTITION 배열에서는 인덱스 0 이 마스터로
예약되어 있어서 비트 i 가 OR_PARTITION[i+1] 을 가리킨다.
partition_match_pred_expr 가 술어 트리를 걸어 다닌다.
T_PRED+B_AND: 두 자식을 재귀, 결과 비트셋을 교집합. 한쪽이MATCH_NOT_FOUND면 다른 쪽 비트셋만 채택한다 (A AND ?는 최대A다 —A가 제외하는 건 다 제외되고,?가 추가로 제외할 수 있는 건 그저 추가 보너스다).T_PRED+B_OR: 두 자식을 재귀, 결과 비트셋을 합집합. 어느 한쪽이라도MATCH_NOT_FOUND면MATCH_NOT_FOUND를 반 환한다 (A OR ?는 최소A이고?는 무엇이든 될 수 있으 니까). 보수적 OR 규칙이다.T_EVAL_TERM/T_COMP_EVAL_TERM: 비교lhs op rhs.lhs가 파티션 expression 과 매치되면rhs로 가지치고,rhs가 매치되면lhs로 가지친다. 둘 다 매치되지 않지만 파티션 expression 이 함수 (예 —YEAR(date)) 이고 술어 한쪽이 파티 션 expression 을 부분 트리로 포함 하고 있으면,partition_prune_for_function으로 흘러 들어가서 더 엄격한 분석을 시도한다.T_EVAL_TERM/T_ALSM_EVAL_TERM:IN (...)/NOT IN (...)과= ANY (...)/<> ANY (...)를 다룬다.eq_flag에 따라 op 가PO_IN/PO_NOT_IN으로 재작성된다.T_LIKE_EVAL_TERM,T_RLIKE_EVAL_TERM,T_NOT_TERM:MATCH_NOT_FOUND반환. CUBRID 는 LIKE 패턴에서 range 를 추 출할 수 있을 때조차 (예 — list 파티션 위의name LIKE 'A%') 하지 않는다.
leaf 동작은 partition_prune_db_val 이고, partition_type 으
로 디스패치한다.
// partition_prune_db_val — partition.cstatic MATCH_STATUSpartition_prune_db_val (PRUNING_CONTEXT * pinfo, const DB_VALUE * val, const PRUNING_OP op, PRUNING_BITSET * pruned){ switch (pinfo->partition_type) { case DB_PARTITION_HASH: return partition_prune_hash (pinfo, val, op, pruned); case DB_PARTITION_RANGE: return partition_prune_range (pinfo, val, op, pruned); case DB_PARTITION_LIST: return partition_prune_list (pinfo, val, op, pruned); default: return MATCH_NOT_FOUND; }}Hash 가지치기
섹션 제목: “Hash 가지치기”partition_prune_hash 가 셋 중 가장 단순하다.
// partition_prune_hash — partition.c (excerpted)case PO_EQ: if (TP_DOMAIN_TYPE (col_domain) != DB_VALUE_TYPE (val_p)) { /* coerce val_p into the partition column's domain */ if (tp_value_cast (val_p, &val, col_domain, false) == DOMAIN_INCOMPATIBLE) { /* impossible; treat as no-match */ er_clear (); status = MATCH_OK; } } else pr_clone_value (val_p, &val);
idx = mht_get_hash_number (hash_size, &val); pruningset_add (pruned, idx); status = MATCH_OK; break;
case PO_IN: /* iterate through values in the IN-list, hash each, set its bit */case PO_IS_NULL: pruningset_add (pruned, 0); /* first partition holds NULLs */ break;default: status = MATCH_NOT_FOUND;핵심 호출이 mht_get_hash_number (hash_size, &val) 다 — mht.c
의 hash-table 들이 쓰는 것과 같은 일반 CUBRID 해시 함수다. =
이면 정확히 한 비트가 set 되고, IN 이면 IN-list 원소마다 한
비트가 set 된다. < > <= >= 같은 다른 op 는 결과가 모름 이
므로 hash 타입은 MATCH_NOT_FOUND 를 돌려 모든 파티션을 살린
다. NULL 은 컨벤션상 파티션 0 으로 고정된다.
Range 가지치기
섹션 제목: “Range 가지치기”partition_prune_range 가 가장 복잡하다 — 구간 겹침을 추론해
야 하기 때문이다. 각 파티션 i 를 partitions[i+1].values[0..1]
에서 min 과 max 를 읽는다 (min == NULL 이면 MINVALUE — 첫
파티션 — 이고 max == NULL 이면 MAXVALUE — 마지막 파티션 -
이다). 그런 다음 val 을 둘 모두와 비교한다.
// partition_prune_range — partition.c (excerpted)rmin = (min is null ? DB_LT : tp_value_compare (&min, val, 1, 1));rmax = (max is null ? DB_LT : tp_value_compare (val, &max, 1, 1));
switch (op) { case PO_EQ: /* min <= val < max */ if ((rmin == DB_EQ || rmin == DB_LT) && rmax == DB_LT) pruningset_add (pruned, i), goto cleanup; /* exactly one fits */ break; case PO_LT: /* val < max for any partition where min < val */ if (rmin == DB_LT) pruningset_add (pruned, i); break; case PO_LE: ... case PO_GT: ... case PO_GE: ... case PO_IS_NULL: if (DB_IS_NULL (&min)) pruningset_add (pruned, i), goto cleanup; break; }range 구간은 [min, max) 다 — 좌측 닫힘, 우측 열림. PO_GT
(strict >) 의 경우, 코드가 먼저 partition_decrement_value 를
max 에 적용해 half-open 구간을 fully-closed 비교로 바꾼다.
트릭은 이렇다 — > 가 max 와 같은 값에 대해서는 [min, max)
파티션과 매치하면 안 되지만, 경계 비교는 val < max 를 원한
다. max 를 max - 1 로 깎아 두면 (정수형이나 날짜형에서만 가
능) 비교가 그대로 옳아진다. decrement 가 실패하는 타입에서는
함수가 false 를 돌려 default 규칙으로 빠진다. 컬렉션 타입 값
(val IN (1, 2, 3) 등) 에 대해서는, 함수가 원소별로 재귀 가지
치기 한 다음 결과를 합집합 한다 (= 의 경우는 컬렉션과의 동
등이 엄밀히 불가능하므로 교집합 이지만, 기존 코드 경로가 방어
적으로 다룬다).
List 가지치기
섹션 제목: “List 가지치기”partition_prune_list 는 파티션별 값 집합을 따라가며 멤버십 질
문을 던진다.
// partition_prune_list — partition.c (excerpted)case PO_EQ: if (db_set_ismember (part_collection, (DB_VALUE *) val)) { pruningset_add (pruned, i); return MATCH_OK; /* values are disjoint, no need to look further */ } break;case PO_IN: /* perform intersection of part_collection and val_collection; if non-empty, this partition qualifies */ for (j = 0; j < size; j++) { db_set_get (part_collection, j, &col); if (db_set_ismember (val_collection, &col)) { pruningset_add (pruned, i); break; } }case PO_IS_NULL: if (db_set_has_null (part_collection)) pruningset_add (pruned, i), return MATCH_OK;case PO_NE: case PO_LT: case PO_LE: case PO_GT: case PO_GE: return MATCH_NOT_FOUND; /* list partitions are unordered */PO_EQ 의 disjointness 최적화 (더 볼 필요 없음) 가 핵심이
다. list 파티션은 정의상 pairwise disjoint 한 값 집합을 가진다
(DDL 이 강제한다). 첫 매치가 곧 유일 매치다. = 와 IN 외의
비교 연산자는 항상 MATCH_NOT_FOUND 를 돌려 준다 — list 분할
이 값 집합 위에 어떤 순서도 부과하지 않기 때문이다.
flowchart TD
Start[partition_prune_spec 호출] --> Init[partition_load_pruning_context<br/>캐시 hit 시 캐시 로드 / miss 시 OR_PARTITION 배열 읽기]
Init --> Bitset[pruned = 모두 1 비트셋]
Bitset --> WhereP{where_pred?}
WhereP -->|있음| MatchP[partition_match_pred_expr<br/>T_PRED 트리 walk]
MatchP --> Intersect1[pruned = pruned ∩ pruned_pred]
WhereP -->|없음| WhereK
Intersect1 --> WhereK{where_key?}
WhereK -->|있음| MatchK[partition_match_pred_expr<br/>key 필터에 적용]
MatchK --> Intersect2[pruned = pruned ∩ pruned_key]
WhereK -->|없음| WhereR
Intersect2 --> WhereR{where_range?}
WhereR -->|있음| MatchR[partition_match_pred_expr<br/>MVCC 재평가 술어에 적용]
MatchR --> Intersect3[pruned = pruned ∩ pruned_range]
WhereR -->|없음| ToList
Intersect3 --> ToList[pruningset_to_spec_list<br/>PARTITION_SPEC_TYPE 사슬 실체화]
ToList --> SpecParts[access_spec.parts = 사슬]
SpecParts --> Done[spec->pruned = true]
INSERT/UPDATE 시점의 행별 파티션 결정
섹션 제목: “INSERT/UPDATE 시점의 행별 파티션 결정”DML 의 경우 가지치기가 유일한 파티션을 만들어 내야 한다 — 그
행이 들어 갈 heap. 이 일을 partition.c:3465 의
partition_find_partition_for_record 가 한다. 이 함수를
partition_prune_insert 와 partition_prune_update 가 감싸서
INSERT 측과 UPDATE 측 의미를 제공한다.
// partition_find_partition_for_record — partition.c (excerpted)static intpartition_find_partition_for_record (PRUNING_CONTEXT * pinfo, const OID * class_oid, RECDES * recdes, OID * partition_oid, HFID * partition_hfid){ ... /* lazy-init heap attribute cache for partition column */ if (pinfo->is_attr_info_inited == false) { heap_attrinfo_start (pinfo->thread_p, &pinfo->root_oid, 1, &pinfo->attr_id, &pinfo->attr_info); partition_set_cache_info_for_expr (pinfo->partition_pred->func_regu, pinfo->attr_id, &pinfo->attr_info); pinfo->is_attr_info_inited = true; }
/* read the partition column value out of the record */ repr_id = or_rep_id (recdes); or_set_rep_id (recdes, pinfo->root_repr_id); /* read as if for the master */ heap_attrinfo_read_dbvalues (pinfo->thread_p, ..., recdes, &pinfo->attr_info); or_set_rep_id (recdes, repr_id); /* restore */
/* evaluate the partition expression */ fetch_peek_dbval (pinfo->thread_p, pinfo->partition_pred->func_regu, ..., &result);
if (db_value_is_null (result)) op = PO_IS_NULL;
/* prune by the resulting value */ status = partition_prune_db_val (pinfo, result, op, &pruned); count = pruningset_popcount (&pruned); if (status != MATCH_OK || count != 1) return ER_PARTITION_NOT_EXIST; /* row has no home */
pos = pruningset_iterator_next (&it); COPY_OID (partition_oid, &pinfo->partitions[pos + 1].class_oid); HFID_COPY (partition_hfid, &pinfo->partitions[pos + 1].class_hfid);
/* rewrite the record's REPR_ID to the partition's representation */ if (!OID_EQ (class_oid, partition_oid)) or_set_rep_id (recdes, pinfo->partitions[pos + 1].rep_id);}흥미로운 순간 셋.
- 마스터 REPR_ID 로 읽고, 파티션 REPR_ID 로 쓴다. 레코드 는 호출자가 어떤 REPR_ID 로 만들었든 그대로 도착한다. 파티 션 컬럼을 꺼내려면, 이 함수가 마스터 클래스의 REPR_ID 로 일 시적으로 바꾼다. 그래야 heap-attribute 기계가 레이아웃을 디 코드한다. 평가가 끝나면 다시 돌려놓는다. 마지막에 반환 직전, 목적 파티션의 REPR_ID 가 다르면 in-place 로 REPR_ID 비트를 덮어 쓴다. 소스 주석이 왜 이 지름길이 안전한지 설명한다 — 파티션은 attribute 레이아웃을 마스터에서 상속받기 때문에, 디 스크 이미지가 REPR_ID 비트만 제외하면 동일하다.
- NULL 처리 —
PO_IS_NULLop. 파티션 expression 이 NULL 을 만들면PO_IS_NULL로 가지치기 된다. 이는 hash 에서는 파 티션 0 으로, range 에서는min == NULL(MINVALUE 파티션) 인 파티션으로, list 에서는 명시적으로 NULL 을 받는 파티션으 로 라우팅된다. NULL 을 받는 파티션이 없으면ER_PARTITION_NOT_EXIST를 돌려 준다. - 정확히 하나 단언. INSERT 의 경우, 한 개를 초과하는 파티
션이 매치되면 파티션 기술자가 손상된 것이다. 0 개가 매치되
면 행이 갈 곳이 없다. 둘 다
ER_PARTITION_NOT_EXIST.
partition_prune_insert 는 얇은 wrapper 다 — pruning context 를
로드하고 (캐시 또는 신규) partition_find_partition_for_record
를 호출한다. 호출자가 특정 파티션을 명시적으로 노렸을 때
(DB_PARTITION_CLASS pruning 타입, 예 — INSERT INTO t__p__p3 VALUES …), 계산된 파티션이 명시한 것과 일치하는지 검증한다.
일치하지 않으면 ER_INVALID_DATA_FOR_PARTITION 을 돌려 준다.
partition_prune_update 는 비슷하지만 마스터 OID 가 아니라 파
티션 클래스 OID 에서 출발한다 (UPDATE 는 마스터가 아니라 항상
특정 파티션의 heap 위에서 동작한다). partition_find_root_class_oid
로 마스터까지 올라간 다음 pruning context 를 로드하고 목적 파
티션을 계산해 호출자에게 돌려 준다. locator (
locator_update_force / locator_move_record) 가 이를 받아 행
을 in-place 로 다시 쓸지 (같은 파티션) 파티션 사이에서 옮길지
(다른 파티션) 결정한다.
flowchart TD
Insert[INSERT INTO t VALUES ...] --> Locator[locator_insert_force<br/>pruning_type != DB_NOT_PARTITIONED_CLASS]
Locator --> Prune[partition_prune_insert]
Prune --> Find[partition_find_partition_for_record]
Find --> Read[heap_attrinfo_read_dbvalues<br/>마스터 REPR_ID 로 파티션 컬럼 읽기]
Read --> Eval[fetch_peek_dbval on partition_pred]
Eval --> Dispatch{partition_type}
Dispatch -->|HASH| Hash[mht_get_hash_number mod N → idx]
Dispatch -->|RANGE| Range[binary search for [min, max)]
Dispatch -->|LIST| List[파티션마다 db_set_ismember]
Hash --> One[정확히 한 비트가 set 되어야 함]
Range --> One
List --> One
One -->|예| Rewrite[or_set_rep_id 으로 파티션 REPR_ID 쓰기]
Rewrite --> Cache[locator_get_partition_scancache 으로 목적 heap]
Cache --> Heap[heap_insert_logical 으로 목적 파티션 HFID 에 삽입]
One -->|아니오| Err[ER_PARTITION_NOT_EXIST]
실행 시점의 파티션별 scan 디스패치
섹션 제목: “실행 시점의 파티션별 scan 디스패치”partition_prune_spec 이 spec->parts 를 채우고 나면, 실행기
가 파티션을 하나씩 차례로 돈다. 관련된 access_spec_node 필드
는 xasl.h 에 있다.
// access_spec_node — xasl.hstruct access_spec_node{ ... int pruning_type; /* DB_PARTITIONED_CLASS | DB_NOT_PARTITIONED_CLASS | DB_PARTITION_CLASS */ ...#if defined (SERVER_MODE) || defined (SA_MODE) SCAN_ID s_id; /* scan identifier */ PARTITION_SPEC_TYPE *parts; /* partitions of the current spec */ PARTITION_SPEC_TYPE *curent; /* current partition */ ... bool pruned; /* true if partition pruning has been performed */#endif};
// partition_spec_node — xasl.hstruct partition_spec_node{ OID oid; /* class oid */ HFID hfid; /* class hfid */ BTID btid; /* index id */ PARTITION_SPEC_TYPE *next; /* next partition */ SCAN_STATS scan_stats;};실행기의 scan 루프 (query_executor.c 와 scan_manager.c) 가
파티션마다 한 번씩 그 아래의 SCAN_ID 를 다시 열어 가며,
curent 를 따라 parts 연결 리스트를 걸어 다닌다. 그리고 각
파티션의 튜플을 차례로 흘려 보낸다. 모든 파티션을 가로질러 합
쳐 버리는 aggregate 의 경우 실행기는 파티션을 논리적으로 한 스
트림으로 다룬다. 반면 파티션별 정체성을 보존하는 sort/hash 의
경우, scan stats 가 partition_spec_node 단위로 추적된다 (
scan_stats).
query_executor.c:8680+ 의 lock 획득 단계도 중요하다. 가지치
기 후에 실행기는 spec->parts 를 걸어 다니며 살아남은 파티션
의 클래스 OID 마다 lock_subclass 로 IS_LOCK (UPDATE/DELETE
면 IX_LOCK) 을 잡는다. 가지치기된 파티션은 lock 이 걸리지
않는다. 가지치기가 주는 성능 이득의 한 축이다 — 100 개 파티션
중 99 개가 가지치기되면 클래스 lock 이 100 개가 아니라 1 개만
잡힌다.
Locator 통합 — DML 을 파티션 heap 으로 라우팅
섹션 제목: “Locator 통합 — DML 을 파티션 heap 으로 라우팅”locator_sr.c 가 heap 레이어 (클래스별 HFID) 와 파티션 레이
어 사이의 다리다. 통합 지점들.
-
locator_insert_force(locator_sr.c:4937+):pruning_type != DB_NOT_PARTITIONED_CLASS으로 호출되면,partition_prune_insert를 불러 목적(real_class_oid, real_hfid)를 계산한다. 그 다음lock_subclass로 파티션 클래스에 IX-lock 을 잡고,locator_get_partition_scancache으로 파티션별 scan 캐시를 가져 온 후, 실제heap_insert_logical호출을 마스터 heap 이 아닌 파티션 heap 으로 라우팅한다. 마스터 클래스는 (파티션된 경우) 자기 heap 이 없으므로, 가지치기 단계를 우회한 insert 는 실패한다. -
locator_get_partition_scancache(locator_sr.c:12314+):partition_get_scancache/partition_new_scancache의 뒤 를 받쳐 주는 헬퍼다. 목적 파티션 OID 와 HFID 가 주어지면, pruning context 의scan_cache_list에서 기존PRUNING_SCAN_CACHE를 찾거나 새로 할당해 그 위에HEAP_SCANCACHE를 시작시킨 다. 캐시는 pruning context 의 수명 동안 — 보통 multi-row INSERT/UPDATE 문 전체 — 살아 있다. 그래서 각 파티션이 scan 캐시 셋업 비용을 한 번만 낸다. -
locator_move_record(locator_sr.c:5295+): 파티션 테 이블에 대한 UPDATE 가 파티션 키 값을 바꿔서 post-image 가 pre-image 와 다른 파티션에 속하게 되면, locator 는 행을 새 파티션에 delete-and-re-insert 해야 한다. 이 함수가 그 둘을 atomic 하게 한다 — 목적 파티션의 scan 캐시를 가져오고, 새 heap 에locator_insert_force, 이전 heap 에locator_delete_force_for_moving를 부른다. pruning 레이어 는 이미 레코드의 REPR_ID 를 목적 파티션의 것으로 바꿔 두었 으므로, insert 는 그대로 바이트만 깐다. 가장 바깥 드라이버 (locator_update_force) 가 먼저partition_prune_update를 부르고, 파티션 할당이 바뀐 경우에만locator_move_record로 디스패치한다 (locator_sr.c:5915, 5953). -
locator_area_op_to_pruning_type(locator_sr.c:12372+):LC_FLUSH_INSERT_PRUNE→DB_PARTITIONED_CLASS,LC_FLUSH_INSERT_PRUNE_VERIFY→DB_PARTITION_CLASS등으 로 매핑한다. 클라이언트 측 워크스페이스 flush 가 dirty 레코 드마다 사용자가 마스터로 insert 했는지 특정 파티션으로 insert 했는지에 따라LC_FLUSH_*코드를 매긴다. 서버는 이를 보고 풀 가지치기와 검증으로서의 가지치기 사이에서 결정한다.
locator_sr.c:4139 의 unique-index 무결성 검사도 가지치기 소비
자다. insert 시 unique-index 제약을 검사할 때, 서버는
partition_prune_unique_btid 로 unique 키를 소유한 파티션을
찾고, 그 파티션의 local 인덱스만 검사한다. 이게 작동하는 이유
는 CUBRID 가 local 인덱스를 쓰기 때문이다 — 행을 소유한 파티
션이 unique-index 엔트리도 소유한다. 그리고 가지치기가 단일 파
티션을 식별하려면 unique-index 컬럼이 파티션 키를 포함해야 한
다 (그러지 않으면 파티션을 가로지르는 unique 가 local 로 강제
될 수 없다).
파티션 테이블 위의 인덱스 — local 만
섹션 제목: “파티션 테이블 위의 인덱스 — local 만”CUBRID 는 현재 파티션 테이블 위에 local 인덱스를 쓴다.
do_create_partition_constraints 가 동작하면, 마스터 클래스의
제약을 따라가며 모든 파티션마다 그 제약의 사본을 만든다. B-tree
ID (BTID) 는 파티션마다 다르다 — heap_get_index_with_name
이 마스터의 인덱스 이름을 가지고 한 파티션의 local BTID 를 찾
아 주는 lookup 이다. local-only 의 함의는 다음과 같다.
- 싸진 DDL. 파티션 추가/제거가 그 파티션의 인덱스만 건드린 다. 글로벌 재구성이 없다.
- 싸진 유지. insert 와 update 가 행이 사는 파티션의 local BTID 만 건드린다.
- unique 컬럼에 파티션 키가 없으면 파티션을 가로지르는 unique
강제가 안 된다. 사용자가
y로 분할된 테이블에UNIQUE(x)를 정의하면, 두 파티션이 각각 같은x값을 가진 행을 가질 수 있고 local-only 강제는 이를 잡지 못한다. 그래서 CUBRID 의 DDL 은 파티션 테이블 위의 UNIQUE 나 PRIMARY KEY 가 파티션 키 를 포함하도록 요구한다. partition_prune_unique_btid가 unique-index search key 가 주어졌을 때 그 키를 소유한 파티션을 (search key 안에 박힌 파티션 키 값으로) 계산하고,(class_oid, hfid, btid)를 파 티션 local 트리플로 다시 쓴다.
partition.c:4250 에 partition_is_global_index 라는 feature
flag 가 있긴 하지만 #if 0 으로 감싸져 있다 (비활성). 이 함수
는 primary-key 인덱스 (global 로 취급할 후보) 와 비-primary-key
unique 인덱스 (인덱스 컬럼이 파티션 키를 포함하는지에 따라
globality 가 결정됨) 를 구별한다. 오늘은 죽은 코드다. 현재 프로
덕션 동작은 모든 인덱스는 local 이다.
Partition-wise join
섹션 제목: “Partition-wise join”query_planner.c 에는 qo_partition / QO_PARTITION 참조가
여럿 있다 — 그러나 이는 planner partition 이지 테이블 레벨
파티션이 아니다. query_planner.c 에서 QO_PARTITION 은 join
그래프의 connected component 를 가리키는 planner 의 용어다 (
join enumeration 을 위한 Selinger 식 query graph 분할).
qo_search_partition, qo_combine_partitions,
QO_PARTITION_NODES, QO_PARTITION_EDGES 같은 심볼이 join-
그래프 파티션 위에서 동작한다 — 수평 테이블 파티션이 아니다.
옵티마이저 안에서 테이블-파티션 인지를 grep 해 보면 거의 비어 있다. 파티션 가지치기는 plan 생성 이후 에 실행된다 — 실행기 가 access spec 을 열 때. 옵티마이저의 비용 계산에 접히지 않는 다. 결과적으로 CUBRID 의 옵티마이저는 파티션 테이블 스캔을 마 스터 클래스에 대한 단일 스캔으로 plan 한다. 가지치기는 런타임 에 불투명하게 적용된다. 그래서 partition-wise join 최적화가 없 다. 같은 파티션 모양을 가진 두 파티션 테이블이 join 되어도, 파 티션이 없는 테이블처럼 join 된다 — T1 의 각 파티션이 T2 의 모 든 파티션과 스캔된다.
flowchart LR
Parser[파서] --> Resolve[이름 해석]
Resolve --> SemCheck[semantic check]
SemCheck --> Opt[옵티마이저<br/>cost-based plan]
Opt --> XASL[XASL 생성<br/>access_spec.pruning_type 설정]
XASL --> Stream[xasl_to_stream → stream_to_xasl]
Stream --> Exec[query_executor<br/>각 spec 을 연다]
Exec --> Prune[partition_prune_spec<br/>spec->parts 채움]
Prune --> Lock[살아남은 파티션마다 lock_subclass]
Lock --> ScanLoop[spec->parts 의 PARTITION_SPEC_TYPE 마다:<br/>scan_open_scan, scan_next_scan, scan_close_scan]
ScanLoop --> Output[합쳐진 튜플 스트림]
다이어그램이 명확히 보여 준다 — 가지치기는 XASL 역직렬화와 스 캔 실행 사이 에 있지, plan 자체에 접히지 않는다. 의도적인 trade-off 다. 가지치기는 빠르고 (1024 파티션에 수백 마이크로초 정도) lazy 하게 동작하므로, “모든 파티션이 살아 있다고 가정해 plan” 의 비용은 실제로는 작다. 비용은 partition-wise join 이 사용 불가능하다는 점이다 — planner 가 파티션별 relation 을 보 지 못하니 결합할 수가 없다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼은 파일 순서가 아니라 역할별로 묶었다. 독자가 파일이 아니 라 어디에서 뭐 하는 코드인가 로 항해할 수 있도록.
카탈로그 메타데이터 (인메모리 + 디스크)
섹션 제목: “카탈로그 메타데이터 (인메모리 + 디스크)”SM_PARTITION(struct,class_object.h) — 각SM_CLASS위의 인메모리 파티션 기술자.partition_type,class_partition_type,pname,expr,values를 들고 다 닌다. 클래스별 인스턴스 —SM_CLASS::partition.OR_PARTITION(struct,object_representation_sr.h) — 서버 측 reload 형태 —(class_oid, class_hfid, partition_type, rep_id, values).heap_get_class_partitions(heap_file.c) 가 실체화한다.OR_CLASSREP::has_partition_info— 각 클래스 representation 위의 단일 비트로 이 클래스는 파티션 정보를 가졌다, 로드해라 를 표시한다.DB_PARTITION_TYPE(enum,storage_common.h) —DB_PARTITION_HASH=0,DB_PARTITION_RANGE=1,DB_PARTITION_LIST=2.DB_CLASS_PARTITION_TYPE(enum,storage_common.h) —DB_NOT_PARTITIONED_CLASS=0,DB_PARTITIONED_CLASS=1,DB_PARTITION_CLASS=2. 첫째는 일반 테이블, 둘째는 마스터 클래스, 셋째는 파티션이다.classobj_make_partition_info,classobj_free_partition_info,classobj_copy_partition_info—class_object.c의 SM_PARTITION 라이프사이클 헬퍼.
DDL (클라이언트 측, execute_schema.c)
섹션 제목: “DDL (클라이언트 측, execute_schema.c)”do_create_partition—CREATE TABLE … PARTITION BY …,ALTER TABLE … ADD PARTITION,ALTER TABLE … ADD HASH PARTITION의 진입점. 파티션 클래스를 실체화하고, 상속으로 마스터에 연결하고,SM_PARTITION레코드를 채운다.do_alter_partitioning_pre/do_alter_partitioning_post— partition-related ALTER 변형의 schema-template flush 앞뒤 에 실행되는 pre/post 훅 한 쌍.do_remove_partition_pre/_post— 한 파티션 drop.do_coalesce_partition_pre/_post— N 개 HASH 파티션 을 그보다 적게 합친다 (행을 rehash).do_reorganize_partition_pre/_post— RANGE/LIST 파티 션 재구성 (재분배).do_create_partition_constraints/do_create_partition_constraint— 마스터의 인덱스와 제약 을 파티션마다 복제.SM_PARTITION_ALTER_INFO(struct) — alter-partition 파이프라인을 따라 흐르는 컨텍스트 묶음 (root 클래스 OP, key 컬럼, promote 된 파티션 이름들).PART_CLASS_INFO(struct) — 생성 도중의 파티션별 임시 정보 (이름, template, object).PARTITION_VARCHAR_LEN,PARTITIONED_SUB_CLASS_TAG (__p__)— 이름 정책 상수.
행별 파티션 결정 (서버 측, partition.c)
섹션 제목: “행별 파티션 결정 (서버 측, partition.c)”partition_prune_insert— INSERT 측 라우팅의 바깥 진입 점.PRUNING_CONTEXT를 로드/사용하고partition_find_partition_for_record를 부르고(pruned_class_oid, pruned_hfid)를 돌려 준다.partition_prune_update— UPDATE 측 라우팅의 바깥 진입 점. 먼저 root 클래스로 올라간 다음 (partition_find_root_class_oid) INSERT 와 같은 일을 한다.partition_find_partition_for_record(static) — 행별 라 우팅의 심장. 레코드에서 파티션 컬럼을 뽑고, 파티션 expression 을 평가하고, 결과로 가지치기 한 다음, 유일한 목적 파티션을 돌려 준다.partition_find_root_class_oid— 어떤 클래스 OID 든 (마스터든 파티션이든) 마스터 클래스 OID 를 돌려 준다.heap_get_class_supers사용.partition_prune_unique_btid— unique-index search key 가 주어졌을 때, 그 키를 소유한 파티션과 파티션 localBTID를 돌려 준다.btree.c의 unique 제약 강제와query_executor.c의 unique scan 이 사용한다.partition_prune_partition_index— search key 와 마스터 의 인덱스 BTID 가 주어졌을 때, 그 키를 소유한 파티션의 (인덱 스 배열에서의) 위치를 돌려 준다.partition_prune_unique_btid의 뒷받침.partition_attrinfo_get_key— (다중 컬럼일 수 있는) 인 덱스 search key 에서 파티션 키 DB_VALUE 를 추출.
옵티마이즈 시점 가지치기 (서버 측, partition.c)
섹션 제목: “옵티마이즈 시점 가지치기 (서버 측, partition.c)”partition_prune_spec— scan 측 가지치기 바깥 진입점.where_pred ∩ where_key ∩ where_range를 파티션 기술자에 적용하고 살아남은 파티션을spec->parts에 채운다.partition_match_pred_expr(static, 재귀) —PRED_EXPR트리 (T_PRED/T_EVAL_TERM/T_COMP_EVAL_TERM/T_ALSM_EVAL_TERM/T_NOT_TERM/T_LIKE_EVAL_TERM) 를 걸어 다니며 살아남은 파티션 비트셋과MATCH_STATUS를 돌려 준다.partition_match_index_key— 인덱스 키 range 가지치기 특수화.partition_match_key_range— 인덱스 스캔의KEY_RANGE하한과 상한을 다룬다.partition_prune—REGU_VARIABLE을partition_get_value_from_regu_var로DB_VALUE로 풀어 낸 다음partition_prune_db_val을 부르는 leaf wrapper.partition_prune_db_val— 전략별 디스패치 — HASH →partition_prune_hash, RANGE →partition_prune_range, LIST →partition_prune_list.partition_prune_hash—mht_get_hash_number (hash_size, &val) → 비트 위치.partition_prune_range— 파티션마다(min, max)와의 구간 비교.>비교를(min, max]에서(min, max)로 변환 하기 위해partition_decrement_value를 사용.partition_prune_list— 파티션마다db_set_ismember. list 파티션은 pairwise disjoint 이므로 첫 매치에서 short-circuit.partition_prune_for_function— 술어의 좌/우 한쪽이 파 티션 expression 을 부분 expression 으로 포함 하는 경우의 가지치기 (예 —YEAR(date)로 분할했을 때YEAR(date) BETWEEN 2020 AND 2024).partition_supports_pruning_op_for_function— function- pruning 로직이 처리할 수 있는(op, function)조합의 lookup 테이블.partition_do_regu_variables_match/partition_do_regu_variables_contain— regu 트리 위의 구 조적 매처 — 이게 같은 파티션 expression 인가? vs “이 regu 트리가 파티션 expression 을 포함하는가?”.partition_get_value_from_key,partition_get_value_from_inarith,partition_get_value_from_regu_var— regu 노드 모양에 따 른 세 가지 regu 노드에서DB_VALUE빼내기 변종.partition_is_reguvar_const— regu 변수가 상수 값인지 (따라서 가지치기가 안전한지).partition_decrement_value— 타입을 아는 정수/날짜 decrement, range 비교를 다듬을 때 사용.partition_get_attribute_id— 파티션 expression regu 트 리에서 기저 attribute id 를 추출. 컨텍스트 로드 도중PRUNING_CONTEXT::attr_id를 채울 때 사용.partition_set_cache_info_for_expr/partition_set_cache_dbvalp_for_attribute— 평가 전에 heap-attribute 캐시를 regu 트리에 wire 한다. 그래야fetch_peek_dbval가 디스크에서 새로 fetch 하지 않고pinfo->attr_info에서 컬럼 값을 읽는다.partition_get_position_in_key— 인덱스 가지치기 시, 인 덱스 키 컬럼 리스트 안에서 파티션 컬럼의 위치를 찾는다.partition_rel_op_to_pruning_op—REL_OP→PRUNING_OPenum 매핑 (R_LT→PO_LT,R_EQ→PO_EQ등).
파티션 기술자 라이프사이클 (서버 측, partition.c)
섹션 제목: “파티션 기술자 라이프사이클 (서버 측, partition.c)”partition_init_pruning_context— zero-init.partition_load_pruning_context— 채우기 (먼저 캐시 시 도. miss 면 heap 에서OR_PARTITION배열 읽고 캐시).partition_clear_pruning_context— scan 캐시와 술어 해 제.partition_load_partition_predicate/partition_free_partition_predicate— 파티션 expression 을 deserialize / 해제 (stx_map_stream_to_func_pred).partition_set_specified_partition— 호출자가 명시적으 로 파티션을 지정한 경우 (DB_PARTITION_CLASS모드),selected_partition을 거기로 가리키게 한다.PARTITION_CACHE_ENTRY(struct) — 클래스별 캐시 엔트리.partition_cache_init/partition_cache_finalize/partition_decache_class— 캐시된 pruning context 의 프 로세스 전역 hash 테이블 (db_Partition_Ht,PARTITION_CACHE_SIZE = 200).partition_cache_pruning_context/partition_load_context_from_cache/partition_cache_entry_to_pruning_context/partition_pruning_context_to_cache_entry— 양방향 변환.partition_free_cache_entry/partition_free_cache_entry_kv— 소멸자.
파티션별 scan 캐시
섹션 제목: “파티션별 scan 캐시”PRUNING_SCAN_CACHE(struct,partition_sr.h) — 파티 션별HEAP_SCANCACHE와 function-index unpack 정보를 감싼 것.SCANCACHE_LIST(struct) —PRUNING_CONTEXT에 매달리 는PRUNING_SCAN_CACHE의 단순 연결 리스트.partition_get_scancache— 파티션 OID 로 기존 scan 캐시 찾기.partition_new_scancache— 새로 만들어 앞에 매달기.locator_get_partition_scancache(locator_sr.c:12314+) —locator_insert_force/locator_move_record가 쓰는 풀서비스 wrapper. 찾거나 만들 고, 새것이라면 안쪽HEAP_SCANCACHE까지 시작시킨다.
가지치기 비트셋 (서버 측, partition.c)
섹션 제목: “가지치기 비트셋 (서버 측, partition.c)”PRUNING_BITSET(struct) —unsigned int set[];와 카 운트로 이루어진, 직접 작성된 비트셋. 1024 파티션까지.PRUNING_BITSET_ITERATOR— 동반 iterator.pruningset_init/_set_all/_copy/_add/_remove/_intersect/_union/_is_set/_popcount— 비트셋 산술.pruningset_iterator_init/pruningset_iterator_next— iteration.pruningset_to_spec_list— 비트셋 → 실행기를 위한PARTITION_SPEC_TYPE연결 리스트.
Scan 디스패치 (executor + scan_manager)
섹션 제목: “Scan 디스패치 (executor + scan_manager)”access_spec_node(struct,xasl.h) — 옵티마이저의 access spec.pruning_type,pruned,parts(살아남은 파 티션의 연결 리스트),curent(현재 iterator 위치) 를 들고 다 닌다.partition_spec_node/PARTITION_SPEC_TYPE(struct,xasl.h) —(oid, hfid, btid, next, scan_stats).prepare_mvcc_reev_data(inquery_executor.c) — scan open 전에partition_prune_spec과 파티션별lock_subclass를 부른다.qexec_clear_partition_expression(partition_free_partition_predicate에서 참조) — deserialize 된 파티션 expression 의 regu 트리 해제.
Locator 통합 (locator_sr.c)
섹션 제목: “Locator 통합 (locator_sr.c)”locator_insert_force— 레코드 insert. 클래스가 파티션 되어 있으면partition_prune_insert를 부르고 heap insert 를 파티션으로 라우팅.locator_update_force— 레코드 update.partition_prune_update를 부르고 파티션 할당이 바뀌었으면locator_move_record로 디스패치.locator_move_record— 두 파티션을 가로지르는 delete- then-insert 를 atomic 하게.locator_get_partition_scancache— 위 참조.locator_area_op_to_pruning_type—LC_FLUSH_*→DB_*_PARTITIONED_CLASSenum 매핑.FOR_INSERT_OR_DELETE/FOR_MOVE— delete-and-reinsert 쌍을 파티션 이동으로 태깅하는 내부 enum (그래야 MVCC 가 새 행을 (일부 bookkeeping 목적상) 신규 insert 가 아니라 연속으 로 다룬다).redistribute_partition_data—REORGANIZE PARTITION이 쓰는 대량 경로.
B-tree / 인덱스 통합
섹션 제목: “B-tree / 인덱스 통합”btree.c::partition_prune_unique_btid호출자 (라인 6080, 22719) — insert 시 unique 검사.btree_load.c::partition_prune_partition_index호출자 ( 라인 4559) — bulk 인덱스 빌드가 파티션별 키를 본다.btree.c::partition_load_pruning_context호출자 (라인 22707) — referencing 측 foreign-key lookup 이 FK 의 pruning context 를 필요로 한다.statistics_sr.c::partition_get_partition_oids호출자 ( 라인 198) — analyze 가 파티션별로 동작.system_catalog.c::partition_get_partition_oids호출자 ( 라인 5449) — 카탈로그 accessor.file_manager.c::partition_get_partition_oids호출자 ( 라인 6854) — file cleanup 이 모든 파티션을 걸어 다닌다.
Aggregate 계층 헬퍼
섹션 제목: “Aggregate 계층 헬퍼”partition_load_aggregate_helper— 파티션 테이블 위의 aggregate 가 있는 query 의 경우, 파티션별(HFID, BTID)배 열을 담은HIERARCHY_AGGREGATE_HELPER를 만들어 aggregate 실행기가 파티션을 차례로 돌 수 있게 한다.HIERARCHY_AGGREGATE_HELPER(inquery_aggregate.hpp) — 받쳐 주는 구조체.
이 개정 시점의 위치 힌트
섹션 제목: “이 개정 시점의 위치 힌트”다음 표는 이 문서의 updated: 시점에 관찰된 심볼 위치를 기록
한다. 심볼 이름이 정본 anchor 이고, 라인 번호는 refactor 와 함
께 시간에 따라 늙는다.
| 심볼 | 파일 | 라인 |
|---|---|---|
MAX_PARTITIONS = 1024 | src/query/partition.h | 26 |
pruning_scan_cache | src/query/partition_sr.h | 50 |
scancache_list | src/query/partition_sr.h | 60 |
pruning_context | src/query/partition_sr.h | 67 |
partition_init_pruning_context (decl) | src/query/partition_sr.h | 95 |
partition_load_pruning_context (decl) | src/query/partition_sr.h | 97 |
partition_prune_spec (decl) | src/query/partition_sr.h | 112 |
partition_prune_insert (decl) | src/query/partition_sr.h | 114 |
partition_prune_update (decl) | src/query/partition_sr.h | 118 |
partition_prune_unique_btid (decl) | src/query/partition_sr.h | 122 |
partition_get_partition_oids (decl) | src/query/partition_sr.h | 125 |
partition_load_aggregate_helper (decl) | src/query/partition_sr.h | 128 |
partition_find_root_class_oid (decl) | src/query/partition_sr.h | 135 |
partition_prune_partition_index (decl) | src/query/partition_sr.h | 137 |
pruning_bitset | src/query/partition.c | 71 |
partition_cache_entry | src/query/partition.c | 95 |
partition_init_pruning_context | src/query/partition.c | 2589 |
partition_load_pruning_context | src/query/partition.c | 2674 |
partition_clear_pruning_context | src/query/partition.c | 2805 |
partition_load_partition_predicate | src/query/partition.c | 2862 |
partition_set_specified_partition | src/query/partition.c | 2929 |
partition_get_attribute_id | src/query/partition.c | 3052 |
partition_get_position_in_key | src/query/partition.c | 3125 |
partition_prune_spec | src/query/partition.c | 3313 |
partition_find_partition_for_record | src/query/partition.c | 3465 |
partition_prune_insert | src/query/partition.c | 3603 |
partition_prune_update | src/query/partition.c | 3710 |
partition_get_scancache | src/query/partition.c | 3831 |
partition_new_scancache | src/query/partition.c | 3861 |
partition_get_partition_oids | src/query/partition.c | 3891 |
partition_decrement_value | src/query/partition.c | 3976 |
partition_prune_unique_btid | src/query/partition.c | 4064 |
partition_find_inherited_btid | src/query/partition.c | 4106 |
partition_load_aggregate_helper | src/query/partition.c | 4150 |
partition_attrinfo_get_key | src/query/partition.c | 4337 |
partition_prune_partition_index | src/query/partition.c | 4441 |
partition_prune_list | src/query/partition.c | 1086 |
partition_prune_hash | src/query/partition.c | 1221 |
partition_prune_range | src/query/partition.c | 1340 |
partition_prune_db_val | src/query/partition.c | 1545 |
partition_prune | src/query/partition.c | 1574 |
partition_match_pred_expr | src/query/partition.c | 1933 |
partition_prune_for_function | src/query/partition.c | 2105 |
partition_match_key_range | src/query/partition.c | 2406 |
pruningset_to_spec_list | src/query/partition.c | 873 |
pruningset_init, etc. | src/query/partition.c | 187-396 |
sm_partition | src/object/class_object.h | 728 |
SM_CLASS::partition | src/object/class_object.h | 798 |
or_partition | src/base/object_representation_sr.h | 196 |
DB_PARTITION_TYPE | src/storage/storage_common.h | 1211 |
DB_CLASS_PARTITION_TYPE | src/storage/storage_common.h | 1218 |
partition_spec_node | src/query/xasl.h | 1029 |
access_spec_node::pruning_type | src/query/xasl.h | 1052 |
access_spec_node::parts | src/query/xasl.h | 1057 |
access_spec_node::pruned | src/query/xasl.h | 1061 |
do_create_partition | src/query/execute_schema.c | 4036 |
do_alter_partitioning_pre (decl) | src/query/execute_schema.c | 334 |
do_alter_partitioning_post (decl) | src/query/execute_schema.c | 335 |
do_remove_partition_pre (decl) | src/query/execute_schema.c | 337 |
do_coalesce_partition_pre (decl) | src/query/execute_schema.c | 339 |
do_reorganize_partition_pre (decl) | src/query/execute_schema.c | 341 |
do_create_partition_constraints | src/query/execute_schema.c | 5728 |
SM_PARTITION_ALTER_INFO | src/query/execute_schema.c | 195 |
partition_prune_spec 호출자 | src/query/query_executor.c | 8656 |
partition_prune_spec parallel-path 호출자 | src/query/query_executor.c | 13361 |
locator_insert_force 파티션 분기 | src/transaction/locator_sr.c | 4969 |
locator_move_record | src/transaction/locator_sr.c | 5295 |
locator_update_force::partition_prune_update | src/transaction/locator_sr.c | 5915 |
locator_get_partition_scancache | src/transaction/locator_sr.c | 12314 |
locator_area_op_to_pruning_type | src/transaction/locator_sr.c | 12372 |
partition_prune_unique_btid (in btree.c) | src/storage/btree.c | 6080, 22719 |
partition_prune_partition_index (in btree_load.c) | src/storage/btree_load.c | 4559 |
partition_get_partition_oids (statistics) | src/storage/statistics_sr.c | 198 |
소스 검증 (2026-05-01 기준)
섹션 제목: “소스 검증 (2026-05-01 기준)”이 문서는 cubrid-class-object.md, cubrid-query-optimizer.md,
cubrid-locator.md, cubrid-catalog-manager.md 이후에 쓰여졌
다. 접점과 불일치 지점.
-
cubrid-class-object.md가SM_PARTITION을SM_CLASS의 멤버로 도입했고, 파티션이 OODB 상속 기계 (SM_CLASS::users/SM_CLASS::inheritance) 로 마스터 클래 스의 subclass 로 묶인다고 적었다. 본 문서가 런타임 측 절반을 채운다 —SM_PARTITION(인메모리 schema 산물) 이OR_PARTITION(디스크 reload 산물) 으로, 그리고PRUNING_CONTEXT(요청별 평가 산물) 로 어떻게 매핑되는가. 셋 모두 파티션 규칙을 들고 다니지만, 모양과 수명이 셋 다 다르다. -
cubrid-query-optimizer.md가 옵티마이저 측 일거리 — XASL 생성과 cost-based plan 선택 — 를 기술하면서, 옵티마이저의 partition 개념 (qo_partition,QO_PARTITION) 이 join 그래프의 connected component 를 가리키는 planner 의 용어이 지 수평 테이블 파티션이 아니라는 점을 짚었다. 본 문서가 그 검증을 확정한다 — CUBRID 의 옵티마이저는 테이블 파티션을 알 지 못한다. 가지치기는 lazy 하게, scan open 직전에partition_prune_spec에서 동작한다. partition-wise join 은 없다. -
cubrid-locator.md가 locator 의 MOP 라우팅과 클래스별 scan-cache 인프라를 기술했다. 본 문서의 DML 라우팅 —locator_insert_force가partition_prune_insert를 부르는 것,locator_move_record가 파티션 이동을,locator_get_partition_scancache가 파티션별 캐시 재사용을 — 이 locator narrative 에 깨끗하게 들어간다. 여기서 새로 짚어 둘 점은 — 가지치기가 워크스페이스 를 거치는 클라이언트 측이 아니라, 완전히 deserialize 된 regu -variable expression 으로 서버 측에서 수행된다는 것이다.locator_cl.c의 클라이언트 측 워크스페이스 코드는 마스터 클래스의 MOP 위에mop->pruning_type = DB_PARTITIONED_CLASS를 set 하기만 한다. 그 결과 flush 단계가LC_FLUSH_INSERT_PRUNE요청을 emit 하고, 서버가 그것을partition_prune_insert로 라우팅한다. -
cubrid-catalog-manager.md가 schema 가_db_class레 코드로 어떻게 flush 되는지 기술했다.SM_PARTITION데이터 는 클래스 객체에 박힌DB_SEQ리터럴로 영속된다 (전용 파티 션 카탈로그 테이블이 없다).OR_PARTITION배열은 마스터 클 래스의_db_class행과 파티션의_db_class_repr행에서 서 버 측에서 빌드된다. 파티션 술어 스트림은 마스터 파티션의values[2]슬롯에 산다. 본 문서의partition_load_partition_predicate독해와 일치한다. -
cubrid-btree.md가 B-tree 서브시스템을 기술했다. 본 문 서가 거기에 “파티션 테이블 위의 B-tree 는 파티션별 local 이 다” 라는 규칙을 더한다.partition_prune_unique_btid가 마 스터의 논리 BTID 위 검색을 올바른 파티션의 물리 BTID 위 검 색으로 옮겨 주는 다리다.
미해결 질문
섹션 제목: “미해결 질문”-
Sub-partitioning / composite 파티셔닝. CUBRID 파티션이 그 자체로 다시 파티션될 수 있는가 (Oracle 식 range-hash composite, 또는 MySQL 의 RANGE 안의 HASH)? DDL 은 파티션 클 래스 위의
CREATE TABLE을 막지만 (SM_NO_PARTITION_ON_HIERARCHIES), 본 문서는ALTER TABLE partition__p__p3 PARTITION BY HASH(...)가 어떤 코드 경로 로 도달 가능한지 추적하지 않았다. 작은 테스트 (./csql -S+ DDL) 로 확인할 만하다. -
온라인 repartition.
do_reorganize_partition_pre/post가 경계가 바뀌면 파티션을 가로질러 데이터를 재분배하는 경로 가 있음을 시사한다. 그게 온라인 (전체 테이블 lock 없이) 인 가, 오프라인인가?locator_sr.c:218의redistribute_partition_data심볼이 추적할 만한 진입점이 다 — 데이터를 복사하는 동안 reader 를 서비스하기 위해 MVCC 를 쓰는가, 아니면 exclusive lock 을 잡는가? -
파티션을 가로지르는 foreign key. 파티션 테이블이 FK 의 referencing 측이라면 FK 는 referenced 테이블의 인덱스 를 lookup 해야 한다.
btree_load.c:4559가partition_prune_partition_index를 referenced 측 FK 컨텍 스트에서 쓰는 모습이 보인다. 그렇다면 FK 가 파티션 테이블을 참조 할 때 (referenced 측이 파티션이고 인덱스는 local) 는 어떨까? 파티션을 가로지르는 unique 가정이 문서화되어 있는 가? FK 가 referenced 행을 찾으리라는 것이 보장되는가? -
파라미터화된 술어에 대한 파티션 가지치기. 본 문서는 plan time 가지치기에 집중했지만,
partition_prune_spec은 access spec 이 재오픈 될 때마다 실행기에서 호출된다. CUBRID 가 prepared statement 의 실행 사이에 파티션 키에 바인드된 host 변수가 바뀐 경우 다시 가지치기 하는가? 캐시partition_load_context_from_cache가 그렇다고 시사한다 ( context 는 execution 사이에 재사용되지만,partition_prune_spec이 돌 때마다 비트셋은 새로 빌드된다). PostgreSQL 의 execution-time pruning (이것도 파라미터 변경을 잡으려고 execution 마다 가지치기를 다시 돌린다) 와의 옆 비교가 CUBRID 가 거기에 맞춰지는지 명료하게 해 줄 것이다. -
비활성
partition_is_global_index. 이 함수는partition.c:4250에서#if 0뒤에 앉아 있다. global 인덱 스 지원이 계획된 미래 기능인가, 아니면 시제품에서 멈추고 버 려진 영구 dead code 인가? 그 블록을git log -p패스 로 주석을 달면 답이 나올 것이다. -
partition_match_pred_expr와T_NOT_TERM. 현재 어떤NOT술어든MATCH_NOT_FOUND를 돌려 준다 (가지치기 없음). 교과서적인 negation 가지치기 처리는 De Morgan 재작성 후 가 지치기로 단순하다. 현재 동작이 의도된 선택 (재작성이 CUBRID 의 술어 언어에서 안전하지 않음) 인가, 그저 미완성인가?
src/query/partition.c— 서버 측 파티션 로직. 가지치기, 행 별 결정, 파티션 기술자 캐시.src/query/partition.h—MAX_PARTITIONS = 1024.src/query/partition_sr.h—PRUNING_CONTEXT,PRUNING_SCAN_CACHE,SCANCACHE_LIST, public API.src/optimizer/query_planner.c— 테이블-파티션 인지 확인. 옵티마이저의QO_PARTITION은 join 그래프의 connected component 이지 테이블 파티션이 아니다. partition-wise join 없음.src/transaction/locator_sr.c—locator_insert_force의 파티션 라우팅,locator_move_record,locator_get_partition_scancache,locator_area_op_to_pruning_type.src/query/execute_schema.c—do_create_partition,do_alter_partitioning_*,do_remove_partition_*,do_coalesce_partition_*,do_reorganize_partition_*,do_create_partition_constraints.src/object/class_object.c/src/object/class_object.h—SM_PARTITION,SM_CLASS::partition, 라이프사이클 헬퍼.src/base/object_representation_sr.h—OR_PARTITION( 디스크 reload 형태).src/storage/storage_common.h—DB_PARTITION_TYPE,DB_CLASS_PARTITION_TYPEenum.src/query/xasl.h—access_spec_node::pruning_type,access_spec_node::parts,partition_spec_node.src/query/query_executor.c—partition_prune_spec호출 자, 파티션별 lock 과 scan 디스패치.src/storage/btree.c와src/storage/btree_load.c—partition_prune_unique_btid와partition_prune_partition_index소비자.
이론적 참고 자료.
- Silberschatz, Korth, Sudarshan. Database System Concepts (7판), 22장 (“Parallel and Distributed Storage”), §22.2 수 평 분할 전략과 §22.5 파티션 가지치기.
- Petrov. Database Internals, 11장 distributed systems 와 파티션 배치 / query planning 에서의 파티션 인지에 관한 절.
- PostgreSQL documentation — PostgreSQL manual 의 “Table
Partitioning” 챕터. PostgreSQL 소스 트리 안의 prune 알고리
즘 —
partprune.c. - MySQL documentation — MySQL Reference Manual 의
Partitioning 챕터. prune 알고리즘 —
sql/sql_partition.cc. - Oracle Database VLDB and Partitioning Guide (Oracle Database 19c 이후) — range/hash/list/composite + interval 파티셔닝.