문서 읽는 데 41분 · D1

D-1: 집합 연산자 — 두 조회 결과를 더하고 빼고 겹쳐 보기

목차 25
전체 24강 중 13강 · 데이터베이스
난이도 · 입문

ℹ️Oracle SQL로 SQLD 자격증을 대비하며 실전 SQL을 쌓는 트랙이에요. 프로그래밍 경험이 없어도 시작할 수 있고, 부트캠프에서는 자바 기초 다음·스프링 부트 직전에 배치하길 권해요.

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 흩어진 표를 키로 이어 붙이는 조인을 배웠어요. 그중 SELF 조인으로 follow 표를 자기 자신과 이어, 서로 맞팔한 쌍 넷을 찾아냈죠. 그런데 그 쿼리, 조인 조건이 꽤 까다로웠던 것 기억나시죠? f1.follower_id = f2.following_id 에 그 반대 조건까지, 거기에 같은 쌍이 두 번 나오지 않게 WHERE f1.follower_id < f1.following_id 까지 붙여야 했어요.

오늘은 같은 맞팔을 훨씬 간단하게 찾아요. 두 조회 결과를 집합처럼 다루는 거예요. 두 결과를 위아래로 더하고(합집합), 겹치는 것만 남기고(교집합), 한쪽에서 다른 쪽을 빼는(차집합) 연산이에요. 이걸 집합 연산자라고 해요. 맞팔은 교집합으로, 짝사랑은 차집합으로 한 줄이면 나와요. 오늘도 지난 시간에 만든 follow 표를 그대로 써요.

텍스트
 오늘의 여정
   ① 집합 연산이란 — 두 조회를 위아래로 붙이기
   ② UNION과 UNION ALL — 중복을 지울까 둘까
   ③ INTERSECT — 겹치는 것만 (맞팔 찾기)
   ④ MINUS — 빼면 방향이 생긴다 (짝사랑 찾기)
   ⑤ ORDER BY 위치와 NULL — 경계의 함정
   ⑥ 네 연산자 총정리 — 규칙·우선순위·표준 방언

💡 오늘 수업의 핵심 — "두 조회 결과를 합치고(UNION) 겹치고(INTERSECT) 빼는(MINUS) 집합 연산으로, 맞팔과 짝사랑을 한 줄에 가려낸다"

🎯 학습 목표

  • UNION·UNION ALL·INTERSECT·MINUS 네 연산자의 동작과 차이(중복 제거·정렬·방향)를 이해한다. (SQLD 2과목 'SQL 활용 — 집합 연산자')
  • 두 SELECT 를 집합 연산으로 잇는 규칙(컬럼 개수·타입 일치, 결과 컬럼명)과 빈출 함정을 익힌다.
  • 맞팔(교집합)·짝사랑(차집합)을 집합 연산으로 표현하고, MINUS 의 방향이 결과를 어떻게 가르는지 확인한다.

Step 1: "집합 연산이란 — 두 조회를 위아래로 붙이기"

지금까지 우리가 쓴 SELECT 는 표 하나에서 행을 골라 왔어요. 조인은 표 둘을 옆으로(가로로) 이어 붙였고요. 오늘 배우는 집합 연산은 방향이 달라요. 두 SELECT 의 결과를 위아래로(세로로) 포개는 거예요.

중학교 때 배운 집합을 떠올려 보세요. 두 집합 A, B 가 있으면 합집합(A∪B), 교집합(A∩B), 차집합(A−B) 을 그릴 수 있죠. SQL 도 똑같아요. 한 SELECT 결과를 집합 A, 다른 SELECT 결과를 집합 B 로 보고, 이 둘을 그대로 연산해요.

텍스트
 두 SELECT 결과를 집합 A, B 라 하면

   UNION      A ∪ B   둘을 합치고 중복은 하나로
   UNION ALL  A + B   둘을 합치되 중복도 그대로
   INTERSECT  A ∩ B   양쪽에 다 있는 것만
   MINUS      A − B   A 에는 있고 B 엔 없는 것만

그런데 두 결과를 포개려면 한 가지 약속이 필요해요. 위아래로 쌓을 거니까 두 SELECT 의 컬럼이 서로 맞아야 해요. 규칙은 세 가지예요.

  • 두 SELECT 의 컬럼 개수가 같아야 한다.
  • 짝이 되는 컬럼끼리 타입이 호환돼야 한다. (숫자는 숫자끼리, 문자는 문자끼리)
  • 결과의 컬럼 이름은 첫 번째 SELECT 의 이름(또는 별칭)을 따른다.

앞의 두 규칙을 어기면 쿼리가 아예 실행되지 않고 에러가 나요. 컬럼 개수가 다르면 이런 식이에요. (★빈출)

SQL
-- sql/queries/D1_set_operators.sql
-- 위는 컬럼 1개, 아래는 2개 → 개수가 안 맞아 에러
SELECT follower_id            FROM follow
UNION
SELECT follower_id, following_id FROM follow;

이걸 실행하면 "쿼리 블록의 결과 컬럼 개수가 맞지 않다(ORA-01789)" 는 에러가 떠요. 위는 한 컬럼, 아래는 두 컬럼이라 포갤 수가 없는 거죠.

타입이 안 맞아도 마찬가지예요. 숫자 컬럼과 문자 컬럼을 포개려 하면요.

SQL
-- 숫자(follower_id) 위에 문자(username) 를 포개려 함 → 타입 에러
SELECT follower_id FROM follow
UNION
SELECT username    FROM member;

"표현식의 타입이 서로 같아야 한다(ORA-01790)" 는 에러가 나요. 짝이 되는 두 컬럼은 같은 종류여야 한다는 뜻이에요.

⚠️ 함정 — SQLD 에서 집합 연산 문제는 거의 항상 이 규칙을 물어요. 두 SELECT 의 컬럼 개수가 다르거나 타입이 어긋난 보기를 주고 "에러가 나는 것을 고르라" 거나, 결과 컬럼명이 무엇인지(첫 SELECT 기준) 묻는 식이에요. ★빈출

💡 한 줄 정리

집합 연산은 두 SELECT 결과를 세로로 포개는 연산(합집합·교집합·차집합)이며, 두 SELECT 의 컬럼 개수와 타입이 맞아야 하고 결과 컬럼명은 첫 SELECT 를 따른다.

