SQL 기초 — SELECT 부터 서브쿼리까지

SELECT·WHERE·JOIN·집계·서브쿼리·CASE 를 익힙니다. SQL 이 처음이라면 여기서 시작하세요. 연습 데이터는 실제 SAP 테이블입니다.

1. SELECT 와 WHERE — 필요한 열과 행만 고르기

SELECT 는 어떤 열을, FROM 은 어느 테이블에서, WHERE 는 어떤 행만 가져올지 정합니다. SAP 데이터셋은 자재(MARA), 고객(KNA1), 공급업체(LFA1) 같은 마스터부터 읽습니다.

SELECT MATNR, MTART, MEINS
FROM MARA
WHERE MTART IN ('FERT', 'HAUN')
  AND LVORM <> 'X';
  • SAP 날짜(DATS)는 'YYYYMMDD' 형식의 문자열입니다. 기간은 ERDAT BETWEEN '20250101' AND '20251231' 처럼 문자열로 비교합니다.
  • 번호 필드는 앞자리 0을 포함해 저장됩니다(KUNNR = '0000100001'). 화면처럼 0을 지우려면 LTRIM(KUNNR, '0').
  • 문자 필드의 빈 값 '' 과 NULL 은 다릅니다. 미설정 값은 빈 문자열인 경우가 많으니 IS NULL 과 = '' 를 구분하세요.
  • 삭제 표시(LVORM = 'X'), 오더 차단(AUFSD) 같은 플래그는 <> 'X' 로 제외합니다.

문제

  1. 판매 대상 자재 목록 (완제품·상품) (초급)
  2. 영문 내역에 PUMP 또는 펌프가 들어간 자재 (초급)
  3. 2020년 이후 등록된 국내 고객 (초급)
  4. 기본 단위가 EA 가 아닌 자재 (초급)
  5. 오더 차단 또는 삭제 표시가 있는 고객 (초급)
  6. 해외·관계사 계정 그룹 공급업체 (초급)
  7. 중앙 차단·삭제 표시가 없는 외부 공급업체 (중급)

2. 정렬과 상위 N개

ORDER BY 로 결과 순서를 정합니다. 기본은 오름차순(ASC), 큰 값부터는 DESC 입니다.

SELECT MATNR, STPRS
FROM MBEW
WHERE BWKEY = '1000'
ORDER BY STPRS DESC, MATNR
LIMIT 5;
  • 정렬 기준을 여러 개 주면 앞의 기준이 같은 행끼리 다음 기준으로 정렬합니다. 금액·수량처럼 동점이 흔한 열에는 행 번호 같은 두 번째 기준을 붙여야 결과가 흔들리지 않습니다.
  • SAP HANA 에서 상위 N개는 LIMIT n 또는 SELECT TOP n … 으로 가져옵니다. 페이지 이동은 LIMIT n OFFSET m.
  • NULL 의 정렬 위치는 DB 기본값에 따라 다릅니다. 필요하면 NULLS FIRST / NULLS LAST 를 명시하세요.

문제

  1. 화성 공장 재고 금액 상위 5개 자재 (초급)
  2. 2025년 11월 원화 판매 오더를 사무소·금액 순으로 나열 (초급)
  3. 재고 금액 상위 자재 목록의 두 번째 페이지 (초급)
  4. 미완료 판매 문서를 처리 상태와 희망 납품일 순으로 점검 (초급)
  5. 상반기 마지막 원화 구매 오더 10건을 공급업체 순으로 (중급)
  6. 2025년 국내 원화 판매 오더 금액 11~20위 (중급)

3. 문자열·날짜 함수

SAP 의 날짜(DATS)와 번호 필드는 문자열이라 함수로 다듬는 일이 많습니다.

