관계 대수와 관계 해석: 조인·디비전을 수강 데이터로 풀어보기


1장. “필수과목을 모두 들은 학생”은 왜 생각보다 어렵나#

학과 사무실에서 다음과 같은 요청이 들어왔다고 해보겠습니다.

필수과목을 모두 수강한 학생 명단을 뽑아 주세요.

처음에는 간단해 보입니다.

필수과목이 두 개라면 두 과목 이상 들은 학생을 찾으면 될 것 같습니다.

하지만 다음과 같은 수강 기록이 있다면 이야기가 달라집니다.

학생 수강 과목
S1 데이터베이스, 선형대수
S2 데이터베이스, 운영체제
S3 데이터베이스, 데이터베이스

필수과목은 다음 두 개입니다.

데이터베이스
운영체제

세 학생 모두 수강 기록이 두 건입니다.

하지만 필수과목을 모두 들은 학생은 S2뿐입니다.

S1은 운영체제를 듣지 않았고, S3는 데이터베이스를 두 번 들었을 뿐입니다.

즉 다음 조건은 틀렸습니다.

수강 건수 >= 필수과목 수

우리가 확인해야 하는 것은 건수가 아닙니다.

필수과목 집합에 있는 각각의 과목이 학생의 수강 집합에 모두 포함되어 있는가?

이처럼 데이터베이스 질의에서는 “몇 개 있는가”보다 “어떤 조건을 만족하는가”가 더 중요한 경우가 많습니다.

관계 대수와 관계 해석은 이러한 질문을 정확하게 표현하기 위한 이론적 기반입니다.


2장. 관계 대수와 관계 해석은 무엇이 다른가#

두 개념은 이름이 비슷하지만 문제를 바라보는 방식이 다릅니다.

관계 대수는 원하는 결과를 얻기 위해 어떤 연산을 적용할지를 표현합니다.

예를 들면 다음과 같습니다.

컴퓨터공학 학생을 고른다.
→ 수강 테이블과 연결한다.
→ 과목 테이블과 연결한다.
→ 과목명만 남긴다.

즉 데이터를 어떻게 변환할 것인지에 초점을 둡니다.

반면 관계 해석은 결과가 어떤 조건을 만족해야 하는지를 표현합니다.

예를 들면 다음과 같습니다.

학생 중에서
모든 필수과목에 대해
그 학생의 수강 기록이 존재하는 학생을 찾는다.

절차를 나열하기보다 결과가 만족해야 하는 조건을 기술합니다.

정리하면 다음과 같습니다.

구분 관계 대수 관계 해석
중심 연산 조건
관점 어떻게 결과를 만들 것인가 어떤 데이터가 답인가
대표 요소 선택, 투영, 조인, 차집합, 디비전 존재 조건, 전칭 조건
표현 성격 절차적 관점 선언적 관점

둘은 접근 방식이 다르지만 적절한 범위에서는 같은 질의를 표현할 수 있습니다.


3장. 예제 데이터를 먼저 고정하자#

관계 대수는 기호만 보면 어렵습니다.

따라서 하나의 데이터를 끝까지 사용해 계산하면 훨씬 쉽게 이해할 수 있습니다.

학생 테이블은 다음과 같습니다.

학번 이름 학과
S1 가람 컴공
S2 나래 컴공
S3 다온 수학
S4 라온 컴공

과목 테이블입니다.

과목코드 과목명
C1 데이터베이스
C2 운영체제
C3 선형대수

수강 테이블입니다.

학번 과목코드
S1 C1
S1 C2
S1 C3
S2 C1
S2 C2
S3 C2

필수과목은 다음과 같습니다.

과목코드
C1
C2

이 데이터를 이용하면 몇 가지 사실을 미리 알 수 있습니다.

S1은 C1, C2, C3을 수강했습니다.

S2는 C1과 C2를 수강했습니다.

S3는 C2만 수강했습니다.

S4는 아무 과목도 수강하지 않았습니다.

따라서 필수과목 C1과 C2를 모두 수강한 학생은 다음 두 명입니다.

S1
S2

이제 관계 대수의 연산을 하나씩 적용해 보겠습니다.


4장. 선택은 행을 고르는 연산이다#

선택은 조건을 만족하는 행을 남기는 연산입니다.

기호는 보통 σ를 사용합니다.

예를 들어 컴퓨터공학과 학생만 선택하고 싶다면 다음과 같이 표현할 수 있습니다.

σ 학과='컴공' 학생

결과는 다음과 같습니다.

학번 이름 학과
S1 가람 컴공
S2 나래 컴공
S4 라온 컴공

중요한 것은 열이 줄어든 것이 아니라는 점입니다.

원래의 세 열은 그대로 유지되고 조건을 만족하는 행만 남습니다.

따라서 선택은 다음처럼 기억하면 됩니다.

선택
→ 행을 고른다

SQL에서는 WHERE와 가장 비슷한 역할을 합니다.

SELECT *
FROM 학생
WHERE 학과 = '컴공';

5장. 투영은 열을 고르는 연산이다#

투영은 필요한 속성만 남기는 연산입니다.

기호는 π를 사용합니다.

학생 테이블에서 학과만 남기고 싶다면 다음과 같습니다.

π 학과 학생

원본 데이터에는 컴공이 세 번 등장합니다.

