SQLite 파일 형식 2
SQLite 파일 형식 2 (SQLite File Format)
SQLite 데이터베이스 파일이 디스크에 어떤 구조로 저장되는지, 페이지, 데이터베이스 헤더, b-tree, 레코드 형식, 롤백 저널, WAL 의 형식을 다루는 문서예요.
본문
1. 데이터베이스 파일 (The Database File)
SQLite 데이터베이스의 완전한 상태는 보통 "메인 데이터베이스 파일(main database file)"이라고 하는 디스크의 단일 파일에 담겨 있어요.
트랜잭션 중에 SQLite 는 "롤백 저널(rollback journal)"이라는 두 번째 파일에 추가 정보를 저장하거나, WAL 모드에서는 write-ahead log 파일에 저장해요.
1.1. 핫 저널 (Hot Journals)
애플리케이션 또는 호스트 컴퓨터가 트랜잭션이 완료되기 전에 크래시하면, 롤백 저널이나 write-ahead log 에 메인 데이터베이스 파일을 일관된 상태로 복원하는 데 필요한 정보가 담겨 있어요. 롤백 저널이나 write-ahead log 가 데이터베이스 상태를 복구하는 데 필요한 정보를 담고 있을 때 이를 "핫 저널(hot journal)" 또는 "핫 WAL 파일"이라고 불러요. 핫 저널과 WAL 파일은 에러 복구 시나리오에서만 발생하므로 흔하지 않지만, SQLite 데이터베이스의 상태의 일부이므로 무시할 수 없어요. 이 문서는 롤백 저널과 write-ahead log 파일의 형식을 정의하지만, 초점은 메인 데이터베이스 파일에 있어요.
1.2. 페이지 (Pages)
메인 데이터베이스 파일은 하나 이상의 페이지로 구성돼요. 페이지 크기는 512 에서 65536 사이(양끝 포함)의 2 의 거듭제곱이에요. 같은 데이터베이스 안의 모든 페이지는 크기가 같아요. 데이터베이스 파일의 페이지 크기는 데이터베이스 파일 시작 부분에서 16 바이트 오프셋에 있는 2바이트 정수로 결정돼요.
페이지는 1 부터 번호가 매겨져요. 최대 페이지 번호는 4294967294 (2^32 - 2)예요. 최소 크기 SQLite 데이터베이스는 단일 512바이트 페이지예요. 최대 크기 데이터베이스는 65536 바이트 페이지당 4294967294 페이지, 즉 281,474,976,579,584 바이트(약 281 테라바이트)예요. 보통 SQLite 는 자체 내부 크기 제한에 도달하기 훨씬 전에 기본 파일시스템이나 디스크 하드웨어의 최대 파일 크기 제한에 도달해요.
일반적인 사용에서 SQLite 데이터베이스는 몇 킬로바이트에서 몇 기가바이트까지 범위를 가지는 경향이 있지만, 테라바이트 크기의 SQLite 데이터베이스가 운영 환경에 존재하는 것으로 알려져 있어요.
메인 데이터베이스의 모든 페이지는 어느 시점에서든 다음 중 하나의 단일 용도를 가져요:
- b-tree 페이지
- 테이블 b-tree 내부 페이지
- 테이블 b-tree 리프 페이지
- 인덱스 b-tree 내부 페이지
- 인덱스 b-tree 리프 페이지
- 프리리스트(freelist) 페이지
- 프리리스트 트렁크 페이지
- 프리리스트 리프 페이지
- 페이로드 오버플로우 페이지
- 포인터 맵 페이지
- 잠금 바이트 페이지
메인 데이터베이스 파일에 대한 모든 읽기와 쓰기는 페이지 경계에서 시작하고, 모든 쓰기는 정수 개의 페이지 크기예요. 읽기도 보통 정수 개의 페이지 크기인데, 한 가지 예외는 데이터베이스를 처음 열 때 데이터베이스 파일의 처음 100바이트(데이터베이스 파일 헤더)를 부분 페이지 크기 단위로 읽는다는 점이에요.
1.3. 데이터베이스 헤더 (The Database Header)
데이터베이스 파일의 처음 100바이트가 데이터베이스 파일 헤더를 구성해요. 데이터베이스 파일 헤더는 아래 표가 보여주는 것처럼 필드로 나뉘어요. 데이터베이스 파일 헤더의 모든 다중 바이트 필드는 가장 중요한 바이트 먼저(big-endian) 저장돼요.
데이터베이스 헤더 형식
| Offset | Size | Description |
|---|---|---|
| 0 | 16 | The header string: "SQLite format 3\000" |
| 16 | 2 | The database page size in bytes. Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536. |
| 18 | 1 | File format write version. 1 for legacy; 2 for WAL. |
| 19 | 1 | File format read version. 1 for legacy; 2 for WAL. |
| 20 | 1 | Bytes of unused "reserved" space at the end of each page. Usually 0. |
| 21 | 1 | Maximum embedded payload fraction. Must be 64. |
| 22 | 1 | Minimum embedded payload fraction. Must be 32. |
| 23 | 1 | Leaf payload fraction. Must be 32. |
| 24 | 4 | File change counter. |
| 28 | 4 | Size of the database file in pages. The "in-header database size". |
| 32 | 4 | Page number of the first freelist trunk page. |
| 36 | 4 | Total number of freelist pages. |
| 40 | 4 | The schema cookie. |
| 44 | 4 | The schema format number. Supported schema formats are 1, 2, 3, and 4. |
| 48 | 4 | Default page cache size. |
| 52 | 4 | The page number of the largest root b-tree page when in auto-vacuum or incremental-vacuum modes, or zero otherwise. |
| 56 | 4 | The database text encoding. A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be. |
| 60 | 4 | The "user version" as read and set by the user_version pragma. |
| 64 | 4 | True (non-zero) for incremental-vacuum mode. False (zero) otherwise. |
| 68 | 4 | The "Application ID" set by PRAGMA application_id. |
| 72 | 20 | Reserved for expansion. Must be zero. |
| 92 | 4 | The version-valid-for number. |
| 96 | 4 | SQLITE_VERSION_NUMBER |
1.3.1. 매직 헤더 문자열
모든 유효한 SQLite 데이터베이스 파일은 다음 16바이트(16진수)로 시작해요: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00. 이 바이트 시퀀스는 끝에 nul 종결자 문자를 포함하는 UTF-8 문자열 "SQLite format 3" 에 해당해요.
1.3.2. 페이지 크기
오프셋 16 에서 시작하는 2바이트 값이 데이터베이스의 페이지 크기를 결정해요. SQLite 3.7.0.1 (2010-08-04) 및 이전 버전의 경우 이 값은 big-endian 정수로 해석되며 512 에서 32768 사이(양끝 포함)의 2 의 거듭제곱이어야 해요. SQLite 버전 3.7.1 (2010-08-23) 부터는 65536 바이트 페이지 크기가 지원돼요. 65536 값은 2바이트 정수에 맞지 않으므로, 65536 바이트 페이지 크기를 지정하려면 오프셋 16 의 값이 0x00 0x01 이에요. 이 값은 big-endian 1 로 해석될 수 있고, 65536 페이지 크기를 나타내는 매직 넘버로 생각할 수 있어요. 또는 이 2바이트 필드를 little-endian 숫자로 보고 페이지 크기를 256 으로 나눈 값을 나타낸다고 말할 수도 있어요. 페이지 크기 필드에 대한 이 두 해석은 동등해요.
1.3.3. 파일 형식 버전 번호
오프셋 18 과 19 의 파일 형식 쓰기 버전과 파일 형식 읽기 버전은 미래 SQLite 버전에서 파일 형식을 개선할 수 있게 하기 위한 것이에요. 현재 SQLite 버전에서 이 두 값은 롤백 저널링 모드에서는 1, WAL 저널링 모드에서는 2예요. 현재 파일 형식 사양으로 코딩된 SQLite 버전이 읽기 버전이 1 또는 2 이지만 쓰기 버전이 2 보다 큰 데이터베이스 파일을 만나면, 그 데이터베이스 파일은 읽기 전용으로 취급되어야 해요. 읽기 버전이 2 보다 큰 데이터베이스 파일을 만나면 그 데이터베이스는 읽거나 쓸 수 없어요.
1.3.4. 페이지당 예약 바이트
SQLite 는 확장 기능이 사용할 수 있도록 모든 페이지 끝에 소량의 추가 바이트를 남겨둘 수 있어요. 예를 들어 이 추가 바이트는 SQLite Encryption Extension 이 각 페이지와 연관된 nonce 및/또는 암호화 체크섬을 저장하는 데 사용해요. 오프셋 20 의 1바이트 정수에 있는 "예약 공간" 크기는 확장 기능을 위해 각 페이지 끝에 예약할 바이트 수예요. 이 값은 보통 0이에요. 값은 홀수가 될 수 있어요.
데이터베이스 페이지의 "사용 가능 크기(usable size)"는 헤더의 오프셋 16 의 2바이트 정수가 지정하는 페이지 크기에서 헤더의 오프셋 20 의 1바이트 정수에 기록된 "예약" 공간 크기를 뺀 값이에요. 페이지의 사용 가능 크기는 홀수일 수 있어요. 하지만 사용 가능 크기는 480 미만일 수 없어요. 즉, 페이지 크기가 512 라면 예약 공간 크기는 32 를 초과할 수 없어요.
1.3.5. 페이로드 분수
최대 및 최소 내장 페이로드 분수와 리프 페이로드 분수 값은 64, 32, 32 여야 해요. 이 값들은 원래 b-tree 알고리즘의 저장 형식을 수정하는 데 사용할 수 있는 조정 가능한 매개변수가 될 의도였어요. 하지만 그 기능은 지원되지 않으며 현재 미래에 지원을 추가할 계획도 없어요. 따라서 이 세 바이트는 지정된 값으로 고정돼요.
1.3.6. 파일 변경 카운터
파일 변경 카운터는 오프셋 24 의 4바이트 big-endian 정수로, 데이터베이스 파일이 수정된 후 잠금이 해제될 때마다 증가해요. 두 개 이상의 프로세스가 같은 데이터베이스 파일을 읽을 때 각 프로세스는 변경 카운터를 모니터링함으로써 다른 프로세스의 데이터베이스 변경을 감지할 수 있어요. 프로세스는 보통 다른 프로세스가 데이터베이스를 수정했을 때 데이터베이스 페이지 캐시를 비우고 싶어해요. 캐시가 오래됐기 때문이에요. 파일 변경 카운터가 이를 가능하게 해요.
WAL 모드에서는 데이터베이스 변경이 wal-index 를 사용해 감지되므로 변경 카운터가 필요 없어요. 따라서 WAL 모드에서는 트랜잭션마다 변경 카운터가 증가하지 않을 수 있어요.
1.3.7. 헤더 내 데이터베이스 크기
헤더의 오프셋 28 의 4바이트 big-endian 정수는 페이지 단위의 데이터베이스 파일 크기를 저장해요. 이 헤더 내 데이터베이스 크기가 유효하지 않으면(다음 문단 참고), 데이터베이스 크기는 데이터베이스 파일의 실제 크기를 보고 계산돼요. 이전 SQLite 버전은 헤더 내 데이터베이스 크기를 무시하고 실제 파일 크기만 사용했어요. 최신 SQLite 버전은 헤더 내 데이터베이스 크기를 사용할 수 있으면 사용하지만, 유효하지 않으면 실제 파일 크기로 대체해요.
헤더 내 데이터베이스 크기는 0 이 아니고, 오프셋 24 의 4바이트 변경 카운터가 오프셋 92 의 4바이트 version-valid-for 숫자와 정확히 일치할 때만 유효한 것으로 간주돼요. 데이터베이스가 최신 SQLite 버전(3.7.0 (2010-07-21) 이상)으로만 수정되면 헤더 내 데이터베이스 크기는 항상 유효해요. 레거시 SQLite 버전이 데이터베이스에 쓰면, 헤더 내 데이터베이스 크기를 갱신하는 법을 모르므로 헤더 내 데이터베이스 크기가 잘못될 수 있어요. 하지만 레거시 SQLite 버전은 오프셋 92 의 version-valid-for 숫자도 변경하지 않고 그대로 두므로 변경 카운터와 일치하지 않게 돼요. 따라서 변경 카운터가 version-valid-for 숫자와 일치하지 않는 것을 관찰함으로써 잘못된 헤더 내 데이터베이스 크기를 감지(그리고 무시)할 수 있어요.
1.3.8. 프리 페이지 목록
데이터베이스 파일의 미사용 페이지는 프리리스트(freelist)에 저장돼요. 오프셋 32 의 4바이트 big-endian 정수는 프리리스트의 첫 페이지 페이지 번호를 저장하고, 프리리스트가 비어 있으면 0 을 저장해요. 오프셋 36 의 4바이트 big-endian 정수는 프리리스트의 총 페이지 수를 저장해요.
1.3.9. 스키마 쿠키
스키마 쿠키는 오프셋 40 의 4바이트 big-endian 정수로, 데이터베이스 스키마가 변경될 때마다 증가해요. 준비된 문장은 특정 버전의 데이터베이스 스키마에 대해 컴파일돼요. 데이터베이스 스키마가 변경되면 문장을 다시 준비해야 해요. 준비된 문장이 실행될 때 먼저 스키마 쿠키를 검사해 값이 문장이 준비됐을 때와 같은지 확인하고, 스키마 쿠키가 변경됐으면 문장이 자동으로 다시 준비되고 다시 실행되거나 SQLITE_SCHEMA 에러로 중단돼요.
1.3.10. 스키마 형식 번호
스키마 형식 번호는 오프셋 44 의 4바이트 big-endian 정수예요. 스키마 형식 번호는 오프셋 18 과 19 의 파일 형식 읽기/쓰기 버전 번호와 비슷하지만, 스키마 형식 번호는 저수준 b-tree 형식이 아니라 고수준 SQL 형식을 가리킨다는 점이 달라요. 현재 4 개의 스키마 형식 번호가 정의돼 있어요:
- 형식 1 은 버전 3.0.0 (2004-06-18)까지 거슬러 올라가는 모든 SQLite 버전이 이해해요.
- 형식 2 는 같은 테이블 안의 행이 가변 개수의 컬럼을 가질 수 있는 기능을 추가해,
ALTER TABLE ... ADD COLUMN기능을 지원해요. 형식 2 의 읽기/쓰기 지원은 2005-02-20 의 SQLite 버전 3.1.3 에서 추가됐어요. - 형식 3 은
ALTER TABLE ... ADD COLUMN으로 추가된 추가 컬럼이 NULL 이 아닌 기본값을 가질 수 있는 기능을 추가해요. 이 기능은 2005-03-11 의 SQLite 버전 3.1.4 에서 추가됐어요. - 형식 4 는 SQLite 가 인덱스 선언의 DESC 키워드를 존중하게 해요. (형식 1, 2, 3 에서는 인덱스에서 DESC 키워드가 무시돼요.) 형식 4 는 또한 두 개의 새 부울 레코드 타입 값(직렬 타입 8 과 9)을 추가해요. 형식 4 지원은 2006-01-10 의 SQLite 3.3.0 에서 추가됐어요.
SQLite 가 만드는 새 데이터베이스 파일은 기본적으로 형식 4를 사용해요. C 언어 인터페이스 sqlite3_db_config() 의 SQLITE_DBCONFIG_LEGACY_FILE_FORMAT 옵션을 사용하면 SQLite 가 형식 1을 사용해 새 데이터베이스 파일을 만들게 할 수 있어요. 컴파일 타임에 SQLITE_DEFAULT_FILE_FORMAT=1 을 설정하면 형식 버전 번호가 4 대신 1 로 기본 설정되게 할 수 있어요.
데이터베이스가 완전히 비어 있고 스키마가 없다면 스키마 형식 번호는 0 일 수 있어요.
1.3.11. 권장 캐시 크기
오프셋 48 의 4바이트 big-endian 부호 있는 정수는 데이터베이스 파일에 대한 페이지 단위의 권장 캐시 크기예요. 이 값은 권고일 뿐이며 SQLite 가 이를 따를 의무는 없어요. 정수의 절대값이 권장 크기로 사용돼요. 권장 캐시 크기는 default_cache_size pragma 로 설정할 수 있어요.
1.3.12. 증분 진공(incremental vacuum) 설정
오프셋 52 와 64 의 두 4바이트 big-endian 정수는 auto_vacuum 및 incremental_vacuum 모드를 관리하는 데 사용돼요. 오프셋 52 의 정수가 0 이면 포인터-맵(ptrmap) 페이지가 데이터베이스 파일에서 생략되고 auto_vacuum 이나 incremental_vacuum 이 모두 지원되지 않아요. 오프셋 52 의 정수가 0 이 아니면, 그것은 데이터베이스 파일의 가장 큰 루트 페이지 번호이고, 데이터베이스 파일은 ptrmap 페이지를 포함하며, 모드는 auto_vacuum 또는 incremental_vacuum 중 하나여야 해요. 후자의 경우 오프셋 64 의 정수는 incremental_vacuum 에 대해 true, auto_vacuum 에 대해 false 예요. 오프셋 52 의 정수가 0 이면 오프셋 64 의 정수도 0 이어야 해요.
1.3.13. 텍스트 인코딩
오프셋 56 의 4바이트 big-endian 정수는 데이터베이스에 저장된 모든 텍스트 문자열에 사용되는 인코딩을 결정해요. 값 1 은 UTF-8 을 의미해요. 값 2 는 UTF-16le 을 의미해요. 값 3 은 UTF-16be 을 의미해요. 다른 값은 허용되지 않아요. sqlite3.h 헤더 파일은 텍스트 인코딩의 숫자 코드 대신 사용할 C 전처리기 매크로 SQLITE_UTF8 (1), SQLITE_UTF16LE (2), SQLITE_UTF16BE (3) 를 정의해요.
1.3.14. 사용자 버전 번호
오프셋 60 의 4바이트 big-endian 정수는 user_version pragma 로 설정되고 조회되는 사용자 버전이에요. 사용자 버전은 SQLite 가 사용하지 않아요.
1.3.15. 애플리케이션 ID
오프셋 68 의 4바이트 big-endian 정수는 데이터베이스가 특정 애플리케이션에 속하거나 연관되어 있음을 식별하기 위해 PRAGMA application_id 명령으로 설정할 수 있는 "Application ID"예요. 애플리케이션 ID 는 애플리케이션 파일 형식으로 사용되는 데이터베이스 파일을 위한 것이에요. 애플리케이션 ID 는 file(1) 같은 유틸리티가 단순히 "SQLite3 Database"라고 보고하는 대신 특정 파일 타입을 결정하는 데 사용할 수 있어요. 할당된 애플리케이션 ID 목록은 SQLite 소스 저장소의 magic.txt 파일을 참고해 볼 수 있어요.
1.3.16. 쓰기 라이브러리 버전 번호와 version-valid-for 숫자
오프셋 96 의 4바이트 big-endian 정수는 데이터베이스 파일을 가장 최근에 수정한 SQLite 라이브러리의 SQLITE_VERSION_NUMBER 값을 저장해요. 오프셋 92 의 4바이트 big-endian 정수는 버전 번호가 저장됐을 때의 변경 카운터 값이에요. 오프셋 92 의 정수는 버전 번호가 유효한 트랜잭션을 나타내며 때로는 "version-valid-for number"라고 불려요.
1.3.17. 확장을 위해 예약된 헤더 공간
데이터베이스 파일 헤더의 다른 모든 바이트는 향후 확장을 위해 예약되어 있으며 0 으로 설정되어야 해요.
1.4. 잠금 바이트 페이지 (The Lock-Byte Page)
잠금 바이트 페이지는 오프셋 1073741824 와 1073742335 사이(양끝 포함)의 바이트를 포함하는 데이터베이스 파일의 단일 페이지예요. 크기가 1073741824 바이트 이하인 데이터베이스 파일은 잠금 바이트 페이지를 포함하지 않아요. 1073741824 보다 큰 데이터베이스 파일은 정확히 하나의 잠금 바이트 페이지를 포함해요.
잠금 바이트 페이지는 운영체제 특정 VFS 구현이 데이터베이스 파일 잠금 기본 요소를 구현하는 데 사용하도록 남겨진 공간이에요. SQLite 는 잠금 바이트 페이지를 사용하지 않아요. SQLite 코어는 잠금 바이트 페이지를 읽거나 쓰지 않지만, 운영체제 특정 VFS 구현은 기본 시스템의 요구와 성향에 따라 잠금 바이트 페이지의 바이트를 읽거나 쓰도록 선택할 수 있어요. SQLite 에 내장된 unix 및 win32 VFS 구현은 잠금 바이트 페이지에 쓰지 않지만, 다른 운영체제용 타사 VFS 구현은 쓸 수 있어요.
잠금 바이트 페이지는 이 파일 형식이 설계됐을 때 주류 운영체제였고 필수 파일 잠금(mandatory file locking)만 지원했던 Win95 를 지원해야 하는 필요에서 생겨났어요. 우리가 아는 모든 현대 운영체제는 권고 파일 잠금(advisory file locking)을 지원하므로 잠금 바이트 페이지는 더 이상 실제로 필요하지 않지만, 하위 호환성을 위해 유지돼요.
1.5. 프리리스트 (The Freelist)
데이터베이스 파일은 활성 사용 중이 아닌 페이지를 하나 이상 포함할 수 있어요. 미사용 페이지는 예를 들어 데이터베이스에서 정보를 삭제할 때 생길 수 있어요. 미사용 페이지는 프리리스트에 저장되고 추가 페이지가 필요할 때 재사용돼요.
프리리스트는 프리리스트 트렁크 페이지의 연결 리스트로 구성되며, 각 트렁크 페이지는 0 개 이상의 프리리스트 리프 페이지의 페이지 번호를 포함해요.
프리리스트 트렁크 페이지는 4바이트 big-endian 정수의 배열로 구성돼요. 배열의 크기는 페이지의 사용 가능 공간에 맞는 만큼의 정수예요. 최소 사용 가능 공간은 480 바이트이므로 배열은 항상 길이가 120 항목 이상이에요. 프리리스트 트렁크 페이지의 첫 번째 정수는 목록의 다음 프리리스트 트렁크 페이지 번호이거나, 마지막 프리리스트 트렁크 페이지라면 0 이에요. 프리리스트 트렁크 페이지의 두 번째 정수는 뒤따르는 리프 페이지 포인터의 수예요. 프리리스트 트렁크 페이지의 두 번째 정수를 L 이라고 불러요. L 이 0 보다 크면 배열 인덱스 2 와 L+1 사이(양끝 포함)의 정수가 프리리스트 리프 페이지의 페이지 번호를 포함해요.
프리리스트 리프 페이지는 어떤 정보도 포함하지 않아요. SQLite 는 디스크 I/O 를 줄이기 위해 프리리스트 리프 페이지의 읽기나 쓰기를 피해요.
3.6.0 (2008-07-16) 이전 SQLite 버전의 버그로 인해, 프리리스트 트렁크 페이지 배열의 마지막 6 개 항목 중 하나라도 0 이 아닌 값을 포함하면 데이터베이스가 손상된 것으로 보고됐어요. 최신 SQLite 버전에는 이 문제가 없어요. 하지만 최신 SQLite 버전도 프리리스트 트렁크 페이지 배열의 마지막 6 개 항목 사용을 피해서, 최신 SQLite 버전이 만든 데이터베이스 파일을 이전 SQLite 버전이 읽을 수 있게 해요.
프리리스트 페이지 수는 파일 시작 부분에서 36 오프셋의 데이터베이스 헤더에 4바이트 big-endian 정수로 저장돼요. 데이터베이스 헤더는 또한 파일 시작 부분에서 32 오프셋의 4바이트 big-endian 정수로 첫 프리리스트 트렁크 페이지의 페이지 번호를 저장해요.
1.6. B-Tree 페이지
b-tree 알고리즘은 페이지 지향 저장 장치에서 고유하고 정렬된 키로 키/데이터 저장을 제공해요. b-tree 에 대한 배경 정보는 Knuth, The Art Of Computer Programming, Volume 3 "Sorting and Searching", 471-479 페이지를 참고하세요. SQLite 는 두 가지 변형의 b-tree 를 사용해요. "테이블 b-tree"는 64비트 부호 있는 정수 키를 사용하고 모든 데이터를 리프에 저장해요. "인덱스 b-tree"는 임의 키를 사용하고 데이터를 전혀 저장하지 않아요.
b-tree 페이지는 내부 페이지 또는 리프 페이지예요. 리프 페이지는 키를 포함하고, 테이블 b-tree 의 경우 각 키에 연관된 데이터가 있어요. 내부 페이지는 K 개의 키와 함께 K+1 개의 자식 b-tree 페이지 포인터를 포함해요. 내부 b-tree 페이지의 "포인터"는 자식 페이지의 32비트 부호 없는 정수 페이지 번호일 뿐이에요.
내부 b-tree 페이지의 키 수 K 는 거의 항상 최소 2 이고 보통 2 보다 훨씬 많아요. 유일한 예외는 페이지 1 이 내부 b-tree 페이지일 때예요. 페이지 1 은 그 페이지의 시작 부분에 데이터베이스 헤더가 있기 때문에 사용 가능한 저장 공간이 100 바이트 더 적고, 그래서 때때로(드물게) 페이지 1 이 내부 b-tree 페이지라면 단일 키만 담을 수 있어요. 다른 모든 경우 K 는 2 이상이에요. K 의 상한은 페이지에 들어갈 수 있는 만큼의 키예요. 인덱스 b-tree 의 큰 키는 오버플로우 페이지로 분할되어 어떤 단일 키도 페이지의 사용 가능한 저장 공간의 4분의 1 이상을 사용하지 않게 해서, 모든 내부 페이지가 최소 4 개의 키를 저장할 수 있게 해요. 테이블 b-tree 의 정수 키는 오버플로우가 필요할 만큼 크지 않으므로, 키 오버플로우는 인덱스 b-tree 에서만 발생해요.
리프 b-tree 의 깊이를 1 로 정의하고, 내부 b-tree 의 깊이를 그 자식 중 최대 깊이보다 1 큰 값으로 정의해요. 올바른 형식의 데이터베이스에서 내부 b-tree 의 모든 자식은 같은 깊이를 가져요.
내부 b-tree 페이지에서 포인터와 키는 논리적으로 양 끝에 포인터가 있는 채로 번갈아 나타나요. (이전 문장은 개념적으로 이해해야 해요. 페이지 안의 키와 포인터의 실제 배치는 더 복잡하고 뒤에서 설명할 거예요.) 같은 페이지 안의 모든 키는 고유하고 왼쪽에서 오른쪽으로 오름차순으로 논리적으로 정리돼요. (다시, 이 순서는 논리적이지 물리적이지 않아요. 페이지 안의 키의 실제 위치는 임의예요.) 어떤 키 X 에 대해 X 의 왼쪽 포인터는 모든 키가 X 보다 작거나 같은 b-tree 페이지를 가리키고, X 의 오른쪽 포인터는 모든 키가 X 보다 큰 페이지를 가리켜요.
내부 b-tree 페이지 안에서 각 키와 그 바로 왼쪽 포인터는 "셀(cell)"이라는 구조로 결합돼요. 가장 오른쪽 포인터는 별도로 보관돼요. 리프 b-tree 페이지는 포인터가 없지만, 인덱스 b-tree 의 키나 테이블 b-tree 의 키와 내용을 담기 위해 여전히 셀 구조를 사용해요. 데이터도 셀에 포함돼요.
모든 b-tree 페이지는 최대 하나의 부모 b-tree 페이지를 가져요. 부모가 없는 b-tree 페이지를 루트 페이지라고 불러요. 루트 b-tree 페이지와 그 자식의 폐포(closure)가 함께 완전한 b-tree 를 형성해요. 리프이자 루트인 단일 페이지로 구성된 완전한 b-tree 를 가지는 것이 가능(실제로 꽤 흔함)해요. 부모에서 자식으로 포인터가 있기 때문에, 루트 페이지만 알면 완전한 b-tree 의 모든 페이지를 찾을 수 있어요. 따라서 b-tree 는 루트 페이지 번호로 식별돼요.
b-tree 페이지는 테이블 b-tree 페이지 또는 인덱스 b-tree 페이지예요. 각 완전한 b-tree 안의 모든 페이지는 같은 타입이에요. 즉 테이블 또는 인덱스 중 하나예요. 데이터베이스 스키마의 각 rowid 테이블(sqlite_schema 같은 시스템 테이블 포함)마다 데이터베이스 파일에 하나의 테이블 b-tree 가 있어요. 고유성 제약 조건이 만든 암시적 인덱스를 포함해 스키마의 각 인덱스마다 데이터베이스 파일에 하나의 인덱스 b-tree 가 있어요. 가상 테이블과 연관된 b-tree 는 없어요. 특정 가상 테이블 구현은 저장을 위해 섀도 테이블(shadow table)을 사용할 수 있지만, 그 섀도 테이블은 데이터베이스 스키마에 별도의 항목을 가질 거예요. WITHOUT ROWID 테이블은 테이블 b-tree 대신 인덱스 b-tree 를 사용하므로, 각 WITHOUT ROWID 테이블마다 데이터베이스 파일에 하나의 인덱스 b-tree 가 있어요. sqlite_schema 테이블에 해당하는 b-tree 는 항상 테이블 b-tree 이고 항상 루트 페이지가 1 이에요. sqlite_schema 테이블은 데이터베이스 파일의 다른 모든 테이블과 인덱스의 루트 페이지 번호를 포함해요.
테이블 b-tree 의 각 항목은 64비트 부호 있는 정수 키와 최대 2147483647 바이트의 임의 데이터로 구성돼요. (테이블 b-tree 의 키는 b-tree 가 구현하는 SQL 테이블의 rowid 에 해당해요.) 내부 테이블 b-tree 는 키와 자식 포인터만 담아요. 모든 데이터는 테이블 b-tree 리프에 포함돼요.
인덱스 b-tree 의 각 항목은 최대 2147483647 바이트 길이의 임의 키로 구성되고 데이터는 없어요.
셀의 "페이로드"를 셀의 임의 길이 부분으로 정의해요. 인덱스 b-tree 에서 키는 항상 임의 길이이므로 페이로드는 키예요. 내부 테이블 b-tree 페이지의 셀에는 임의 길이 요소가 없으므로 그 셀은 페이로드가 없어요. 테이블 b-tree 리프 페이지는 임의 길이 내용을 포함하므로 그 페이지의 셀은 페이로드가 내용이에요.
셀의 페이로드 크기가 특정 임계값(뒤에서 정의)을 초과하면 페이로드의 처음 몇 바이트만 b-tree 페이지에 저장되고 나머지는 내용 오버플로우 페이지의 연결 리스트에 저장돼요.
b-tree 페이지는 다음 순서로 영역으로 나뉘어요:
- 100바이트 데이터베이스 파일 헤더 (페이지 1 에만 있음)
- 8 또는 12 바이트 b-tree 페이지 헤더
- 셀 포인터 배열
- 할당되지 않은 공간
- 셀 내용 영역
- 예약 영역
100바이트 데이터베이스 파일 헤더는 항상 테이블 b-tree 페이지인 페이지 1 에만 있어요. 데이터베이스 파일의 다른 모든 b-tree 페이지는 이 100바이트 헤더를 생략해요.
예약 영역은 확장 기능이 페이지별 정보를 담을 수 있도록 모든 페이지(잠금 페이지 제외) 끝의 미사용 공간 영역이에요. 예약 영역의 크기는 데이터베이스 파일 헤더의 오프셋 20 에 있는 1바이트 부호 없는 정수로 결정돼요. 예약 영역의 크기는 보통 0 이에요.
b-tree 페이지 헤더는 리프 페이지의 경우 8 바이트, 내부 페이지의 경우 12 바이트 크기예요. 페이지 헤더의 모든 다중 바이트 값은 big-endian 이에요. b-tree 페이지 헤더는 다음 필드로 구성돼요:
B-tree 페이지 헤더 형식
| Offset | Size | Description |
|---|---|---|
| 0 | 1 | The one-byte flag at offset 0 indicating the b-tree page type. A value of 2 (0x02) means the page is an interior index b-tree page. A value of 5 (0x05) means the page is an interior table b-tree page. A value of 10 (0x0a) means the page is a leaf index b-tree page. A value of 13 (0x0d) means the page is a leaf table b-tree page. Any other value for the b-tree page type is an error. |
| 1 | 2 | The two-byte integer at offset 1 gives the start of the first freeblock on the page, or is zero if there are no freeblocks. |
| 3 | 2 | The two-byte integer at offset 3 gives the number of cells on the page. |
| 5 | 2 | The two-byte integer at offset 5 designates the start of the cell content area. A zero value for this integer is interpreted as 65536. |
| 7 | 1 | The one-byte integer at offset 7 gives the number of fragmented free bytes within the cell content area. |
| 8 | 4 | The four-byte page number at offset 8 is the right-most pointer. This value appears in the header of interior b-tree pages only and is omitted from all other pages. |
b-tree 페이지의 셀 포인터 배열은 b-tree 페이지 헤더 바로 뒤에 나와요. b-tree 의 셀 수를 K 라고 하자. 셀 포인터 배열은 셀 내용에 대한 K 개의 2바이트 정수 오프셋으로 구성돼요. 셀 포인터는 키 순서로 정렬되어, 가장 왼쪽 셀(가장 작은 키를 가진 셀)이 먼저, 가장 오른쪽 셀(가장 큰 키를 가진 셀)이 마지막이에요.
셀 내용은 b-tree 페이지의 셀 내용 영역에 저장돼요. SQLite 는 셀 포인터 배열의 미래 성장을 위한 공간을 남기기 위해 셀을 b-tree 페이지의 끝쪽으로 최대한 멀리 배치하려고 해요. 마지막 셀 포인터 배열 항목과 첫 번째 셀의 시작 사이의 영역이 할당되지 않은 영역이에요.
페이지에 셀이 없으면(행이 없는 테이블의 루트 페이지에서만 가능), 셀 내용 영역으로의 오프셋은 페이지 크기에서 예약 공간 바이트를 뺀 값과 같아요. 데이터베이스가 65536 바이트 페이지 크기를 사용하고 예약 공간이 0(예약 공간의 보통 값)이면, 빈 페이지의 셀 내용 오프셋은 65536 이 되고 싶어해요. 하지만 그 정수는 2바이트 부호 없는 정수에 저장하기엔 너무 크므로, 그 자리에 0 값이 사용돼요.
프리블록(freeblock)은 b-tree 페이지 안의 할당되지 않은 공간을 식별하는 데 사용되는 구조예요. 프리블록은 체인으로 구성돼요. 프리블록의 처음 2바이트는 체인의 다음 프리블록의 b-tree 페이지 오프셋인 big-endian 정수이거나, 프리블록이 체인의 마지막이면 0 이에요. 각 프리블록의 3번째와 4번째 바이트는 4바이트 헤더를 포함한 프리블록의 바이트 크기인 big-endian 정수를 형성해요. 프리블록은 항상 오프셋이 증가하는 순서로 연결돼요. b-tree 페이지 헤더의 두 번째 필드는 첫 프리블록의 오프셋이거나, 페이지에 프리블록이 없으면 0 이에요. 올바른 형식의 b-tree 페이지에서 첫 프리블록 앞에는 항상 최소 하나의 셀이 있어요.
프리블록은 최소 4 바이트의 공간이 필요해요. 셀 내용 영역 안에 1, 2, 또는 3 바이트의 고립된 미사용 바이트 그룹이 있으면, 그 바이트들이 조각(fragment)을 구성해요. 모든 조각의 총 바이트 수는 b-tree 페이지 헤더의 다섯 번째 필드에 저장돼요. 올바른 형식의 b-tree 페이지에서 조각의 총 바이트 수는 60 을 초과할 수 없어요.
b-tree 페이지의 총 자유 공간은 할당되지 않은 영역의 크기와 모든 프리블록의 총 크기와 분할된 자유 바이트 수를 합한 것이에요. SQLite 는 때때로 b-tree 페이지를 재구성해서 프리블록이나 조각 바이트가 없게 하고, 모든 미사용 바이트가 할당되지 않은 공간 영역에 포함되게 하며, 모든 셀이 페이지 끝에 촘촘히 채워지게 해요. 이를 b-tree 페이지 "조각 모음(defragment)"이라고 불러요.
가변 길이 정수 또는 "varint"는 64비트 2의 보수 정수의 정적 Huffman 인코딩으로, 작은 양수 값에 더 적은 공간을 사용해요. varint 는 길이가 1 에서 9 바이트 사이예요. varint 는 최상위 비트가 설정된 0 개 이상의 바이트와 그 뒤에 따르는 최상위 비트가 0 인 단일 바이트, 또는 9 바이트 중 더 짧은 것으로 구성돼요. 처음 8 바이트 각각의 낮은 7 비트와 아홉 번째 바이트의 전체 8 비트가 64비트 2의 보수 정수를 재구성하는 데 사용돼요. varint 는 big-endian 이에요. varint 의 앞쪽 바이트에서 가져온 비트가 뒤쪽 바이트에서 가져온 비트보다 더 중요해요.
셀의 형식은 셀이 나타나는 b-tree 페이지의 종류에 따라 달라요. 다음 표는 다양한 b-tree 페이지 타입에 대한 셀의 요소를 나타나는 순서대로 보여줘요.
테이블 B-Tree 리프 셀 (헤더 0x0d):
- 오버플로우를 포함한 페이로드의 총 바이트 수인 varint
- 정수 키인 varint, 일명 "rowid"
- 오버플로우 페이지로 넘치지 않는 페이로드의 초기 부분
- 오버플로우 페이지 목록의 첫 페이지에 대한 4바이트 big-endian 정수 페이지 번호 - 모든 페이로드가 b-tree 페이지에 맞으면 생략
테이블 B-Tree 내부 셀 (헤더 0x05):
- 왼쪽 자식 포인터인 4바이트 big-endian 페이지 번호
- 정수 키인 varint
인덱스 B-Tree 리프 셀 (헤더 0x0a):
- 오버플로우를 포함한 키 페이로드의 총 바이트 수인 varint
- 오버플로우 페이지로 넘치지 않는 페이로드의 초기 부분
- 오버플로우 페이지 목록의 첫 페이지에 대한 4바이트 big-endian 정수 페이지 번호 - 모든 페이로드가 b-tree 페이지에 맞으면 생략
인덱스 B-Tree 내부 셀 (헤더 0x02):
- 왼쪽 자식 포인터인 4바이트 big-endian 페이지 번호
- 오버플로우를 포함한 키 페이로드의 총 바이트 수인 varint
- 오버플로우 페이지로 넘치지 않는 페이로드의 초기 부분
- 오버플로우 페이지 목록의 첫 페이지에 대한 4바이트 big-endian 정수 페이지 번호 - 모든 페이로드가 b-tree 페이지에 맞으면 생략
위 정보를 표 형식으로 다시 쓰면 다음과 같아요:
B-tree 셀 형식
| Datatype | Table Leaf (0x0d) | Table Interior (0x05) | Index Leaf (0x0a) | Index Interior (0x02) | Description |
|---|---|---|---|---|---|
| 4-byte integer | ✔ | ✔ | Page number of left child | ||
| varint | ✔ | ✔ | ✔ | Number of bytes of payload | |
| varint | ✔ | ✔ | Rowid | ||
| byte array | ✔ | ✔ | ✔ | Payload | |
| 4-byte integer | ✔ | ✔ | ✔ | Page number of first overflow page |
오버플로우 페이지로 넘치는 페이로드의 양도 페이지 타입에 따라 달라져요. 다음 계산을 위해 U 를 데이터베이스 페이지의 사용 가능 크기(총 페이지 크기에서 각 페이지 끝의 예약 공간을 뺀 값)라고 하고, P 를 페이로드 크기라고 하자. 아래에서 기호 X 는 오버플로우 페이지로 넘치지 않고 직접 b-tree 페이지에 저장할 수 있는 최대 페이로드 양을 나타내고, 기호 M 은 넘침이 허용되기 전에 b-tree 페이지에 저장해야 하는 최소 페이로드 양을 나타내요.
테이블 B-Tree 리프 셀:
X 를 U-35 라고 하자. 페이로드 크기 P 가 X 보다 작거나 같으면 전체 페이로드가 b-tree 리프 페이지에 저장돼요. M 을 ((U-12)*32/255)-23 이라 하고 K 를 M+((P-M)%(U-4)) 라고 하자. P 가 X 보다 크면 테이블 b-tree 리프 페이지에 저장되는 바이트 수는 K 가 X 보다 작거나 같으면 K, 그렇지 않으면 M 이에요. 리프 페이지에 저장되는 바이트 수는 절대 M 보다 작지 않아요.
테이블 B-Tree 내부 셀:
테이블 b-tree 의 내부 페이지는 페이로드가 없으므로 넘칠 페이로드가 절대 없어요.
인덱스 B-Tree 리프 또는 내부 셀:
X 를 ((U-12)*64/255)-23 이라고 하자. 페이로드 크기 P 가 X 보다 작거나 같으면 전체 페이로드가 b-tree 페이지에 저장돼요. M 을 ((U-12)*32/255)-23 이라 하고 K 를 M+((P-M)%(U-4)) 라고 하자. P 가 X 보다 크면 인덱스 b-tree 페이지에 저장되는 바이트 수는 K 가 X 보다 작거나 같으면 K, 그렇지 않으면 M 이에요. 인덱스 페이지에 저장되는 바이트 수는 절대 M 보다 작지 않아요.
같은 계산에 대한 대체 설명은 다음과 같아요:
- X 는 테이블 btree 리프 페이지의 경우 U-35 이거나 인덱스 페이지의 경우 ((U-12)*64/255)-23 이에요.
- M 은 항상 ((U-12)*32/255)-23 이에요.
- K 를 M+((P-M)%(U-4)) 라고 하자.
- P<=X 이면 P 바이트의 페이로드 전체가 오버플로우 없이 b-tree 페이지에 직접 저장돼요.
- P>X 이고 K<=X 이면 P 의 처음 K 바이트가 b-tree 페이지에 저장되고 나머지 P-K 바이트는 오버플로우 페이지에 저장돼요.
- P>X 이고 K>X 이면 P 의 처음 M 바이트가 b-tree 페이지에 저장되고 나머지 P-M 바이트는 오버플로우 페이지에 저장돼요.
오버플로우 임계값은 인덱스 b-tree 에 최소 팬아웃 4 를 제공하고, 보통 오버플로우 페이지를 조회하지 않고도 레코드 헤더에 접근할 수 있도록 페이로드의 충분한 부분을 b-tree 페이지에 유지하도록 설계됐어요. 돌아보면 SQLite b-tree 로직의 설계자는 이 임계값을 훨씬 더 단순하게 만들 수 있었음을 깨달았어요. 하지만 계산을 바꾸면 호환되지 않는 파일 형식이 생길 수 있어요. 그리고 현재 계산은 조금 복잡하지만 잘 작동해요.
1.7. 셀 페이로드 오버플로우 페이지
b-tree 셀의 페이로드가 b-tree 페이지에 너무 크면, 나머지가 오버플로우 페이지로 넘쳐요. 오버플로우 페이지는 연결 리스트를 형성해요. 각 오버플로우 페이지의 처음 4바이트는 체인의 다음 페이지 번호인 big-endian 정수이거나, 체인의 마지막 페이지이면 0 이에요. 다섯 번째 바이트부터 마지막 사용 가능 바이트까지가 오버플로우 내용을 담는 데 사용돼요.
1.8. 포인터 맵 또는 Ptrmap 페이지
포인터 맵 또는 ptrmap 페이지는 auto_vacuum 및 incremental_vacuum 모드의 작동을 더 효율적으로 만들기 위해 데이터베이스에 삽입되는 추가 페이지예요. 데이터베이스의 다른 페이지 타입은 보통 부모에서 자식으로 포인터를 가져요. 예를 들어 내부 b-tree 페이지는 자식 b-tree 페이지에 대한 포인터를 포함하고, 오버플로우 체인은 체인에서 앞쪽 링크에서 뒤쪽 링크로의 포인터가 있어요. ptrmap 페이지는 반대 방향, 즉 자식에서 부모로 가는 연결 정보를 포함해요.
데이터베이스 헤더의 오프셋 52 에 0 이 아닌 가장 큰 루트 b-tree 페이지 값이 있는 데이터베이스 파일에는 ptrmap 페이지가 반드시 존재해야 해요. 가장 큰 루트 b-tree 페이지 값이 0 이면 데이터베이스는 ptrmap 페이지를 포함해서는 안 돼요.
ptrmap 페이지가 있는 데이터베이스에서 첫 ptrmap 페이지는 페이지 2예요. ptrmap 페이지는 5바이트 항목의 배열로 구성돼요. 페이지의 사용 가능 공간에 들어갈 5바이트 항목의 수를 J 라고 하자. (즉, J=U/5.) 첫 ptrmap 페이지는 페이지 3 부터 J+2 까지(양끝 포함)의 뒤 포인터 정보를 포함해요. 두 번째 포인터 맵 페이지는 페이지 J+3 에 있고, 그 ptrmap 페이지는 페이지 J+4 부터 2*J+3 까지(양끝 포함)의 뒤 포인터 정보를 제공해요. 이렇게 데이터베이스 파일 전체에 걸쳐 계속돼요.
ptrmap 페이지를 사용하는 데이터베이스에서 이전 문단의 계산이 식별하는 위치의 모든 페이지는 ptrmap 페이지여야 하고, 다른 어떤 페이지도 ptrmap 페이지일 수 없어요. 단, 바이트-잠금 페이지가 우연히 ptrmap 페이지와 같은 페이지 번호에 떨어지면, 그 경우 한 번만 ptrmap 이 다음 페이지로 이동해요.
ptrmap 페이지의 각 5바이트 항목은 포인터 맵 바로 뒤에 오는 페이지 중 하나에 대한 뒤 링크 정보를 제공해요. 페이지 B 가 ptrmap 페이지라면 페이지 B+1 에 대한 뒤 링크 정보는 포인터 맵의 첫 항목이 제공해요. 페이지 B+2 에 대한 정보는 두 번째 항목이 제공해요. 이런 식으로 계속돼요.
각 5바이트 ptrmap 항목은 1바이트의 "페이지 타입" 정보와 그 다음 4바이트 big-endian 페이지 번호로 구성돼요. 다섯 가지 페이지 타입이 인식돼요:
- b-tree 루트 페이지. 페이지 번호는 0 이어야 해요.
- 프리리스트 페이지. 페이지 번호는 0 이어야 해요.
- 셀 페이로드 오버플로우 체인의 첫 페이지. 페이지 번호는 내용이 넘친 셀을 포함하는 b-tree 페이지예요.
- 첫 페이지가 아닌 오버플로우 체인의 페이지. 페이지 번호는 오버플로우 체인의 이전 페이지예요.
- 루트가 아닌 b-tree 페이지. 페이지 번호는 부모 b-tree 페이지예요.
ptrmap 페이지를 포함하는 데이터베이스 파일에서 모든 b-tree 루트 페이지는 어떤 루트가 아닌 b-tree 페이지, 셀 페이로드 오버플로우 페이지, 또는 프리리스트 페이지보다 앞에 와야 해요. 이 제한은 루트 페이지가 auto-vacuum 이나 incremental-vacuum 중에 절대 이동되지 않도록 보장해요. auto-vacuum 논리는 sqlite_schema 테이블의 root_page 필드를 갱신하는 방법을 모르므로, sqlite_schema 테이블의 무결성을 보존하기 위해 auto-vacuum 중에 루트 페이지가 이동되는 것을 막는 것이 필요해요. 루트 페이지는 CREATE TABLE, CREATE INDEX, DROP TABLE, DROP INDEX 작업으로 데이터베이스 파일의 시작 부분으로 이동돼요.
2. 스키마 계층 (Schema Layer)
앞선 설명은 SQLite 파일 형식의 저수준 측면을 기술해요. b-tree 메커니즘은 큰 데이터 세트에 접근하는 강력하고 효율적인 수단을 제공해요. 이 섹션에서는 저수준 b-tree 계층이 더 높은 수준의 SQL 기능을 구현하는 데 어떻게 사용되는지 설명할 거예요.
2.1. 레코드 형식 (Record Format)
테이블 b-tree 리프 페이지의 데이터와 인덱스 b-tree 페이지의 키는 위에서 임의 바이트 시퀀스로 특징지어졌어요. 이전 논의는 한 키가 다른 키보다 작다는 것을 언급했지만, "작다"가 무엇을 의미하는지는 정의하지 않았어요. 현재 섹션에서 이 누락을 다룰 거예요.
페이로드는, 테이블 b-tree 데이터든 인덱스 b-tree 키든, 항상 "레코드 형식"이에요. 레코드 형식은 테이블이나 인덱스의 컬럼에 해당하는 값의 시퀀스를 정의해요. 레코드 형식은 컬럼 수, 각 컬럼의 데이터타입, 각 컬럼의 내용을 지정해요.
레코드 형식은 위에서 정의한 64비트 부호 있는 정수의 가변 길이 정수 또는 varint 표현을 광범위하게 사용해요.
레코드는 헤더와 본문을 그 순서대로 포함해요. 헤더는 헤더의 총 바이트 수를 결정하는 단일 varint 로 시작해요. varint 값은 크기 varint 자체를 포함한 바이트 단위의 헤더 크기예요. 크기 varint 다음에 컬럼당 하나씩 하나 이상의 추가 varint 가 나와요. 이 추가 varint 들은 "직렬 타입(serial type)" 번호라고 불리며, 다음 차트에 따라 각 컬럼의 데이터타입을 결정해요:
레코드 형식의 직렬 타입 코드
| Serial Type | Content Size | Meaning |
|---|---|---|
| 0 | 0 | Value is a NULL. |
| 1 | 1 | Value is an 8-bit twos-complement integer. |
| 2 | 2 | Value is a big-endian 16-bit twos-complement integer. |
| 3 | 3 | Value is a big-endian 24-bit twos-complement integer. |
| 4 | 4 | Value is a big-endian 32-bit twos-complement integer. |
| 5 | 6 | Value is a big-endian 48-bit twos-complement integer. |
| 6 | 8 | Value is a big-endian 64-bit twos-complement integer. |
| 7 | 8 | Value is a big-endian IEEE 754-2008 64-bit floating point number. |
| 8 | 0 | Value is the integer 0. (Only available for schema format 4 and higher.) |
| 9 | 0 | Value is the integer 1. (Only available for schema format 4 and higher.) |
| 10,11 | variable | Reserved for internal use. These serial type codes will never appear in a well-formed database file, but they might be used in transient and temporary database files that SQLite sometimes generates for its own use. The meanings of these codes can shift from one release of SQLite to the next. |
| N≥12 and even | (N-12)/2 | Value is a BLOB that is (N-12)/2 bytes in length. |
| N≥13 and odd | (N-13)/2 | Value is a string in the text encoding and (N-13)/2 bytes in length. The nul terminator is not stored. |
헤더 크기 varint 와 직렬 타입 varint 는 보통 단일 바이트로 구성돼요. 큰 문자열과 BLOB 에 대한 직렬 타입 varint 는 2~3 바이트 varint 로 확장될 수 있지만, 그것은 규칙이라기보다 예외예요. varint 형식은 레코드 헤더를 코딩하는 데 매우 효율적이에요.
레코드의 각 컬럼 값은 헤더 바로 뒤에 나와요. 직렬 타입 0, 8, 9, 12, 13 의 경우 값의 길이는 0 바이트예요. 모든 컬럼이 이런 타입이면 레코드의 본문 부분은 비어 있어요.
레코드는 해당 테이블의 컬럼 수보다 더 적은 값을 가질 수 있어요. 이는 예를 들어 ALTER TABLE ... ADD COLUMN SQL 문이 테이블의 기존 행을 수정하지 않고 테이블 스키마의 컬럼 수를 늘린 후에 발생할 수 있어요. 레코드 끝의 누락된 값은 테이블 스키마에 정의된 해당 컬럼의 기본값을 사용해 채워져요.
2.2. 레코드 정렬 순서 (Record Sort Order)
인덱스 b-tree 의 키 순서는 키가 나타내는 레코드의 정렬 순서로 결정돼요. 레코드 비교는 컬럼별로 진행돼요. 레코드의 컬럼은 왼쪽에서 오른쪽으로 검사돼요. 같지 않은 첫 번째 컬럼 쌍이 두 레코드의 상대적 순서를 결정해요. 개별 컬럼의 정렬 순서는 다음과 같아요:
- NULL 값(직렬 타입 0)이 먼저 정렬돼요.
- 숫자 값(직렬 타입 1~9)은 NULL 다음에 숫자 순서로 정렬돼요.
- 텍스트 값(홀수 직렬 타입 13 이상)은 숫자 값 다음에 컬럼의 정렬 함수(collating function)가 결정하는 순서로 정렬돼요.
- BLOB 값(짝수 직렬 타입 12 이상)은 마지막에 memcmp() 가 결정하는 순서로 정렬돼요.
텍스트 필드의 순서를 계산하려면 각 컬럼의 정렬 함수가 필요해요. SQLite 는 세 가지 내장 정렬 함수를 정의해요:
| BINARY | The built-in BINARY collation compares strings byte by byte using the memcmp() function from the standard C library. |
|---|---|
| NOCASE | The NOCASE collation is like BINARY except that uppercase ASCII characters ('A' through 'Z') are folded into their lowercase equivalents prior to running the comparison. Only ASCII characters are case-folded. NOCASE does not implement a general purpose unicode caseless comparison. |
| RTRIM | RTRIM is like BINARY except that extra spaces at the end of either string do not change the result. In other words, strings will compare equal to one another as long as they differ only in the number of spaces at the end. |
추가 애플리케이션별 정렬 함수는 sqlite3_create_collation() 인터페이스를 사용해 SQLite 에 추가할 수 있어요.
모든 문자열의 기본 정렬 함수는 BINARY 예요. 테이블 컬럼의 대체 정렬 함수는 CREATE TABLE 문에서 컬럼 정의의 COLLATE 절을 사용해 지정할 수 있어요. 컬럼이 인덱싱될 때 기본적으로 CREATE TABLE 문에서 지정된 것과 같은 정렬 함수가 인덱스의 컬럼에 사용되지만, 이는 CREATE INDEX 문의 COLLATE 절로 재정의할 수 있어요.
2.3. SQL 테이블의 표현
데이터베이스 스키마의 각 일반적인 SQL 테이블은 테이블 b-tree 로 디스크에 표현돼요. 테이블 b-tree 의 각 항목은 SQL 테이블의 행에 해당해요. SQL 테이블의 rowid 는 테이블 b-tree 의 각 항목에 대한 64비트 부호 있는 정수 키예요.
각 SQL 테이블 행의 내용은, 먼저 다양한 컬럼의 값을 레코드 형식의 바이트 배열로 결합한 다음, 그 바이트 배열을 테이블 b-tree 의 항목에 페이로드로 저장함으로써 데이터베이스 파일에 저장돼요. 레코드의 값 순서는 SQL 테이블 정의의 컬럼 순서와 같아요. SQL 테이블에 INTEGER PRIMARY KEY 컬럼(rowid 의 별칭)이 포함되면 그 컬럼은 레코드에 NULL 값으로 나타나요. SQLite 는 INTEGER PRIMARY KEY 컬럼을 참조할 때 항상 NULL 값 대신 테이블 b-tree 키를 사용해요.
컬럼의 어피니티가 REAL 이고 그 컬럼이 정보 손실 없이 정수로 변환될 수 있는 값(소수 부분이 없고 정수로 표현할 수 있을 만큼 크지 않은 값)을 포함하면, 그 컬럼은 레코드에 정수로 저장될 수 있어요. SQLite 는 레코드에서 추출할 때 값을 부동소수점으로 다시 변환해요.
2.4. WITHOUT ROWID 테이블의 표현
SQL 테이블이 CREATE TABLE 문 끝에 "WITHOUT ROWID" 절을 사용해 생성되면, 그 테이블은 WITHOUT ROWID 테이블이고 다른 디스크 표현을 사용해요. WITHOUT ROWID 테이블은 저장에 테이블 b-tree 대신 인덱스 b-tree 를 사용해요. WITHOUT ROWID b-tree 의 각 항목의 키는 PRIMARY KEY 의 컬럼 뒤에 테이블의 나머지 모든 컬럼이 오는 레코드예요. PRIMARY KEY 컬럼은 PRIMARY KEY 절에 선언된 순서대로 나타나고, 나머지 컬럼은 CREATE TABLE 문에 나타나는 순서대로 나타나요.
따라서 WITHOUT ROWID 테이블의 내용 인코딩은 일반적인 rowid 테이블의 내용 인코딩과 같지만, PRIMARY KEY 컬럼이 먼저 오도록 컬럼 순서가 재배열되고, 내용이 테이블 b-tree 의 데이터가 아니라 인덱스 b-tree 의 키로 사용된다는 점이 달라요. REAL 어피니티를 가진 컬럼에 대한 특별 인코딩 규칙은 rowid 테이블과 마찬가지로 WITHOUT ROWID 테이블에 적용돼요.
2.4.1. WITHOUT ROWID 테이블의 PRIMARY KEY 에서 중복 컬럼 억제
WITHOUT ROWID 테이블의 PRIMARY KEY 가 같은 정렬 순서로 같은 컬럼을 두 번 이상 사용하면, PRIMARY KEY 정의에서 그 컬럼의 두 번째 및 이후 발생은 무시돼요. 예를 들어 다음 CREATE TABLE 문은 모두 디스크에 정확히 같은 표현을 가질 같은 테이블을 지정해요:
CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,c)) WITHOUT ROWID;
CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,c,a,c)) WITHOUT ROWID;
CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,A,a,C)) WITHOUT ROWID;
CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,a,a,a,c)) WITHOUT ROWID;
물론 위 첫 예가 테이블의 선호 정의예요. 모든 예는 그 순서대로 두 개의 PRIMARY KEY 컬럼 "a" 와 "c" 를 가진 WITHOUT ROWID 테이블을 만들고, 그 뒤에 또한 그 순서대로 두 개의 데이터 컬럼 "b" 와 "d" 를 만들어요.
2.5. SQL 인덱스의 표현
각 SQL 인덱스는, CREATE INDEX 문으로 명시적으로 선언됐든 UNIQUE 나 PRIMARY KEY 제약으로 암시됐든, 데이터베이스 파일의 인덱스 b-tree 에 해당해요. 인덱스 b-tree 의 각 항목은 연관된 SQL 테이블의 단일 행에 해당해요. 인덱스 b-tree 의 키는 인덱싱되는 컬럼 뒤에 해당 테이블 행의 키가 오는 레코드예요. 일반적인 테이블의 경우 행 키는 rowid 이고, WITHOUT ROWID 테이블의 경우 행 키는 PRIMARY KEY 예요. 테이블의 모든 행이 고유한 행 키를 가지므로 인덱스의 모든 키는 고유해요.
일반 인덱스에서 테이블의 행과 그 테이블과 연관된 각 인덱스의 항목 사이에는 일대일 매핑이 있어요. 하지만 부분 인덱스(partial index)에서 인덱스 b-tree 는 CREATE INDEX 문의 WHERE 절 표현식이 참인 테이블 행에 해당하는 항목만 포함해요. 인덱스와 테이블 b-tree 의 해당 행은 같은 rowid 또는 primary key 값을 공유하고 모든 인덱싱된 컬럼에 대해 같은 값을 포함해요.
2.5.1. WITHOUT ROWID 보조 인덱스의 중복 컬럼 억제
WITHOUT ROWID 테이블의 인덱스에서 PRIMARY KEY 의 컬럼이 또한 인덱스의 컬럼이고 일치하는 정렬 순서를 가지면, 인덱싱된 컬럼은 인덱스 레코드 끝의 테이블-키 접미어에서 반복되지 않아요. 예를 들어 다음 SQL 을 고려해보세요:
CREATE TABLE ex25(a,b,c,d,e,PRIMARY KEY(d,c,a)) WITHOUT rowid;
CREATE INDEX ex25ce ON ex25(c,e);
CREATE INDEX ex25acde ON ex25(a,c,d,e);
CREATE INDEX ex25ae ON ex25(a COLLATE nocase,e);
ex25ce 인덱스의 각 행은 c, e, d, a 컬럼을 가진 레코드예요. 처음 두 컬럼은 인덱싱되는 컬럼인 c 와 e 예요. 나머지 컬럼은 해당 테이블 행의 primary key 예요. 보통 primary key 는 d, c, a 컬럼이지만, 컬럼 c 가 이미 인덱스 앞부분에 나타나므로 키 접미어에서 생략돼요.
인덱싱되는 컬럼이 PRIMARY KEY 의 모든 컬럼을 덮는 극단적인 경우, 인덱스는 인덱싱되는 컬럼만으로 구성될 거예요. 위의 ex25acde 예가 이를 보여줘요. ex25acde 인덱스의 각 항목은 그 순서대로 a, c, d, e 컬럼만으로 구성돼요.
ex25ae 의 각 행은 a, e, d, c, a 의 다섯 컬럼을 포함해요. "a" 컬럼이 반복되는데, 첫 번째 "a" 의 발생은 "nocase" 정렬 함수를, 두 번째는 "binary" 정렬 순서를 가지기 때문이에요. "a" 컬럼이 반복되지 않고 테이블에 같은 "e" 값에서 "a" 만 대소문자가 다른 두 개 이상의 항목이 있다면, 그 모든 테이블 항목이 인덱스의 단일 항목에 해당하게 되어 테이블과 인덱스 사이의 일대일 대응이 깨질 거예요.
인덱스 항목의 키 접미어에서 중복 컬럼의 억제는 WITHOUT ROWID 테이블에서만 발생해요. 일반적인 rowid 테이블에서 인덱스 항목은 INTEGER PRIMARY KEY 컬럼이 인덱싱되는 컬럼 중 하나라도 항상 rowid 로 끝나요.
2.6. SQL 데이터베이스 스키마의 저장
데이터베이스 파일의 페이지 1 은 "sqlite_schema"라는 특별한 테이블을 담는 테이블 b-tree 의 루트 페이지예요. 이 b-tree 는 완전한 데이터베이스 스키마를 저장하므로 "스키마 테이블"로 알려져 있어요. sqlite_schema 테이블의 구조는 다음 SQL 로 생성된 것과 같아요:
CREATE TABLE sqlite_schema(
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);
sqlite_schema 테이블은 데이터베이스 스키마의 각 테이블, 인덱스, 뷰, 트리거(통틀어 "객체")마다 하나의 행을 포함하는데, sqlite_schema 테이블 자체에 대한 항목은 없어요. sqlite_schema 테이블은 애플리케이션 및 프로그래머 정의 객체뿐만 아니라 내부 스키마 객체에 대한 항목도 포함해요.
sqlite_schema.type 컬럼은 정의된 객체의 타입에 따라 'table', 'index', 'view', 'trigger' 텍스트 문자열 중 하나가 될 거예요. 'table' 문자열은 일반 테이블과 가상 테이블 모두에 사용돼요.
sqlite_schema.name 컬럼은 객체의 이름을 담을 거예요. 테이블의 UNIQUE 와 PRIMARY KEY 제약은 SQLite 가 "sqlite_autoindex_TABLE_N" 형태의 이름을 가진 내부 인덱스를 만들게 해요. 여기서 TABLE 은 제약을 포함하는 테이블의 이름으로 대체되고 N 은 1 부터 시작해 테이블 정의에서 만나는 제약마다 1 씩 증가하는 정수예요. WITHOUT ROWID 테이블에서 PRIMARY KEY 에 대한 sqlite_schema 항목은 없지만, sqlite_schema 항목이 존재하는 것처럼 "sqlite_autoindex_TABLE_N" 이름이 PRIMARY KEY 를 위해 남겨져요. 이는 이후 UNIQUE 제약의 번호 매기기에 영향을 줄 거예요. "sqlite_autoindex_TABLE_N" 이름은 rowid 테이블이든 WITHOUT ROWID 테이블이든 INTEGER PRIMARY KEY 에 대해 절대 할당되지 않아요.
sqlite_schema.tbl_name 컬럼은 객체가 연관된 테이블이나 뷰의 이름을 담아요. 테이블이나 뷰의 경우 tbl_name 컬럼은 name 컬럼의 복사본이에요. 인덱스의 경우 tbl_name 은 인덱싱되는 테이블의 이름이에요. 트리거의 경우 tbl_name 컬럼은 트리거가 발화하게 만드는 테이블이나 뷰의 이름을 저장해요.
sqlite_schema.rootpage 컬럼은 테이블과 인덱스의 루트 b-tree 페이지의 페이지 번호를 저장해요. 뷰, 트리거, 가상 테이블을 정의하는 행의 경우 rootpage 컬럼은 0 또는 NULL 이에요.
sqlite_schema.sql 컬럼은 객체를 설명하는 SQL 텍스트를 저장해요. 이 SQL 텍스트는 CREATE TABLE, CREATE VIRTUAL TABLE, CREATE INDEX, CREATE VIEW, 또는 CREATE TRIGGER 문으로, 데이터베이스 연결의 메인 데이터베이스일 때 데이터베이스 파일에 대해 평가하면 객체를 다시 만들 거예요. 텍스트는 보통 객체를 만드는 데 사용된 원래 문의 복사본이지만, 텍스트가 다음 규칙을 따르도록 정규화가 적용돼요:
- 문 시작 부분의 CREATE, TABLE, VIEW, TRIGGER, INDEX 키워드가 모두 대문자로 변환돼요.
- 초기 CREATE 키워드 뒤에 오면 TEMP 또는 TEMPORARY 키워드가 제거돼요.
- 생성되는 객체의 이름 앞에 오는 데이터베이스 이름 한정어가 제거돼요.
- 앞쪽 공백이 제거돼요.
- 처음 두 키워드 다음의 모든 공백이 단일 공백으로 변환돼요.
sqlite_schema.sql 컬럼의 텍스트는 객체를 만든 원래 CREATE 문 텍스트의 복사본인데, 위에서 설명한 대로 정규화되고 이후 ALTER TABLE 문으로 수정된 것이에요. UNIQUE 나 PRIMARY KEY 제약이 자동으로 만드는 내부 인덱스에 대해 sqlite_schema.sql 은 NULL 이에요.
2.6.1. 스키마 테이블의 대체 이름
"solit_schema"라는 이름은 파일 형식 어디에도 나타나지 않아요. 그 이름은 데이터베이스 구현이 사용하는 관례일 뿐이에요. 역사적 및 운영적 고려 사항으로 인해 "sqlite_schema" 테이블은 때로 다음 별칭 중 하나로 불릴 수 있어요:
- sqlite_master
- sqlite_temp_schema
- sqlite_temp_master
스키마 테이블의 이름이 파일 형식 어디에도 나타나지 않기 때문에, 애플리케이션이 스키마 테이블을 이 대체 이름 중 하나로 부르기로 선택해도 데이터베이스 파일의 의미는 바뀌지 않아요.
2.6.2. 내부 스키마 객체
애플리케이션 및/또는 개발자가 CREATE 문 SQL 로 만든 테이블, 인덱스, 뷰, 트리거 외에도, sqlite_schema 테이블은 SQLite 가 자체 내부 사용을 위해 만든 내부 스키마 객체에 대한 0 개 이상의 항목을 포함할 수 있어요. 내부 스키마 객체의 이름은 항상 "sqlite_"로 시작하고, 이름이 "sqlite_"로 시작하는 어떤 테이블, 인덱스, 뷰, 트리거든 내부 스키마 객체예요. SQLite 는 애플리케이션이 "sqlite_"로 시작하는 이름의 객체를 만드는 것을 금지해요.
SQLite 가 사용하는 내부 스키마 객체는 다음을 포함할 수 있어요:
- 일반 테이블의 UNIQUE 와 PRIMARY KEY 제약을 구현하는 데 사용되는 "sqlite_autoindex_TABLE_N" 형태 이름의 인덱스.
- AUTOINCREMENT 를 사용하는 테이블의 최대 역사적 INTEGER PRIMARY KEY 를 추적하는 데 사용되는 "sqlite_sequence"라는 이름의 테이블.
- "sqlite_statN" 형태의 이름을 가진 테이블(N 은 정수). 이러한 테이블은 ANALYZE 명령이 수집한 데이터베이스 통계를 저장하며, 쿼리 플래너가 각 쿼리에 가장 좋은 알고리즘을 결정하는 데 사용해요.
항상 "sqlite_"로 시작하는 새로운 내부 스키마 객체 이름이 향후 릴리스에서 SQLite 파일 형식에 추가될 수 있어요.
2.6.3. sqlite_sequence 테이블
sqlite_sequence 테이블은 AUTOINCREMENT 구현을 돕는 데 사용되는 내부 테이블이에요. sqlite_sequence 테이블은 AUTOINCREMENT 정수 primary key 를 가진 일반 테이블이 생성될 때마다 자동으로 만들어져요. 생성되면 sqlite_sequence 테이블은 sqlite_schema 테이블에 영원히 존재해요; 드롭할 수 없어요. sqlite_sequence 테이블의 스키마는:
CREATE TABLE sqlite_sequence(name,seq);
AUTOINCREMENT 를 사용하는 각 일반 테이블마다 sqlite_sequence 테이블에 단일 행이 있어요. 테이블의 이름(sqlite_schema.name 에 나타나는 대로)은 sqlite_sequence.name 필드에 있고, 그 테이블에 삽입된 가장 큰 INTEGER PRIMARY KEY 는 sqlite_sequence.seq 필드에 있어요. AUTOINCREMENT 테이블의 새 자동 생성 정수 primary key 는 그 테이블의 sqlite_sequence.seq 필드보다 크다고 보장돼요. AUTOINCREMENT 테이블의 sqlite_sequence.seq 필드가 이미 가장 큰 정수 값(9223372036854775807)이라면, 자동 생성 정수 primary 로 새 행을 추가하려는 시도는 SQLITE_FULL 에러로 실패해요. AUTOINCREMENT 테이블에 새 항목이 삽입될 때 필요하면 sqlite_sequence.seq 필드가 자동으로 갱신돼요. 테이블이 드롭되면 AUTOINCREMENT 테이블의 sqlite_sequence 행이 자동으로 삭제돼요. AUTOINCREMENT 테이블이 갱신될 때 AUTOINCREMENT 테이블의 sqlite_sequence 행이 존재하지 않으면 새 sqlite_sequence 행이 만들어져요. AUTOINCREMENT 테이블의 sqlite_sequence.seq 값이 수동으로 정수가 아닌 다른 것으로 설정되고 이후 AUTOINCREMENT 테이블을 삽입하거나 갱신하려는 시도가 있으면, 동작은 정의되지 않아요.
애플리케이션 코드는 sqlite_sequence 테이블을 수정할 수 있어요. 새 행을 추가하거나, 행을 삭제하거나, 기존 행을 수정할 수 있어요. 하지만 애플리케이션 코드는 sqlite_sequence 테이블이 이미 존재하지 않으면 만들 수 없어요. 애플리케이션 코드는 sqlite_sequence 테이블에서 모든 항목을 삭제할 수 있지만, sqlite_sequence 테이블을 드롭할 수는 없어요.
2.6.4. sqlite_stat1 테이블
sqlite_stat1 은 ANALYZE 명령이 만들고, 쿼리 플래너가 쿼리를 수행하는 더 나은 방법을 찾는 데 사용할 수 있는 테이블과 인덱스에 대한 보충 정보를 담는 데 사용되는 내부 테이블이에요. 애플리케이션은 sqlite_stat1 테이블을 갱신하거나, 삭제하거나, 삽입하거나, 드롭할 수 있지만, sqlite_stat1 테이블을 만들거나 변경할 수는 없어요. sqlite_stat1 테이블의 스키마는 다음과 같아요:
CREATE TABLE sqlite_stat1(tbl,idx,stat);
보통 인덱스당 하나의 행이 있고, 인덱스는 sqlite_stat1.idx 컬럼의 이름으로 식별돼요. sqlite_stat1.tbl 컬럼은 인덱스가 속한 테이블의 이름이에요. 각 행에서 sqlite_stat1.stat 컬럼은 정수 목록과 그 뒤에 오는 0 개 이상의 인자로 구성된 문자열일 거예요. 이 목록의 첫 번째 정수는 인덱스의 대략적인 행 수예요. (인덱스의 행 수는 부분 인덱스가 아니면 테이블의 행 수와 같아요.) 두 번째 정수는 인덱스의 첫 컬럼에 같은 값을 가진 인덱스의 대략적인 행 수예요. 세 번째 정수는 처음 두 컬럼에 같은 값을 가진 인덱스의 행 수예요. N 번째 정수(N>1)는 처음 N-1 컬럼에 같은 값을 가진 인덱스의 대략적인 평균 행 수예요. K-컬럼 인덱스의 경우 stat 컬럼에 K+1 개의 정수가 있을 거예요. 인덱스가 고유하면 마지막 정수는 1 일 거예요.
stat 컬럼의 정수 목록 뒤에는 선택적으로 인자가 올 수 있고, 각 인자는 공백이 아닌 문자 시퀀스예요. 모든 인자 앞에는 단일 공백이 있어요. 인식되지 않는 인자는 조용히 무시돼요.
"unordered" 인자가 있으면 쿼리 플래너는 인덱스가 정렬되지 않았다고 가정하고 그 인덱스를 범위 쿼리나 정렬에 사용하지 않을 거예요.
"sz=NNN" 인자(NNN 은 1 개 이상의 숫자 시퀀스를 나타냄)는 테이블 또는 인덱스의 모든 레코드에 대한 평균 행 크기가 행당 NNN 바이트라는 뜻이에요. SQLite 쿼리 플래너는 "sz=NNN" 토큰이 제공하는 추정 행 크기 정보를 사용해 더 적은 디스크 I/O 가 필요한 더 작은 테이블과 인덱스를 선택하는 데 도움을 받을 수 있어요.
인덱스의 sqlite_stat1.stat 필드에 "noskipscan" 토큰이 있으면 그 인덱스가 skip-scan 최적화와 함께 사용되는 것을 막아요.
새 텍스트 토큰이 SQLite 의 향후 개선에서 stat 컬럼의 끝에 추가될 수 있어요. 호환성을 위해 stat 컬럼 끝의 인식되지 않는 토큰은 조용히 무시돼요.
sqlite_stat1.idx 컬럼이 NULL 이면, sqlite_stat1.stat 컬럼은 sqlite_stat1.tbl 이 식별하는 테이블의 대략적인 행 수인 단일 정수를 포함해요. sqlite_stat1.idx 컬럼이 sqlite_stat1.tbl 컬럼과 같으면 그 테이블은 WITHOUT ROWID 테이블이고 sqlite_stat1.stat 필드는 그 WITHOUT ROWID 테이블을 구현하는 인덱스 btree 에 대한 정보를 포함해요.
2.6.5. sqlite_stat2 테이블
sqlite_stat2 는 SQLite 가 SQLITE_ENABLE_STAT2 로 컴파일되고 SQLite 버전 번호가 3.6.18 (2009-09-11) 과 3.7.8 (2011-09-19) 사이일 때만 생성되고 사용돼요. sqlite_stat2 테이블은 3.6.18 이전이나 3.7.8 이후의 어떤 SQLite 버전도 읽거나 쓰지 않아요. sqlite_stat2 테이블은 인덱스 내 키 분포에 대한 추가 정보를 포함해요. sqlite_stat2 테이블의 스키마는 다음과 같아요:
CREATE TABLE sqlite_stat2(tbl,idx,sampleno,sample);
sqlite_stat2 테이블의 각 행에서 sqlite_stat2.idx 컬럼과 sqlite_stat2.tbl 컬럼은 그 행이 설명하는 인덱스를 식별해요. 보통 각 인덱스에 대해 sqlite_stat2 테이블에 10 개의 행이 있어요.
sqlite_stat2.sampleno 가 0 과 9 사이(양끝 포함)인 인덱스의 sqlite_stat2 항목은 인덱스의 가장 왼쪽 키 값을 인덱스를 따라 균등한 간격으로 취한 샘플이에요. C 를 인덱스의 행 수라고 하자. 그러면 샘플링된 행은 rownumber = (iC2 + C)/20 이에요.
이전 식의 변수 i 는 0 과 9 사이에서 변해요. 개념적으로 인덱스 공간은 10 개의 균일한 버킷으로 나뉘고 샘플은 각 버킷의 중간 행이에요.
sqlite_stat2 의 형식은 레거시 참조용으로 여기에 기록돼요. 최근 SQLite 버전은 더 이상 sqlite_stat2 를 지원하지 않으며, sqlite_stat2 테이블이 존재하면 그냥 무시돼요.
2.6.6. sqlite_stat3 테이블
sqlite_stat3 는 SQLite 가 SQLITE_ENABLE_STAT3 또는 SQLITE_ENABLE_STAT4 로 컴파일되고 SQLite 버전 번호가 3.7.9 (2011-11-01) 이상일 때만 사용돼요. sqlite_stat3 테이블은 3.7.9 이전의 어떤 SQLite 버전도 읽거나 쓰지 않아요. SQLITE_ENABLE_STAT4 컴파일 타임 옵션을 사용하고 SQLite 버전 번호가 3.8.1 (2013-10-17) 이상이면 sqlite_stat3 는 읽힐 수 있지만 쓰이지는 않아요. sqlite_stat3 테이블은 인덱스 내 키 분포에 대한 추가 정보, 즉 쿼리 플래너가 더 나은 더 빠른 쿼리 알고리즘을 설계하는 데 사용할 수 있는 정보를 포함해요. sqlite_stat3 테이블의 스키마는 다음과 같아요:
CREATE TABLE sqlite_stat3(tbl,idx,nEq,nLt,nDLt,sample);
보통 각 인덱스에 대해 sqlite_stat3 테이블에 여러 항목이 있어요. sqlite_stat3.sample 컬럼은 sqlite_stat3.idx 와 sqlite_stat3.tbl 이 식별하는 인덱스의 가장 왼쪽 필드 값을 담아요. sqlite_stat3.nEq 컬럼은 가장 왼쪽 컬럼이 샘플과 정확히 일치하는 인덱스의 항목 대략 수를 담아요. sqlite_stat3.nLt 는 가장 왼쪽 컬럼이 샘플보다 작은 인덱스의 항목 대략 수를 담아요. sqlite_stat3.nDLt 컬럼은 샘플보다 작은 인덱스의 고유한 가장 왼쪽 항목의 대략 수를 담아요.
인덱스당 sqlite_stat3 항목 수는 임의일 수 있어요. ANALYZE 명령은 보통 키 공간에 분산되고 큰 nEq 값을 가진 10 에서 40 개 사이의 샘플을 포함하는 sqlite_stat3 테이블을 생성해요.
올바른 형식의 sqlite_stat3 테이블에서 단일 인덱스에 대한 샘플은 인덱스에 나타나는 것과 같은 순서로 나타나야 해요. 즉, 가장 왼쪽 컬럼이 S1 인 항목이 가장 왼쪽 컬럼이 S2 인 항목보다 인덱스 b-tree 에서 더 앞에 있으면, sqlite_stat3 테이블에서 샘플 S1 은 샘플 S2 보다 더 작은 rowid 를 가져야 해요.
2.6.7. sqlite_stat4 테이블
sqlite_stat4 는 SQLite 가 SQLITE_ENABLE_STAT4 로 컴파일되고 SQLite 버전 번호가 3.8.1 (2013-10-17) 이상일 때만 생성되고 사용돼요. sqlite_stat4 테이블은 3.8.1 이전의 어떤 SQLite 버전도 읽거나 쓰지 않아요. sqlite_stat4 테이블은 인덱스 내 키 분포 또는 WITHOUT ROWID 테이블의 primary key 의 키 분포에 대한 추가 정보를 포함해요. 쿼리 플래너는 때때로 sqlite_stat4 테이블의 추가 정보를 사용해 더 나은 더 빠른 쿼리 알고리즘을 설계할 수 있어요. sqlite_stat4 테이블의 스키마는 다음과 같아요:
CREATE TABLE sqlite_stat4(tbl,idx,nEq,nLt,nDLt,sample);
보통 통계를 사용할 수 있는 각 인덱스에 대해 sqlite_stat4 테이블에 10 에서 40 개 사이의 항목이 있지만, 이 제한은 엄격한 경계가 아니에요. sqlite_stat4 테이블의 컬럼 의미는 다음과 같아요:
| Column | Description |
|---|---|
| tbl | The sqlite_stat4.tbl column holds name of the table that owns the index that the row describes |
| idx | The sqlite_stat4.idx column holds name of the index that the row describes, or in the case of an sqlite_stat4 entry for a WITHOUT ROWID table, the name of the table itself. |
| sample | The sqlite_stat4.sample column holds a BLOB in the record format that encodes the indexed columns followed by the rowid for a rowid table or by the columns of the primary key for a WITHOUT ROWID table. The sqlite_stat4.sample BLOB for the WITHOUT ROWID table itself contains just the columns of the primary key. Let the number of columns encoded by the sqlite_stat4.sample blob be N. For indexes on an ordinary rowid table, N will be one more than the number of columns indexed. For indexes on WITHOUT ROWID tables, N will be the number of columns indexed plus the number of columns in the primary key. For a WITHOUT ROWID table, N will be the number of columns in the primary key. |
| nEq | The sqlite_stat4.nEq column holds a list of N integers where the K-th integer is the approximate number of entries in the index whose left-most K columns exactly match the K left-most columns of the sample. |
| nLt | The sqlite_stat4.nLt column holds a list of N integers where the K-th integer is the approximate number of entries in the index whose K left-most columns are collectively less than the K left-most columns of the sample. |
| nDLt | The sqlite_stat4.nDLt column holds a list of N integers where the K-th integer is the approximate number of entries in the index that are distinct in the first K columns and where the left-most K columns are collectively less than the left-most K columns of the sample. |
sqlite_stat4 는 sqlite_stat3 테이블의 일반화예요. sqlite_stat3 테이블은 인덱스의 가장 왼쪽 컬럼에 대한 정보를 제공하는 반면, sqlite_stat4 테이블은 인덱스의 모든 컬럼에 대한 정보를 제공해요.
인덱스당 sqlite_stat4 항목 수는 임의일 수 있어요. ANALYZE 명령은 보통 키 공간에 분산되고 큰 nEq 값을 가진 10 에서 40 개 사이의 샘플을 포함하는 sqlite_stat4 테이블을 생성해요.
올바른 형식의 sqlite_stat4 테이블에서 단일 인덱스에 대한 샘플은 인덱스에 나타나는 것과 같은 순서로 나타나야 해요. 즉, 항목 S1 이 항목 S2 보다 인덱스 b-tree 에서 더 앞에 있으면, sqlite_stat4 테이블에서 샘플 S1 은 샘플 S2 보다 더 작은 rowid 를 가져야 해요.
3. 롤백 저널 (The Rollback Journal)
롤백 저널은 각 SQLite 데이터베이스 파일과 연관된 파일로, 트랜잭션 중에 데이터베이스 파일을 초기 상태로 복원하는 데 사용되는 정보를 담아요. 롤백 저널 파일은 항상 데이터베이스 파일과 같은 디렉토리에 있고, 데이터베이스 파일과 같은 이름에 "-journal" 문자열이 붙어요. 주어진 데이터베이스와 연관된 롤백 저널은 단 하나만 있을 수 있으므로, 한 번에 단일 데이터베이스에 대해 하나의 쓰기 트랜잭션만 열릴 수 있어요.
데이터베이스의 정보를 담은 페이지가 수정되기 전에, 그 페이지의 수정되지 않은 원본 내용이 롤백 저널에 기록돼요. 트랜잭션이 중단되어 롤백해야 하면, 롤백 저널을 사용해 데이터베이스를 원래 상태로 복원할 수 있어요. 프리리스트 리프 페이지는 롤백 시 복원해야 할 정보를 담지 않으므로 디스크 I/O 를 줄이기 위해 수정 전에 저널에 기록되지 않아요.
트랜잭션이 애플리케이션 크래시, 운영체제 크래시, 하드웨어 전원 실패나 크래시로 인해 중단되면 메인 데이터베이스 파일이 일관되지 않은 상태로 남을 수 있어요. 다음에 SQLite 가 데이터베이스 파일을 열려고 시도할 때 롤백 저널 파일의 존재가 감지되고, 저널이 자동으로 재생되어 데이터베이스를 불완전한 트랜잭션 시작 시점의 상태로 복원해요.
롤백 저널은 존재하고 유효한 헤더를 포함할 때만 유효한 것으로 간주돼요. 따라서 트랜잭션은 세 가지 방법 중 하나로 커밋될 수 있어요:
- 롤백 저널 파일을 삭제할 수 있고,
- 롤백 저널 파일을 0 길이로 잘라낼 수 있고,
- 롤백 저널의 헤더를 잘못된 헤더 텍스트(예: 모두 0)로 덮어쓸 수 있어요.
이 세 가지 트랜잭션 커밋 방법은 각각 journal_mode pragma 의 DELETE, TRUNCATE, PERSIST 설정에 해당해요.
유효한 롤백 저널은 다음 형식의 헤더로 시작해요:
롤백 저널 헤더 형식
| Offset | Size | Description |
|---|---|---|
| 0 | 8 | Header string: 0xd9, 0xd5, 0x05, 0xf9, 0x20, 0xa1, 0x63, 0xd7 |
| 8 | 4 | The "Page Count" - The number of pages in the next segment of the journal, or -1 to mean all content to the end of the file |
| 12 | 4 | A random nonce for the checksum |
| 16 | 4 | Initial size of the database in pages |
| 20 | 4 | Size of a disk sector assumed by the process that wrote this journal. |
| 24 | 4 | Size of pages in this journal. |
롤백 저널 헤더는 단일 섹터 크기(오프셋 20 의 섹터 크기 정수로 정의)까지 0 으로 채워져요. 헤더는 자체로 한 섹터 안에 있어서, 섹터를 쓰는 동안 정전이 발생하면 헤더 뒤의 정보가 (바라건대) 손상되지 않도록 해요.
헤더와 0 패딩 뒤에는 0 개 이상의 페이지 레코드가 있어요. 각 페이지 레코드는 변경되기 전의 데이터베이스 파일의 페이지 내용 복사본을 저장해요. 같은 페이지는 단일 롤백 저널 안에 두 번 이상 나타나지 않을 수 있어요. 불완전한 트랜잭션을 롤백하려면 프로세스는 롤백 저널을 처음부터 끝까지 읽고 저널에서 발견한 페이지를 데이터베이스 파일의 적절한 위치에 다시 쓰기만 하면 돼요.
데이터베이스 페이지 크기(저널 헤더의 오프셋 24 의 정수 값)를 N 이라고 하자. 그러면 페이지 레코드의 형식은 다음과 같아요:
롤백 저널 페이지 레코드 형식
| Offset | Size | Description |
|---|---|---|
| 0 | 4 | The page number in the database file |
| 4 | N | Original content of the page prior to the start of the transaction |
| N+4 | 4 | Checksum |
체크섬은 다음과 같이 계산되는 부호 없는 32비트 정수예요:
- 체크섬을 저널 헤더의 오프셋 12 에 있는 체크섬 nonce 값으로 초기화해요.
- 인덱스 X 를 N-200 으로 초기화해요 (N 은 데이터베이스 페이지의 바이트 크기).
- 페이지의 오프셋 X 의 바이트를 8비트 부호 없는 정수로 해석하고 그 정수의 값을 체크섬에 더해요.
- X 에서 200 을 빼요.
- X 가 0 보다 크거나 같으면 3 단계로 돌아가요.
체크섬 값은 정전 후 저널 페이지 레코드의 불완전한 쓰기를 방지하는 데 사용돼요. 트랜잭션이 시작될 때마다 다른 무작위 nonce 가 사용되어, 쓰지 않은 섹터가 우연히 이전 저널의 일부였던 같은 페이지의 데이터를 포함할 위험을 최소화해요. 트랜잭션마다 nonce 를 바꿈으로써 디스크의 오래된 데이터는 여전히 잘못된 체크섬을 생성하고 높은 확률로 감지될 거예요. 체크섬은 성능상 이유로 데이터 레코드에서 32비트 워드의 희소 샘플만 사용해요. SQLite 3.0.0 계획 단계의 설계 연구에서 전체 페이지에 대한 체크섬이 상당한 성능 저하를 보였어요.
저널 헤더의 오프셋 8 의 페이지 카운트 값을 M 이라고 하자. M 이 0 보다 크면 M 개의 페이지 레코드 뒤에 저널 파일이 다음 섹터 크기의 배수까지 0 으로 채워지고 다른 저널 헤더가 삽입될 수 있어요. 같은 저널 안의 모든 저널 헤더는 같은 데이터베이스 페이지 크기와 섹터 크기를 포함해야 해요.
초기 저널 헤더에서 M 이 -1 이면, 뒤따르는 페이지 레코드의 수는 저널 파일의 나머지 사용 가능 공간에 몇 개의 페이지 레코드가 들어갈지 계산해서 구해요.
4. Write-Ahead Log (WAL)
버전 3.7.0 (2010-07-21) 부터 SQLite 는 "write-ahead log" 또는 "WAL"이라는 새 트랜잭션 제어 메커니즘을 지원해요. 데이터베이스가 WAL 모드에 있으면 그 데이터베이스에 대한 모든 연결이 WAL 을 사용해야 해요. 특정 데이터베이스는 롤백 저널 또는 WAL 중 하나를 사용할 거고, 둘을 동시에 사용하지는 않아요. WAL 은 항상 데이터베이스 파일과 같은 디렉토리에 있고, 데이터베이스 파일과 같은 이름에 "-wal" 문자열이 붙어요.
4.1. WAL 파일 형식
WAL 파일은 헤더와 그 뒤에 오는 0 개 이상의 "프레임(frame)"으로 구성돼요. 각 프레임은 데이터베이스 파일의 단일 페이지의 수정된 내용을 기록해요. 데이터베이스에 대한 모든 변경은 WAL 에 프레임을 써서 기록돼요. 커밋 마커를 포함하는 프레임이 쓰일 때 트랜잭션이 커밋돼요. 단일 WAL 은 여러 트랜잭션을 기록할 수 있고 보통 기록해요. 주기적으로 WAL 의 내용이 "체크포인트(checkpoint)"라는 작업에서 데이터베이스 파일로 다시 전송돼요.
단일 WAL 파일은 여러 번 재사용될 수 있어요. 즉, WAL 이 프레임으로 가득 찬 다음 체크포인트되고, 그런 다음 새 프레임이 이전 프레임을 덮어쓸 수 있어요. WAL 은 항상 시작부터 끝으로 자라요. 각 프레임에 붙은 체크섬과 카운터는 WAL 안의 어떤 프레임이 유효하고 어떤 것이 이전 체크포인트의 잔여물인지 결정하는 데 사용돼요.
WAL 헤더는 32 바이트 크기이고 다음 여덟 개의 big-endian 32비트 부호 없는 정수 값으로 구성돼요:
WAL 헤더 형식
| Offset | Size | Description |
|---|---|---|
| 0 | 4 | Magic number. 0x377f0682 or 0x377f0683 |
| 4 | 4 | File format version. Currently 3007000. |
| 8 | 4 | Database page size. Example: 1024 |
| 12 | 4 | Checkpoint sequence number |
| 16 | 4 | Salt-1: random integer incremented with each checkpoint |
| 20 | 4 | Salt-2: a different random number for each checkpoint |
| 24 | 4 | Checksum-1: First part of a checksum on the first 24 bytes of header |
| 28 | 4 | Checksum-2: Second part of the checksum on the first 24 bytes of header |
wal-header 바로 다음에 0 개 이상의 프레임이 있어요. 각 프레임은 24바이트 프레임-헤더와 그 뒤에 오는 page-size 바이트의 페이지 데이터로 구성돼요. 프레임-헤더는 다음과 같은 여섯 개의 big-endian 32비트 부호 없는 정수 값이에요:
WAL 프레임 헤더 형식
| Offset | Size | Description |
|---|---|---|
| 0 | 4 | Page number |
| 4 | 4 | For commit records, the size of the database file in pages after the commit. For all other records, zero. |
| 8 | 4 | Salt-1 copied from the WAL header |
| 12 | 4 | Salt-2 copied from the WAL header |
| 16 | 4 | Checksum-1: Cumulative checksum up through and including this page |
| 20 | 4 | Checksum-2: Second half of the cumulative checksum. |
프레임은 다음 조건이 모두 참일 때만 유효한 것으로 간주돼요:
- 프레임-헤더의 salt-1 과 salt-2 값이 wal-header 의 salt 값과 일치하고,
- 프레임-헤더의 마지막 8 바이트의 체크섬 값이 WAL 헤더의 처음 24 바이트와, 현재 프레임을 포함한 모든 프레임의 첫 8 바이트와 내용에 대해 연속으로 계산된 체크섬과 정확히 일치한다.
4.2. 체크섬 알고리즘
체크섬은 입력을 짝수 개의 부호 없는 32비트 정수, x(0) 부터 x(N) 으로 해석해 계산돼요. WAL 헤더의 처음 4바이트의 매직 숫자가 0x377f0683 이면 32비트 정수는 big-endian 이고, 매직 숫자가 0x377f0682 이면 정수는 little-endian 이에요. 체크섬 값은 체크섬을 계산하는 데 어떤 바이트 순서를 사용하든 항상 프레임 헤더에 big-endian 형식으로 저장돼요.
체크섬 알고리즘은 길이가 8 바이트의 배수인 내용에 대해서만 작동해요. 즉, 입력이 x(0) 부터 x(N) 이라면 N 은 홀수여야 해요. 체크섬 알고리즘은 다음과 같아요:
s0 = s1 = 0
for i from 0 to n-1 step 2:
s0 += x(i) + s1;
s1 += x(i+1) + s0;
endfor
# result in s0 and s1
출력 s0 와 s1 은 둘 다 역순으로 된 Fibonacci 가중치를 사용하는 가중 체크섬이에요. (가장 큰 Fibonacci 가중치는 합산되는 시퀀스의 첫 요소에서 발생해요.) s1 값은 시퀀스의 모든 32비트 정수 항을 포함하는 반면 s0 은 마지막 항을 생략해요.
4.3. 체크포인트 알고리즘
체크포인트에서 WAL 은 먼저 VFS 의 xSync 메서드를 사용해 영구 저장소에 플러시돼요. 그런 다음 WAL 의 유효한 내용이 데이터베이스 파일로 전송돼요. 마지막으로 데이터베이스가 다른 xSync 메서드 호출로 영구 저장소에 플러시돼요. xSync 작업은 쓰기 장벽 역할을 해요. xSync 전에 시작된 모든 쓰기는 xSync 후에 시작되는 어떤 쓰기 시작 전에 완료되어야 해요.
체크포인트는 완료까지 실행될 필요가 없어요. 일부 독자가 데이터베이스 파일에 포함된 데이터로 이전 트랜잭션을 여전히 사용하고 있을 수도 있어요. 그 경우 WAL 파일에서 데이터베이스로 더 새로운 트랜잭션의 내용을 전송하면 이전 트랜잭션을 여전히 사용하는 독자의 밑에서 내용이 삭제될 거예요. 이를 피하기 위해 체크포인트는 모든 독자가 WAL 의 마지막 트랜잭션을 사용할 때만 완료까지 실행돼요.
4.4. WAL 재설정
완전한 체크포인트 후에, 다른 연결이 WAL 을 사용하는 트랜잭션에 없으면, 이후 쓰기 트랜잭션이 WAL 파일을 처음부터 덮어쓸 수 있어요. 이를 "WAL 재설정"이라고 불러요. 첫 번째 새 쓰기 트랜잭션의 시작에서 WAL 헤더 salt-1 값이 증가하고 salt-2 값이 무작위화돼요. salt 에 대한 이러한 변경은 이미 체크포인트됐지만 아직 덮어쓰지 않은 WAL 의 오래된 프레임을 무효화하고, 그것들이 다시 체크포인트되지 않도록 방지해요.
WAL 파일은 재설정 시 선택적으로 잘릴 수 있지만, 잘릴 필요는 없어요. WAL 을 자르지 않으면 성능이 보통 조금 더 좋은데, 파일시스템이 일반적으로 파일을 키우는 것보다 기존 파일을 덮어쓰는 것이 더 빠르기 때문이에요.
4.5. 읽기 알고리즘
데이터베이스에서 페이지(페이지 번호 P 라고 하자)를 읽으려면 읽는 쪽은 먼저 WAL 에 페이지 P 가 포함되어 있는지 확인해요. 그렇다면, 커밋 프레임이 뒤따르거나 자체가 커밋 프레임인 페이지 P 의 마지막 유효 인스턴스가 읽은 값이 돼요. WAL 에 유효하고 커밋 프레임이거나 커밋 프레임이 뒤따르는 페이지 P 의 복사본이 없으면, 페이지 P 는 데이터베이스 파일에서 읽혀요.
읽기 트랜잭션을 시작하려면 읽는 쪽이 WAL 의 유효 프레임 수를 "mxFrame"으로 기록해요. 읽는 쪽은 이후 모든 읽기 작업에 이 기록된 mxFrame 값을 사용해요. 새 트랜잭션은 WAL 에 추가될 수 있지만, 읽는 쪽이 원래 mxFrame 값을 사용하고 이후에 추가된 내용을 무시하는 한, 읽는 쪽은 한 시점의 데이터베이스 일관된 스냅샷을 볼 거예요. 이 기법은 여러 동시 읽는 쪽이 동시에 데이터베이스 내용의 다른 버전을 볼 수 있게 해줘요.
이전 문단의 읽기 알고리즘은 올바르게 작동하지만, 페이지 P 의 프레임이 WAL 안 어디에나 나타날 수 있으므로 읽는 쪽은 페이지 P 프레임을 찾기 위해 전체 WAL 을 스캔해야 해요. WAL 이 크면(보통 수 메가바이트) 그 스캔은 느릴 수 있고 읽기 성능이 저하돼요. 이 문제를 극복하기 위해 wal-index 라는 별도의 데이터 구조가 특정 페이지의 프레임 검색을 빠르게 하기 위해 유지돼요.
4.6. WAL-Index 형식
개념적으로 wal-index 는 공유 메모리이지만, 현재 VFS 구현은 운영체제 이식성을 위해 메모리-맵 파일을 사용해요. 메모리-맵 파일은 데이터베이스와 같은 디렉토리에 있고 데이터베이스와 같은 이름에 "-shm" 접미어가 붙어요. wal-index 는 공유 메모리이므로, 클라이언트가 다른 머신에 있을 때 SQLite 는 네트워크 파일시스템에서 journal_mode=WAL 을 지원하지 않아요. 데이터베이스의 모든 클라이언트가 같은 메모리를 공유할 수 있어야 하기 때문이에요.
wal-index 의 목적은 이 질문에 빠르게 답하는 것이에요: 페이지 번호 P 와 최대 WAL 프레임 인덱스 M 이 주어지면, M 을 초과하지 않는 페이지 P 에 대한 가장 큰 WAL 프레임 인덱스를 반환하거나, M 을 초과하지 않는 페이지 P 의 프레임이 없으면 NULL 을 반환한다.
이전 문단의 M 값은 섹션 4.5 에서 정의한 "mxFrame" 값으로, 트랜잭션 시작 시 읽히고 읽는 쪽이 사용할 WAL 의 최대 프레임을 정의해요.
wal-index 는 일시적이에요. 크래시 후 wal-index 는 원래 WAL 파일에서 재구성돼요. VFS 는 마지막 연결이 닫힐 때 wal-index 의 헤더를 잘라내거나 0 으로 만들도록 요구돼요. wal-index 는 일시적이므로 아키텍처별 형식을 사용할 수 있어요. 크로스 플랫폼일 필요가 없어요. 따라서 모든 값을 big endian 으로 저장하는 데이터베이스와 WAL 파일 형식과 달리, wal-index 는 다중 바이트 값을 호스트 컴퓨터의 네이티브 바이트 순서로 저장해요.
이 문서는 데이터베이스 파일의 영구 상태에 관심이 있고, wal-index 는 일시적 구조이므로 wal-index 의 형식에 대한 추가 정보는 여기 제공되지 않아요. wal-index 의 형식에 대한 추가 세부 사항은 별도의 WAL-index File Format 문서에 포함돼 있어요.
더 알아보기 (Learn more)
- WAL-index File Format — wal-index 의 상세 형식
- WAL 모드 — write-ahead logging 소개
- 데이터타입 — 레코드와 직렬 타입
- PRAGMA journal_mode — 저널 모드 설정
- ANALYZE — sqlite_statN 테이블 생성