2026년 08월 14일 | 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 엔진이 이 에러를 발생시킵니다. 이 에러는 주로 동적 쿼리 생성, 페이지네이션 로직 구현, 또는 애플리케이션에서 파라미터 바인딩 처리가 잘못된 경우에 실무에서 자주 마주치게 됩니다.
주요 발생 원인
- 음수(Negative) OFFSET 값 사용
가장 흔한 원인 중 하나로, 애플리케이션에서 페이지 번호를 계산하여 OFFSET 값을 동적으로 생성할 때 발생합니다. 예를 들어 페이지 번호가 0 또는 음수로 잘못 계산되거나, 사용자 입력값이 검증 없이 쿼리에 직접 삽입되는 경우 음수 OFFSET이 생성될 수 있습니다. PostgreSQL은 OFFSET에 음수를 허용하지 않으며, 0 이상의 정수만 허용합니다.
“`sql
— 에러 발생 예시: 음수 OFFSET
SELECT * FROM orders
ORDER BY created_at DESC
OFFSET -10 LIMIT 20;
— ERROR: OFFSET must not be negative
“`
- NULL 값이 OFFSET 절에 전달되는 경우
애플리케이션에서 파라미터 바인딩을 사용할 때, OFFSET 파라미터가 초기화되지 않거나 명시적으로 NULL로 설정된 경우 이 에러가 발생합니다. 특히 ORM(Object-Relational Mapping) 프레임워크나 동적 쿼리 빌더를 사용할 때 NULL 값이 의도치 않게 전달되는 경우가 많습니다. PostgreSQL에서 OFFSET 절은 NULL 값을 유효한 입력으로 허용하지 않습니다.
“`sql
— 에러 발생 예시: NULL OFFSET
DO $$
DECLARE
v_offset INTEGER := NULL;
BEGIN
EXECUTE format(‘SELECT * FROM orders OFFSET %s’, v_offset);
END;
$$;
— ERROR: invalid row count in result offset clause
“`
- 정수로 변환 불가능한 타입 또는 소수점(Float) 값 사용
OFFSET 절에는 반드시 정수(INTEGER) 값이 와야 하는데, 실수(FLOAT 또는 NUMERIC) 타입이나 문자열 등 정수로 명확히 변환할 수 없는 값이 전달되면 에러가 발생합니다. 일부 언어나 프레임워크에서 페이지 계산 결과가 실수형으로 반환되는 경우, 이를 명시적으로 정수형으로 변환하지 않고 쿼리에 삽입하면 이 문제가 생깁니다.
“`sql
— 에러 발생 예시: 소수점 OFFSET
SELECT * FROM products
ORDER BY price
OFFSET 10.5 LIMIT 10;
— ERROR: OFFSET must not be negative (또는 관련 캐스팅 오류)
— 문자열 전달 시 에러
SELECT * FROM products
ORDER BY price
OFFSET ‘ten’ LIMIT 10;
— ERROR: invalid input syntax for type bigint: “ten”
“`
해결 방법
원인 1 해결: 음수 OFFSET 방지를 위한 GREATEST() 함수 사용
OFFSET 값이 음수가 되지 않도록 GREATEST() 함수를 사용하여 항상 0 이상의 값이 되도록 보장할 수 있습니다.
-- GREATEST를 사용하여 음수 방지
SELECT * FROM orders
ORDER BY created_at DESC
OFFSET GREATEST(0, -10) LIMIT 20;
-- 결과: OFFSET 0 으로 처리됨 (에러 없음)
-- 애플리케이션 로직에서 페이지네이션 계산 예시
-- page: 현재 페이지 번호 (1부터 시작), page_size: 페이지당 행 수
SELECT * FROM orders
ORDER BY created_at DESC
OFFSET GREATEST(0, (2 - 1) * 20) -- page=2, page_size=20
LIMIT 20;
원인 2 해결: NULL 값 처리를 위한 COALESCE() 함수 사용
COALESCE() 함수를 사용하여 OFFSET 파라미터가 NULL인 경우 기본값(0)으로 대체합니다.
-- COALESCE를 사용하여 NULL을 0으로 처리
DO $$
DECLARE
v_offset INTEGER := NULL;
BEGIN
EXECUTE format(
'SELECT * FROM orders ORDER BY created_at DESC OFFSET %s LIMIT 20',
COALESCE(v_offset, 0)
);
END;
$$;
-- 실제 쿼리에서의 활용
SELECT * FROM products
ORDER BY name
OFFSET COALESCE(NULL, 0) LIMIT 10;
-- 결과: OFFSET 0으로 정상 실행
원인 3 해결: 명시적 타입 캐스팅(CAST) 사용
실수형 값이나 다른 타입의 값을 OFFSET에 사용해야 할 경우, 명시적으로 정수형으로 변환합니다.
-- CAST를 사용한 명시적 정수 변환
SELECT * FROM products
ORDER BY price
OFFSET CAST(FLOOR(10.9) AS BIGINT) LIMIT 10;
-- 결과: OFFSET 10으로 정상 실행
-- :: 연산자를 사용한 캐스팅
SELECT * FROM products
ORDER BY price
OFFSET (10.9::INT) LIMIT 10;
-- 종합적인 안전한 페이지네이션 함수 예시
CREATE OR REPLACE FUNCTION get_paginated_orders(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 20
)
RETURNS SETOF orders AS $$
BEGIN
RETURN QUERY
SELECT *
FROM orders
ORDER BY created_at DESC
OFFSET GREATEST(0, COALESCE(p_page - 1, 0) * COALESCE(p_page_size, 20))
LIMIT GREATEST(1, COALESCE(p_page_size, 20));
END;
$$ LANGUAGE plpgsql;
-- 함수 호출
SELECT * FROM get_paginated_orders(1, 20);
SELECT * FROM get_paginated_orders(NULL, NULL); -- NULL 안전 처리
예방 방법
- 입력값 검증 레이어 구축 및 파라미터 바인딩 철저히 사용
애플리케이션 레벨에서 OFFSET과 LIMIT에 전달되는 값을 반드시 사전 검증하는 로직을 구축하세요. 직접 쿼리 문자열에 값을 삽입하는 방식(SQL Injection 위험 포함) 대신 파라미터 바인딩($1, $2 방식)을 항상 사용하고, 애플리케이션에서 값이 0 이상의 정수인지 확인한 후 쿼리에 전달해야 합니다. 아래와 같이 PostgreSQL 함수 내에서도 입력값 유효성 검사를 추가하는 것이 좋습니다.
“`sql
CREATE OR REPLACE FUNCTION safe_paginate(
p_table_name TEXT,
p_offset INTEGER,
p_limit INTEGER
)
RETURNS void AS $$
BEGIN
IF p_offset IS NULL OR p_offset < 0 THEN
RAISE EXCEPTION ‘OFFSET must be a non-negative integer, got: %’, p_offset;
END IF;
IF p_limit IS NULL OR p_limit <= 0 THEN
RAISE EXCEPTION ‘LIMIT must be a positive integer, got: %’, p_limit;
END IF;
— 실제 쿼리 실행 로직
END;
$$ LANGUAGE plpgsql;
“`
- Keyset Pagination(커서 기반 페이지네이션) 도입으로 OFFSET 의존성 줄이기
대용량 데이터에서 OFFSET 기반 페이지네이션은 성능 문제뿐만 아니라 이와 같은 에러 위험도 내포합니다. 가능하면 커서 기반(Keyset) 페이지네이션을 도입하여 OFFSET 사용 자체를 최소화하세요. 이 방식은 마지막으로 조회된 레코드의 고유 키를 기준으로 다음 페이지를 조회하므로, OFFSET 관련 에러를 근본적으로 방지할 수 있습니다.
“`sql
— Keyset Pagination 예시 (OFFSET 없이 안전하게 페이지네이션)
— 첫 페이지
SELECT id, created_at, customer_name
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;
— 다음 페이지: 마지막 레코드의 값을 기준으로 조회
SELECT id, created_at, customer_name
FROM orders
WHERE (created_at, id) < ('2024-01-15 10:30:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 20;
“`
관련 에러
- 22012 (
division_by_zero): OFFSET 값을 계산하는 수식에서 0으로 나누기가 발생하는 경우 연계되어 나타날 수 있습니다. - 2201W (
invalid row count in limit clause): OFFSET과 함께 자주 사용되는LIMIT절에서 동일하게 유효하지 않은 값이 전달될 때 발생하는 에러로, 2201X와 쌍을 이루는 관련 에러입니다. LIMIT에 음수나 NULL이 전달될 때 발생합니다. - 22003 (
numeric_value_out_of_range): OFFSET에 전달된 숫자 값이 PostgreSQL의 bigint 범위를 초과할 경우 발생할 수 있는 에러입니다. - 42601 (
syntax_error): 동적 쿼리 생성 과정에서 OFFSET 절 자체의 문법이 잘못 구성된 경우 발생합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.