🙋 학생 질문 — "튜터님, 조인이랑 집합 연산이랑 뭐가 달라요? 둘 다 두 표를 합치는 거 아닌가요?"

방향이 달라요. 조인은 가로로 붙여요. 회원 표의 한 줄 옆에 게시물 표의 컬럼을 이어 붙여 한 줄을 더 넓게 만들죠. 집합 연산은 세로로 쌓아요. 두 조회 결과의 줄을 위아래로 합쳐 줄 수를 늘리는 거예요. 그래서 조인은 컬럼(가로)이 늘고, 집합 연산은 행(세로)이 늘어요. 또 하나, 조인은 보통 서로 다른 표를 잇지만 집합 연산은 같은 컬럼 구조를 가진 두 조회면 무엇이든 포갤 수 있어요. 같은 표를 조건만 바꿔 두 번 조회한 결과를 합치는 것도 자주 해요. 오늘 맞팔·짝사랑이 딱 그 경우예요.


Step 2: "UNION과 UNION ALL — 중복을 지울까 둘까"

이제 진짜로 포개 볼게요. 회원 1번 김재훈 님을 기준으로, 김재훈 님과 팔로우로 엮인 모든 사람을 뽑아 볼 거예요. 엮였다는 건 둘 중 하나예요. 김재훈 님이 따르는 사람(팔로잉)이거나, 김재훈 님을 따르는 사람(팔로워)이거나요.

지난 시간에 봤듯이 follow 표는 follower_id(따르는 사람)와 following_id(따라지는 사람) 두 컬럼이에요. 그래서,

  • 김재훈 님이 따르는 사람 = follower_id = 1 인 행의 following_id → 2번, 6번
  • 김재훈 님을 따르는 사람 = following_id = 1 인 행의 follower_id → 2번, 3번, 4번, 6번, 7번

이 두 결과를 위아래로 합치면 김재훈 님과 엮인 사람 전체가 나와요. 합칠 때 쓰는 게 UNION(합집합) 이에요.

SQL
-- sql/queries/D1_set_operators.sql
SELECT following_id AS id FROM follow WHERE follower_id = 1
UNION
SELECT follower_id  AS id FROM follow WHERE following_id = 1;

결과는 다섯 줄이에요.

id
2
3
4
6
7

위 조회는 {2, 6} 을, 아래 조회는 {2, 3, 4, 6, 7} 을 내놓는데 2번과 6번은 양쪽에 다 있죠. UNION 은 이 중복을 하나로 줄여요. 그래서 2+5=7 이 아니라 5 줄이 나온 거예요. 그리고 결과가 id 순으로 정렬돼 있는 것도 보이시죠? UNION 은 중복을 없애려고 내부에서 한 번 정렬을 해요. 그 과정에서 결과가 자연히 정렬돼 나와요.

중복을 안 지우고 싶을 때도 있어요. 그땐 UNION ALL 을 써요.

SQL
SELECT following_id AS id FROM follow WHERE follower_id = 1
UNION ALL
SELECT follower_id  AS id FROM follow WHERE following_id = 1;

이번엔 일곱 줄이에요. 2번과 6번이 양쪽에서 한 번씩, 두 번 나와요. {2, 6} 과 {2, 3, 4, 6, 7} 을 그냥 이어 붙인 거예요. 그리고 정렬도 안 해요. 위 조회 결과가 먼저, 아래 조회 결과가 그다음으로 순서대로 붙어요.

그럼 둘 중 뭘 써야 할까요? 중복이 없을 게 확실하거나 중복이 있어도 상관없다면 UNION ALL 이 나아요. UNION 은 중복을 지우려고 정렬을 한 번 더 하니, 데이터가 많으면 그만큼 느려요. (🌟 실무에서는 중복 걱정이 없을 땐 습관적으로 UNION ALL 을 써서 불필요한 정렬을 피해요.)

⚠️ 함정UNION 은 중복 제거 + 정렬, UNION ALL 은 둘 다 안 함. SQLD 는 같은 두 조회에 UNIONUNION ALL 을 각각 걸고 "결과 건수가 몇 건이냐", "정렬이 보장되느냐" 를 물어요. UNION ALL 은 정렬을 보장하지 않는다는 점이 특히 자주 나와요. ★빈출

💡 한 줄 정리

UNION 은 두 결과를 합치며 중복을 없애고(정렬 동반), UNION ALL 은 중복도 정렬도 건드리지 않고 그대로 이어 붙인다.

🙋 학생 질문 — "튜터님, 결과가 정렬돼 나오면 좋은 거 아니에요? 왜 UNION ALL 을 권해요?"

정렬이 공짜가 아니라서요. UNION 의 정렬은 "보여 주려고" 하는 게 아니라 "중복을 찾으려고" 하는 거예요. 같은 값을 옆에 모아야 중복을 지울 수 있으니까요. 그 정렬 비용이 데이터가 커질수록 부담돼요. 그런데 두 조회 결과에 애초에 겹치는 값이 없다면, 중복 제거도 그 정렬도 다 헛수고예요. 그럴 땐 UNION ALL 로 그 비용을 통째로 아끼는 거죠. 정렬된 결과가 정말 필요하면 어차피 ORDER BY 를 따로 붙이는 게 정확해요. UNION 이 정렬해 준 것에 기대지 마세요. 그건 우연한 부산물이지 보장된 동작이 아니거든요.


Step 3: "INTERSECT — 겹치는 것만 (맞팔 찾기)"

지난 시간에 자기 조인으로 맞팔을 찾았죠. 조건이 꽤 복잡했어요. 오늘은 교집합 하나로 끝내요. 맞팔이 뭐였죠? 내가 따르는데 그 사람도 나를 따르는 관계예요. 김재훈 님으로 보면 "김재훈 님이 따르는 사람"과 "김재훈 님을 따르는 사람", 두 집합에 동시에 들어 있는 사람이에요. 두 집합에 다 있는 것 — 그게 교집합, INTERSECT 예요.

SQL
-- sql/queries/D1_set_operators.sql
SELECT following_id AS id FROM follow WHERE follower_id = 1
INTERSECT
SELECT follower_id  AS id FROM follow WHERE following_id = 1;

Step 2 와 똑같은 두 조회인데 가운데만 INTERSECT 로 바꿨어요. 결과는 두 줄이에요.

id
2
6

