본문으로 건너뛰기

4.4 Instant Restore, Reset, Snapshot

되돌리기는 branching과 같은 저장 구조를 사용합니다. pageserver가 history window 안의 page version을 모두 보관하기 때문입니다. 과거 시점으로 돌아가려면 해당 LSN을 가리키는 timeline 하나를 만들면 됩니다. Neon은 이 기능을 세 가지 작업으로 제공합니다. restore는 branch를 자신의 과거나 다른 branch의 시점으로 덮어씁니다. reset은 자식 branch를 부모의 현재 상태로 덮어씁니다. snapshot은 시점에 이름을 붙여 history window와 무관하게 보관합니다.

history window

history window는 프로젝트 단위 설정으로, 변경 이력을 보관할 기간을 정합니다. restore와 과거 시점 branch 생성은 이 범위 안에서만 동작합니다.

플랜기본값최대
Free6시간6시간 (1 GB 상한)
Launch1일7일
Scale1일30일

window를 늘리면 GC가 오래된 layer를 지우지 않아 storage 비용이 늘어납니다. 비용 증가 폭은 쓰기량에 비례합니다. 쓰기가 적은 database는 30일로 늘려도 부담이 작습니다. 쓰기가 많은 database는 며칠만 늘려도 부담이 크게 늘어납니다. history window와 GC의 관계는 2.4 Timeline과 Branch 내부에서 설명했습니다.

Time Travel Assist: 시점 먼저 찾기

restore 전에 어느 시점이 맞는지 확인해야 합니다. Console의 SQL Editor에는 Time Travel Assist가 있습니다. 이 기능은 과거 시점의 데이터를 대상으로 read-only 쿼리를 실행합니다. 시각을 선택하고 쿼리를 실행하면 해당 시점의 데이터가 나옵니다. 잘못된 DELETE가 언제 실행됐는지 모를 때 사용합니다. 시각을 조금씩 옮겨 행이 남아 있는 마지막 시점을 찾습니다.

CLI에서 같은 작업을 하려면 과거 시점의 branch를 만들어 조회합니다.

# 기본 branch 의 과거 시점이면 --parent 에 시각만 줍니다.
neon branches create --name probe/1400 --parent 2026-09-07T14:00:00Z --no-compute
neon branches add-compute probe/1400 --type read_only --cu 0.25
psql "$(neon connection-string probe/1400 --endpoint-type read_only)" \
-c "SELECT count(*) FROM orders WHERE customer_id = 42"

branches create--parent는 branch 이름, ID, timestamp, LSN 중 하나만 받습니다. production@2026-09-07T14:00:00Z처럼 결합한 형태는 파싱되지 않습니다. 그 이름의 branch를 찾다가 실패합니다. branch@timestamp 형태는 schema-diffrestore 계열 명령의 문법입니다.

기본 branch가 아닌 특정 부모의 과거 시점이 필요하면 API를 사용합니다. parent_idparent_timestamp를 함께 넘깁니다.

neon api /projects/$PROJECT_ID/branches -X POST \
-F branch.parent_id=$PRODUCTION_BRANCH_ID \
-F branch.parent_timestamp=2026-09-07T14:00:00Z \
-F branch.name=probe/1400

connection-string은 기본적으로 read-write endpoint를 찾습니다. read-only compute만 연결한 branch에서는 --endpoint-type read_only를 지정해야 연결 문자열이 나옵니다. 확인이 끝나면 지웁니다.

Instant Restore

neon branches restore <target> <source>[@timestamp|@lsn] [--preserve-under-name <name>]

source는 세 가지 형태입니다.

source
^self@<timestamp>자기 과거 시점으로 되돌림
^parent부모의 현재 상태로 덮어씀 (reset과 같은 효과)
<branch>@<timestamp> 또는 <branch>@<lsn>다른 branch의 시점으로 덮어씀

자신의 과거로 되돌릴 때는 --preserve-under-name이 필수입니다. 덮어쓰기 직전 상태를 해당 이름의 branch로 남겨 둡니다. CLI와 API는 이 이름을 생략하면 요청을 거부합니다. Console에서 restore하면 {branch}_old_{timestamp} 형식의 이름을 자동으로 붙입니다. 이 자동 생성은 Console의 동작이므로 CLI에는 적용되지 않습니다.

neon branches restore production ^self@2026-09-07T13:55:00Z \
--preserve-under-name production_before_restore

