PostgreSQL 03000 오류 원인과 해결 방법 완벽 가이드

03000
2026년 10월 06일 | DBMS Error 가이드

이 글에서 다루는 내용

03000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

03000 sql statement not yet complete 는?

PostgreSQL 에러 코드 03000은 “SQL statement not yet complete” 라는 메시지로 발생하며, SQL 구문이 아직 완전히 처리되지 않은 상태에서 다음 작업을 시도할 때 나타납니다. 주로 PL/pgSQL 함수나 프로시저, 트리거 내부에서 동적 SQL을 실행하거나 커서(cursor)를 사용하는 도중 쿼리가 완전히 완료되기 전에 다른 명령이 끼어드는 상황에서 발생합니다. 이 에러는 SQLState 클래스 03에 속하며, 트랜잭션 상태나 커서 상태와 밀접한 관련이 있어 복잡한 저장 프로시저 환경에서 특히 주의가 필요합니다.


주요 발생 원인

1. 커서(Cursor)를 완전히 소비하지 않고 다른 SQL 명령 실행

커서를 열어 FETCH로 데이터를 읽는 도중, 커서를 명시적으로 닫지 않거나 모든 행을 소비하지 않은 상태에서 다른 DML(INSERT, UPDATE, DELETE) 문을 실행하면 이 에러가 발생할 수 있습니다. PostgreSQL은 내부적으로 커서의 상태를 추적하는데, 쿼리 실행 컨텍스트가 중첩되거나 충돌하는 경우 SQL 구문이 아직 완료되지 않았다고 판단합니다. 특히 루프 안에서 커서와 일반 쿼리를 혼용할 때 이런 문제가 자주 발생합니다.

2. PL/pgSQL 동적 SQL(EXECUTE) 사용 중 트랜잭션 경계 위반

EXECUTE 구문을 사용한 동적 SQL 실행 도중, 트랜잭션 제어 명령(COMMIT, ROLLBACK)을 부적절하게 삽입하거나, 서브트랜잭션(SAVEPOINT)이 제대로 정리되지 않은 상태에서 후속 SQL을 실행할 때 이 에러가 발생합니다. PL/pgSQL 함수 내부에서는 기본적으로 트랜잭션 제어를 직접 수행할 수 없으며(PostgreSQL 11 이전), 이를 무시하고 사용하려 할 경우 SQL 실행 상태가 불완전하게 남습니다. 동적 SQL은 컴파일 시점에 구문 검증이 되지 않으므로 런타임에서야 이런 문제가 드러나는 경우가 많습니다.

3. 클라이언트 라이브러리 또는 커넥션 풀에서의 비정상적인 쿼리 파이프라이닝

libpq 기반의 클라이언트 라이브러리나 PgBouncer 같은 커넥션 풀러를 사용할 때, 이전 쿼리의 결과를 완전히 읽지 않은 상태에서 새로운 쿼리를 전송하면 서버 측에서 SQL 구문이 아직 완료되지 않았다고 인식할 수 있습니다. 특히 비동기(async) 쿼리 모드나 파이프라인 모드(PostgreSQL 14+의 libpq pipeline mode)를 사용할 때, 결과 세트를 끝까지 소비하지 않고 새 명령을 보내는 경우 이 에러가 트리거됩니다. 애플리케이션 레벨의 커넥션 관리 코드를 면밀히 검토해야 합니다.


해결 방법

원인 1 해결: 커서를 명시적으로 닫고 루프 구조 정리

커서를 사용한 후에는 반드시 CLOSE 명령으로 커서를 닫아야 합니다. 아래 예제는 올바른 커서 사용 패턴을 보여줍니다.

-- 잘못된 예: 커서를 닫지 않고 다른 DML 실행
CREATE OR REPLACE FUNCTION bad_cursor_example()
RETURNS void AS $$
DECLARE
    cur CURSOR FOR SELECT id, name FROM employees WHERE dept_id = 10;
    rec RECORD;
