SQL의 숨겨진 설계자, 관계 대수(Relational Algebra) 완벽 정복

우리가 SQL(Structured Query Language)을 사용하여 데이터베이스에 원하는 데이터를 요청할 때, 그 내부에서는 어떤 일이 벌어질까요? 데이터베이스 관리 시스템(DBMS)은 우리가 작성한 SQL 쿼리를 곧바로 실행하는 것이 아니라, 먼저 정해진 절차와 규칙에 따라 해석하고 최적화하는 과정을 거칩니다. 이때 그 이론적 기반이 되는 것이 바로 관계 대수(Relational Algebra)입니다. 관계 대수는 원하는 결과를 얻기 위해 데이터베이스에 어떤 연산을 순서대로 수행해야 하는지를 기술하는 절차적 언어입니다.

많은 개발자들이 SQL의 편리함에 익숙해져 그 이면의 원리를 간과하곤 하지만, 관계 대수를 이해하는 것은 SQL을 한 차원 깊게 사용하는 것과 같습니다. 이는 쿼리가 내부적으로 어떻게 처리되는지 예측하고, 더 효율적인 쿼리를 작성하는 데 혜안을 제공하며, 나아가 복잡한 데이터 문제를 해결하는 논리적 사고의 틀을 마련해 줍니다. 마치 자동차 운전법을 넘어 엔진의 동작 원리를 이해하는 것과 같다고 할 수 있습니다. 이 글에서는 SQL의 뿌리가 되는 관계 대수의 핵심 개념과 주요 연산자들을 체계적으로 탐구하고, 이것이 실제 데이터베이스 세계에서 어떻게 활용되는지 그 여정을 함께 따라가 보겠습니다.


관계 대수란 무엇인가? 데이터를 위한 절차적 언어

관계 대수의 핵심 개념: 원하는 것을 얻는 방법

관계 대수(Relational Algebra)는 관계형 데이터베이스 모델에서 원하는 데이터를 검색하기 위해, 릴레이션(테이블)에 적용할 수 있는 연산(Operation)들의 집합을 정의한 것입니다. 수학의 대수학(Algebra)이 숫자와 연산자를 사용하여 식을 만들고 해를 구하는 것처럼, 관계 대수는 릴레이션(데이터 집합)과 연산자를 사용하여 새로운 릴레이션(결과 데이터 집합)을 만들어내는 과정을 다룹니다.

관계 대수의 가장 큰 특징은 ‘절차적 언어’라는 점입니다. 이는 “무엇(What)을 원하는가”뿐만 아니라, “어떻게(How) 그 결과를 얻을 것인가”에 대한 절차를 명시적으로 기술한다는 의미입니다. 예를 들어, ‘컴퓨터공학과 학생 중 3학년인 학생의 이름과 학번을 찾아라’라는 요구사항이 있다면, 관계 대수로는 1) 학생 테이블에서 ‘학과’가 ‘컴퓨터공학’인 학생들을 먼저 찾고(선택 연산), 2) 그 결과에서 ‘학년’이 ‘3’인 학생들을 다시 찾은 다음(선택 연산), 3) 최종 결과에서 ‘이름’과 ‘학번’ 열만 남기는(프로젝트 연산) 방식으로 해결 과정을 순서대로 서술합니다.

이러한 절차적 특성은 데이터베이스 관리 시스템(DBMS) 내부의 쿼리 실행 엔진이 SQL과 같은 비절차적 언어(사용자는 원하는 결과만 선언)를 어떤 순서로 처리할지 계획을 세우는 데 이론적 기반을 제공합니다. 사용자가 SQL로 “SELECT 이름, 학번 FROM 학생 WHERE 학과 = ‘컴퓨터공학’ AND 학년 = 3;”이라고 선언하면, DBMS의 쿼리 옵티마이저는 여러 가능한 관계 대수 실행 계획을 평가하여 가장 비용이 적게 드는 최적의 절차를 선택하여 실행하게 됩니다. 따라서 관계 대수는 보이지 않는 곳에서 데이터 검색의 효율성을 책임지는 핵심적인 이론이라 할 수 있습니다.

