AD JB Toolbox 01

MariaDB 강좌 / LEFT JOIN으로 연결되지 않은 행도 조회하기

LEFT JOIN은 왼쪽 테이블의 행을 유지하면서 오른쪽의 일치 행을 연결하고, 일치가 없으면 오른쪽 열을 NULL로 반환합니다. 조건을 ON에 둘지 WHERE에 둘지에 따라 주문 없는 고객이 사라질 수 있으므로 유지하려는 기준 행을 먼저 정해야 합니다.

남겨야 하는 기준 테이블을 왼쪽에 두기

가입한 모든 고객과 주문 유무를 함께 보려면 고객을 왼쪽에 둡니다. 결과가 없는 주문 데이터를 만들거나 실제 주문을 NULL로 저장하는 것이 아니라 조회 결과에서 연결되지 않은 오른쪽 부분만 NULL이 됩니다. 고객 한 명의 여러 주문이 있으면 그 고객은 여러 결과 행으로 나타납니다. 일치 조합만 필요한 경우는 INNER JOIN으로 서로 연결된 데이터 조회하기과 비교하고, 특정 주문이 없는 고객만 고르는 목적은 서브쿼리·EXISTS·NOT EXISTS로 조건 표현하기의 NOT EXISTS로도 표현할 수 있습니다.

관심 대상 연결 기준 주의
전체 고객과 주문 customers LEFT JOIN orders 주문 없는 고객 유지
전체 주문과 고객 orders LEFT JOIN customers 유지하는 대상이 다름
paid 주문이 없는 고객 ON에 paid 조건 후 NULL 검사 취소 주문만 있는 고객도 포함
모든 상태의 주문이 없는 고객 상태 제한 없는 연결 후 NULL 검사 완전한 미주문 고객

주문 없는 고객을 포함한 데이터 준비하기

학습용 MariaDB 서버의 shopcourse에 course_user로 접속합니다. 명령줄 클라이언트와 UTF-8 SQL 파일 실행 방법대로 코드를 UTF-8 파일에 저장하고 SOURCE C:/mariadb-lab/lesson.sql;처럼 실제 파일 경로로 실행할 수 있습니다. SQL 문은 운영체제 터미널이 아닌 MariaDB 연결 안에서 실행합니다.

먼저 SELECT VERSION(), DATABASE();와 SHOW TABLES;로 대상 서버·DB·이름 중복을 확인합니다. 아래 c27_customers, c27_products, c27_orders, c27_items는 이 글의 독립된 연습용 이름입니다. 이미 존재하면 정의와 행을 확인하고, 보관할 자료가 없는 이 글의 실습 테이블임이 확실할 때만 마지막 절의 정리 순서로 지운 뒤 다시 만듭니다. 기존 강좌의 동명 없는 테이블이나 운영 데이터는 삭제하지 않습니다.

