콘텐츠로 이동

(KO) PostgreSQL 데이터타입 라이브러리 — varlena, numeric, datetime, jsonb, 배열

목차

관계형 데이터베이스는 근본적으로 *값(value)*을 저장하고 비교하는 기계이며, 값은 타입 없이는 의미가 없다. Codd(1970)의 관계 모델(Database System Concepts, Silberschatz 7e, 4장 “Intermediate SQL” 및 5장 타입 시스템 논의)은 도메인 — 속성에 허용되는 값의 집합 — 을 기본 단위로 삼는다. SQL은 이 도메인을 데이터 타입으로 바꾸어 외부 텍스트 문법, 내부 저장 인코딩, 전순서(total order) 또는 부분순서, 그리고 연산자 집합을 부여한다.

엔진 입장에서 타입은 단순한 레이블이 아니라 **추상 데이터 타입(ADT)**이다. 표현(representation)과 그 표현을 존중하는 연산(operation)의 결합체다. 나머지 시스템은 그 바이트를 해석할 수 없고, 오직 해당 타입의 함수들만 해석할 권한을 갖는다. Liskov & Zilles가 프로그래밍 언어에서 형식화한 캡슐화가 온디스크 튜플에 그대로 적용된 셈이다.

DBMS가 타입을 구현할 때 부딪히는 세 가지 힘이 있다. 첫째는 저장 효율이다. 행 스토어는 많은 속성을 고정 폭 튜플에 밀어 넣으므로, 4바이트 정수나 8바이트 타임스탬프 같은 고정 길이 타입은 오버헤드 없이 인라인 저장된다. 반면 문자열, 십진수, JSON 문서, 배열은 본질적으로 가변 길이이고, 10억 행 테이블은 작은 문자열마다 비대한 헤더를 감당할 수 없다. 인코딩은 크기에 비례한 비용만 지불해야 한다 — 한 글자짜리 문자열이 1 MB 문서와 같은 헤더를 써서는 안 된다.

둘째는 비교와 정렬이다. B-tree 인덱스, 정렬, 해시 조인, GROUP BY는 모두 타입이 일관된 비교 함수를 노출하기를 요구한다. 문제는 “일관된”의 기준이 타입마다, 때로는 로케일마다 다르다는 것이다. bytea에서는 바이트 순서가 맞는 정렬이지만 사람 언어 text에서는 틀리고, 1.01.00은 바이트 표현이 달라도 numeric에서 같아야 한다. 이 순서 계약 — 연산자 클래스 — 이 generic B-tree가 타입을 모르면서도 어떤 타입이든 인덱싱할 수 있게 해준다(postgres-nbtree.md, postgres-index-am.md 참조).

셋째는 확장성이다. What Goes Around Comes Around(Stonebraker & Hellerstein 2005; dbms-papers/goes-around.md에 수록)는 POSTGRES를 위시한 객체-관계형 계보가 타입 시스템을 열린(open) 구조로 선택한 과정을 추적한다. 사용자와 확장 모듈이 새로운 기본 타입을 추가할 수 있고, 엔진은 그것을 내장 타입과 동등하게 취급한다. 타입이 카탈로그 행과 등록된 C 함수의 집합으로 기술되기 때문이다. 그 개방성의 대가는 엄격한 **호출 규약(calling convention)**이고, 이득은 PostGIS가 geometry 타입을 추가하거나 citext 확장이 대소문자 무감각 텍스트를 추가할 때 코어 실행기를 건드리지 않아도 된다는 점이다. The Design of POSTGRES(Stonebraker & Rowe 1986)가 처음부터 추구한 일반성이다.

핵심 아이디어는 간접 참조를 통한 균일성이다. 쿼리 실행기는 값을 미분화된 Datum으로 조작한다. Datum은 레지스터 폭 토큰으로, pass-by-value 타입(int4 등)에서는 값 그 자체이고 pass-by-reference 타입(text 등)에서는 포인터다. 모든 타입 특화 동작은 카탈로그에서 함수를 조회해 균일한 시그니처로 호출하는 방식으로 도달한다. ADT 라이브러리는 내장 타입에 대한 그 함수들의 모음이다. 추상적인 관계형 도메인 개념이 구체적인 바이트와 C 코드로 실체화되는 계층이다.

모든 SQL 엔진은 타입마다 동일한 질문에 답해야 하며, 그 답은 인식 가능한 공통 패턴으로 수렴한다.

I/O 쿼텟. 타입은 내부 바이트 표현과 (a) SQL 리터럴·COPY·psql 출력에 쓰는 사람 가독 텍스트 형식, (b) 클라이언트 와이어 프로토콜 효율을 위한 기계 가독 이진 형식 사이를 변환할 수 있어야 한다. 이로부터 거의 모든 엔진이 갖추는 네 개의 함수가 나온다:

  • input — 외부 텍스트를 내부 바이트로 파싱 (리터럴, COPY ... FROM, 텍스트 캐스트에 사용);
  • output — 내부 바이트를 텍스트로 렌더링 (결과 행, COPY ... TO에 사용);
  • receive — 이진 와이어 형식을 내부 바이트로 디코딩;
  • send — 내부 바이트를 이진 와이어 형식으로 인코딩.

텍스트 I/O는 정규적이고 안정적이다. 이진 I/O는 파싱 속도와 정확성(float 십진 왕복 없음)을 위한 최적화다. 견고한 엔진은 이진 형식이 진화 불가능한 숨겨진 온디스크 포맷이 되는 상황을 막는다 — 그래서 이진 프로토콜은 대개 버전 바이트를 포함한다.

가변 길이 표현. 고정 길이 타입은 단순하다: 바이트를 인라인 저장하면 된다. 가변 길이 타입은 어딘가에 길이가 필요하다. 두 고전적 선택지는 길이 접두사 인코딩(헤더 워드에 바이트 수, 이후 페이로드)과 종료자 종결 인코딩(C 문자열의 NUL 종결)이다. 데이터베이스는 압도적으로 길이 접두사를 선택한다. 내장 NUL 허용, O(1) 길이, 이진 안전 복사가 가능하기 때문이다. 성숙한 엔진이 결국 도달하는 개선은 두 가지다: 작은 값에 헤더를 줄이고 큰 값은 아웃오브라인으로 밀어낸다. 행 스토어는 튜플이 페이지에 들어가길 원하고, 수 메가바이트 값은 그렇지 않으면 튜플을 저장 불가능하게 만든다. 아웃오브라인 메커니즘(Oracle LOB, SQL Server LOB_DATA/행 오버플로 페이지, PostgreSQL TOAST)은 큰 페이로드를 사이드 릴레이션에 저장하고 튜플에 작은 포인터를 남긴다.

임의 정밀도 십진수. float는 빠르지만 손실이 있다. 금융과 정밀 산술 워크로드는 0.1을 정확히 표현하고 수백 자리 유효숫자를 지원하는 십진수 타입이 필요하다. 보편적인 구현은 부호, 지수/스케일, 그리고 어떤 기수(보통 10의 거듭제곱, 십진 반올림과 텍스트 변환이 깔끔해지므로)의 자릿수 배열이며, 더하기/빼기/곱하기/나누기에 교과서 알고리즘을 쓴다.

