Skip to content

[CBRD-27251] Normalize histogram string keys by the column type and re-pad CHAR values for LIKE/REGEXP estimates - #7977

Open
soheejung-cs wants to merge 6 commits into
CUBRID:developfrom
soheejung-cs:CBRD-27251
Open

soheejung-cs wants to merge 6 commits into
CUBRID:developfrom
soheejung-cs:CBRD-27251

Conversation

@soheejung-cs

@soheejung-cs soheejung-cs commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

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

Purpose

문자열 컬럼의 히스토그램에서 값을 적어 넣는 쪽과 읽어 맞춰보는 쪽이, CHAR 의 자리 채움 공백을 떼어내는 기준을 서로 다르게 잡고 있었습니다.

  • 적어 넣는 쪽 — 서버측 통계 수집기 (histogram_sampler_sr.cpp, update statistics 가 서버에서 돌리는 샘플러). 힙에 저장된 값을 그대로 읽으므로 판단 기준이 컬럼 타입입니다. 컬럼이 CHAR 이면 채움 공백을 떼고 저장합니다.
  • 읽어 맞춰보는 쪽 — 클라이언트측 질의 최적화기 (histogram_cl.cpphistogram_extract_key, query_planner.c 가 플랜을 세우며 선택도를 추정할 때 호출). 여기서는 질의문에 적힌 상수(리터럴)의 타입으로 판단했습니다. 문자열 리터럴은 CHAR 이므로, 컬럼이 VARCHAR 여도 상수에서 공백을 떼어냈습니다.

같은 히스토그램을 서로 다른 기준으로 다루니 VARCHAR 컬럼에서 어긋납니다. 타입 검사기는 CHAR↔VARCHAR 비교에서 리터럴을 컬럼 타입으로 강제변환하지 않으므로 varchar_col = 'abcd ' 의 상수는 CHAR 인 채로 남고, 최적화기만 이를 'abcd' 로 줄여 엉뚱한 MCV 에 맞췄습니다 (1,600행 재현에서 참값 100행인데 sel 0.125 = 200행).

두 쪽이 맞춰야 할 정답은 서버 실행기가 실제로 적용하는 비교 규칙입니다. 매뉴얼 「데이터 타입 > 문자열 > 비교 규칙」이 정한 대로, 후행 공백을 무시하는 것은 양쪽이 모두 고정 길이(CHAR) 일 때뿐이고 한쪽이 가변 길이(VARCHAR) 면 후행 공백을 포함해서 비교합니다 (db_string_compare, 기본값 ignore_trailing_space=no). 즉 VARCHAR 컬럼에서 'abcd''abcd ' 는 엄연히 다른 값이고, 히스토그램의 추정도 이 규칙을 따라야 합니다.

같은 뿌리에서 나온 두 번째 결함 — CHAR 컬럼의 LIKE / REGEXP. CHAR(8) 컬럼은 매뉴얼대로 선언 길이까지 오른쪽을 공백으로 채워 저장하므로 힙에는 'abcd ' 가 들어 있고, 실행기의 LIKE / REGEXP 는 이 채워진 값에 패턴을 맞춥니다. 반면 히스토그램에는 공백을 떼어낸 'abcd' 만 남아 있어 LIKE 'abcd %', LIKE 'abcd ', REGEXP '^abcd +$' 같이 공백 자리를 요구하는 패턴이 거의 아무것도 맞지 않았습니다 (참값 340행인데 sel 0.003 = 5행).

