tiguclaw 데이터베이스(대화·기억·세션)의 백업·용량·아카이브를 다룬다. "백업 해줘·백업 되고 있어?·DB 가 너무 커·용량·디스크·오래된 대화 정리·데이터 옮기기·복구·날아가면" 류 요청, 그리고 데이터가 걸린 유지보수를 시작하기 전에 사용한다. ★백업은 v0.27.0 부터 **자동**이다(스케줄이 아니라 데몬 유지보수 루프) — 스케줄 목록에 없다고 '백업이 없다'고 결론내지 말고 `/status` 나 `<home>/data/backup/` 을 보라. 정리 ≠ 삭제: 콜드 레코드는 옮기되 지우지 않는다.
Installs into .claude/skills of the current project.
Are you the author of Db Maintenance?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tigu77-db-maintenance)
---
name: db-maintenance
description: tiguclaw 데이터베이스(대화·기억·세션)의 백업·용량·아카이브를 다룬다. "백업 해줘·백업 되고 있어?·DB 가 너무 커·용량·디스크·오래된 대화 정리·데이터 옮기기·복구·날아가면" 류 요청, 그리고 데이터가 걸린 유지보수를 시작하기 전에 사용한다. ★백업은 v0.27.0 부터 **자동**이다(스케줄이 아니라 데몬 유지보수 루프) — 스케줄 목록에 없다고 '백업이 없다'고 결론내지 말고 `/status` 나 `<home>/data/backup/` 을 보라. 정리 ≠ 삭제: 콜드 레코드는 옮기되 지우지 않는다.
reach: main
---
# DB 유지보수
`<home>/data/tiguclaw.db` 한 파일에 **기억·세션·대화 기록이 전부** 있다. 그래서 이 문서의
순서는 백업 → 용량 → 아카이브다. 크기는 나중에 문제가 되지만 **백업은 처음부터 문제다.**
## 0. 먼저 잰다 — **숫자를 여기서 읽지 말고 지금 재라**
★이 문서에 "현재 몇 MB · 몇 년 뒤 1GB" 같은 **기준선을 적어두지 않는다.** 그런 값은 적는
순간부터 늙고, 다음 사람이 그걸 사실로 믿는다. **재는 법만 둔다.**
```bash
DB="<home>/data/tiguclaw.db"
sqlite3 "$DB" "
WITH sz AS (SELECT
(SELECT SUM(pgsize) FROM dbstat WHERE name='transcripts') t,
(SELECT SUM(pgsize) FROM dbstat WHERE name LIKE 'transcripts_fts%') f,
(SELECT page_count*page_size FROM pragma_page_count(), pragma_page_size()) total),
grow AS (SELECT sum(length(content))*1.0 c, (max(ts)-min(ts))/86400000.0 days
FROM transcripts WHERE ts > (strftime('%s','now')-30*86400)*1000)
SELECT
'현재 '||round(total/1048576.0)||'MB (대화 '||round(t/1048576.0)||' + 색인 '||round(f/1048576.0)||' = '||round(100.0*(t+f)/total)||'%)',
'월 증가 ~'||round((c/nullif(days,0))*30*(1.0+f*1.0/nullif(t,0))/1048576.0)||'MB',
'1GB 까지 '||round((1073741824.0-total)/nullif((c/nullif(days,0))*30*(1.0+f*1.0/nullif(t,0)),0)/12.0,1)||'년'
FROM sz, grow;"
```
★**증가는 대화 원문만 세면 틀린다** — 검색 색인이 원문에 비례해 따라 붙는다(그 비율도 위
쿼리가 현재 값으로 잰다). 원문만 세서 증가율을 절반으로 잡은 적이 있다.
**판정:** `transcripts`(대화 원문) + `transcripts_fts`(그 색인)가 대부분이면 정상이다 —
둘 다 설계상 무한 증가고 대화가 쌓이는 만큼 는다. `events` 는 상한이 있어 안 는다.
**1GB 까지 몇 년이 남았는지**가 구조를 바꿀 때인지 아닌지의 눈금이고, SQLite 는 수 GB 도
잘 다루므로 **수백 MB 에서 손대는 건 이르다.**
## 1. 백업 — **이미 자동으로 돈다**
★**먼저 이것부터 알아라: 백업은 v0.27.0 부터 자동이다.** 스케줄이 아니라 **데몬 유지보수
루프**(코어 자기보전 루프)에서 하루 한 벌씩 `VACUUM INTO` 로 뜨고 7벌만
남긴다. **스케줄 목록에는 안 보인다** — 없다고 "백업 체계가 없다"고 결론내지 마라(중복 스케줄을 제안하지 마라).
**상태 확인은 두 가지로:**
```
/status → "백업: 3시간 전 · 7벌 (940MB)" 한 줄
ls -lt <home>/data/backup/ → 실제 파일
```
성공은 **일부러 조용하다**(매일 알림은 배경 소음이 된다). 실패와 첫 벌만 알린다.
끄려면 `settings.json` 의 `backup.enabled: false` — 기본은 켜짐이다.
**손으로 한 벌 더 뜨려면:** 대상 인스턴스의 실제 경로를 확인하고, 기존 백업과 겹치지 않는
새 출력 경로로 `VACUUM INTO`를 실행한다. 날짜만 붙이면 당일 자동 백업과 충돌할 수 있다.
출력 파일은 없거나 비어 있어야 하며, 기존 백업을 지워서 재시도하지 않는다.
```bash
DB="<home>/data/tiguclaw.db"
BACKUP="<home>/data/backup/manual-$(date +%Y%m%d-%H%M%S)-$$.db"
test -f "$DB" &&
mkdir -p "<home>/data/backup" &&
sqlite3 "$DB" "VACUUM INTO '$BACKUP';" &&
sqlite3 -readonly "$BACKUP" "PRAGMA integrity_check;"
```
실제 경로에 작은따옴표가 있으면 SQL 문자열에 그대로 삽입하지 말고 바인딩 가능한 도구를 쓴다.
위 `manual-*` 사본은 자동 7벌 보관 및 `/status` 집계 대상이 아니다. 보관·정리는 별도로 관리한다.
명령 성공과 검사 결과 `ok`를 확인한 뒤 백업 완료로 보고한다. 중단된 출력은 정상 백업으로 쓰지 않는다.
- 실행 중인 DB 본파일만 `cp`로 복사하면 WAL에 있는 최신 내용이 빠질 수 있다.
`VACUUM INTO`는 원본을 변경하지 않고 일관된 사본을 만든다. 읽기 트랜잭션을 사용하므로
**락이 전혀 없다**고 보장하지 않는다. 백업 중 원본의 `-wal`·`-shm`을 지우거나 옮기지 않는다.
- **주기·보관은 기본 매일 1회·7벌**이다. 같은 일을 하는 스케줄을 중복 등록하지 않는다.
- 같은 디스크의 사본은 디스크 고장을 대비하지 못한다. 외장·클라우드 이전은 사용자와 목적지를 정한다.
### 복구 — 백업 생성과 별도 작업
복구는 현재 DB를 선택한 시점으로 교체한다. 대상 인스턴스·백업 시점·교체 영향을 확인하고
사용자가 승인한 복구 범위에서 다음 순서로 진행한다.
1. 백업을 읽기 전용으로 열어 `PRAGMA integrity_check` 결과 `ok`와 필요한 데이터가 있는지 확인한다.
2. 대상 데몬과 그 DB를 여는 다른 프로세스를 모두 멈추고, 자동 재시작도 중지됐는지 확인한다.
3. 현재 `tiguclaw.db`와 존재하는 `-wal`·`-shm`을 **한 묶음으로 별도 위치에 보존**한다.
기존 WAL을 복구할 DB 옆에 남기지 않는다. 실행 중 체크포인트만 했다는 이유로 이 단계를 생략하지 않는다.
4. 검증한 백업의 사본을 대상 `tiguclaw.db` 위치에 놓고 소유권·권한을 확인한다. 원본 백업은 보존한다.
5. 데몬을 시작해 상태와 복구하려던 기록을 확인한다. 실패하면 다시 모든 접근을 멈추고
보존한 묶음으로 되돌린다. 새로 생긴 WAL과 이전 묶음을 섞지 않는다.
근거: [SQLite VACUUM INTO](https://www.sqlite.org/lang_vacuum.html),
[WAL 파일의 수명과 보존](https://www.sqlite.org/wal.html#the_wal_file).
## 2. WAL 파일이 커 보일 때 — **최고수위지 누수가 아니다**
`ls` 에서 `-wal` 이 수십~수백 MB 로 보여도 그 자체는 이상이 아니다. SQLite 는 WAL 파일을
**줄이지 않고 재사용**한다 — 큰 작업이 한 번 지나가면 그 크기가 그대로 박힌다.
★**파일 크기로 판정하지 마라. 실제 내용은 pragma 로 잰다:**
```bash
sqlite3 "$DB" "PRAGMA wal_checkpoint(PASSIVE);" # → busy | log | checkpointed (단위: 페이지)
```
`busy=0` 이고 `log` 가 작으면 **정상**이다(1페이지 = 4KB). 134페이지(=549KB)뿐인 경우가 있었고, 파일 크기만 보고 "체크포인트가 막혔다"고 오판했다.
파일 자체를 줄이려면 `PRAGMA wal_checkpoint(TRUNCATE)` 인데 **짧은 쓰기 락**이 걸린다 —
데몬을 멈춘 뒤나 재시작 직후에 한 번 하면 된다. 급한 일이 아니다(디스크만 차지한다).
## 3. 아카이브 — **"오래된"이 아니라 "안 읽힌 지 오래된"**
날짜로 자르면 **3년 전 것인데 어제도 읽은 대화**가 잘려나간다. 기준은 마지막으로 쓰인 때다.
- ★**정리 ≠ 삭제.** 콜드 레코드는 **별도 파일로 옮기고** 본 DB 에선 뺀다 — 지우지 않는다.
사용자가 "정리해줘" 라고 해도 **삭제는 명시 승인 없이는 하지 않는다**(비가역).
- **파생물은 다르다.** `transcripts_fts` 는 원문에서 **다시 만들 수 있으므로** 아카이브
대상이 아니다. 색인만 재생성해도 큰 폭으로 줄어드는 경우가 있다(원문은 안 건드린다).
- **착수 조건**: DB 가 수 GB 이거나 검색·부팅이 체감되게 느려질 때. 그 전엔 하지 마라 —
안 쓰는 구조를 몇 년 유지보수하게 된다.
★**설계 전에 재료부터 확인하라.** 세션 마지막 사용 시각(`threads.last_used_at`)은 있지만,
`transcripts` 행이 그 세션과 **이어지지 않는 경우가 많다**(서브에이전트·백그라운드 작업 등이
남긴 기록으로 **추정** — 확인된 바 아님). 한 인스턴스에선 대부분이 그랬다. 비율은 인스턴스마다
다르니 **아래 쿼리로 지금 재라.** 이걸 모르고 "마지막 사용 시각으로 자르면 된다"고 설계하면 **대부분을 못 건드리거나, 못
건드리는 것을 오래됐다고 지운다.** 먼저 그 비율부터 세라:
```bash
sqlite3 "$DB" "SELECT CASE WHEN th.claude_session_id IS NULL THEN '세션 연결 없음' ELSE '연결됨' END,
count(*), round(sum(length(t.content))/1048576.0,1)||'MB'
FROM transcripts t LEFT JOIN threads th ON th.claude_session_id = t.claude_session_id GROUP BY 1;"
```
## 4. 하지 말 것
- **DB 파일을 `cp`·`rsync` 로 백업하지 마라**(§1).
- **대화 기록을 지우지 마라.** 용량이 급해도 먼저 옮긴다. 삭제는 사용자 명시 승인 사안이다.
- **크기만 보고 손대지 마라** — 수백 MB 는 SQLite 에게 작다. 바꿀 근거는 *체감되는 느려짐*이다.
- **`VACUUM`(INTO 없이)을 습관처럼 돌리지 마라** — 전체를 다시 쓰느라 오래 걸리고 그동안 잠긴다.
조각이 실제로 문제일 때만(`PRAGMA freelist_count` 가 크다) 데몬을 멈추고 한다.