컴공
컴공
수학
컴공

하지만 관계 대수의 결과는 릴레이션이므로 중복이 제거됩니다.

컴공
수학

따라서 결과는 다음과 같습니다.

학과
컴공
수학

선택과 투영을 비교하면 매우 간단합니다.

선택
→ 행

투영
→ 열

SQL에서는 다음과 비슷합니다.

SELECT DISTINCT 학과
FROM 학생;

여기서 DISTINCT가 중요한 이유는 SQL의 일반적인 SELECT는 중복을 자동으로 제거하지 않기 때문입니다.


6장. 선택과 투영을 함께 사용하면#

컴퓨터공학과 학생의 학번만 알고 싶다고 하겠습니다.

먼저 컴퓨터공학과 학생을 선택합니다.

σ 학과='컴공' 학생

결과:

S1 | 가람 | 컴공
S2 | 나래 | 컴공
S4 | 라온 | 컴공

그다음 학번만 투영합니다.

π 학번

최종 결과는 다음과 같습니다.

S1
S2
S4

전체 식은 다음처럼 표현할 수 있습니다.

π 학번 (σ 학과='컴공' 학생)

관계 대수는 이렇게 연산을 하나씩 조합해 복잡한 질의를 구성합니다.


7장. 합집합은 두 결과를 합친다#

집합 A가 다음과 같다고 하겠습니다.

컴공 학생
{S1, S2, S4}

그리고 집합 B가 다음과 같습니다.

C1 수강 학생
{S1, S2}

합집합은 두 집합 가운데 어느 한쪽에라도 존재하는 값을 모두 가져옵니다.

A ∪ B

결과는 다음과 같습니다.

{S1, S2, S4}

S1과 S2가 두 집합에 모두 있지만 한 번씩만 나타납니다.

집합이기 때문입니다.

SQL에서는 다음 연산과 비슷합니다.

SELECT 학번
FROM ...
UNION
SELECT 학번
FROM ...;

UNION은 중복을 제거합니다.

반면 UNION ALL은 중복을 유지합니다.


8장. 교집합은 양쪽에 모두 있는 값을 찾는다#

같은 두 집합을 사용하겠습니다.

A = {S1, S2, S4}
B = {S1, S2}

교집합은 양쪽에 모두 존재하는 값을 찾습니다.

A ∩ B

결과는 다음과 같습니다.

{S1, S2}

즉 컴퓨터공학과 학생이면서 C1을 수강한 학생입니다.

집합 연산에서는 “그리고” 조건을 표현할 때 교집합 사고가 매우 유용합니다.


9장. 차집합은 방향이 중요하다#

이번에는 차집합입니다.

A - B

A에는 있지만 B에는 없는 값을 구합니다.

A = {S1, S2, S4}
B = {S1, S2}

따라서 다음과 같습니다.

A - B = {S4}

즉 컴공 학생 중 C1을 수강하지 않은 학생입니다.

반대로 계산하면 결과가 달라집니다.

B - A

B의 모든 학생은 A에도 있기 때문에 결과는 공집합입니다.

∅

차집합은 반드시 방향을 확인해야 합니다.

A - B
≠
B - A

10장. 집합 연산에는 합병 가능성이 필요하다#

합집합과 차집합 같은 연산을 아무 테이블에나 적용할 수 있는 것은 아닙니다.

예를 들어 다음 두 결과는 비교할 수 있습니다.

컴공 학생 학번
S1
S2
S4
C1 수강 학생 학번
S1
S2

둘 다 학번 하나의 열을 가지고 있기 때문입니다.

하지만 다음 두 테이블은 그대로 합칠 수 없습니다.

학생
학번 | 이름 | 학과
과목
과목코드 | 과목명

속성 수부터 다릅니다.

집합 연산을 적용하려면 보통 다음 조건이 필요합니다.

  • 속성의 수가 같아야 한다.
  • 대응되는 속성의 도메인이 호환되어야 한다.

이를 합병 가능성이라고 합니다.


11장. 카티션 곱은 가능한 모든 조합을 만든다#

카티션 곱은 두 릴레이션의 모든 행을 서로 조합합니다.

기호는 ×를 사용합니다.

학생이 4명이고 필수과목이 2개라고 하겠습니다.

학생:

S1
S2
S3
S4

필수과목:

C1
C2

카티션 곱을 하면 다음 여덟 조합이 만들어집니다.

S1 C1
S1 C2
S2 C1
S2 C2
S3 C1
S3 C2
S4 C1
S4 C2

행 수는 다음처럼 계산할 수 있습니다.

4 × 2 = 8

카티션 곱은 그 자체로 실제 업무에서 원하는 결과인 경우는 많지 않습니다.

하지만 조인을 이해할 때 중요한 기반이 됩니다.

조인은 개념적으로 가능한 모든 조합 가운데 조건을 만족하는 행을 선택하는 것으로 설명할 수 있습니다.


12장. 조인은 서로 다른 테이블의 데이터를 연결한다#

학생 이름과 수강 과목을 함께 보고 싶다고 하겠습니다.

학생 테이블에는 이름이 있습니다.

수강 테이블에는 과목코드가 있습니다.

두 테이블에는 공통으로 학번이 있습니다.

학생.학번
=
수강.학번