{2, 6} 과 {2, 3, 4, 6, 7} 의 교집합이니 {2, 6} 이죠. 김재훈 님의 맞팔 상대는 2번 이민지, 6번 강지수, 두 사람이에요. 김재훈 님이 따르기도 하고 그 둘도 김재훈 님을 따르니까요.

한 사람 말고 전체 맞팔 쌍을 한 번에 찾을 수도 있어요. follow 표를 통째로 두 번 조회하되, 두 번째는 두 컬럼의 순서를 뒤집는 거예요.

SQL
SELECT follower_id, following_id FROM follow
INTERSECT
SELECT following_id, follower_id FROM follow;

이건 두 컬럼짜리 집합 연산이에요. 첫 조회는 (따르는 사람, 따라지는 사람) 순서 그대로, 두 번째 조회는 그 둘을 뒤집은 (따라지는 사람, 따르는 사람) 이에요. 두 결과에 똑같이 들어 있는 (a, b) 는 follow 표에 (a, b) 도 있고 (b, a) 도 있다는 뜻이에요. 바로 맞팔이죠. 결과는 여덟 줄이에요. 맞팔 4쌍이 양방향으로, 즉 (1,2)와 (2,1) 처럼 한 쌍당 두 줄씩 나와서 8 줄이에요.

지난 시간 자기 조인 기억나시죠? 그때 맞팔 4쌍을 얻으려고 WHERE f1.follower_id < f1.following_id 로 한쪽 방향만 남겨 8줄을 4줄로 줄였어요. INTERSECT 는 그 8줄(양방향)을 까다로운 조인 조건 없이 바로 내놓아요. 한쪽 방향만 보고 싶으면 똑같이 번호 대소 조건을 더 걸면 되고요.

⚠️ 함정INTERSECTUNION 처럼 중복을 제거하고 정렬해요. 그리고 컬럼 개수·타입 일치 규칙은 모든 집합 연산자에 똑같이 적용돼요. 교집합이라고 규칙이 느슨해지지 않아요. ★빈출

💡 한 줄 정리

INTERSECT 는 두 조회 결과에 공통으로 있는 행만 남기는 교집합이며, "따르는 사람 ∩ 따라지는 사람" 으로 맞팔을 한 줄에 찾는다.

🙋 학생 질문 — "튜터님, 그럼 조인으로 맞팔 찾던 건 이제 안 써도 되나요?"

상황 따라 갈라요. "맞팔인 사람의 id 만" 필요하면 INTERSECT 가 압도적으로 간단해요. 두 조회를 겹치기만 하면 되니까요. 그런데 맞팔이면서 "그 사람의 이름, 팔로우 시작 날짜까지 한 줄에" 보고 싶다면 결국 조인이 필요해요. 집합 연산은 같은 구조의 두 결과를 포개는 거라, 서로 다른 표의 여러 컬럼을 한 줄에 가로로 붙이는 일은 조인의 몫이거든요. 그래서 "겹치는 id 를 가려내는 단계" 는 집합 연산으로, "그 id 에 살을 붙이는 단계" 는 조인으로 나눠 쓰는 감각이 좋아요. 둘은 경쟁이 아니라 짝이에요.


Step 4: "MINUS — 빼면 방향이 생긴다 (짝사랑 찾기)"

합집합과 교집합을 봤으니 이제 차집합이에요. 차집합은 한쪽에서 다른 쪽을 빼요. Oracle 에서는 MINUS 라고 써요. 짝사랑을 찾기에 딱이에요. 짝사랑이 뭐죠? 내가 따르는데 그 사람은 나를 안 따르는 관계예요. "내가 따르는 사람" 에서 "나를 따르는 사람" 을 빼면, 나만 일방적으로 따르는 사람이 남죠.

김재훈 님으로 해볼게요.

SQL
-- sql/queries/D1_set_operators.sql
-- 내가 따르는 사람 - 나를 따르는 사람 = 내가 짝사랑하는 사람
SELECT following_id AS id FROM follow WHERE follower_id = 1
MINUS
SELECT follower_id  AS id FROM follow WHERE following_id = 1;

결과는 한 줄도 없어요. 0건이에요. {2, 6} 에서 {2, 3, 4, 6, 7} 을 빼면 2도 6도 뒤 집합에 있으니 다 지워지고 남는 게 없어요. 김재훈 님이 따르는 두 사람은 모두 맞팔이라, 김재훈 님 쪽에서 일방적인 짝사랑은 없는 거예요.

여기서 MINUS 의 가장 중요한 성질이 나와요. 순서를 뒤집어 볼게요.

SQL
-- 나를 따르는 사람 - 내가 따르는 사람 = 나를 짝사랑하는 사람
SELECT follower_id  AS id FROM follow WHERE following_id = 1
MINUS
SELECT following_id AS id FROM follow WHERE follower_id = 1;

이번엔 세 줄이에요.

id
3
4
7

{2, 3, 4, 6, 7} 에서 {2, 6} 을 빼니 {3, 4, 7} 이 남죠. 3번 박승우, 4번 정하루, 7번 윤태양 — 이 셋은 김재훈 님을 따르는데 김재훈 님은 안 따르는, 김재훈 님을 짝사랑하는 사람이에요.

같은 두 집합인데 순서만 바꿨더니 0건과 3건으로 결과가 완전히 달라졌어요. 이게 MINUS 의 핵심이에요. A − B 와 B − A 는 다르다. 뺄셈에는 방향이 있는 거예요. UNION 이나 INTERSECT 는 순서를 바꿔도 결과가 같지만(합치고 겹치는 건 방향이 없으니까), MINUS 만은 순서가 결과를 가른다.

양쪽 다 짝사랑이 있는 회원도 볼까요. 12번 박지훈 님이에요. 박지훈 님이 따르는 사람은 {9, 6}, 박지훈 님을 따르는 사람은 {9, 11} 이에요.

SQL
-- 박지훈이 일방적으로 따르는 사람
SELECT following_id AS id FROM follow WHERE follower_id = 12
MINUS
SELECT follower_id  AS id FROM follow WHERE following_id = 12;

{9, 6} − {9, 11} = {6}. 박지훈 님이 짝사랑하는 사람은 6번 강지수 한 명이에요. 반대로 뒤집으면 {9, 11} − {9, 6} = {11}, 박지훈 님을 짝사랑하는 사람은 11번 한소라 한 명이고요. 양쪽이 다르죠. 역시 방향이 결과를 갈라요.