시간 타입. 날짜와 타임스탬프는 정수로 저장된다 — 에포크로부터 일수 또는 마이크로초 수. 정수 산술이 정확하고 빠르며, 달력 변환(복잡한 부분: 윤년, 월별 일수, 그레고리력 개혁)은 Julian-day 커널로 격리되어 (연, 월, 일)을 단일 일 번호로 왔다 갔다 매핑한다.

반구조화 및 컬렉션 타입. 현대 엔진은 JSON과 배열 타입을 추가한다. 순진한 구현은 텍스트를 저장하고 매 접근마다 재파싱한다. 성숙한 구현은 파싱된 이진 트리를 저장해 필드 접근과 포함 검사를 빠르게 하며, 삽입 시 직렬화 비용을 쿼리 시 속도와 맞바꾼다. 컬렉션 타입(배열, 중첩 테이블)은 차원 정보와 null 맵을 팩된 엘리먼트 데이터와 함께 담는다.

flowchart TD
  subgraph cat["카탈로그 기술"]
    T["pg_type 행<br/>typinput typoutput<br/>typreceive typsend<br/>typlen typbyval typalign typstorage"]
    P["pg_proc 행<br/>(C 함수들)"]
  end
  subgraph adt["ADT 라이브러리 (utils/adt)"]
    IN["typeIN: cstring → 내부"]
    OUT["typeOUT: 내부 → cstring"]
    RECV["typeRECV: 이진 → 내부"]
    SEND["typeSEND: 내부 → 이진"]
    CMP["btTYPEcmp / hashTYPE / sortsupport"]
  end
  T --> P --> adt
  EXEC["실행기 / COPY / 와이어 프로토콜"] -->|"fmgr 경유 FunctionCall"| adt
  IN --> DATUM["Datum (값 또는 포인터)"]
  RECV --> DATUM
  DATUM --> OUT
  DATUM --> SEND
  DATUM --> CMP

PostgreSQL의 독특한 선택은 이 패턴을 완전히 카탈로그 주도로, 내장 타입과 사용자 타입에 걸쳐 균일하게 만든다는 것이다. 실행기에 특권을 가진 “내장 타입 코드 경로”는 없다. int4와 PostGIS geometry는 함수 관리자(fmgr)로 동일하게 디스패치된다. 이것이 이 문서의 나머지 부분을 관통하는 주제다.

타입은 카탈로그 행이고, 동작은 등록된 함수다

섹션 제목: “타입은 카탈로그 행이고, 동작은 등록된 함수다”

PostgreSQL 타입은 pg_type의 한 행이다. 스칼라 속성들 — typlen(길이, 또는 varlena는 -1, cstring은 -2), typbyval(값 전달 vs 참조 전달), typalign(c/s/i/d), typstorage(plain/extended/external/main, TOAST 제어) — 은 generic 코드에게 타입의 내용을 이해하지 않고도 값을 옮기는 방법을 알려준다. 함수 참조들 — typinput, typoutput, typreceive, typsend, 비교·해시용 연산자 클래스 항목들 — 은 pg_proc 행, 즉 ADT 라이브러리의 C 함수를 가리킨다. 실행기, COPY, 와이어 프로토콜, 플래너의 선택도 코드 같은 generic 서브시스템은 타입을 하드코딩하지 않는다. 카탈로그 필드를 읽고 fmgr로 등록된 함수를 호출한다. 디스패치 메커니즘 — FmgrInfo, FunctionCallInfo, PG_FUNCTION_ARGS, V1 ABI — 은 postgres-fmgr.md의 주제다. 여기서는 그것을 주어진 것으로 받아들이고 ADT 함수들이 무엇을 하는지에 집중한다.

모든 ADT 함수는 동일한 C 시그니처를 갖는다:

// textin — src/backend/utils/adt/varlena.c
Datum
textin(PG_FUNCTION_ARGS)
{
char *inputText = PG_GETARG_CSTRING(0);
PG_RETURN_TEXT_P(cstring_to_text(inputText));
}
// textout — src/backend/utils/adt/varlena.c
Datum
textout(PG_FUNCTION_ARGS)
{
Datum txt = PG_GETARG_DATUM(0);
PG_RETURN_CSTRING(TextDatumGetCString(txt));
}

PG_FUNCTION_ARGSFunctionCallInfo fcinfo 단일 파라미터로 확장된다. PG_GETARG_*PG_RETURN_* 매크로가 인자를 언팩하고 결과를 Datum으로 박싱한다. 실행기가 OID로 임의 타입의 입력 함수를 호출할 수 있는 균일성이 여기서 나온다. recv/send 쌍은 in/out의 이진 쌍둥이로, StringInfo 메시지 버퍼를 읽고 쓴다:

// textrecv — src/backend/utils/adt/varlena.c
Datum
textrecv(PG_FUNCTION_ARGS)
{
StringInfo buf = (StringInfo) PG_GETARG_POINTER(0);
text *result;
char *str;
int nbytes;
str = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes);
result = cstring_to_text_with_len(str, nbytes);
pfree(str);
PG_RETURN_TEXT_P(result);
}

가변 길이 값 — text, bytea, numeric, jsonb, 배열, typlen = -1로 선언된 사용자 타입 전부 — 은 varlena다. 계약(src/include/varatt.h 상단에 명시)은 값이 첫 바이트 하위 비트로 네 가지 물리 레이아웃 중 어느 것인지를 인코딩하는 헤더로 시작한다는 것이다. 헤더는 레이아웃이 엔디언과 형식에 따라 다르기 때문에 필드 접근이 아닌 매크로로만 읽는다.

네 가지 형식(리틀 엔디언 플래그 비트 기준; 빅 엔디언은 미러):

형식첫 바이트 플래그헤더 크기최대 용량용도
4-byte 비압축xxxxxx004 B (정렬)~1 GB일반 값
4-byte 인라인 압축xxxxxx104 B + va_tcinfo~1 GBpglz/lz4 압축, 여전히 튜플 내
1-byte 단축xxxxxxx11 B (비정렬)최대 126 B작은 값, 정렬 패딩 절약
TOAST 포인터000000012 B 태그 + 본문해당 없음아웃오브라인 / 간접 / 확장

4-byte 길이 워드는 자기 자신을 포함하므로 페이로드 길이는 VARSIZE - VARHDRSZ다. 엔디언별 추출 방식:

// VARSIZE_4B / VARSIZE_1B — src/include/varatt.h (리틀 엔디언)
#define VARSIZE_4B(PTR) \
((((varattrib_4b *) (PTR))->va_4byte.va_header >> 2) & 0x3FFFFFFF)
#define VARSIZE_1B(PTR) \
((((varattrib_1b *) (PTR))->va_header >> 1) & 0x7F)

1-byte 단축 헤더가 가장 영리한 부분이다. 일반 4-byte 헤더는 4-byte 정렬이 필요해서, 작은 문자열이 가득한 튜플에서는 값 하나당 최대 3바이트 패딩 낭비와 헤더 4바이트 중 3바이트 낭비가 발생한다. 단축 형식은 길이와 플래그를 단일 바이트로 표현하고, 비정렬 저장되며, 126바이트로 상한이 있다 — 실제 스키마를 지배하는 짧은 문자열에 딱 맞다. 값이 들어갈 경우(VARATT_CAN_MAKE_SHORT) 단축 형식으로 다운변환할 수 있다. 단축 및 외부 datum은 비정렬이므로, 이들을 볼 수 있는 코드는 플래그 비트로 디스패치하는 _ANY 패밀리 매크로를 써야 한다:

// VARSIZE_ANY_EXHDR / VARDATA_ANY — src/include/varatt.h
#define VARSIZE_ANY_EXHDR(PTR) \
(VARATT_IS_1B_E(PTR) ? VARSIZE_EXTERNAL(PTR)-VARHDRSZ_EXTERNAL : \
(VARATT_IS_1B(PTR) ? VARSIZE_1B(PTR)-VARHDRSZ_SHORT : \
VARSIZE_4B(PTR)-VARHDRSZ))
#define VARDATA_ANY(PTR) \
(VARATT_IS_1B(PTR) ? VARDATA_1B(PTR) : VARDATA_4B(PTR))

인라인 압축되거나 아웃오브라인으로 밀려난 값은 확장(extended) 상태다(VARATT_IS_EXTENDED). 타입 함수가 페이로드를 건드리기 전에 detoast해야 한다. pg_detoast_datum은 확장 datum을 일반 4-byte varlena로 확장하고, pg_detoast_datum_packed는 단축 헤더는 그대로 두고 압축/외부화만 해제한다(_ANY 매크로가 단축 헤더를 처리하므로). 실제 아웃오브라인 페치와 압축 해제는 access/common/detoast.cdetoast_attr이며, postgres-toast.md에서 다룬다:

// pg_detoast_datum_packed — src/backend/utils/fmgr/fmgr.c
struct varlena *
pg_detoast_datum_packed(struct varlena *datum)
{
if (VARATT_IS_COMPRESSED(datum) || VARATT_IS_EXTERNAL(datum))
return detoast_attr(datum);
else
return datum;
}

그래서 정규 text_to_cstringpg_detoast_datum_packedVARDATA_ANY/VARSIZE_ANY_EXHDR를 호출하고, 언팩된 복사본이 원본과 다를 때만(즉, detoasting이 실제로 할당했을 때만) 해제하는 것이다:

// text_to_cstring — src/backend/utils/adt/varlena.c
char *
text_to_cstring(const text *t)
{
text *tunpacked = pg_detoast_datum_packed(unconstify(text *, t));
int len = VARSIZE_ANY_EXHDR(tunpacked);
char *result;
result = (char *) palloc(len + 1);
memcpy(result, VARDATA_ANY(tunpacked), len);
result[len] = '\0';
if (tunpacked != t)
pfree(tunpacked);
return result;
}

생성자 측은 대칭이며 항상 전체 4-byte 헤더를 구축한다(datum은 “비토스트 상태로 태어난다”). 시스템이 나중에 튜플 조립 시 단축하거나 TOAST할 수 있다:

// cstring_to_text_with_len — src/backend/utils/adt/varlena.c
text *
cstring_to_text_with_len(const char *s, int len)
{
text *result = (text *) palloc(len + VARHDRSZ);
SET_VARSIZE(result, len + VARHDRSZ);
memcpy(VARDATA(result), s, len);
return result;
}
flowchart TD
  D["들어오는 varlena Datum"] --> Q{"첫 바이트 플래그 비트"}
  Q -->|"xxxxxx00"| FB["4-byte 헤더<br/>정렬됨, 일반<br/>VARDATA / VARSIZE"]
  Q -->|"xxxxxx10"| FC["4-byte 헤더<br/>인라인 압축<br/>va_tcinfo에 rawsize+method"]
  Q -->|"xxxxxxx1"| SH["1-byte 헤더<br/>비정렬 단축<br/>최대 126바이트"]
  Q -->|"00000001"| EX["TOAST 포인터 (1b_e)<br/>태그: ONDISK / INDIRECT / EXPANDED"]
  FC -->|"detoast_attr"| FB
  EX -->|"detoast_attr"| FB
  FB --> USE["타입 함수가 페이로드 읽기"]
  SH --> USE

text와 bytea: 복사와 콜레이션 인식 정렬

섹션 제목: “text와 bytea: 복사와 콜레이션 인식 정렬”

textbytea는 추가 구조 없는 varlena다. 페이로드 자체가 문자열/바이트 시퀀스다. 입출력은 본질적으로 헤더 주변의 memcpy다(textin/textout는 위에 나왔고, byteain/byteaout는 비인쇄 바이트에 이스케이프 처리를 추가한다). 흥미로운 코드는 정렬이다. text는 바이트 값이 아닌 콜레이션 기준으로 정렬해야 하기 때문이다. varstr_cmp가 그 핵심이다. C 로케일은 memcmp로 빠른 경로를 타고, 그렇지 않으면 콜레이션 제공자(pg_strncoll)를 호출하며, 바이트 동일성 memcmp 숏컷으로 문자열이 바이트까지 같을 때 비싼 콜레이션 호출을 회피한다:

// varstr_cmp — src/backend/utils/adt/varlena.c
int
varstr_cmp(const char *arg1, int len1, const char *arg2, int len2, Oid collid)
{
int result;
pg_locale_t mylocale;
check_collation_set(collid);
mylocale = pg_newlocale_from_collation(collid);
if (mylocale->collate_is_c)
{
result = memcmp(arg1, arg2, Min(len1, len2));
if ((result == 0) && (len1 != len2))
result = (len1 < len2) ? -1 : 1;
}
else
{
if (len1 == len2 && memcmp(arg1, arg2, len1) == 0)
return 0;
result = pg_strncoll(arg1, len1, arg2, len2, mylocale);
/* Break tie if necessary. */
if (result == 0 && mylocale->deterministic)
{
result = memcmp(arg1, arg2, Min(len1, len2));
if ((result == 0) && (len1 != len2))
result = (len1 < len2) ? -1 : 1;
}
}
return result;
}

정렬을 위해 textSortSupport 함수를 등록한다. 그러면 실행기가 비교마다 fmgr 호출을 건너뛸 수 있고, *약어 키(abbreviated key)*를 사용해 콜레이션 키의 접두사를 Datum에 압축해 1차 비교를 저렴하게 할 수도 있다. C 로케일 빠른 비교기는 단순한 memcmp다:

// varstrfastcmp_c — src/backend/utils/adt/varlena.c
static int
varstrfastcmp_c(Datum x, Datum y, SortSupport ssup)
{
VarString *arg1 = DatumGetVarStringPP(x);
VarString *arg2 = DatumGetVarStringPP(y);
char *a1p = VARDATA_ANY(arg1);
char *a2p = VARDATA_ANY(arg2);
int len1 = VARSIZE_ANY_EXHDR(arg1);
int len2 = VARSIZE_ANY_EXHDR(arg2);
int result;
result = memcmp(a1p, a2p, Min(len1, len2));
if ((result == 0) && (len1 != len2))
result = (len1 < len2) ? -1 : 1;
/* We can't afford to leak memory here. */
if (PointerGetDatum(arg1) != x)
pfree(arg1);
if (PointerGetDatum(arg2) != y)
pfree(arg2);
return result;
}

detoast된 복사본을 명시적으로 pfree하는 점에 주목하라. B-tree 비교기는 긴 정렬 도중 비교마다 한 번씩 실행되므로 메모리를 누수시켜서는 안 된다. 콜레이션 기계와 약어 키 인코딩은 i18n/정렬 서브시스템(postgres-overview-i18n-text.md, postgres-agg-sort-nodes.md)에 속한다. 여기서 핵심은 text 정렬이 일반적으로 memcmp가 아니라는 것 — 카탈로그 주도, 콜레이션 인식 함수이며, B-tree가 임의 타입에 쓰는 것과 같은 연산자 클래스로 도달한다.