따라서 학번이 같은 행을 연결할 수 있습니다.

결과는 다음과 같습니다.

학번 이름 학과 과목코드
S1 가람 컴공 C1
S1 가람 컴공 C2
S1 가람 컴공 C3
S2 나래 컴공 C1
S2 나래 컴공 C2
S3 다온 수학 C2

S4는 수강 기록이 없기 때문에 결과에서 사라졌습니다.

이 점이 중요합니다.

일반적인 내부 조인은 양쪽에 짝이 있는 행만 남깁니다.


13장. 세타 조인은 조건을 이용해 연결한다#

세타 조인은 비교 조건을 사용하여 두 릴레이션을 연결합니다.

개념적으로 다음처럼 표현할 수 있습니다.

R ⋈조건 S

조건에는 다음과 같은 비교가 들어갈 수 있습니다.

=
<
>
<=
>=

학생과 수강을 학번으로 연결한다면 다음과 같습니다.

학생 ⋈ 학생.학번 = 수강.학번 수강

SQL에서는 다음과 비슷합니다.

SELECT
    s.학번,
    s.이름,
    e.과목코드
FROM 학생 AS s
JOIN 수강 AS e
    ON s.학번 = e.학번;

14장. 동등 조인은 같은 값을 조건으로 사용한다#

세타 조인 가운데 비교 조건이 =인 경우를 동등 조인이라고 합니다.

학생과 수강의 다음 조건이 대표적입니다.

학생.학번 = 수강.학번

주문과 회원도 같은 방식입니다.

주문.회원ID = 회원.회원ID

관계형 데이터베이스에서 가장 자주 사용하는 조인 형태 가운데 하나입니다.


15장. 자연 조인은 같은 이름의 속성을 자동으로 연결한다#

자연 조인은 두 릴레이션에 같은 이름을 가진 속성이 있으면 그 속성을 기준으로 결합합니다.

학생과 수강에 공통으로 있는 속성이 학번 하나라면 자연스럽게 연결할 수 있습니다.

그리고 결과에서는 공통 속성을 한 번만 남깁니다.

편리해 보이지만 주의가 필요합니다.

처음에는 공통 열이 학번뿐이었다고 하겠습니다.

그런데 나중에 두 테이블에 모두 상태라는 열을 추가했습니다.

학생.상태
수강.상태

자연 조인은 이름이 같은 속성을 모두 연결 조건으로 사용할 수 있습니다.

원래 의도하지 않았던 조건까지 조인에 포함될 수 있습니다.

그래서 실무 SQL에서는 다음처럼 연결 조건을 명시적으로 적는 방식이 더 읽기 쉬운 경우가 많습니다.

JOIN 수강 AS e
ON s.학번 = e.학번

어떤 값을 기준으로 연결하는지가 명확하기 때문입니다.


16장. 내부 조인에서 수강하지 않은 학생이 사라지는 이유#

학생 테이블에는 S4가 있습니다.

S4 | 라온 | 컴공

하지만 수강 테이블에는 S4가 없습니다.

따라서 내부 조인을 하면 S4와 연결할 행이 존재하지 않습니다.

결과적으로 S4는 사라집니다.

이 차이는 다음과 같은 업무 질문에서 중요합니다.

수강한 학생과 과목을 보여 주세요.

내부 조인이 적합합니다.

하지만 질문이 다음과 같다면 어떨까요?

모든 학생을 보여 주고 수강 과목이 없으면 미수강으로 표시해 주세요.

내부 조인을 사용하면 S4가 아예 없어집니다.

이럴 때 외부 조인이 필요합니다.


17장. 왼쪽 외부 조인은 왼쪽 데이터를 보존한다#

학생을 왼쪽에 두고 수강을 왼쪽 외부 조인한다고 하겠습니다.

SELECT
    s.학번,
    s.이름,
    e.과목코드
FROM 학생 AS s
LEFT JOIN 수강 AS e
    ON s.학번 = e.학번;

S1, S2, S3은 수강 기록과 연결됩니다.

S4는 연결되는 수강 행이 없습니다.

하지만 왼쪽 테이블의 행은 보존되므로 다음처럼 남습니다.

S4 | 라온 | NULL

따라서 외부 조인은 “연결되지 않은 데이터도 보여줘야 하는가”라는 요구와 밀접하게 관련됩니다.


18장. 외부 조인 뒤 COUNT를 잘못 사용하면 숫자가 틀어진다#

왼쪽 외부 조인 결과가 다음과 같다고 하겠습니다.

학번 과목코드
S1 C1
S1 C2
S1 C3
S2 C1
S2 C2
S3 C2
S4 NULL

학생별 수강 건수를 계산한다고 하겠습니다.

다음처럼 COUNT(*)를 사용하면 S4도 하나의 행을 가지고 있으므로 1로 집계될 수 있습니다.

하지만 실제 수강 건수는 0입니다.

따라서 실제 수강 행의 값을 세는 방법을 사용할 수 있습니다.

COUNT(e.과목코드)

NULL은 일반적인 COUNT(열)에서 제외됩니다.

그러면 S4는 0으로 계산됩니다.

즉 외부 조인에서는 다음 두 행을 구분해야 합니다.

학생을 보존하기 위해 생긴 행

실제로 존재하는 수강 행

