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

2201X
2026년 06월 10일 | 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이거나 정수가 아닌 경우 PostgreSQL은 해당 에러를 반환합니다. 주로 동적 쿼리를 생성하는 애플리케이션이나 사용자 입력값을 직접 쿼리에 바인딩할 때 자주 발생하며, 페이지네이션(pagination) 구현 시 특히 빈번하게 나타납니다.


주요 발생 원인

  • 음수 값이 OFFSET에 전달되는 경우

가장 흔한 원인입니다. 페이지네이션 로직에서 현재 페이지 번호와 페이지 크기를 곱하여 OFFSET을 계산할 때, 페이지 번호가 0보다 작거나 잘못된 연산으로 인해 음수가 만들어지는 경우가 있습니다. PostgreSQL은 OFFSET 값으로 0 이상의 정수만 허용하기 때문에 음수가 들어오면 즉시 에러를 발생시킵니다.

  • NULL 값이 OFFSET에 바인딩되는 경우

애플리케이션에서 파라미터를 동적으로 바인딩할 때 OFFSET 파라미터가 초기화되지 않거나 누락되어 NULL이 전달되는 경우입니다. ORM(Object-Relational Mapping) 프레임워크나 쿼리 빌더를 사용할 때 특히 발생하기 쉬우며, NULL은 유효한 행 개수로 인정되지 않기 때문에 에러가 발생합니다.

  • 정수가 아닌 실수(float) 또는 문자열이 OFFSET에 전달되는 경우

사용자 입력을 그대로 쿼리에 삽입하거나, 언어별 타입 변환이 제대로 이루어지지 않아 문자열이나 소수점이 포함된 숫자가 OFFSET 절에 들어오는 경우입니다. 예를 들어 Python에서 float 타입의 값을 변환 없이 쿼리에 넣으면 이 에러가 발생할 수 있습니다.


해결 방법

원인 1: 음수 값 방지

OFFSET 값을 쿼리에 사용하기 전에 반드시 0 이상인지 검증하거나, GREATEST 함수를 사용해 음수가 들어오더라도 0으로 처리되도록 합니다.

-- 잘못된 예시 (음수 OFFSET)
SELECT *
FROM orders
ORDER BY created_at DESC
OFFSET -10 LIMIT 20;
-- ERROR:  invalid row count in result offset clause

-- 올바른 예시: GREATEST 함수로 음수 방지
SELECT *
FROM orders
ORDER BY created_at DESC
OFFSET GREATEST(0, -10) LIMIT 20;

-- 애플리케이션에서 페이지네이션 계산 시 안전하게 처리
-- page_num과 page_size가 외부에서 들어온다고 가정
DO $$
DECLARE
    page_num  INTEGER := -1;  -- 잘못된 입력값 예시
    page_size INTEGER := 20;
    safe_offset INTEGER;
BEGIN
    safe_offset := GREATEST(0, (page_num - 1) * page_size);
    RAISE NOTICE 'Safe OFFSET: %', safe_offset;
END;
$$;

원인 2: NULL 값 방지

NULL이 들어올 가능성이 있는 경우 COALESCE 함수를 활용하여 기본값(0)으로 대체합니다.

-- 잘못된 예시 (NULL OFFSET)
SELECT *
FROM products
ORDER BY id
OFFSET NULL LIMIT 10;
-- ERROR:  invalid row count in result offset clause

-- 올바른 예시: COALESCE로 NULL 처리
SELECT *
FROM products
ORDER BY id
OFFSET COALESCE(NULL, 0) LIMIT 10;

-- 파라미터 바인딩 시나리오 (psql에서 변수 사용 예시)
-- 실제로는 애플리케이션 레이어에서 NULL 체크 후 기본값 적용
SELECT *
FROM products
ORDER BY id
OFFSET COALESCE($1::BIGINT, 0) LIMIT COALESCE($2::BIGINT, 10);

원인 3: 타입 불일치 방지

명시적 타입 캐스팅을 사용하여 OFFSET 값이 반드시 정수형이 되도록 보장합니다.

-- 잘못된 예시 (문자열 또는 실수 OFFSET)
SELECT *
FROM employees
ORDER BY last_name
OFFSET '10.5' LIMIT 5;
-- ERROR:  invalid row count in result offset clause