restore는 병합이 아니라 timeline 전체를 덮어쓰는 작업입니다. 되돌린 시점 이후에 들어온 정상 변경도 함께 사라집니다. 해당 변경을 살려야 하면 백업 branch에서 필요한 행을 읽어 다시 넣습니다. 연결 문자열은 바뀌지 않지만 기존 session은 종료됩니다. 다시 연결할지는 클라이언트나 connection pool의 재연결 구현에 달려 있습니다. application에 재연결과 재시도가 없으면 restore 직후 오류가 사용자에게 노출됩니다.

다음과 같은 제약이 있습니다. 자신의 과거 시점으로 restore하려면 root branch여야 합니다. 자식 branch에서는 지원하지 않습니다. snapshot에서 복원해 만든 branch도 자신의 과거 restore를 지원하지 않습니다. 이 조건은 문서 시점 기준이므로 실행 전에 현재 문서를 확인합니다.

API로는 다음과 같습니다.

curl -s -X POST "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/restore" \
-H "Authorization: Bearer $NEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_branch_id": "'"$BRANCH_ID"'",
"source_timestamp": "2026-09-07T13:55:00Z",
"preserve_under_name": "production_before_restore"
}'

source_branch_id에 자신의 ID를 넣으면 ^self, 부모 ID를 넣으면 ^parent와 같습니다. 대신 source_lsn을 써도 됩니다.

Reset from parent

neon branches reset <child> --parent

자식 branch의 모든 database를 부모의 현재 schema와 data로 덮어씁니다. 리허설이나 개발 branch를 부모와 다시 맞출 때 사용합니다. restore와 다른 점은 두 가지입니다. 백업 branch를 만들지 않습니다. 대상은 항상 부모의 현재 상태입니다.

reset이 실패하는 조건은 다음과 같습니다.

  • root branch에는 부모가 없어 reset 대상이 아닙니다.
  • 자식이 있는 branch는 먼저 자식을 지워야 합니다.
  • 보호 branch는 reset되지 않습니다.
  • 부모가 snapshot에서 복원된 뒤 24시간 동안은 reset할 수 없습니다.
  • schema-only branch는 root branch이므로 reset 대상이 아닙니다.

branch에 만료가 설정되어 있으면 reset 시점에 원래 간격으로 TTL이 다시 시작됩니다.

Snapshot

history window가 지나면 해당 시점으로 돌아갈 수 없습니다. 특정 시점을 window와 무관하게 보관하려면 snapshot을 만듭니다. snapshot은 branch의 특정 LSN을 가리키는 논리적 참조입니다. 따라서 생성이 즉시 끝나며 데이터를 복사하지 않습니다.

제약은 두 가지입니다. snapshot은 root branch, 즉 부모가 없는 branch에서만 생성합니다. 자식 branch에는 만들 수 없으므로 history window 안의 restore나 reset을 사용합니다. 수동 생성 snapshot 개수는 Free 1개, 유료 플랜 100개로 제한됩니다. 두 값은 문서 시점 기준입니다.

neon snapshots create --branch production --name before-2026-09-release
neon snapshots create --branch production --timestamp 2026-09-07T13:55:00Z
neon snapshots list
neon snapshots get <snapshot id>
neon snapshots restore <snapshot id> --name recovered-2026-09-07

대상 branch는 positional 인자가 아니라 --branch 옵션입니다. --timestamp--lsn을 주면 과거 시점의 snapshot을 만듭니다. 생략하면 현재 시점의 snapshot을 만듭니다. --expires-at으로 만료를 설정합니다. API의 Update snapshot endpoint에서는 expires_at을 뒤로 미룰 수 있습니다. null로 지우면 영구 보관으로 바뀝니다.

snapshots restore는 snapshot 내용을 담은 새 branch를 만듭니다. --finalize를 지정하면 원래 branch의 compute endpoint를 새 branch로 옮깁니다. 연결 문자열은 유지하면서 데이터만 바꿉니다. 이 방식이 문서에서 권장하는 구성입니다. 교체된 원래 branch는 이름 뒤에 표시가 붙은 채 남습니다. 조사한 뒤 정리합니다. 복원된 branch에서는 자신의 과거 restore를 지원하지 않습니다.

GC는 snapshot이 가리키는 layer를 삭제하지 않습니다. 오래 보관한 snapshot은 그만큼 storage를 차지합니다. 릴리스 전 snapshot처럼 목적이 분명한 것만 영구 보관합니다. 나머지에는 만료를 설정합니다.

시나리오: 잘못된 DELETE 복구