19장. 세미 조인은 연결된 상대방의 존재만 확인한다#

이번에는 이런 질문이 들어왔다고 하겠습니다.

한 과목이라도 수강한 학생에게 공지를 보내 주세요.

학생의 수강 과목까지 모두 보여줄 필요가 없습니다.

수강 기록이 존재하는지만 알면 됩니다.

학생 S1은 세 과목을 수강했지만 공지를 세 번 보낼 필요는 없습니다.

결과는 다음처럼 학생 한 명당 한 행이면 됩니다.

S1
S2
S3

이런 관점의 연산이 세미 조인입니다.

SQL에서는 EXISTS를 사용하면 의미를 명확하게 표현할 수 있습니다.

SELECT
    s.학번,
    s.이름
FROM 학생 AS s
WHERE EXISTS (
    SELECT 1
    FROM 수강 AS e
    WHERE e.학번 = s.학번
);

S1에게 수강 기록이 세 건 있어도 학생 S1은 한 번만 반환됩니다.


20장. JOIN과 EXISTS는 질문의 단위가 다를 수 있다#

다음 질문을 비교해 보겠습니다.

질문 A#

학생별 수강 과목을 보여 주세요.

이 경우 한 학생에게 여러 행이 필요합니다.

S1 C1
S1 C2
S1 C3

JOIN이 자연스럽습니다.

질문 B#

수강 기록이 존재하는 학생을 보여 주세요.

이 경우 학생 한 명당 한 행이면 됩니다.

S1
S2
S3

EXISTS가 질문의 의미를 더 직접적으로 표현할 수 있습니다.

무조건 JOIN을 사용하는 것보다 결과의 한 행이 무엇을 의미해야 하는지를 먼저 정하는 것이 중요합니다.


21장. “컴공 학생이 들은 과목”을 관계 대수로 풀어보기#

이제 여러 연산을 조합해 보겠습니다.

질문은 다음과 같습니다.

컴퓨터공학과 학생이 수강한 과목의 종류를 구하라.

첫 번째 단계는 컴공 학생만 선택하는 것입니다.

σ 학과='컴공' 학생

결과:

S1
S2
S4

두 번째 단계는 수강 테이블과 조인합니다.

S4는 수강 기록이 없으므로 빠집니다.

S1 C1
S1 C2
S1 C3
S2 C1
S2 C2

세 번째 단계는 과목 테이블과 연결해 과목명을 가져옵니다.

S1 데이터베이스
S1 운영체제
S1 선형대수
S2 데이터베이스
S2 운영체제

마지막으로 과목명만 투영합니다.

관계 대수의 투영은 중복을 제거하므로 결과는 다음 세 종류입니다.

데이터베이스
운영체제
선형대수

여기서 질문은 다음과 다릅니다.

모든 컴공 학생이 공통으로 들은 과목은 무엇인가?

첫 번째 질문은 컴공 학생 누군가가 들은 과목입니다.

두 번째 질문은 컴공 학생 모두가 들은 과목입니다.

같은 단어가 비슷하게 보이지만 완전히 다른 질의입니다.


22장. 디비전은 “모두”를 처리하는 연산이다#

관계 대수에서 가장 어렵게 느껴지는 연산 중 하나가 디비전입니다.

하지만 용도를 한 문장으로 정리하면 이해하기 쉽습니다.

특정 집합의 모든 항목과 관계를 가진 대상을 찾는다.

현재 필수과목은 다음 두 개입니다.

C1
C2

수강 데이터는 다음과 같습니다.

S1 C1
S1 C2
S1 C3

S2 C1
S2 C2

S3 C2

질문은 이것입니다.

C1과 C2를 모두 들은 학생은 누구인가?

학생별로 확인하면 다음과 같습니다.

학번 C1 C2 결과
S1 수강 수강 포함
S2 수강 수강 포함
S3 미수강 수강 제외
S4 미수강 미수강 제외

결과는 다음과 같습니다.

S1
S2

이를 개념적으로 다음처럼 표현할 수 있습니다.

수강 ÷ 필수과목

23장. 디비전은 필수과목만 들은 학생을 찾는 것이 아니다#

S1은 다음 세 과목을 수강했습니다.

C1
C2
C3

필수과목은 다음 두 개뿐입니다.

C1
C2

S1은 필수과목 외에 C3도 들었습니다.

그렇다면 디비전 결과에서 제외될까요?

아닙니다.

S1은 C1과 C2를 모두 가지고 있기 때문에 포함됩니다.

디비전의 질문은 다음이 아닙니다.

필수과목만 들었는가?

정확한 질문은 다음입니다.

필수과목을 하나도 빠뜨리지 않았는가?

추가적인 선택과목은 결과에 영향을 주지 않습니다.


24장. “모두 수강했다”를 집합 차이로 생각하면 쉽다#

학생 S1이 수강한 과목은 다음과 같습니다.

{C1, C2, C3}

필수과목은 다음과 같습니다.

{C1, C2}

필수과목에서 S1이 들은 과목을 빼보겠습니다.

{C1, C2} - {C1, C2, C3}

결과는 다음과 같습니다.

∅

빠진 필수과목이 없습니다.

따라서 조건을 만족합니다.

S3을 보겠습니다.

수강 과목:

{C2}

