위장 테이블
위장 테이블 (Imposter Tables)
1. 소개 (Introduction)
위장 테이블(imposter table)은 인덱스와 같은 b-tree에 부착된 테이블이에요. 위장 테이블은 인덱스의 내용을 마치 그 인덱스가 일반 테이블인 것처럼 질의할 수 있게 해줘요.
출처: Imposter Tables
본문
위장 테이블은 분석과 디버깅 전용으로 만들어졌어요. 대부분의 애플리케이션 개발자가 이해해야 하거나 심지어 알아야 할 기능은 아니에요.
위장 테이블은 기본적으로 읽기 전용이에요. 그리고 읽기 전용인 한은 해롭지 않아요. 하지만 PRAGMA writable_schema=ON이 켜져 있으면 위장 테이블에 쓸 수 있어요. 이는 인덱스 손상을 초래할 수 있으며, 이렇게 만들어진 손상은 REINDEX를 실행해 고칠 수 있어요.
2. 상세 내용 (Details)
SQLite의 각 테이블과 각 인덱스는 데이터베이스 파일의 별도 b-tree에 저장돼요. 각 b-tree는 루트 페이지 번호로 식별돼요. 어떤 인덱스나 테이블의 루트 페이지 번호는 sqlite_schema 테이블의 "rootpage" 컬럼을 질의해 찾을 수 있어요. 이 설계에 대한 추가 배경은 인덱싱 튜토리얼과 파일 형식 문서를 참조하세요.
보통 테이블과 인덱스의 b-tree는 약간 달라요. 테이블 b-tree는 64비트 정수 키와 임의의 데이터를 포함해요. 64비트 정수 키가 ROWID예요. 인덱스 b-tree는 임의의 바이너리 키를 포함하고 데이터는 없어요. 그래서 테이블 b-tree와 인덱스 b-tree는 직접 호환되지 않아요.
하지만 WITHOUT ROWID 테이블의 b-tree는 인덱스 b-tree와 같은 형식이에요. 따라서 인덱스 b-tree는 마치 WITHOUT ROWID 테이블인 것처럼 접근할 수 있어요.
2.1. 수동으로 만든 위장 테이블 (Manually Created Imposter Tables)
위장 테이블을 만드는 한 가지 방법은 새로운 행을 삽입하도록 sqlite_schema 테이블을 직접 편집해 그 테이블을 설명하는 것이에요. 예를 들어 스키마가 다음과 같다고 가정해 봅시다:
CREATE TABLE t1(a INTEGER PRIMARY KEY,b TEXT,c INT, d INT);
CREATE INDEX t1bc ON t1(b,c);
t1bc 인덱스와 같은 구조를 가진 WITHOUT ROWID 테이블은 다음과 같을 거예요:
CREATE TABLE t2(b TEXT,c INT,a INT, PRIMARY KEY(b,c,a)) WITHOUT ROWID;
인덱스 "t1bc"에 대한 영구적인 위장 테이블 "t2"를 만들려면 먼저 "PRAGMA writable_schema=ON"를 실행해 sqlite_schema 테이블의 편집을 활성화해야 해요. (이 PRAGMA에 딸린 경고를 잘 살펴보세요. 실수는 심각한 데이터베이스 손상을 일으킬 수 있어요.) 그런 다음 sqlite_schema 테이블에 다음과 같이 새 항목을 삽입해요:
INSERT INTO sqlite_schema(type,name,tbl_name,rootpage,sql)
SELECT 'table','t2','t2',rootpage,
'CREATE TABLE t2(b,c,a,PRIMARY KEY(b,c,a))WITHOUT ROWID'
FROM sqlite_schema
WHERE name='t1bc';
위 INSERT 문은 인덱스 "t1bc"와 같은 디스크 형식을 갖고 같은 b-tree를 가리키는 테이블 "t2"를 정의하는 새 행을 sqlite_schema 테이블에 추가해요. 이 sqlite_schema 테이블 항목을 추가한 후에는 SQLite가 스키마를 다시 읽도록 데이터베이스를 닫고 다시 열어야 해요. 그러면 "t2" 테이블을 질의해 "t1bc" 인덱스의 내용을 볼 수 있어요.
2.1.1. 손상된 데이터베이스 (Corrupted Database)
위에서 설명한 수동 위장 테이블 방식의 심각한 문제는 "sqlite_schema" 테이블에 새 "t2" 항목을 추가한 후 데이터베이스 파일이 기술적으로 손상된다는 점이에요. "t1bc" 인덱스와 "t2" 테이블 모두 같은 b-tree를 가리킬 거예요. 이는 즉각적인 문제를 일으키지 않지만, VACUUM 실행은 피해야 해요.
PRAGMA writable_schema가 켜져 있으면 "t2" 테이블에 쓸 수 있어서 인덱스의 내용을 바꿀 수 있어요. 그렇게 하면 "t1bc" 인덱스가 부모 테이블 "t1"과 동기화되지 않게 돼요. 동기화되지 않은 인덱스는 잘못된 질의 결과를 초래할 수 있어요.
"t2" 위장 테이블이 데이터베이스 손상의 한 형태이므로, 위장 테이블을 만드는 수동 방식은 권장되지 않아요. 사실 전문 개발자를 제외한 모든 사람에게 위장 테이블 사용은 권장되지 않지만, 수동으로 만든 위장 테이블은 영구적이기 때문에 특히 더 권장되지 않아요.
2.2. 일시적인 위장 테이블 (Transient Imposter Tables)
위장 테이블을 만드는 또 다른 (더 안전한) 접근 방식은 디스크의 "sqlite_schema" 테이블을 업데이트하지 않고 SQLite의 내부 심볼 테이블에 위장 테이블 항목을 추가하는 것이에요. 그렇게 하면 위장 테이블은 단일 데이터베이스 연결에만 존재하고 스키마가 다시 로드될 때마다 자동으로 제거돼요.
일시적인 위장 테이블의 생성은 특수한 sqlite3_test_control() 호출을 수반해요. 다른 모든 SQLite API와 달리 sqlite3_test_control() 인터페이스는 릴리스마다 호환되지 않는 변경이 적용될 수 있으므로, 아래에서 설명하는 메커니즘은 SQLite의 향후 릴리스에서 작동한다는 보장이 없어요. SQLite 개발자들은 위장 테이블이 애플리케이션에서 사용되어서는 안 되기 때문에 이것을 문제로 여기지 않아요. 위장 테이블은 분석과 테스트 용도로만 사용돼요.
일시적인 위장 테이블을 만들려면 먼저 다음과 같이 sqlite3_test_control()을 호출해요:
sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, "main", 1, tnum);
"db" 매개변수는 데이터베이스 연결에 대한 포인터예요. "main" 인자는 위장 테이블이 생성될 스키마의 이름이에요. "1" 인자는 위장 테이블 메커니즘을 활성화해요. 인자가 "1" 대신 "2"이면 읽기 전용 위장 테이블이 만들어져요. "tnum"은 위장 테이블이 미러링해야 할 인덱스의 루트 페이지예요.
위 sqlite3_test_control() 호출 후, 위장 테이블을 정의하는 CREATE TABLE 문을 실행해요. 위장 메커니즘이 활성화된 상태에서 이 CREATE TABLE 문은 실제 테이블을 만들지 않고 SQLite의 내부 심볼 테이블에 항목만 추가해요. CREATE TABLE 문이 인덱스에 맞는 올바른 형식이어야 한다는 점에 유의하세요. 위장 테이블이 잘못된 컬럼 수를 가지거나 WITHOUT ROWID 테이블이 아니거나 다른 방식으로 인덱스 b-tree와 호환되지 않으면, 위장 테이블이 사용될 때 SQLITE_CORRUPT 오류가 발생해요.
CREATE TABLE 문을 실행한 후 다음과 같이 위장 메커니즘을 비활성화해요:
sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, db, "main", 0, 0);
즉 마지막 두 매개변수를 0으로 바꾸는 것 외에는 같은 sqlite3_test_control() 호출을 해요.
위장 테이블이 위에서 설명한 대로 SQLite의 내부 스키마에 로드된 후에는, 위장 테이블을 다른 테이블처럼 사용할 수 있어요. 하지만 위장 테이블은 그것을 만든 하나의 데이터베이스 연결에만 보여요. 디스크의 데이터베이스 파일에는 아무 변화도 없어요. 그리고 위장 테이블은 다음에 스키마가 로드될 때 사라져요.
2.3. .imposter 셸 명령 (The .imposter Shell Command)
명령줄 셸 (command-line shell)에는 일시적인 위장 테이블을 설정하는 모든 작업을 수행하는 도트 명령 ".imposter"가 들어 있어요. sqlite3_test_control()에 여러 번 호출하고 호환되는 CREATE TABLE 문을 알아내고 호출하는 대신, 일시적인 위장 테이블을 다음과 같이 만들 수 있어요:
.imposter t1bc t2
물론 예제에 보이는 "t1bc"와 "t2" 대신 원하는 인덱스와 위장 테이블 이름을 대입하세요. ".imposter" 명령은 "t1bc" 인덱스의 스키마를 읽고, 그 정보를 사용해 위장 테이블용 호환 CREATE TABLE 문을 만든 다음, 일시적인 위장 테이블을 자동으로 만들기 위한 모든 필요한 호출을 수행해요.
".imposter" 명령은 SQLite 버전 3.51.0 (2025-11-04) 이상에서 기본적으로 작동하며, 만들어진 위장 테이블은 읽기 전용이에요. 이전 버전의 CLI에서 ".imposter" 명령을 활성화하거나 읽기-쓰기 위장 테이블을 만들려면 --unsafe-testing 명령줄 옵션을 사용하세요.
3. 요약과 마지막 경고 (Summary And Final Warning)
위장 테이블 메커니즘은 SQLite를 위한 강력한 분석 및 디버깅 도구예요. 하지만 모든 날카로운 도구가 그렇듯이, 위장 테이블도 위험할 수 있고 잘못 사용하면 손상된 데이터베이스 파일을 초래할 수 있어요. 애플리케이션에서 위장 테이블을 사용하려고 하지 마세요. 위장 테이블은 전문가들이 실험실에서 사용하도록 만들어진 거예요.