개요 및 무엇을 얻어갈 것인가
여러 테이블에 흩어진 결과를 하나로 합치거나, 두 결과 집합의 공통/차이만 뽑아내야 할 때 JOIN으로 풀면 조건문이 복잡해지고 가독성이 떨어집니다. ABAP SQL의 집합 연산자(UNION / INTERSECT / EXCEPT)는 이런 문제를 행 단위 집합 대수로 깔끔하게 해결해줍니다. 이 글에서는 ABAP 7.5x 이후 강화된 집합 연산자 문법을 SalesOrder, Customer, DeliveryRecord 시나리오로 살펴봅니다.
- UNION과 UNION ALL의 동작 차이와 성능 트레이드오프 이해
- INTERSECT로 두 결과 집합의 교집합 추출
- EXCEPT로 첫 결과 집합에만 존재하는 행 추출
- CDS View에서 UNION을 활용한 분할 테이블 통합
- 집합 연산 vs JOIN의 선택 기준 체득
이미 알고 있어야 할 것
ABAP Open SQL의 SELECT ... FROM ... INTO TABLE 기본 구문, 내부 테이블(STANDARD TABLE OF) 선언, 그리고 간단한 WHERE 절 조건을 작성할 수 있어야 합니다. CDS View 부분은 ABAP CDS의 기본 define view 문법을 알고 있다면 더 수월하게 따라올 수 있습니다.
환경 / 버전 / 준비물
집합 연산자는 ABAP 릴리스에 따라 지원 범위가 다릅니다. 일반적으로 다음 환경을 권장합니다.
- NetWeaver AS ABAP 7.50 이상:
UNION지원 (가장 먼저 도입) - ABAP 7.53 이상:
INTERSECT,EXCEPT지원 추가 - ABAP CDS: 7.40 SP08 이후
UNION/UNION ALL이 가능하며, INTERSECT/EXCEPT는 ABAP SQL에서 사용 - ABAP Cloud / SAP BTP ABAP Environment: 모두 지원되며 릴리스 노트 확인 권장
- 실습 도구: ADT(ABAP Development Tools in Eclipse) 또는 SE80, 테스트용 클래스 +
cl_demo_output
예제에서 사용할 가상 테이블은 다음과 같다고 가정합니다. zsales_order(주문 헤더), zdelivery_rec(배송 기록), zcustomer_a·zcustomer_b(서로 다른 채널의 고객 마스터). 실제 시스템에서는 본인의 테이블 명으로 치환해 사용하면 됩니다.
핵심 개념 — 집합 대수로 보는 SELECT
관계형 데이터베이스는 테이블을 "행의 집합"으로 다룹니다. 집합 연산자는 두 개 이상의 SELECT 결과를 입력 집합으로 받아, 합집합/교집합/차집합을 출력하는 연산자입니다. 비유하자면, 두 장의 OHP 필름을 겹쳐서 "둘 다 칠해진 부분"(INTERSECT), "어느 쪽이든 칠해진 부분"(UNION), "왼쪽에만 칠해진 부분"(EXCEPT)을 따로 떼어내는 것과 같습니다.
세 연산자의 동작을 정리하면 다음과 같습니다.
| 연산자 | 의미 | 중복 처리 |
|---|---|---|
| UNION | 합집합 (A ∪ B) | 중복 행 제거 |
| UNION ALL | 합집합 (단순 연결) | 중복 행 유지 |
| INTERSECT | 교집합 (A ∩ B) | 중복 행 제거 |
| EXCEPT | 차집합 (A − B) | 중복 행 제거 |
집합 연산이 성립하려면 두 가지 규칙을 지켜야 합니다. 첫째, 모든 SELECT의 컬럼 개수가 같아야 합니다. 둘째, 같은 위치의 컬럼 데이터 타입이 호환되어야 합니다. 결과의 컬럼명은 첫 번째 SELECT의 이름을 따릅니다. ORDER BY는 전체 결과에 대해 한 번만 마지막에 적용됩니다.
우선순위는 일반적으로 INTERSECT가 UNION/EXCEPT보다 높습니다. 가독성을 위해 괄호로 명시하는 것을 권장합니다.
실전 예제 1단계 — UNION / UNION ALL 기본
두 개의 고객 마스터(zcustomer_a는 오프라인 채널, zcustomer_b는 온라인 채널)에서 전체 고객 목록을 하나로 합치는 시나리오입니다. UNION으로 중복을 제거하고, UNION ALL로 단순 연결한 결과를 비교합니다.
REPORT z_set_op_basic.
TYPES: BEGIN OF ty_customer,
customer_id TYPE c LENGTH 10,
customer_name TYPE c LENGTH 40,
channel TYPE c LENGTH 10,
END OF ty_customer.
DATA: lt_all_unique TYPE STANDARD TABLE OF ty_customer,
lt_all_dup TYPE STANDARD TABLE OF ty_customer.
" 1) UNION: 두 채널에 동시에 등록된 고객은 한 번만 나타남
SELECT customer_id, customer_name, 'OFFLINE' AS channel
FROM zcustomer_a
UNION
SELECT customer_id, customer_name, 'ONLINE' AS channel
FROM zcustomer_b
INTO TABLE @lt_all_unique.
" 2) UNION ALL: 중복 제거 없이 단순 연결
SELECT customer_id, customer_name
FROM zcustomer_a
UNION ALL
SELECT customer_id, customer_name
FROM zcustomer_b
INTO TABLE @lt_all_dup.
cl_demo_output=>display( lt_all_unique ).
cl_demo_output=>display( lt_all_dup ).
UNION에서 두 번째 쿼리의 별칭(channel)은 무시되고, 첫 번째 쿼리의 컬럼명이 기준이 됩니다. 리터럴로 채널 구분자를 넣어 두 결과 행의 출처를 추적할 수 있습니다. UNION ALL은 정렬·해시 단계가 없으므로 중복이 비즈니스적으로 불가능한 경우엔 항상 UNION ALL을 선택해야 합니다.
실전 예제 2단계 — INTERSECT와 EXCEPT로 비즈니스 질문 풀기
실무에서 자주 발생하는 두 가지 질문을 집합 연산으로 풉니다. (1) "오프라인과 온라인에 모두 등록된 충성 고객은 누구인가?"는 INTERSECT, (2) "주문은 들어왔는데 아직 배송 기록이 없는 미배송 건은 어떤 것인가?"는 EXCEPT로 해결합니다.
CLASS lcl_set_demo DEFINITION.
PUBLIC SECTION.
METHODS: find_loyal_customers,
find_undelivered_orders.
ENDCLASS.
CLASS lcl_set_demo IMPLEMENTATION.
METHOD find_loyal_customers.
TYPES: BEGIN OF ty_cust_key,
customer_id TYPE c LENGTH 10,
END OF ty_cust_key.
DATA lt_loyal TYPE STANDARD TABLE OF ty_cust_key.
TRY.
SELECT customer_id
FROM zcustomer_a
WHERE active_flag = @abap_true
INTERSECT
SELECT customer_id
FROM zcustomer_b
WHERE active_flag = @abap_true
INTO TABLE @lt_loyal.
IF lt_loyal IS INITIAL.
MESSAGE 'No loyal customers found' TYPE 'I'.
ELSE.
cl_demo_output=>display( lt_loyal ).
ENDIF.
CATCH cx_sy_open_sql_db INTO DATA(lx).
MESSAGE lx->get_text( ) TYPE 'E'.
ENDTRY.
ENDMETHOD.
METHOD find_undelivered_orders.
TYPES: BEGIN OF ty_order_key,
order_id TYPE c LENGTH 10,
END OF ty_order_key.
DATA lt_pending TYPE STANDARD TABLE OF ty_order_key.
" 주문은 존재하지만 배송 기록에는 없는 order_id
SELECT order_id
FROM zsales_order
WHERE order_status = 'OPEN'
EXCEPT
SELECT order_id
FROM zdelivery_rec
INTO TABLE @lt_pending.
cl_demo_output=>display( lt_pending ).
ENDMETHOD.
ENDCLASS.
EXCEPT 예제가 특히 강력합니다. 같은 결과를 JOIN으로 작성하면 LEFT OUTER JOIN ... WHERE delivery~order_id IS NULL 패턴이 되는데, 의도를 한눈에 파악하기 어렵습니다. EXCEPT는 "주문 집합에서 배송 집합을 빼라"는 비즈니스 언어와 SQL이 1:1로 대응합니다.
실전 예제 3단계 — CDS View에서 UNION ALL로 분할 테이블 통합
운영 환경에서는 분할된 이력 테이블을 통합 뷰로 묶거나, 사용자 권한과 결합하는 패턴이 많이 사용됩니다. ABAP CDS에서 UNION ALL로 연/월 단위 분할 주문 테이블을 합치는 예제입니다.
@AbapCatalog.sqlViewName: 'ZSALES_ALL_V'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Unified Sales Orders (current + archive)'
define view Z_C_SalesOrderAll
as select from zsales_order as cur
{
key cur.order_id as OrderId,
cur.customer_id as CustomerId,
cur.order_date as OrderDate,
cur.total_amount as TotalAmount,
'CURRENT' as SourceFlag
}
union all
select from zsales_order_arc as arc
{
key arc.order_id as OrderId,
arc.customer_id as CustomerId,
arc.order_date as OrderDate,
arc.total_amount as TotalAmount,
'ARCHIVE' as SourceFlag
};
CDS에서 UNION ALL을 선택한 이유는 분명합니다. 현재 테이블과 아카이브 테이블은 키가 겹치지 않도록 운영 정책으로 보장되므로 중복 제거가 불필요하며, UNION ALL이 정렬·해시 작업을 생략하므로 일반적으로 더 빠릅니다. 이 뷰를 ABAP 프로그램에서 호출하면 분할 구조를 완전히 숨길 수 있습니다.
집합 연산 vs JOIN — 선택 기준
집합 연산과 JOIN은 목적이 다릅니다. 두 방식의 선택 기준을 정리합니다.
| 기준 | 집합 연산 (UNION/INTERSECT/EXCEPT) | JOIN |
|---|---|---|
| 결과 형태 | 행의 합치기/교집합/차집합 | 컬럼의 조합 (수평 확장) |
| 추가 컬럼 필요 | 불가능 (컬럼 구조 동일해야 함) | 양쪽 컬럼 자유롭게 조합 |
| 비즈니스 언어 일치 | "A에 있고 B에 없는 것" → EXCEPT | "A와 B를 연결해 조회" → JOIN |
| 성능 (중복 없는 경우) | UNION ALL이 빠름 | 인덱스 설계에 의존 |
미배송 주문처럼 "차집합" 개념이 명확하면 EXCEPT가 의도를 더 잘 표현합니다. 반면 주문과 고객을 연결해 고객명까지 같이 보여줘야 하면 JOIN이 필요합니다. 두 방식은 상호 대체재가 아니라 보완재입니다.
흔한 실수 / 트러블슈팅
- 컬럼 수 불일치:
SELECT a, b, cUNIONSELECT a, b는 컴파일 에러. 누락된 컬럼은CAST( '' AS CHAR( 10 ) )등 리터럴로 자리를 채워야 합니다. - 타입 불일치: 같은 위치의 컬럼이 한쪽은
NUMC, 다른 쪽은CHAR이면 변환 실패.CAST로 명시적으로 타입을 맞춰야 합니다. - ORDER BY 위치 오해: 각
SELECT뒤에ORDER BY를 붙이면 문법 오류. 전체 결과의 가장 마지막에 한 번만 작성해야 합니다. - INTERSECT/EXCEPT 릴리스 제한: ABAP 7.52 이하에서는 지원되지 않습니다. 릴리스 확인 후 사용 여부를 결정하세요.
Q&A
Q1. UNION 결과의 컬럼명이 두 번째 쿼리와 다르게 나옵니다. 왜 그런가요? 결과 컬럼명은 첫 번째 SELECT의 컬럼 이름(또는 별칭)을 따릅니다. 두 번째 쿼리에서 별칭을 다르게 줘도 무시됩니다. 원하는 이름이 있다면 첫 번째 쿼리에서 지정하세요.
Q2. INTERSECT 대신 IN 서브쿼리를 써도 되지 않나요? 단일 컬럼이라면 WHERE id IN (SELECT ...)도 가능합니다. 하지만 여러 컬럼을 동시에 매칭해야 하거나, 양쪽 결과 집합 자체를 명확히 표현하고 싶다면 INTERSECT가 가독성과 유지보수 면에서 우위입니다.
Q3. EXCEPT가 LEFT JOIN + IS NULL 패턴보다 항상 빠른가요? 그렇지 않습니다. 옵티마이저와 인덱스 상태에 따라 다릅니다. SE30이나 ST05로 실측 후 결정하길 권장합니다.
이어서 익히면 좋은 주제
집합 연산에 익숙해졌다면 다음 주제로 확장해보세요. (1) CTE(WITH 절)로 복잡한 집합 연산 조합을 단계별로 분리해 가독성을 높이는 방법, (2) CDS Table Function과 AMDP를 활용해 SAP HANA의 집합 처리 성능을 극대화하는 패턴, (3) ABAP RAP의 CDS Projection View에서 UNION 기반 뷰를 비즈니스 객체로 노출하는 방법, (4) Open SQL Hints로 옵티마이저에 힌트를 주는 기법까지 확장하면 대용량 분석성 쿼리도 ABAP에서 능숙하게 다룰 수 있습니다.
댓글 0
아직 댓글이 없습니다.