14시 10분에 DELETE FROM orders WHERE created_at < '2026-01-01'이 실행됐다고 가정합니다. WHERE 절 오타로 전체 삭제가 발생했습니다. 그 뒤 application이 정상 데이터를 조금 넣은 상태입니다.

이 절차는 사고 이후 발생한 변경이 신규 INSERT뿐인 경우로 한정합니다. 같은 구간에 UPDATE나 정상 DELETE가 섞여 있으면 행을 다시 넣는 방식으로는 정확히 복구할 수 없습니다. 그때는 restore 대신 백업 branch를 기준으로 삼습니다. 사고 이후 변경을 application 로그로 재생하는 편이 안전합니다.

  1. 즉시 쓰기를 멈춥니다. history window가 짧은 Free 플랜에서는 6시간 안에 복구를 끝내야 합니다.
  2. Time Travel Assist 또는 과거 시점 branch로 14시 09분 시점에 행이 있는지 확인합니다.
  3. 잘못된 삭제 이후 들어온 변경의 종류와 범위를 기록합니다. INSERT만인지 확인하는 단계입니다.
  4. restore를 실행합니다.
neon branches restore production ^self@2026-09-07T14:09:30Z \
--preserve-under-name production_after_bad_delete
  1. production_after_bad_delete branch에서 14시 10분 이후 삽입된 행을 임시 테이블로 옮깁니다. 비교한 뒤 삽입합니다. 컬럼을 명시하고 충돌을 무시하면 이미 복원된 행과 겹쳐도 실패하지 않습니다.
BACKUP_URL=$(neon connection-string production_after_bad_delete --database-name app)
PROD_URL=$(neon connection-string production --database-name app)

psql "$BACKUP_URL" -Atc \
"COPY (SELECT id, customer_id, amount, status, created_at FROM orders
WHERE created_at >= '2026-09-07 14:10:00+00') TO STDOUT" \
> /tmp/after_delete.tsv

psql "$PROD_URL" -v ON_ERROR_STOP=1 <<'SQL'
CREATE TEMP TABLE orders_recovered (LIKE orders INCLUDING DEFAULTS);
\copy orders_recovered (id, customer_id, amount, status, created_at) FROM '/tmp/after_delete.tsv'

-- 이미 복원된 행과 겹치는지 확인합니다.
SELECT count(*) AS overlapping
FROM orders_recovered r JOIN orders o USING (id);

INSERT INTO orders (id, customer_id, amount, status, created_at)
SELECT id, customer_id, amount, status, created_at FROM orders_recovered
ON CONFLICT (id) DO NOTHING;

-- 명시적 id 를 넣었으므로 sequence 를 현재 최대값으로 맞춥니다.
SELECT setval(pg_get_serial_sequence('orders', 'id'), max(id)) FROM orders;
SQL
  1. 검증합니다. 복원 전후 행 수와 사고 구간의 표본을 비교합니다.
SELECT count(*) AS total,
count(*) FILTER (WHERE created_at >= '2026-09-07 14:10:00+00') AS after_incident,
max(id) AS max_id,
(SELECT last_value
FROM pg_sequences
WHERE schemaname || '.' || sequencename
= pg_get_serial_sequence('orders', 'id')) AS seq_value
FROM orders;
  1. 확인이 끝나면 백업 branch를 즉시 지우거나 며칠 보관한 뒤 지웁니다.

이 절차는 4단계까지 데이터 크기와 무관하게 수 초 안에 끝납니다. 시간이 걸리는 부분은 사람이 시점을 찾는 2단계입니다. 되돌린 뒤 정상 데이터를 합치는 5단계에도 시간이 걸립니다. id가 identity 컬럼이고 GENERATED ALWAYS로 선언되어 있으면 INSERTOVERRIDING SYSTEM VALUE가 필요합니다.

연습 문제

  1. 테스트 branch에 표를 만들고 몇 분 뒤 전체 삭제합니다. 그런 다음 ^self@로 삭제 직전 시점으로 restore합니다. --preserve-under-name을 반드시 넣고, 남은 백업 branch에 삭제된 행이 있는지 확인합니다.
  2. 같은 명령에서 --preserve-under-name을 빼고 실행해 어떤 오류가 나는지 확인합니다.
  3. 자식 branch에서 ^self@ restore를 시도해 어떤 오류가 나는지 확인합니다.
  4. root branch에 snapshot을 만들고 history window가 지난 뒤에도 조회되는지 확인합니다. Free 플랜이면 6시간 뒤에 확인하고, manual snapshot이 한 개까지라는 점을 고려합니다.

참고