SQLite 톺아보기

D. Richard Hipp이 14년간이나 자비로운 종신 독재자(BDFL, Benevolent Dictator For Life) 모델로 개발을 이어오고 있는 SQLite의 특징과 API, 빌드 과정을 하나씩 살펴본다.

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에서 소스를 내려 받았다면 manifestmanifest.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를 설치한다.

  1. 모든 이미지는 『The Definitive Guide to SQLite』 책에서 발췌했다. 

is a collection of Papers I have written.
© 2000 - Sang-Kil Park Except where otherwise noted, content on this site is licensed under a CC BY 4.0.
This site design was brought from Distill.