정보처리기사 실기 SQL JOIN과 NULL을 예제 표로 이해하기

2026. 10. 4. 18:53ㆍWORK/커리어

저녁 도서관 창가에서 책장을 넘기며 차분히 공부하는 사람

저녁에 공부할 책을 펼치고 두 개의 표를 나란히 본다. 회원은 세 명인데 조회 결과는 네 줄이다. JOIN을 잘못 쓴 것인지, NULL이 보이는 행을 지워야 하는지 잠시 망설이게 된다.

SQL JOIN의 결과는 연결 조건을 만족하는 행의 조합부터 만들고, LEFT JOIN이면 연결되지 않은 왼쪽 행도 남겨 읽는다. NULL은 0이나 ‘없음’이라는 문자열로 바꾸지 않고, 무엇이 기록되지 않았는지 구분한다. 이 두 기준을 잡으면 결과 표를 한 줄씩 설명할 수 있다.

아래 회원·주문 표는 JOIN과 NULL의 결과를 한 줄씩 따라가 보기 위해 직접 만든 예제다. 결과는 SQLite에서 실행해 확인했으니, 같은 표를 만들어 직접 실행해 보면서 읽어도 좋다.

1. 표를 연결하기 전에 한 행의 의미부터 정한다

members의 한 행은 회원 한 명이다. member_id는 회원을 구분하는 기본키다. orders의 한 행은 주문 한 건이며 order_id는 NULL이 될 수 없는 기본키다. member_id는 그 주문을 한 회원의 번호다.

members — 회원 표

member_id name
1 민지
2 준호
3 서연

orders — 주문 표

order_id member_id amount
101 1 12000
102 1 NULL
103 2 8000

민지는 주문이 두 건이고 준호는 한 건이다. 서연에게 연결되는 주문은 없다. 주문 102는 주문 자체가 존재하지만 금액이 기록되지 않았다. 이 차이를 먼저 읽어 두면 ‘주문 없음’과 ‘금액 미기록’을 혼동하지 않는다.

오늘의 작은 시작은 두 표 옆에 ‘회원 한 명’, ‘주문 한 건’이라고 적는 것이다. 같은 숫자가 들어 있어도 member_id와 order_id는 서로 다른 대상을 가리킨다.

2. INNER JOIN은 맞는 조합을 모두 남긴다

회원 번호가 같은 회원과 주문을 연결한다. m과 o는 표에 붙인 별칭이다. m.member_id는 회원 표의 번호이고 o.member_id는 주문 표에 기록된 회원 번호다.

SELECT m.member_id, m.name, o.order_id, o.amount
FROM members m
INNER JOIN orders o ON m.member_id = o.member_id
ORDER BY m.member_id, o.order_id;
member_id name order_id amount
1 민지 101 12000
1 민지 102 NULL
2 준호 103 8000

민지의 회원 행은 주문 101과도, 주문 102와도 조건이 맞는다. 그래서 두 줄이 생긴다. 준호는 주문 103과 연결되어 한 줄이 생긴다. 서연은 연결 조건을 만족하는 주문이 없으므로 결과에 나오지 않는다.

amount가 NULL인 주문 102도 남는다. 이 조회의 연결 조건은 금액이 아니라 회원 번호의 일치다. INNER JOIN이 NULL이 들어 있는 모든 행을 제거하는 것은 아니다. 어떤 열을 조건에 사용했는지 확인해야 한다.

3. LEFT JOIN은 연결되지 않은 왼쪽 행도 남긴다

이번에는 주문이 없는 회원까지 보고 싶다. 왼쪽에 놓인 members를 기준으로 LEFT JOIN을 사용한다.

SELECT m.member_id, m.name, o.order_id, o.amount
FROM members m
LEFT JOIN orders o ON m.member_id = o.member_id
ORDER BY m.member_id, o.order_id;
member_id name order_id amount
1 민지 101 12000
1 민지 102 NULL
2 준호 103 8000
3 서연 NULL NULL

앞의 세 조합은 유지된다. 서연은 맞는 주문이 없지만 왼쪽 회원 행은 남고, 오른쪽 표의 열인 order_id와 amount가 NULL로 채워진다. 결과는 네 줄이다.

LEFT JOIN은 ‘왼쪽 표의 행 수와 항상 같은 결과’를 뜻하지 않는다. 왼쪽 한 행에 오른쪽 두 행이 맞으면 두 조합이 만들어진다. 이번에는 회원 세 명에 대해 민지 두 줄, 준호 한 줄, 서연 한 줄이 남는다. 표 이름의 실제 위치도 중요하다. orders를 왼쪽에 놓으면 보존하는 대상이 바뀐다.