필수과목에서 빼면 다음과 같습니다.

{C1, C2} - {C2}

결과:

{C1}

C1이 빠져 있습니다.

따라서 조건을 만족하지 않습니다.

디비전은 이렇게 생각할 수 있습니다.

필수 목록에서 이 학생이 가진 것을 빼고도 남는 것이 있는가?

아무것도 남지 않아야 합니다.


25장. SQL에서는 이중 NOT EXISTS로 “모두”를 표현할 수 있다#

SQL에는 일반적으로 관계 대수의 디비전 기호를 그대로 사용하는 명령이 없습니다.

대신 NOT EXISTS를 중첩해 표현할 수 있습니다.

SELECT s.학번
FROM 학생 AS s
WHERE NOT EXISTS (
    SELECT 1
    FROM 필수과목 AS r
    WHERE NOT EXISTS (
        SELECT 1
        FROM 수강 AS e
        WHERE e.학번 = s.학번
          AND e.과목코드 = r.과목코드
    )
);

처음 보면 상당히 복잡해 보입니다.

하지만 안쪽부터 읽으면 쉽습니다.

안쪽 NOT EXISTS는 다음을 확인합니다.

이 학생이 이 필수과목을 듣지 않았는가?

바깥쪽 NOT EXISTS는 다음을 확인합니다.

듣지 않은 필수과목이 하나라도 존재하는가?

전체 의미는 다음과 같습니다.

수강하지 않은 필수과목이 존재하지 않는 학생을 찾는다.

즉:

빠진 필수과목이 하나도 없다.

라는 의미입니다.


26장. 단순 COUNT로 “모두”를 검사하면 왜 틀릴까#

필수과목이 두 개라고 하겠습니다.

C1
C2

S1의 수강 기록은 다음과 같습니다.

C1
C3

S2는 다음과 같습니다.

C1
C2

두 학생 모두 수강 건수가 2입니다.

다음 조건을 사용하면:

수강 건수 = 필수과목 수

S1도 조건을 만족하는 것처럼 보입니다.

하지만 S1은 C2를 듣지 않았습니다.

선택과목 C3이 하나 있을 뿐입니다.

그래서 단순한 전체 수강 건수 비교는 사용할 수 없습니다.


27장. COUNT를 사용하려면 필수과목만 세어야 한다#

COUNT 방식으로 구현하고 싶다면 적어도 필수과목과 일치하는 수강 기록만 세어야 합니다.

그리고 재수강이 존재할 수 있다면 중복 과목도 제거해야 합니다.

개념적으로 다음을 비교합니다.

학생이 수강한 서로 다른 필수과목 수
=
전체 필수과목 수

예를 들면 SQL에서 다음과 같은 접근을 사용할 수 있습니다.

SELECT e.학번
FROM 수강 AS e
JOIN 필수과목 AS r
    ON r.과목코드 = e.과목코드
GROUP BY e.학번
HAVING COUNT(DISTINCT e.과목코드)
    = (
        SELECT COUNT(*)
        FROM 필수과목
    );

여기서는 선택과목 C3이 집계에 들어가지 않습니다.

또 C1을 두 번 수강했더라도 DISTINCT 때문에 하나로 계산됩니다.


28장. 재수강 때문에 COUNT가 틀어지는 사례#

필수과목이 다음 두 개라고 하겠습니다.

C1
C2

학생 S3의 수강 기록은 다음과 같습니다.

C1
C1

수강 건수는 2입니다.

필수과목 수도 2입니다.

단순 COUNT 비교라면 조건을 통과합니다.

하지만 실제로 C2는 수강하지 않았습니다.

따라서 다음 두 값은 다릅니다.

수강 기록 건수 = 2

서로 다른 필수과목 수 = 1

이 차이 때문에 “모두” 조건에서는 단순한 행 수보다 서로 다른 필수 항목의 충족 여부가 중요합니다.


29장. 필수과목 목록이 비어 있으면 어떻게 될까#

조금 이상한 상황을 생각해 보겠습니다.

필수과목 테이블이 비어 있습니다.

필수과목 = {}

질문은 여전히 다음과 같습니다.

모든 필수과목을 수강한 학생은 누구인가?

필수과목 자체가 하나도 없다면 어떤 학생도 빠뜨린 필수과목이 없습니다.

논리적으로는 모든 학생이 조건을 만족합니다.

S1
S2
S3
S4

이것은 전칭 조건의 중요한 특성입니다.

검사해야 할 대상이 하나도 없으면 모든 대상을 만족한다고 판단할 수 있다.

이를 논리학에서는 공집합에 대한 전칭 조건이 참이라고 설명합니다.

물론 실제 업무에서 이것이 원하는 결과인지 여부는 별도의 문제입니다.

업무 규칙이 다음과 같을 수도 있습니다.

필수과목이 하나 이상 정의된 경우에만 판정한다.

그렇다면 해당 조건을 별도로 추가해야 합니다.


30장. 후보 집합을 어디서 시작하느냐도 중요하다#

필수과목이 비어 있을 때 다음 방식으로 학생 후보를 구한다고 하겠습니다.

수강 테이블에 등장한 학생만 후보

그러면 S4는 수강 기록이 없으므로 애초에 후보에서 빠집니다.

하지만 질문이 다음과 같다면:

전체 학생 중 모든 필수과목을 수강한 학생

후보는 학생 테이블 전체여야 합니다.

S1
S2
S3
S4

질의에서 어떤 테이블을 출발점으로 선택하느냐가 결과에 영향을 줄 수 있습니다.


31장. “듣지 않은 학생”을 수강 테이블에서만 찾으면 놓치는 사람이 생긴다#

C1을 듣지 않은 학생을 찾는다고 하겠습니다.

수강 테이블만 보면 다음 학생들이 등장합니다.

S1
S2
S3

S4는 수강 자체를 하지 않았으므로 없습니다.

수강 데이터에서 C1을 제외하는 방식으로만 접근하면 S4를 놓칠 수 있습니다.

정확한 질문은 다음입니다.

전체 학생 중 C1 수강자가 아닌 사람은 누구인가?

따라서 개념적으로 다음처럼 접근해야 합니다.

전체 학생
-
C1 수강 학생

전체 학생:

{S1, S2, S3, S4}

C1 수강 학생:

{S1, S2}

차집합:

{S3, S4}

S4까지 올바르게 포함됩니다.


32장. 관계 해석은 연산보다 조건을 먼저 생각한다#

관계 대수에서는 다음과 같이 생각했습니다.

학생을 선택한다.
→ 수강과 조인한다.
→ 과목과 조인한다.
→ 필요한 속성을 투영한다.

관계 해석에서는 방식이 다릅니다.

결과가 어떤 조건을 만족해야 하는지를 표현합니다.

예를 들어 C1을 수강한 학생은 다음 조건으로 생각할 수 있습니다.

학생 s가 존재하고, 그 학생과 같은 학번을 가진 C1 수강 기록 e가 존재한다.

기호로 표현하면 다음과 같은 형태가 됩니다.

{ s |
  학생(s)
  ∧
  ∃e (
      수강(e)
      ∧ e.학번 = s.학번
      ∧ e.과목코드 = C1
  )
}

핵심은 ∃입니다.

이 기호는 존재한다는 뜻입니다.

즉 C1 수강 기록 하나라도 존재하면 됩니다.


33장. 튜플 관계 해석은 행 자체를 변수로 사용한다#

튜플 관계 해석은 튜플 전체를 변수로 놓습니다.

학생 튜플을 s, 수강 튜플을 e라고 하겠습니다.

C1을 수강한 학생은 다음처럼 표현할 수 있습니다.

학생 s 중에서

수강 e가 존재하고

e.학번 = s.학번이며

e.과목코드 = C1인

s를 찾는다.

관계 대수처럼 어떤 연산 순서를 수행해야 하는지 직접 지정하지 않습니다.

결과가 만족해야 하는 조건만 정의합니다.


34장. “모든 필수과목”은 전칭 조건으로 표현할 수 있다#

모든 필수과목을 수강한 학생은 다음과 같이 생각할 수 있습니다.

모든 필수과목 r에 대해, 해당 학생이 그 과목을 수강한 기록 e가 존재해야 한다.

논리적으로는 다음과 같은 형태입니다.

{ s |
    학생(s)
    ∧
    ∀r (
        필수과목(r)
        ⇒
        ∃e (
            수강(e)
            ∧ e.학번 = s.학번
            ∧ e.과목코드 = r.과목코드
        )
    )
}

여기서 ∀는 다음 의미입니다.

모든

∃는 다음 의미입니다.

존재한다

따라서 문장으로 읽으면 다음과 같습니다.

학생 s에 대해 모든 필수과목 r마다 대응하는 수강 기록 e가 존재해야 한다.

디비전의 의미와 같습니다.


35장. 도메인 관계 해석은 값에 변수를 둔다#

도메인 관계 해석은 튜플 전체가 아니라 개별 값에 변수를 둡니다.

학생 테이블이 다음 구조라고 하겠습니다.

학생(
    학번,
    이름,
    학과
)

C1을 수강한 학생 이름을 구한다면 다음과 같은 방식으로 표현할 수 있습니다.

이름 name이 답이 되려면

어떤 학번 n과 학과 dept가 존재하고

학생(n, name, dept)가 존재하며

수강(n, C1)이 존재해야 한다.

기호로 표현하면 다음과 같은 형태가 됩니다.

{ name |
    ∃n ∃dept (
        학생(n, name, dept)
        ∧
        수강(n, C1)
    )
}

튜플 관계 해석과 결과의 의미는 비슷하지만 변수를 두는 단위가 다릅니다.

구분 변수 단위
튜플 관계 해석 행 전체
도메인 관계 해석 개별 속성값

36장. 같은 이름의 학생이 있다면 이름만 반환하는 것도 주의해야 한다#

학생 테이블에 다음 두 사람이 있다고 하겠습니다.

S1 | 김하늘
S5 | 김하늘

둘 다 C1을 수강했습니다.

학생 이름만 결과로 반환한다면 관계 해석의 수학적 집합에서는 다음처럼 하나의 값만 남을 수 있습니다.

김하늘

하지만 실제 학생은 두 명입니다.

학생을 구분하는 것이 중요하다면 학번까지 반환해야 합니다.

S1 | 김하늘
S5 | 김하늘

결과에 어떤 속성을 포함하느냐에 따라 데이터가 표현하는 단위가 달라집니다.


