Skip to content

[CBRD-26630] Heap Recdes contains OOS length info#6921

Merged
vimkim merged 3 commits intoCUBRID:feat/oosfrom
vimkim:oos-heap-recdes-knows-oos-length
Apr 1, 2026
Merged

[CBRD-26630] Heap Recdes contains OOS length info#6921
vimkim merged 3 commits intoCUBRID:feat/oosfrom
vimkim:oos-heap-recdes-knows-oos-length

Conversation

@vimkim
Copy link
Copy Markdown
Contributor

@vimkim vimkim commented Mar 18, 2026

http://jira.cubrid.org/browse/CBRD-26630

Purpose / 목적

OOS 변수 속성이 저장될 때, heap recdes에 OOS OID(8바이트)와 OOS 데이터 길이(8바이트, DB_BIGINT)를 함께 인라인으로 저장하도록 변경합니다. 총 16바이트(OR_OOS_INLINE_SIZE)를 사용합니다.

이를 통해 OOS 데이터 크기를 파악할 때 oos_get_length()를 통한 추가 페이지 I/O가 불필요해집니다.

  • 기존: OOS 변수 속성의 인라인 데이터에 OOS OID(8바이트)만 저장
  • 문제: OOS 데이터의 실제 크기를 알기 위해 oos_get_length() 호출 시 OOS 페이지를 읽는 추가 I/O 발생
  • 개선: OOS OID 옆에 길이 정보(8바이트)를 함께 저장하여 I/O 없이 크기 파악 가능
Heap Record (variable area)
┌─────────────────────────────────────────────────┐
│  ...  │  OOS column (inline)        │  ...      │
│       ├─────────────┬───────────────┤           │
│       │ OOS OID     │ OOS length    │           │
│       │ (8 bytes)   │ (8 bytes)     │           │
│       ├─────────────┼───────────────┤           │
│       │ volid  (2B) │               │           │
│       │ pageid (4B) │   DB_BIGINT   │           │
│       │ slotid (2B) │               │           │
│       ├─────────────┴───────┬───────┘           │
│       │  total: 16 bytes    │                   │
│       │  (OR_OOS_INLINE_SIZE)                   │
└─────────────────────────────────────────────────┘
         │                    │
         │  OOS OID points    │  length = size of
         │  to OOS pages      │  OOS data (no I/O
         ▼                    │  needed to read)
┌─────────────────┐           │
│  OOS Page(s)    │           │
│ ┌─────────────┐ │           │
│ │ chunk 1     │ │           │
│ │ (data...)   │ │           │
│ ├─────────────┤ │           │
│ │ chunk 2     │ │           │
│ │ (data...)   │ │           │
│ └─────────────┘ │           │
│  total = length ◄───────────┘
└─────────────────┘

Before (CBRD-26565): inline stored only OOS OID (8B), requiring oos_get_length() I/O to get size.
After (this PR): inline stores OOS OID + length (16B), eliminating I/O in heap_midxkey_get_oos_extra_size().

Implementation / 구현 내용

src/base/object_representation.h

  • OR_OOS_INLINE_SIZE 매크로 추가 (OR_OID_SIZE + OR_BIGINT_SIZE = 16바이트)

src/storage/heap_file.c

  • heap_attrinfo_determine_disk_layout(): OOS 컬럼 크기 계산 시 OR_OID_SIZEOR_OOS_INLINE_SIZE로 변경
  • heap_attrinfo_insert_to_oos(): OOS 삽입 후 길이 정보를 oos_lengths 벡터에 기록
  • heap_attrinfo_transform_variable_to_disk(): OOS OID 기록 후 or_put_bigint(buf, oos_length)로 길이도 기록
  • heap_attrinfo_transform_columns_to_disk(): oos_lengths 벡터 전달
  • heap_attrinfo_transform_to_disk_internal(): oos_lengths 벡터 선언 및 파이프라인 전달
  • heap_midxkey_get_oos_extra_size(): oos_get_length() I/O 호출 제거, recdes 인라인 데이터에서 or_get_bigint()로 직접 읽기

