2026년 06월 06일 | DBMS Error 가이드
이 글에서 다루는 내용
22014 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
22014 invalid argument for ntile function 는?
PostgreSQL 에러 코드 22014는 NTILE() 윈도우 함수에 잘못된 인수를 전달했을 때 발생하는 에러입니다. NTILE(n) 함수는 결과 집합을 n개의 버킷(그룹)으로 나누는 윈도우 함수인데, 이 n 값이 NULL이거나 0 이하의 정수일 경우 해당 에러가 발생합니다. 실무에서는 동적으로 생성된 쿼리나 사용자 입력값을 그대로 NTILE 함수에 전달할 때 자주 마주치는 에러입니다.
주요 발생 원인
1. NTILE 인수로 NULL 값 전달
가장 흔한 원인은 NTILE 함수의 인수로 NULL 값이 전달되는 경우입니다. 서브쿼리나 변수, 또는 사용자 입력에서 가져온 값이 NULL일 경우 PostgreSQL은 이를 유효하지 않은 인수로 판단하여 22014 에러를 발생시킵니다. 특히 동적 SQL을 구성하거나 애플리케이션 레이어에서 파라미터 바인딩을 할 때 NULL 체크를 빠뜨리는 경우가 많습니다.
-- 에러 발생 예시: NULL 전달
SELECT
employee_id,
salary,
NTILE(NULL) OVER (ORDER BY salary DESC) AS bucket
FROM employees;
-- ERROR: 22014: argument of ntile must be greater than zero
-- NULL이 들어오는 현실적인 상황
WITH params AS (
SELECT NULL::INTEGER AS bucket_count
)
SELECT
employee_id,
salary,
NTILE((SELECT bucket_count FROM params)) OVER (ORDER BY salary DESC) AS bucket
FROM employees;
-- 위 쿼리도 동일한 에러 발생
2. NTILE 인수로 0 또는 음수 값 전달
NTILE 함수의 인수는 반드시 1 이상의 양의 정수여야 합니다. 0이나 음수를 전달하면 PostgreSQL은 “버킷을 0개 또는 음수 개로 나눈다”는 의미 없는 연산을 수행할 수 없기 때문에 에러를 발생시킵니다. 잘못된 비즈니스 로직이나 계산 결과가 음수로 떨어지는 경우에 발생할 수 있습니다.
-- 에러 발생 예시: 0 전달
SELECT
product_id,
sales_amount,
NTILE(0) OVER (ORDER BY sales_amount DESC) AS tier
FROM sales;
-- ERROR: 22014: argument of ntile must be greater than zero
-- 에러 발생 예시: 음수 전달
SELECT
product_id,
sales_amount,
NTILE(-5) OVER (ORDER BY sales_amount DESC) AS tier
FROM sales;
-- ERROR: 22014: argument of ntile must be greater than zero
3. 동적 쿼리 또는 함수 내에서 검증 없이 파라미터 사용
PL/pgSQL 함수나 동적 SQL(EXECUTE 구문)에서 외부로부터 입력받은 값을 검증 없이 NTILE 함수에 그대로 사용하는 경우입니다. 이 경우 개발 및 테스트 환경에서는 문제가 없더라도, 운영 환경에서 예상치 못한 입력값이 들어오면 갑작스럽게 에러가 발생할 수 있습니다.
-- 위험한 PL/pgSQL 함수 예시
CREATE OR REPLACE FUNCTION get_employee_buckets(p_bucket_count INTEGER)
RETURNS TABLE(employee_id INT, salary NUMERIC, bucket INT) AS $$
BEGIN
RETURN QUERY
SELECT
e.employee_id,
e.salary,
NTILE(p_bucket_count) OVER (ORDER BY e.salary DESC)::INT AS bucket
FROM employees e;
-- p_bucket_count가 NULL, 0, 음수이면 22014 에러 발생!
END;
$$ LANGUAGE plpgsql;
-- 이렇게 호출하면 에러 발생
SELECT * FROM get_employee_buckets(0);
SELECT * FROM get_employee_buckets(NULL);
해결 방법
해결책 1: COALESCE와 GREATEST를 활용한 NULL 및 음수 방어
-- COALESCE로 NULL 처리, GREATEST로 최솟값 보장
SELECT
employee_id,
salary,
NTILE(GREATEST(COALESCE(bucket_param, 1), 1)) OVER (ORDER BY salary DESC) AS bucket
FROM employees
CROSS JOIN (SELECT NULL::INTEGER AS bucket_param) AS params;
-- 실제 애플리케이션 파라미터 적용 예시
SELECT
employee_id,
department_id,
salary,
NTILE(GREATEST(COALESCE($1, 4), 1)) OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS salary_tier
FROM employees;
해결책 2: PL/pgSQL 함수에 입력값 유효성 검사 추가
CREATE OR REPLACE FUNCTION get_employee_buckets(p_bucket_count INTEGER)
RETURNS TABLE(employee_id INT, salary NUMERIC, bucket INT) AS $$
BEGIN
-- 입력값 유효성 검사
IF p_bucket_count IS NULL OR p_bucket_count <= 0 THEN
RAISE EXCEPTION 'bucket_count must be a positive integer, got: %', p_bucket_count
USING ERRCODE = '22014';
END IF;
RETURN QUERY
SELECT
e.employee_id,
e.salary,
NTILE(p_bucket_count) OVER (ORDER BY e.salary DESC)::INT AS bucket
FROM employees e;
END;
$$ LANGUAGE plpgsql;
-- 정상 호출
SELECT * FROM get_employee_buckets(5);
-- 잘못된 호출 시 명확한 에러 메시지 출력
SELECT * FROM get_employee_buckets(0); -- 명시적 에러
SELECT * FROM get_employee_buckets(-1); -- 명시적 에러
해결책 3: 동적 SQL에서의 안전한 처리
-- 동적 쿼리에서의 안전한 NTILE 사용 예시
DO $$
DECLARE
v_bucket_count INTEGER := 0; -- 잘못된 값이 들어온 상황
v_safe_count INTEGER;
v_sql TEXT;
BEGIN
-- 안전한 값으로 보정
v_safe_count := GREATEST(COALESCE(v_bucket_count, 1), 1);
v_sql := format(
'SELECT employee_id, salary, NTILE(%s) OVER (ORDER BY salary DESC) AS bucket FROM employees',
v_safe_count
);
EXECUTE v_sql;
RAISE NOTICE 'Query executed with bucket count: %', v_safe_count;
END;
$$;
해결책 4: CHECK 제약조건을 활용한 데이터 레이어 방어
-- 설정 테이블에 CHECK 제약조건 추가
CREATE TABLE report_config (
config_id SERIAL PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
bucket_count INTEGER NOT NULL CHECK (bucket_count >= 1), -- 1 이상 강제
created_at TIMESTAMP DEFAULT NOW()
);
-- 이제 잘못된 값은 INSERT 단계에서 차단됨
INSERT INTO report_config (config_name, bucket_count) VALUES ('sales_tier', 0);
-- ERROR: new row for relation "report_config" violates check constraint
예방 방법
1. 입력값 방어 로직을 데이터베이스 레이어에 내재화하기
NTILE 함수를 사용하는 모든 쿼리와 함수에는 반드시 GREATEST(COALESCE(입력값, 기본값), 1) 패턴을 적용하는 코딩 컨벤션을 팀 내에 정착시켜야 합니다. 애플리케이션 코드에서의 검증만으로는 부족하며, 데이터베이스 레이어 자체에서 방어 로직을 갖추는 것이 다중 방어 전략의 핵심입니다. 또한 NTILE을 사용하는 뷰(View)나 함수를 생성할 때 주석으로 인수 제약 조건을 명시하여 후임 개발자나 DBA가 쉽게 파악할 수 있도록 해야 합니다.
-- 팀 컨벤션 적용 예시 (안전한 NTILE 래퍼 함수)
CREATE OR REPLACE FUNCTION safe_ntile(p_n INTEGER, p_default INTEGER DEFAULT 4)
RETURNS INTEGER AS $$
BEGIN
RETURN GREATEST(COALESCE(p_n, p_default), 1);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 사용 예시
SELECT
employee_id,
salary,
NTILE(safe_ntile($1)) OVER (ORDER BY salary DESC) AS bucket
FROM employees;
2. 통합 테스트에 경계값(Boundary Value) 케이스 포함하기
NTILE을 사용하는 쿼리나 함수에 대한 테스트를 작성할 때, 반드시 NULL, 0, -1, 1(최솟값) 등의 경계값 케이스를 포함시켜야 합니다. pgTAP 같은 PostgreSQL 전용 테스트 프레임워크를 활용하면 데이터베이스 레이어의 함수를 직접 테스트할 수 있으며, CI/CD 파이프라인에 통합하여 배포 전에 문제를 사전 차단할 수 있습니다.
-- pgTAP을 활용한 경계값 테스트 예시
SELECT plan(3);
SELECT throws_ok(
$$ SELECT * FROM get_employee_buckets(0) $$,
'22014',
'bucket_count must be a positive integer, got: 0',
'NTILE with 0 should raise error 22014'
);
SELECT throws_ok(
$$ SELECT * FROM get_employee_buckets(NULL) $$,
'22014',
'bucket_count must be a positive integer, got: <NULL>',
'NTILE with NULL should raise error 22014'
);
SELECT lives_ok(
$$ SELECT * FROM get_employee_buckets(1) $$,
'NTILE with 1 should work fine'
);
SELECT * FROM finish();
관련 에러
- 22003 (numeric_value_out_of_range): NTILE 함수의 인수가 INTEGER 범위를 초과하는 매우 큰 값일 경우 발생할 수 있는 연관 에러입니다.
- 22012 (division_by_zero): 윈도우 함수 계산 과정에서 발생하는 유사한 성격의 산술 오류로, 0으로 나누는 상황에서 발생합니다.
- 42883 (undefined_function): NTILE 함수의 인수 타입이 INTEGER가 아닌 다른 타입(예: FLOAT, TEXT)으로 전달되어 함수 시그니처가 맞지 않을 때 발생할 수 있습니다.
- 42P13 (invalid_function_definition): NTILE을 포함한 함수를 잘못 정의할 때 발생하며, 윈도우 함수 관련 개발 시 함께 숙지해야 할 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.