USE shopcourse;
CREATE TABLE c27_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 c27_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 c27_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_c27_order_customer FOREIGN KEY (customer_id)
  REFERENCES c27_customers(customer_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c27_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_c27_item_order FOREIGN KEY (order_id)
  REFERENCES c27_orders(order_id) ON DELETE RESTRICT,
 CONSTRAINT fk_c27_item_product FOREIGN KEY (product_id)
  REFERENCES c27_products(product_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO c27_customers VALUES
 (1,'김민수','서울'),(2,'이서연','부산'),
 (3,'박지우','서울'),(4,'최하늘','대전');
INSERT INTO c27_products VALUES
 (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50),
 (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5);
INSERT INTO c27_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 c27_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);

최하늘 고객 4번은 주문이 전혀 없고, 박지우 고객 3번은 취소 주문만 있습니다. 이 둘을 구분할 수 있도록 상태가 섞인 예제를 사용합니다. 쿼리 결과를 보기 전에 어떤 고객을 미구매자로 정의할지부터 정하세요.

오른쪽 NULL과 기준 행 유지 확인하기

SELECT c.customer_id,c.customer_name,o.order_id,o.status
FROM c27_customers AS c LEFT JOIN c27_orders AS o
 ON o.customer_id=c.customer_id ORDER BY c.customer_id,o.order_id;

김민수 두 행, 이서연 한 행, 박지우 한 행, 최하늘 한 행으로 총 다섯 행입니다. 최하늘의 주문 열만 NULL이며 고객 번호와 이름은 유지됩니다. 결과를 고객 네 명으로만 예상했다면 한 고객의 주문 여러 건이 각각 출력된다는 점을 확인합니다.

ON의 상태 조건과 WHERE의 차이 비교하기

SELECT c.customer_id,COUNT(o.order_id) AS paid_orders
FROM c27_customers AS c LEFT JOIN c27_orders AS o
 ON o.customer_id=c.customer_id AND o.status='paid'
GROUP BY c.customer_id ORDER BY c.customer_id;
customer_id paid_orders
1 2
2 1
3 0
4 0

paid 조건을 ON에 두었으므로 일치하는 paid 주문이 없는 고객도 남습니다. 같은 조건을 WHERE o.status='paid'로 옮기면 오른쪽 값이 NULL인 행은 조건을 통과하지 못하고, 취소 주문만 있는 고객도 제외됩니다. 고객 3·4를 0으로 보여 주려면 필터 위치를 유지해야 합니다. LEFT JOIN의 연결 구문을 함께 확인하세요.

같은 paid 조건을 WHERE로 옮긴 결과도 직접 비교할 수 있습니다. 아래 쿼리는 고객 1·2만 반환하므로 앞 표의 고객 3·4와 0건 결과를 보존하지 않습니다.

SELECT c.customer_id,COUNT(o.order_id) AS paid_orders
FROM c27_customers c LEFT JOIN c27_orders o
 ON o.customer_id=c.customer_id
WHERE o.status='paid'
GROUP BY c.customer_id ORDER BY c.customer_id;

필터 위치만 바꾸고 고객별 결과 행과 건수를 대조한 뒤, 전체 고객을 남길 목적이라면 앞의 ON 조건으로 되돌립니다.

COUNT(*) 대신 실제 오른쪽 키를 세기

주문 없는 고객에게도 LEFT JOIN 결과 행은 하나 존재하므로 COUNT(*)를 쓰면 1로 셀 수 있습니다. 오른쪽의 NOT NULL 기본키인 o.order_id를 세면 일치한 주문만 계산되어 0이 됩니다. 항상 존재하는 고객 키를 세는 COUNT(c.customer_id)도 이 목적에는 맞지 않습니다.

SELECT c.customer_id FROM c27_customers AS c
LEFT JOIN c27_orders AS o ON o.customer_id=c.customer_id
WHERE o.order_id IS NULL ORDER BY c.customer_id;

상태 조건 없이 모든 주문을 연결하고 o.order_id IS NULL로 찾으면 최하늘인 4번만 반환합니다. 같은 검사에서 ON에 paid 조건을 넣으면 3·4번이 반환됩니다. 오른쪽에서 원래 NULL을 허용하는 메모 열로 미일치 여부를 검사하면 실제 주문의 빈 메모와 혼동하므로 기본키를 사용합니다.

기준 행·필터 위치·집계 수를 점검하기

잘못된 결과를 DISTINCT로 숨기기 전에 기준 행을 유지하려는 JOIN인지와 상태 조건의 위치를 확인합니다. 오른쪽 열을 WHERE에서 비교하면 NULL을 제외하는지 살펴보고, 결과 한 행이 고객인지 주문인지 명확히 합니다. 설명한 결과가 나오지 않으면 고객·주문의 초기 키와 상태부터 조회하세요.

반복하기 전 현재 DB와 실제 정의·행을 확인하고 필요한 자료를 백업합니다. 보관할 데이터가 없는 이 글의 전용 실습 객체만 아래 순서로 정리합니다. 외래키가 있으면 자식부터 지웁니다. DROP TABLE은 구조와 데이터를 모두 없애며 일반적인 ROLLBACK으로 복원되지 않으므로 취소 가능한 DML 실습과 분리합니다.

DROP TABLE shopcourse.c27_items;
DROP TABLE shopcourse.c27_orders;
DROP TABLE shopcourse.c27_products;
DROP TABLE shopcourse.c27_customers;
같은 카테고리의 다른 글
MariaDB 강좌 / 데이터베이스 만들기: CREATE DATABASE·USE·문자 집합

MariaDB 강좌 / 데이터베이스 만들기: CREATE DATABASE·USE·문자 집합

shopcourse 데이터베이스를 만들고 현재 연결의 작업 대상으로 선택합니다. utf8mb4와 collation, 기존 DB에서 IF NOT EXISTS의 한계, 권한·문자 설정 확인을 예제로 설명합니다.

MariaDB 강좌 / 날짜와 시간 저장하기: DATE·DATETIME·TIMESTAMP

MariaDB 강좌 / 날짜와 시간 저장하기: DATE·DATETIME·TIMESTAMP

DATE·DATETIME·TIMESTAMP의 저장 의미와 시간대 처리를 비교합니다. 동일 입력의 한국 시간·UTC 조회 결과, 버전별 TIMESTAMP 범위, 날짜 오류와 설정 복원을 실습합니다.

MariaDB 강좌 / 숫자 계산과 타입 변환: ROUND·CAST

MariaDB 강좌 / 숫자 계산과 타입 변환: ROUND·CAST

ROUND와 CAST를 할인 금액·정확한 소수·숫자 문자열로 실습합니다. 반올림 시점, 앞자리 0 손실, 잘림 경고와 SQL 모드에 따른 저장 동작의 차이를 설명합니다.

MariaDB 강좌 / 윈도 함수로 순번과 순위 계산하기

MariaDB 강좌 / 윈도 함수로 순번과 순위 계산하기

고객별 금액의 ROW_NUMBER·RANK·DENSE_RANK를 비교하고 지역별 순위와 바깥 조회의 상위 N개 필터를 실습합니다. 공동 순위와 행 개수 제한의 차이를 결과로 확인합니다.

MariaDB 강좌 / 로컬·원격 접속과 연결 오류 해결하기

MariaDB 강좌 / 로컬·원격 접속과 연결 오류 해결하기

로컬 TCP 연결을 명시하고 서버 정보·현재 계정·실제 테이블 권한을 확인합니다. 연결 거절·인증·DB 오류를 나누어 진단하고 원격 수신 주소·계정·TLS의 적용 조건을 설명합니다.

MariaDB 강좌 / 두 연결로 격리 수준·잠금·교착 상태 확인하기

MariaDB 강좌 / 두 연결로 격리 수준·잠금·교착 상태 확인하기

별도 연결 A·B의 실행 순서대로 격리 수준, 행 잠금 대기와 교착 상태를 비교합니다. 초기화·재조회·오류 후 정리와 연결별 설정 복원까지 전체 코드로 확인합니다.

MariaDB 강좌 / mariadb-dump로 데이터베이스 백업하기

MariaDB 강좌 / mariadb-dump로 데이터베이스 백업하기

별도 주문 실습 DB를 준비해 mariadb-dump로 새 SQL 백업 파일을 만듭니다. 종료 코드·해시·문자 인코딩, InnoDB 스냅샷과 동시 DDL의 한계, 객체·권한·별도 복원 검증 기준을 설명합니다.

MariaDB 강좌 / 조회 종합 실습: 월별 매출·고객별 구매·미판매 상품

MariaDB 강좌 / 조회 종합 실습: 월별 매출·고객별 구매·미판매 상품

월별 paid 매출, 미구매 포함 고객별 구매 금액, 미판매 상품 조회를 종합 실습합니다. 주문·상세 단위와 취소 정책을 정하고 세 결과의 전체 금액을 교차 검증합니다.

MariaDB 강좌 / Docker로 MariaDB 실습 환경 만들기

MariaDB 강좌 / Docker로 MariaDB 실습 환경 만들기

Compose 파일로 MariaDB 포트·암호 파일·데이터 볼륨·헬스체크를 구성합니다. 연결 확인과 컨테이너 교체 실습을 통해 데이터 유지, 초기화와 접속 오류의 차이를 이해합니다.

MariaDB 강좌 / CASE로 조건별 값과 집계 만들기

MariaDB 강좌 / CASE로 조건별 값과 집계 만들기

CASE 조건의 순서와 ELSE를 상품 재고 상태로 실습합니다. SUM·COUNT 안의 조건부 집계, 0과 NULL의 차이, WHERE로 그룹을 없애는 경우와 0을 표시하는 경우를 설명합니다.