MariaDB 강좌 / INNER JOIN으로 서로 연결된 데이터 조회하기
INNER JOIN은 연결 조건이 참인 두 테이블의 행 조합만 반환합니다. 어떤 열로 연결할지 ON에 명시하고, 결과 한 행이 고객인지 주문인지부터 정해야 일대다 관계에서 늘어난 행을 중복 오류로 오해하지 않습니다.
연결 조건과 결과의 단위 정하기
주문 화면에 고객 이름을 보여 주려면 주문의 customer_id와 고객의 기본키를 연결합니다. 두 테이블에 customer_id라는 같은 이름이 있어도 자동으로 올바르게 연결되는 것은 아닙니다. AS로 짧은 테이블 별칭을 정하고 각 열의 소속을 명시합니다. 연결 키의 근거는 테이블 관계 설계하기: 일대다·다대다·외래키의 관계 설계이며, 일치 행이 없는 기준 행까지 남길 때는 LEFT JOIN으로 연결되지 않은 행도 조회하기를 선택합니다.
| 요소 | 예 | 역할 |
|---|---|---|
| 주 테이블 | orders AS o | 주문 한 건을 기준으로 시작 |
| 연결 테이블 | customers AS c | 고객 이름 읽기 |
| ON 조건 | c.customer_id=o.customer_id | 관계 키를 대응 |
| WHERE 조건 | o.status='paid' | 반환할 주문의 업무 조건 |
고객·주문과 연결 데이터를 함께 준비하기
학습용 MariaDB 서버의 shopcourse에 course_user로 접속합니다. 명령줄 클라이언트와 UTF-8 SQL 파일 실행 방법대로 코드를 UTF-8 파일에 저장하고 SOURCE C:/mariadb-lab/lesson.sql;처럼 실제 파일 경로로 실행할 수 있습니다. SQL 문은 운영체제 터미널이 아닌 MariaDB 연결 안에서 실행합니다.
먼저 SELECT VERSION(), DATABASE();와 SHOW TABLES;로 대상 서버·DB·이름 중복을 확인합니다. 아래 c26_customers, c26_products, c26_orders, c26_items는 이 글의 독립된 연습용 이름입니다. 이미 존재하면 정의와 행을 확인하고, 보관할 자료가 없는 이 글의 실습 테이블임이 확실할 때만 마지막 절의 정리 순서로 지운 뒤 다시 만듭니다. 기존 강좌의 동명 없는 테이블이나 운영 데이터는 삭제하지 않습니다.
USE shopcourse; CREATE TABLE c26_customers ( customer_id INT NOT NULL PRIMARY KEY, customer_name VARCHAR(50) NOT NULL, city VARCHAR(30) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c26_products ( product_id INT NOT NULL PRIMARY KEY, product_name VARCHAR(100) NOT NULL, category VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL CHECK (price >= 0), stock INT NOT NULL CHECK (stock >= 0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c26_orders ( order_id INT NOT NULL PRIMARY KEY, customer_id INT NOT NULL, ordered_at DATETIME NOT NULL, status VARCHAR(20) NOT NULL, shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 1000, CONSTRAINT fk_c26_order_customer FOREIGN KEY (customer_id) REFERENCES c26_customers(customer_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c26_items ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), unit_price DECIMAL(10,2) NOT NULL CHECK (unit_price >= 0), PRIMARY KEY (order_id, product_id), CONSTRAINT fk_c26_item_order FOREIGN KEY (order_id) REFERENCES c26_orders(order_id) ON DELETE RESTRICT, CONSTRAINT fk_c26_item_product FOREIGN KEY (product_id) REFERENCES c26_products(product_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; INSERT INTO c26_customers VALUES (1,'김민수','서울'),(2,'이서연','부산'), (3,'박지우','서울'),(4,'최하늘','대전'); INSERT INTO c26_products VALUES (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50), (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5); INSERT INTO c26_orders (order_id,customer_id,ordered_at,status,shipping_fee) VALUES (101,1,'2026-01-10 10:00:00','paid',1000), (102,1,'2026-01-20 11:00:00','paid',1000), (103,2,'2026-02-05 12:00:00','paid',1000), (104,3,'2026-02-10 13:00:00','canceled',1000); INSERT INTO c26_items VALUES (101,1,2,3500),(101,2,3,1500),(102,3,1,8000), (103,1,1,3500),(103,3,2,8000),(104,2,1,1500);
실습 테이블 네 개를 완전히 준비하므로 다른 JOIN 강좌를 먼저 실행할 필요는 없습니다. 이 글은 customers·orders만 사용하고 나머지는 동일한 주문 데이터의 구조를 유지하기 위해 함께 제공합니다. 학습을 끝낸 뒤에는 이 글의 접두어 c26_ 객체만 정리합니다.
주문과 고객을 기본키로 연결하기
SELECT o.order_id,c.customer_id,c.customer_name,o.status FROM c26_orders AS o INNER JOIN c26_customers AS c ON c.customer_id=o.customer_id ORDER BY o.order_id;
| order_id | customer_id | 고객 | 상태 |
|---|---|---|---|
| 101 | 1 | 김민수 | paid |
| 102 | 1 | 김민수 | paid |
| 103 | 2 | 이서연 | paid |
| 104 | 3 | 박지우 | canceled |
김민수 이름이 두 번 보이는 것은 주문 두 건에 각각 대응하기 때문입니다. 결과 한 행은 고객 하나가 아니라 주문 하나입니다. 고객 목록 한 번씩만 필요하면 EXISTS나 목적에 맞는 집계를 검토하며 무조건 DISTINCT로 행을 숨기지 않습니다. MariaDB JOIN 문법에서 ON과 테이블 별칭의 구성을 확인하세요.
연결 조건과 필터 조건을 분리하기
SELECT o.order_id,c.customer_name,o.ordered_at FROM c26_orders AS o INNER JOIN c26_customers AS c ON c.customer_id=o.customer_id WHERE o.status='paid' ORDER BY o.order_id;
paid 주문은 101·102·103번이며 canceled인 104번이 빠집니다. ON은 어떤 고객과 주문이 연결되는지를, WHERE는 연결된 주문 중 어떤 상태를 보여 줄지를 드러냅니다. INNER JOIN에서는 조건을 이동해 같은 결과를 만들 수 있는 경우도 있지만 이후 OUTER JOIN으로 확장할 때 의미가 달라질 수 있으므로 역할을 구분하는 습관이 좋습니다.
양쪽의 customer_id를 모두 표시하면 연결 키가 맞는지 점검하기 쉽습니다. customer_name처럼 업무상 바뀔 수 있는 값으로 연결하거나, 서로 의미가 다른 번호를 이름만 같다는 이유로 연결하지 않습니다.
ON 조건이 빠진 곱집합과 행 증가 확인하기
SELECT COUNT(*) AS unconstrained_pairs FROM c26_customers AS c CROSS JOIN c26_orders AS o;
고객 4명과 주문 4건의 모든 조합은 16개입니다. CROSS JOIN은 모든 조합이 필요할 때 쓰는 기능이지만 잘못된 JOIN 조건으로 같은 결과를 만들면 각 주문에 엉뚱한 고객까지 연결됩니다. 실제 연결 JOIN의 4행과 비교해 보세요.
외래키가 있는 이 데이터에서는 모든 주문의 부모가 존재하므로 주문 기준 INNER JOIN에서 네 건이 모두 남습니다. 실제로 연결되지 않은 주문이 있을 수 있는 데이터에서는 INNER JOIN이 그 행을 제외하므로, 누락을 점검할 때는 LEFT JOIN과 NULL 검사를 사용해야 합니다.
모호한 열·잘못된 연결을 확인하고 반복하기
Column is ambiguous 오류가 나면 양쪽에 있는 customer_id 같은 열 앞에 o. 또는 c.를 붙입니다. 예상보다 행이 많으면 두 연결 열이 고유한지와 일대다 관계를, 너무 적으면 필터와 연결되지 않는 키를 확인합니다. 결과 정렬도 ORDER BY로 명시하고 원본 저장 순서에 의존하지 않습니다.
반복하기 전 현재 DB와 실제 정의·행을 확인하고 필요한 자료를 백업합니다. 보관할 데이터가 없는 이 글의 전용 실습 객체만 아래 순서로 정리합니다. 외래키가 있으면 자식부터 지웁니다. DROP TABLE은 구조와 데이터를 모두 없애며 일반적인 ROLLBACK으로 복원되지 않으므로 취소 가능한 DML 실습과 분리합니다.
DROP TABLE shopcourse.c26_items; DROP TABLE shopcourse.c26_orders; DROP TABLE shopcourse.c26_products; DROP TABLE shopcourse.c26_customers;