Implementation

  • 정규화 규약을 한 곳으로: hist::string_key_size_for_column (column_type, s, size) (histogram_reader.hpp, inline). 판단 기준을 컬럼 타입 하나로 못박아, CHAR 컬럼이면 채움 공백을 뺀 길이를, 그 외(VARCHAR·BIT)는 바이트를 그대로 돌려줍니다. 서버측 샘플러(extract<std::string> · ndv_hll_hash)와 클라이언트측 histogram_extract_key같은 함수를 부르므로 두 쪽이 어긋날 수 없습니다.
  • 최적화기는 상수가 아니라 컬럼을 보고 정규화: histogram_extract_key 에 히스토그램 컬럼 타입 인자를 추가하고(reader.value_type ()), 리터럴 타입으로 분기하던 코드를 없앴습니다. 호출부는 histogram_get_equal_selectivity · histogram_get_comp_selectivity 두 곳입니다.
  • CHAR 값 되채우기: histogram_repad_char_value (value, column_type, precision, codeset, buf) (histogram_cl.cpp). CHAR 컬럼이면 히스토그램에 저장된 MCV·버킷 경계값을 컬럼 정밀도(문자 수, intl_char_count)까지 공백으로 실행기가 보는 모양대로 되돌려 like_match_value (db_string_like) 와 rlike_match_string (cubregex::search) 에 넣습니다. 정밀도는 히스토그램 blob 헤더에서, 코드셋은 이미 결정된 콜레이션에서 읽습니다(아래 항목). 질의문의 컬럼 노드는 보지 않습니다.
  • 되채우기 폭의 출처를 blob 으로: 컬럼 정밀도는 히스토그램이 서술하는 컬럼의 속성이지 질의문의 속성이 아닌데, 처음에는 컬럼 노드의 data_type 에서 읽었습니다. 그 노드가 data_type 을 달고 오지 않으면 정밀도가 0 이 되어 되채우기가 조용히 꺼지고 이 PR 이 고친 추정이 그 경로에서 되살아납니다. 헤더 v2 의 오프셋 28 은 빌더가 상수 0 만 쓰고 읽는 곳이 없던 예약 4바이트였으므로, 이를 HV2_PRECISION 으로 삼아 컬럼 폭(문자 수)을 적고 HistogramReader::value_precision () 으로 꺼냅니다. 샘플러는 이미 읽고 있는 데이터에서 폭을 얻습니다 — 힙 CHAR 값은 도메인을 거쳐 만들어지므로(mr_readval_char_internal ()domain->precision 으로 DB_VALUE 를 초기화) 널이 아닌 첫 행이 컬럼 폭을 들고 옵니다. 병렬 경로의 최종 컬렉터는 행을 직접 먹지 않고 병합만 하므로 merge_peers () 가 워커가 본 폭을 가져옵니다. 코드셋은 두 추정 경로가 이미 맞춰 둔 콜레이션이 결정하므로 거기서 읽습니다(LANG_GET_COLLATION 은 release 에서 경계 검사 없는 배열 접근이라 LIKE 쪽도 REGEXP 쪽처럼 lang_get_collation () + NULL 검사를 씁니다).
  • 오프셋·헤더 크기·포맷 버전은 그대로라 구·신 바이너리가 서로의 blob 을 읽습니다. 다만 이전에 수집된 blob 은 이 자리가 0 — '폭 모름' 이라 CHAR 의 LIKE/REGEXP 되채우기는 update statistics 를 다시 돌린 뒤부터 동작합니다(등가·범위 추정은 재수집 없이 그대로입니다).

