조인의 결과를 시각적으로 이해하기 위해, 다음과 같은 샘플 데이터를 가진 nano 테이블과 banana 테이블이 존재한다고 가정합니다.
nano 테이블 (좌측)
banana 테이블 (우측)
SELECT n.tech_name, b.brand FROM nano n INNER JOIN banana b ON n.banana_code = b.banana_code;B01, B02 데이터만 결합됩니다.SELECT n.tech_name, b.brand FROM nano n LEFT JOIN banana b ON n.banana_code = b.banana_code;nano 테이블은 무조건 다 나오고, 매핑 안 되는 Robot-Nano는 우측 값이 NULL로 채워집니다.SELECT n.tech_name, b.brand FROM nano n RIGHT JOIN banana b ON n.banana_code = b.banana_code;banana 테이블이 기준이 되므로, 매핑 안 되는 DelMonte가 포함되고 좌측은 NULL이 됩니다.-- Oracle/PostgreSQL 기준 문법
SELECT n.tech_name, b.brand FROM nano n FULL OUTER JOIN banana b ON n.banana_code = b.banana_code;SELECT n.tech_name, b.brand FROM nano n CROSS JOIN banana b;-- banana 테이블 내에 상위 바나나(parent_code) 개념이 있다고 가정한 쿼리
SELECT b1.brand AS 하위바나나, b2.brand AS 상위바나나
FROM banana b1 JOIN banana b2 ON b1.parent_code = b2.banana_code;-- 두 테이블에 공통으로 존재하는 'banana_code' 컬럼을 알아서 찾아서 조인
SELECT tech_name, brand FROM nano NATURAL JOIN banana;-- 각 nano 행별로 우측 서브쿼리(최고가 banana 가격 계산 등)를 동적으로 호출하여 결합
SELECT n.tech_name, b_sub.brand
FROM nano n, LATERAL (SELECT * FROM banana b WHERE b.banana_code = n.banana_code) b_sub;
실무에서 대다수의 RDBMS가 조인을 처리할 때 사용하는 기본 알고리즘은 Nested Loops Join(중첩 루프 조인)입니다. 이는 자바의 이중 for문과 유사하게 작동합니다.
for(Row nano : nano_table) { // 1. 외부에 노출된 드라이빙 테이블
for(Row banana : banana_table) { // 2. 내부에서 맞춰보는 드리븐 테이블
if(nano.code == banana.code) { 결합; }
}
}WHERE 조건에 의해 필터링되어 최종 결과 행의 수가 더 적은 테이블이 드라이빙 테이블로 선택되어야 이중 루프의 바깥쪽 횟수가 줄어듭니다.ON n.banana_code = b.banana_code)에서 드리븐 테이블 쪽 컬럼(b.banana_code)에는 반드시 인덱스가 생성되어 있어야 합니다. 인덱스가 있어야만 바깥에서 던져주는 키 값을 받아 $O(1)$ 또는 $O(\log N)$으로 즉시 데이터를 찾아 엮을 수 있습니다.두 개 이상의 컬럼을 묶어서 하나의 PK로 지정하는 방식입니다.
장점: * 비즈니스적 관계성(예: 주문번호 + 상품순번) 자체를 물리적 유일성으로 강제할 수 있어 데이터 정합성이 완벽해집니다.
복합키로 지정된 순서대로 정렬 인덱스가 빌드되므로, 해당 컬럼들이 동시에 WHERE 절에 조건으로 들어오는 다중 조건 쿼리의 조회 속도가 극대화됩니다.
인덱스 크기 비대화: InnoDB 구조상 모든 보조 인덱스(Secondary Index)들은 실제 데이터의 위치 주소 대신 PK 값을 주소록으로 품고 있습니다. 따라서 PK가 복합키로 인해 무거워지면 해당 테이블에 생성되는 다른 모든 보조 인덱스의 크기도 함께 커져 버퍼 풀(Buffer Pool) 메모리를 과도하게 낭비하게 됩니다.
유지보수성 저하: JPA 같은 ORM 프레임워크를 다룰 때 복합키를 처리하기 위해 별도의 식별자 클래스(@IdClass 또는 @EmbeddedId)를 계속 선언해 주어야 하므로 개발 공수와 코드가 복잡해집니다.
실무 대규모 트래픽 환경이나 현대적인 마이크로서비스 아키텍처(MSA)에서는 데이터베이스 스키마 상에 물리적인 FK 제약조건을 명시적으로 선언하지 않는 경우가 대다수입니다. 설계도(ERD) 상에는 선으로 관계가 연결되어 있지만, 실제 DB를 조회해 보면 외래 키 제약조건이 한 개도 없는 상태를 묵시적 ERD 구조라고 부릅니다.
정규화는 관계형 데이터베이스 설계에서 데이터의 중복을 최소화하고 무결성을 극대화하기 위해 테이블을 구조화하는 프로세스입니다. 하나의 거대한 테이블을 상호 연관성이 있는 작은 테이블들로 분할해 나가는 과정이며, 비즈니스 규칙에 따른 함수적 종속성을 기반으로 수행됩니다.
실무 및 엔터프라이즈 설계에서는 대개 1정규형부터 3정규형 또는 BCNF까지를 필수 단계로 다룹니다.
비정규화는 정규화를 거쳐 완벽하게 분리된 데이터베이스 아키텍처에서, 시스템의 조회(Read) 성능을 극대화하기 위해 의도적으로 데이터의 중복을 허용하거나 테이블을 다시 병합하는 최적화 기법입니다. 정규화 규칙을 몰라서 무작정 짜는 형태와 달리, 철저히 정규화된 상태를 바탕으로 트래픽 병목을 해결하기 위해 수행하는 역방향 설계입니다.
현대 백엔드 개발 및 데이터베이스 모델링은 이 두 개념의 팽팽한 줄다리기 속에서 최적의 균형점을 찾는 과정입니다.
현대 대규모 트래픽 환경에서는 무조건적인 정규화나 무분별한 비정규화 모두 장애의 원인이 됩니다. 따라서 다음과 같은 단계적 전략을 취합니다.
선(先) 정규화, 후(後) 비정규화 원칙: 최초 설계 단계에서는 데이터 정합성을 위해 3정규형 수준까지 철저하게 분리합니다.
조회 병목 구간 탐색: 시스템 운영 중 특정 어드민 대시보드나 메인 화면 쿼리에서 다중 조인으로 인해 응답 속도가 떨어지는 병목을 모니터링(APM 툴, Slow Log 등)합니다.
컬럼 중복: 주문 목록을 보여줄 때 매번 회원 테이블을 조인하지 않도록, 주문 테이블에 '회원명' 컬럼을 중복하여 들고 있게 합니다.
파생/통계 컬럼 추가: 특정 게시글의 댓글 수를 조회하기 위해 매번 COUNT() 쿼리를 날리지 않고, 게시글 테이블에 reply_count 컬럼을 두고 댓글이 달릴 때마다 +1 해주는 방식을 씁니다.
NoSQL 캐싱 기술과의 융합: RDBMS 자체를 비정규화하면 정합성 리스크가 너무 크기 때문에, 최근 아키텍처에서는 RDBMS는 정규화 상태를 유지하고 비정규화된 조회 전용 데이터는 Redis나 Elasticsearch 같은 외부 저장소로 이관하여 해결하는 폴리글랏 퍼시스턴스 방식으로 보완합니다.
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.