STRING_AGG가 필요한 이유 — 여러 행을 한 줄로
관계형 데이터베이스에서 자주 마주치는 상황이 있습니다. 하나의 고객이 여러 개의 주문을 갖고, 하나의 주문이 여러 개의 상품 라인을 갖습니다. 이 데이터를 화면이나 리포트에 표시할 때 "고객 A: 노트북, 마우스, 키보드" 같은 형태로 한 줄에 나열하고 싶은 경우가 많습니다. 애플리케이션 계층에서 반복문으로 문자열을 이어 붙일 수도 있지만, 데이터베이스 왕복 횟수가 늘어나고 성능 저하가 발생합니다.
SAP HANA는 STRING_AGG라는 집계 함수를 제공합니다. GROUP BY 결과에서 특정 컬럼 값을 지정한 구분자로 이어 붙여 하나의 문자열로 반환하는 함수입니다. 이 글에서는 문법 분석부터 실전 시나리오까지 단계별로 다룹니다.
이 글에서 다루는 것
- STRING_AGG 함수의 문법과 각 파라미터의 역할
- 구분자(delimiter) 지정과 ORDER BY 절 결합
- WHERE, FILTER, DISTINCT를 활용한 값 선별
- 주문·상품 목록을 한 줄로 합치는 실전 SQL 패턴
- NULL 처리, 길이 제한, 성능 최적화 팁
배경 지식과 사전 준비
이 글의 예제를 이해하려면 SQL의 SELECT, GROUP BY, JOIN 문법에 익숙해야 합니다. 집계 함수인 COUNT, SUM, AVG를 사용해 본 경험이 있으면 충분합니다. 문자열 결합 개념(CONCAT이나 || 연산자)도 알아두면 도움이 됩니다. HANA 특유의 CDS View나 Calculation View 지식은 필수가 아니지만, 마지막 섹션에서 잠깐 언급됩니다.
실행 환경과 버전 정보
예제는 다음 환경을 기준으로 작성되었습니다.
- SAP HANA Cloud (QRC 2/2024 이상) 또는 SAP HANA 2.0 SPS 06 이상
- 클라이언트: SAP HANA Database Explorer, SAP HANA Studio, DBeaver 등 표준 SQL 클라이언트
- 권한: 스키마 내 SELECT 권한, 테스트 테이블 생성을 위한 CREATE TABLE 권한
- 문자셋: UTF-8 (한글 상품명 처리를 위해 권장)
SAP HANA에서 STRING_AGG는 2.0 SPS 03 시점부터 정식 함수로 제공되며, HANA Cloud에서는 최신 문법이 계속 확장되고 있습니다. 구버전(1.0)에서는 유사 기능을 위해 CONCAT과 재귀 CTE로 구현해야 했지만, 현재는 단일 함수 호출로 충분합니다. 실습용 스키마 이름은 예제에서 SALES로 통일하겠습니다.
핵심 개념 — 집계와 문자열 결합의 만남
일반 집계 함수를 요리에 비유하면 SUM은 여러 재료를 저울에 올려 총 무게를 재는 것과 같고, AVG는 평균 무게를 계산하는 것입니다. STRING_AGG는 여러 재료의 이름을 리본으로 묶어 하나의 꼬리표로 만드는 작업에 해당합니다. 값을 수치로 요약하지 않고 문자열로 나열해 보존한다는 점이 다릅니다.
SQL 표준에서는 이 기능을 LISTAGG(Oracle, DB2)라 부르기도 하고, PostgreSQL과 SQL Server, HANA는 STRING_AGG라는 이름을 씁니다. 이름은 다르지만 개념은 같습니다. 다음 도식을 보면 이해가 쉽습니다.
-- 입력 (5행)
CUSTOMER_ID | PRODUCT_NAME
101 | Laptop
101 | Mouse
101 | Keyboard
102 | Monitor
102 | Cable
-- STRING_AGG(PRODUCT_NAME, ', ') + GROUP BY CUSTOMER_ID
-- 출력 (2행)
CUSTOMER_ID | PRODUCTS
101 | Laptop, Mouse, Keyboard
102 | Monitor, Cable
내부적으로 HANA는 그룹별로 값을 버퍼에 모은 뒤 구분자를 사이에 삽입하며 이어 붙입니다. 이때 ORDER BY를 명시하지 않으면 순서가 실행 계획에 따라 달라질 수 있으므로, 결과의 재현성이 중요하다면 정렬 절을 반드시 붙여야 합니다. 반환 타입은 기본적으로 NVARCHAR이며 최대 길이는 5000자로 알려져 있습니다(HANA Cloud 최신 버전 기준, 버전별 차이 확인 권장).
함수 문법 상세
STRING_AGG(
<expression> -- 이어 붙일 컬럼 또는 표현식
[, <delimiter>] -- 구분자 (기본값 없음, 명시 권장)
[ORDER BY <sort_key>] -- 결합 순서
)
expression에는 컬럼명뿐 아니라 UPPER(name), CAST(price AS NVARCHAR) 같은 표현식도 넣을 수 있습니다. delimiter를 생략하면 값들이 붙어서 출력되므로 실무에서는 대부분 ', '이나 ' | ' 같은 구분자를 지정합니다.
1단계 — 기본 예제: 카테고리별 상품 목록
실습을 위해 판매 상품 테이블을 만들고 데이터를 넣습니다.
CREATE COLUMN TABLE SALES.PRODUCT_CATALOG (
PRODUCT_ID NVARCHAR(10) PRIMARY KEY,
CATEGORY NVARCHAR(30),
PRODUCT_NAME NVARCHAR(100),
UNIT_PRICE DECIMAL(10,2)
);
INSERT INTO SALES.PRODUCT_CATALOG VALUES ('P001', 'Electronics', 'Wireless Headphone', 89.90);
INSERT INTO SALES.PRODUCT_CATALOG VALUES ('P002', 'Electronics', 'Smart Watch', 199.00);
INSERT INTO SALES.PRODUCT_CATALOG VALUES ('P003', 'Electronics', 'Bluetooth Speaker', 65.50);
INSERT INTO SALES.PRODUCT_CATALOG VALUES ('P004', 'Office', 'Ergonomic Chair', 320.00);
INSERT INTO SALES.PRODUCT_CATALOG VALUES ('P005', 'Office', 'Standing Desk', 450.00);
INSERT INTO SALES.PRODUCT_CATALOG VALUES ('P006', 'Kitchen', 'Espresso Machine', 210.00);
카테고리별로 상품명을 한 줄에 모으는 가장 단순한 쿼리는 다음과 같습니다.
SELECT
CATEGORY,
STRING_AGG(PRODUCT_NAME, ', ') AS PRODUCT_LIST
FROM SALES.PRODUCT_CATALOG
GROUP BY CATEGORY;
결과는 카테고리마다 상품명이 쉼표로 연결된 형태로 나옵니다. 이때 상품이 알파벳순으로 정렬되어야 한다면 ORDER BY를 함수 안에 추가합니다.
SELECT
CATEGORY,
STRING_AGG(PRODUCT_NAME, ', ' ORDER BY PRODUCT_NAME) AS PRODUCT_LIST
FROM SALES.PRODUCT_CATALOG
GROUP BY CATEGORY;
2단계 — 실무 시나리오: 주문 라인 통합과 값 가공
실제 리포트에서는 단순 이름만 필요하지 않고 "상품명(수량)" 같은 형태로 가공된 문자열을 요구하는 경우가 많습니다. 주문 헤더와 라인 테이블을 만들고 조인해서 처리해 봅니다.
CREATE COLUMN TABLE SALES.ORDER_HEADER (
ORDER_ID NVARCHAR(10) PRIMARY KEY,
CUSTOMER NVARCHAR(50),
ORDER_DATE DATE
);
CREATE COLUMN TABLE SALES.ORDER_LINE (
ORDER_ID NVARCHAR(10),
LINE_NO INTEGER,
PRODUCT_ID NVARCHAR(10),
QUANTITY INTEGER,
PRIMARY KEY (ORDER_ID, LINE_NO)
);
INSERT INTO SALES.ORDER_HEADER VALUES ('SO2001', 'Acme Corp', '2026-07-01');
INSERT INTO SALES.ORDER_HEADER VALUES ('SO2002', 'Globex Ltd', '2026-07-02');
INSERT INTO SALES.ORDER_LINE VALUES ('SO2001', 10, 'P001', 3);
INSERT INTO SALES.ORDER_LINE VALUES ('SO2001', 20, 'P002', 1);
INSERT INTO SALES.ORDER_LINE VALUES ('SO2001', 30, 'P006', 2);
INSERT INTO SALES.ORDER_LINE VALUES ('SO2002', 10, 'P004', 5);
INSERT INTO SALES.ORDER_LINE VALUES ('SO2002', 20, 'P005', 2);
각 주문의 상품 구성을 "상품명 x 수량" 형식으로 한 줄에 나열하고, 라인 번호 순서를 유지합니다.
SELECT
h.ORDER_ID,
h.CUSTOMER,
STRING_AGG(
p.PRODUCT_NAME || ' x ' || TO_NVARCHAR(l.QUANTITY),
' | '
ORDER BY l.LINE_NO
) AS ORDER_SUMMARY
FROM SALES.ORDER_HEADER h
INNER JOIN SALES.ORDER_LINE l ON h.ORDER_ID = l.ORDER_ID
INNER JOIN SALES.PRODUCT_CATALOG p ON l.PRODUCT_ID = p.PRODUCT_ID
GROUP BY h.ORDER_ID, h.CUSTOMER
ORDER BY h.ORDER_ID;
실행 결과는 다음과 같은 모양이 됩니다.
ORDER_ID | CUSTOMER | ORDER_SUMMARY
SO2001 | Acme Corp | Wireless Headphone x 3 | Smart Watch x 1 | Espresso Machine x 2
SO2002 | Globex Ltd | Ergonomic Chair x 5 | Standing Desk x 2
여기서 눈여겨볼 점은 두 가지입니다. 첫째, || 연산자로 표현식을 만들어 함수에 넣었다는 점. 둘째, ORDER BY LINE_NO로 라인 순서를 보장했다는 점입니다. 정렬을 생략하면 병렬 실행 시 순서가 뒤바뀔 수 있어 리포트 가독성이 떨어집니다.
3단계 — 프로덕션: 조건부 집계와 중복 제거
실제 서비스에서는 특정 조건을 만족하는 값만 문자열로 모으거나 중복을 제거해야 하는 상황이 자주 발생합니다. 예를 들어 "고가 상품(100 이상)만 나열", "동일 상품이 여러 라인에 있어도 한 번만 표시" 같은 요구입니다.
-- 고가 상품만 나열 (WHERE로 사전 필터링)
SELECT
h.ORDER_ID,
STRING_AGG(p.PRODUCT_NAME, ', ' ORDER BY p.PRODUCT_NAME) AS PREMIUM_ITEMS
FROM SALES.ORDER_HEADER h
INNER JOIN SALES.ORDER_LINE l ON h.ORDER_ID = l.ORDER_ID
INNER JOIN SALES.PRODUCT_CATALOG p ON l.PRODUCT_ID = p.PRODUCT_ID
WHERE p.UNIT_PRICE >= 100
GROUP BY h.ORDER_ID;
중복 제거는 DISTINCT를 함께 사용합니다. 동일 주문에 같은 상품이 여러 번 등장해도 한 번만 문자열에 포함시킵니다.
SELECT
h.CUSTOMER,
STRING_AGG(DISTINCT p.CATEGORY, ', ' ORDER BY p.CATEGORY) AS CATEGORIES_ORDERED
FROM SALES.ORDER_HEADER h
INNER JOIN SALES.ORDER_LINE l ON h.ORDER_ID = l.ORDER_ID
INNER JOIN SALES.PRODUCT_CATALOG p ON l.PRODUCT_ID = p.PRODUCT_ID
GROUP BY h.CUSTOMER;
더 정교한 조건부 집계가 필요하면 CASE 표현식을 안에 넣습니다. 아래는 카테고리별로 다른 태그를 붙이는 예입니다.
SELECT
h.ORDER_ID,
STRING_AGG(
CASE
WHEN p.UNIT_PRICE >= 300 THEN '[HIGH] ' || p.PRODUCT_NAME
WHEN p.UNIT_PRICE >= 100 THEN '[MID] ' || p.PRODUCT_NAME
ELSE '[LOW] ' || p.PRODUCT_NAME
END,
' / '
ORDER BY p.UNIT_PRICE DESC
) AS TAGGED_ITEMS
FROM SALES.ORDER_HEADER h
INNER JOIN SALES.ORDER_LINE l ON h.ORDER_ID = l.ORDER_ID
INNER JOIN SALES.PRODUCT_CATALOG p ON l.PRODUCT_ID = p.PRODUCT_ID
GROUP BY h.ORDER_ID;
흔한 실수와 트러블슈팅
FAQ 1. 결과 문자열이 잘려서 나옵니다.
STRING_AGG의 반환 타입은 NVARCHAR이며 길이 제한이 있습니다(HANA Cloud 최신 기준 5000자 수준, 버전별 확인 권장). 그룹당 값이 많고 개별 문자열도 길다면 잘림이 발생할 수 있습니다. 방법은 두 가지입니다. 첫째, 요약 문자열을 만들 때 필요한 필드만 최소한으로 넣습니다. 둘째, 정말 긴 텍스트가 필요하면 애플리케이션 레이어에서 CLOB 형태로 조합하는 방식을 고려합니다.
FAQ 2. 정렬이 매번 다르게 나옵니다.
ORDER BY를 함수 안에 명시하지 않으면 HANA 옵티마이저의 실행 계획에 따라 순서가 달라질 수 있습니다. 리포트 재현성이 필요하다면 반드시 정렬 키를 지정하세요. 라인 번호, 생성 시각, 값 자체 등 명확한 기준이 있어야 합니다.
FAQ 3. NULL이 이상하게 처리됩니다.
집계 대상 값이 NULL이면 STRING_AGG는 해당 값을 무시합니다. 즉 NULL은 빈 문자열로 취급되어 결과에 나타나지 않습니다. 만약 NULL도 "N/A" 같은 표시로 보여주고 싶다면 COALESCE(column, 'N/A')로 감싸 미리 대체하세요. 반대로 모든 값이 NULL인 그룹은 결과 자체가 NULL이 됩니다.
FAQ 4. 구분자에 개행문자를 넣고 싶어요.
CHAR(10)(라인피드) 또는 CHAR(13) || CHAR(10)(CRLF)를 구분자로 사용합니다. 예를 들어 STRING_AGG(name, CHAR(10))처럼 씁니다. 결과를 표시하는 툴이 개행을 인식하는지에 따라 화면상 표현이 달라집니다.
성능과 대안 패턴
STRING_AGG는 그룹 내 데이터를 메모리에 모아 정렬·결합하기 때문에 그룹당 행 수가 매우 많으면 메모리 사용량이 늘어납니다. 수십만 건 이상을 하나의 문자열로 합치는 시나리오는 지양하고, 필요한 경우 사전에 WHERE로 대상 행을 좁히는 것이 좋습니다. 또한 실행 계획에서 Column Search 노드 아래 Aggregation 단계가 어떻게 분산되는지 확인해 인덱스나 파티셔닝을 함께 검토하는 접근이 일반적으로 권장됩니다.
이후 활용 확장
STRING_AGG를 익혔다면 다음 주제로 확장할 수 있습니다.
- CDS View에서의 활용: ABAP CDS에서는 유사 기능을 위해
STRING_AGG가 지원되지만 문법 제약이 있으니 버전별 문서를 확인해야 합니다. - JSON_OBJECTAGG / JSON_ARRAYAGG: 단순 문자열이 아니라 구조화된 JSON을 그룹별로 조립할 때 유용합니다.
- Calculation View의 Aggregation 노드: BI 리포트에서 재사용하려면 뷰로 감싸는 방법을 익혀두세요.
- Window 함수와 조합: 그룹별 누적 문자열이 필요하면
OVER (PARTITION BY ...)를 결합해 활용할 수 있습니다.
댓글 0
아직 댓글이 없습니다.