Remarks

  • 검증(goto release 빌드 a82e6d6ae, demodb 1,600행: 'abcd'×200 · 'abcd '×100 · 'abcd '×40 · 'xyz'×60 · 필러 1,200, update statistics ... with fullscan, 300 buckets, set optimization level 513(sel N)count(*) 비교, ignore_trailing_space=no):

    술어 수정 전 sel 수정 후 sel 참값
    t_vc.c = 'abcd ' (varchar(32)) 0.125 0.0625 100/1600
    t_vc.c = 'abcd ' 0.125 0.025 40/1600
    t_vc.c = 'abcd' 0.125 0.125 200/1600
    t_ch.c = 'abcd' / = 'abcd ' (char(8)) 0.2125 0.2125 340/1600
    t_ch.c LIKE 'abcd %' (히스토그램 LIKE 추정) 0.000625 0.2126 340/1600
    t_ch.c LIKE 'abcd ' 0.000625 0.2126 340/1600
    t_ch.c REGEXP '^abcd +$' (미측정) 0.2126 340/1600
    t_ch.c REGEXP 'abcd$' (미측정) 0.000625 0/1600
    t_vc.c LIKE 'abcd %' (범위 재작성, VARCHAR 는 되채우기 대상 아님) 0.211875 0.211875 140/1600

    ignore_trailing_space=yes 에서도 t_vc.c = 'abcd' 등가 추정 0.2125(340행)는 그대로입니다.

  • 재현 SQL 은 JIRA 에 첨부했습니다(cbrd27251_repro.sql). 조합 TC 는 통계 구현이 바뀔 때마다 sel 값이 흔들려 별도 발번하지 않았습니다(2026-09-17 결정).

  • 범위 밖으로 남긴 것: ignore_trailing_space=yes 에서의 문자열 범위 비교(버킷 경계는 바이트 순서), CHAR↔VARCHAR 조인의 MCV 매칭. 별건으로 기록했습니다.

  • upstream/develop 051d5f61d 위로 리베이스했습니다. develop 신규 커밋은 히스토그램 파일을 건드리지 않아 충돌이 없었고, 위 검증 결과는 리베이스 전 트리(a82e6d6ae)에서 얻은 것입니다.

  • 리뷰 대응: db_make_varchar() 가 버퍼를 복사한다고 적은 주석을 고쳤습니다(067b319c9). 실제로는 db_make_db_char()medium.buf 에 포인터만 넣고 need_clear 를 false 로 두므로 빌려 쓰는 것이고, 안전 조건은 db_string_like() 가 같은 호출 안에서 값을 소비한다는 점입니다. rlike_match_string()std::string 으로 실제 복사하므로 기존 주석을 유지했습니다.

  • 자기 리뷰(harness-review)에서 걸러낸 것: 되채우기 후 value 의 출처(blob 또는 pad_buf)를 잘못 안내하던 like_match_value · rlike_match_string 의 주석을 고쳤습니다. 수치 표는 이 Remarks 에 옮겼습니다.

  • 2차 자기 리뷰(harness-review, HEAD 9cd1c1274)에서 걸러낸 것: 되채우기가 필요로 하는 컬럼 정밀도·코드셋을 질의문에서 읽고 있던 것을 blob·콜레이션으로 옮겼습니다(위 Implementation 두 항목). 그 과정에서 병렬 수집의 최종 컬렉터가 폭을 못 받는 경로도 함께 막았습니다.

  • CAST(...) 상수는 접히지 않아 히스토그램 경로(PC_CONST)를 타지 않습니다 — 이 PR 과 무관합니다.

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

❌ TC Merge Gate — Merge Blocked

One or more TC PRs are still open. Please merge or close them before merging this PR.

TC Repositories & Branches:

  • cubrid-testcases: TC PR tc/pr-7977 is open (draft) — must be merged or closed first
  • cubrid-testcases-private-ex: TC PR tc/pr-7977 is open (draft) — must be merged or closed first

Steps to unblock:

  1. Merge or close all TC PRs listed above.
  2. Re-run this check: Actions tab → TC Merge Gate → Re-run failed jobs

@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.

@soheejung-cs
soheejung-cs marked this pull request as ready for review September 18, 2026 03:38
@greptile-apps

greptile-apps Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Retrigger

기능상 차단 결함은 확인되지 않아 병합 가능한 상태로 보이지만, 메모리 소유권 주석을 바로잡고 핵심 정규화 계약에 대한 자동화 회귀 테스트를 보강하는 것이 좋습니다.

Reviews (1) · Last reviewed commit: "[CBRD-27251] Fix stale value-origin comm..."

Comment thread src/optimizer/histogram/histogram_cl.cpp Outdated
Comment thread src/optimizer/histogram/histogram_reader.hpp
SOHEE_JUNG added 2 commits September 18, 2026 14:15
…e-pad CHAR values for LIKE/REGEXP estimates

