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

2201W
2026년 06월 09일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2201W invalid row count in limit clause 는?

PostgreSQL 에러 코드 2201W는 LIMIT 절에 유효하지 않은 행 수가 지정되었을 때 발생하는 에러입니다. LIMIT 절에는 0 이상의 정수 값이 와야 하는데, 음수(-1, -100 등)나 소수점이 있는 실수, 또는 NULL이 아닌 잘못된 타입의 값이 전달될 때 이 에러가 트리거됩니다. 주로 동적 쿼리를 생성하는 애플리케이션 코드나 사용자 입력값을 그대로 SQL에 바인딩하는 구조에서 자주 목격되는 에러입니다.


주요 발생 원인

1. 음수 값을 LIMIT 절에 직접 전달하는 경우

가장 흔한 원인으로, 애플리케이션 레이어에서 페이지네이션 로직을 처리할 때 잘못된 계산으로 인해 음수가 만들어지는 경우입니다. 예를 들어 페이지 크기 - 오프셋 같은 잘못된 연산의 결과가 바인딩 변수를 통해 LIMIT에 전달되면 즉시 이 에러가 발생합니다. 사용자 입력값을 충분히 검증하지 않고 SQL에 직접 삽입하는 패턴도 동일한 문제를 일으킵니다.

2. 함수나 표현식의 반환값이 유효하지 않은 경우

동적으로 LIMIT 값을 계산하는 함수나 서브쿼리가 예상치 못한 값(음수, NULL, 비정수)을 반환할 때 에러가 발생합니다. 특히 조건에 따라 분기되는 CASE 표현식이나 집계 함수의 결과를 LIMIT에 사용할 경우, 특정 데이터 상태에서 유효하지 않은 값이 나올 수 있습니다. 개발 환경에서는 정상 작동하다가 프로덕션 데이터에서만 에러가 발생하는 이유가 여기에 있습니다.

3. 저장 프로시저나 PL/pgSQL 내부에서 변수 초기화 오류

PL/pgSQL 함수나 프로시저 내에서 LIMIT 값을 담는 변수가 제대로 초기화되지 않았거나, 로직 오류로 인해 음수가 할당된 채로 동적 SQL이 실행될 때 에러가 발생합니다. EXECUTE 구문을 사용하는 동적 SQL에서는 컴파일 시점에 오류를 잡을 수 없기 때문에 런타임에서야 에러가 표면화되는 경우가 많습니다. 배치 처리 스크립트에서 자주 나타나는 패턴입니다.


해결 방법

원인 1 해결: 음수 값 방어 처리

LIMIT에 전달되는 값이 항상 0 이상이 되도록 GREATEST() 함수를 활용하거나, 조건 분기를 통해 방어 코드를 작성하세요.

-- 문제가 발생하는 쿼리 예시 (limit_val이 음수인 경우)
-- SELECT * FROM orders LIMIT -5;  -- 에러 발생!

-- 해결책 1: GREATEST 함수로 최솟값 보장
SELECT *
FROM orders
LIMIT GREATEST(0, :limit_val);  -- 음수가 와도 0으로 보정됨

-- 해결책 2: CASE 표현식으로 명시적 처리
SELECT *
FROM orders
LIMIT (
    CASE
        WHEN :limit_val < 0 THEN 0
        WHEN :limit_val > 10000 THEN 10000  -- 최댓값도 함께 제한
        ELSE :limit_val
    END
);

-- 해결책 3: NULLIF를 활용해 전체 조회 허용
-- NULL이면 LIMIT 절이 없는 것과 동일하게 동작
SELECT *
FROM orders
LIMIT NULLIF(GREATEST(0, :limit_val), 0);

원인 2 해결: 함수/표현식 반환값 검증

-- 문제가 되는 패턴: 서브쿼리 결과를 바로 LIMIT에 사용
-- SELECT * FROM products LIMIT (SELECT count - 10 FROM config WHERE id = 1);

-- 해결책: COALESCE와 GREATEST를 조합한 안전한 패턴
SELECT *
FROM products
LIMIT (
    SELECT GREATEST(0, COALESCE(row_limit, 100))
    FROM app_config
    WHERE config_key = 'product_page_size'
);

-- CASE 표현식을 활용한 방어적 쿼리
SELECT *
FROM sales_data
LIMIT (
    CASE
        WHEN (SELECT calc_value FROM dynamic_settings) IS NULL THEN 50
        WHEN (SELECT calc_value FROM dynamic_settings) < 0 THEN 50
        ELSE (SELECT calc_value FROM dynamic_settings)
    END
);

원인 3 해결: PL/pgSQL 내 변수 검증