numeric은 “비대한 varlena”의 정수(正數)다. 임의 정밀도 십진수로, 온디스크 형식은 부호, 가중치(weight), 표시 스케일, 자릿수 배열을 팩하고, 산술 형식(NumericVar)은 같은 자릿수를 가변 버퍼에 언팩한다. 자릿수 기수는 NBASE = 10000(DEC_DIGITS = 4 — 저장된 int16 자릿수 하나당 십진 4자리)이다. 두 자릿수 곱이 int32에 들어가고, 십진 텍스트 변환이 4자리 단위 반복으로 간단해지도록 선택한 것이다:

// numeric.c — 기수 선택 (NBASE==10000 분기가 실제 사용됨)
#define NBASE 10000
#define HALF_NBASE 5000
#define DEC_DIGITS 4 /* decimal digits per NBASE digit */
#define MUL_GUARD_DIGITS 2 /* these are measured in NBASE digits */
#define DIV_GUARD_DIGITS 4
typedef int16 NumericDigit;

인메모리 NumericVar는 palloc된 버퍼(buf)와 첫 번째 유효 자릿수(digits)를 분리한다. 앞에 여분의 선두 자릿수 공간을 의도적으로 남겨두어, 최상위에서 올림(carry out)이 발생해도 digits를 한 칸 앞으로 당기고 weight를 올리는 것만으로 흡수한다 — 재할당 없이:

// NumericVar — src/backend/utils/adt/numeric.c
typedef struct NumericVar
{
int ndigits; /* # of digits in digits[] - can be 0! */
int weight; /* weight of first digit */
int sign; /* NUMERIC_POS, _NEG, _NAN, _PINF, or _NINF */
int dscale; /* display scale */
NumericDigit *buf; /* start of palloc'd space for digits[] */
NumericDigit *digits; /* base-NBASE digits */
} NumericVar;

온디스크 헤더도 적응형이다. 단축 형식(16-bit 헤더 워드 하나, weight와 scale이 작을 때 사용)이나 장형 형식(헤더 워드 두 개), 그리고 NaN / +Inf / -Inf를 헤더만으로 인코딩하는 특수 형식이 있다. 플래그 비트가 첫 번째 헤더 워드 상단에 있다:

// numeric.c — 온디스크 헤더 플래그 비트
#define NUMERIC_SIGN_MASK 0xC000
#define NUMERIC_POS 0x0000
#define NUMERIC_NEG 0x4000
#define NUMERIC_SHORT 0x8000 /* short (1-word) header */
#define NUMERIC_SPECIAL 0xC000 /* NaN / Inf, header is all there is */
#define NUMERIC_HDRSZ (VARHDRSZ + sizeof(uint16) + sizeof(int16))
#define NUMERIC_HDRSZ_SHORT (VARHDRSZ + sizeof(uint16))

출력은 “언팩 후 문자열화”다. numeric_out은 세 비유한 값을 특수 처리하고, 그 외에는 init_var_from_num으로 NumericVar 뷰를 얻고 get_str_from_var로 렌더링한다:

// numeric_out — src/backend/utils/adt/numeric.c
Datum
numeric_out(PG_FUNCTION_ARGS)
{
Numeric num = PG_GETARG_NUMERIC(0);
NumericVar x;
char *str;
if (NUMERIC_IS_SPECIAL(num)) /* NaN / Infinity / -Infinity */
{
if (NUMERIC_IS_PINF(num)) PG_RETURN_CSTRING(pstrdup("Infinity"));
else if (NUMERIC_IS_NINF(num)) PG_RETURN_CSTRING(pstrdup("-Infinity"));
else PG_RETURN_CSTRING(pstrdup("NaN"));
}
init_var_from_num(num, &x);
str = get_str_from_var(&x);
PG_RETURN_CSTRING(str);
}

산술은 자릿수 배열의 교과서 알고리즘이다. add_var는 부호에 따라 add_abs/sub_abs로 디스패치하고(부호가 다를 때 어느 쪽이 더 큰지는 cmp_abs로 판단), 절댓값 루틴들은 같은 부호 덧셈과 큰 수에서 작은 수를 빼는 뺄셈만 처리한다:

// add_var — src/backend/utils/adt/numeric.c (부호 디스패치, 요약)
static void
add_var(const NumericVar *var1, const NumericVar *var2, NumericVar *result)
{
if (var1->sign == NUMERIC_POS)
{
if (var2->sign == NUMERIC_POS) /* (+a) + (+b) */
{
add_abs(var1, var2, result);
result->sign = NUMERIC_POS;
}
else /* (+a) + (-b) */
{
switch (cmp_abs(var1, var2))
{
case 0: zero_var(result); ...; break;
case 1: sub_abs(var1, var2, result); result->sign = NUMERIC_POS; break;
/* case -1: sub_abs(var2, var1, result); sign = NUMERIC_NEG; */
}
}
}
/* ... var1->sign == NUMERIC_NEG 대칭 분기 ... */
}

NumericVar를 온디스크 Numeric으로 다시 팩하는 것이 make_result(make_result_opt_error 경유)다. 선두 및 후미 0 자릿수를 제거하고, 표준 영(canonical zero)을 강제하며, weight와 scale이 들어가는지(NUMERIC_CAN_BE_SHORT)에 따라 단축 vs 장형 헤더를 선택한다. 인메모리와 온디스크 표현이 만나는 바로 그 지점이다:

// make_result_opt_error — src/backend/utils/adt/numeric.c (요약)
n = var->ndigits;
while (n > 0 && *digits == 0) { digits++; weight--; n--; } /* 선두 0 제거 */
while (n > 0 && digits[n - 1] == 0) n--; /* 후미 0 제거 */
if (n == 0) { weight = 0; sign = NUMERIC_POS; } /* 표준 영 */
if (NUMERIC_CAN_BE_SHORT(var->dscale, weight)) /* 단축 헤더 */
{
len = NUMERIC_HDRSZ_SHORT + n * sizeof(NumericDigit);
result = (Numeric) palloc(len);
SET_VARSIZE(result, len);
result->choice.n_short.n_header =
(sign == NUMERIC_NEG ? (NUMERIC_SHORT | NUMERIC_SHORT_SIGN_MASK)
: NUMERIC_SHORT)
| (var->dscale << NUMERIC_SHORT_DSCALE_SHIFT)
| (weight < 0 ? NUMERIC_SHORT_WEIGHT_SIGN_MASK : 0)
| (weight & NUMERIC_SHORT_WEIGHT_MASK);
}
else { /* 장형 헤더: n_sign_dscale + n_weight */ }
memcpy(NUMERIC_DIGITS(result), digits, n * sizeof(NumericDigit));

이진 I/O 쌍(numeric_recv/numeric_send)은 같은 자릿수를 int16 워드와 weight/sign/dscale로 와이어 전송한다. 이진 전송은 정확하다(십진 텍스트 왕복 없음).

flowchart TD
  ON["온디스크 Numeric<br/>short / long / special 헤더<br/>+ base-10000 자릿수 배열"]
  ON -->|"init_var_from_num"| NV["NumericVar<br/>ndigits weight sign dscale<br/>buf + digits (여분 선두 자릿수)"]
  NV -->|"add_var / mul_var<br/>(add_abs sub_abs cmp_abs)"| NV2["NumericVar 결과<br/>가드 자릿수, 이후 반올림"]
  NV2 -->|"make_result<br/>0 제거, 헤더 선택"| ON2["온디스크 Numeric"]
  NV -->|"get_str_from_var"| STR["cstring (numeric_out)"]
  ON -->|"numeric_send"| WIRE["이진 와이어 (int16 자릿수)"]

datetime: Julian-day 커널 위의 정수 저장

섹션 제목: “datetime: Julian-day 커널 위의 정수 저장”

date, time, timestamp, timestamptz고정 길이 타입(typlen 4 또는 8, 맞는 경우 typbyval)이다. 위의 varlena 타입들과 달리 헤더가 전혀 필요 없고 Datum이 정수를 직접 담는다. date는 PostgreSQL 에포크(2000-01-01)로부터의 일수이고, timestamp는 같은 에포크로부터의 마이크로초 수다. I/O는 달력을 인라인으로 파싱하지 않는다. 공유 datetime 토크나이저(ParseDateTimeDecodeDateTime)를 통하는데, 이것이 필드 순서, 타임존, 로케일 월 이름을 처리한다. 달력 산술 자체는 src/backend/utils/adt/datetime.c의 Julian-day 커널 date2j/j2date에 격리된다. 그것이 §“DBMS의 공통 설계”의 교과서 설계다: 정렬과 구간 연산을 위한 정확한 정수 산술에, 복잡한 그레고리력 변환은 두 함수에만 가둔다. 토크나이저와 타임존 데이터베이스는 독립적인 처리를 정당화할 만큼 크므로, 이 문서는 타입 계약이 동일한 I/O 쿼텟이며 달력 복잡성이 그 아래에 밀려난다는 점만 확인한다.

jsonb: TOAST 압축 가능한 이진 문서 트리

섹션 제목: “jsonb: TOAST 압축 가능한 이진 문서 트리”

jsonb는 가장 정교한 ADT 타입이다. 파싱된 문서를 저장해 키 조회와 포함 검사가 빠르면서도 단일 varlena를 유지해 TOAST가 압축하고 밀어낼 수 있다. 단위는 JsonbContainer다. 하위 28비트가 자식 수를 세고 상위 비트가 배열/객체/스칼라를 플래그하는 32-bit 헤더, 뒤이어 병렬 JEntry 배열, 뒤이어 자식들의 가변 길이 페이로드로 구성된다:

// JsonbContainer + JEntry — src/include/utils/jsonb.h
typedef struct JsonbContainer
{
uint32 header; /* # of elements or pairs, plus flag bits */
JEntry children[FLEXIBLE_ARRAY_MEMBER];
/* the data for each child node follows. */
} JsonbContainer;
#define JB_CMASK 0x0FFFFFFF /* mask for the count field */
#define JB_FSCALAR 0x10000000 /* top-level scalar wrapped in a 1-elem array */
#define JB_FOBJECT 0x20000000
#define JB_FARRAY 0x40000000
typedef uint32 JEntry;
#define JENTRY_OFFLENMASK 0x0FFFFFFF /* length OR offset of this child */
#define JENTRY_TYPEMASK 0x70000000 /* string/numeric/bool/null/container */
#define JENTRY_HAS_OFF 0x80000000 /* this JEntry holds an offset, not a len */

길이-또는-오프셋과 스트라이드가 미묘한 설계 선택이다. 자식마다 길이를 저장하면 압축성이 높지만(비슷한 자식들의 길이가 비슷한 바이트이므로) “k번째 자식 찾기”가 O(k) 전위합이 된다. 자식마다 오프셋을 저장하면 O(1) 랜덤 접근이 가능하지만 압축을 망친다. PostgreSQL은 절충한다. 대부분의 JEntry에는 길이를 저장하지만 JB_OFFSET_STRIDE(= 32)번째마다 누적 오프셋으로 변환하고(JENTRY_HAS_OFF 플래그), 어느 자식이든 가장 가까운 저장된 오프셋에서 최대 31개 길이를 더해 도달한다:

// jsonb.h — 스트라이드 근거 (그대로인 상수)
#define JB_OFFSET_STRIDE 32

값은 먼저 인메모리 JsonbValue 트리로 구축된 후(파서 또는 pushJsonbValue에 의해) 깊이 우선으로 평탄한 이진 형식으로 직렬화된다. JsonbValueToJsonb가 진입점이다. 노출 스칼라를 1-엘리먼트 JB_FSCALAR 배열로 감싸고(최상위 레벨이 항상 컨테이너가 되도록) 나머지는 convertToJsonb를 호출한다:

// JsonbValueToJsonb — src/backend/utils/adt/jsonb_util.c (요약)
Jsonb *
JsonbValueToJsonb(JsonbValue *val)
{
if (IsAJsonbScalar(val)) /* 스칼라를 rawScalar 배열로 감싸기 */
{
JsonbParseState *pstate = NULL;
JsonbValue scalarArray, *res;
scalarArray.type = jbvArray;
scalarArray.val.array.rawScalar = true;
scalarArray.val.array.nElems = 1;
pushJsonbValue(&pstate, WJB_BEGIN_ARRAY, &scalarArray);
pushJsonbValue(&pstate, WJB_ELEM, val);
res = pushJsonbValue(&pstate, WJB_END_ARRAY, NULL);
out = convertToJsonb(res);
}
else if (val->type == jbvObject || val->type == jbvArray)
out = convertToJsonb(val); /* 객체/배열 컨테이너 */
else { /* jbvBinary: 이미 직렬화된 자식, 헤더와 함께 복사 */ }
return out;
}

재귀 직렬화기 convertJsonbArray에서 스트라이드 로직이 작동한다. JEntry 슬롯을 예약하고, 각 엘리먼트를 변환하면(엘리먼트 페이로드가 버퍼에 추가되고 해당 엘리먼트의 길이가 하위 비트에 담긴 JEntry 반환), 스트라이드 경계마다 길이를 누적 오프셋으로 변환한다:

// convertJsonbArray — src/backend/utils/adt/jsonb_util.c (요약)
containerhead = nElems | JB_FARRAY;
if (val->val.array.rawScalar) containerhead |= JB_FSCALAR;
appendToBuffer(buffer, &containerhead, sizeof(uint32));
jentry_offset = reserveFromBuffer(buffer, sizeof(JEntry) * nElems);
totallen = 0;
for (i = 0; i < nElems; i++)
{
convertJsonbValue(buffer, &meta, &val->val.array.elems[i], level + 1);
totallen += JBE_OFFLENFLD(meta); /* 누적 데이터 크기 */
if (totallen > JENTRY_OFFLENMASK) ereport(ERROR, ...); /* 256 MB 상한 */
if ((i % JB_OFFSET_STRIDE) == 0) /* 32번째마다: 오프셋 저장 */
meta = (meta & JENTRY_TYPEMASK) | totallen | JENTRY_HAS_OFF;
copyToBuffer(buffer, jentry_offset, &meta, sizeof(JEntry));
jentry_offset += sizeof(JEntry);
}
*header = JENTRY_ISCONTAINER | (buffer->len - base_offset);

비교는 구조적이며 바이트 단위가 아니다. compareJsonbContainers는 두 문서를 동기화된 이터레이터(JsonbIteratorNext)로 순회하며 토큰 단위로 비교한다. 키 삽입 순서나 공백만 다른 두 jsonb 값이 같다고 판정된다. 이것이 jsonb가 B-tree와 GROUP BY에서 의미적 동등성을 갖고 참여할 수 있게 하는 이유다:

// compareJsonbContainers — src/backend/utils/adt/jsonb_util.c (요약)
ita = JsonbIteratorInit(a);
itb = JsonbIteratorInit(b);
do {
ra = JsonbIteratorNext(&ita, &va, false);
rb = JsonbIteratorNext(&itb, &vb, false);
if (ra == rb) {
if (ra == WJB_DONE) break; /* 결정적으로 동등 */
if (va.type == vb.type)
res = compareJsonbScalarValue(&va, &vb); /* 같은 타입 비교 */
else
res = (va.type > vb.type) ? 1 : -1; /* 타입 순서 */
}
else res = ...; /* 더 짧거나 구조적으로 다른 문서가 먼저 */
} while (res == 0);
flowchart TD
  TXT["jsonb 입력 텍스트"] -->|"jsonb_in / 파서"| JV["JsonbValue 트리<br/>(인메모리, jbvObject/Array/String/...)"]
  JV -->|"JsonbValueToJsonb<br/>convertJsonbValue 깊이 우선"| BIN["평탄한 Jsonb varlena<br/>JsonbContainer 헤더<br/>+ JEntry[] (길이, 32번째마다 오프셋)<br/>+ 페이로드"]
  BIN -->|"TOAST 압축 / 밀어내기"| DISK["온디스크 속성"]
  BIN -->|"JsonbIteratorNext"| ACCESS["키 조회 / 포함 검사"]
  BIN2["다른 jsonb"] --> CMP["compareJsonbContainers<br/>구조적, 순서 독립"]
  BIN --> CMP

배열: 선택적 null 비트맵이 있는 차원형 varlena

섹션 제목: “배열: 선택적 null 비트맵이 있는 차원형 varlena”

PostgreSQL 배열은 ArrayType 헤더에 차원 수, 엘리먼트 타입 OID, 그리고 null 비트맵 부재 시 정확히 0인 오프셋을 담는 varlena다. null 비트맵의 존재 여부가 그 단일 필드로 인코딩된다:

// ArrayType — src/include/utils/array.h
typedef struct ArrayType
{
int32 vl_len_; /* varlena 헤더 (직접 건드리지 말 것!) */
int ndim; /* # of dimensions */
int32 dataoffset; /* offset to data, or 0 if no null bitmap */
Oid elemtype; /* element type OID */
} ArrayType;
/* 뒤이어: int dims[ndim], int lbound[ndim], [null 비트맵], 엘리먼트 데이터 */
#define ARR_NDIM(a) ((a)->ndim)
#define ARR_HASNULL(a) ((a)->dataoffset != 0)
#define ARR_ELEMTYPE(a) ((a)->elemtype)

엘리먼트 타입이 OID로만 저장되기 때문에, generic 배열 코드는 팩된 데이터를 보폭 이동하기 위해 엘리먼트의 typlen/typbyval/typalign을 조회해야 한다. §“PostgreSQL의 접근 방식”의 카탈로그 주도 간접 참조가 그대로 적용된다. array_get_element가 정규 리더다. detoast하고, dims/lbound에 맞서 첨자를 검증하고, ArrayGetOffset으로 선형 offset을 계산하고, 엘리먼트 단위로 보폭 이동한다(엘리먼트가 가변 길이이거나 null 가능할 때 배열은 랜덤 접근이 불가하므로):

// array_get_element — src/backend/utils/adt/arrayfuncs.c (요약)
else /* 일반 평탄 배열 경우 */
{
ArrayType *array = DatumGetArrayTypeP(arraydatum); /* detoast */
ndim = ARR_NDIM(array);
dim = ARR_DIMS(array);
lb = ARR_LBOUND(array);
arraydataptr = ARR_DATA_PTR(array);
arraynullsptr = ARR_NULLBITMAP(array);
}
if (ndim != nSubscripts || ndim <= 0 || ndim > MAXDIM) { *isNull = true; return (Datum) 0; }
for (i = 0; i < ndim; i++)
if (indx[i] < lb[i] || indx[i] >= (dim[i] + lb[i])) { *isNull = true; return (Datum) 0; }
offset = ArrayGetOffset(nSubscripts, dim, lb, indx);
/* 이후 array_seek + fetch_att가 elmlen/elmbyval/elmalign과 null 맵을 고려 */

대량 소비자는 deconstruct_array로 팩된 형식을 한 번에 병렬 Datum/isnull C 배열로 풀어낸다. array_in/array_out{...} 텍스트 문법을 처리하며, 엘리먼트 타입의 I/O 함수로 재귀 호출한다 — 동일한 쿼텟이 한 레벨 아래에서 다시 작동한다. 확장 배열 기계(ExpandedArrayHeader, VARATT_IS_EXTERNAL_EXPANDED)는 반복 인플레이스 업데이트를 위한 최적화로, postgres-toast.md의 TOAST/확장 datum 이야기에 속한다.

ADT 라이브러리는 src/backend/utils/adt/ 아래에 타입 패밀리별 파일 하나씩으로 구성된다. 관통선은 항상 동일하다. pg_proc에 등록된 V1 함수(Datum f(PG_FUNCTION_ARGS))가 Datum 인자를 언팩하고, 타입 특화 작업을 하고, Datum을 다시 박싱해 돌려준다.

I/O 쿼텟과 varlena 배관 (varlena.c, varatt.h, fmgr.c)

섹션 제목: “I/O 쿼텟과 varlena 배관 (varlena.c, varatt.h, fmgr.c)”
  • textin / textout / textrecv / textsend — 참조 쿼텟; cstring_to_text* / text_to_cstringStringInfo 와이어 헬퍼(pq_getmsgtext)를 얇게 감싼다.
  • cstring_to_text_with_len — 생성자; SET_VARSIZE로 항상 전체 4-byte 헤더를 구축한다(datum은 “비토스트 상태로 태어난다”).
  • text_to_cstring — 소비자; pg_detoast_datum_packedVARDATA_ANY / VARSIZE_ANY_EXHDR, 언팩된 복사본이 입력과 다를 때만 해제한다.
  • VARSIZE_4B / VARSIZE_1B / VARSIZE_ANY_EXHDR / VARDATA_ANY (varatt.h) — 네 가지 물리 레이아웃을 호출자에게 균일하게 보이게 하는 엔디언 및 형식 디스패치 헤더 매크로.
  • pg_detoast_datum_packed / pg_detoast_datum (fmgr.c) — detoast 진입점; 압축/외부 datum에 한해 detoast_attr(access/common/detoast.c, postgres-toast.md 참조)로 위임.
  • varstr_cmp / varstrfastcmp_c — 콜레이션 인식 정렬과 C 로케일 memcmp SortSupport 빠른 경로.
  • NumericVar — 인메모리 산술 형식; init_var_from_num이 온디스크 NumericNumericVar로 보고, make_result / make_result_opt_error가 단축 vs 장형 헤더를 선택해 다시 팩한다.
  • numeric_in / numeric_out / numeric_recv / numeric_send — 쿼텟; 출력은 get_str_from_var를 거치고, 이진 I/O는 raw int16 자릿수를 전송한다.
  • add_var / add_abs / sub_abs / cmp_abs / mul_var — 가드 자릿수(MUL_GUARD_DIGITS)가 있는 자릿수 배열 교과서 산술.

datetime (date.c, timestamp.c, datetime.c)

섹션 제목: “datetime (date.c, timestamp.c, datetime.c)”
  • ParseDateTime / DecodeDateTime — 모든 시간 입력 함수가 공유하는 필드 토크나이저/디코더.
  • date2j / j2date — Julian-day 커널: (연, 월, 일) ↔ 일 번호, 그레고리력 산술을 정수 산술 저장에서 격리.
  • JsonbContainer / JEntry / JB_OFFSET_STRIDE — 이진 컨테이너 형식: 카운트+플래그 헤더, 길이-또는-오프셋 자식 배열, 팩된 페이로드.
  • JsonbValueToJsonb / convertToJsonb / convertJsonbValue / convertJsonbArray / convertJsonbObject / convertJsonbScalar — 인메모리 트리에서 평탄 이진으로의 깊이 우선 직렬화기.
  • getJsonbOffset / JsonbIteratorNext — 가장 가까운 저장 오프셋에서 최대 31개 길이 합산으로 랜덤 접근, 순서 순회.
  • compareJsonbContainers / compareJsonbScalarValue — 구조적, 순서 독립 비교.
  • ArrayTypeARR_* 매크로(ARR_NDIM, ARR_DIMS, ARR_LBOUND, ARR_DATA_PTR, ARR_NULLBITMAP, ARR_HASNULL, ARR_ELEMTYPE) — 헤더 레이아웃과 접근자.
  • array_in / array_out / array_recv — 텍스트 {...} 및 이진 I/O, 각 엘리먼트 타입 함수로 재귀.
  • array_get_element / ArrayGetOffset / deconstruct_array — 엘리먼트 접근과 Datum/isnull 배열로의 대량 분해.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
textinsrc/backend/utils/adt/varlena.c588
textoutsrc/backend/utils/adt/varlena.c599
textrecvsrc/backend/utils/adt/varlena.c610
cstring_to_text_with_lensrc/backend/utils/adt/varlena.c205
text_to_cstringsrc/backend/utils/adt/varlena.c226
varstr_cmpsrc/backend/utils/adt/varlena.c1666
varstrfastcmp_csrc/backend/utils/adt/varlena.c2121
VARSIZE_4B / VARSIZE_1Bsrc/include/varatt.h192 / 194
VARSIZE_ANY_EXHDRsrc/include/varatt.h317
VARDATA_ANYsrc/include/varatt.h324
pg_detoast_datumsrc/backend/utils/fmgr/fmgr.c1832
pg_detoast_datum_packedsrc/backend/utils/fmgr/fmgr.c1864
NumericVarsrc/backend/utils/adt/numeric.c313
numeric_insrc/backend/utils/adt/numeric.c637
numeric_outsrc/backend/utils/adt/numeric.c816
numeric_recvsrc/backend/utils/adt/numeric.c1078
numeric_sendsrc/backend/utils/adt/numeric.c1163
init_var_from_numsrc/backend/utils/adt/numeric.c7570
get_str_from_varsrc/backend/utils/adt/numeric.c7613
make_result_opt_errorsrc/backend/utils/adt/numeric.c7901
make_resultsrc/backend/utils/adt/numeric.c8010
add_varsrc/backend/utils/adt/numeric.c8550
mul_varsrc/backend/utils/adt/numeric.c8788
add_abssrc/backend/utils/adt/numeric.c11942
JsonbContainer / JEntrysrc/include/utils/jsonb.h190 / 136
JB_OFFSET_STRIDEsrc/include/utils/jsonb.h178
JsonbValueToJsonbsrc/backend/utils/adt/jsonb_util.c92
compareJsonbContainerssrc/backend/utils/adt/jsonb_util.c191
getJsonbOffsetsrc/backend/utils/adt/jsonb_util.c134
JsonbIteratorNextsrc/backend/utils/adt/jsonb_util.c859
convertJsonbValuesrc/backend/utils/adt/jsonb_util.c1603
convertJsonbArraysrc/backend/utils/adt/jsonb_util.c1628
convertJsonbObjectsrc/backend/utils/adt/jsonb_util.c1712
ArrayTypesrc/include/utils/array.h92
array_insrc/backend/utils/adt/arrayfuncs.c179
array_outsrc/backend/utils/adt/arrayfuncs.c1016
array_recvsrc/backend/utils/adt/arrayfuncs.c1271
array_get_elementsrc/backend/utils/adt/arrayfuncs.c1820
deconstruct_arraysrc/backend/utils/adt/arrayfuncs.c3631

아래의 모든 심볼, 상수, 코드 발췌는 2026-06-05 기준 REL_18_STABLE 작업 트리 커밋 273fe94에서 직접 읽은 것이다.

  • I/O 쿼텟은 실재하며 얇다. varlena.ctextin/textout/textrecv는 인용한 대로 몇 줄짜리 래퍼다. V1 ABI(PG_FUNCTION_ARGS, PG_GETARG_*, PG_RETURN_*)가 범용 시그니처다. 확인됨.
  • 네 가지 varlena 물리 형식. varatt.h가 4-byte(일반/압축), 1-byte 단축, 1B_E 외부/TOAST 포인터 형식을 정의하고, VARSIZE_ANY_EXHDR/VARDATA_ANY에 플래그 비트 디스패치가 있다. 단축 헤더는 126바이트(VARATT_SHORT_MAX 유래)로 상한이 있다. 확인됨.
  • Detoast 분리. pg_detoast_datum_packed(fmgr.c)는 VARATT_IS_COMPRESSED 또는 VARATT_IS_EXTERNAL이 아닌 한 입력을 그대로 반환하고, 아닌 경우 detoast_attr를 호출한다. 확인됨; 실제 페치/압축 해제는 access/common/detoast.c에 있다(postgres-toast.md에 위임).
  • numeric 기수. 실제 NBASE10000, DEC_DIGITS = 4, NumericDigit = int16; MUL_GUARD_DIGITS = 2. NumericVar 구조체와 short/long/special 온디스크 헤더 패밀리는 인용한 대로다. 확인됨.
  • make_result 표준화. make_result_opt_error는 선두 및 후미 0 자릿수를 제거하고, 영을 weight 0 / 양수로 표준화하며, NUMERIC_CAN_BE_SHORT일 때 단축 헤더를 선택한다. 확인됨.
  • jsonb 스트라이드. JB_OFFSET_STRIDE == 32; convertJsonbArray는 32번째마다 자식의 JEntryJENTRY_HAS_OFF 누적 오프셋으로 변환하고, 페이로드를 JENTRY_OFFLENMASK(0x0FFFFFFF, 256 MB)로 상한 제한한다. 확인됨.
  • 구조적 jsonb 비교. compareJsonbContainers는 두 JsonbIterator를 잠금 단계로 구동하므로 동등성이 순서 독립이다. 확인됨.
  • 배열 null 비트맵 인코딩. ARR_HASNULL(a)는 문자 그대로 ((a)->dataoffset != 0)이다. dataoffset이 0이 아닐 때만 null 비트맵이 있다. array_get_element는 detoast, 경계 검사, ArrayGetOffset으로 선형 오프셋을 계산한다. 확인됨.
  • 단축 헤더 다운변환이 정확히 어디서 일어나는가. 생성자는 전체 4-byte 헤더를 구축한다. 1-byte 단축 형식으로의 변환은 튜플 조립 시(heap_fill_tuple / fill_val 경로) 일어난다. 정확한 트리거와 typstorage와의 상호작용은 postgres-toast.md / postgres-heap-am.md 영역이다. 이 문서는 네 가지 형식이 존재한다는 것만 주장한다.
  • 인라인 압축에서 lz4 vs pglz 선택. va_tcinfo 상위 비트가 방법을 인코딩하지만, 기본 압축 GUC와 열별 ALTER ... SET COMPRESSION 경로는 TOAST 관심사이며 ADT 관심사가 아니다. 위임됨.
  • C 이외 콜레이션에서 약어 키 인코딩. varstrfastcmp_c가 C 로케일 빠른 경로다. ICU/libc 약어 키 변환기와 “도움이 안 되면 중단” 휴리스틱은 SortSupport/i18n 코드에 있으며 범위 밖이다.

PostgreSQL 너머 — 비교 설계와 연구 동향

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 동향”

객체-관계형 도박. PostgreSQL의 “타입은 카탈로그 행과 등록된 C 함수”는 The Design of POSTGRES(Stonebraker & Rowe 1986)와 What Goes Around Comes Around(Stonebraker & Hellerstein 2005; dbms-papers/goes-around.md)에 추적된 계보의 직계 후손이다. 객체-관계형 학파는 고정된 타입 시스템 대신 열린 타입 시스템을 선택했다. 그 결과가 오늘날 보인다 — PostGIS(geometry), pgvector(vector), citext, hstore는 모두 실행기 패치 없이 “그냥 타입”이다. 비용은 엄격한 V1 ABI와 모든 호출의 fmgr 간접 참조이며, 폐쇄형 시스템(예: 소수 타입을 하드코딩한 수작업 분석 엔진)은 이를 회피한다.

가변 길이 헤더 비교. “작은 값에 헤더를 줄이고 큰 것은 아웃오브라인으로” 패턴은 엔진 전반에서 반복된다. SQL Server는 인행 vs 행오버플로 vs LOB_DATA 할당 단위를 사용하고, Oracle은 인라인 VARCHAR2와 LOB 로케이터가 있는 아웃오브라인 LOB 세그먼트를 구분하며, MySQL/InnoDB는 긴 VARCHAR/BLOB 열을 페이지 밖에 20-byte 포인터로 저장한다. PostgreSQL의 1-byte 단축 헤더(비정렬 길이+플래그 바이트, 126바이트 상한)는 작은 문자열 일반 케이스에 특히 공격적이며, 실제 스키마의 대부분이 짧은 텍스트라는 점을 반영한다. 열 스토어는 이를 더 밀어붙인다. 사전/RLE/비트 팩 인코딩(C-Store/Vertica 계보, dbms-papers/column-vs-row.md)은 “길이”를 값별 헤더가 아닌 인코딩 자체에 내포시킨다.

십진 산술. Base-10000 교과서 산술은 관례적 선택이다(IBM decNumber, Java BigDecimal, 대부분의 엔진이 깔끔한 반올림과 텍스트 변환을 위해 10의 거듭제곱 기수를 쓴다). 하드웨어 십진(IEEE 754-2008 decimal floating point, POWER의 DFP 유닛)은 범용 엔진이 가지 않은 길이다. 이식성과 소프트웨어 자릿수 배열의 무제한 정밀도를 선호한다.

이진 JSON. 길이-또는-오프셋-스트라이드 기법은 모든 이진 JSON 형식이 직면하는 긴장에 대한 PostgreSQL의 답이다. MySQL 이진 JSON은 전체 오프셋 테이블을 저장해(빠른 접근, 낮은 압축성), 순수 길이 인코딩은 잘 압축되지만 인덱싱이 O(n)이다. 스트라이드는 조정 가능한 중간점이다. 분석 워크로드에서 JSON의 완전 컬럼형 분해가 단일 TOASTed blob을 이기는지를 탐색하는 연구(succinct/압축 반구조화 저장, JSON 열 스키마 추론 — JSON tiles, Sinew 등)가 계속된다. PostgreSQL의 “문서당 하나의 varlena”는 의도적으로 OLTP 친화적 쪽에 있다.

배열 vs 중첩 릴레이션. PostgreSQL의 엘리먼트 타입 OID를 갖는 평탄 차원형 배열은 SQL 표준 ARRAY를 단일 값으로 구현한 것이다. 대안 계보 — Oracle의 중첩 테이블/MULTISET, NF²(non-first-normal-form) 연구 전통 — 는 컬렉션을 1급 릴레이션으로 모델링한다. PostgreSQL은 제1정규형에 더 가깝게 머물며, unnest/array_agg 연산자가 배열과 행 사이를 잇는 다리 역할을 한다.

소스 트리 내 파일 (REL_18_STABLE, 커밋 273fe94)

섹션 제목: “소스 트리 내 파일 (REL_18_STABLE, 커밋 273fe94)”
  • src/backend/utils/adt/varlena.ctext/bytea I/O, varstr_cmp, SortSupport, *_to_text / text_to_cstring.
  • src/backend/utils/adt/numeric.c — 임의 정밀도 십진수: I/O, NumericVar, make_result, 자릿수 배열 산술.
  • src/backend/utils/adt/jsonb.c, src/backend/utils/adt/jsonb_util.cjsonb I/O, JsonbContainer/JEntry 이진 형식, 직렬화(convertJsonb*), 구조적 비교, 이터레이션.
  • src/backend/utils/adt/arrayfuncs.c — 배열 I/O, ArrayType 접근, array_get_element, deconstruct_array.
  • src/backend/utils/adt/date.c, src/backend/utils/adt/datetime.c — 시간 I/O와 date2j/j2date Julian 커널.
  • src/include/varatt.h — varlena 헤더 레이아웃과 VAR* 매크로.
  • src/include/utils/jsonb.h, src/include/utils/array.hjsonb와 배열 온디스크 구조체 및 접근자 매크로.
  • src/backend/utils/fmgr/fmgr.cpg_detoast_datum* detoast 진입점.
  • postgres-fmgr.md — V1 호출 규약, FmgrInfo, FunctionCallInfo, ADT 함수가 OID로 디스패치되는 방식.
  • postgres-toast.md — 아웃오브라인 저장, 인라인 압축(pglz/lz4), detoast_attr, 확장 datum.
  • postgres-nbtree.md, postgres-index-am.md — 이 타입들이 등록하는 비교/해시 함수를 연산자 클래스가 소비하는 방식.
  • postgres-overview-base-infra.md, postgres-overview-i18n-text.md — 주변 기본 인프라와 콜레이션/텍스트 컨텍스트.
  • dbms-papers/goes-around.mdWhat Goes Around Comes Around (객체-관계형 타입 시스템 계보).
  • research/dbms-general/database-system-concepts.md — 도메인, 타입, 관계형 타입 시스템 (4–5장).