4. NULL은 값을 비교하기보다 상태를 확인한다

NULL은 값이 알려지지 않았거나 기록되지 않은 상태를 나타낸다. NULL이 들어 있는 금액을 0원이라고 읽거나, 문자열 'NULL'과 같다고 읽지 않는다.

SELECT order_id, amount
FROM orders
WHERE amount IS NULL
ORDER BY order_id;

이 결과는 주문 102의 한 줄이다. 반면 같은 표에서 WHERE amount = NULL을 사용하면 이 예제에서는 한 줄도 남지 않는다. 일반적인 등호 비교로는 NULL 여부를 판별하지 못하기 때문이다. NULL 여부에는 IS NULL, 값이 기록되어 있는지에는 IS NOT NULL을 사용한다.

WHERE는 조건이 참인 행만 남긴다. NULL과의 일반 비교는 ‘알 수 없음’이 될 수 있으며, 이는 WHERE에서 참으로 취급되지 않는다. 따라서 amount >= 10000 조건에 주문 102를 넣을 수 없다. ‘금액이 작다’고 확인한 것이 아니라 기준을 만족한다고 판단할 금액이 없는 것이다.

5. 같은 조건도 ON과 WHERE에 놓으면 결과가 달라진다

‘회원은 모두 보여 주되 1만 원 이상인 주문만 연결한다’와 ‘연결 결과에서 1만 원 이상인 주문 행만 남긴다’는 다른 요청이다. 먼저 금액 조건을 ON에 넣는다.

SELECT m.member_id, m.name, o.order_id, o.amount
FROM members m
LEFT JOIN orders o
  ON m.member_id = o.member_id AND o.amount >= 10000
ORDER BY m.member_id, o.order_id;
member_id name order_id amount
1 민지 101 12000
2 준호 NULL NULL
3 서연 NULL NULL

민지에게는 조건에 맞는 주문 101이 연결된다. 주문 102는 금액 조건을 만족한다고 판단할 수 없으므로 연결되지 않는다. 준호의 주문 103은 8000원이므로 연결되지 않는다. 그래도 LEFT JOIN이므로 준호와 서연의 회원 행은 남는다. 준호의 NULL은 ‘모든 주문이 없음’이 아니라 ‘이번 연결 조건에 맞는 주문이 없음’이다.

금액 조건을 WHERE로 옮기면 다음과 같다.

SELECT m.member_id, m.name, o.order_id, o.amount
FROM members m
LEFT JOIN orders o ON m.member_id = o.member_id
WHERE o.amount >= 10000
ORDER BY m.member_id, o.order_id;

먼저 네 줄의 LEFT JOIN 결과를 생각한 다음 금액 조건을 적용한다. 남는 것은 민지·주문 101·12000원의 한 줄이다. 준호의 8000원은 기준 미달이고, 주문 102와 서연 행의 NULL은 조건이 참이 아니므로 제외된다.

손으로 풀 때는 ON으로 맞는 조합을 만들고, LEFT JOIN의 미연결 행을 보충한 뒤, WHERE로 걸러 보는 순서를 쓴다. 이는 이번 조회의 논리적 해석 방법이며 DBMS의 물리적 실행 순서를 고정한다는 뜻은 아니다.

6. 주문이 없는 사람은 NULL이 될 수 없는 주문 번호로 찾는다

회원 번호만으로 LEFT JOIN한 뒤 오른쪽 기본키인 order_id가 NULL인 행을 찾으면, 연결되는 주문이 없는 회원을 구할 수 있다.

SELECT m.member_id, m.name
FROM members m
LEFT JOIN orders o ON m.member_id = o.member_id
WHERE o.order_id IS NULL
ORDER BY m.member_id;

결과는 member_id 3, 서연 한 명이다. order_id는 실제 주문에 반드시 존재하는 기본키이므로 이번 연결에서 NULL이면 맞는 주문 행이 없었다고 판단할 수 있다.

조건을 o.amount IS NULL로 바꾸면 민지의 주문 102와 서연의 미연결 행이 모두 남는다. 금액 열은 실제 주문에도 NULL이 들어갈 수 있기 때문이다. ‘주문 없음’을 찾는 문제에서 ‘금액 미기록’을 함께 넣으면 요구와 달라진다.

앞 절처럼 ON에 금액 조건을 추가했다면 order_id IS NULL의 의미도 달라진다. 그때는 주문 전체의 부재가 아니라 금액 조건에 맞는 주문의 부재를 찾는다. NULL을 판별하기 전에 JOIN 조건을 함께 읽어야 한다.

7. COUNT는 무엇을 세는지 열 이름까지 확인한다