변경 불필요한 부분

  • heap_attrvalue_point_variable(): or_get_oid가 8바이트만 읽으므로 영향 없음
  • locator_fixup_oos_oids_in_recdes(): or_put_oid가 8바이트만 덮어쓰므로 길이 필드는 보존됨

unit_tests/oos/test_oos.cpp

  • OosInlineFormatWriteAndReadBack: 순수 직렬화 라운드트립 테스트
  • OosInlineFormatWithRealOosInsert: 실제 OOS 삽입 후 인라인 길이와 I/O 기반 길이 일치 검증
  • OosInlineLengthMatchesAcrossPages: 다양한 크기(512B, 경계값, 160KB)에서 인라인 길이 정확성 검증

Remarks / 비고

  • 인라인 OOS 데이터 포맷: [OOS OID (8B) | OOS length (8B)] = 16바이트
  • 레코드당 OOS 컬럼마다 8바이트 추가 사용 (기존 8B → 16B)
  • OOS length를 DB_BIGINT(8바이트)로 저장하는 이유: 현재 recdes.lengthint(4B)이지만, 향후 대용량 OOS 데이터 지원을 위해 8바이트로 설계
  • midxkey 버퍼 크기 추정 시 페이지 I/O 제거로 성능 향상

@github-actions
Copy link
Copy Markdown

🧪 TC Test Environment Ready

CircleCI Testing:

  • CircleCI will automatically test using the branches below.

TC Repositories & Branches:

Next Steps:

  1. Wait for CircleCI tests to complete
  2. If CircleCI tests failed, please check the test results and fix the issues.
  3. When ready to merge this PR, please merge the TC PR first, then merge this PR.

When a variable attribute is stored as OOS, the heap recdes now stores
both the OOS OID (8 bytes) and the OOS data length (4 bytes) inline,
totaling 12 bytes (OR_OOS_INLINE_SIZE). This eliminates the need for
an extra page I/O via oos_get_length() when determining OOS data size,
which is particularly beneficial for midxkey buffer size estimation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vimkim vimkim force-pushed the oos-heap-recdes-knows-oos-length branch from 5801830 to 4692f76 Compare March 18, 2026 11:45
@vimkim
Copy link
Copy Markdown
Contributor Author

vimkim commented Mar 18, 2026

/run sql medium

@vimkim vimkim changed the title [CBRD-xxxxx] Heap Recdes contains OOS length info [CBRD-26630] Heap Recdes contains OOS length info Mar 18, 2026
@vimkim
Copy link
Copy Markdown
Contributor Author

vimkim commented Mar 19, 2026

/run sql medium

@vimkim
Copy link
Copy Markdown
Contributor Author

vimkim commented Mar 27, 2026

Claude Code Opus 4.6

PR Review: [CBRD-26630] Heap Recdes contains OOS length info

요약

OOS 변수 속성의 인라인 데이터에 OOS OID(8B)만 저장하던 것을 OOS OID + OOS length (총 16바이트)로 확장하여, heap_midxkey_get_oos_extra_size()에서 oos_get_length() I/O 호출을 제거하는 성능 최적화 PR이다.

코드 변경 자체는 깔끔하고, LSP 진단 결과 경고/에러가 없으며, 단위 테스트 3건이 잘 구성되어 있다. 다만 PR 설명문과 실제 코드 사이의 불일치, 그리고 DB_BIGINT(8바이트) 사용의 적절성에 대해 논의가 필요하다.


Findings

[MAJOR] PR 설명문과 코드 간 불일치

위치: PR description (Remarks 섹션, Implementation 섹션)

문제: PR 설명문 내부에 상호 모순되는 정보가 있다.

위치 기술 내용
PR 설명 ASCII 다이어그램 OOS length (8 bytes), total 16 bytes
PR 설명 Remarks 섹션 [OOS OID (8B) | OOS length (4B)] = 12바이트
PR 설명 Implementation 섹션 OR_OID_SIZE + OR_INT_SIZE = 12바이트
실제 코드 (object_representation.h:449) OR_OID_SIZE + OR_BIGINT_SIZE = 16바이트