-- 올바른 예시: 명시적 캐스팅 및 반올림 처리
SELECT *
FROM employees
ORDER BY last_name
OFFSET FLOOR(10.5)::BIGINT LIMIT 5;

-- 사용자 입력값을 안전하게 처리하는 함수 예시
CREATE OR REPLACE FUNCTION safe_offset(input_val TEXT)
RETURNS BIGINT AS $$
DECLARE
    result BIGINT;
BEGIN
    BEGIN
        result := GREATEST(0, FLOOR(input_val::NUMERIC)::BIGINT);
    EXCEPTION WHEN OTHERS THEN
        result := 0;
    END;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- 함수 사용 예시
SELECT *
FROM employees
ORDER BY last_name
OFFSET safe_offset('10.5') LIMIT 5;
-- OFFSET 10 으로 처리됨

공통 방어 쿼리 패턴

실무에서 페이지네이션을 구현할 때 아래와 같이 복합적인 방어 로직을 적용하는 것이 좋습니다.

-- 실무 페이지네이션 안전 패턴
CREATE OR REPLACE FUNCTION get_paged_orders(
    p_page      INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 20
)
RETURNS TABLE (
    order_id   BIGINT,
    customer   TEXT,
    total      NUMERIC,
    created_at TIMESTAMPTZ
) AS $$
DECLARE
    v_offset BIGINT;
    v_limit  BIGINT;
BEGIN
    -- 입력값 정규화
    v_limit  := GREATEST(1, LEAST(COALESCE(p_page_size, 20), 100));
    v_offset := GREATEST(0, (COALESCE(p_page, 1) - 1) * v_limit);

    RETURN QUERY
    SELECT
        o.order_id,
        c.name AS customer,
        o.total_amount AS total,
        o.created_at
    FROM orders o
    JOIN customers c ON c.id = o.customer_id
    ORDER BY o.created_at DESC
    LIMIT v_limit
    OFFSET v_offset;
END;
$$ LANGUAGE plpgsql;

-- 사용 예시
SELECT * FROM get_paged_orders(1, 20);
SELECT * FROM get_paged_orders(-1, 0);  -- 안전하게 처리됨

예방 방법

  • 애플리케이션 레이어에서 입력값 유효성 검사 철저히 수행

OFFSET 값은 데이터베이스에 도달하기 전에 반드시 애플리케이션 레벨에서 먼저 검증해야 합니다. 페이지 번호는 1 이상의 정수인지, 페이지 크기는 허용 범위 내인지, NULL 여부는 없는지를 확인하는 검증 로직을 공통 유틸리티 함수로 만들어 모든 쿼리에서 재사용하세요. 또한 PostgreSQL의 CHECK 제약이나 함수 내부에서 ASSERT를 활용해 이중 방어선을 구축하는 것이 좋습니다.

  • 페이지네이션 로직을 Stored Function으로 캡슐화

위 예시처럼 페이지네이션 관련 로직을 PostgreSQL Stored Function 안으로 캡슐화하면, OFFSET 계산과 유효성 검사를 한 곳에서 중앙 관리할 수 있습니다. 이렇게 하면 여러 애플리케이션이나 팀에서 동일한 함수를 사용하므로, 각기 다른 곳에서 동일한 실수가 반복되는 것을 원천 차단할 수 있습니다. Keyset Pagination(커서 기반 페이지네이션)으로 전환하는 것도 OFFSET 자체를 없애는 근본적인 해결책이 됩니다.


관련 에러

  • 22012 (division_by_zero): OFFSET 계산 과정에서 0으로 나누기가 발생할 때 함께 마주칠 수 있는 에러입니다.
  • 2201W (invalid_row_count_in_limit_clause): 에러 코드 2201X와 쌍을 이루는 에러로, LIMIT 절에 유효하지 않은 값(음수 또는 NULL)이 전달될 때 발생합니다. OFFSET과 LIMIT은 함께 사용되는 경우가 많으므로 두 절 모두 방어 로직을 적용해야 합니다.
  • 22003 (numeric_value_out_of_range): 매우 큰 정수값이 OFFSET에 전달될 때 오버플로우로 인해 발생할 수 있으며, BIGINT 범위를 초과하는 경우에 나타납니다.
  • 42601 (syntax_error): OFFSET 절에 완전히 잘못된 형식의 값이 들어와 파싱 자체가 실패할 때 발생하며, 2201X와 혼동하기 쉽습니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기