SQLite 톺아보기
2015년 5월 21일 1차 개정
2014년 12월 23일 초안 작성
Features
- D. Richard Hipp이 14년동안 오픈소스로 개발을 리딩하고 있다. 또 하나의 자비로운 종신 독재자(BDFL, Benevolent Dictator For Life) 모델이다. 그는 아내와 2인 기업을 운영하고 있다. 세계적인 S/W가 됐지만 아내와 조촐하게 2인 기업을 운영하며 홀로 개발하고 있다.
- 라이센스가 Public Domain이다. 이 말은 코드 일부를 따서 내가 만든 프로덕트에 삽입하고 전부 내가 짠거라고 주장해도 아무 문제 없다는 뜻이다.
- C++이 아닌 C다. 코드는 10만 LOC이며 각 기능별 나뉜(eg. btree, malloc, time) 파일에는 당연히 수십개의 functions로만 구성되어 있다. 테스트 코드만 4500만 LOC가 넘는다. 신뢰가 중요한 DB인만큼 테스트에 엄청난 노력을 기울이고 있으며 100% 테스트 커버리지를 갖고 있다고 한다. SQLite 사이트의 글을 일본인이 소개한 블로그 참조. 구글 번역으로 보면 읽을만 하다.
- DB 전체가 단일 파일로 디스크에 저장되나 특정 상황에서는 메모리에 저장한다.
An SQLite database is normally stored in a single ordinary disk file. However, in certain circumstances, the database might be stored in memory.
- 단일 파일 구조 특성상 write시 전체 DB에 lock이 걸리고 항상 순차적으로 실행된다. 그러나 read는 multi-task로 수행된다.
It implements this simple design by locking the entire database file during writing. SQLite read operations can be multitasked, though writes can only be performed sequentially.
- transactional 하게 동작한다. 명시적으로
commit()
을 선언해야 하나 별도로 commit 하지않아도 table 단위의 액션이 수행될 경우 기존 변경 사항이 commit 된다.
Install
Yosemite에는 3.8.5가 기본 설치되어 있다. brew로 최신 버전을 설치할 수 있지만(현재 3.8.7.4) 충돌 방지를 위한 keg-only 팩키지므로 brew link --force sqlite3
로 링크를 걸어줘야 한다.
API
SQLite는 C API로 접근하는 구조이며 다른 언어에서는 이를 wrapping한 형태를 사용하게 된다. Python의 API 구현은 pysqlite라는 C wrapper이며 java 구현 또한 JNI를 사용하도록 되어 있다. Serverless한 특성상 클라이언트에 DB의 모든 핵심기능이 구현되어야 하므로 C API를 사용하지 않고 언어별 독립적인 형태로는 구현이 힘들다. API는 내부적으로 아래와 같은 계층구조로 동작한다.1
맥에서 C API 구현시 #include <sqlite3.h>
를 하면 include는 /usr/local/include
가 우선이지만 기본 설치된 /usr/lib
의 dylib를 참조한다. gcc 옵션에서 Library Path를 따로 지정해서 brew로 설치된 최신 버전의 dylib 위치 /usr/local/lib
를 가리켜야 한다. 또한 빌드시 -lsqlite3
를 명시해야 한다.
gcc -L/usr/local/lib -lsqlite3 simplesqlite3.c
SQLite 설치 버전을 구하는 간단한 C 샘플은 아래와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>
static int callback(void *NotUsed, int argc, char **argv, char **azColName) {
NotUsed = 0;
int i;
for (i = 0; i < argc; i++) {
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}
int main(int argc, char **argv) {
sqlite3 *db;
char *zErrMsg = 0;
int rc;
if (argc != 2) {
fprintf(stderr, "Usage: %s DATABASE\n", argv[0]);
exit(1);
}
rc = sqlite3_open(argv[1], &db);
if (rc) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
exit(1);
}
rc = sqlite3_exec(db, "SELECT sqlite_version()", callback, 0, &zErrMsg);
if (rc != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", zErrMsg);
/* This will free zErrMsg if assigned */
if (zErrMsg)
free(zErrMsg);
}
sqlite3_close(db);
return 0;
}
python도 비슷한데 /usr/bin/python
대신 /usr/local/bin/python
을 이용하면 최신 버전 sqlite를 import 할 수 있다.
두 버전이 동시에 시스템에 존재하므로 여러모로 혼란스럽다. 구분하는 가장 쉬운 방법은 SELECT sqlite_version()
로 라이브러리가 뱉는 버전을 확인하는 것. 3.8.5
가 나오면 맥 시스템에 기본 설치된 라이브러리가 적용된 것이고 3.8.7.4
가 나오면 brew로 설치한 최신 버전 라이브러리가 적용된 것이다.
Build
맥에 기본으로 설치되어 있고 brew로 설치되기 때문에 직접 빌드할 일은 없으나 src/func.c
를 수정해 사용자 정의 함수를 추가하는등의 작업을 한다면 직접 빌드해야 하므로 빌드 과정을 정리한다.
git에서 소스를 내려 받았다면 manifest
와 manifest.uuid
라는 파일을 생성해야 한다. 원래 fossil vcs를 사용하고 빌드시 그 파일들을 이용하기 때문이다.
#!/bin/sh
# See http://www.wtfpl.net/txt/copying for license details
# Creates a minimal manifest and manifest.uuid file so sqlite (and fossil) can build
git rev-parse --git-dir >/dev/null || exit 1
git log -1 --format=format:%ci%n | sed -e 's/ [-+].*$//;s/ /T/;s/^/D /' > manifest
echo $(git log -1 --format=format:%H) > manifest.uuid
프로젝트에 autoconf가 적용됐기 때문에 이후 빌드과정에 어려운 점은 없다. github에 보면 별도 디렉토리(bld)에서 빌드하는 예제가 있어 그대로 사용한다.
mkdir bld ;# Build will occur in a sibling directory
cd bld ;# Change to the build directory
../sqlite/configure --prefix=$HOME/out ;# Run the configure script
make ;# Run the makefile.
이미 SQLite가 시스템에 여러벌 설치되어 있어 구분하기 위해 별도 디렉토리(out)를 빌드 디렉토리로 지정했다.
make sqlite3.c ;# Build the "amalgamation" source file
github에는 make sqlite3.c
를 하라고 명시 되어 있는데 내 경우 make: 'sqlite3.c' is up to date.
로 더 이상 빌드되지 않았다. sqlite3.c
는 기능별로 쪼갠 C 파일을 하나로 합친 거대한 파일인데(이를 the amalgamation이라고 표현) 이렇게 합치는 이유는 나뉘어져 있을때에 비해 컴파일러 최적화가 훨씬 더 잘 동작해서 약 5% 정도 성능 개선 효과가 있다고 한다. 이렇게 합친 sqlite3.c
의 크기는 10만 LOC가 넘는다.
그리고 tcl을 설치하면 앞서 언급한 수천만 라인의 테스트케이스를 수행할 수 있다.
brew install tcl-tk
make test ;# Run some tests (requires Tcl)
아쉽게도 3 errors out of 138995 tests
가 나왔다.
Failures on these tests: oserror-1.1.1 oserror-1.1.2 oserror-1.1.3
13만개 중 oserror 관련해서 딱 3개 테스트가 실패했는데 추후에 원인을 찾아볼 예정이다. 이후 make 하면 .libs 디렉토리 하위에 각종 라이브러리와 바이너리가 설치된다. make install
을 하면 앞서 빌드 디렉토리로 선언한 out 하위에 bin, include, lib를 설치한다.
-
모든 이미지는 『The Definitive Guide to SQLite』 책에서 발췌했다. ↩