Menu

SQLite INNER JOIN으로 여러 테이블 합치기

SQLite의 INNER JOIN 동작 방식부터 ON 절, 3개 테이블 조인, USING 단축 문법까지 한 번에 정리합니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

조인은 두 테이블을 하나로 엮어준다

관계형 데이터베이스는 데이터를 일부러 여러 테이블에 나눠 담는다. 고객은 고객 테이블에, 주문은 주문 테이블에, 상품은 상품 테이블에 따로 보관하는 식이다. 이렇게 해야 같은 사실이 한 곳에만 존재하니까. 그런데 막상 "어떤 고객이 무엇을 주문했는가?" 같은 실제 질문에 답하려면 흩어진 조각들을 다시 모아야 한다. 바로 이때 사용하는 것이 조인(join)이다.

SQLite에서 가장 많이 쓰이는 조인은 INNER JOIN이다. 두 테이블의 행을 지정한 조건에 맞춰 짝지어 주고, 매칭되지 않는 행은 모두 버린다.

고객은 세 명, 주문도 세 건이지만 Chen은 주문이 없습니다. 그래서 Chen은 결과에 나타나지 않죠. 이게 바로 inner의 핵심입니다. 매칭된 행만 살아남는다는 뜻이에요.

사고 모델: 행을 매칭한 뒤 걸러낸다

INNER JOIN은 이렇게 읽으면 됩니다. 첫 번째 테이블의 모든 행을 가져와 두 번째 테이블의 모든 행과 비교한 다음, ON 조건이 참인 쌍만 남긴다. 개념적으로는 거대한 교차곱을 만든 뒤 필터를 적용하는 것과 같습니다. 실제로 SQLite가 이렇게 동작하지는 않습니다(가능하면 인덱스를 활용하죠). 하지만 결과를 예측할 때는 이 모델로 생각하는 게 정확합니다.

여기서 같이 챙겨두면 좋은 습관 몇 가지:

  • 같은 테이블을 두 번 이상 언급할 거라면 별칭(customers AS c)을 붙이자. 쿼리가 한결 깔끔해진다.
  • 양쪽 테이블에 같은 이름의 컬럼이 있을 법하면 c.name, o.total처럼 컬럼명을 한정해서 써주자.
  • ON o.customer_id = c.id에서 좌우 순서는 상관없다. c.id = o.customer_id로 써도 결과는 똑같다.

INNER JOIN과 JOIN의 차이

SQLite에서도 표준 SQL과 마찬가지로, 그냥 JOIN이라고 쓰면 INNER JOIN을 뜻한다. INNER 키워드는 생략해도 된다.

두 방식 모두 동일한 실행 계획을 만들고, 결과 행도 같습니다. 다만 여러 종류의 JOIN이 섞여 있는 쿼리에서는 INNER JOIN이라고 명시하는 편이 가독성에 살짝 도움이 됩니다. 몇 줄 아래에 LEFT JOIN이 등장할 때 의도가 한눈에 들어오기 때문이죠.

ON 절 vs USING 절

조인할 컬럼명이 양쪽 테이블에서 동일하다면, ON a.col = b.col보다 USING (column)이 더 간결합니다:

USING (customer_id)는 두 가지 일을 한꺼번에 처리합니다. customer_id가 같은 행끼리 매칭하고, 동시에 해당 컬럼을 하나로 합쳐 결과에 한 번만 나타나게 해주죠. 양쪽 테이블에서 정말로 같은 컬럼명을 쓰고 있을 때 쓰면 좋습니다. 컬럼명이 다르거나(orders.customer_id = customers.id) 단순 동등 비교 이상의 조건이 필요하다면 ON을 그대로 쓰는 편이 낫습니다.

SQLite 3개 테이블 조인하기

조인을 이어 붙이려면 JOIN ... ON ... 구문을 계속 추가하면 됩니다. 새로 붙는 조인은 지금까지 만들어진 결과를 다음 테이블과 연결하는 역할을 합니다.

위에서 아래로 읽으면 됩니다. customers는 orders와 연결되고, orders는 items와 연결됩니다. 결과의 각 행은 고객–주문–상품 조합 하나를 나타내죠. 연결 고리 어디에서든 짝이 없는 행은 결과에서 빠집니다. 이게 바로 단계마다 적용되는 INNER JOIN의 원칙입니다.

WHERE로 결과 걸러내기

ON은 행을 어떻게 짝지을지를 정하고, WHERE는 짝지어진 결과를 걸러냅니다. INNER JOIN에 한해서는 추가 조건을 ON에 넣든 WHERE에 넣든 동일한 결과가 나옵니다. 다만 관례적으로 조인 조건은 ON에, 행 필터링은 WHERE에 두는 것이 좋습니다.

