03000 sql statement not yet complete 는?
PostgreSQL 에러 코드 03000은 "sql statement not yet complete" 로, SQL 구문이 완전히 완료되지 않은 상태에서 다음 명령이 실행되거나 특정 함수가 호출될 때 발생하는 경고성 에러입니다. 주로 PL/pgSQL 함수, 프로시저, 또는 동적 SQL 실행 블록 내에서 커서(cursor)나 쿼리 결과 처리 도중 불완전한 상태로 다른 작업을 시도할 때 나타납니다. 이 에러는 단순 SELECT 오류가 아니라 실행 흐름 제어(flow control) 와 밀접하게 연관되어 있어, 실무에서 놓치기 쉬운 유형에 속합니다.
주요 발생 원인
1. 커서(Cursor)가 열린 상태에서 불완전한 페치(FETCH) 처리
커서를 OPEN한 후 FETCH로 데이터를 모두 소비하지 않은 채 다른 SQL 작업을 수행하거나 커서를 닫지 않으면 이 에러가 발생할 수 있습니다. 특히 반복문(loop) 내에서 조기 탈출(EXIT)하거나 예외 처리 없이 커서를 방치하는 패턴이 주요 원인입니다.
2. PL/pgSQL 동적 SQL(EXECUTE) 블록의 불완전한 문자열 조합
EXECUTE 명령에 전달되는 SQL 문자열이 런타임에 동적으로 생성될 때, 조건 분기 오류나 변수 누락으로 인해 SQL 구문이 중간에 잘리거나 세미콜론(;)이 누락된 채 실행 시도가 발생할 수 있습니다. PostgreSQL 파서가 해당 문자열을 완전한 SQL로 인식하지 못하면 03000 에러를 반환합니다.
3. 트랜잭션 블록 내부에서의 불완전한 복합 명령 처리
BEGIN ... END 블록 안에서 여러 DML 또는 DDL 작업을 처리할 때, 앞선 명령이 내부적으로 아직 완료되지 않은 시점에 후속 명령이 투입되면 이 에러가 트리거됩니다. 특히 PostgreSQL의 파이프라인 모드(pipelining mode)나 일부 클라이언트 라이브러리에서 비동기 실행을 사용할 때 발생 빈도가 높습니다.
해결 방법
원인 1 해결 - 커서 정상 종료 보장
커서를 사용할 때는 반드시 CLOSE 구문으로 명시적으로 닫아야 하며, 예외 상황에서도 커서가 닫히도록 EXCEPTION 블록을 활용해야 합니다.
잘못된 예시 (에러 발생 가능성 있음):
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM users WHERE active = true;
rec RECORD;
BEGIN
OPEN cur;
FETCH cur INTO rec;
-- 작업 중 조기 탈출 또는 커서를 닫지 않음
IF rec.id = 1 THEN
RETURN; -- 커서가 열린 채로 종료됨
END IF;
-- CLOSE cur; 누락
END;
$$;
올바른 예시 (커서 안전 처리):
DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM users WHERE active = true;
rec RECORD;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
-- 실제 작업 수행
RAISE NOTICE 'Processing user: %', rec.name;
-- 조기 종료가 필요할 경우에도 CLOSE 후 탈출
IF rec.id = 999 THEN
CLOSE cur;
RETURN;
END IF;
END LOOP;
CLOSE cur; -- 반드시 명시적 종료
EXCEPTION
WHEN OTHERS THEN
-- 예외 발생 시에도 커서 닫기
IF cur IS NOT NULL THEN
CLOSE cur;
END IF;
RAISE;
END;
$$;
원인 2 해결 - 동적 SQL 문자열 검증
동적으로 구성하는 SQL 문자열은 실행 전에 반드시 로그로 출력하거나 format() 함수를 활용해 안전하게 조합해야 합니다.
잘못된 예시 (문자열이 불완전할 수 있음):
CREATE OR REPLACE FUNCTION get_table_count(p_table_name TEXT)
RETURNS INTEGER AS $$
DECLARE
v_sql TEXT;
v_count INTEGER;
BEGIN
-- 위험: 입력값 검증 없이 직접 문자열 연결
v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;
-- 만약 p_table_name이 NULL이거나 비어있으면 불완전한 SQL 생성
EXECUTE v_sql INTO v_count;
RETURN v_count;
END;
$$ LANGUAGE plpgsql;
올바른 예시 (안전한 동적 SQL 구성):
CREATE OR REPLACE FUNCTION get_table_count(p_schema TEXT, p_table_name TEXT)
RETURNS INTEGER AS $$
DECLARE
v_sql TEXT;
v_count INTEGER;
BEGIN
-- 입력값 NULL 검증
IF p_table_name IS NULL OR trim(p_table_name) = '' THEN
RAISE EXCEPTION 'Table name cannot be NULL or empty';
END IF;
-- format() 함수로 안전하게 식별자 처리 (%I = identifier quoting)
v_sql := format(
'SELECT COUNT(*) FROM %I.%I',
COALESCE(p_schema, 'public'),
p_table_name
);
-- 디버깅용 로그 (운영 환경에서는 DEBUG 레벨로 조정)
RAISE DEBUG 'Executing SQL: %', v_sql;
EXECUTE v_sql INTO v_count;
RETURN v_count;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Failed to execute dynamic SQL [%]: %', v_sql, SQLERRM;
END;
$$ LANGUAGE plpgsql;
-- 호출 예시
SELECT get_table_count('public', 'users');
SELECT get_table_count(NULL, 'orders');
원인 3 해결 - 트랜잭션 블록 내 명령 순서 보장
복합 트랜잭션 내에서 명령의 완료를 보장하고, 비동기 처리나 파이프라인을 사용하는 경우 명시적인 동기화 포인트를 두어야 합니다.
-- 올바른 트랜잭션 블록 처리 예시
DO $$
BEGIN
-- 1단계: 임시 테이블 생성 (완전히 완료 후 다음 단계로)
CREATE TEMP TABLE IF NOT EXISTS temp_processing (
id SERIAL PRIMARY KEY,
data TEXT,
processed_at TIMESTAMPTZ DEFAULT NOW()
);
-- 2단계: 데이터 삽입
INSERT INTO temp_processing (data)
SELECT name FROM users WHERE created_at > NOW() - INTERVAL '1 day';
-- 3단계: 결과 확인 후 처리 (각 단계가 완료된 후 실행)
UPDATE temp_processing
SET data = upper(data)
WHERE data IS NOT NULL;
RAISE NOTICE 'Processing complete. Rows affected: %', (SELECT COUNT(*) FROM temp_processing);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Transaction failed: %', SQLERRM;
END;
$$;
예방 방법
1. 커서 및 동적 SQL에 대한 Wrapper 함수 패턴 도입
반복적으로 커서나 동적 SQL을 사용하는 로직은 별도의 헬퍼 함수로 추상화하여 열기-사용-닫기 패턴을 일관되게 적용하세요. 모든 커서 사용 함수에 EXCEPTION WHEN OTHERS 블록을 반드시 포함시키고, 해당 블록 내에서 커서 상태를 초기화하는 코드를 표준 템플릿으로 팀 내에 공유하는 것이 좋습니다. 코드 리뷰 체크리스트에 "커서 CLOSE 여부", "동적 SQL NULL 검증 여부"를 추가하면 사전 예방에 큰 효과가 있습니다.
-- 팀 공용 커서 처리 표준 템플릿
CREATE OR REPLACE FUNCTION safe_cursor_template()
RETURNS VOID AS $$
DECLARE
cur REFCURSOR;
rec RECORD;
BEGIN
OPEN cur FOR SELECT * FROM target_table;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
-- 비즈니스 로직
END LOOP;
CLOSE cur;
EXCEPTION
WHEN OTHERS THEN
IF cur IS NOT NULL THEN
CLOSE cur;
END IF;
RAISE;
END;
$$ LANGUAGE plpgsql;
2. PostgreSQL 로그 레벨 및 log_min_messages 설정 최적화
개발 및 스테이징 환경에서는 postgresql.conf의 log_min_messages를 DEBUG1 이상으로 설정하여 불완전한 SQL 실행 시도를 조기에 포착하세요. 또한 pg_stat_activity 뷰를 정기적으로 모니터링하여 state 컬럼이 idle in transaction으로 오래 머무는 세션을 탐지하면, 커서나 트랜잭션이 미완료 상태로 방치되는 상황을 사전에 차단할 수 있습니다.
-- 미완료 트랜잭션 세션 모니터링 쿼리
SELECT
pid,
usename,
application_name,
state,
query,
now() - xact_start AS transaction_duration
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'active')
AND xact_start IS NOT NULL
AND now() - xact_start > INTERVAL '5 minutes'
ORDER BY transaction_duration DESC;
관련 에러
-
25P01(no active SQL transaction): 트랜잭션 블록 외부에서 트랜잭션 제어 명령을 실행할 때 발생하며,03000과 함께 트랜잭션 흐름 오류의 대표 사례입니다. -
34000(invalid cursor name): 커서 이름이 잘못 참조되거나 이미 닫힌 커서를 다시 사용할 때 발생하며, 커서 관련03000에러의 후속 증상으로 나타나는 경우가 많습니다. -
42601(syntax error): 동적 SQL 문자열 조합 오류 시03000과 유사한 맥락에서 발생하며, 두 에러가 동시에 로그에 기록될 경우 동적 SQL 구성 로직을 우선 점검해야 합니다. -
40001(serialization failure): 트랜잭션 격리 수준 관련 충돌로 발생하며, 복잡한 트랜잭션 블록에서03000과 함께 나타날 경우 전체 트랜잭션 설계를 재검토할 필요가 있습니다.
Top comments (0)