2026년 08월 02일 | DBMS Error 가이드
이 글에서 다루는 내용
03000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
03000 sql statement not yet complete 는?
PostgreSQL 에러 코드 03000 (sql_statement_not_yet_complete)은 SQL 문장이 아직 완전히 실행되지 않은 상태에서 다른 작업을 시도할 때 발생하는 에러입니다. 주로 PL/pgSQL 또는 서버 사이드 프로그래밍 환경에서 비동기적으로 쿼리를 처리하거나, 커서(Cursor) 기반 작업 도중 예상치 못한 상태 전환이 일어날 때 트리거됩니다. 이 에러는 PostgreSQL 내부 실행 엔진이 현재 진행 중인 SQL 문의 완료를 기다리지 않고 다음 작업으로 넘어가려 할 때 발생하며, 특히 복잡한 트랜잭션 블록이나 동적 SQL 처리 과정에서 자주 목격됩니다.
주요 발생 원인
1. 커서(Cursor)가 열린 상태에서 트랜잭션 제어 명령 실행
PostgreSQL에서 커서는 트랜잭션 범위 안에서만 유효합니다. 커서를 열고 데이터를 페치(FETCH)하는 도중에 COMMIT 또는 ROLLBACK을 명시적으로 호출하면, 내부적으로 SQL 문이 아직 완료되지 않은 상태로 판단하여 03000 에러가 발생할 수 있습니다. 특히 PL/pgSQL 함수 안에서 루프를 돌며 커서를 사용하는 경우 이 문제가 빈번히 발생합니다.
2. 동적 SQL(EXECUTE) 내부에서 불완전한 문장 실행
EXECUTE 명령어를 사용하여 동적으로 SQL 문자열을 실행할 때, 문자열 연결 오류나 조건 분기 실수로 인해 SQL 문장이 중간에 잘리거나 구문이 불완전하게 구성되는 경우가 있습니다. 이런 상태에서 PostgreSQL 파서가 문장의 끝을 인식하지 못하면 실행 엔진은 문장이 아직 완료되지 않았다고 판단합니다. 런타임에만 문자열이 확정되므로 컴파일 시점에 오류를 잡기 어렵다는 점에서 더욱 주의가 필요합니다.
3. 중첩된 트랜잭션 블록 또는 SAVEPOINT 오용
PostgreSQL은 기본적으로 중첩 트랜잭션을 지원하지 않으며, SAVEPOINT를 이용한 부분 롤백만 허용합니다. PL/pgSQL 프로시저 또는 함수 내에서 BEGIN ... END 블록을 잘못 중첩하거나 SAVEPOINT를 릴리즈하지 않은 상태에서 다른 SQL 문을 실행하려 하면, 실행 엔진이 현재 문장의 완료 여부를 판단하지 못해 03000 에러가 발생할 수 있습니다.
해결 방법
원인 1 해결: 커서 작업과 트랜잭션 분리
커서를 사용할 때는 반드시 커서를 닫은(CLOSE) 이후에 트랜잭션 제어 명령을 실행해야 합니다.
-- 잘못된 예시: 커서가 열린 상태에서 COMMIT 호출
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM employees;
rec RECORD;
BEGIN
OPEN cur;
FETCH cur INTO rec;
COMMIT; -- ❌ 커서가 열려있는 상태에서 COMMIT → 에러 발생 가능
CLOSE cur;
END;
$$;
-- 올바른 예시: 커서를 닫은 후 트랜잭션 처리
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM employees;
rec RECORD;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
-- 데이터 처리 로직
RAISE NOTICE 'Processing: %', rec.name;
END LOOP;
CLOSE cur; -- ✅ 먼저 커서를 닫고
-- 이후 트랜잭션 작업 수행
END;
$$;
원인 2 해결: 동적 SQL 문자열 유효성 검사
EXECUTE로 동적 SQL을 실행하기 전에 반드시 생성된 SQL 문자열을 로그로 출력하여 확인하는 습관을 들여야 합니다.
-- 잘못된 예시: 불완전한 동적 SQL
DO $$
DECLARE
tbl_name TEXT := 'employees';
col_name TEXT := ''; -- 빈 문자열로 인해 SQL 불완전
query TEXT;
BEGIN
query := 'SELECT ' || col_name || ' FROM ' || tbl_name; -- ❌ SELECT 뒤에 컬럼 없음
EXECUTE query;
END;
$$;
-- 올바른 예시: 실행 전 쿼리 검증
DO $$
DECLARE
tbl_name TEXT := 'employees';
col_name TEXT := 'id, name';
query TEXT;
BEGIN
-- NULL 또는 빈 값 방어 처리
IF col_name IS NULL OR col_name = '' THEN
RAISE EXCEPTION '컬럼명이 비어 있습니다.';
END IF;
query := 'SELECT ' || col_name || ' FROM ' || quote_ident(tbl_name);
-- 실행 전 쿼리 내용 확인 (디버깅용)
RAISE NOTICE 'Executing query: %', query;
EXECUTE query; -- ✅ 검증된 쿼리 실행
END;
$$;
-- 파라미터 바인딩을 사용하는 더 안전한 방법
DO $$
DECLARE
dept_id INT := 10;
query TEXT;
result RECORD;
BEGIN
query := 'SELECT id, name, salary FROM employees WHERE department_id = $1';
FOR result IN EXECUTE query USING dept_id
LOOP
RAISE NOTICE 'Employee: %, Salary: %', result.name, result.salary;
END LOOP;
END;
$$;
원인 3 해결: SAVEPOINT 올바른 사용
중첩 트랜잭션 대신 SAVEPOINT를 올바르게 사용하여 부분 롤백을 구현합니다.
-- 올바른 SAVEPOINT 활용 예시
BEGIN;
INSERT INTO orders (customer_id, amount) VALUES (101, 5000);
SAVEPOINT sp_after_order; -- ✅ SAVEPOINT 설정
INSERT INTO order_items (order_id, product_id, qty) VALUES (1, 200, 3);
-- 오류 발생 시 SAVEPOINT까지만 롤백
-- ROLLBACK TO SAVEPOINT sp_after_order;
RELEASE SAVEPOINT sp_after_order; -- ✅ 사용 후 반드시 RELEASE
COMMIT;
-- PL/pgSQL 내에서 예외 처리와 함께 사용
DO $$
BEGIN
INSERT INTO audit_log (action, ts) VALUES ('START', NOW());
SAVEPOINT before_risky_op;
BEGIN
-- 위험한 작업 시도
UPDATE accounts SET balance = balance - 9999999 WHERE id = 1;
-- 조건 체크
IF (SELECT balance FROM accounts WHERE id = 1) < 0 THEN
RAISE EXCEPTION '잔액 부족';
END IF;
EXCEPTION WHEN OTHERS THEN
ROLLBACK TO SAVEPOINT before_risky_op; -- ✅ 부분 롤백
RAISE NOTICE '작업 실패, SAVEPOINT로 복구: %', SQLERRM;
END;
RELEASE SAVEPOINT before_risky_op;
END;
$$;
예방 방법
1. PL/pgSQL 함수 작성 시 명시적 상태 관리 및 로깅 적용
복잡한 PL/pgSQL 함수나 프로시저를 작성할 때는 각 단계에서 RAISE NOTICE 또는 별도의 로그 테이블을 통해 현재 실행 상태를 추적하는 습관을 들이세요. 커서의 열림/닫힘 상태, 동적 SQL의 실제 내용, 트랜잭션 경계를 명확히 기록해두면 03000 에러가 발생했을 때 원인을 빠르게 파악할 수 있습니다. 또한 개발 환경에서는 SET client_min_messages = DEBUG;를 활성화하여 내부 실행 흐름을 상세히 모니터링하는 것을 권장합니다.
-- 상태 추적용 로그 테이블 예시
CREATE TABLE IF NOT EXISTS plpgsql_exec_log (
log_id SERIAL PRIMARY KEY,
proc_name TEXT,
step TEXT,
status TEXT,
logged_at TIMESTAMPTZ DEFAULT NOW()
);
-- 함수 내 로깅 예시
CREATE OR REPLACE FUNCTION process_batch(p_batch_id INT)
RETURNS VOID AS $$
DECLARE
cur CURSOR FOR SELECT * FROM batch_items WHERE batch_id = p_batch_id;
rec RECORD;
BEGIN
INSERT INTO plpgsql_exec_log(proc_name, step, status)
VALUES ('process_batch', 'START', 'OK');
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
-- 처리 로직
END LOOP;
CLOSE cur; -- 반드시 닫기
INSERT INTO plpgsql_exec_log(proc_name, step, status)
VALUES ('process_batch', 'END', 'OK');
END;
$$ LANGUAGE plpgsql;
2. 코드 리뷰 체크리스트에 SQL 완전성 항목 추가
팀 단위로 개발할 때는 PL/pgSQL 코드 리뷰 시 커서 닫힘 여부, 동적 SQL의 NULL/빈값 처리, SAVEPOINT 릴리즈 여부를 반드시 확인하는 체크리스트를 운영하세요. CI/CD 파이프라인에 pgTAP 등의 데이터베이스 테스트 프레임워크를 통합하여 다양한 엣지 케이스(빈 결과셋, NULL 파라미터 등)에 대한 자동 테스트를 수행하면 운영 환경 배포 전에 잠재적 03000 에러를 사전 차단할 수 있습니다.
관련 에러
- 25001 (active_sql_transaction): 트랜잭션이 이미 활성화된 상태에서 트랜잭션을 시작하려 할 때 발생하며, 03000과 유사한 트랜잭션 상태 관리 문제와 연관됩니다.
- 25P02 (in_failed_sql_transaction): 이전 SQL 문장이 실패한 상태에서 계속 쿼리를 실행하려 할 때 발생합니다. 03000과 함께 트랜잭션 블록 오류 처리 시 자주 함께 나타납니다.
- 34000 (invalid_cursor_name): 커서 이름이 잘못 지정되었을 때 발생하며, 커서 관련 03000 에러와 함께 점검이 필요한 에러입니다.
- 42601 (syntax_error): 동적 SQL 생성 시 문자열 조합 오류로 인한 구문 에러로, 03000 에러의 전조 증상으로 나타나는 경우가 많습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.