커밋 d1ac1eb4a ("typo(*): oos size 4 byte -> 8 byte")에서 4바이트에서 8바이트로 변경되었으나, PR 설명의 하단 섹션들이 갱신되지 않았다.

권장 조치: PR 설명의 Remarks와 Implementation 섹션을 실제 코드에 맞게 8바이트/16바이트로 수정.


[MAJOR] DB_BIGINT(8바이트) 사용의 적절성 검토

파일: src/base/object_representation.h:449, src/storage/heap_file.c 전반

문제: OOS length 저장에 DB_BIGINT(8바이트)를 사용하고 있으나, 소스인 recdes.lengthint(4바이트)이다.

// heap_file.c:12384 — 소스가 int인 recdes.length
(*oos_lengths)[i] = (DB_BIGINT) recdes.length;

// heap_file.c:10904-10907 — 읽은 후 다시 int로 truncation
DB_BIGINT length = or_get_bigint (&buf, &rc);
return (int) length;
  • recdes.lengthint이므로 OOS 데이터 크기는 INT_MAX 이내로 제한됨
  • DB_BIGINT를 사용하면 OOS 컬럼당 레코드마다 4바이트 추가 낭비 (인라인 8B → 16B, vs OR_INT_SIZE 사용 시 8B → 12B)
  • 원래 JIRA 스펙(CBRD-26630)에서도 OR_INT_SIZE(4바이트)로 설계됨

질문: DB_BIGINT로 변경한 이유가 있는지? (미래 확장성? 정렬(alignment) 이유?) 명확한 이유가 없다면 OR_INT_SIZE(4바이트)로 복귀하여 레코드당 공간 절약을 권장.


[MINOR] or_put_bigint 반환값 미확인

파일: src/storage/heap_file.c:12703

or_put_oid (buf, oos_oid);
or_put_bigint (buf, oos_length);  // 반환값 미확인

문제: or_put_bigintint 에러 코드를 반환하지만, 반환값을 확인하지 않는다. 바로 위(line 12696)에서 버퍼 오버플로우를 이미 체크하고 있고, or_put_bigint 구현상 항상 NO_ERROR를 반환하므로 실질적 위험은 없으나, CUBRID의 에러 처리 관례상 반환값 확인이 바람직하다.

참고로 or_put_oid도 동일하게 반환값을 확인하지 않고 있어 기존 코드 관례와는 일치한다.


[MINOR] heap_midxkey_get_oos_extra_size에서 DB_BIGINT → int 축소 변환

파일: src/storage/heap_file.c:10904-10907

DB_BIGINT length = or_get_bigint (&buf, &rc);
assert (rc == NO_ERROR);
return (int) length;

문제:

  1. DB_BIGINTint 축소 변환이 명시적 캐스트 없이 return (int) length로 수행됨. 현재는 소스가 int이므로 안전하지만, 향후 실제로 큰 값이 들어올 경우 silent truncation 발생.
  2. assert(rc == NO_ERROR)는 릴리스 빌드에서 제거되므로, 에러 시 검증되지 않은 값을 반환. 다만 or_get_bigint 구현이 항상 NO_ERROR만 설정하므로 실질적 문제는 없음.

긍정적 관찰

  • 성능 개선이 명확함: heap_midxkey_get_oos_extra_size()에서 OOS 컬럼마다 발생하던 oos_get_length() 페이지 I/O를 완전히 제거. 여러 OOS 컬럼이 있는 경우 효과가 누적됨.
  • 단위 테스트가 잘 구성됨: 직렬화 라운드트립, 실제 OOS 삽입 검증, 다양한 크기 경계값 테스트 3건이 충실함.
  • 기존 코드 경로 안전성 확인됨:
    • locator_fixup_oos_oids_in_recdes()or_put_oid()가 8바이트만 덮어쓰므로 뒤의 length 필드 보존 확인 (locator_sr.c:14140)
    • heap_attrvalue_point_variable()or_get_oid()가 8바이트만 읽으므로 인라인 포맷 변경에 영향 없음 확인 (heap_file.c:10602)
  • LSP 진단 결과 깨끗함: heap_file.c, object_representation.h, test_oos.cpp 모두 경고/에러 없음.
  • oos_lengths 벡터 적절히 초기화됨: std::vector<DB_BIGINT> oos_lengths(attr_info->num_values, 0) (line 12880)
  • 변환 파이프라인 전반에 걸쳐 일관성 있게 전달됨: transform_to_disk_internalinsert_to_oostransform_columns_to_disktransform_variable_to_disk

