2026년 08월 07일 | DBMS Error 가이드
이 글에서 다루는 내용
ORA-02014 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
ORA-02014 cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc. 는?
ORA-02014 에러는 Oracle 데이터베이스에서 SELECT FOR UPDATE 구문을 사용할 때, 해당 쿼리가 DISTINCT, GROUP BY, UNION, INTERSECT, MINUS, 집계 함수(SUM, COUNT 등)를 포함하는 뷰(View)나 서브쿼리를 대상으로 하는 경우 발생합니다. FOR UPDATE 절은 조회된 행(Row)에 대한 잠금(Lock)을 설정하기 위해 특정 행과 테이블을 명확히 식별해야 하는데, 위의 연산들은 여러 행을 집계하거나 중복을 제거하기 때문에 Oracle이 어떤 원본 행에 잠금을 걸어야 하는지 결정할 수 없게 됩니다. 즉, 행 식별 불가로 인해 잠금 메커니즘 자체가 동작할 수 없어 에러가 발생합니다.
주요 발생 원인
1. DISTINCT 또는 GROUP BY를 포함한 뷰에서 FOR UPDATE 사용
가장 흔한 원인입니다. DISTINCT나 GROUP BY는 여러 행을 하나의 결과 행으로 합치거나 중복을 제거하므로, 결과 행이 원본 테이블의 어느 특정 행에 대응되는지 Oracle이 판단할 수 없습니다. 잠금을 걸기 위해서는 반드시 원본 테이블의 특정 ROWID를 특정할 수 있어야 하기 때문에, 이런 집합 연산이 포함된 뷰에서는 FOR UPDATE를 사용할 수 없습니다.
2. UNION, INTERSECT, MINUS 등 집합 연산자를 포함한 뷰에서 FOR UPDATE 사용
UNION, UNION ALL, INTERSECT, MINUS와 같은 집합 연산자는 두 개 이상의 쿼리 결과를 병합하거나 차집합을 구하는 연산입니다. 이 경우에도 결과 행이 어느 테이블의 어느 행에서 비롯된 것인지 명확하게 추적하기 어렵습니다. Oracle은 잠금 대상 행을 명확히 특정하지 못하므로 FOR UPDATE 구문을 허용하지 않습니다.
3. 집계 함수(SUM, COUNT, AVG 등)를 포함한 서브쿼리나 뷰에서 FOR UPDATE 사용
SUM(), COUNT(), AVG(), MAX(), MIN() 등의 집계 함수는 여러 행의 데이터를 계산하여 하나의 값으로 반환합니다. 집계된 결과는 단일 원본 행으로 역추적이 불가능하며, Oracle의 FOR UPDATE 메커니즘이 잠금을 설정할 특정 행을 식별할 수 없게 됩니다. 실무에서 복잡한 리포팅 뷰를 기반으로 데이터를 수정하려는 패턴에서 자주 발생합니다.
해결 방법
원인 1 해결: DISTINCT/GROUP BY 뷰 대신 베이스 테이블에 직접 FOR UPDATE 사용
뷰를 통하지 않고 원본 테이블을 직접 쿼리하여 FOR UPDATE를 적용합니다.
-- 문제가 되는 코드 (뷰에 DISTINCT가 포함된 경우)
-- 가정: vw_employee_dept 뷰는 DISTINCT를 포함
SELECT *
FROM vw_employee_dept
WHERE dept_id = 10
FOR UPDATE; -- ORA-02014 발생!
-- 해결: 베이스 테이블에 직접 FOR UPDATE 적용
SELECT emp_id, emp_name, dept_id, salary
FROM employees
WHERE dept_id = 10
FOR UPDATE;
원인 2 해결: 집합 연산자 포함 뷰에서 인라인 뷰 + ROWID 활용
집합 연산을 사용하는 경우, 먼저 대상 ROWID를 수집한 뒤 해당 ROWID로 원본 테이블을 FOR UPDATE하는 방식으로 우회할 수 있습니다.
-- 문제가 되는 코드 (UNION을 포함한 뷰에서 FOR UPDATE)
SELECT employee_id, name
FROM vw_active_employees -- UNION으로 정의된 뷰
FOR UPDATE; -- ORA-02014 발생!
-- 해결 방법 1: 각 베이스 테이블을 별도로 잠금
SELECT employee_id, name
FROM employees_full_time
WHERE status = 'ACTIVE'
FOR UPDATE;
-- 해결 방법 2: ROWID를 미리 수집하여 원본 테이블에 잠금 적용
DECLARE
CURSOR cur_emp IS
SELECT ROWID AS row_id
FROM employees_full_time
WHERE status = 'ACTIVE';
BEGIN
FOR rec IN cur_emp LOOP
-- ROWID 기반으로 직접 잠금 후 처리
UPDATE employees_full_time
SET last_updated = SYSDATE
WHERE ROWID = rec.row_id;
END LOOP;
COMMIT;
END;
/
원인 3 해결: 집계 함수 뷰 사용 시 서브쿼리로 대상 행 먼저 식별 후 잠금
집계 결과를 기반으로 원본 행을 특정한 뒤, 원본 테이블에 FOR UPDATE를 적용합니다.
-- 문제가 되는 코드 (집계 함수를 포함한 뷰에서 FOR UPDATE)
SELECT dept_id, SUM(salary) AS total_salary
FROM vw_dept_salary_summary -- GROUP BY, SUM 포함 뷰
FOR UPDATE; -- ORA-02014 발생!
-- 해결: 집계 조건을 만족하는 원본 행을 서브쿼리로 찾아 FOR UPDATE 적용
SELECT emp_id, dept_id, salary
FROM employees
WHERE dept_id IN (
SELECT dept_id
FROM employees
GROUP BY dept_id
HAVING SUM(salary) > 500000
)
FOR UPDATE;
-- 또는 CURSOR를 이용한 행별 처리 방식
DECLARE
CURSOR cur_high_dept IS
SELECT dept_id
FROM employees
GROUP BY dept_id
HAVING SUM(salary) > 500000;
BEGIN
FOR dept_rec IN cur_high_dept LOOP
-- 해당 부서의 원본 행에 대해 개별 UPDATE 처리
UPDATE employees
SET salary = salary * 1.05
WHERE dept_id = dept_rec.dept_id;
END LOOP;
COMMIT;
END;
/
보조 해결책: SKIP LOCKED 또는 NOWAIT 활용
잠금 충돌 상황을 우아하게 처리하고 싶다면 베이스 테이블에 FOR UPDATE SKIP LOCKED 또는 FOR UPDATE NOWAIT를 조합하세요.
-- 잠금된 행은 건너뛰고 가용한 행만 처리
SELECT emp_id, emp_name, salary
FROM employees
WHERE dept_id = 20
FOR UPDATE SKIP LOCKED;
-- 잠금 대기 없이 즉시 실패 처리
SELECT emp_id, emp_name, salary
FROM employees
WHERE dept_id = 20
FOR UPDATE NOWAIT;
예방 방법
1. 뷰 설계 시 FOR UPDATE 가능 여부를 명시적으로 문서화하라
뷰를 생성할 때 DISTINCT, GROUP BY, 집계 함수, 집합 연산자 포함 여부를 DDL 주석(COMMENT)에 명시하고, 애플리케이션 개발자가 해당 뷰에 FOR UPDATE를 사용할 수 없음을 미리 인지하도록 합니다. 또한 코드 리뷰 단계에서 뷰 기반의 FOR UPDATE 구문이 포함된 코드를 사전에 걸러낼 수 있는 체크리스트를 팀 표준으로 수립하는 것이 좋습니다.
-- 뷰 생성 시 주석으로 FOR UPDATE 사용 불가를 명시
COMMENT ON TABLE vw_dept_salary_summary IS
'주의: 이 뷰는 GROUP BY 및 SUM 집계를 포함하므로 FOR UPDATE 사용 불가.
행 잠금이 필요한 경우 EMPLOYEES 베이스 테이블을 직접 참조할 것.';
2. 잠금이 필요한 로직은 반드시 베이스 테이블 기반으로 설계하라
애플리케이션의 데이터 수정 로직(Update/Delete)을 설계할 때, 뷰를 통한 조회 후 수정 패턴 대신 베이스 테이블을 직접 다루는 저장 프로시저(Stored Procedure) 또는 PL/SQL 블록 중심의 아키텍처를 채택하세요. 특히 동시성 제어가 중요한 OLTP 환경에서는 뷰는 읽기 전용(Read-Only) 목적으로만 사용하고, 쓰기(Write) 작업은 항상 베이스 테이블에 직접 적용하는 원칙을 팀 코딩 표준으로 정립하는 것이 가장 효과적인 예방책입니다.
관련 에러
- ORA-02049:
timeout occurred while waiting to lock object—FOR UPDATE사용 시 다른 세션이 이미 해당 행을 잠금 중인 경우 대기 시간 초과로 발생.FOR UPDATE NOWAIT또는FOR UPDATE WAIT n옵션 활용을 고려할 것. - ORA-01436:
CONNECT BY loop in user data— 계층형 쿼리(CONNECT BY)에서FOR UPDATE를 잘못 사용하는 경우 연관하여 발생할 수 있음. - ORA-02049, ORA-00054:
resource busy and acquire with NOWAIT specified—FOR UPDATE NOWAIT사용 시 행이 이미 잠금된 경우 발생하며, 예외 처리 로직에서 함께 고려해야 하는 에러. - ORA-01732:
data manipulation operation not legal on this view— 집계 뷰에 대한 DML(INSERT, UPDATE, DELETE) 시도 시 발생하는 에러로, ORA-02014와 유사한 맥락에서 발생.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.