전체 짝사랑(한쪽만 따르는 모든 관계)도 한 번에 찾을 수 있어요. Step 3 의 전역 맞팔과 똑같은 모양인데 INTERSECTMINUS 로 바꾸면 돼요.

SQL
SELECT follower_id, following_id FROM follow
MINUS
SELECT following_id, follower_id FROM follow;

(a, b) 는 있는데 (b, a) 는 없는 행, 즉 한쪽만 따르는 관계만 여덟 줄 나와요. 지난 시간 [심화] 과제에서 외팔(짝사랑)을 LEFT 조인으로 찾아보라 했었죠. 그걸 MINUS 한 줄로도 풀 수 있는 거예요.

⚠️ 함정MINUS 는 비대칭이에요. A MINUS B ≠ B MINUS A. SQLD 는 두 집합과 함께 "A MINUS B 의 결과" 와 "B MINUS A 의 결과" 를 따로 묻거나, 둘이 같다고 한 보기를 함정으로 깔아요. "차집합은 순서가 중요하다" 를 기억하세요. ★빈출

💡 한 줄 정리

MINUS 는 앞 결과에서 뒤 결과를 빼는 차집합이며 방향이 있어(A−B ≠ B−A), "따르는 사람 − 따라지는 사람" 의 순서로 짝사랑의 방향을 가른다.

🙋 학생 질문 — "튜터님, MINUS 는 왜 순서가 중요한데 UNION 은 안 중요해요?"

연산의 성질 자체가 달라서예요. 합집합은 "둘을 합친다" 라, A 에 B 를 합치든 B 에 A 를 합치든 모인 결과는 같아요. 교집합도 "둘 다에 있는 것" 이라 어느 쪽을 먼저 보든 똑같고요. 그런데 차집합은 "A 에는 있고 B 에는 없는 것" 이에요. 기준이 되는 쪽(A)과 빼는 쪽(B)의 역할이 정해져 있죠. 김재훈 님 예처럼 "내가 따르는 사람에서 나를 따르는 사람을 뺀 것" 과 "나를 따르는 사람에서 내가 따르는 사람을 뺀 것" 은 묻는 게 아예 달라요. 앞은 내 짝사랑, 뒤는 나를 향한 짝사랑이니까요. 초등학교 뺄셈에서 5−3 과 3−5 가 다른 것과 같은 이치예요.


Step 5: "ORDER BY 위치와 NULL — 경계의 함정"

집합 연산을 거의 다 봤어요. 이번엔 실수하기 쉬운 두 경계를 짚어요. 정렬 위치와 NULL 처리예요.

먼저 정렬이에요. 집합 연산 결과를 정렬하고 싶으면 ORDER BY 를 어디에 둘까요? 두 조회가 위아래로 있으니 위쪽 조회에 붙이고 싶을 수 있는데, 그러면 에러가 나요.

SQL
-- sql/queries/D1_set_operators.sql
-- 첫 SELECT 에 ORDER BY 를 붙이면 에러
SELECT following_id FROM follow WHERE follower_id = 1 ORDER BY following_id
UNION
SELECT follower_id  FROM follow WHERE following_id = 1;

ORDER BY 는 집합 연산 전체에 딱 한 번, 맨 마지막 조회 뒤에만 올 수 있어요. 정렬은 최종 결과 하나에만 거는 거니까요. 중간에 끼면 "UNION 이 문법상 올 수 없는 곳에 있다(ORA-03048)" 같은 에러가 나요. 제대로 쓰면 이래요.

SQL
SELECT following_id AS id FROM follow WHERE follower_id = 1
UNION
SELECT follower_id  AS id FROM follow WHERE following_id = 1
ORDER  BY id;

ORDER BY 가 맨 아래 한 번만 있죠. 정렬 기준 컬럼명은 첫 SELECT 의 별칭인 id 를 써요. Step 1 의 세 번째 규칙 기억나시죠? 결과 컬럼명은 첫 SELECT 를 따른다고요. ORDER BY 도 그 이름을 기준으로 삼아요.

다음은 NULL 이에요. 우리가 NULL 을 배울 때, NULL 은 "값이 없음" 이라 = 로 비교하면 같은 NULL 끼리도 같다고 판정하지 않는다고 했죠. NULL = NULL 은 참이 아니었어요. 그런데 집합 연산에서는 정반대예요. 집합 연산은 NULL 을 서로 같은 값으로 봐요.

bio(자기소개)가 비어 있는(NULL) 회원이 4명 있었죠. 3·5·8·10번이에요. 이들의 bioUNION 으로 합쳐 볼게요.

SQL
SELECT bio FROM member WHERE member_id IN (3, 5)
UNION
SELECT bio FROM member WHERE member_id IN (8, 10);

네 명의 bio 가 모두 NULL 인데 결과는 한 줄이에요. UNION 이 중복을 지우면서 NULL 네 개를 "같은 값" 으로 보고 하나로 합친 거예요. = 로는 안 같다던 NULL 이 집합 연산에서는 중복으로 묶이는 거죠. 같은 쿼리를 UNION ALL 로 바꾸면 중복을 안 지우니 NULL 네 줄이 그대로 나와요.

⚠️ 함정 — 비교 연산(=, WHERE)에서 NULL 은 서로 다르게(알 수 없음으로) 다뤄지지만, 집합 연산의 중복 제거에서는 NULL 끼리 같은 값으로 묶여요. 이 정반대 동작이 SQLD 단골 함정이에요. ★빈출

💡 한 줄 정리

ORDER BY 는 집합 연산 맨 마지막에 한 번만(첫 SELECT 컬럼명 기준) 오고, 집합 연산의 중복 제거에서 NULL 은 비교 때와 반대로 서로 같은 값으로 묶인다.

🙋 학생 질문 — "튜터님, NULL 이 = 으로는 안 같은데 집합 연산에선 같다니, 헷갈려요. 왜 달라요?"

쓰임이 다르기 때문이에요. WHERE a = b= 는 "이 두 값이 같은 값이냐" 를 따지는 비교예요. NULL 은 "값이 뭔지 모름" 이라, 모르는 값과 모르는 값이 같은지도 알 수 없죠. 그래서 참이 아니에요. 반면 집합 연산의 중복 제거는 "결과에 똑같이 생긴 줄이 여럿이면 하나로 묶자" 는 정리 작업이에요. 여기서는 "둘 다 NULL(빈칸)" 이면 생김새가 같으니 같은 줄로 묶는 게 자연스러워요. 한쪽은 값을 비교하는 일이고 다른 쪽은 줄을 정리하는 일이라, NULL 을 다루는 방식이 갈리는 거예요. "비교의 NULL 과 집합의 NULL 은 반대" 라고 묶어서 외워 두면 시험에서 안 헷갈려요.