-- 문제가 되는 PL/pgSQL 함수 예시
CREATE OR REPLACE FUNCTION get_recent_orders_bad(p_days INT)
RETURNS SETOF orders AS $$
DECLARE
    v_limit INT;
BEGIN
    v_limit := p_days * -10;  -- 잘못된 계산으로 음수 발생
    RETURN QUERY
        SELECT * FROM orders LIMIT v_limit;  -- 2201W 에러 발생!
END;
$$ LANGUAGE plpgsql;

-- 수정된 안전한 PL/pgSQL 함수
CREATE OR REPLACE FUNCTION get_recent_orders_safe(p_days INT)
RETURNS SETOF orders AS $$
DECLARE
    v_limit INT;
BEGIN
    -- 입력값 유효성 검사
    IF p_days IS NULL OR p_days <= 0 THEN
        RAISE EXCEPTION 'p_days must be a positive integer, got: %', p_days;
    END IF;

    v_limit := p_days * 10;

    -- LIMIT 값 보정 및 상한선 설정
    v_limit := GREATEST(1, LEAST(v_limit, 10000));

    RETURN QUERY
        SELECT * FROM orders
        ORDER BY created_at DESC
        LIMIT v_limit;
END;
$$ LANGUAGE plpgsql;

-- 동적 SQL에서의 안전한 처리
CREATE OR REPLACE FUNCTION dynamic_query_safe(p_table TEXT, p_limit INT)
RETURNS void AS $$
DECLARE
    v_safe_limit INT;
    v_sql TEXT;
BEGIN
    -- LIMIT 값 검증
    v_safe_limit := GREATEST(0, COALESCE(p_limit, 100));
    v_safe_limit := LEAST(v_safe_limit, 50000);  -- 최대 5만 건으로 제한

    v_sql := format('SELECT * FROM %I LIMIT %s', p_table, v_safe_limit);
    EXECUTE v_sql;
END;
$$ LANGUAGE plpgsql;

예방 방법

1. 애플리케이션 레이어에서 입력값 검증 레이어 구축

SQL에 LIMIT 값을 전달하기 전에 반드시 애플리케이션 코드(Python, Java, Node.js 등)에서 입력값이 0 이상의 정수인지 검증하는 레이어를 두세요. DB 레이어에서도 GREATEST(0, ...) 패턴을 사용하는 이중 방어 전략이 프로덕션 환경에서 가장 안정적입니다. 또한 페이지네이션 파라미터는 반드시 상한값도 함께 설정해 LIMIT 999999999 같은 리소스 낭비도 방지해야 합니다.

2. CHECK CONSTRAINT 와 도메인 타입으로 DB 수준 강제

반복적으로 LIMIT 값을 관리하는 설정 테이블이 있다면 CHECK CONSTRAINT를 통해 DB 수준에서 유효하지 않은 값이 저장되지 않도록 강제하세요. PostgreSQL의 도메인(Domain) 타입을 활용하면 재사용 가능한 타입 제약을 만들 수 있어 여러 테이블에서 일관된 검증을 적용할 수 있습니다.

-- 설정 테이블에 CHECK CONSTRAINT 적용
CREATE TABLE pagination_config (
    config_name TEXT PRIMARY KEY,
    page_size    INT NOT NULL CHECK (page_size > 0 AND page_size <= 1000),
    max_limit    INT NOT NULL CHECK (max_limit > 0 AND max_limit <= 50000)
);

-- 재사용 가능한 도메인 타입 생성
CREATE DOMAIN positive_limit AS INT
    CHECK (VALUE > 0 AND VALUE <= 10000);

-- 도메인 타입을 함수 파라미터에 적용
CREATE OR REPLACE FUNCTION get_paginated_users(p_limit positive_limit)
RETURNS SETOF users AS $$
BEGIN
    RETURN QUERY SELECT * FROM users LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;

관련 에러

  • 22003 (numeric_value_out_of_range): LIMIT에 정수 범위를 초과한 숫자가 지정될 때 발생하며 2201W와 함께 자주 등장합니다.
  • 2201X (invalid row count in result offset clause): OFFSET 절에 유효하지 않은 값(음수 등)이 전달될 때 발생하는 에러로, LIMIT 에러와 거의 항상 쌍으로 방어 처리해야 합니다.
  • 22P02 (invalid_text_representation): LIMIT에 숫자가 아닌 문자열이 전달될 때 타입 변환 과정에서 발생하며, 동적 SQL 환경에서 2201W와 연관되어 나타납니다.
  • 42P18 (indeterminate_datatype): 바인딩 변수의 타입이 결정되지 않아 LIMIT 절 처리 중 에러가 발생하는 케이스와 연관됩니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기