37장. 관계 대수와 SQL은 완전히 같지 않다#

관계 대수의 릴레이션은 집합입니다.

따라서 투영 결과에서 중복이 제거됩니다.

하지만 SQL에서는 다음 쿼리를 실행하면:

SELECT 학과
FROM 학생;

결과가 다음처럼 나올 수 있습니다.

컴공
컴공
수학
컴공

관계 대수의 투영과 같은 결과를 원한다면 다음처럼 해야 합니다.

SELECT DISTINCT 학과
FROM 학생;

결과:

컴공
수학

따라서 관계 대수식을 SQL로 단순 치환하면 중복 때문에 결과 행 수가 달라질 수 있습니다.


38장. NULL 때문에 SQL은 더 복잡해진다#

관계 대수의 고전적인 집합 개념과 실제 SQL 사이에는 NULL이라는 차이도 있습니다.

왼쪽 외부 조인을 하면 수강하지 않은 S4가 다음처럼 나타날 수 있습니다.

S4 | 라온 | NULL

이 NULL은 일반적인 값과 같은 방식으로 비교되지 않습니다.

예를 들어 다음처럼 작성하면 원하는 결과가 나오지 않을 수 있습니다.

WHERE 과목코드 = NULL

NULL 여부를 확인하려면 다음처럼 사용합니다.

WHERE 과목코드 IS NULL

이 때문에 관계 대수의 순수한 집합 연산과 실제 SQL을 비교할 때는 중복과 NULL을 따로 고려해야 합니다.


39장. “모든”이라는 단어를 만나면 건수보다 누락을 확인하라#

데이터베이스 업무에서 다음과 같은 요구는 매우 자주 등장합니다.

모든 필수 교육을 완료한 직원

모든 약관에 동의한 사용자

모든 점검 항목을 통과한 장비

모든 필수 서류를 제출한 신청자

모든 권한을 가진 사용자

이런 문제는 단순한 건수 비교보다 다음 방식으로 생각하면 정확합니다.

필요한 항목 가운데 빠진 것이 하나라도 있는가?

빠진 것이 존재하면 실패입니다.

빠진 것이 하나도 없다면 성공입니다.

즉:

필수 항목 - 완료 항목 = ∅

이라는 구조입니다.


40장. 약관 동의 문제도 디비전과 같은 구조다#

서비스 이용을 위해 다음 두 약관 동의가 필요하다고 하겠습니다.

A: 서비스 이용약관
B: 개인정보 처리 안내

사용자 U1의 동의 기록은 다음과 같습니다.

A
A

기록은 두 건입니다.

필수 약관도 두 개입니다.

하지만 B에는 동의하지 않았습니다.

단순 건수 비교를 하면 잘못 통과할 수 있습니다.

사용자 U2는 다음과 같습니다.

A
B

U2만 모든 필수 약관을 만족합니다.

수강 문제와 완전히 같은 구조입니다.

수강
→ 학생이 필요한 과목을 모두 가지고 있는가

약관 동의
→ 사용자가 필요한 동의를 모두 가지고 있는가

디비전은 단지 학교 수강 문제에서만 쓰이는 개념이 아닙니다.

“필요한 항목을 모두 가지고 있는가”라는 다양한 업무 조건을 이해하는 방법입니다.


41장. 장비 점검에서도 같은 문제가 발생한다#

공장 장비가 가동되기 전에 다음 검사 세 개를 통과해야 한다고 하겠습니다.

온도 검사
압력 검사
안전센서 검사

장비 A의 완료 기록:

온도
압력
안전센서

장비 B:

온도
압력

장비 C:

온도
온도
압력

검사 기록 수만 보면 A와 C 모두 3건입니다.

하지만 모든 검사를 완료한 것은 A뿐입니다.

중요한 것은 행의 개수가 아니라 필수 검사 종류 전체를 충족했는가입니다.

이것 역시 디비전 문제와 같습니다.


42장. 관계 대수에서 결과 행 수가 변하는 순간을 보라#

관계 대수를 이해할 때 기호만 보지 말고 각 연산 뒤에 몇 행이 남는지를 확인하면 좋습니다.

예를 들어:

학생
4행

컴공 학생 선택:

3행

수강과 내부 조인:

5행

S1에게 3건, S2에게 2건이 있기 때문입니다.

과목명만 투영:

3행

중복되는 C1과 C2가 하나씩 합쳐졌기 때문입니다.

이렇게 결과 행 수가 늘거나 줄어드는 이유를 설명할 수 있으면 연산을 제대로 이해하고 있는 것입니다.


43장. 조인 때문에 행이 늘어나는 것은 오류가 아닐 수 있다#

학생 S1이 세 과목을 수강했습니다.

학생 테이블에는 S1이 한 행입니다.

하지만 수강과 조인하면 다음처럼 세 행이 됩니다.

S1 C1
S1 C2
S1 C3

이를 보고 다음처럼 판단하면 안 됩니다.

S1이 중복됐다.

학생이 중복된 것이 아닙니다.

한 학생과 연결된 수강 관계가 세 건인 것입니다.

조인 결과에서는 한쪽의 하나의 행이 관계 수만큼 반복될 수 있습니다.

따라서 조인 결과의 행 수를 볼 때는 다음 질문이 중요합니다.

