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

2201X
2026년 10월 18일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2201X invalid row count in result offset clause 는?

PostgreSQL 에러 코드 2201X(invalid row count in result offset clause)는 SQL 쿼리에서 OFFSET 절에 유효하지 않은 값이 지정되었을 때 발생합니다. 구체적으로, OFFSET 값으로 음수가 전달되거나 NULL이 아닌 정수가 아닌 값이 사용될 경우 이 에러가 트리거됩니다. 페이지네이션(Pagination) 처리를 하는 웹 애플리케이션이나 배치 작업에서 동적으로 오프셋 값을 계산할 때 자주 마주치는 에러입니다.


주요 발생 원인

1. OFFSET 절에 음수 값 전달

가장 흔한 원인으로, 애플리케이션 레이어에서 페이지 번호를 계산하다가 잘못된 음수 값이 OFFSET에 전달되는 경우입니다. 예를 들어 (page - 1) * page_size 공식을 사용할 때 page가 0이거나 음수이면 OFFSET 값이 음수가 됩니다. PostgreSQL은 OFFSET에 반드시 0 이상의 정수만 허용하므로 즉시 에러를 발생시킵니다.

2. 사용자 입력 값을 검증 없이 OFFSET에 직접 사용

사용자가 URL 파라미터나 API 요청 바디를 통해 전달한 값을 별도의 유효성 검사 없이 동적 SQL에 직접 삽입하는 경우입니다. 악의적이거나 잘못된 입력(예: -10, abc, 또는 매우 큰 음수)이 그대로 쿼리에 반영될 수 있습니다. 이는 보안 취약점(SQL 인젝션 위험)과 런타임 에러를 동시에 유발할 수 있어 특히 주의가 필요합니다.

3. 저장 프로시저 또는 함수 내부의 동적 SQL 연산 오류

PL/pgSQL 함수나 저장 프로시저 내에서 변수 연산을 통해 OFFSET 값을 동적으로 계산할 때, 중간 연산 결과가 의도치 않게 음수가 되는 경우입니다. 특히 COALESCE, NULLIF 등의 함수 조합이나 복잡한 비즈니스 로직 처리 중에 기대값과 다른 결과가 나올 수 있습니다. 이런 에러는 특정 조건에서만 발생하기 때문에 디버깅이 까다롭습니다.


해결 방법

원인 1 해결: OFFSET 값에 음수가 전달되는 경우

GREATEST() 함수를 사용하여 OFFSET 값이 항상 0 이상이 되도록 강제합니다.

-- 문제가 되는 쿼리 (page=0일 때 OFFSET = -10이 되어 에러 발생)
SELECT *
FROM orders
ORDER BY created_at DESC
OFFSET (0 - 1) * 10  -- OFFSET -10 → 에러!
LIMIT 10;

-- 해결: GREATEST()를 이용해 최솟값을 0으로 보정
SELECT *
FROM orders
ORDER BY created_at DESC
OFFSET GREATEST(0, (0 - 1) * 10)  -- OFFSET 0으로 처리
LIMIT 10;

-- 실무 예시: 페이지 번호가 1 미만이면 1로 보정 후 계산
DO $$
DECLARE
    v_page     INTEGER := 0;   -- 잘못된 페이지 번호
    v_pagesize INTEGER := 10;
    v_offset   INTEGER;
BEGIN
    -- 페이지 번호가 1 미만이면 1로 강제 보정
    v_page   := GREATEST(1, v_page);
    v_offset := (v_page - 1) * v_pagesize;

    RAISE NOTICE 'Calculated OFFSET: %', v_offset;
END;
$$;

원인 2 해결: 사용자 입력 값 검증 없이 사용하는 경우

파라미터 바인딩과 서버 사이드 유효성 검사를 반드시 병행해야 합니다.

-- 나쁜 예: 동적 SQL에 사용자 입력을 직접 삽입 (절대 금지)
-- EXECUTE 'SELECT * FROM products OFFSET ' || user_input || ' LIMIT 10';