관계 대수의 연산자 분류

관계 대수의 연산자들은 크게 두 가지 그룹으로 나눌 수 있습니다. 첫 번째는 관계형 데이터베이스 모델을 위해 특별히 고안된 순수 관계 연산자(Pure Relational Operators)이고, 두 번째는 수학의 집합 이론에서 가져온 일반 집합 연산자(General Set Operators)입니다. 이 두 그룹의 연산자들이 조합되어 복잡한 데이터 검색 요구사항을 처리하게 됩니다.

  • 순수 관계 연산자:
    • 셀렉트 (Select, σ): 릴레이션에서 특정 조건을 만족하는 튜플(행)들을 수평적으로 추출합니다.
    • 프로젝트 (Project, π): 릴레이션에서 특정 속성(열)들만 수직적으로 추출합니다.
    • 조인 (Join, ⋈): 두 릴레이션을 공통된 속성을 기준으로 결합하여 새로운 릴레이션을 만듭니다.
    • 디비전 (Division, ÷): 한 릴레이션이 다른 릴레이션의 모든 튜플과 관계를 맺고 있는 튜플을 추출합니다.
  • 일반 집합 연산자:
    • 합집합 (Union, ∪): 두 릴레이션의 튜플을 모두 포함하는 릴레이션을 만듭니다. (단, 중복은 제거)
    • 차집합 (Difference, -): 첫 번째 릴레이션에는 속하지만 두 번째 릴레이션에는 속하지 않는 튜플을 추출합니다.
    • 교집합 (Intersection, ∩): 두 릴레이션에 공통으로 존재하는 튜플을 추출합니다.
    • 카티전 프로덕트 (Cartesian Product, ×): 두 릴레이션의 튜플들을 가능한 모든 조합으로 연결하여 새로운 릴레이션을 만듭니다.

이 연산자들은 하나 이상의 릴레이션을 입력으로 받아 반드시 하나의 릴레이션을 결과로 반환하는 ‘닫힘(Closure)’ 속성을 가집니다. 이 덕분에 연산의 결과를 다시 다른 연산의 입력으로 사용하는 중첩된 연산이 가능하며, 이를 통해 복잡한 쿼리를 단계적으로 구성할 수 있습니다.


순수 관계 연산자: 데이터베이스의 핵심 도구

셀렉트 (Select, σ) 연산: 원하는 행(Row)을 고르다

셀렉트 연산은 릴레이션에서 주어진 조건을 만족하는 튜플(행)들의 부분집합을 구하는 연산입니다. 마치 체로 원하는 것만 걸러내듯, 수많은 데이터 행 중에서 우리가 필요로 하는 특정 행들만 수평적으로 추출합니다. 기호로는 그리스 문자 시그마(σ)를 사용하며, σ 뒤의 아래첨자로 선택 조건을 기술하고 괄호 안에 대상 릴레이션을 명시합니다.

  • 표기법: σ<조건>(릴레이션)

예를 들어, 아래와 같은 <학생> 테이블에서 ‘컴퓨터공학’과 학생들을 찾고 싶다고 가정해 봅시다.

<학생>

| 학번 | 이름 | 학과 | 학년 |

| :— | :— | :— | :— |

| 1001 | 김철수 | 컴퓨터공학 | 3 |

| 1002 | 박영희 | 전기공학 | 4 |

| 1003 | 이민준 | 컴퓨터공학 | 2 |

| 1004 | 최유리 | 경영학 | 3 |

이때의 관계 대수식은 σ학과='컴퓨터공학'(학생) 이 됩니다. 이 연산의 결과는 다음과 같은 새로운 릴레이션입니다.

학번이름학과학년
1001김철수컴퓨터공학3
1003이민준컴퓨터공학2

SQL에서는 WHERE 절이 바로 이 셀렉트 연산에 해당합니다. SELECT * FROM 학생 WHERE 학과 = '컴퓨터공학'; 구문이 위의 관계 대수식과 동일한 역할을 수행합니다. 셀렉트 연산의 조건으로는 AND(∧), OR(∨), NOT(¬)과 같은 논리 연산자를 사용하여 복잡한 조건을 만들 수도 있습니다.