목적HANA 함수
일부 자르기LEFT(s, n), SUBSTRING(s, 시작, 길이)
앞자리 0 지우기LTRIM(KUNNR, '0')
자리 채우기LPAD(s, n, '0')
문자 → 날짜TO_DATE(s, 'YYYYMMDD')
날짜 계산ADD_DAYS(d, n), ADD_MONTHS(d, n)
날짜 차이DAYS_BETWEEN(시작, 끝)
날짜 → 문자TO_VARCHAR(d, 'YYYY-MM')
SELECT VBELN,
       TO_DATE(ERDAT, 'YYYYMMDD') AS ORDER_DATE,
       ADD_DAYS(TO_DATE(ERDAT, 'YYYYMMDD'), 30) AS DUE_DATE
FROM VBAK
WHERE ERDAT >= '20250101';
  • DAYS_BETWEEN(a, b) 는 b − a 입니다. 지급 기한이 지난 일수는 DAYS_BETWEEN(기한, 기준일) 로 양수가 됩니다.
  • 기간 조건은 날짜로 바꾼 뒤 비교하면 월·연 경계를 다루기 쉽습니다.

문제

  1. 미국 고객 목록을 외부 형식으로 내보내기 (초급)
  2. 미국 판매 법인의 2025년 하반기 표준 오더 (초급)
  3. 2025년 12월 납품 예정 일정 라인 (초급)
  4. 지급 기한이 내년으로 넘어가는 구매 오더 (중급)
  5. 처리가 끝나지 않은 표준 오더의 경과 일수 (중급)
  6. 12월 공급업체 송장의 전기 지연과 지급 기한 (중급)
  7. 월말에 접수돼 희망 납품일이 다음 달로 넘어간 오더 (고급)

4. 집계 함수 — COUNT · SUM · AVG · MIN · MAX

집계 함수는 여러 행을 한 값으로 줄입니다. GROUP BY 가 없으면 전체가 한 그룹입니다.

SELECT COUNT(*) AS DOC_COUNT,
       COUNT(DISTINCT MATNR) AS MATERIALS,
       SUM(NETWR) AS TOTAL_AMOUNT,
       AVG(NETWR) AS AVG_AMOUNT
FROM EKPO;
  • COUNT(*) 는 모든 행을, COUNT(컬럼) 은 그 컬럼이 NULL 이 아닌 행만 셉니다.
  • COUNT(DISTINCT 컬럼) 은 서로 다른 값의 개수입니다. 중복 없는 목록 자체는 SELECT DISTINCT.
  • SUM·AVG 도 NULL 은 빼고 계산합니다. 행이 하나도 없으면 SUM 은 NULL, COUNT 는 0 입니다.
  • 결과 열에는 AS 로 이름을 붙이세요. 문제의 결과 명세가 그 이름을 기준으로 채점됩니다.

문제

  1. 2025년 화성 공장 판매 문서 품목 요약 (초급)
  2. 2025년 화성 공장 입고 금액과 기간 (초급)
  3. 화성 공장 EA 단위 구매 품목의 평균 발주 수량 (초급)
  4. 국내 공급업체 마스터 현황 요약 (초급)
  5. 평택 물류센터 2년간 고객 출하 업무량 세기 (중급)
  6. Dallas DC 2025년 수주의 품목당·문서당 평균 금액 (중급)

5. GROUP BY 와 HAVING — 묶어서 집계하기

GROUP BY 는 같은 값을 가진 행을 묶고, 묶음마다 집계합니다.

SELECT KUNNR, COUNT(*) AS ORDER_COUNT, SUM(NETWR) AS SALES
FROM VBAK
WHERE AUART = 'OR'
GROUP BY KUNNR
HAVING SUM(NETWR) >= 1000000;
  • SELECT 에는 묶는 기준 컬럼과 집계 함수만 올 수 있습니다. GROUP BY 에 없는 컬럼을 그냥 쓰면 오류입니다.
  • WHERE 는 묶기 전에 행을 거르고, HAVING 은 묶은 뒤 그룹을 거릅니다. 집계 결과로 거를 때는 HAVING 입니다.
  • 묶는 기준이 여러 개면 그 조합마다 한 행이 나옵니다. 기간·플랜트·통화처럼 업무 단위를 먼저 정하세요.

