JOIN 키가 없었다 — Oracle에서 col_co를 복구한 과정
ROW_NUMBER()와 MERGE INTO로 Oracle에서 안전하게 코드를 복구한 과정을 정리했다.
일단 테이블 구조부터
이번 작업에 등장하는 테이블은 두 개다.
테이블A (마스터 테이블)
특정 엔티티의 코드와 이름을 관리하는 마스터 테이블. col_id(식별코드) + col_co(회사코드)가 복합 PK다.
| 컬럼명 | 타입 | 설명 |
|---|---|---|
| col_id | VARCHAR2 NOT NULL | 🔑 식별코드 (PK) |
| col_co | VARCHAR2 NOT NULL | 🔑 회사코드 (PK) |
| col_nm | VARCHAR2 NOT NULL | 이름 ← 이게 매핑 기준 |
| col_biz | VARCHAR2 | 기타 정보 |
| col_yn | VARCHAR2(1) NOT NULL | 상태여부 |
| col_reg | DATE NOT NULL | 등록일시 |
| col_upd | DATE NOT NULL | 수정일시 |
여기서 col_nm으로 항목을 찾아서 col_id를 가져오는 게 핵심이다.
테이블B (트랜잭션 테이블)
실제 업무 데이터를 관리하는 테이블. col_co + col_no가 복합 PK.
문제는 여기에 col_nm 컬럼이 없다는 것. col_id만 있는데, 그 값이 잘못 들어가 있는 상태다.
| 컬럼명 | 타입 | 설명 |
|---|---|---|
| col_co | VARCHAR2 NOT NULL | 🔑 회사코드 (PK) |
| col_no | VARCHAR2 NOT NULL | 🔑 일련번호 (PK) |
| col_id | VARCHAR2 | ⚠️ 식별코드 — 오늘 고쳐야 할 컬럼 |
| col_nm2 | VARCHAR2 NOT NULL | 항목명 |
| col_dt1 | VARCHAR2(8) | 시작일자 |
| col_dt2 | VARCHAR2(8) | 종료일자 |
| col_reg | DATE NOT NULL | 등록일시 |
| col_upd | DATE NOT NULL | 수정일시 |
둘의 관계를 그림으로 보면
테이블A (마스터)
┌──────────────────────────────────────┐
│ col_id VARCHAR2 PK ◄── 정답 코드 │──┐
│ col_co VARCHAR2 PK │ │ col_nm으로 찾아서
│ col_nm VARCHAR2 ← 매핑 기준 │ │ col_id를 가져옴
└──────────────────────────────────────┘ │
│
테이블B (트랜잭션) │
┌──────────────────────────────────────┐ │
│ col_co VARCHAR2 PK │ │
│ col_no VARCHAR2 PK │ │
│ col_id VARCHAR2 ────────────────── ┼──┘ ← 잘못된 값 → 복구 대상
│ col_nm2 VARCHAR2 │
│ (col_nm 없음!) │
└──────────────────────────────────────┘
처음엔 쉬울 줄 알았다
외부에서 이름 목록 N건을 받았고, 테이블A에서 코드를 찾아 테이블B에 넣으면 끝이라고 생각했다.
-- 처음에 떠올린 방법
UPDATE 테이블B H
SET H.col_id = (
SELECT A.col_id
FROM 테이블A A
WHERE A.col_nm = H.col_nm -- ← 근데 이 컬럼이 없잖아...
)
바로 막혔다. 테이블B에 col_nm이 없으니까 JOIN 자체가 불가능한 상황.
그래서 외부에서 받은 이름 목록을 다른 방식으로 활용해야 했다.
두 번째 함정: 같은 이름인데 코드가 다르다?
테이블A를 들여다봤더니 이런 게 있었다.
| col_id | col_nm |
|---|---|
| C001 | AAA(주) |
| C002 | AAA |
| C003 | AAA㈜ |
응? 셋 다 같은 항목 아닌가? 맞다. 근데 코드가 다르다.
(주), ㈜, 아무것도 없는 것 — 표기 방식에 따라 별개 코드로 등록돼 있었다.
| 표기 | 코드 |
|---|---|
| BBB㈜ | C010 |
| BBB | C011 |
| ㈜CCC | C020 |
| CCC | C021 |
이 상태에서 LIKE나 TRIM으로 퉁치면 어떻게 될까? 엉뚱한 코드가 들어간다.
표기 그대로 정확히 매핑하는 수밖에 없었다.
Python으로 깔끔하게 해결
N개를 손으로 매핑하다가 실수하면 답이 없다. Python 딕셔너리로 자동화했다.
# 테이블A 기준으로 만든 매핑 딕셔너리 (이름 → 코드)
mapping = {
"AAA": "C001",
"AAA(주)": "C002",
"AAA㈜": "C003",
"㈜BBB": "C010",
"BBB": "C011",
# ... 총 N개
}
# 외부에서 받은 이름 목록
nm_list = ["AAA", "㈜BBB", ...]
# 매핑 실행
results = []
for nm in nm_list:
code = mapping.get(nm.strip(), "NOT_FOUND")
results.append(code)
print(results) # 미매칭 0건!
결과: N개 전부 매칭 성공. NOT_FOUND 하나도 없었다.
본론: MERGE INTO + ROW_NUMBER()
왜 ROW_NUMBER()를 썼냐면
테이블B에 col_nm이 없으니 JOIN 키가 없다.
그래서 Python이 뽑아준 코드 순서와 col_no 오름차순 순서를 맞춰서 매핑하는 방식을 썼다.
여기서 순서 보장이 핵심인데, ROWNUM을 쓰면 낭패 본다.
ROWNUM이 왜 위험한가?
| 구분 | ROWNUM | ROW_NUMBER() |
|---|---|---|
| 정렬 보장 | ❌ 안 됨 | ✅ ORDER BY로 확정 |
| 사용 위치 | WHERE 절 | SELECT 절 (윈도우 함수) |
| 안전성 | 낮음 | 높음 |
ORDER BY를 붙여도 ROWNUM은 정렬 전에 번호가 붙는다.
실행할 때마다 순서가 달라질 수 있다는 뜻. 무조건 ROW_NUMBER() OVER (ORDER BY col_no)로 가야 한다.
최종 SQL
MERGE INTO 테이블B A
USING (
SELECT col_no, ROW_NUMBER() OVER (ORDER BY col_no) AS RN
FROM 테이블B
) B ON (A.col_no = B.col_no)
WHEN MATCHED THEN UPDATE SET A.col_id =
CASE B.RN
WHEN 1 THEN 'C001'
WHEN 2 THEN 'C002'
WHEN 3 THEN 'C003'
-- Python이 생성한 N개 매핑
WHEN N THEN 'C0NN'
END;
단순 UPDATE 대신 MERGE INTO를 쓴 이유는 매칭 실패 시 에러 없이 넘어가고,
필요하면 WHEN NOT MATCHED → INSERT로 확장도 되기 때문이다.
-- MERGE INTO 기본 구조
MERGE INTO [대상 테이블] A
USING [기준 서브쿼리 or 테이블] B
ON (A.키 = B.키)
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ... -- 선택사항
실행할 때는 꼭 이 순서대로
-- 1. 먼저 현재 데이터 확인 (행 수가 N건인지 체크!)
SELECT col_no, col_id FROM 테이블B ORDER BY col_no;
-- 2. MERGE 실행 (아직 커밋 아님)
MERGE INTO 테이블B A
USING ( ... ) B ON (A.col_no = B.col_no)
WHEN MATCHED THEN UPDATE SET A.col_id = CASE B.RN ... END;
-- 3. 결과 다시 확인
SELECT col_no, col_id FROM 테이블B ORDER BY col_no;
-- 4-A. 맞으면 커밋
COMMIT;
-- 4-B. 이상하면 롤백
ROLLBACK;
⚠️ Oracle은 DML 후 자동 커밋이 안 된다. 눈으로 확인하고 커밋하자.
1번에서 행 수가 정확히 N건이어야 순서 매핑이 맞게 들어간다.
마무리 정리
- 테이블B에
col_nm이 없어서 JOIN UPDATE는 처음부터 불가능했다. - 같은 항목이라도 표기 차이로 코드가 달라지는 케이스는 Python 딕셔너리로만 안전하게 잡을 수 있었다.
- Oracle 순서 기반 UPDATE에서 ROWNUM은 쓰면 안 된다.
ROW_NUMBER() OVER (ORDER BY 컬럼)이 답이다. - UPDATE보다 MERGE INTO가 더 유연하고 안전하다.