Step 6: "네 연산자 총정리 — 규칙·우선순위·표준 방언"

오늘 배운 네 연산자를 한데 모아 정리해요. 먼저 비교표예요.

연산자 하는 일 중복 제거 정렬 순서 영향
UNION 합집합 (A ∪ B) 없음
UNION ALL 그냥 잇기 (A + B) 안 함 안 함 없음
INTERSECT 교집합 (A ∩ B) 없음
MINUS 차집합 (A − B) 있음

네 연산자 모두 컬럼 개수·타입이 맞아야 하고 결과 컬럼명은 첫 SELECT 를 따른다는 규칙은 공통이에요. UNION ALL 만 중복 제거와 정렬을 안 하고, MINUS 만 순서가 결과를 가르고요.

이제 빈출 함정 하나를 더 짚어요. 연산자를 여러 개 섞어 쓰면 어느 것부터 계산할까요? 작은 예로 볼게요. 세 집합을 A={1,2}, B={2,3}, C={3,4} 라 하고 A UNION B INTERSECT C 를 괄호 없이 써 봤어요.

SQL
-- sql/queries/D1_set_operators.sql
SELECT member_id FROM member WHERE member_id IN (1, 2)
UNION
SELECT member_id FROM member WHERE member_id IN (2, 3)
INTERSECT
SELECT member_id FROM member WHERE member_id IN (3, 4)
ORDER  BY member_id;

결과는 한 줄, {3} 이에요. Oracle 은 위에서 아래로 순서대로 계산했어요. 먼저 (A UNION B) = {1, 2, 3}, 거기에 INTERSECT C = {1, 2, 3} ∩ {3, 4} = {3}. 그래서 {3} 이죠.

그런데 여기에 함정이 있어요. SQL 표준에서는 INTERSECTUNION·MINUS 보다 우선순위가 높아요. 표준대로라면 B INTERSECT C 를 먼저 계산해 A UNION (B INTERSECT C) = {1, 2} ∪ {3} = {1, 2, 3}, 세 줄이 나와야 해요. 하지만 Oracle 은 세 연산자의 우선순위를 모두 같게 보고 그냥 쓴 순서대로(위→아래) 계산해서 한 줄이 나온 거예요. 우선순위를 바꾸고 싶으면 직접 괄호를 쳐야 해요.

SQL
-- INTERSECT 를 먼저 묶으면 결과가 세 줄로 달라진다
SELECT member_id FROM member WHERE member_id IN (1, 2)
UNION
( SELECT member_id FROM member WHERE member_id IN (2, 3)
  INTERSECT
  SELECT member_id FROM member WHERE member_id IN (3, 4) )
ORDER  BY member_id;

괄호로 INTERSECT 를 먼저 묶으니 {1, 2, 3}, 세 줄이 나와요. 같은 세 조회인데 괄호 하나로 결과가 갈렸죠.

⚠️ 함정 — Oracle 은 집합 연산자 우선순위가 모두 같아 위에서 아래로 계산해요. 표준의 "INTERSECT 우선" 과 다를 수 있으니, 섞어 쓸 땐 괄호로 의도를 분명히 하세요. SQLD 는 섞인 집합 연산의 결과 건수를 묻는 문제를 내요. ★빈출

마지막으로 방언 한 가지예요. MINUS 는 Oracle 식 이름이에요. SQL 표준과 다른 데이터베이스에서는 같은 차집합을 EXCEPT 라고 써요. 동작은 똑같고 이름만 달라요. 두 방언을 나란히 대조하는 건 뒤에 E 카테고리에서 본격적으로 다뤄요. 지금은 "차집합 = Oracle 은 MINUS, 표준은 EXCEPT" 정도만 기억해 두세요.

💡 한 줄 정리

네 연산자는 컬럼·타입 규칙을 공유하되 중복·정렬·순서에서 갈리며, Oracle 은 우선순위가 모두 같아 위→아래로 계산하므로 섞어 쓸 땐 괄호로 의도를 분명히 한다.

🙋 학생 질문 — "튜터님, 그럼 집합 연산 섞을 땐 무조건 괄호를 치는 게 안전한가요?"

네, 그게 안전해요. 우선순위가 데이터베이스마다 다를 수 있으니, 괄호로 묶어 "이 둘을 먼저 계산하라" 고 못 박으면 어디서 돌려도 같은 결과가 나와요. 괄호 없이 쓰면 Oracle 은 위→아래로 계산하지만, 다른 데이터베이스나 표준 해석에서는 INTERSECT 를 먼저 볼 수 있어 결과가 달라질 위험이 있죠. 읽는 사람도 괄호가 있으면 계산 순서를 헷갈리지 않고요. 그러니 두 개를 넘겨 섞을 땐 의도한 순서대로 괄호를 치는 습관을 들이세요. 본인을 위해서도, 나중에 그 쿼리를 읽을 동료를 위해서도요.


마무리

오늘은 두 조회 결과를 집합처럼 다루는 연산을 배웠어요. UNION 으로 합치고, INTERSECT 로 겹치는 것만 남기고, MINUS 로 뺐죠. 지난 시간 자기 조인으로 복잡하게 찾던 맞팔을 교집합 한 줄로, 짝사랑을 차집합 한 줄로 가려냈어요. 같은 follow 표 하나로요.

오늘 배운 핵심 세 가지

  • 💡 하나 — 집합 연산은 두 SELECT 결과를 세로로 포개며(컬럼 개수·타입 일치 필수, 결과 컬럼명은 첫 SELECT), UNION/UNION ALL/INTERSECT/MINUS 네 가지가 있다.
  • 💡 UNION·INTERSECT·MINUS 는 중복 제거와 정렬을 하지만 UNION ALL 은 둘 다 안 하고, MINUS 만 순서가 결과를 가른다(A−B ≠ B−A).
  • 💡 ORDER BY 는 맨 마지막에 한 번, 중복 제거에서 NULL 은 같은 값으로 묶이며, Oracle 은 연산자 우선순위가 모두 같아 섞어 쓸 땐 괄호가 필요하다.

다음 시간 예고

