data/database

[데이터베이스] 05. 관계 데이터 연산

렁치 2026. 8. 25. 15:40

데이터를 표(릴레이션)로 저장했으니, 이제 그 표에서 원하는 데이터를 꺼내는 방법을 정의할 차례다.

관계 데이터 연산은 "릴레이션을 입력받아 릴레이션을 출력하는" 연산들의 체계다. 다소 수학적이고 추상적이라 처음엔 "이걸 왜 배우나" 싶었다.

그런데 우리가 매일 쓰는 SQL의 SELECT가 사실 이 연산들이 옷을 갈아입은 모습이었다. 이 토대를 알아두니 SQL이 내부에서 무슨 일을 하는지가 보였다.


1. 관계 대수 vs 관계 해석

원하는 데이터를 표현하는 방식에는 두 갈래가 있다. 이 구분은 프로그래밍의 "명령형 vs 선언형"과 정확히 같다.

구분 성격 표현하는 것
관계 대수(Relational Algebra) 절차적 어떻게(How) 얻을지, 즉 연산 순서를 명시
관계 해석(Relational Calculus) 비절차적 무엇(What)을 원하는지만 명시
  • 관계 대수: "A·B 테이블을 조인한 뒤, 나이 20 이상인 행을 고르고, 이름 열만 추출하라"처럼 절차를 일일이 지시한다.
  • 관계 해석: "나이 20 이상인 사람의 이름"이라는 결과 조건만 기술하고 절차는 맡긴다.

흥미로운 건 둘의 표현력이 동일하다는 점이다(관계적으로 완전, relationally complete). 같은 질의를 한쪽은 절차로, 한쪽은 조건으로 쓸 뿐 결과가 같다.

그리고 우리가 쓰는 SQL은 겉모습은 관계 해석(선언적)에 가깝지만, DBMS 내부에서는 이를 관계 대수 연산으로 바꿔 실행한다. 이 변환과 최적화가 데이터베이스 엔진의 핵심 일이다(아래 '한 걸음 더' 참고).


2. 일반 집합 연산자

릴레이션은 결국 튜플의 집합이므로, 수학의 집합 연산을 그대로 적용할 수 있다.

연산 기호 의미
합집합(Union) R ∪ S R 또는 S에 있는 튜플
교집합(Intersection) R ∩ S R과 S 모두에 있는 튜플
차집합(Difference) R − S R에 있고 S에는 없는 튜플
카티션 프로덕트(Cartesian Product) R × S 두 릴레이션 튜플의 모든 조합
합병 가능(union-compatible) 조건
합집합·교집합·차집합을 하려면 두 릴레이션이 합병 가능해야 한다. 즉 ① 차수(열 개수)가 같고, ② 대응하는 속성의 도메인이 일치해야 한다. 열 구성이 다른 두 표의 행을 한데 합치면 어느 칸에 무엇을 넣을지 정의되지 않기 때문이다.

카티션 프로덕트만은 합병 가능 조건이 필요 없다. 두 표의 모든 행을 짝지어 조합하는 연산이라, R이 m행·S가 n행이면 결과는 m×n행이 된다.

그 자체로는 잘 안 쓰지만, 다음에 볼 조인이 사실은 이 카티션 프로덕트에서 조건에 맞는 행만 골라낸 것이라 개념적으로 중요하다.


3. 순수 관계 연산자

집합 연산과 달리, 관계 모델을 위해 특별히 정의된 연산들이다. 데이터를 자르는 방향으로 이해하니 깔끔했다.

연산 기호 의미 자르는 방향
셀렉트(Select) σ (시그마) 조건을 만족하는 튜플(행) 추출 수평(행)
프로젝트(Project) π (파이) 지정한 속성(열) 추출 수직(열)
조인(Join) 공통 속성으로 두 릴레이션을 결합 (해당 없음)
디비전(Division) ÷ S의 모든 튜플과 관련된 R의 튜플 (해당 없음)
셀렉트 vs 프로젝트: 방향이 정반대
가장 헷갈리는 한 쌍이다. 셀렉트(σ)는 행을 가로로 잘라내고("여학생 행만"), 프로젝트(π)는 열을 세로로 잘라낸다("이름·나이 열만"). 이름과 달리 셀렉트가 SQL의 SELECT 절이 아니라 WHERE 절에 해당한다는 점이 함정이다.

