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

02000
2026년 08월 02일 | DBMS Error 가이드

이 글에서 다루는 내용

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

02000 no data 는?

PostgreSQL 에러 코드 02000은 SQLSTATE 표준에서 정의된 “no data” 상태를 나타내며, 쿼리나 커서 작업의 결과로 데이터가 반환되지 않았을 때 발생합니다. 이 에러는 일반적인 오류(ERROR)가 아니라 경고(WARNING) 또는 예외(EXCEPTION) 에 가까운 상태 신호로, PL/pgSQL 함수나 프로시저 내에서 SELECT INTO, FETCH, PERFORM 등의 구문이 아무런 행(row)을 반환하지 못할 때 주로 나타납니다. 실제 운영 환경에서는 애플리케이션 레이어보다 스토어드 프로시저나 트리거 내부에서 조용히 발생하여 로직 오류로 이어지는 경우가 많아 각별한 주의가 필요합니다.


주요 발생 원인

1. PL/pgSQL에서 SELECT INTO로 데이터를 조회했으나 조건에 맞는 행이 없는 경우

PL/pgSQL 블록 내에서 SELECT INTO 구문을 사용하면 결과가 변수에 담기는데, 조건에 맞는 행이 존재하지 않으면 변수는 NULL로 남고 FOUND 시스템 변수는 false가 됩니다. 이 상태를 적절히 처리하지 않으면 이후 로직에서 NULL 값을 참조하거나 잘못된 계산이 이루어져 데이터 무결성 문제가 발생할 수 있습니다.

2. 커서(CURSOR)를 통한 FETCH 작업에서 더 이상 읽을 행이 없는 경우

커서를 사용해 대용량 데이터를 순차적으로 처리할 때, 모든 행을 소진한 이후에도 FETCH를 시도하면 02000 상태가 발생합니다. 루프 종료 조건을 명확하게 구현하지 않으면 커서가 끝났음에도 불구하고 계속해서 처리를 시도하는 무한 루프 또는 잘못된 데이터 처리로 이어질 수 있습니다.

3. STRICT 옵션이 적용된 SELECT INTO 또는 EXECUTE ... INTO STRICT 구문 사용

STRICT 키워드를 명시하면 PostgreSQL은 정확히 하나의 행만 반환되어야 한다고 엄격하게 검사합니다. 결과가 없으면 NO_DATA_FOUND 예외(내부적으로 SQLSTATE P0002)가 발생하지만, 이와 유사하게 일반 SELECT INTO에서도 행이 없을 때 02000 신호가 발생하여 예외 처리 블록에서 혼동을 일으킬 수 있습니다. 특히 동적 SQL을 작성할 때 STRICT 옵션의 동작을 정확히 이해하지 못하면 예상치 못한 에러 흐름이 만들어집니다.


해결 방법

원인 1 해결: SELECT INTO 이후 FOUND 변수 확인

SELECT INTO 실행 직후 반드시 FOUND 시스템 변수를 검사하여 데이터가 존재하는지 확인해야 합니다.

DO $$
DECLARE
    v_user_name TEXT;
    v_user_id   INT := 9999; -- 존재하지 않는 ID
BEGIN
    SELECT name
    INTO v_user_name
    FROM users
    WHERE id = v_user_id;

    -- 핵심: FOUND 변수로 행 존재 여부 확인
    IF NOT FOUND THEN
        RAISE NOTICE 'User ID %에 해당하는 데이터가 없습니다.', v_user_id;
        -- 기본값 설정 또는 조기 종료
        v_user_name := 'UNKNOWN';
    END IF;

    RAISE NOTICE '사용자 이름: %', v_user_name;
END;
$$;

실무에서는 FOUND를 확인하는 패턴을 함수 템플릿에 기본으로 포함시켜 누락을 방지하는 것이 좋습니다.


원인 2 해결: 커서 FETCH 루프에서 FOUND 또는 EXIT WHEN NOT FOUND 활용

커서 기반 루프에서는 FETCH 이후 항상 종료 조건을 명시해야 합니다.

DO $$
DECLARE
    cur_orders CURSOR FOR
        SELECT order_id, amount FROM orders WHERE status = 'PENDING';
    v_order_id  INT;
    v_amount    NUMERIC;
BEGIN
    OPEN cur_orders;

    LOOP
        FETCH cur_orders INTO v_order_id, v_amount;

        -- 더 이상 행이 없으면 루프 종료
        EXIT WHEN NOT FOUND;

        -- 실제 처리 로직
        RAISE NOTICE 'Processing order_id: %, amount: %', v_order_id, v_amount;
        -- UPDATE orders SET processed = true WHERE order_id = v_order_id;
    END LOOP;

    CLOSE cur_orders;
    RAISE NOTICE '커서 처리 완료';