-- 좋은 예: 파라미터 바인딩 + 입력값 사전 검증 함수
CREATE OR REPLACE FUNCTION get_products_paged(
    p_offset INTEGER,
    p_limit  INTEGER DEFAULT 10
)
RETURNS SETOF products AS $$
BEGIN
    -- 입력값 유효성 검사
    IF p_offset IS NULL OR p_offset < 0 THEN
        RAISE EXCEPTION 'OFFSET 값은 0 이상의 정수여야 합니다. 입력값: %', p_offset
            USING ERRCODE = '22023';
    END IF;

    IF p_limit IS NULL OR p_limit <= 0 OR p_limit > 1000 THEN
        RAISE EXCEPTION 'LIMIT 값은 1 이상 1000 이하여야 합니다. 입력값: %', p_limit
            USING ERRCODE = '22023';
    END IF;

    RETURN QUERY
    SELECT *
    FROM products
    ORDER BY id
    OFFSET p_offset
    LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;

-- 올바른 사용 예
SELECT * FROM get_products_paged(0, 10);   -- 1페이지
SELECT * FROM get_products_paged(10, 10);  -- 2페이지

-- 잘못된 입력 시 명확한 에러 메시지 반환
-- SELECT * FROM get_products_paged(-5, 10);  -- 에러 발생 및 안내

원인 3 해결: 동적 SQL에서 연산 오류로 음수가 발생하는 경우

PL/pgSQL 함수 내에서 중간 연산 결과를 검증하는 로직을 추가합니다.

CREATE OR REPLACE FUNCTION fetch_audit_logs(
    p_page     INTEGER,
    p_pagesize INTEGER DEFAULT 20
)
RETURNS TABLE(log_id BIGINT, event_name TEXT, created_at TIMESTAMPTZ) AS $$
DECLARE
    v_offset INTEGER;
    v_safe_page INTEGER;
BEGIN
    -- 안전한 페이지 번호 계산 (최소 1 보장)
    v_safe_page := GREATEST(1, COALESCE(p_page, 1));
    v_offset    := (v_safe_page - 1) * GREATEST(1, COALESCE(p_pagesize, 20));

    -- 디버그: 실제 OFFSET 값 로깅
    RAISE DEBUG 'fetch_audit_logs: page=%, pagesize=%, offset=%',
        v_safe_page, p_pagesize, v_offset;

    RETURN QUERY
    SELECT al.log_id, al.event_name, al.created_at
    FROM audit_logs al
    ORDER BY al.created_at DESC
    OFFSET v_offset
    LIMIT p_pagesize;
END;
$$ LANGUAGE plpgsql;

-- 테스트
SELECT * FROM fetch_audit_logs(1, 20);
SELECT * FROM fetch_audit_logs(NULL, NULL);  -- NULL 처리 확인

예방 방법

1. 애플리케이션 레이어와 DB 레이어 이중 검증 적용

OFFSET 값은 반드시 애플리케이션(백엔드 API)에서 1차로 유효성 검사를 수행하고, DB 함수/프로시저에서도 2차로 검증하는 이중 방어 전략을 채택하세요. GREATEST(0, offset_value) 패턴을 공통 유틸리티로 표준화하고, 팀 내 코딩 컨벤션으로 정착시키면 실수를 크게 줄일 수 있습니다.

2. 페이지네이션 로직을 전담 함수로 캡슐화

페이지네이션 관련 SQL을 여러 곳에 분산하지 말고, 검증 로직이 내장된 전용 함수(예: paginate(table_name, page, pagesize))로 캡슐화하여 재사용하세요. 이렇게 하면 OFFSET 계산 로직이 단일 진입점에서 관리되어 버그 수정이나 정책 변경 시 한 곳만 수정하면 됩니다. 또한 pg_stat_statements를 활용해 실제 실행된 OFFSET 값의 분포를 주기적으로 모니터링하는 것도 좋은 습관입니다.


관련 에러

  • 22012 (division_by_zero): OFFSET 계산 중 0으로 나누기 연산이 포함된 경우 함께 발생할 수 있습니다.
  • 2201W (invalid row count in fetch first clause): FETCH FIRST n ROWS ONLY 구문에서 유사한 유효하지 않은 값이 사용될 때 발생하는 에러로, LIMIT 절과 관련이 깊습니다.
  • 22003 (numeric_value_out_of_range): OFFSET에 INTEGER 범위를 초과하는 매우 큰 값이 전달될 때 연관되어 나타날 수 있습니다.
  • 42P01 (undefined_table): 동적 SQL로 OFFSET 처리 시 테이블명 오류와 함께 복합적으로 발생하기도 합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기