PostgreSQL invalid page in block 오류 원인과 복구 대응 방법
PostgreSQL에서 invalid page in block xxxx of relation base/... 오류가 발생했다면 단순 SQL 오류가 아니라 테이블 또는 인덱스 파일의 물리적 페이지 손상 가능성을 먼저 봐야 한다. 특히 테이블 데이터 페이지가 깨진 경우에는 REINDEX나 VACUUM FULL만으로 데이터 손실 없이 복구하기 어렵다.
오류의 의미
PostgreSQL은 테이블과 인덱스를 여러 개의 8KB 페이지 단위로 저장한다. invalid page in block 오류는 PostgreSQL이 특정 relation 파일의 특정 block을 읽었는데, 그 페이지 헤더나 내부 구조가 정상적인 PostgreSQL 페이지 형식이 아니라고 판단했다는 뜻이다.
오류 메시지의 base/16395/361204 같은 경로는 데이터베이스 OID와 relation 파일 번호를 나타낸다. 여기서 마지막 숫자에 해당하는 relfilenode를 조회하면 어떤 테이블이나 인덱스 파일에서 문제가 발생했는지 추적할 수 있다.
SELECT
c.relfilenode,
n.nspname AS schema_name,
c.relname,
c.relkind
FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE c.relfilenode = 361204;
r: 일반 테이블i: 인덱스S: 시퀀스t: TOAST 테이블m: materialized view
질문1: invalid page in block 오류는 왜 발생하나
가장 직접적인 원인은 PostgreSQL이 읽어야 할 데이터 파일의 일부 페이지가 손상된 것이다. 원인은 하나로 단정하기 어렵지만, 일반적으로 비정상 종료, 디스크 문제, 파일시스템 손상, 백신 또는 보안 프로그램의 데이터 디렉터리 간섭, 저장장치 캐시 문제, 메모리 오류, 강제 전원 종료 등이 원인이 될 수 있다.
PostgreSQL 자체가 정상적으로 WAL을 기록하고 종료되었다면 이런 형태의 페이지 손상은 흔하지 않다. 그래서 같은 데이터베이스 안에서 테이블과 인덱스 손상이 점차 늘어나는 것처럼 보인다면, 데이터베이스 내부 복구만 볼 것이 아니라 OS와 저장장치 상태를 반드시 함께 점검해야 한다.
| 가능 원인 | 확인할 내용 |
|---|---|
| 비정상 종료 | Windows 이벤트 로그, PostgreSQL 로그에서 강제 종료, 전원 차단, 프로세스 강제 종료 이력을 확인한다. |
| 디스크 또는 SSD 문제 | SMART 정보, 불량 섹터, 컨트롤러 오류, 파일시스템 오류를 점검한다. |
| 메모리 오류 | 메모리 진단 도구로 RAM 오류 여부를 확인한다. |
| 백신·보안 프로그램 간섭 | PostgreSQL data directory를 실시간 검사 대상에서 제외했는지 확인한다. |
| 스토리지 동기화 도구 | OneDrive, 백업 동기화, 클라우드 동기화 폴더 안에 DB data directory가 있는지 확인한다. |
| 강제 중지 습관 | 서비스 종료 대신 프로세스 강제 종료, PC 전원 강제 종료가 반복되는지 확인한다. |
Windows 11 Home 환경에서 로컬 개발용으로 PostgreSQL을 운영한다면 절전, 강제 재부팅, 백신 실시간 검사, 클라우드 동기화 폴더 사용 여부를 우선 확인하는 것이 좋다.
인덱스 손상과 테이블 손상은 대응이 다르다
같은 invalid page in block 오류라도 손상된 relation이 인덱스인지 테이블인지에 따라 복구 가능성이 크게 달라진다. 인덱스가 깨진 경우에는 원본 테이블 데이터가 살아 있다면 인덱스를 버리고 다시 만들 수 있다.
반면 테이블 자체의 데이터 페이지가 깨진 경우에는 그 페이지 안에 있던 row가 손상된 것이다. 이 경우 REINDEX는 해결책이 아니다. VACUUM FULL도 테이블을 다시 쓰는 과정에서 손상 페이지를 읽어야 하므로 오류가 발생할 수 있다.
| 손상 대상 | 가능한 조치 | 데이터 손실 가능성 |
|---|---|---|
| 인덱스 | REINDEX INDEX, REINDEX TABLE, 인덱스 재생성 |
낮음. 테이블 데이터가 정상이라면 대부분 복구 가능하다. |
| PK 인덱스 | PK 인덱스 재생성 또는 제약조건 재생성 | 테이블 데이터가 정상이어야 한다. |
| 테이블 heap | 백업 복구, 정상 row 덤프, 손상 페이지 제외 이관 | 높음. 손상 페이지의 row는 복구가 어려울 수 있다. |
| TOAST 테이블 | 대형 컬럼 데이터 복구 또는 해당 row 제외 이관 | 높음. text, bytea, jsonb 등 큰 컬럼이 영향을 받을 수 있다. |
| 시퀀스 | 시퀀스 재생성 후 setval로 보정 |
낮음. 값 재설정으로 대응 가능한 경우가 많다. |
질문2: 테이블 손상도 데이터 손실 없이 복구할 수 있나
결론부터 말하면, 백업이나 정상 복제본이 없다면 테이블 데이터 페이지 손상을 완전히 데이터 손실 없이 복구하는 방법은 거의 없다. PostgreSQL이 손상 페이지를 읽지 못한다는 것은 그 페이지 안의 row를 정상적인 tuple로 해석할 수 없다는 의미이기 때문이다.
데이터 손실 없이 복구할 수 있는 경우는 보통 세 가지다. 첫째, 손상 전 정상 백업이 있다. 둘째, WAL 아카이브가 있어 PITR로 손상 전 시점까지 복구할 수 있다. 셋째, 손상되지 않은 Standby 또는 파일시스템 스냅샷이 있다.
SET zero_damaged_pages = on;은 복구 기능이라기보다 손상된 페이지를 0으로 처리하고 나머지 정상 페이지를 읽기 위한 응급 조치에 가깝다. 이 설정 후 VACUUM FULL을 수행하면 손상 페이지에 있던 row는 사라질 수 있으므로, 원본을 보존하지 않고 바로 적용하면 되돌리기 어렵다.
WAL 아카이브가 있어 PITR이 가능하다.
손상 전 파일시스템 스냅샷이 있다.
정상 Standby 서버가 있다.
손상된 페이지가 인덱스에만 있고 테이블 데이터는 정상이다.
zero_damaged_pages 사용 전 반드시 해야 할 일
zero_damaged_pages는 매우 위험한 응급 옵션이다. 손상 페이지를 건너뛰고 나머지 데이터를 읽게 해주지만, 손상 페이지의 row는 사실상 버려진다. 따라서 원본 데이터 디렉터리 복사본을 확보하기 전에 이 설정으로 VACUUM FULL이나 대량 작업을 수행하면 복구 가능성이 더 낮아질 수 있다.
실무 기준으로 보면 우선 PostgreSQL을 중지하고 data directory 전체를 별도 디스크에 파일 단위로 복사해 보존해야 한다. 그 다음 복사본 또는 별도 인스턴스에서 손상 범위 확인, 덤프, 부분 복구를 시도하는 것이 안전하다.
- PostgreSQL 서비스를 중지한다.
- data directory 전체를 별도 저장장치에 복사한다.
- 복사본을 기준으로 복구 테스트를 진행한다.
- 운영 원본에는 바로
zero_damaged_pages와VACUUM FULL을 적용하지 않는다. - 손상 페이지를 zero 처리한 뒤에는 해당 page의 row 손실을 전제로 검증한다.
-- 응급 덤프나 정상 row 회수 목적일 때만 세션 단위로 사용
SET zero_damaged_pages = on;
-- 이후 가능한 데이터를 별도 테이블이나 파일로 회수
CREATE TABLE recovered_table AS
SELECT *
FROM damaged_table;
질문3: 손상이 퍼지는 것처럼 보일 때 PostgreSQL 대응 방향
손상이 처음에는 한두 테이블에서만 보이다가 점차 다른 테이블이나 인덱스에서도 발견된다면, 실제로 손상이 계속 확산되고 있거나 원래 여러 곳이 손상되어 있었는데 조회하면서 뒤늦게 발견되는 상황일 수 있다.
이 경우 PostgreSQL 안에서 계속 REINDEX, VACUUM FULL을 반복하기보다 새 인스턴스로 이관하는 방식이 안전하다. 다만 pg_dump가 손상 테이블을 읽다가 중단될 수 있으므로, 정상 객체와 문제 객체를 분리해 덤프해야 한다.
DBMS 내부 작업만 반복하기 전에 디스크, 메모리, OS 이벤트 로그, PostgreSQL 로그를 먼저 확인해야 한다. 원인이 저장장치나 시스템 불안정이라면 새로 덤프해도 같은 문제가 다시 발생할 수 있다.
문제 relation 확인 방법
오류 메시지에 나온 relfilenode가 현재 catalog와 일치한다면 다음 쿼리로 손상 대상이 테이블인지 인덱스인지 확인할 수 있다.
SELECT
pg_relation_filepath(c.oid) AS file_path,
c.relfilenode,
n.nspname AS schema_name,
c.relname,
c.relkind,
pg_size_pretty(pg_relation_size(c.oid)) AS relation_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relfilenode = 361204;
만약 relfilenode가 바뀌었거나 조회되지 않는다면 pg_relation_filepath를 기준으로 전체 relation을 확인하거나, 오류가 발생하는 SQL과 실행 계획을 함께 봐야 한다.
SELECT
n.nspname AS schema_name,
c.relname,
c.relkind,
pg_relation_filepath(c.oid) AS file_path,
pg_size_pretty(pg_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_relation_size(c.oid) DESC;
문제 테이블을 찾는 점검 쿼리
어떤 테이블이 깨졌는지 모르는 상태라면 전체 사용자 테이블을 순차적으로 읽어보는 방식으로 1차 확인할 수 있다. 다만 손상 테이블을 읽는 순간 오류가 발생하므로, 아래와 같은 DO 블록으로 테이블별 성공·실패를 확인하는 방식이 현실적이다.
DO $$
DECLARE
r record;
v_count bigint;
BEGIN
FOR r IN
SELECT n.nspname, c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, c.relname
LOOP
BEGIN
EXECUTE format(
'SELECT count(*) FROM %I.%I',
r.nspname,
r.relname
)
INTO v_count;
RAISE NOTICE 'OK %.% rows=%', r.nspname, r.relname, v_count;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'ERROR %.%: %', r.nspname, r.relname, SQLERRM;
END;
END LOOP;
END $$;
단, count(*)가 항상 모든 데이터 페이지를 원하는 방식으로 읽는다고 단정할 수는 없다. 더 확실히 확인하려면 인덱스 스캔을 꺼서 순차 스캔을 유도하거나, PostgreSQL 14 환경에서는 pg_amcheck 같은 점검 도구도 함께 검토할 수 있다.
SET enable_indexscan = off;
SET enable_bitmapscan = off;
SET enable_indexonlyscan = off;
SELECT count(*) FROM schema_name.table_name;
pg_dump가 중단될 때 제외하고 이관하는 방법
pg_dump가 특정 테이블이나 시퀀스에서 invalid page in block 오류로 중단된다면, 문제 객체를 drop할 필요는 없다. 우선 정상 객체를 살리는 것이 중요하므로 문제가 있는 테이블을 제외하고 덤프할 수 있다.
pg_dump -U postgres -d mydb ^
-F c ^
-f mydb_without_bad_tables.dump ^
-T bad_schema.bad_table
여러 테이블을 제외해야 한다면 -T 옵션을 반복해서 사용할 수 있다.
pg_dump -U postgres -d mydb ^
-F c ^
-f mydb_without_bad_tables.dump ^
-T bad_schema.bad_table1 ^
-T bad_schema.bad_table2 ^
-T bad_schema.bad_table3
데이터만 문제가 있고 테이블 구조는 새 인스턴스에 유지하고 싶다면 스키마 덤프와 데이터 덤프를 분리하는 방식이 좋다.
-- 전체 스키마만 덤프
pg_dump -U postgres -d mydb ^
--schema-only ^
-f schema_only.sql
-- 정상 데이터만 덤프
pg_dump -U postgres -d mydb ^
--data-only ^
-F c ^
-f data_only_without_bad_tables.dump ^
-T bad_schema.bad_table
먼저 전체 파일 복사본을 만든다.
정상 객체를 새 PostgreSQL 인스턴스로 이관한다.
손상 테이블은 별도 복구 대상으로 분리한다.
필요한 경우 손상 페이지를 제외하고 회수 가능한 row만 추출한다.
손상 테이블에서 일부 데이터라도 회수하는 방법
백업이 없고 테이블 데이터 페이지가 손상된 경우에는 완전 복구보다 “읽을 수 있는 row를 최대한 회수”하는 방향으로 접근해야 한다. 이때 원본을 보존한 복사본에서 작업해야 한다.
손상 block 번호를 알고 있다면 ctid를 이용해 해당 block을 피해서 데이터를 복사하는 방법을 시도할 수 있다. 예를 들어 block 18이 손상되었다면 해당 block을 제외하고 나머지 row를 새 테이블에 담는 식이다.
CREATE TABLE recovered_bad_table AS
SELECT *
FROM bad_schema.bad_table
WHERE ctid < '(18,1)'::tid
OR ctid >= '(19,1)'::tid;
손상 block이 여러 개라면 조건을 추가해야 한다. 다만 이 방식은 테이블 전체를 안정적으로 읽을 수 있는 상황에서만 성공한다. TOAST 데이터나 다른 block도 손상되어 있다면 중간에 다시 오류가 발생할 수 있다.
CREATE TABLE recovered_bad_table AS
SELECT *
FROM bad_schema.bad_table
WHERE NOT (
ctid >= '(18,1)'::tid AND ctid < '(19,1)'::tid
)
AND NOT (
ctid >= '(25,1)'::tid AND ctid < '(26,1)'::tid
);
이 방식으로 회수한 뒤에는 row 수, 주요 PK 범위, 업무적으로 중요한 금액·상태값·일자별 집계를 반드시 검증해야 한다. 손상 block에 있던 row는 빠질 수 있기 때문이다.
시퀀스에서 오류가 발생할 때
pg_dump 중 시퀀스에서 invalid page in block 오류가 발생한다면, 해당 시퀀스 파일이 손상되었을 가능성이 있다. 시퀀스는 일반 테이블 데이터와 달리 현재 번호를 관리하는 객체이므로, 연결된 테이블의 최대값을 기준으로 재생성하거나 값을 보정할 수 있다.
-- 예: id 컬럼 기준으로 시퀀스 값을 보정
SELECT setval(
'public.my_table_id_seq',
COALESCE((SELECT max(id) FROM public.my_table), 0) + 1,
false
);
새 인스턴스로 이관할 때는 스키마를 복원한 뒤 시퀀스 값을 업무 테이블의 실제 최대값에 맞춰 다시 세팅하는 방식이 안전하다.
권장 복구 절차
현재 환경이 Windows 11 Home, PostgreSQL 14, DB 크기 약 15GB라면 데이터 크기는 비교적 크지 않은 편이다. 따라서 손상 원인을 조사하면서 새 인스턴스로 이관하는 방식이 현실적이다.
| 단계 | 작업 | 목적 |
|---|---|---|
| 1 | PostgreSQL 중지 후 data directory 전체 복사 | 복구 시도 전 원본 보존 |
| 2 | 디스크, 메모리, 이벤트 로그, PostgreSQL 로그 점검 | 손상 원인 확인 |
| 3 | relfilenode 조회와 테이블별 scan으로 문제 객체 식별 | 인덱스 손상인지 테이블 손상인지 구분 |
| 4 | 인덱스만 문제라면 REINDEX 수행 | 데이터 손실 없이 인덱스 재생성 |
| 5 | 테이블 손상이면 정상 객체를 pg_dump로 우선 이관 | 살릴 수 있는 데이터 우선 확보 |
| 6 | 문제 테이블은 백업 복구 또는 손상 block 제외 추출 | 부분 데이터 회수 |
| 7 | 새 PostgreSQL 최신 minor 버전 인스턴스로 복원 | 깨진 클러스터를 계속 사용하지 않기 위함 |
| 8 | 백업, WAL 아카이브, 정기 복구 테스트 구성 | 재발 시 복구 가능성 확보 |
새 인스턴스로 이관할 때 주의할 점
손상된 PostgreSQL 클러스터에서 계속 운영을 이어가는 것은 권장하기 어렵다. 특히 여러 relation에서 오류가 발생한다면 새 data directory를 가진 새 PostgreSQL 인스턴스를 만들고, 정상 덤프본을 복원하는 방향이 안전하다.
이때 PostgreSQL 14의 최신 minor 버전을 사용하는 것이 좋다. 같은 14 버전이라도 minor update에는 데이터 손상과 직접 관련되지 않더라도 안정성, 보안, 복구 관련 수정이 포함될 수 있다.
- 기존 data directory를 재사용하지 않는다.
- 새 PostgreSQL 인스턴스를 새 경로에 초기화한다.
- 정상 덤프본을 먼저 복원한다.
- 문제 테이블은 별도 복구 결과를 검증 후 반영한다.
- 복원 후 전체 테이블 count, 주요 업무 집계, 제약조건, 시퀀스 값을 점검한다.
운영 재발 방지 체크리스트
invalid page in block 오류는 한 번 복구했다고 끝나는 문제가 아니다. 원인을 제거하지 않으면 새 인스턴스에서도 다시 발생할 수 있다. 특히 개인 PC나 개발 장비에서는 DBMS를 일반 파일처럼 다루는 과정에서 문제가 생기기 쉽다.
| 항목 | 권장 조치 |
|---|---|
| 백업 | 정기적으로 pg_dump 또는 물리 백업을 수행하고 복구 테스트까지 진행한다. |
| WAL 보관 | 중요 데이터라면 PITR을 위해 WAL 아카이브 구성을 검토한다. |
| 종료 방식 | PostgreSQL 서비스를 정상 종료하고, PC 강제 종료를 피한다. |
| 백신 예외 | PostgreSQL data directory를 실시간 검사 대상에서 제외한다. |
| 스토리지 | SMART, chkdsk, 제조사 진단 도구로 저장장치 상태를 확인한다. |
| 동기화 폴더 | DB data directory를 OneDrive, Dropbox 같은 동기화 폴더에 두지 않는다. |
| 정기 점검 | 주기적으로 로그, 아카이브 실패, 디스크 오류, 테이블 접근 오류를 확인한다. |
질문별 결론
첫째, invalid page in block 오류는 PostgreSQL이 특정 테이블 또는 인덱스 파일의 페이지를 정상적인 데이터 페이지로 해석하지 못할 때 발생한다. 원인은 비정상 종료, 디스크·메모리 문제, 파일시스템 손상, 외부 프로그램 간섭 등 다양하다.
둘째, 테이블 데이터 페이지가 깨진 경우 백업, WAL, 정상 복제본, 스냅샷이 없다면 데이터 손실 없는 완전 복구는 어렵다. 인덱스 손상은 재생성으로 해결될 수 있지만, 테이블 heap 손상은 손상 페이지 안의 row를 복구하기 어렵다.
셋째, 손상이 여러 객체로 늘어나는 상황에서는 기존 클러스터를 계속 고쳐 쓰기보다 새 PostgreSQL 인스턴스로 정상 객체를 이관하고, 문제 테이블은 별도 복구 대상으로 분리하는 것이 좋다. pg_dump -T로 문제 테이블을 제외할 수 있으며, drop은 최후의 선택으로 두는 편이 안전하다.
'지식 공유 > DBMS' 카테고리의 다른 글
| oracle to postgresql ORA-00943 오류, Oracle 테이블 대소문자와 스키마 확인 방법 (0) | 2026.06.28 |
|---|---|
| PostgreSQL archive_mode on과 always 차이, 운영 환경별 선택 기준 (0) | 2026.06.28 |
| 오라클 테이블 Block Corrupt로 인한 백업 실패 조치 정리 (0) | 2026.06.24 |
| 로그마이너란 무엇이며 DELETE 오실행 데이터를 복구하는 방법 (0) | 2026.06.22 |
| ORA-01031 해결: 권한 부족 원인별 점검과 최소 권한 복구 (0) | 2026.06.04 |