문제

  1. 구매 오더가 10건 이상인 공급업체 (초급)
  2. 2년간 자주 쓰인 이동 유형과 입출고 방향 (초급)
  3. 구매 그룹·통화별 발주 현황 (초급)
  4. 2025년에 반복 주문된 제품 (플랜트·자재·통화별) (중급)
  5. 판매 사무소별 2025년 원화 오더가 몰린 달 (중급)
  6. 일부 품목만 거부된 2025년 판매 문서 (중급)
  7. 반품이 있었던 판매처의 오더 대비 반품 비율 (고급)

6. INNER JOIN — 테이블 잇기

JOIN 은 두 테이블을 공통 키로 잇습니다. SAP 테이블 대부분은 헤더-품목, 마스터-트랜잭션 관계라 JOIN 이 가장 중요한 문법입니다.

SELECT K.EBELN, L.NAME1, P.EBELP, P.MATNR, P.NETWR
FROM EKKO K
JOIN EKPO P ON P.EBELN = K.EBELN
JOIN LFA1 L ON L.LIFNR = K.LIFNR
WHERE K.BUKRS = '1000';
  • INNER JOIN(그냥 JOIN)은 양쪽에 짝이 있는 행만 남깁니다.
  • 테이블 별칭(K, P, L)을 붙이면 컬럼이 어느 테이블 것인지 분명해집니다.
  • 키 전체로 이어야 합니다. 헤더-품목은 문서 번호, 품목끼리는 문서 번호 + 품목 번호처럼 조합 키가 흔합니다.
  • 헤더 금액(VBAK.NETWR)을 품목과 조인한 뒤 SUM 하면 품목 수만큼 여러 번 더해집니다. 금액은 한쪽 기준으로만 집계하세요.

문제

  1. 반품 오더와 고객 이름 (초급)
  2. 미국 법인 구매 오더 품목 목록 (초급)
  3. 해외 공급업체 구매 오더 목록 (초급)
  4. 납품이 끝나지 않은 수주 품목과 자재 한국어 이름 (중급)
  5. K020 사무소 4분기 수주 품목과 고객 이름 (중급)
  6. 구매 그룹 120의 하반기 발주 품목과 영문 자재명 (중급)
  7. 미국 법인 하반기 수주 품목: 고객 이름과 영문 자재명 (고급)

7. LEFT JOIN 과 NULL

LEFT JOIN 은 왼쪽 테이블의 행을 모두 남기고, 오른쪽에 짝이 없으면 NULL 로 채웁니다. "없는 것"까지 보고서에 넣을 때 씁니다.

SELECT K.KUNNR, K.NAME1, COUNT(V.VBELN) AS ORDER_COUNT
FROM KNA1 K
LEFT JOIN VBAK V
       ON V.KUNNR = K.KUNNR
      AND V.AUART = 'OR'
WHERE K.LAND1 = 'KR'
GROUP BY K.KUNNR, K.NAME1;
  • COUNT(V.VBELN) 은 짝이 없는 고객을 0 으로 셉니다. COUNT(*) 를 쓰면 1 이 되니 주의하세요.
  • 짝이 없는 행만 찾을 때는 LEFT JOIN … WHERE 오른쪽키 IS NULL 을 씁니다.
  • 필터 위치가 중요합니다. 오른쪽 테이블 조건을 WHERE 에 쓰면 NULL 행이 걸러져 INNER JOIN 처럼 됩니다. 조건은 ON 절에 두세요.
  • NULL 을 다른 값으로 바꿀 때는 COALESCE(a, b) 또는 HANA 의 IFNULL(a, b).