저자에게 질문

  1. DB_BIGINT(8바이트) 선택 이유는?recdes.lengthint(4바이트)인데, 커밋 d1ac1eb4a에서 OR_INT_SIZEOR_BIGINT_SIZE로 변경한 의도가 무엇인가? 미래 확장성? 메모리 정렬(alignment) 최적화? OOS 데이터가 2GB를 초과하는 시나리오 고려?
  2. PR 설명문 정리 계획은? — 다이어그램(16B)과 Remarks/Implementation 섹션(12B) 간 불일치가 있는데, merge 전에 정리할 예정인가?
  3. UPDATE 경로에서의 inline length 갱신 — UPDATE 시 heap_attrinfo_transform_to_disk_internal()이 다시 호출되어 inline length가 새로 기록되는 것으로 보이는데, 이 이해가 맞는지 확인.

@vimkim
Copy link
Copy Markdown
Contributor Author

vimkim commented Mar 27, 2026

저자에게 질문

Q. DB_BIGINT(8바이트) 선택 이유는? — recdes.length가 int(4바이트)인데, 커밋 d1ac1eb4a에서 OR_INT_SIZE → OR_BIGINT_SIZE로 변경한 의도가 무엇인가? 미래 확장성? 메모리 정렬(alignment) 최적화? OOS 데이터가 2GB를 초과하는 시나리오 고려?

A. 미래 확장성입니다. recdes.length = int 이기 때문에 현재 OOS는 최대 4G 밖에 담을 수 없다는 문제가 있습니다. 추후 OOS span 도입으로 이를 8바이트로 확장하고자 하는 TODO plan이 있습니다.

Q. PR 설명문 정리 계획은? — 다이어그램(16B)과 Remarks/Implementation 섹션(12B) 간 불일치가 있는데, merge 전에 정리할 예정인가?

A. 정리 완료

Q. UPDATE 경로에서의 inline length 갱신 — UPDATE 시 heap_attrinfo_transform_to_disk_internal()이 다시 호출되어 inline length가 새로 기록되는 것으로 보이는데, 이 이해가 맞는지 확인.

UPDATE 시: 만약 OOS 컬럼의 데이터 크기가 바뀌었다면 (예: 2KB → 100KB), 인라인의 length도 새 값으로 갱신되어야 합니다. UPDATE도 동일하게 heap_attrinfo_transform_to_disk_internal()을 통해 레코드 전체를 재직렬화하므로, OOS 삽입 → 새 length 기록이 다시 일어납니다. 즉, "UPDATE 때 옛날 length가 남아있는 버그는 없다"는 걸 확인한 것이고, 결론은 문제 없음입니다.

@vimkim vimkim requested review from a team, H2SU, InChiJun, YeunjunLee, hgryoo, hornetmj, hyahong and youngjun9072 and removed request for a team March 27, 2026 06:27
@vimkim vimkim marked this pull request as ready for review March 27, 2026 06:27
@vimkim vimkim requested a review from beyondykk9 as a code owner March 27, 2026 06:27
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 27, 2026

Important Files Changed