오늘은 여러 조회 결과를 가로가 아닌 세로로, 옆이 아닌 위아래로 포개 봤어요. 그런데 한 가지 아쉬운 게 있어요. 오늘 쓴 두 조회는 서로 독립적이었죠. 한쪽 결과를 다른 쪽이 참고하진 못했어요. 예를 들어 "평균보다 좋아요를 많이 받은 게시물" 처럼 먼저 평균을 구하고 그 값을 조건으로 다시 거르는 일은 아직 못 해요. 다음 시간엔 한 쿼리 안에 또 다른 조회를 중첩하는 서브쿼리를 배워요. 조회 결과를 다른 조회의 재료로 쓰는, 한 단계 깊은 SQL 이에요.


과제

오늘 배운 집합 연산을 직접 써 보는 과제예요. 결과가 몇 건 나올지 먼저 예상한 뒤 실행해 맞는지 확인해 보세요.

[기초] 합집합과 그냥 잇기

follow 표로 다음을 해 보세요. (가) 6번 강지수 님과 팔로우로 엮인 모든 사람(따르는 사람 + 따라지는 사람)의 id 를 UNION 으로 구해 보세요. (나) 같은 쿼리를 UNION ALL 로 바꿔 결과 건수를 비교하고, 두 건수가 왜 다른지(또는 같은지) 한 줄로 적어 보세요. (다) (가)의 결과를 id 순으로 정렬해 보세요. ORDER BY 를 어디에 둬야 하는지 확인하면서요.

[응용] 교집합과 차집합

follow 표로 다음을 해 보세요. (가) 6번 강지수 님의 맞팔 상대를 INTERSECT 로 구해 보세요. (나) 강지수 님이 일방적으로 따르는 사람과, 강지수 님을 일방적으로 따르는 사람을 각각 MINUS 로 구해 보세요. (다) (나)의 두 결과가 서로 다른 이유를 MINUS 의 성질로 설명해 보세요.

[심화] NULL 과 우선순위

다음을 해 보세요. (가) bio 가 NULL 인 회원의 bio 와, NULL 이 아닌 회원의 bio 를 각각 조회해 UNION 으로 합치면 NULL 은 결과에 몇 줄로 나오는지 예상하고 확인해 보세요. (나) 회원 id 로 세 집합 {1,2}, {2,3}, {3,4} 를 만들어 UNION 다음 INTERSECT 순으로 괄호 없이 쓴 결과와, INTERSECT 를 괄호로 먼저 묶은 결과를 비교해 보세요. (다) 두 결과가 다르다면 왜 다른지 우선순위로 설명해 보세요.


생각해볼 주제

1. UNION 과 UNION ALL, 무엇을 언제 쓸까

두 조회 결과를 합칠 때 UNION 은 중복을 지우고 UNION ALL 은 그대로 둬요. 중복이 없을 게 확실한 데이터를 합치면서 습관적으로 UNION 을 쓰면 어떤 손해가 생기는지 생각해 보세요. 반대로 중복이 있을 수 있는데 무심코 UNION ALL 을 쓰면 결과가 어떻게 부풀려지는지도요. 같은 데이터를 다루더라도 "중복이 생길 수 있는가" 를 먼저 따지는 습관이 왜 중요한지 떠올려 보세요.

2. 맞팔과 짝사랑, 집합 연산과 조인 중 무엇으로 풀까

같은 맞팔을 지난 시간엔 자기 조인으로, 오늘은 INTERSECT 로 찾았어요. id 만 필요할 때와, 이름·팔로우 날짜까지 한 줄에 필요할 때 각각 어느 쪽이 나은지 생각해 보세요. 집합 연산이 잘 맞는 문제와 조인이 꼭 필요한 문제의 경계가 어디인지, 그리고 둘을 어떻게 이어 쓰면 좋은지도 고민해 보세요.

3. MINUS 의 방향을 어떻게 다룰까

MINUS 는 A−B 와 B−A 가 달라요. 짝사랑처럼 방향이 중요한 관계를 다룰 때 이 성질이 왜 유용한지 생각해 보세요. 동시에, 두 집합의 "서로 다른 부분 전체"(한쪽에만 있는 것 모두)를 보고 싶을 때는 MINUS 한 번으로 부족한데 그걸 어떻게 메울 수 있을지(두 방향의 차집합을 합치는 식으로) 떠올려 보세요. 방향이 있다는 게 강점이자 한계가 되는 지점이에요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다, 왜 그 연산자를 골랐는지 흐름을 따라와 주세요. "합칠까 겹칠까 뺄까", "중복을 지울까 둘까", "뺄 때 어느 쪽을 기준으로 둘까" 를 떠올리는 감각을 기르는 게 목표예요.


🎯 [과제 1 예시답안] 합집합과 그냥 잇기

채점 포인트

항목 배점 핵심
(가) UNION 으로 엮인 사람 35% 따르는 사람 + 따라지는 사람 · 4건
(나) UNION ALL 비교 35% 5건 · 김재훈(1)이 양쪽에 있어 중복
(다) ORDER BY 위치 30% 맨 마지막에 한 번

풀이 예시

(가) 강지수 님과 엮인 사람 (UNION)

강지수 님은 6번이에요. 따르는 사람은 follower_id = 6, 따라지는 사람은 following_id = 6 으로 뽑아 합쳐요.

SQL
SELECT following_id AS id FROM follow WHERE follower_id = 6
UNION
SELECT follower_id  AS id FROM follow WHERE following_id = 6;
id
1
5
10
12

4건이에요. 강지수 님이 따르는 사람은 1번 김재훈 한 명, 강지수 님을 따르는 사람은 1·5·10·12번이에요. 둘을 합치면 1번이 겹치니, UNION 이 중복을 지워 4건이 돼요.

(나) UNION ALL 로 바꾸기

SQL
SELECT following_id AS id FROM follow WHERE follower_id = 6
UNION ALL
SELECT follower_id  AS id FROM follow WHERE following_id = 6;

5건이에요. (가)보다 한 건 많죠. 1번 김재훈이 "강지수가 따르는 사람" 에도, "강지수를 따르는 사람" 에도 있어 두 번 나오기 때문이에요. 김재훈과 강지수가 맞팔이라 양쪽에 다 등장하는 거예요.

(다) 정렬 붙이기

SQL
SELECT following_id AS id FROM follow WHERE follower_id = 6
UNION
SELECT follower_id  AS id FROM follow WHERE following_id = 6
ORDER  BY id;