문제

  1. 완제품별 화성 공장 가용 재고 (재고 행이 없는 자재 포함) (초급)
  2. 반품 이력이 전혀 없는 고객 (초급)
  3. 2025년 주문이 없는 한국 고객까지 포함한 고객별 수주 현황 (중급)
  4. 납품 미완료 구매 품목의 입고 이력 현황 (중급)
  5. 화성 공장 원자재별 구매 품목 수와 발주 금액 (중급)
  6. 화성 공장 원자재·상품의 현재 재고와 2025년 순입고 수량 비교 (고급)

8. 서브쿼리 — 쿼리 안의 쿼리

다른 쿼리의 결과를 조건이나 값, 또는 임시 테이블처럼 씁니다.

SELECT KUNNR, NETWR
FROM VBAK
WHERE NETWR > (SELECT AVG(NETWR) FROM VBAK WHERE AUART = 'OR');
  • 값 하나를 돌려주면 비교 연산자와, 여러 값이면 IN (SELECT …), 존재 여부만 보면 EXISTS (SELECT …) 와 씁니다.
  • FROM 절에 쓰는 서브쿼리는 파생 테이블입니다. 먼저 집계한 결과를 다시 필터·조인할 수 있습니다.
  • 바깥 행을 참조하는 상관 서브쿼리는 바깥 행마다 다시 계산됩니다.
  • NOT IN (서브쿼리) 결과에 NULL 이 섞이면 어떤 행도 선택되지 않습니다. "없음"을 찾을 때는 NOT EXISTS 가 안전합니다.

문제

  1. 유로 발주 이력이 있는 공급업체 (초급)
  2. 구매 오더가 많은 공급업체 상위 5곳 (초급)
  3. 거부된 품목이 있었던 고객 찾기 (중급)
  4. 2025년 원화 오더 고객별 합계 3위 이상 고객 (중급)
  5. 반품 오더를 낸 고객의 표준 오더 건수 (중급)
  6. 오더별로 금액이 가장 큰 품목 (미국 판매 조직) (중급)
  7. 평가 영역 평균보다 재고 금액이 큰 자재 (고급)

9. CASE 와 조건 분기

CASE 는 조건에 따라 다른 값을 만듭니다. 구간 분류와 조건부 집계에 자주 씁니다.

SELECT KUNNR,
       SUM(CASE WHEN AUART = 'OR' THEN NETWR ELSE 0 END) AS ORDER_AMOUNT,
       SUM(CASE WHEN AUART = 'RE' THEN NETWR ELSE 0 END) AS RETURN_AMOUNT,
       CASE WHEN SUM(NETWR) >= 100000000 THEN '대형'
            WHEN SUM(NETWR) >= 10000000  THEN '중형'
            ELSE '소형' END AS CUSTOMER_BAND
FROM VBAK
GROUP BY KUNNR;
  • 조건은 위에서부터 검사하고 처음 맞는 것을 씁니다. 아무것도 안 맞고 ELSE 도 없으면 NULL 입니다.
  • SUM(CASE …) 로 조건별 합계를 한 행에 나란히 놓는 것이 피벗의 기본입니다.
  • HANA 의 MAP(값, 'A', '가', 'B', '나', '기타') 는 단순 값 치환을 짧게 씁니다.
  • NULL 비교는 = NULL 이 아니라 IS NULL 입니다. 기본값은 COALESCE/IFNULL 로 채웁니다.

문제

  1. 납품 상태별 판매 문서 건수 (초급)
  2. 구매 품목의 오더 단위별 품목 수와 수량 (초급)
  3. 판매 사무소별 국내·해외 고객 오더와 반품 건수 (중급)
  4. 원화 오더 품목의 금액 구간별 건수와 합계 (중급)
  5. 플랜트별 구매 품목 납품 완료 현황 (중급)
  6. 고객 미결 항목의 만기 경과 구간별 현황 (중급)
  7. 고객 국가·통화별 판매 품목 거부 사유 현황 (고급)