콘텐츠로 이동

(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 또는 시스템이 정한다. 모든 튜플은 정확히 하나의 파티션으로 매핑된다. 교과서 가 세 가지 정본 전략을 나열한다.

  1. Range 파티셔닝. P(t) = i 이고 t.k ∈ [low_i, high_i). 파티션 키 컬럼이 전순서를 가지며, 파티션은 그 순서 위의 겹 치지 않는 구간을 덮는다. 시계열 데이터처럼 query 가 최근 slice 만 두드리는 패턴에 좋다.
  2. Hash 파티셔닝. P(t) = h(t.k) mod N. 파티션 키를 해시 하고, modulus 로 파티션을 고른다. 파티션 분포를 균등하게 만들고 싶거나 활용할 수 있는 자연스러운 순서가 없을 때 (혹 은 자연 순서가 skew 되어 있을 때) 적합하다.
  3. 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. T1T2 가 같은 키와 같은 파티 셔닝 함수로 분할되어 있다면, T1 ⨝ T2 ON T1.k = T2.kT1.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 으로 라우팅하는지.

파티셔닝을 지원하는 모든 관계형 엔진은 같은 결정 다발을 약간씩 다른 모양으로 처리한다. 변형들을 훑고 나면 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 의 파티셔닝은 정신적으로 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 는 사용자가 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.h
struct 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, 파티션 술어 sequencevalues. 파티션 술어는 직렬화된 FUNC_PRED (regu-variable expression) 다. 행을 평가하면 파티션 할당에 쓰이는 값이 나온다. 자식 클래스의 경우 partition->class_partition_typeDB_PARTITION_CLASS 이고 values 는 그 파티션의 데이터를 담는다 — range 라면 [min, max), list 라면 명시적인 값 집합, hash 는 사용하지 않는다.

DB_PARTITION_TYPEDB_CLASS_PARTITION_TYPE enum 은 storage_common.h 에 있다.

// DB_PARTITION_TYPE — storage_common.h
typedef 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.hOR_PARTITION 이다.

// or_partition — object_representation_sr.h
typedef 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_oidheap_get_class_supers 를 부르고, 단 하나뿐인 super 를 파티션된 클래스의 root 로 본다. 둘째, 파티션 술어는 컬럼 id + 연산자가 아니라 직렬화된 regu-variable expression 으로 저장된다. 더 유연하다 — PARTITION BY HASH (YEAR(birthdate)) 같은 형태가 가능하고, 술어가 함수 호출이 된다. 대신 가지치기는 stx_map_stream_to_func_predfetch_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+) 을 부르고, 이 함수가 다음 일을 한다.

  1. 마스터 클래스를 au_fetch_class 로 가져 온다. attribute 리스트와 OID 가 손에 들어 온다.
  2. 마스터 클래스가 상속 hierarchy 의 일부라면 파티셔닝 DDL 을 거부한다 — if (smclass->users != NULL || smclass->inheritance != NULL) { … ER_SM_NO_PARTITION_ON_HIERARCHIES }. 이 제약 이 파티션 테이블을 subclass 로 두지 못하게, 그리고 subclass 를 파티션 테이블로 만들지 못하게 막는다.
  3. HASH 의 경우 파티션 이름 <class>__p__p<i> (i ∈ [0, hashsize)) 을 자동 생성하고, 각각을 dbt_create_classdo_create_local 을 부른다. 파티션 클래스가 실체화된다. PARTITIONED_SUB_CLASS_TAG 매크로가 이름 가운데의 구분자 __p__ 다. 나중 코드가 파티션 이름을 grep 으로 잡을 수 있게 한다.
  4. RANGE/LIST 의 경우 파스 트리의 명시적 parts 리스트를 따라 가며 range 경계가 monotonic 하고 disjoint 한지 (또는 list 값이 파티션 사이에서 pairwise disjoint 한지) 검증한 다음, 각 파티션 엔트리당 클래스 하나씩을 만든다.
  5. 만들어진 각 파티션 클래스가 단일 상속으로 마스터에 연결된다 (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_constraintsdo_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 를 만든다. 구조체는 partition_sr.h 에 있다.

// pruning_context — partition_sr.h
struct 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 캐시다.

라이프사이클은 명료하다.

  1. partition_init_pruning_context 가 구조체를 0 으로 초기화한다.
  2. partition_load_pruning_context 가 채운다. 먼저 서버별 hash 캐시 (db_Partition_Ht, root 클래스 OID 키, 크기는 PARTITION_CACHE_SIZE = 200) 에서 로드를 시도한다. miss 면 heap_get_class_partitions 로 디스크에서 OR_PARTITION 배 열을 읽고, partition_load_partition_predicatepartitions[0].values[2] (직렬화된 FUNC_PRED 스트림) 에 서 regu 트리를 deserialize 한 다음, 결과를 캐시에 넣는다.
  3. 호출자가 가지치기를 한다 (spec 단위, insert 단위, update 단 위, 인덱스 lookup 단위).
  4. 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:3313partition_prune_spec 이다. query_executor.c:8656 (일반 scan 준비) 과 query_executor.c:13361 (parallel-query 클래스 lookup) 에서 호출되며, parallel 의 px_heap_scan task 에서도 부른다. 계약은 다음과 같다 — pruning_type == DB_PARTITIONED_CLASSaccess_spec_node 가 주어지면, spec->where_pred, spec->where_key, spec->where_range 를 파티션 기술자를 걸어 다니며, 살아남은 파티션의 PARTITION_SPEC_TYPE 연결 리스 트를 spec->parts 에 만든다.

// partition_prune_spec — partition.c
int
partition_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.hMAX_PARTITIONS = 1024 매크로와 맞춘다). 연산은 평범하다 — pruningset_init, _set_all, _add, _remove, _intersect, _union, _is_set, _popcount, 그리 고 iterator. 인코딩은 파티션당 1 비트. 파티션 i 가 비트 i 에 대응하지만, OR_PARTITION 배열에서는 인덱스 0 이 마스터로 예약되어 있어서 비트 iOR_PARTITION[i+1] 을 가리킨다.