프로젝트 (Project, π) 연산: 원하는 열(Column)을 뽑다

프로젝트 연산은 릴레이션의 전체 속성(열) 중에서 특정 속성들만 선택하여 수직적으로 추출하는 연산입니다. 보고서에 필요한 특정 데이터 항목만 뽑아서 보여주는 것과 같습니다. 기호로는 그리스 문자 파이(π)를 사용하며, π 뒤의 아래첨자로 추출할 속성 리스트를 기술하고 괄호 안에 대상 릴레이션을 명시합니다.

  • 표기법: π<속성 리스트>(릴레이션)

앞선 예제의 <학생> 테이블에서 모든 학생의 ‘이름’과 ‘학과’ 정보만 보고 싶다고 가정해 봅시다. 이때의 관계 대수식은 π이름, 학과(학생) 입니다. 연산 결과는 다음과 같습니다.

이름학과
김철수컴퓨터공학
박영희전기공학
이민준컴퓨터공학
최유리경영학

프로젝트 연산의 중요한 특징 중 하나는 결과에서 중복된 행을 자동으로 제거한다는 것입니다. 만약 결과에 동일한 (이름, 학과) 쌍이 여러 개 존재한다면 하나만 남깁니다. SQL에서는 SELECT 절이 이 프로젝트 연산에 해당합니다. SELECT DISTINCT 이름, 학과 FROM 학생; 구문이 관계 대수의 프로젝트 연산과 가장 유사한 의미를 가집니다. (SQL의 일반 SELECT는 중복을 제거하지 않음)

조인 (Join, ⋈) 연산: 두 테이블을 합치다

조인 연산은 관계 대수에서 가장 중요하고 강력한 연산 중 하나로, 두 개 이상의 릴레이션을 공통된 속성을 기준으로 연결하여 하나의 새로운 릴레이션을 만드는 연산입니다. 흩어져 있는 관련 정보를 하나로 모으는 역할을 합니다. 기호로는 ⋈를 사용하며, 조인 조건에 따라 다양한 종류의 조인이 존재합니다. 가장 기본적인 조인은 동등 조인(Equi Join)과 자연 조인(Natural Join)입니다.

  • 표기법 (자연 조인): 릴레이션1 ⋈ 릴레이션2

예를 들어, <학생> 테이블과 아래의 <수강> 테이블이 있다고 가정해 봅시다.

<수강>

| 학번 | 과목코드 |

| :— | :— |

| 1001 | CS101 |

| 1002 | EE201 |

| 1003 | CS101 |

학생 ⋈ 수강 이라는 자연 조인 연산을 수행하면, 두 테이블에서 이름이 같은 속성(‘학번’)을 기준으로 값이 동일한 튜플들을 연결합니다. 결과 릴레이션에서는 공통 속성인 ‘학번’이 한 번만 나타납니다.

학번이름학과학년과목코드
1001김철수컴퓨터공학3CS101
1002박영희전기공학4EE201
1003이민준컴퓨터공학2CS101

SQL에서는 JOIN 절이 이 연산을 수행합니다. SELECT * FROM 학생 NATURAL JOIN 수강; 이 위와 동일한 결과를 반환합니다. 조인 연산 덕분에 우리는 데이터를 정규화하여 여러 테이블에 나누어 저장한 뒤, 필요할 때 다시 합쳐서 의미 있는 정보를 얻을 수 있습니다.


일반 집합 연산자: 수학적 원리의 적용

합집합, 차집합, 교집합: 테이블 간의 집합 연산

일반 집합 연산자들은 두 릴레이션을 수학의 집합(Set)으로 간주하고 연산을 수행합니다. 이 연산들을 적용하기 위해서는 두 릴레이션이 합병 가능(Union-compatible)해야 한다는 전제 조건이 따릅니다. 즉, 두 릴레이션의 속성(열) 개수가 같고, 대응되는 속성끼리 도메인(데이터 타입)이 같아야 합니다.

  • 합집합 (Union, ∪): 두 릴레이션의 튜플을 모두 합쳐서 보여줍니다. SQL의 UNION에 해당합니다.
  • 차집합 (Difference, -): 첫 번째 릴레이션에는 있지만 두 번째 릴레이션에는 없는 튜플을 보여줍니다. SQL의 EXCEPT 또는 MINUS에 해당합니다.
  • 교집합 (Intersection, ∩): 두 릴레이션에 공통으로 존재하는 튜플만 보여줍니다. SQL의 INTERSECT에 해당합니다.