Filename Overview
src/base/object_representation.h OR_OOS_INLINE_SIZE 매크로 추가 (OR_OID_SIZE + OR_BIGINT_SIZE = 16바이트). 변경 자체는 명확하고 올바름.
src/storage/heap_file.c OOS 인라인 포맷을 8바이트(OID만)에서 16바이트(OID+length)로 확장. heap_midxkey_get_oos_extra_size에서 버퍼 경계 사전 검사 및 rc 오류 처리가 assert에만 의존하는 안정성 문제와, DB_BIGINT → int 무방비 캐스팅 문제 존재.
unit_tests/oos/test_oos.cpp 직렬화 라운드트립, 실제 OOS 삽입 후 인라인 길이 일치, 멀티페이지 크기 검증 등 3개 테스트 케이스 추가. 단, 스택 char 배열에 대한 INT_ALIGNMENT 정렬 미보장으로 디버그 빌드에서 ASSERT_ALIGN 실패 가능.

Reviews (1): Last reviewed commit: "test(oos): check inserted length equals ..." | Re-trigger Greptile

Comment thread src/storage/heap_file.c
Comment on lines +10896 to +10907
int rc = NO_ERROR;

buf.ptr = (char *) recdes->data + OR_VAR_OFFSET (recdes->data, att->location);
buf.endptr = recdes->data + recdes->length;
or_get_oid (&buf, &oos_oid);
assert (!OID_ISNULL (&oos_oid));

/* Query OOS length without reading/allocating the full data */
THREAD_ENTRY *thread_p = thread_get_thread_entry_info ();
int length = oos_get_length (thread_p, oos_oid);
/* Read OOS length directly from recdes inline data (no I/O needed) */
DB_BIGINT length = or_get_bigint (&buf, &rc);
assert (rc == NO_ERROR);

return length;
return (int) length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 인라인 데이터 경계 검사 및 rc 오류 처리 누락

heap_midxkey_get_oos_extra_size에서 두 가지 심각한 방어 로직이 빠져 있습니다.

1) 버퍼 경계 사전 검사 없음

or_get_oidor_get_bigint를 호출하기 전에 레코드에 OR_OOS_INLINE_SIZE(16바이트)만큼의 공간이 실제로 있는지 확인하지 않습니다. 기존 포맷(8바이트만 저장된 레코드)으로 기록된 레코드가 이 경로를 지나게 되면 endptr 너머의 메모리를 읽을 수 있습니다.

/* 경계 검사를 추가해야 합니다 */
if ((buf.endptr - buf.ptr) < OR_OOS_INLINE_SIZE)
  {
    assert_release (false);   /* 레코드 포맷 불일치: 디버그 빌드에서 조기 발견 */
    return 0;
  }

2) rc 오류 처리가 assert에만 의존

DB_BIGINT length = or_get_bigint (&buf, &rc);
assert (rc == NO_ERROR);   // 릴리즈 빌드에서는 무시됨
return (int) length;

assert는 릴리즈 빌드에서 제거됩니다. 만약 or_get_bigint가 실패하면 length는 초기화 값 0으로 남아 0을 반환하게 되고, heap_midxkey_* 계층에서 midxkey 버퍼 크기가 과소 추정되어 힙 버퍼 오버플로우를 유발할 수 있습니다.

3) DB_BIGINT → int 오버플로우 검사 없음

DB_BIGINTint로 묵시적 캐스팅할 때 INT_MAX를 초과하면 값이 잘립니다. PR 설명에서 대용량 OOS 데이터 지원을 목적으로 8바이트를 사용한다고 명시했으므로, 최소한 assert(length <= INT_MAX)는 있어야 합니다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 확장을 염두에 둔 것이더라도 인터페이스는 8-bytes 기준으로 제공되어야 할 것 같습니다.