결과 한 행은 무엇을 의미하는가?

여기서는 학생 한 명이 아니라 학생과 수강 과목의 한 연결을 의미합니다.


44장. 결과가 줄어드는 것도 오류가 아닐 수 있다#

학생 테이블에는 S4가 있습니다.

하지만 내부 조인 결과에는 S4가 없습니다.

이 역시 데이터가 삭제된 것이 아닙니다.

수강 테이블에 연결되는 행이 없기 때문입니다.

다음 두 질문은 다른 결과를 요구합니다.

수강 기록이 있는 학생만 보여 주세요.

내부 조인이 적합합니다.

수강 여부와 관계없이 모든 학생을 보여 주세요.

외부 조인이 필요합니다.

연산은 데이터 자체보다 질문에 맞게 선택해야 합니다.


45장. 관계 대수식을 읽는 가장 쉬운 방법#

복잡한 관계 대수식을 한 번에 해석하려 하면 어렵습니다.

안쪽부터 한 단계씩 읽으면 훨씬 쉽습니다.

예를 들어 다음 형태의 식이 있다고 하겠습니다.

π 과목명 (
    (
        σ 학과='컴공' 학생
        ⋈ 수강
    )
    ⋈ 과목
)

순서는 다음과 같습니다.

1단계#

σ 학과='컴공' 학생

컴공 학생만 남깁니다.

2단계#

⋈ 수강

그 학생들의 수강 기록을 붙입니다.

3단계#

⋈ 과목

과목코드에 과목명을 붙입니다.

4단계#

π 과목명

과목명만 남깁니다.

복잡한 식도 결국 작은 연산 여러 개의 조합입니다.


46장. 관계 대수와 관계 해석을 한 번에 비교하기#

같은 질문을 두 가지 방식으로 생각해 보겠습니다.

질문은 다음과 같습니다.

C1을 수강한 학생을 찾는다.

관계 대수에서는 다음처럼 생각합니다.

수강에서 C1을 선택한다.
→ 학생과 조인한다.
→ 학생 정보를 투영한다.

관계 해석에서는 다음처럼 생각합니다.

학생 중에서
C1 수강 기록이 존재하는 학생을 찾는다.

이번에는 다음 질문입니다.

모든 필수과목을 수강한 학생을 찾는다.

관계 대수에서는 디비전이나 차집합을 이용해 표현할 수 있습니다.

수강 ÷ 필수과목

관계 해석에서는 다음 조건으로 표현합니다.

모든 필수과목마다
해당 학생의 수강 기록이 존재한다.

표현 방식은 달라도 결국 같은 업무 질문을 해결합니다.


47장. 실무에서는 “어떤 연산인가”보다 “무엇을 묻는가”가 먼저다#

SQL을 작성하다 보면 JOIN, EXISTS, GROUP BY, DISTINCT 가운데 어떤 것을 써야 할지부터 고민하기 쉽습니다.

하지만 먼저 질문을 정확하게 정의해야 합니다.

예를 들어:

학생별 수강 과목을 보여 달라.

결과 한 행은 학생과 수강 과목의 연결입니다.

수강한 학생 명단을 보여 달라.

결과 한 행은 학생 한 명입니다.

모든 필수과목을 수강한 학생을 보여 달라.

결과 한 행은 필수 조건 전체를 충족한 학생 한 명입니다.

질문의 단위가 다릅니다.

같은 데이터를 사용해도 필요한 연산이 달라지는 이유입니다.


48장. 관계 대수와 관계 해석 핵심 정리#

관계 대수는 릴레이션에 연산을 적용해 새로운 릴레이션을 만드는 방법입니다.

대표적인 연산은 다음과 같습니다.

연산 의미
선택 조건에 맞는 행을 남김
투영 필요한 열을 남김
합집합 두 결과를 합침
교집합 양쪽에 공통인 데이터를 찾음
차집합 한쪽에만 있는 데이터를 찾음
카티션 곱 가능한 모든 행 조합을 생성
조인 관련된 행을 조건으로 연결
세미 조인 상대 데이터의 존재 여부만 이용
디비전 특정 집합의 모든 항목과 관계된 대상을 찾음

관계 해석은 연산 순서보다 결과가 만족해야 하는 조건을 표현합니다.

튜플 관계 해석은 행 전체에 변수를 두고, 도메인 관계 해석은 개별 속성값에 변수를 둡니다.

그리고 “모든”이라는 조건은 특별히 주의해야 합니다.

모든 필수과목을 수강했다.
모든 약관에 동의했다.
모든 검사 항목을 통과했다.

이런 조건을 만났을 때 단순히 행 수를 비교하면 오류가 생길 수 있습니다.

가장 안전한 사고방식은 다음과 같습니다.

필요한 항목
-
실제로 충족한 항목
=
∅

즉 빠진 것이 하나도 없는지를 확인하는 것입니다.

관계 대수와 관계 해석의 핵심은 복잡한 기호를 외우는 데 있지 않습니다.

업무에서 묻는 질문을 행, 집합, 관계, 존재 조건으로 정확하게 바꾸는 것에 있습니다.

질문이 정확하면 선택인지 조인인지 디비전인지도 자연스럽게 결정됩니다.

이 페이지의 목차