Go 서버에서 고성능 SQLite 읽기

발행: (2025년 12월 16일 오전 05:44 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

Workload Assumptions

이 권장 사항은 다음을 전제로 합니다:

  • 읽기가 대부분이며(쓰기 작업은 드물거나 오프라인)
  • 단일 서버 프로세스가 데이터베이스를 소유함
  • 여러 goroutine이 동시에 SELECT를 실행함
  • 데이터베이스 크기가 대부분 RAM 또는 OS 캐시에 들어감
  • 전원 손실 시 내구성은 중요하지 않음

이 가정이 바뀌면 아래의 몇몇 트레이드‑오프를 재검토해야 합니다.

1. Use WAL Mode (Non‑Negotiable)

PRAGMA journal_mode = WAL;

왜 중요한가

  • 읽기가 쓰기를 차단하지 않음
  • 읽기가 다른 읽기를 차단하지 않음
  • 읽는 동안 메인 데이터베이스 파일에 접근하지 않음
  • 페이지를 WAL + DB에서 순차적으로 읽음

읽기‑중심 워크로드에서는 WAL이 SQLite를 잠금‑없는 리더 엔진으로 바꿔줍니다.

2. Set synchronous = NORMAL

PRAGMA synchronous = NORMAL;

WAL 모드에서 이것이 최적점입니다:

  • 트랜잭션은 원자적이고 일관성을 유지함
  • 매 커밋마다 추가 fsync가 없음
  • 디스크 플러시 횟수가 크게 감소함

읽기‑중심 시스템에서는 갑작스러운 전원 손실에 대한 내구성보다 지연 시간과 처리량이 더 중요합니다.

3. Aggressively Increase Page Cache

PRAGMA cache_size = -65536;  -- ~64 MiB per connection
  • 음수 값은 킬로바이트 단위이며, 페이지 수가 아님
  • 캐시는 연결당 별도임
  • 큰 캐시는 페이지 폴트와 B‑tree 탐색 비용을 감소시킴

캐시가 클수록 디스크 읽기가 줄어들고 스캔 속도가 빨라집니다.

4. Enable Memory‑Mapped I/O (Huge Win)

PRAGMA mmap_size = 20000000000;  -- 20 GiB (or larger than DB)

메모리‑맵 I/O는 OS 페이지 캐시가 무거운 작업을 수행하도록 합니다:

  • 페이지당 read() 시스템 콜이 없음
  • 커널이 자동으로 readahead 수행
  • 전체 테이블 스캔이 크게 빨라짐

데이터베이스가 RAM에 들어간다면 SQLite 읽기는 메모리 속도에 근접하게 됩니다.
경험 법칙: mmap_size를 데이터베이스 파일보다 크게 설정하세요.

5. Keep Temporary Objects in Memory

PRAGMA temp_store = MEMORY;

다음 작업에서 디스크 I/O를 회피합니다:

  • 정렬
  • GROUP BY
  • 임시 인덱스
  • 서브쿼리 물리화

분석용 또는 스캔‑중심 쿼리에서는 이로 인해 보이지 않지만 비용이 큰 병목이 사라집니다.

6. Use Exclusive Locking (Single‑Process Optimization)

PRAGMA locking_mode = EXCLUSIVE;

장점

  • 파일 시스템 락/언락 시스템 콜이 감소
  • 쿼리당 지연 시간이 약간 낮아짐
  • 공유 메모리 락 조정이 필요 없음

다른 프로세스가 데이터베이스에 접근할 필요가 없을 때만 안전합니다.

7. Allow SQLite to Use Worker Threads

PRAGMA threads = 4;

활성화 효과:

  • 병렬 스캔
  • SELECT가 더 빨라짐
  • 다코어 머신에서 CPU 활용도 향상

Go의 goroutine 동시성과 잘 맞습니다.

8. Index Like Your Throughput Depends on It (It Does)

아무리 PRAGMA를 튜닝해도 나쁜 쿼리는 구제되지 않습니다.

가이드라인

  • WHERE, JOIN, ORDER BY에 사용되는 모든 컬럼에 인덱스 생성
  • 큰 테이블에 SELECT * 사용을 피함
  • EXPLAIN QUERY PLAN으로 실행 계획 확인

하나의 인덱스 누락이 1 ms 읽기를 200 ms 전체 스캔으로 만들 수 있습니다.

9. Keep Query Planner Stats Fresh

PRAGMA optimize;

실행 시점

  • 스키마 변경 후
  • 대량 데이터 삽입 후
  • 오래 살아있는 연결에서 주기적으로

SQLite가 가장 빠른 접근 경로를 선택하도록 보장합니다.

10. Read‑Only Mode for Extra Safety & Speed

PRAGMA query_only = ON;

장점

  • 실수로 인한 쓰기 방지
  • 일부 쓰기 관련 안전 검사 생략
  • 운영상 안전성 향상

데이터베이스가 실행 중에 절대 변경되지 않을 때 사용하세요.

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -65536;
PRAGMA mmap_size = 20000000000;
PRAGMA temp_store = MEMORY;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA threads = 4;
PRAGMA query_only = ON;

이 설정은 연결당 한 번 적용하면 됩니다(journal_mode는 지속됩니다).

Go‑Specific Notes

  • 연결 풀(database/sql) 사용
  • 많은 읽기 연결을 허용( WAL에서는 비용이 저렴)
  • 핫 경로에서는 준비된 문을 재사용
  • 읽기를 불필요하게 직렬화하지 않음

올바르게 구성하면 SQLite는 동시 읽기에서 매우 잘 확장됩니다.

Final Takeaway

SQLite는 느리지 않습니다. 잘못 구성된 SQLite가 느립니다.

WAL, 메모리‑맵 I/O, 적절한 캐시, 그리고 합리적인 내구성 트레이드‑오프만 갖추면 SQLite는 단일 파일에서 수백에서 수천 건의 읽기/초를 최소 메모리와 운영 복잡도로 충분히 처리할 수 있습니다.

읽기‑중심 워크로드와 단순한 배포 환경이라면 SQLite는 가장 효율적인 데이터베이스 중 하나입니다.

FreeDevTools

Check out: FreeDevTools

Any feedback or contributors are welcome! It’s online, open‑source, and ready for anyone to use.

⭐ Star it on GitHub: freedevtools

Back to Blog

관련 글

더 보기 »

Go 프로파일링(pprof 사용)

pprof란 무엇인가요? pprof는 Go의 내장 프로파일링 도구로, 애플리케이션의 런타임 데이터(예: CPU 사용량, 메모리 할당 등)를 수집하고 분석할 수 있게 해줍니다.

Go와 Rust에 대한 사랑 열변!

> 경고: 란트! 소개 나는 이 상황에 지쳤다. 몇 주마다 어떤 Rustacean이 여기 슬며시 들어와서 그들의 거만한 “하지만 두려움 없는 동시성을 시도해봤나요...” 라고 말한다.