ORDER BY id 는 맨 마지막 조회 뒤에 한 번만 와요. 중간이나 첫 조회에 붙이면 에러가 나요.

💡 튜터의 한마디 — (가)와 (나)의 건수 차이(4 vs 5)가 곧 "양쪽에 겹친 사람 수" 예요. UNIONUNION ALL 의 건수 차이를 보면 두 결과에 중복이 몇 개 있었는지 거꾸로 읽어낼 수 있어요. 차이가 1이면 겹친 값이 하나, 0이면 겹침이 없었다는 뜻이죠.


🎯 [과제 2 예시답안] 교집합과 차집합

채점 포인트

항목 배점 핵심
(가) INTERSECT 맞팔 30% 1건 · 김재훈(1)
(나) MINUS 양방향 40% 0건 / 3건
(다) 비대칭 설명 30% A−B ≠ B−A

풀이 예시

(가) 강지수 님의 맞팔 (INTERSECT)

SQL
SELECT following_id AS id FROM follow WHERE follower_id = 6
INTERSECT
SELECT follower_id  AS id FROM follow WHERE following_id = 6;

1건, 1번 김재훈이에요. 강지수 님이 따르는 사람({1})과 강지수 님을 따르는 사람({1,5,10,12})의 교집합이 {1} 이죠. 강지수와 김재훈은 서로 따르는 맞팔이에요.

(나) 강지수 님의 짝사랑 양방향 (MINUS)

SQL
-- 강지수가 일방적으로 따르는 사람 (따르는 - 따라지는)
SELECT following_id AS id FROM follow WHERE follower_id = 6
MINUS
SELECT follower_id  AS id FROM follow WHERE following_id = 6;

0건이에요. 강지수 님이 따르는 사람은 1번뿐인데, 그 1번이 강지수 님을 도로 따르니(맞팔) 빼고 나면 남는 게 없어요.

SQL
-- 강지수를 일방적으로 따르는 사람 (따라지는 - 따르는)
SELECT follower_id  AS id FROM follow WHERE following_id = 6
MINUS
SELECT following_id AS id FROM follow WHERE follower_id = 6;

3건, 5·10·12번이에요. 최도연·이민수·박지훈, 이 셋은 강지수 님을 따르는데 강지수 님은 안 따르는 사람이에요.

(다) 왜 두 결과가 다른가

MINUS 는 비대칭이라 A−B 와 B−A 가 달라요. (나)의 두 쿼리는 빼는 기준이 반대예요. 앞은 "강지수가 따르는 사람" 을 기준으로 두고, 뒤는 "강지수를 따르는 사람" 을 기준으로 둬요. 기준 집합이 다르니 0건과 3건으로 갈리는 거예요.

💡 튜터의 한마디MINUS 는 "누구를 기준에 두느냐" 가 질문을 통째로 바꿔요. 같은 두 집합이라도 빼는 순서를 정하는 순간 "내 짝사랑" 과 "나를 향한 짝사랑" 으로 의미가 갈려요. 쿼리를 쓰기 전에 "내가 알고 싶은 게 어느 방향이지?" 를 먼저 정하면 순서를 안 헷갈려요.


🎯 [과제 3 예시답안] NULL 과 우선순위

채점 포인트

항목 배점 핵심
(가) NULL 줄 수 30% 1줄 (NULL 끼리 합쳐짐)
(나) 괄호 유무 비교 40% 1건 vs 3건
(다) 우선순위 설명 30% Oracle 위→아래 · 괄호로 변경

풀이 예시

(가) NULL 은 몇 줄로 나오나

SQL
SELECT bio FROM member WHERE bio IS NULL
UNION
SELECT bio FROM member WHERE bio IS NOT NULL;

전체 9건이 나오고, 그중 NULL 은 1줄이에요. bio 가 NULL 인 회원은 4명(3·5·8·10번)이지만, UNION 이 중복을 지우면서 NULL 네 개를 하나로 묶어요. 그래서 서로 다른 자기소개 8개 + NULL 1줄 = 9건이에요. 집합 연산에서는 NULL 끼리 같은 값으로 보기 때문이에요.

(나) 괄호 유무 비교

SQL
-- 괄호 없이: Oracle 은 위→아래로 계산
SELECT member_id FROM member WHERE member_id IN (1, 2)
UNION
SELECT member_id FROM member WHERE member_id IN (2, 3)
INTERSECT
SELECT member_id FROM member WHERE member_id IN (3, 4);

1건, {3} 이에요. (1,2 ∪ 2,3) = {1,2,3}, 거기에 ∩ {3,4} = {3} 이죠.

SQL
-- INTERSECT 를 괄호로 먼저 묶기
SELECT member_id FROM member WHERE member_id IN (1, 2)
UNION
( SELECT member_id FROM member WHERE member_id IN (2, 3)
  INTERSECT
  SELECT member_id FROM member WHERE member_id IN (3, 4) );

3건, {1,2,3} 이에요. {2,3} ∩ {3,4} = {3} 을 먼저 구하고, {1,2} ∪ {3} = {1,2,3} 이죠.

(다) 왜 다른가

Oracle 은 집합 연산자 우선순위가 모두 같아 쓴 순서대로 위→아래로 계산해요. 그래서 괄호 없는 쿼리는 UNION 을 먼저 했어요. 괄호를 치면 그 안을 먼저 계산하라고 강제하니 INTERSECT 가 앞서고, 결과가 1건에서 3건으로 갈려요.

💡 튜터의 한마디 — NULL 줄 수를 셀 땐 "중복 제거가 NULL 도 묶는다" 를, 우선순위 문제를 풀 땐 "Oracle 은 위→아래" 를 떠올리세요. 둘 다 SQLD 단골이에요. 특히 섞인 집합 연산은 괄호 위치만 바꿔 보기를 여러 개 깔고 결과 건수를 묻는 식으로 나와요.


🤔 [생각해볼 주제 1] UNION 과 UNION ALL, 무엇을 언제 쓸까

문제 상황 요약

두 조회 결과를 합칠 때 UNION 은 중복을 지우고 정렬도 하지만, UNION ALL 은 둘 다 안 해요. 중복 걱정이 없는데 습관적으로 UNION 을 쓰는 게 어떤 손해인지, 반대로 중복이 있을 수 있는데 UNION ALL 을 쓰면 어떤 문제가 생기는지를 가르는 문제예요.