BEGIN
    OPEN cur;
    FETCH cur INTO rec;
    -- 커서를 닫지 않고 바로 UPDATE 실행 -> 문제 발생 가능
    UPDATE departments SET updated_at = NOW() WHERE id = 10;
    -- CLOSE cur; 누락!
END;
$$ LANGUAGE plpgsql;

-- 올바른 예: 커서를 완전히 소비하거나 명시적으로 닫기
CREATE OR REPLACE FUNCTION good_cursor_example()
RETURNS void AS $$
DECLARE
    cur CURSOR FOR SELECT id, name FROM employees WHERE dept_id = 10;
    rec RECORD;
BEGIN
    OPEN cur;
    LOOP
        FETCH cur INTO rec;
        EXIT WHEN NOT FOUND;  -- 모든 행을 소비
        -- 행별 처리 로직
        RAISE NOTICE 'Processing employee: %', rec.name;
    END LOOP;
    CLOSE cur;  -- 반드시 커서 닫기

    -- 커서가 닫힌 후 안전하게 DML 실행
    UPDATE departments SET updated_at = NOW() WHERE id = 10;
END;
$$ LANGUAGE plpgsql;

원인 2 해결: 동적 SQL과 트랜잭션 경계 명확히 분리

PL/pgSQL 프로시저(PostgreSQL 11+)에서 트랜잭션을 사용할 때는 CALL을 통해 프로시저로 분리하고, COMMIT/ROLLBACK을 명확하게 작성해야 합니다.

-- 잘못된 예: 함수 내부에서 트랜잭션 제어 시도
CREATE OR REPLACE FUNCTION bad_transaction_example()
RETURNS void AS $$
BEGIN
    EXECUTE 'INSERT INTO audit_log(msg) VALUES(''start'')';
    COMMIT;  -- 함수 내부에서 COMMIT -> 에러 발생!
    EXECUTE 'UPDATE orders SET status = ''done'' WHERE id = 1';
END;
$$ LANGUAGE plpgsql;

-- 올바른 예: 프로시저(PROCEDURE)를 사용하여 트랜잭션 제어
CREATE OR REPLACE PROCEDURE good_transaction_example()
LANGUAGE plpgsql AS $$
BEGIN
    EXECUTE 'INSERT INTO audit_log(msg) VALUES(''start'')';
    COMMIT;  -- 프로시저에서는 COMMIT 가능 (PostgreSQL 11+)

    EXECUTE 'UPDATE orders SET status = ''done'' WHERE id = 1';
    COMMIT;

EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$;

-- 프로시저 호출
CALL good_transaction_example();

원인 3 해결: 클라이언트에서 결과 세트 완전히 소비

Python(psycopg2/psycopg3) 예제에서 결과를 완전히 읽는 올바른 패턴:

-- PostgreSQL 측: 대용량 결과를 반환하는 쿼리 예시
-- 클라이언트에서 반드시 fetchall() 또는 루프로 완전히 소비해야 함

-- 서버 사이드 커서를 사용하는 경우의 올바른 선언
BEGIN;
DECLARE large_result_cursor CURSOR FOR
    SELECT * FROM large_table WHERE created_at > NOW() - INTERVAL '30 days';

-- 데이터를 청크 단위로 모두 소비
FETCH 1000 FROM large_result_cursor;
-- ... 계속 FETCH ...
FETCH 1000 FROM large_result_cursor;  -- FETCH가 0행 반환할 때까지 반복

CLOSE large_result_cursor;
COMMIT;
-- SAVEPOINT를 이용한 안전한 서브트랜잭션 패턴
BEGIN;

SAVEPOINT sp1;
INSERT INTO orders(customer_id, total) VALUES(101, 5000);

-- 검증 로직
DO $$
BEGIN
    IF (SELECT balance FROM customers WHERE id = 101) < 5000 THEN
        RAISE EXCEPTION 'Insufficient balance';
    END IF;
