About VATOS
home
Company
home

techlog_8

← Tech Insights

저장구조: 같은 테이블이 같은 모양으로 놓이지 않는다

테이블스페이스·세그먼트 등 저장구조가 DB마다 다르게 놓이는 이유와 대응법을 정리합니다.

앞 글에서 USER가 묶고 있던 "누구의 것인가"를 분해했습니다. 그런데 USER가 함께 들고 있던 메타데이터가 하나 더 있습니다. DEFAULT TABLESPACE와 QUOTA — "어디에 놓일 것인가"입니다. 객체의 소유자가 결정되면, 그 객체가 어떤 물리 공간에 어떻게 놓일지가 따라옵니다.

그런데 저장구조 이관은 USER에 묶여 있던 TABLESPACE 기본값을 재배치하는 것에 그치지 않습니다. 세 DB가 "테이블을 저장한다"는 작업 자체를 다르게 정의하기 때문입니다. Oracle은 풍부한 저장 옵션을 제공하고, PostgreSQL은 Heap 중심의 단순한 기본형을 유지하면서 일부 저장 옵션을 조정하는 구조이고, MySQL InnoDB는 모든 테이블을 PK 기반 클러스터드 인덱스로 만듭니다. 같은 테이블이 같은 모양으로 놓이지 않습니다.

이 글은 양방향, 즉 Oracle의 풍부한 저장 메타데이터를 오픈소스의 단순한 모델로 흡수하는 방향과, 반대로 오픈소스의 단순한 저장 결정을 Oracle의 다층 옵션으로 재설계하는 방향 모두를 다룹니다.


세 DB의 저장구조 — 같은 단어, 다른 단위

개념OraclePostgreSQLMySQL (InnoDB)
논리 저장 영역TABLESPACE 기반 (모든 Segment를 TABLESPACE에 저장)TABLESPACE (선택)System / file-per-table / General Tablespace
물리 단위DATAFILERelation file / segment file (TABLESPACE는 디렉토리 매핑)file-per-table 기본 (.ibd)
테이블 기본형HeapHeapClustered Index (PK 강제)
인덱스 조직 테이블IOT(없음, CLUSTER는 일회성 정렬)InnoDB 자체가 Clustered
파티셔닝별도 옵션 (라이센스 정책 확인 필요)10+ Range/List, 11+ Hash5.1+ (FK 미지원 등 제약 큼)
압축Basic / Advanced(OLTP) / HCCTOAST + 14 LZ4InnoDB Compression / Page Compression

이 표가 보여주는 것은 단순한 기능 매트릭스가 아닙니다. 저장구조에 대한 결정권이 DB마다 누구에게 있는지를 보여줍니다. Oracle은 설계자가 결정합니다. TABLESPACE 분리, IOT 선택, 파티셔닝, 압축 옵션이 모두 설계 결정의 결과입니다. 오픈소스는 일정 부분을 DB가 결정합니다. PostgreSQL은 Heap이 강제되고, MySQL InnoDB는 클러스터드 구조가 강제됩니다. 설계자가 선택할 수 있는 폭이 좁은 만큼, 설계 단순성과 운영 일관성이 따라옵니다.


TABLESPACE — 같은 단어, 다른 무게감

Oracle에서 TABLESPACE는 핵심 자원입니다. 모든 객체는 어떤 TABLESPACE에든 속해야 하고, USER마다 DEFAULT TABLESPACE와 TEMP TABLESPACE, QUOTA가 정의됩니다. 운영 환경에서는 데이터와 인덱스를 다른 TABLESPACE에 분리하고, 대용량 테이블을 전용 TABLESPACE로 빼고, LOB도 별도 TABLESPACE에 둡니다. 12c+ Multitenant에서는 각 PDB가 자체 TABLESPACE 셋을 가지므로 격리 단위가 한 층 더 명확해집니다.

PostgreSQL의 TABLESPACE는 디렉토리 매핑에 가깝습니다. 객체 생성 시 명시하지 않으면 pg_default에 저장되며, 별도 TABLESPACE 사용은 선택입니다. 테이블별, 인덱스별로 다른 TABLESPACE 지정이 가능하지만, Oracle처럼 다층 분리를 강제하는 운영 관행은 일반적이지 않습니다.

MySQL InnoDB의 저장 구조는 여러 종류의 tablespace로 구성됩니다. 시스템 tablespace(ibdata1)는 5.7 기준 데이터 딕셔너리와 doublewrite buffer 같은 공용 구조를 담았고(8.0부터는 데이터 딕셔너리가 InnoDB 트랜잭셔널 DD인 mysql.ibd로, doublewrite buffer가 8.0.20+에서 별도 .dblwr 파일로 각각 분리되었습니다), file-per-table tablespace는 각 테이블이 자체 .ibd 파일을 갖는 기본 모드이며, General Tablespace(5.7+)는 여러 테이블을 하나의 공유 tablespace에 묶을 수 있는 선택지입니다. 운영 환경에서는 대부분 file-per-table 모드가 기본이어서, OS 파일 시스템 자체가 분리 단위 역할을 합니다.

Oracle → 오픈소스 방향에서는 다중 TABLESPACE 분리가 어떤 의도로 만들어져 있었는지를 먼저 파악해야 합니다. 과거 분리 디스크 시대에 강조되었던 데이터/인덱스 I/O 분산 의도는, ASM이나 Exadata, 외부 스토리지 가상화 환경에서는 실제 I/O 분산 효과가 약합니다. 실질적인 가치는 공간 통제, 백업·복구 단위 분리, 객체 수명 주기 관리(예: 과거 파티션을 별도 TABLESPACE로 옮긴 뒤 read-only로 전환) 쪽으로 옮겨가 있습니다. 따라서 TABLESPACE 이름만 옮기지 말고, 각 분리의 현재 의도가 무엇인지부터 정리하고, 그 의도를 타겟 DB의 어떤 메커니즘(파티션 단위 백업, 별도 디렉토리, OS 파일 시스템 등)으로 재현할지 결정해야 합니다.

오픈소스 → Oracle 방향에서는 반대 결정이 필요합니다. 단일 영역에 저장되던 객체들을 Oracle TABLESPACE 설계로 다시 분리할 것인지, 단일 TABLESPACE에 모아둘 것인지를 결정해야 합니다. 분리의 목적이 명확히 있을 때만 분리하는 것이 합리적이며, 이관 직후에는 단순 구조로 시작해 운영 안정화 후 필요한 분리를 단계적으로 도입하는 접근도 가능합니다. 어느 쪽이든 USER의 DEFAULT TABLESPACE와 QUOTA가 새로 설계되어야 하므로, 앞 글에서 정리한 USER 분류 결과를 입력으로 삼아야 합니다.


테이블 기본형 — Heap, IOT, Clustered Index

테이블이 디스크에 어떤 모양으로 놓이는지는 DB마다 다릅니다. 이 차이는 PK 설계, Secondary Index 비용, 쓰기 성능 모두에 직접 영향을 미칩니다.

Oracle은 Heap이 기본입니다. 테이블에 행이 들어오는 순서대로 저장되고, PK는 별도의 B-tree 인덱스로 관리됩니다. 행의 물리 주소는 ROWID로 식별됩니다. 이 구조에서는 PK가 길어져도 Secondary Index 비용이 늘지 않습니다.

Oracle의 IOT(Index-Organized Table)는 옵션입니다. 테이블 전체를 PK B-tree 안에 저장합니다. PK로 조회가 잦은 코드성 테이블이나 룩업 테이블에서 효과가 큽니다. 다만 비-PK 컬럼이 많거나 변경이 잦으면 단점이 더 큽니다. Overflow Segment 설계와 PCTTHRESHOLD 조정 같은 별도 메타데이터도 따라붙습니다.

PostgreSQL은 Heap만 제공합니다. CLUSTER 명령으로 테이블을 특정 인덱스 순서로 정렬할 수 있지만, 일회성 정렬일 뿐 이후 INSERT/UPDATE는 정렬을 유지하지 않습니다. Oracle IOT에 직접 대응되는 기능은 없습니다.

MySQL InnoDB는 모든 테이블이 PK 기준 클러스터드 인덱스 구조입니다. PK가 없으면 UNIQUE NOT NULL 인덱스를, 그것도 없으면 6바이트 숨겨진 ROW_ID를 강제로 생성해 클러스터링 키로 씁니다. Secondary Index의 리프 노드에는 PK 값 전체가 복사됩니다. 이 때문에 InnoDB로 가는 경우 PK 길이가 Secondary Index 전체 크기에 직결되며, 그 영향은 뒤이어 인덱스 편에서 다시 살펴봅니다.

Oracle → 오픈소스 방향에서 가장 큰 영향을 받는 것은 Oracle IOT를 사용하던 테이블입니다. PostgreSQL로 가면 Heap + PK 인덱스로 흡수되면서 PK 조회 성능이 떨어집니다. PostgreSQL 11부터 INCLUDE 절로 커버링 인덱스를 만들 수 있고, autovacuum과 visibility map이 잘 관리되면 Index Only Scan으로 IOT에 근접한 성능을 낼 수 있지만, 완전한 대체는 아닙니다. MySQL로 가는 경우 InnoDB의 클러스터드 구조가 자연스럽게 IOT 비슷한 효과를 내지만, Oracle IOT처럼 명시적으로 설계한 것이 아니라 모든 테이블에 강제되는 구조라는 점이 다릅니다.

오픈소스 → Oracle 방향에서는 반대로 클러스터드 구조에 의존하던 패턴이 사라집니다. MySQL InnoDB에서 PK 순서로 데이터가 자연 정렬되어 있어 ORDER BY pk 조회가 빠르던 코드가, Oracle Heap에서는 자연 정렬이 깨집니다. 정렬이 필요하면 별도 인덱스나 명시적 ORDER BY가 필요합니다. PostgreSQL Heap에서 Oracle Heap으로 가는 경우는 구조 차이가 작아 직접 영향은 적지만, Oracle IOT를 활용한 신규 설계 기회는 여기서 검토할 수 있습니다.


파티셔닝 — 같은 이름, 다른 구현

파티셔닝은 세 DB에 모두 존재하지만, 구현은 모두 다릅니다.

Oracle Partitioning은 일반적으로 별도 옵션으로 제공됩니다. 다만 에디션, 클라우드 서비스, 시점별 라이센스 정책에 따라 적용 범위가 달라질 수 있으므로, 도입 검토 시에는 자사 라이센스 계약 기준으로 확인하는 것이 안전합니다. 기능 측면에서는 Range, List, Hash, Composite, Interval, Reference, System 등 풍부한 파티션 타입과 Global Index, Local Index를 모두 지원합니다. PARTITION EXCHANGE로 대량 데이터를 메타데이터 변경만으로 교체할 수 있고, Interval Partitioning은 신규 파티션을 자동 생성합니다.

PostgreSQL은 10부터 선언적 파티셔닝을 도입했습니다. 10에서는 Range와 List를 지원했고, Hash 파티셔닝은 11부터 추가되었습니다. Global Index가 없습니다. 모든 인덱스가 파티션별로 분리되어 생성됩니다. PRIMARY KEY나 UNIQUE 제약을 걸려면 파티션 키가 제약 컬럼에 반드시 포함되어야 합니다. 이 제약은 Oracle에서 자유롭게 사용하던 PK 설계를 깨뜨립니다.

MySQL의 파티셔닝은 5.1.6에서 도입되어 Range, List, Hash, Key를 지원하며, 8.0부터는 InnoDB native partitioning으로 재구현되었습니다. 제약은 큽니다. Foreign Key를 파티션 테이블에서 사용할 수 없고, 파티션 키는 모든 UNIQUE 키의 일부여야 합니다. Global Index도 없습니다.

항목OraclePostgreSQLMySQL
라이센스별도 옵션 (정책 확인 필요)Core 기능Core 기능
도입 / 주요 시점오래된 버전부터 지원, 후속 버전에서 타입 확장10+ (Range/List), 11+ (Hash)5.1+, 8.0에서 native 통합
지원 타입Range/List/Hash/Composite/Interval/Reference/SystemRange/List/HashRange/List/Hash/Key
Global Index지원미지원미지원
PK/UNIQUE에 파티션 키 강제불필요필요필요
Foreign Key지원부분 지원 (12+)미지원
신규 파티션 자동 생성Interval(수동 또는 확장)(수동)

Oracle → 오픈소스 방향에서 파티셔닝 이관은 단순 매핑이 어렵습니다. 첫째, Interval이나 Reference 같은 Oracle 고유 파티션 타입은 직접 대응이 없어 자동화 스크립트나 애플리케이션 로직으로 보완해야 합니다. 둘째, Global Index에 의존하던 PK/UNIQUE 제약은 파티션 키를 포함하는 형태로 재설계해야 합니다. 셋째, MySQL로 가는 경우 파티션 테이블의 FK가 사라지므로 무결성 통제 방식을 재검토해야 합니다. 넷째, PARTITION EXCHANGE로 처리하던 대량 적재 패턴은 PostgreSQL의 ATTACH PARTITION으로 대체할 수 있지만, 트랜잭션과 락 동작이 다르므로 운영 절차를 다시 설계해야 합니다.

오픈소스 → Oracle 방향에서는 라이센스 정책 확인이 첫 결정입니다. Partitioning 옵션을 도입하지 못하는 환경이라면 파티션 테이블이 일반 테이블로 흡수되어야 하고, 그러면 운영 SQL과 배치의 파티션 의존 코드(PARTITION 절, EXCHANGE 등)를 모두 손봐야 합니다. 도입할 수 있다면 오히려 Global Index와 풍부한 파티션 타입을 활용해 새로운 파티션 전략을 설계할 기회가 됩니다.


압축 — 기능보다 운영 영향

압축 옵션은 세 DB 모두 제공합니다. 하지만 실무에서 더 중요한 것은 압축 기능이 데이터 변경 패턴과 어떻게 맞물리는가입니다.

Oracle은 Basic Compression(읽기 전용 워크로드용), Advanced/OLTP Compression(별도 옵션), HCC(Oracle 엔지니어드 스토리지 전용 — Exadata, ZFS Storage Appliance 등)를 제공합니다. PostgreSQL은 TOAST가 대용량 컬럼을 자동 압축하고, 14부터 압축 알고리즘으로 LZ4를 선택할 수 있습니다. MySQL InnoDB는 Table Compression(KEY_BLOCK_SIZE)과 5.7+ Transparent Page Compression을 제공합니다.

방향과 무관하게 압축 이관 시 검토해야 할 지점은 같습니다.

첫째, 압축률이 데이터 변경 빈도와 반비례합니다. 자주 UPDATE/DELETE되는 테이블에서는 압축 효율이 급격히 떨어지고, 페이지 단위 재압축 비용이 추가됩니다. Oracle OLTP Compression이 이 문제를 부분적으로 해결하지만, 다른 DB에서는 압축 대상 테이블 선택 자체를 신중하게 해야 합니다.

둘째, 백업/복구와 복제 성능에 미치는 영향입니다. 압축된 데이터를 디스크에서 읽는 비용은 줄지만 CPU 사용량은 늘어납니다. 복제 환경에서 변경 데이터를 전송할 때 압축 형태가 어떻게 처리되는지는 도구마다 다릅니다.

셋째, 하드웨어 플랫폼에 종속된 압축은 이관 시 사라집니다. Oracle 엔지니어드 시스템의 HCC에 의존해 대용량 데이터를 작은 공간에 담고 있던 시스템이 일반 스토리지의 오픈소스 환경으로 가면 디스크 사용량이 급증할 수 있습니다. 압축률을 사전에 측정하고 스토리지 용량 계획에 반영해야 합니다.


LOB 저장 — 저장구조 사고가 가장 많이 나는 지점

LOB은 저장구조 이관에서 가장 사고가 자주 발생하는 영역입니다. 데이터 타입 자체의 변환은 LOB 편(04편)에서 다뤘지만, LOB이 어디에 어떻게 저장되는지는 별도의 메타데이터이고, 이관 시 가장 자주 누락됩니다.

Oracle은 LOB을 위한 풍부한 저장 메타데이터를 가집니다. LOB은 별도의 LOB Segment와 LOB Index를 가지며, 별도 TABLESPACE에 분리할 수 있습니다. 11g부터 권장되는 SecureFile은 BasicFile보다 압축, 중복 제거(DEDUPLICATION), 암호화 옵션을 제공합니다. STORAGE IN ROW 옵션은 LOB이 4000바이트 이하일 때 인라인 저장할지 여부를 결정하고, CHUNK 크기는 LOB I/O 단위를 결정합니다. PCTVERSION/RETENTION은 LOB의 undo 보존 정책을 제어합니다.

PostgreSQL은 TOAST(The Oversized-Attribute Storage Technique)로 대용량 컬럼을 자동 처리합니다. 약 2KB를 넘는 데이터는 자동으로 별도의 TOAST 테이블에 저장됩니다. 다만 PostgreSQL TOAST는 Oracle처럼 LOB 컬럼별 TABLESPACE, CHUNK, RETENTION을 세밀하게 지정하는 모델이 아니며, 사용자가 LOB Segment를 별도 관리 단위로 직접 설계하기 어렵습니다. 컬럼 단위로 STORAGE 속성(PLAIN/EXTERNAL/EXTENDED/MAIN)을 조정해 압축·외부 저장 여부를 바꾸는 정도가 한계입니다. PostgreSQL의 Large Object(pg_largeobject)는 별도 메커니즘이지만 운영 부담이 커서 실무에서는 보통 bytea/text + TOAST를 사용합니다. 14부터는 TOAST 압축 알고리즘으로 LZ4를 선택할 수 있습니다.

MySQL InnoDB의 BLOB/TEXT 저장 방식은 ROW_FORMAT에 따라 달라집니다. REDUNDANT/COMPACT 포맷은 768바이트까지 인라인 저장하고 나머지는 off-page에 저장하며, 5.7부터 기본 포맷이 DYNAMIC으로 바뀌었고(COMPRESSED는 옵트인) 이 포맷은 20바이트 포인터만 인라인 저장하고 전부 off-page에 저장합니다. file-per-table 모드라면 LOB 데이터도 같은 .ibd 파일 안에 저장되고, General Tablespace에 LOB을 분리해 두기는 사실상 어렵습니다.

Oracle → 오픈소스 방향에서 가장 큰 위험은 SecureFile의 풍부한 옵션이 사라진다는 점입니다. DEDUPLICATION에 의존해 중복 LOB을 단일 복사본으로 저장하고 있던 시스템이 오픈소스로 이관되면 디스크 사용량이 몇 배로 늘어날 수 있습니다. 사전에 SecureFile DEDUPLICATION/COMPRESSION이 적용된 LOB의 실제 저장 크기와 논리 크기의 차이를 측정해 두지 않으면, 타겟 환경의 스토리지 용량 계획이 크게 어긋납니다. 별도 LOB TABLESPACE로 분리해 두었던 의도(공간 통제, 백업 단위 분리, read-only 전환) 역시 PostgreSQL에서는 직접 재현이 어렵습니다. TOAST는 LOB을 사용자가 관리할 별도 단위로 노출하지 않기 때문입니다.

오픈소스 → Oracle 방향에서는 LOB 저장 옵션을 새로 설계할 기회가 됩니다. 신규 시스템에서는 SecureFile이 권장 기본값이고, BasicFile은 레거시 호환 외에는 선택할 이유가 거의 없습니다. STORAGE IN ROW, CHUNK 크기, 별도 TABLESPACE, 압축·중복 제거 옵션 같은 의사결정이 모두 이관 시점의 설계 영역으로 들어옵니다. 이 결정을 도구의 기본값에 맡기면, 운영 안정화 후 LOB I/O 패턴이 비효율적이라는 사실을 뒤늦게 발견하게 됩니다.

LOB 저장구조 이관에서 자주 빠지는 것은 LOB 컬럼이 실제로 얼마나 사용되고 있는지에 대한 측정입니다. 정의만 LOB일 뿐 실제로는 NULL이 대부분이거나 4000바이트 이하 데이터가 대다수인 컬럼이라면, 이관 시점에 LOB이 아닌 일반 가변 문자형으로 재정의하는 것이 운영 부담을 크게 줄입니다. 이 판단은 LOB 편에서 다룬 LOB 데이터 타입 결정과 맞물려 함께 진행되어야 합니다.


결론

저장구조 이관은 객체의 위치를 옮기는 작업이 아닙니다. 객체가 놓이는 자리와 모양, 그리고 그 자리를 설계할 권한이 누구에게 있는지가 모두 바뀝니다. Oracle은 설계자에게 풍부한 옵션을 주고 그만큼 결정의 책임도 줍니다. 오픈소스는 결정의 폭이 좁은 대신 운영 일관성을 강제합니다. 어느 쪽으로 옮기든, 한쪽의 결정이 다른 쪽에서는 다른 방식의 결정으로 번역되어야 합니다.

방향에 관계없이 다섯 가지 질문으로 수렴됩니다.

첫째, 소스 시스템의 TABLESPACE 분리(또는 통합)는 어떤 의도(공간 통제, 백업·복구 단위, 객체 수명 주기, 그리고 일부 경우 I/O 분산)로 설계되었고, 타겟 DB에서 그 의도를 어떻게 재현할 것인가.

둘째, 테이블 기본형 차이(Heap ↔ IOT ↔ Clustered Index)가 PK 설계와 Secondary Index 비용에 어떤 영향을 주는가. 특히 InnoDB로 가는 경우 PK 길이 재검토가 필요한 테이블은 어디인가.

셋째, 파티셔닝 의존 SQL은 어디에 얼마나 있고, 타겟 DB의 제약(Global Index 부재, PK/FK 제약, 파티션 키 강제)을 어떻게 견딜 것인가.

넷째, 압축 정책이 어떤 테이블의 어떤 데이터 변경 패턴과 맞물려 있고, 타겟에서 같은 효율을 낼 수 있는가. 특히 하드웨어 종속 압축에 의존하던 경우 스토리지 용량 계획이 다시 필요하지 않은가.

다섯째, LOB 컬럼의 저장 메타데이터(SecureFile/BasicFile, DEDUPLICATION, STORAGE IN ROW, 별도 TABLESPACE)가 어떤 의도로 설계되어 있고, 타겟에서 그 의도를 얼마나 재현할 수 있는가. 그리고 LOB 컬럼이 실제로 LOB이어야 하는가.

이 질문에 답이 나오기 전에 DDL을 옮기기 시작하면, 객체는 만들어지지만 저장 의도는 사라진 상태가 됩니다. 그리고 사라진 의도는 운영 중 I/O 병목, 인덱스 비대화, 파티션 정합성 문제, 그리고 가장 흔하게는 LOB 스토리지 폭증으로 돌아옵니다.

저장 형태의 마지막 조각이 정리되면, 이제 데이터가 따라야 할 규칙으로 넘어갑니다. 다음 글에서는 제약조건과 DEFAULT — 데이터가 들어오고 변하는 동안 그 의미가 어떻게 지켜지는지 — 를 살펴봅니다.

← 목록으로 돌아가기