세 연산을 SQL로 옮기니 정체가 분명해졌다. 예를 들어 SELECT 이름, 나이 FROM 학생 WHERE 성별='여'는 다음으로 분해된다.

  • WHERE 성별='여'셀렉트 σ (조건에 맞는 행 선택)
  • SELECT 이름, 나이프로젝트 π (원하는 열 선택)
  • FROM A, B ... WHERE A.학번=B.학번조인 ⋈ (여러 테이블 결합)

즉 무심코 쓰는 SQL 한 줄이 σ·π·⋈의 조합으로 분해된다.

이 중 조인은 실무에서 가장 자주, 가장 무겁게 쓰는 연산이다. 본질은 "카티션 프로덕트 후 조인 조건을 만족하는 행만 셀렉트"한 것이다.

그래서 조인이 비효율적으로 수행되면 m×n개의 거대한 중간 결과가 생길 수 있고, 이를 어떻게 피하느냐가 쿼리 성능의 핵심이 된다.

마지막으로 디비전(÷)은 "~의 모든 것을 만족하는"을 표현한다. 예를 들어 "개설된 모든 과목을 수강한 학생"을 찾는 질의가 디비전이다. 자주 쓰진 않지만, "전부 다(for all)"라는 조건을 대수로 표현하는 유일한 연산이라 빠지지 않는다.


오늘 느낀 점

  • "이걸 왜 배우나" 싶던 관계 대수가, 내가 매일 쓰는 SQL 한 줄을 σ·π·⋈로 분해해보고 나서야 의미가 잡혔다. 추상적인 이론이 실제 쓰는 도구의 속살이었다.
  • 조인이 "카티션 프로덕트 + 셀렉트"라는 걸 알고 나니, 조인이 왜 무겁고 왜 조건을 먼저 거는 게 빠른지가 자연히 이해됐다.
  • 관계 대수(How)와 관계 해석(What)의 구분이 프로그래밍의 명령형/선언형과 같다는 점에서, 분야가 달라도 같은 사고 틀이 반복된다는 걸 느꼈다.

한 걸음 더

  • SQL은 선언적 언어라 우리는 "무엇을" 원하는지만 쓴다. 그러면 DBMS의 질의 최적화기(query optimizer)가 이를 관계 대수 식으로 바꾸고, 같은 결과를 내는 여러 실행 방법 중 가장 빠른 것을 고른다. 예를 들어 "조인 먼저 하고 셀렉트"보다 "셀렉트로 행을 먼저 줄이고 조인"하는 편이 중간 결과가 작아 대개 빠른데(셀렉션 푸시다운), 이런 변환이 전부 관계 대수의 등가 규칙(연산 순서를 바꿔도 결과가 같다) 위에서 이뤄진다.
  • 조인에도 종류가 많다. 공통 속성 값이 일치하는 행만 남기는 내부 조인(inner join), 짝이 없는 행도 NULL로 채워 보존하는 외부 조인(outer join), 두 테이블의 모든 조합을 만드는 교차 조인(cross join, = 카티션 프로덕트) 등이 있다. 외부 조인 결과에는 NULL이 곳곳에 등장하므로, NULL 처리를 모르면 결과를 잘못 읽기 쉽다.
  • 관계 해석은 기준으로 삼는 변수에 따라 튜플 관계 해석도메인 관계 해석으로 나뉜다. 학문적 구분이지만, 이 선언적 사고방식이 곧 SQL의 WHERE 조건을 쓰는 감각과 직결된다. 우리는 절차가 아니라 "참이어야 할 조건"을 기술하고 있는 것이다.