결과 행 수와 주문 수를 같은 숫자라고 생각하면 외부 조인 집계에서 헷갈리기 쉽다. 이번 예제에서 COUNT(*)는 결과 행을 세고, COUNT(o.order_id)는 NULL이 아닌 주문 번호를 센다. COUNT(o.amount)는 금액이 기록된 행만 센다.

SELECT m.member_id,
       COUNT(*) AS row_count,
       COUNT(o.order_id) AS order_count,
       COUNT(o.amount) AS known_amount_count
FROM members m
LEFT JOIN orders o ON m.member_id = o.member_id
GROUP BY m.member_id
ORDER BY m.member_id;
member_id row_count order_count known_amount_count
1 2 2 1
2 1 1 1
3 1 0 0

서연은 LEFT JOIN 결과에 한 줄 남으므로 row_count가 1이다. 하지만 실제 주문 번호와 금액은 NULL이라 나머지 둘은 0이다. 민지는 주문 두 건 중 한 건의 금액만 기록되어 마지막 값이 1이다.

이 예제에서 order_count가 주문 수와 같은 것은 다른 표를 추가해 주문 행이 더 늘어나지 않았기 때문이다. 이후 주문 상세 같은 표를 다시 연결하면 같은 주문 번호가 여러 줄에 반복될 수 있다. 집계할 때도 한 행이 무엇을 뜻하는지 다시 확인한다.

8. 짧은 연습은 먼저 행의 존재와 값의 기록을 나눈다

같은 두 표로 다음 조회의 결과를 직접 적어 본다. 정답을 보기 전에 네 줄의 LEFT JOIN 결과 중 어느 행이 조건을 만족하는지 표시한다.

SELECT m.member_id, m.name, o.order_id, o.amount
FROM members m
LEFT JOIN orders o ON m.member_id = o.member_id
WHERE o.amount IS NULL
ORDER BY m.member_id, o.order_id;
member_id name order_id amount
1 민지 102 NULL
3 서연 NULL NULL

정답은 두 줄이다. 민지의 주문 102는 실제 주문이 있지만 금액이 NULL이다. 서연은 주문이 연결되지 않아 오른쪽 열이 모두 NULL이다. 같은 amount의 NULL이라도 만들어진 이유는 다르다.

서연만 적었다면 금액 미기록 주문을 놓친 것이고, 민지의 주문 101까지 적었다면 IS NULL 조건을 다시 확인한다. 이름만 보고 두 사람을 고르는 데서 멈추지 말고, order_id까지 써야 어떤 행이 남았는지 설명할 수 있다.

결과의 순서는 ORDER BY로 지정했다. SQL 결과가 원본 표의 표시 순서나 입력 순서를 자동으로 유지한다고 단정하지 않는다. 이번 데이터에서는 회원 번호와 주문 번호로 필요한 순서를 구분할 수 있다.

9. 오늘은 답을 외우기보다 남은 행의 이유를 설명한다

복습할 때는 다음 네 가지를 한 번만 확인한다.

  • 왼쪽과 오른쪽 표의 한 행이 각각 무엇을 뜻하는가.
  • ON 조건에 맞는 조합은 몇 개이고, 미연결 왼쪽 행을 보존하는가.
  • NULL은 원래 값의 미기록인가, 외부 조인의 미연결 결과인가.
  • WHERE를 통과한 행에서 요청한 열과 정렬 순서를 정확히 적었는가.

SELECT·WHERE·ORDER BY의 기본 흐름이 아직 낯설다면 이전 SQL 결과 읽기 글에서 한 표의 조회부터 복습한 뒤 두 표의 연결로 넘어가면 된다.

학습 기준은 Q-Net 정보처리기사 공식 안내에서 확인했다. 2026년 적용 출제기준의 기간은 2026년 1월 1일~12월 31일이며, 실기는 정보처리 실무·필답형 2시간 30분으로 안내된다. 응시 일정은 자신의 회차 안내와 수험표로 확인한다.

SQL 설명은 PostgreSQL 공식 문서의 JOIN과 WHERE, NULL 비교, COUNT 집계와 대조했다. 자료 확인일은 2026년 10월 2일이다.

오늘은 LEFT JOIN의 네 줄을 다시 적고, 각 줄 옆에 ‘주문 연결’, ‘금액 미기록’, ‘주문 미연결’ 중 맞는 이유를 써 본다. 긴 공부 시간이 없어도 한 결과를 자기 말로 설명하는 연습은 할 수 있다.

표에 남은 한 줄의 이유를 알면, 다음 문제에서도 생각을 시작할 자리가 생긴다.

반응형