예를 들어, ‘1학년 학생’ 릴레이션과 ‘동아리 회원’ 릴레이션이 있을 때, 두 릴레이션의 합집합은 1학년이거나 동아리 회원인 모든 학생의 목록이 되고, 교집합은 1학년이면서 동아리 회원인 학생들의 목록이 됩니다.

카티전 프로덕트 (Cartesian Product, ×): 모든 경우의 수 조합

카티전 프로덕트는 두 릴레이션에 속한 튜플들의 모든 가능한 조합을 결과로 반환하는 연산입니다. 결과 릴레이션의 차수(열 개수)는 두 릴레이션 차수의 합이 되고, 카디널리티(행 개수)는 두 릴레이션 카디널리티의 곱이 됩니다.

  • 표기법: 릴레이션1 × 릴레이션2

이 연산 자체는 의미 없는 데이터를 대량으로 생성할 수 있기 때문에 단독으로 쓰이는 경우는 드뭅니다. 하지만 다른 연산과 결합될 때 그 진가를 발휘합니다. 사실, 조인 연산은 카티전 프로덕트의 결과에서 특정 조건을 만족하는 튜플만 선택(Select)하는 연산(σ<조인조건>(R × S))으로 정의될 수 있습니다. SQL에서 FROM 테이블1, 테이블2 처럼 JOIN 조건을 생략하고 여러 테이블을 나열하면 이 카티전 프로덕트가 발생하므로 주의해야 합니다.


결론: 효율적인 데이터 여정을 위한 내비게이션

관계 대수의 중요성과 현대적 의의

관계 대수는 1970년대에 에드거 F. 커드(Edgar F. Codd)에 의해 제안된 이후, 지난 수십 년간 관계형 데이터베이스 기술의 이론적 뼈대를 굳건히 지켜왔습니다. 오늘날 우리가 사용하는 거의 모든 관계형 DBMS의 쿼리 처리기는 관계 대수의 원리를 기반으로 동작합니다. 사용자가 작성한 선언적인 SQL 쿼리는 내부적으로 파싱, 분석 과정을 거쳐 관계 대수 식으로 표현되는 논리적 쿼리 계획(Logical Query Plan)으로 변환됩니다. 그리고 쿼리 옵티마이저는 이 계획을 비용 기반으로 평가하여 가장 효율적인 물리적 실행 계획(Physical Execution Plan)으로 바꾸어 실행합니다.

따라서 관계 대수를 이해하는 것은 단순히 학문적 이론을 배우는 것을 넘어, 데이터베이스의 내부 동작을 이해하고 성능 병목 현상의 원인을 추론하며, 궁극적으로 더 나은 SQL 쿼리를 작성하는 능력으로 이어집니다. 예를 들어, 조인 순서나 인덱스 사용 여부에 따라 쿼리 성능이 크게 달라지는 이유를 관계 대수 연산의 비용 관점에서 설명할 수 있게 되는 것입니다.

복잡한 데이터 분석이나 ETL(Extract, Transform, Load) 파이프라인을 설계할 때도 관계 대수의 단계적이고 절차적인 사고방식은 매우 유용합니다. 원본 데이터에서 어떤 조건을 걸러내고(Select), 필요한 필드만 추출한 뒤(Project), 다른 데이터 소스와 결합(Join)하는 일련의 과정을 논리적으로 명확하게 설계할 수 있게 도와줍니다. 관계 대수는 SQL이라는 편리한 도구 뒤에 숨어 있는, 데이터 여정을 위한 가장 정확하고 신뢰할 수 있는 내비게이션과 같습니다. 이 내비게이션의 원리를 이해할 때, 우리는 데이터라는 광활한 세계를 더 빠르고 정확하게 탐험할 수 있을 것입니다.