The sampler stripped trailing spaces when the heap value (= column) type
was CHAR, but the client probe stripped when the constant's type was CHAR.
The type checker leaves a CHAR literal compared to a VARCHAR column as
CHAR, so `varchar_col = 'abcd '` probed the stripped key 'abcd' against
unstripped VARCHAR MCVs and matched the wrong entry (sel 0.125 for 100 of
1600 rows, expected 0.0625).

- hist::string_key_size_for_column (histogram_reader.hpp): the single
  definition of the rule (strip iff the COLUMN is CHAR); used by the
  sampler (extract<std::string>, ndv_hll_hash) and the probe
  (histogram_extract_key, which now takes the histogram's column type).
- histogram_repad_char_value (histogram_cl.cpp): LIKE/REGEXP run on the
  padded heap value at execution, so re-pad stored CHAR MCV/bucket values
  to the column precision (in characters, per codeset) before matching.
  Without data_type on the column node the estimate falls back to the
  stripped comparison.

Histogram blob format is unchanged; no re-collection needed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBMgWWzmrvFiQBSBUiL2YD

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
After histogram_repad_char_value () the matched value may live in pad_buf
rather than the histogram blob; say so where the comment described its
lifetime (self-review finding, no code change).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBMgWWzmrvFiQBSBUiL2YD

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
db_make_varchar () borrows the buffer instead of copying it (db_make_db_char
sets medium.buf to the caller's pointer and leaves need_clear false), so the
comment claiming it copies the value out described the ownership contract
backwards. The operand is safe because db_string_like () consumes it in the
same call, before the next iteration re-pads pad_buf -- say that instead.

Review comment from greptile-apps on PR CUBRID#7977.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBMgWWzmrvFiQBSBUiL2YD

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
@soheejung-cs soheejung-cs self-assigned this Sep 18, 2026
@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run all

soheejung-cs and others added 3 commits September 21, 2026 15:33
…not the query

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

The CHAR re-padding added for LIKE/REGEXP estimates took the column width and
charset from the query's column node (PT_NAME->data_type). Both describe the
column the histogram was collected on, not the query text, and the column node
is not guaranteed to carry them: a node without data_type yielded precision 0,
which disables re-padding, so the estimate silently fell back to matching the
stripped stored value. The same two properties were read from two different
places -- the value type from the blob, the width and charset from the parse
tree.

- Width now travels in the blob. Header v2 reserved four bytes at offset 28 that
  nothing ever read (the builder wrote a constant 0); that field becomes
  HV2_PRECISION and carries the column's declared width in characters.
  HistogramReader exposes it as value_precision (). Offsets, header size and
  format version are unchanged, so old and new binaries read each other's blobs;
  a blob written before this change reports 0, which means "width unknown" and
  keeps re-padding off until the statistics are collected again.
- The sampler picks the width up from the data it already reads. A heap CHAR
  value is materialized through its domain (mr_readval_char_internal () inits the
  DB_VALUE with domain->precision), so the first non-null row of the column
  reports it; col_collector records it once and passes it down through
  build_blob () to HistogramBuilder::build (). The parallel path needs one more
  step: parallel_scan_merge_multi () builds the final collector fresh and only
  merges peers into it, so merge_peers () takes the width the workers saw.
- Charset comes from the collation instead. Both estimate paths already resolve
  the collation they match under, and a collation determines its charset, so
  histogram_get_like_selectivity () and histogram_get_rlike_selectivity () read
  it there. LANG_GET_COLLATION indexes the collation array without a bounds
  check in release builds, so the LIKE path uses lang_get_collation () with a
  NULL check, as the REGEXP path already did.

After this the re-padding depends only on the blob and the resolved collation.
Existing statistics keep working, but their CHAR LIKE/REGEXP estimates stay at
the pre-CBRD-27251 behaviour until update statistics runs again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zfELL2BypfaeCFB9fj8Dd
@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run all

@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run rerun 35686538794

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.

1 participant