partition_match_pred_expr 가 술어 트리를 걸어 다닌다.

  • T_PRED + B_AND: 두 자식을 재귀, 결과 비트셋을 교집합. 한쪽이 MATCH_NOT_FOUND 면 다른 쪽 비트셋만 채택한다 (A AND ? 는 최대 A 다 — A 가 제외하는 건 다 제외되고, ? 가 추가로 제외할 수 있는 건 그저 추가 보너스다).
  • T_PRED + B_OR: 두 자식을 재귀, 결과 비트셋을 합집합. 어느 한쪽이라도 MATCH_NOT_FOUNDMATCH_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.c
static MATCH_STATUS
partition_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;
}
}

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 으로 고정된다.

partition_prune_range 가 가장 복잡하다 — 구간 겹침을 추론해 야 하기 때문이다. 각 파티션 ipartitions[i+1].values[0..1] 에서 minmax 를 읽는다 (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_valuemax 에 적용해 half-open 구간을 fully-closed 비교로 바꾼다. 트릭은 이렇다 — >max 와 같은 값에 대해서는 [min, max) 파티션과 매치하면 안 되지만, 경계 비교는 val < max 를 원한 다. maxmax - 1 로 깎아 두면 (정수형이나 날짜형에서만 가 능) 비교가 그대로 옳아진다. decrement 가 실패하는 타입에서는 함수가 false 를 돌려 default 규칙으로 빠진다. 컬렉션 타입 값 (val IN (1, 2, 3) 등) 에 대해서는, 함수가 원소별로 재귀 가지 치기 한 다음 결과를 합집합 한다 (= 의 경우는 컬렉션과의 동 등이 엄밀히 불가능하므로 교집합 이지만, 기존 코드 경로가 방어 적으로 다룬다).

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:3465partition_find_partition_for_record 가 한다. 이 함수를 partition_prune_insertpartition_prune_update 가 감싸서 INSERT 측과 UPDATE 측 의미를 제공한다.

// partition_find_partition_for_record — partition.c (excerpted)
static int
partition_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);
}