Comment on lines +538 to +541
char buf_data[OR_OOS_INLINE_SIZE];
OR_BUF write_buf;
or_init (&write_buf, buf_data, OR_OOS_INLINE_SIZE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 스택 버퍼 정렬 미보장으로 디버그 빌드 테스트 실패 가능

or_put_oid, or_get_oid, or_put_bigint, or_get_bigint 모두 함수 진입 시 ASSERT_ALIGN(buf->ptr, INT_ALIGNMENT)을 수행합니다. char 배열은 C++ 표준상 1바이트 정렬만 보장되므로, 디버그 빌드에서 assert 실패가 발생할 수 있습니다.

동일한 문제가 다음 위치에도 존재합니다:

  • unit_tests/oos/test_oos.cpp:591OosInlineFormatWithRealOosInsertchar inline_buf[OR_OOS_INLINE_SIZE]
  • unit_tests/oos/test_oos.cpp:653OosInlineLengthMatchesAcrossPageschar inline_buf[OR_OOS_INLINE_SIZE]

세 곳 모두 alignas를 사용하여 정렬을 명시적으로 보장해야 합니다:

Suggested change
char buf_data[OR_OOS_INLINE_SIZE];
OR_BUF write_buf;
or_init (&write_buf, buf_data, OR_OOS_INLINE_SIZE);
alignas(INT_ALIGNMENT) char buf_data[OR_OOS_INLINE_SIZE];

(INT_ALIGNMENTobject_representation.h에 이미 정의되어 있으며, 실제 데이터 접근은 memcpy 기반이라 런타임 크래시는 없지만, CUBRID의 디버그 빌드 CI/테스트에서는 ASSERT_ALIGN 실패가 발생할 수 있습니다.)

Comment thread src/storage/heap_file.c

thread_p->oos_oids.push_back (oos_oid); /* for replication log */
(*oos_oids)[i] = oos_oid;
(*oos_lengths)[i] = (DB_BIGINT) recdes.length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 recdes.length vs. oos_get_length() 의미 명확화 필요

(*oos_lengths)[i] = (DB_BIGINT) recdes.length;

recdes.length는 OOS 삽입 직렬화된 데이터 길이입니다. 테스트 OosInlineFormatWithRealOosInsert에서 oos_get_length()의 반환값과 일치하는지 검증하고 있어 현재는 올바르게 동작하지만, 추후 OOS 저장 포맷이 변경될 경우(예: 청크 헤더 오버헤드가 추가되거나 압축이 도입되는 경우) 이 값이 실제 저장 크기와 달라질 수 있습니다.

oos_insert 호출 후 반환값을 통해 실제 저장된 크기를 확인할 수 있다면 그 값을 사용하는 것이 더 안전합니다. 아니라면, 최소한 이 가정이 성립하는 이유를 주석으로 명시해두기를 권장합니다.

Comment thread src/storage/heap_file.c
Comment on lines +10896 to +10907
int rc = NO_ERROR;

buf.ptr = (char *) recdes->data + OR_VAR_OFFSET (recdes->data, att->location);
buf.endptr = recdes->data + recdes->length;
or_get_oid (&buf, &oos_oid);
assert (!OID_ISNULL (&oos_oid));

/* Query OOS length without reading/allocating the full data */
THREAD_ENTRY *thread_p = thread_get_thread_entry_info ();
int length = oos_get_length (thread_p, oos_oid);
/* Read OOS length directly from recdes inline data (no I/O needed) */
DB_BIGINT length = or_get_bigint (&buf, &rc);
assert (rc == NO_ERROR);

return length;
return (int) length;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 확장을 염두에 둔 것이더라도 인터페이스는 8-bytes 기준으로 제공되어야 할 것 같습니다.

Comment thread src/storage/heap_file.c
@vimkim vimkim merged commit 71eae56 into CUBRID:feat/oos Apr 1, 2026
10 checks passed
@vimkim vimkim deleted the oos-heap-recdes-knows-oos-length branch April 1, 2026 06:50
H2SU pushed a commit to H2SU/cubrid that referenced this pull request Apr 13, 2026
http://jira.cubrid.org/browse/CBRD-26630

* Store OOS length inline in heap recdes alongside OOS OID

When a variable attribute is stored as OOS, the heap recdes now stores
both the OOS OID (8 bytes) and the OOS data length (4 bytes) inline,
totaling 12 bytes (OR_OOS_INLINE_SIZE). This eliminates the need for
an extra page I/O via oos_get_length() when determining OOS data size,
which is particularly beneficial for midxkey buffer size estimation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants