통계 정보: 옵티마이저가 같은 SQL을 다르게 해석하는 이유
통계 정보와 옵티마이저 차이가 실행계획을 바꾸는 원리와 이관 후 대응을 정리합니다.
인덱스 편(11)의 결론에서 "옵티마이저는 DB마다 다르게 생각한다"고 했습니다. 그 차이는 어디서 오는가 — 비용 모델의 가중치, 조인 알고리즘의 편향, 인덱스 선택 휴리스틱이 있지만, 옵티마이저가 무엇을 보고 결정하는지의 입력 자체가 다르기 때문이기도 합니다.
그 입력이 바로 통계 정보입니다. 테이블 행 수, 컬럼별 값의 분포, 히스토그램, 다중 컬럼 상관관계, 인덱스의 물리 순서와 테이블 정렬도의 일치 정도, 시스템의 I/O 비용. 옵티마이저는 이 메타데이터를 보고 실행 계획을 만듭니다. 통계 정보가 부정확하거나 누락되면, 같은 SQL이 전혀 다른 계획으로 실행됩니다.
이관 시점에 통계 정보는 메타데이터로 인식되지 않는 경우가 많습니다. DDL에 적혀 있지 않고, 데이터 자체도 아니며, "데이터를 적재하면 알아서 따라오는 것"으로 여겨집니다. 하지만 통계는 적재되지 않습니다. 이관 직후에는 통계가 비어 있거나 부정확한 상태이고, 첫 번째 자동 수집이 돌기 전까지 옵티마이저는 잘못된 입력으로 계획을 만듭니다. 이 사이의 운영 안정성을 어떻게 확보하느냐가 이관 후 첫 며칠의 성능을 결정합니다.
이 글은 양방향, 즉 Oracle의 풍부한 통계 메타데이터를 오픈소스의 단순한 모델로 흡수하는 방향과, 반대로 오픈소스의 통계 모델이 Oracle로 옮겨질 때 새로 활용할 수 있는 옵션을 모두 다룹니다.
세 DB의 통계 정보 모델 — 같은 목적, 다른 모델
| 항목 | Oracle | PostgreSQL | MySQL (InnoDB) |
|---|---|---|---|
| 수집 명령 | DBMS_STATS.GATHER_* | ANALYZE | ANALYZE TABLE |
| 자동 수집 | 11g+ maintenance window 기본 활성화 | autovacuum이 분석 자동 트리거 | innodb_stats_auto_recalc (5.6.6+ 기본 ON) |
| 통계 저장 위치 | DBA_TAB_STATISTICS, DBA_TAB_COL_STATISTICS, DBA_HISTOGRAMS | pg_class, pg_statistic(pg_stats 뷰) | mysql.innodb_table_stats, mysql.innodb_index_stats |
| 샘플링 기본값 | AUTO_SAMPLE_SIZE (11g+ 근사 NDV 알고리즘) | default_statistics_target 기본 100 | innodb_stats_persistent_sample_pages 기본 20 |
| 히스토그램 | Frequency, Height-Balanced, Top-Frequency / Hybrid (12c+) | MCV + equi-depth 히스토그램 | 8.0+ singleton / equi-height |
| 확장 통계 | Extended Statistics (11g+; 컬럼 그룹·표현식) | CREATE STATISTICS (10+: ndistinct/dependencies, 12+: mcv, 14+: expression) | (없음) |
| 인덱스↔테이블 정렬도 | CLUSTERING_FACTOR (USER_INDEXES) | pg_stats.correlation (컬럼 단위) | (별도 통계 없음; 클러스터드 구조에 흡수) |
| 시스템 통계 | DBMS_STATS.GATHER_SYSTEM_STATS (CPU/IO) | seq_page_cost, random_page_cost 등 GUC | 별도 시스템 통계 객체 없음 (서버 변수 기반) |
| 통계 잠금/이력 | LOCK_TABLE_STATS, RESTORE_TABLE_STATS | (네이티브 부재; 백업 필요) | (네이티브 부재) |
| 실행 계획 안정화 | SQL Plan Baseline, SQL Profile, SQL Patch, Outline(legacy) | pg_hint_plan(외부 확장) 또는 GUC 조정 | Optimizer Hints (인라인) |
표가 보여주는 핵심은 세 가지입니다. 첫째, 수집 메커니즘은 비슷하지만 자동화 주체가 다릅니다. Oracle은 별도 maintenance window의 잡으로, PostgreSQL은 autovacuum 데몬으로, MySQL은 InnoDB가 변경량 기반으로 트리거합니다. 둘째, 히스토그램·확장 통계·인덱스 정렬도 통계의 표현력이 비대칭입니다. 같은 목적을 다른 방식으로 풀어내며, 일부는 다른 DB에 대응 자체가 없습니다. 셋째, 실행 계획 안정화는 Oracle이 압도적으로 풍부합니다. SQL Plan Baseline 같은 객체가 다른 DB에는 직접 대응이 없습니다.
수집 메커니즘 — 누가 언제 통계를 모으는가
Oracle은 11g부터 자동 통계 수집이 기본 활성화되어 있습니다. 평일 밤과 주말의 maintenance window 동안 DBMS_STATS.GATHER_DATABASE_STATS_JOB_PROC이 돌며 변경량이 임계치를 넘은 객체를 분석합니다. 샘플링은 AUTO_SAMPLE_SIZE가 기본인데, 11g 이후 자동 샘플링 알고리즘(HLL 계열 근사 NDV 추정 등)이 도입되어 전수 스캔에 가까운 정확도를 더 낮은 비용으로 얻도록 동작합니다. 운영자는 잡 스케줄을 조정하거나, 특정 객체에 다른 옵션(METHOD_OPT, ESTIMATE_PERCENT 등)을 설정해 커스터마이즈합니다.
PostgreSQL은 autovacuum 데몬이 분석을 함께 처리합니다. autovacuum_analyze_scale_factor(기본 0.1, 즉 10% 변경 시 트리거)와 autovacuum_analyze_threshold로 시점이 결정됩니다. 샘플 크기는 컬럼별 default_statistics_target(기본 100)에 비례하며, 기본값에서는 대략 수만 행 수준의 샘플을 기반으로 통계를 만듭니다. 큰 테이블이나 분포가 까다로운 컬럼은 ALTER TABLE ... ALTER COLUMN ... SET STATISTICS N으로 컬럼별 조정합니다. autovacuum이 꺼져 있거나 임계치를 넘지 못한 테이블의 통계는 갱신되지 않습니다.
MySQL InnoDB는 Persistent Statistics가 5.6.6부터 기본입니다(innodb_stats_persistent = ON). innodb_stats_auto_recalc가 켜져 있으면 테이블 변경량이 10%를 넘을 때 자동 재계산하고, 결과는 mysql.innodb_table_stats/mysql.innodb_index_stats에 영구 저장됩니다. 샘플링 페이지 수는 innodb_stats_persistent_sample_pages(기본 20)로 결정되며, 카디널리티 추정이 부정확한 큰 테이블에서는 이 값을 늘려야 하는 경우가 많습니다.
Oracle → 오픈소스 방향에서 가장 흔한 사고가 자동 수집 메커니즘의 격차에서 나옵니다. Oracle은 maintenance window에 차분히 정밀한 통계를 모으는 운영 관행에 익숙해져 있지만, PostgreSQL autovacuum은 변경량 임계치 기반이라 변경이 적은 테이블의 통계는 영원히 갱신되지 않을 수도 있습니다. 대용량 로그 테이블, 거의 변하지 않는 마스터 테이블에서 이 격차가 드러납니다. MySQL은 샘플 페이지 기본값(20)이 작아, 큰 테이블의 카디널리티 오차가 Oracle보다 크게 잡히는 경우가 잦습니다.
오픈소스 → Oracle 방향에서는 반대로 자동 수집의 정밀도가 올라가지만, PostgreSQL/MySQL에서 운영자가 명시적으로 조정해 두었던 컬럼별 통계 타깃은 Oracle 매핑이 다릅니다. PostgreSQL의 SET STATISTICS 1000 같은 컬럼별 설정은 Oracle에서는 DBMS_STATS.SET_TABLE_PREFS 또는 METHOD_OPT 인자로 옮겨야 합니다.
통계의 내용 — 히스토그램, 확장 통계, 시스템 통계
세 DB가 모으는 기본 통계(행 수, 컬럼별 distinct 값 수, NULL 비율, 평균 길이)는 비슷합니다. 차이가 큰 영역은 히스토그램·확장 통계·시스템 통계입니다.
히스토그램 — 컬럼 값의 분포가 균일하지 않을 때, 옵티마이저가 특정 값의 선택도를 정확히 추정하기 위한 메타데이터입니다.
Oracle은 네 종류의 히스토그램을 가집니다. Frequency(distinct 값이 적을 때, 모든 값을 그대로 저장), Height-Balanced(distinct가 많을 때, 동일 깊이 버킷), 그리고 12c부터 추가된 Top-Frequency(상위 N개 값 + 나머지)와 Hybrid(빈도 + 깊이의 혼합). 12c 이후 옵티마이저는 데이터 분포를 보고 가장 적합한 유형을 자동 선택합니다.
PostgreSQL은 두 축으로 나뉩니다. Most Common Values(MCV) — 가장 자주 등장하는 값들과 그 빈도를 저장 — 와 equi-depth 히스토그램 — 나머지 값의 분포를 동일 깊이 버킷으로 분할. default_statistics_target 값이 곧 MCV 슬롯 수이자 히스토그램 버킷 수입니다. Oracle의 Frequency는 PostgreSQL MCV로, Height-Balanced는 PostgreSQL 히스토그램으로 대응되지만 Top-Frequency/Hybrid에 정확히 대응되는 단일 구조는 없습니다.
MySQL은 8.0에서 히스토그램을 도입했습니다. ANALYZE TABLE ... UPDATE HISTOGRAM ON col WITH N BUCKETS로 명시적으로 만들고, 자동 갱신되지 않습니다. singleton(distinct가 적을 때)과 equi-height(많을 때) 두 종류를 지원합니다. 따라서 MySQL에서 히스토그램을 사용하는 경우, 데이터 분포가 바뀌는 테이블에 대해 ANALYZE TABLE ... UPDATE HISTOGRAM을 정기 운영 절차에 포함해야 합니다. 인덱스 카디널리티 외의 컬럼 통계가 약했던 5.x 시절의 한계를 일부 보완하지만, 자동성은 여전히 부족합니다.
확장 통계(Extended Statistics) — 단일 컬럼 통계로는 표현되지 않는 컬럼 간 상관관계와 표현식 통계입니다.
Oracle은 11g+ Extended Statistics로 컬럼 그룹(('col1','col2'))이나 표현식(UPPER(col))에 대한 통계를 만들 수 있습니다. 예를 들어 country='KR' 조건과 city='서울' 조건이 함께 들어올 때, 두 컬럼이 강한 상관관계를 가지면 독립 가정 기반의 선택도 추정이 빗나갑니다. 컬럼 그룹 통계가 이 문제를 해결합니다.
PostgreSQL은 10에서 CREATE STATISTICS를 도입했습니다. 10에서는 ndistinct(다중 컬럼 distinct)와 dependencies(함수적 종속성), 12에서는 mcv(다중 컬럼 MCV), 14에서는 표현식 통계가 추가되었습니다. 명시적으로 생성하지 않으면 만들어지지 않습니다.
MySQL은 다중 컬럼/표현식 통계가 사실상 없습니다. 함수 인덱스나 생성 컬럼(generated column)을 인덱스로 만들어 우회하는 정도가 한계입니다.
시스템 통계 — 옵티마이저가 비용을 계산할 때 쓰는 CPU/IO 비용 모델입니다.
Oracle은 DBMS_STATS.GATHER_SYSTEM_STATS로 실제 워크로드 기간 동안 측정한 CPU 속도, 단일 블록 읽기 시간, 다중 블록 읽기 시간을 수집해 비용 계산에 반영합니다. PostgreSQL은 seq_page_cost(기본 1.0), random_page_cost(기본 4.0), cpu_tuple_cost 같은 GUC 파라미터로 동일한 역할을 합니다. 다만 PostgreSQL은 실제 측정 기반의 자동 갱신이 아니라 운영자가 환경에 맞게 직접 조정하는 모델입니다. SSD/NVMe 환경에서는 random_page_cost를 기본값보다 낮게 조정하는 경우가 많지만, 실제 스토리지 지연시간과 워크로드를 기준으로 검증해야 합니다. MySQL은 통합된 시스템 통계 객체가 없고, 옵티마이저 비용 상수는 서버 변수와 mysql.server_cost/mysql.engine_cost 테이블(5.7+)로 일부 조정합니다.
시스템 통계는 이관되지 않습니다. 하드웨어와 워크로드 특성이 달라지면 비용 모델도 다시 정해져야 합니다. Oracle → 오픈소스 방향에서 SSD/NVMe 환경으로 옮기면서 PostgreSQL의 기본 random_page_cost = 4.0을 그대로 두면, 옵티마이저는 디스크가 느리다고 가정한 채 인덱스를 회피하는 결정을 자주 내립니다. 이관 후 옵티마이저 파라미터 튜닝이 별도 단계로 들어가야 하는 이유입니다.
Clustering Factor — 인덱스가 선택되지 않는 가장 큰 이유
인덱스가 있는데도 옵티마이저가 풀스캔을 고르는 현상의 단골 원인이 인덱스 순서와 테이블 물리 순서의 괴리입니다. 인덱스를 따라 한 행씩 테이블에 접근할 때, 같은 인덱스 키 근처의 행들이 테이블에서도 가까이 있으면 한 블록 안에서 여러 행을 동시에 읽을 수 있어 비용이 낮지만, 멀리 흩어져 있으면 행마다 별도 블록을 읽어야 해서 비용이 급격히 부풀려집니다. 이 정렬도를 정량화한 통계가 세 DB에 모두 존재하지만, 표현 방식과 비용 모델에서의 위상이 다릅니다.
Oracle의 CLUSTERING_FACTOR(USER_INDEXES/DBA_INDEXES)는 인덱스 키 순서대로 테이블을 훑었을 때 블록이 바뀌는 횟수입니다. 값이 테이블 행 수에 가까우면 거의 모든 행이 다른 블록에 흩어져 있다는 뜻이고, 블록 수에 가까우면 인덱스 순서와 테이블 순서가 잘 정렬되어 있다는 뜻입니다. 옵티마이저는 인덱스 범위 스캔 비용을 계산할 때 이 값을 직접 곱해 사용하므로, 같은 선택도라도 clustering factor가 나쁜 인덱스는 풀스캔보다 비싸게 평가됩니다. 즉 통계 수집 직후 인덱스가 선택되지 않는다면, 카디널리티가 아니라 clustering factor를 먼저 봐야 하는 경우가 많습니다.
PostgreSQL의 pg_stats.correlation(컬럼별 통계)은 같은 개념을 다른 방식으로 표현합니다. -1에서 +1 사이의 값으로, 컬럼 값의 논리 순서와 테이블의 물리 행 순서 사이의 통계적 상관관계를 나타냅니다. 1에 가까우면 잘 정렬된 상태, 0에 가까우면 무작위 분포입니다. 옵티마이저는 이 값을 인덱스 스캔의 random I/O 비율을 추정하는 데 사용합니다. PostgreSQL에는 인덱스 단위가 아니라 컬럼 단위로 저장된다는 점, 그리고 CLUSTER 명령으로 테이블을 한 인덱스 순서로 재정렬할 수 있다는 점이 Oracle과 다릅니다. 다만 CLUSTER는 일회성이며 이후 INSERT/UPDATE는 정렬을 유지하지 않으므로(저장구조 편에서 다룬 대로), 시간이 지나면 correlation은 다시 떨어집니다.
MySQL InnoDB는 별도 clustering factor 통계가 없습니다. 모든 테이블이 PK 기반 클러스터드 인덱스 구조이기 때문에, secondary index 접근 비용이 본질적으로 PK 클러스터링에 묶여 있습니다. Secondary index에서 PK를 얻고, PK로 다시 클러스터드 인덱스를 탐색하는 두 단계가 강제되며, 이 비용 자체가 옵티마이저의 인덱스 선택 결정에 반영됩니다. 그래서 InnoDB에서 인덱스가 선택되지 않을 때는 카디널리티와 PK 길이(저장구조 편의 논의), 그리고 secondary index의 커버링 여부를 같이 봐야 합니다.
Oracle → PostgreSQL 방향에서 clustering factor 격차가 만드는 함정이 있습니다. Oracle에서 PK 단조 증가 패턴으로 자연스럽게 좋은 clustering factor를 유지하던 테이블이 PostgreSQL로 이관되면, 초기 적재 직후에는 correlation이 1에 가깝지만 운영이 진행되면서 떨어집니다. PostgreSQL은 MVCC 특성상 UPDATE가 새 위치에 행을 쓰므로 정렬이 빠르게 무너집니다. 이관 직후 잘 돌던 SQL이 몇 주 후 갑자기 풀스캔으로 바뀌는 패턴의 상당수가 여기서 옵니다.
대응은 여러 갈래이며 비용이 모두 다릅니다. VACUUM FULL과 CLUSTER는 정렬을 정확히 회복하지만 테이블 전체 rewrite와 ACCESS EXCLUSIVE 락을 동반하므로 정기 운영책으로는 위험합니다. 운영 중 테이블이 차단되며, 큰 테이블에서는 점검 윈도우에 한정해 신중하게 계획해야 합니다. 운영 중에도 정렬을 정리해야 한다면 pg_repack 같은 외부 확장으로 락 시간을 최소화하는 방법이 일반적입니다. 다만 가장 권장되는 접근은 사후 정리보다 설계 차원의 예방입니다 — BRIN 같은 정렬도 친화 인덱스를 처음부터 선택하거나, PK를 단조 증가 패턴으로 유지하거나, 파티셔닝으로 정렬도가 무너지는 영역을 격리하면 사후 rewrite의 필요성 자체가 줄어듭니다.
Oracle → MySQL 방향에서는 clustering factor라는 개념 자체가 사라지지만, PK 설계가 그 자리를 대신합니다. InnoDB에서는 PK가 데이터 정렬 순서를 결정하므로, 자주 함께 조회되는 컬럼을 PK에 묶거나 PK 값 패턴을 단조 증가로 유지하는 결정이 곧 clustering factor 관리에 해당합니다. 이 관점은 저장구조 편(08)과 인덱스 편(11)의 PK 설계 논의로 이어집니다.
실행 계획 안정화 — Plan Baseline의 부재가 만드는 결정
Oracle은 실행 계획을 객체로 관리하는 풍부한 메커니즘을 가집니다.
- SQL Plan Baseline(11g+): 검증된 실행 계획을 저장하고, 새 계획이 더 빠른지 확인된 경우에만 자동 교체. 통계가 갱신되어도 계획이 흔들리지 않게 보호합니다.
- SQL Profile(10g+): 옵티마이저가 잘못 추정하는 카디널리티를 보정하는 메타데이터.
- SQL Patch(11g+): 특정 SQL에 힌트를 적용.
- Stored Outline(legacy, 11g에서 deprecated): SQL Plan Baseline의 전신.
이 메커니즘들은 DBA가 운영 중 실행 계획을 객체로 다룰 수 있게 합니다. "이 SQL은 이 계획으로만 돌아라"를 보장하거나, "통계가 부정확하니 카디널리티를 N으로 가정하라"를 강제할 수 있습니다.
PostgreSQL과 MySQL에는 이에 직접 대응하는 객체가 없습니다. PostgreSQL은 외부 확장 pg_hint_plan이 SQL 주석에 힌트를 넣어 일부 역할을 흉내 내고, GUC 파라미터(enable_seqscan, enable_nestloop 등)로 옵티마이저 동작을 세션 단위로 끌 수 있습니다. MySQL은 SQL 안에 인라인 옵티마이저 힌트(/*+ INDEX(...) */, /*+ NO_INDEX_MERGE(...) */ 등)를 넣어 계획을 강제합니다.
Oracle → 오픈소스 방향에서 가장 큰 운영 충격이 여기서 옵니다. Oracle에서 운영 중 발견된 plan regression을 SQL Plan Baseline으로 고정해 두었던 시스템이라면, 그 안정성이 사라집니다. 이관 후 통계가 새로 잡히고 옵티마이저가 다른 계획을 선택하는 순간, 동일 SQL이 다른 성능으로 돌게 됩니다.
대응 방향은 우선순위가 분명합니다. Oracle에서 Plan Baseline이 걸려 있던 SQL은 타겟 DB에서 동일 계획으로 자동 변환할 수 없습니다. 대신 critical SQL 목록으로 분류하고, 타겟 DB의 실행 계획을 별도로 검증한 뒤 다음 순서로 안정화합니다.
첫째, 통계 정밀도부터 점검합니다. 옵티마이저가 잘못된 계획을 고르는 가장 흔한 원인은 카디널리티 오차이고, 그 답은 보통 컬럼별 통계 타깃 상향(PostgreSQL SET STATISTICS), 확장 통계 추가(컬럼 그룹/표현식), 히스토그램 정밀도 조정에 있습니다. 둘째, 인덱스 설계를 다시 봅니다. 누락된 인덱스, 잘못된 인덱스 선두 컬럼, clustering factor 악화 같은 구조적 원인을 먼저 해결합니다. 셋째, 그래도 안 풀리면 시스템 통계/비용 파라미터를 조정합니다. SSD 환경의 random_page_cost, effective_cache_size 같은 파라미터가 환경과 맞지 않으면 모든 SQL이 영향을 받습니다. 힌트는 위 세 단계로도 해결되지 않을 때 쓰는 최후 수단입니다. 인라인 힌트는 SQL에 박혀 옵티마이저의 자율적 개선을 차단하고, 통계가 좋아진 뒤에도 강제된 계획을 따라가게 만듭니다. 운영 부담이 가장 큰 선택이라는 점을 잊지 않아야 합니다.
이관 직후 며칠은 평소보다 촘촘한 plan regression 모니터링이 필수입니다. PostgreSQL은 pg_stat_statements로 SQL별 실행 통계를 수집하고, MySQL은 Performance Schema의 events_statements_summary_by_digest를 활용합니다.
오픈소스 → Oracle 방향에서는 반대로 Plan Baseline을 새로 도입할 기회가 됩니다. 오픈소스에서 SQL 인라인 힌트로 통제하던 계획들을, Oracle에서는 SQL Plan Baseline으로 코드 수정 없이 고정할 수 있습니다. 이관 시점에 baseline을 어떤 정책으로 운영할지 결정해 두면 운영 안정성이 크게 올라갑니다.
이관 직후 통계 동기화 — 적재가 끝났다고 통계가 따라오지 않는다
데이터 이관이 끝나고 첫 SQL이 들어오기 전, 반드시 거쳐야 할 절차가 통계 수집입니다. 데이터 적재만으로는 통계가 자동으로 잡히지 않습니다. Oracle은 직접 경로 적재(direct path insert)나 CTAS 같은 일부 경로에서 통계를 함께 생성하지만, 일반 INSERT 경로로 적재된 데이터는 통계가 비어 있습니다. PostgreSQL은 적재 직후의 테이블은 pg_class.reltuples가 0으로 남아 있어 옵티마이저가 "이 테이블은 비어 있다"고 판단하고 잘못된 계획을 만듭니다. MySQL InnoDB는 자동 재계산이 동작하기까지 시간이 걸립니다.
따라서 적재 직후 다음 순서로 통계를 정렬해 두어야 합니다.
첫째, 모든 적재 대상 테이블에 대해 통계 수집을 일괄 실행합니다. Oracle은 DBMS_STATS.GATHER_SCHEMA_STATS, PostgreSQL은 ANALYZE [VERBOSE](데이터베이스 전체) 또는 vacuumdb --analyze-only, MySQL은 스키마 전체에 대한 ANALYZE TABLE 일괄 실행입니다.
둘째, 확장 통계와 히스토그램은 별도로 정의합니다. Oracle Extended Statistics, PostgreSQL CREATE STATISTICS, MySQL 히스토그램은 자동 수집 대상이 아니거나 명시적 생성이 필요합니다. 소스 시스템에 정의되어 있던 것들을 추출해 타겟에 다시 만들고, 그 후에 한 번 더 통계를 수집해야 채워집니다.
셋째, 시스템 통계(또는 옵티마이저 비용 파라미터)를 환경에 맞게 조정합니다. 앞서 다룬 random_page_cost처럼 하드웨어가 달라졌다면 기본값으로는 부족합니다. 운영 안정화 기간의 실측 데이터를 기반으로 조정합니다.
넷째, 자동 수집 임계치를 검토합니다. 변경이 적은 테이블이 통계 갱신에서 빠지지 않도록 autovacuum_analyze_scale_factor를 테이블별로 낮추거나(PostgreSQL), innodb_stats_auto_recalc를 끄고 명시적 잡으로 관리(MySQL)하는 결정이 필요합니다.
Oracle 통계 자체를 옮기는 것도 선택지입니다. DBMS_STATS.EXPORT_TABLE_STATS / IMPORT_TABLE_STATS로 통계만 export/import 할 수 있습니다. 다만 이관이 Oracle → Oracle이 아니라면 이 경로는 쓸 수 없습니다. 그리고 데이터가 함께 옮겨진 뒤의 통계는 어차피 새로 수집해야 신뢰할 수 있으므로, 동종 마이그레이션이 아니라면 새 환경에서 재수집하는 것이 표준입니다.
결론
통계 정보는 메타데이터의 마지막 층이자 가장 자주 잊히는 층입니다. DDL에 적혀 있지 않고, 데이터도 아니며, 이관 도구가 자동으로 옮겨주는 카테고리도 아닙니다. 하지만 옵티마이저가 결정을 내리는 모든 입력이 여기에 있습니다.
방향에 관계없이 다섯 가지 질문으로 수렴됩니다.
첫째, 이관 직후 통계 수집을 일괄 수행하는 절차가 마련되어 있는가. 그리고 첫 SQL이 들어오기 전에 그 수집이 완료되는가.
둘째, 소스 시스템에 정의되어 있던 히스토그램·확장 통계(Oracle Extended Statistics, PostgreSQL CREATE STATISTICS, MySQL 히스토그램) 가 타겟 DB에서 어떤 메커니즘으로 재현되는가. 자동 수집 대상이 아닌 항목은 명시적으로 생성·재수집되었는가.
셋째, 인덱스 접근 비용을 결정하는 인덱스↔테이블 정렬도 통계(Oracle clustering factor, PostgreSQL pg_stats.correlation)의 상태는 어떠한가. 이관 후 시간이 지나면서 정렬이 무너질 때의 대응 정책 — 사후 rewrite(VACUUM FULL/CLUSTER는 강한 락 동반, 점검 윈도우 한정 / 운영 중 적용은 pg_repack 같은 외부 확장 검토)보다 설계 차원의 예방(BRIN 같은 정렬도 친화 인덱스, PK 단조 증가, 파티셔닝) — 이 정해져 있는가.
넷째, 시스템 통계 또는 옵티마이저 비용 파라미터(random_page_cost 등)가 새 하드웨어 환경에 맞게 조정되었는가. SSD/NVMe로 옮기면서 디스크 비용 기본값을 그대로 두지 않았는가.
다섯째, Oracle에서 SQL Plan Baseline·SQL Profile로 고정해 두었던 계획은 타겟 DB에서 어떤 우선순위(통계 정밀도 → 인덱스 설계 → 비용 파라미터 → 최후 수단으로 힌트)로 안정화되는가. 그리고 이관 후 plan regression을 감지할 모니터링 체계(pg_stat_statements, Performance Schema)가 첫 SQL 이전에 활성화되어 있는가.
이 질문에 답이 나오기 전에 운영을 시작하면, 데이터는 다 옮겨졌지만 옵티마이저는 잘못된 입력으로 계획을 만들고, 사용자는 "이상하게 느려졌다"는 형태로 그 결과를 마주합니다. 인덱스가 있어도 선택되지 않고, JOIN 순서가 뒤집히고, Full Table Scan이 갑자기 더 싸 보이는 — 인덱스 편(11)에서 예고했던 옵티마이저 차이의 상당 부분이 사실은 통계 격차, 그중에서도 카디널리티 오차와 clustering factor 악화에서 발생합니다.
13편에 걸쳐 데이터 타입·캐릭터셋·저장구조·권한·제약조건·채번·인덱스·내부 로직·통계까지, 메타데이터의 모든 층을 살펴봤습니다. 시리즈 전체를 가로지르는 통합 정리와 메타데이터 변환의 본질에 대한 회고는 별도의 마무리 글로 이어집니다.