흥미로운 순간 셋.

  1. 마스터 REPR_ID 로 읽고, 파티션 REPR_ID 로 쓴다. 레코드 는 호출자가 어떤 REPR_ID 로 만들었든 그대로 도착한다. 파티 션 컬럼을 꺼내려면, 이 함수가 마스터 클래스의 REPR_ID 로 일 시적으로 바꾼다. 그래야 heap-attribute 기계가 레이아웃을 디 코드한다. 평가가 끝나면 다시 돌려놓는다. 마지막에 반환 직전, 목적 파티션의 REPR_ID 가 다르면 in-place 로 REPR_ID 비트를 덮어 쓴다. 소스 주석이 왜 이 지름길이 안전한지 설명한다 — 파티션은 attribute 레이아웃을 마스터에서 상속받기 때문에, 디 스크 이미지가 REPR_ID 비트만 제외하면 동일하다.
  2. NULL 처리 — PO_IS_NULL op. 파티션 expression 이 NULL 을 만들면 PO_IS_NULL 로 가지치기 된다. 이는 hash 에서는 파 티션 0 으로, range 에서는 min == NULL (MINVALUE 파티션) 인 파티션으로, list 에서는 명시적으로 NULL 을 받는 파티션으 로 라우팅된다. NULL 을 받는 파티션이 없으면 ER_PARTITION_NOT_EXIST 를 돌려 준다.
  3. 정확히 하나 단언. 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_specspec->parts 를 채우고 나면, 실행기 가 파티션을 하나씩 차례로 돈다. 관련된 access_spec_node 필드 는 xasl.h 에 있다.

// access_spec_node — xasl.h
struct 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.h
struct 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.cscan_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_subclassIS_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_PRUNEDB_PARTITIONED_CLASS, LC_FLUSH_INSERT_PRUNE_VERIFYDB_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:4250partition_is_global_index 라는 feature flag 가 있긴 하지만 #if 0 으로 감싸져 있다 (비활성). 이 함수 는 primary-key 인덱스 (global 로 취급할 후보) 와 비-primary-key unique 인덱스 (인덱스 컬럼이 파티션 키를 포함하는지에 따라 globality 가 결정됨) 를 구별한다. 오늘은 죽은 코드다. 현재 프로 덕션 동작은 모든 인덱스는 local 이다.

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_infoclass_object.c 의 SM_PARTITION 라이프사이클 헬퍼.

DDL (클라이언트 측, execute_schema.c)