"고객과 주문을 조인한 다음, 영국 고객 중에서 주문 금액이 20을 넘는 것만 남긴다"라고 읽힙니다. 역할이 두 개니까 절도 두 개로 나누는 거죠. 나중에 코드를 다시 볼 미래의 내가 고마워할 겁니다. (LEFT JOIN을 쓰기 시작하면 ONWHERE의 차이는 단순히 스타일 문제가 아니게 됩니다. 그 얘기는 다음 페이지에서 다룹니다.)

ON 절에 여러 조건 넣기

ON 절에는 단순한 등호 비교 하나만이 아니라 어떤 불리언 표현식이든 들어갈 수 있습니다. 두 컬럼 이상으로 관계가 맺어져 있거나, 조인 시점에 오른쪽 테이블을 미리 걸러내고 싶을 때 유용합니다.

두 번째 조건이 만족되지 않기 때문에 취소된 주문은 결과에서 사라집니다. INNER JOIN에서는 WHERE o.status = 'paid'로 써도 같은 결과를 얻을 수 있는데요. ON 절에 조건을 두면 "무엇을 매칭으로 볼 것인가"라는 로직이 조인 바로 옆에 붙어 있어 가독성이 좋습니다.

자주 하는 실수

INNER JOIN을 쓸 때 흔히 헷갈리는 부분들을 정리해 봤습니다.

  • ON 절 빼먹기. SQLite에서 FROM a INNER JOIN b처럼 ON을 생략하면 문법 오류가 납니다. (다만 쉼표만 찍은 FROM a, b는 컴파일은 되지만 크로스 조인이 되어 버려서, 대부분의 경우 원하던 결과가 아닙니다.)
  • 예상치 못한 중복. 한 고객에게 주문이 3건이라면 결과에는 그 고객 이름이 3번 등장합니다. 버그가 아니라 조인의 정상 동작이에요. 고객당 한 행으로 보고 싶다면 GROUP BY로 집계하세요.
  • 사라진 행. 분명히 나와야 할 고객이 결과에 없다면 조인 조건이 맞지 않았다는 뜻입니다. 조인 컬럼에 NULL이 있는지 확인하거나, LEFT JOIN을 고려해 보세요.
  • 모호한 컬럼명. SELECT id FROM customers JOIN orders ON ...은 두 테이블 모두 id 컬럼이 있어서 오류가 발생합니다. c.ido.id처럼 테이블 별칭으로 명확히 지정해 주세요.

다음: LEFT JOIN

INNER JOIN은 매칭되지 않는 행을 그냥 버려도 괜찮을 때 잘 어울립니다. 하지만 주문이 한 건도 없는 고객까지 포함해서 모든 고객을 나열하고, 빠진 데이터 자리에는 NULL을 채우고 싶을 때도 있죠. 그럴 때 쓰는 게 바로 LEFT JOIN입니다. 다음 글에서 자세히 살펴보겠습니다.

자주 묻는 질문

SQLite에서 INNER JOIN은 정확히 어떤 동작을 하나요?

INNER JOINON 조건을 만족해서 양쪽 테이블 모두에 매칭되는 행만 반환합니다. 한쪽이라도 매칭이 없으면 그 행은 결과에서 제외됩니다. 참고로 SQLite에서 JOIN은 기본적으로 INNER JOIN과 동일하게 동작합니다.

INNER JOIN과 LEFT JOIN은 어떻게 다른가요?

INNER JOIN은 매칭되는 행만 남기는 반면, LEFT JOIN은 왼쪽 테이블의 모든 행을 유지하고 오른쪽에 매칭이 없으면 NULL로 채워줍니다. "매칭이 없으면 그냥 제외"하고 싶다면 INNER JOIN, "매칭이 없어도 일단 보여줘"라면 LEFT JOIN을 쓰면 됩니다.

SQLite에서 테이블 3개를 INNER JOIN할 수 있나요?

네, JOIN ... ON ... 구문을 계속 이어 붙이면 됩니다. 각 조인이 지금까지의 결과에 새 테이블을 연결하는 식이죠. 개수 제한은 없지만 테이블이 4~5개를 넘어가면 가독성이 급격히 떨어지기 때문에, 이럴 땐 CTE(WITH 절)로 분리하는 편이 훨씬 깔끔합니다.

ON 대신 USING은 언제 쓰면 좋나요?

USING (컬럼명)은 양쪽 테이블에서 조인할 컬럼 이름이 똑같을 때 쓰는 단축 문법입니다. 코드가 짧아지고 결과에서도 중복 컬럼이 하나로 합쳐집니다. 컬럼 이름이 다르거나 좀 더 복잡한 조건이 필요하면 ON을 사용하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기