END;
$$;

-- FOR 루프를 사용하면 EXIT WHEN NOT FOUND 없이도 자동 종료됩니다
DO $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN
        SELECT order_id, amount FROM orders WHERE status = 'PENDING'
    LOOP
        RAISE NOTICE 'order_id: %, amount: %', rec.order_id, rec.amount;
    END LOOP;
END;
$$;

원인 3 해결: STRICT 사용 시 명시적 예외 처리

SELECT INTO STRICT를 사용할 때는 반드시 EXCEPTION 블록으로 NO_DATA_FOUNDTOO_MANY_ROWS를 함께 처리해야 합니다.

CREATE OR REPLACE FUNCTION get_user_by_id(p_id INT)
RETURNS TEXT
LANGUAGE plpgsql AS
$$
DECLARE
    v_name TEXT;
BEGIN
    -- STRICT: 정확히 1건이 아니면 예외 발생
    SELECT name
    INTO STRICT v_name
    FROM users
    WHERE id = p_id;

    RETURN v_name;

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        -- SQLSTATE '02000' / P0002 처리
        RAISE WARNING 'ID %에 해당하는 사용자가 없습니다.', p_id;
        RETURN NULL;

    WHEN TOO_MANY_ROWS THEN
        -- SQLSTATE 'P0003' 처리
        RAISE EXCEPTION 'ID %에 대한 복수 행이 존재합니다. 데이터를 확인하세요.', p_id;
END;
$$;

-- 테스트
SELECT get_user_by_id(1);    -- 정상
SELECT get_user_by_id(9999); -- NO_DATA_FOUND 처리

동적 SQL에서의 처리

DO $$
DECLARE
    v_table_name TEXT := 'users';
    v_result     TEXT;
BEGIN
    EXECUTE format('SELECT name FROM %I WHERE id = $1', v_table_name)
    INTO v_result
    USING 9999;

    IF NOT FOUND THEN
        RAISE NOTICE '동적 SQL 결과 없음';
    ELSE
        RAISE NOTICE '결과: %', v_result;
    END IF;
END;
$$;

예방 방법

1. SELECT INTO 이후 항상 FOUND 체크를 코드 리뷰 체크리스트에 포함

팀 내 PL/pgSQL 코딩 컨벤션에 “모든 SELECT INTO 구문 다음에는 반드시 IF NOT FOUND 블록을 작성한다”는 규칙을 명문화하세요. CI/CD 파이프라인에서 pgTAP 또는 단위 테스트를 통해 “데이터 없음” 시나리오를 필수 테스트 케이스로 등록하면 코드 품질을 지속적으로 유지할 수 있습니다.

-- pgTAP을 이용한 예방적 테스트 예시
SELECT throws_ok(
    $$ SELECT get_user_by_id(-1) $$,
    NULL,
    '존재하지 않는 사용자 조회 시 NULL 반환 확인'
);

2. COALESCEDEFAULT 값을 활용한 방어적 쿼리 설계

함수 내부뿐만 아니라 일반 SQL 레벨에서도 COALESCE, LEFT JOIN, DEFAULT 등을 활용해 데이터 부재 상황을 사전에 흡수하는 쿼리 구조를 설계하세요. 이는 no data 상황이 애플리케이션 레이어까지 전파되는 것을 근본적으로 차단합니다.

-- 방어적 쿼리: 데이터 없을 때 기본값 반환
SELECT COALESCE(
    (SELECT name FROM users WHERE id = 9999),
    'UNKNOWN_USER'
) AS user_name;

-- LEFT JOIN 기반 방어적 설계
SELECT
    o.order_id,
    COALESCE(u.name, '탈퇴한 사용자') AS user_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id;

관련 에러

  • P0002 (NO_DATA_FOUND): SELECT INTO STRICT에서 행이 없을 때 발생하는 PL/pgSQL 전용 예외로, 02000의 엄격한 버전입니다.
  • P0003 (TOO_MANY_ROWS): STRICT 옵션 사용 시 2건 이상의 행이 반환될 때 발생하며, 02000과 함께 쌍으로 처리해야 합니다.
  • 02001 (no additional dynamic result sets returned): 동적 결과 집합 처리 시 관련될 수 있는 상태 코드입니다.
  • 23000 (integrity constraint violation): 데이터 없음으로 인한 외래 키 참조 실패와 연관될 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기