END;
$$;

-- 정상 처리 시 SAVEPOINT 해제
RELEASE SAVEPOINT sp1;

COMMIT;

예방 방법

1. 커서와 동적 SQL 사용 시 반드시 예외 처리(EXCEPTION) 블록으로 자원 해제 보장

PL/pgSQL에서 커서나 동적 SQL을 사용하는 모든 함수/프로시저에 EXCEPTION 블록을 추가하여, 에러가 발생하더라도 커서가 반드시 닫히고 트랜잭션 상태가 정리되도록 코드를 작성하는 습관을 들여야 합니다. BEGIN ... EXCEPTION WHEN OTHERS THEN CLOSE cur; RAISE; 패턴을 표준 템플릿으로 팀 내에 공유하고, 코드 리뷰 체크리스트에 “커서 CLOSE 여부 확인” 항목을 포함시키는 것이 좋습니다.

-- 예외 처리로 커서 자원 해제를 보장하는 패턴
CREATE OR REPLACE FUNCTION safe_cursor_pattern()
RETURNS void AS $$
DECLARE
    cur CURSOR FOR SELECT id FROM orders WHERE status = 'pending';
    rec RECORD;
BEGIN
    OPEN cur;
    BEGIN  -- 내부 BEGIN 블록
        LOOP
            FETCH cur INTO rec;
            EXIT WHEN NOT FOUND;
            -- 비즈니스 로직 처리
            UPDATE orders SET status = 'processing' WHERE id = rec.id;
        END LOOP;
    EXCEPTION
        WHEN OTHERS THEN
            CLOSE cur;  -- 에러 시에도 커서 반드시 닫기
            RAISE;      -- 에러 재발생
    END;
    CLOSE cur;  -- 정상 종료 시 커서 닫기
END;
$$ LANGUAGE plpgsql;

2. 정기적인 pg_stat_activity 모니터링으로 불완전한 쿼리 상태 탐지

운영 환경에서는 pg_stat_activity 뷰를 주기적으로 조회하여 state가 비정상적으로 오래 active 또는 idle in transaction 상태에 머무는 세션을 조기에 발견하고, 필요시 pg_terminate_backend()로 정리해야 합니다. 또한 statement_timeout과 idle_in_transaction_session_timeout 파라미터를 적절히 설정하여 불완전한 상태의 연결이 장시간 유지되는 것을 방지하는 것이 실무에서 검증된 Best Practice입니다.

-- 불완전한 트랜잭션 상태의 세션 모니터링 쿼리
SELECT
    pid,
    usename,
    application_name,
    state,
    query_start,
    NOW() - query_start AS elapsed,
    left(query, 100) AS query_snippet
FROM pg_stat_activity
WHERE state IN ('active', 'idle in transaction')
  AND query_start < NOW() - INTERVAL '5 minutes'
ORDER BY elapsed DESC;

-- 타임아웃 설정 (postgresql.conf 또는 ALTER SYSTEM)
ALTER SYSTEM SET statement_timeout = '30s';
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();

관련 에러

  • 25P02 – in_failed_sql_transaction: 이미 실패한 트랜잭션 블록 안에서 SQL을 실행하려 할 때 발생하며, 03000과 함께 트랜잭션 상태 문제에서 자주 동반 발생합니다.
  • 34000 – invalid_cursor_name: 존재하지 않거나 이미 닫힌 커서를 참조할 때 발생하며, 커서 관련 03000 에러의 후속 에러로 나타날 수 있습니다.
  • 25001 – active_sql_transaction: 활성 트랜잭션 중에 허용되지 않는 명령을 실행할 때 발생하며, 동적 SQL 트랜잭션 경계 위반과 관련이 깊습니다.
  • 40001 – serialization_failure: 동시성 환경에서 트랜잭션이 직렬화 검사에 실패할 때 발생하며, 복잡한 PL/pgSQL 루틴에서 03000과 함께 나타날 수 있습니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기