섹션 제목: “DDL (클라이언트 측, execute_schema.c)”
  • do_create_partitionCREATE 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 가 주어졌을 때, 그 키를 소유한 파티션과 파티션 local BTID 를 돌려 준다. 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_pruneREGU_VARIABLEpartition_get_value_from_regu_varDB_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_hashmht_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_opREL_OPPRUNING_OP enum 매핑 (R_LTPO_LT, R_EQPO_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 — 소멸자.
  • 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 (in query_executor.c) — scan open 전에 partition_prune_spec 과 파티션별 lock_subclass 를 부른다.
  • qexec_clear_partition_expression ( partition_free_partition_predicate 에서 참조) — deserialize 된 파티션 expression 의 regu 트리 해제.
  • 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_typeLC_FLUSH_*DB_*_PARTITIONED_CLASS enum 매핑.
  • FOR_INSERT_OR_DELETE / FOR_MOVE — delete-and-reinsert 쌍을 파티션 이동으로 태깅하는 내부 enum (그래야 MVCC 가 새 행을 (일부 bookkeeping 목적상) 신규 insert 가 아니라 연속으 로 다룬다).
  • redistribute_partition_dataREORGANIZE PARTITION 이 쓰는 대량 경로.
  • 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 이 모든 파티션을 걸어 다닌다.
  • partition_load_aggregate_helper — 파티션 테이블 위의 aggregate 가 있는 query 의 경우, 파티션별 (HFID, BTID) 배 열을 담은 HIERARCHY_AGGREGATE_HELPER 를 만들어 aggregate 실행기가 파티션을 차례로 돌 수 있게 한다.
  • HIERARCHY_AGGREGATE_HELPER (in query_aggregate.hpp) — 받쳐 주는 구조체.

다음 표는 이 문서의 updated: 시점에 관찰된 심볼 위치를 기록 한다. 심볼 이름이 정본 anchor 이고, 라인 번호는 refactor 와 함 께 시간에 따라 늙는다.

심볼파일라인
MAX_PARTITIONS = 1024src/query/partition.h26
pruning_scan_cachesrc/query/partition_sr.h50
scancache_listsrc/query/partition_sr.h60
pruning_contextsrc/query/partition_sr.h67
partition_init_pruning_context (decl)src/query/partition_sr.h95
partition_load_pruning_context (decl)src/query/partition_sr.h97
partition_prune_spec (decl)src/query/partition_sr.h112
partition_prune_insert (decl)src/query/partition_sr.h114
partition_prune_update (decl)src/query/partition_sr.h118
partition_prune_unique_btid (decl)src/query/partition_sr.h122
partition_get_partition_oids (decl)src/query/partition_sr.h125
partition_load_aggregate_helper (decl)src/query/partition_sr.h128
partition_find_root_class_oid (decl)src/query/partition_sr.h135
partition_prune_partition_index (decl)src/query/partition_sr.h137
pruning_bitsetsrc/query/partition.c71
partition_cache_entrysrc/query/partition.c95
partition_init_pruning_contextsrc/query/partition.c2589
partition_load_pruning_contextsrc/query/partition.c2674
partition_clear_pruning_contextsrc/query/partition.c2805
partition_load_partition_predicatesrc/query/partition.c2862
partition_set_specified_partitionsrc/query/partition.c2929
partition_get_attribute_idsrc/query/partition.c3052
partition_get_position_in_keysrc/query/partition.c3125
partition_prune_specsrc/query/partition.c3313
partition_find_partition_for_recordsrc/query/partition.c3465
partition_prune_insertsrc/query/partition.c3603
partition_prune_updatesrc/query/partition.c3710
partition_get_scancachesrc/query/partition.c3831
partition_new_scancachesrc/query/partition.c3861
partition_get_partition_oidssrc/query/partition.c3891
partition_decrement_valuesrc/query/partition.c3976
partition_prune_unique_btidsrc/query/partition.c4064
partition_find_inherited_btidsrc/query/partition.c4106
partition_load_aggregate_helpersrc/query/partition.c4150
partition_attrinfo_get_keysrc/query/partition.c4337
partition_prune_partition_indexsrc/query/partition.c4441
partition_prune_listsrc/query/partition.c1086
partition_prune_hashsrc/query/partition.c1221
partition_prune_rangesrc/query/partition.c1340
partition_prune_db_valsrc/query/partition.c1545
partition_prunesrc/query/partition.c1574
partition_match_pred_exprsrc/query/partition.c1933
partition_prune_for_functionsrc/query/partition.c2105
partition_match_key_rangesrc/query/partition.c2406
pruningset_to_spec_listsrc/query/partition.c873
pruningset_init, etc.src/query/partition.c187-396
sm_partitionsrc/object/class_object.h728
SM_CLASS::partitionsrc/object/class_object.h798
or_partitionsrc/base/object_representation_sr.h196
DB_PARTITION_TYPEsrc/storage/storage_common.h1211
DB_CLASS_PARTITION_TYPEsrc/storage/storage_common.h1218
partition_spec_nodesrc/query/xasl.h1029
access_spec_node::pruning_typesrc/query/xasl.h1052
access_spec_node::partssrc/query/xasl.h1057
access_spec_node::prunedsrc/query/xasl.h1061
do_create_partitionsrc/query/execute_schema.c4036
do_alter_partitioning_pre (decl)src/query/execute_schema.c334
do_alter_partitioning_post (decl)src/query/execute_schema.c335
do_remove_partition_pre (decl)src/query/execute_schema.c337
do_coalesce_partition_pre (decl)src/query/execute_schema.c339
do_reorganize_partition_pre (decl)src/query/execute_schema.c341
do_create_partition_constraintssrc/query/execute_schema.c5728
SM_PARTITION_ALTER_INFOsrc/query/execute_schema.c195
partition_prune_spec 호출자src/query/query_executor.c8656
partition_prune_spec parallel-path 호출자src/query/query_executor.c13361
locator_insert_force 파티션 분기src/transaction/locator_sr.c4969
locator_move_recordsrc/transaction/locator_sr.c5295
locator_update_force::partition_prune_updatesrc/transaction/locator_sr.c5915
locator_get_partition_scancachesrc/transaction/locator_sr.c12314
locator_area_op_to_pruning_typesrc/transaction/locator_sr.c12372
partition_prune_unique_btid (in btree.c)src/storage/btree.c6080, 22719
partition_prune_partition_index (in btree_load.c)src/storage/btree_load.c4559
partition_get_partition_oids (statistics)src/storage/statistics_sr.c198

이 문서는 cubrid-class-object.md, cubrid-query-optimizer.md, cubrid-locator.md, cubrid-catalog-manager.md 이후에 쓰여졌 다. 접점과 불일치 지점.

  • cubrid-class-object.mdSM_PARTITIONSM_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_forcepartition_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 위 검 색으로 옮겨 주는 다리다.

  1. 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) 로 확인할 만하다.

  2. 온라인 repartition. do_reorganize_partition_pre/post 가 경계가 바뀌면 파티션을 가로질러 데이터를 재분배하는 경로 가 있음을 시사한다. 그게 온라인 (전체 테이블 lock 없이) 인 가, 오프라인인가? locator_sr.c:218redistribute_partition_data 심볼이 추적할 만한 진입점이 다 — 데이터를 복사하는 동안 reader 를 서비스하기 위해 MVCC 를 쓰는가, 아니면 exclusive lock 을 잡는가?

  3. 파티션을 가로지르는 foreign key. 파티션 테이블이 FK 의 referencing 측이라면 FK 는 referenced 테이블의 인덱스 를 lookup 해야 한다. btree_load.c:4559partition_prune_partition_index 를 referenced 측 FK 컨텍 스트에서 쓰는 모습이 보인다. 그렇다면 FK 가 파티션 테이블을 참조 할 때 (referenced 측이 파티션이고 인덱스는 local) 는 어떨까? 파티션을 가로지르는 unique 가정이 문서화되어 있는 가? FK 가 referenced 행을 찾으리라는 것이 보장되는가?

  4. 파라미터화된 술어에 대한 파티션 가지치기. 본 문서는 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 가 거기에 맞춰지는지 명료하게 해 줄 것이다.

  5. 비활성 partition_is_global_index. 이 함수는 partition.c:4250 에서 #if 0 뒤에 앉아 있다. global 인덱 스 지원이 계획된 미래 기능인가, 아니면 시제품에서 멈추고 버 려진 영구 dead code 인가? 그 블록을 git log -p 패스 로 주석을 달면 답이 나올 것이다.

  6. partition_match_pred_exprT_NOT_TERM. 현재 어떤 NOT 술어든 MATCH_NOT_FOUND 를 돌려 준다 (가지치기 없음). 교과서적인 negation 가지치기 처리는 De Morgan 재작성 후 가 지치기로 단순하다. 현재 동작이 의도된 선택 (재작성이 CUBRID 의 술어 언어에서 안전하지 않음) 인가, 그저 미완성인가?

  • src/query/partition.c — 서버 측 파티션 로직. 가지치기, 행 별 결정, 파티션 기술자 캐시.
  • src/query/partition.hMAX_PARTITIONS = 1024.
  • src/query/partition_sr.hPRUNING_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.clocator_insert_force 의 파티션 라우팅, locator_move_record, locator_get_partition_scancache, locator_area_op_to_pruning_type.
  • src/query/execute_schema.cdo_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.hSM_PARTITION, SM_CLASS::partition, 라이프사이클 헬퍼.
  • src/base/object_representation_sr.hOR_PARTITION ( 디스크 reload 형태).
  • src/storage/storage_common.hDB_PARTITION_TYPE, DB_CLASS_PARTITION_TYPE enum.
  • src/query/xasl.haccess_spec_node::pruning_type, access_spec_node::parts, partition_spec_node.
  • src/query/query_executor.cpartition_prune_spec 호출 자, 파티션별 lock 과 scan 디스패치.
  • src/storage/btree.csrc/storage/btree_load.cpartition_prune_unique_btidpartition_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 파티셔닝.