튜터의 가이드 및 해설

핵심은 "이 두 결과에 겹치는 값이 생길 수 있는가" 를 먼저 따지는 거예요. 서로 다른 게시판의 글을 합치는데 한 글이 양쪽에 동시에 있을 수 없다면, 중복은 애초에 없어요. 그런데도 UNION 을 쓰면 데이터베이스는 "혹시 중복이 있나" 확인하려고 전체를 한 번 정렬해요. 그 정렬은 데이터가 많아질수록 비싸지고, 결국 헛수고예요. 그럴 땐 UNION ALL 이 맞아요.

반대로 두 결과에 같은 값이 들어올 수 있는데 UNION ALL 을 쓰면 그 중복이 그대로 결과에 남아요. 회원 수를 센다거나 목록을 보여 줄 때 같은 사람이 두 번 나오면 값이 부풀려지죠. 이럴 땐 UNION 이 맞고요. 그러니 둘 중 무엇을 쓸지는 성능 취향이 아니라 "중복이 의미상 허용되는가" 라는 데이터의 성질로 정해요.

🎯 SQLD는 이렇게 나온다

같은 두 조회에 UNIONUNION ALL 을 각각 걸고 결과 건수를 비교시키는 문제가 단골이에요. "UNION ALL 은 정렬을 보장하지 않는다", "UNION 은 중복을 제거한다" 같은 진술의 참·거짓을 묻기도 해요. UNION ALL 이 더 빠른 이유(정렬·중복 제거를 생략)도 자주 나와요.

💡 실무에선

중복이 없을 게 확실하면 UNION ALL 을 기본으로 둬요. 불필요한 정렬을 피해 쿼리가 가벼워지거든요. 정렬된 결과가 필요하면 UNION 의 부산물에 기대지 말고 ORDER BY 를 명시하는 게 정확해요.


🤔 [생각해볼 주제 2] 맞팔과 짝사랑, 집합 연산과 조인 중 무엇으로 풀까

문제 상황 요약

같은 맞팔을 지난 시간엔 자기 조인으로, 이번엔 INTERSECT 로 찾았어요. 둘 중 어느 쪽이 나은지는 "무엇을 보고 싶은가" 에 달려 있어요. 두 도구의 경계와 이어 쓰는 법을 가르는 문제예요.

튜터의 가이드 및 해설

"맞팔인 사람의 id 만" 알면 되면 INTERSECT 가 압도적으로 간단해요. 두 조회를 겹치기만 하면 되니까요. 자기 조인처럼 까다로운 조인 조건이 필요 없죠. 반면 "맞팔인 사람의 이름, 팔로우 시작 날짜까지 한 줄에" 보고 싶으면 결국 조인이 필요해요. 집합 연산은 같은 구조의 두 결과를 세로로 포개는 거라, 서로 다른 표의 여러 컬럼을 가로로 붙이는 일은 못 하거든요.

그래서 둘은 경쟁이 아니라 짝이에요. "겹치는 id 를 가려내는 단계" 는 집합 연산으로 깔끔하게, "그 id 에 이름·날짜 같은 살을 붙이는 단계" 는 조인으로. 이렇게 역할을 나눠 생각하면 어떤 문제든 조립하기 쉬워져요. 어느 하나가 늘 옳은 게 아니라, 단계마다 맞는 도구가 따로 있는 거예요.

🎯 SQLD는 이렇게 나온다

"이 결과를 집합 연산으로 표현하면?" 또는 "같은 결과를 조인으로 바꾸면?" 처럼 두 방식을 오가게 하는 문제가 나와요. 집합 연산은 컬럼 구조가 같아야 한다는 제약, 조인은 컬럼을 가로로 늘린다는 특징을 구분하는지 물어요.

💡 실무에선

복잡한 조건은 단계로 쪼개요. 먼저 집합 연산으로 대상 id 집합을 추리고, 그 다음 조인으로 보여 줄 정보를 붙이는 식이죠. 한 번에 다 하려 들기보다 단계를 나누면 쿼리가 읽기 쉽고 고치기도 편해요.


🤔 [생각해볼 주제 3] MINUS 의 방향을 어떻게 다룰까

문제 상황 요약

MINUS 는 A−B 와 B−A 가 달라요. 이 방향성이 짝사랑처럼 방향이 중요한 관계엔 강점이지만, "두 집합의 서로 다른 부분 전체" 를 보고 싶을 땐 한 번으로 부족해요. 강점과 한계를 함께 가르는 문제예요.

튜터의 가이드 및 해설

방향이 있다는 게 짝사랑 문제에선 정확히 필요한 성질이에요. "내가 일방적으로 따르는 사람" 과 "나를 일방적으로 따르는 사람" 은 다른 질문인데, MINUS 의 순서가 이 둘을 정확히 갈라 주니까요. 순서가 곧 질문의 방향인 셈이죠.

그런데 "한쪽에만 있는 것 전부" (대칭 차집합)를 보고 싶을 땐 MINUS 한 번으로 안 돼요. A−B 는 A 에만 있는 것, B−A 는 B 에만 있는 것이라, 둘을 따로 구해 UNION 으로 합쳐야 전체가 나와요. 즉 (A MINUS B) UNION (B MINUS A) 같은 모양이 되죠. 방향이 있다는 건 "한 방향씩 정확히 본다" 는 강점이자, "양쪽을 한 번에 못 본다" 는 한계예요. 그래서 무엇을 원하는지에 따라 한 번 쓸지 두 번 합칠지 정해요.

🎯 SQLD는 이렇게 나온다

A MINUS B 의 결과와 B MINUS A 의 결과를 따로 묻거나, 둘이 같다고 한 보기를 함정으로 깔아요. "차집합은 교환 법칙이 성립하지 않는다(순서 중요)" 를 진술로 확인하는 문제도 나와요. UNION·INTERSECT 와 달리 MINUS 만 순서에 민감하다는 점이 변별 포인트예요.

💡 실무에선

"기준에서 빠진 것" 을 찾을 때 MINUS 가 깔끔해요. 예를 들어 "전체 회원 중 한 번도 글을 안 쓴 회원" 처럼요. 다만 빼는 순서를 거꾸로 두면 정반대 결과가 나오니, 쿼리를 짜기 전에 "어느 쪽을 기준에 둘지" 를 분명히 정하는 습관이 중요해요.

전체 목록 데이터베이스