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 강좌 / EXPLAIN으로 실행 계획 읽고 인덱스 전후 비교하기

MariaDB 강좌 / EXPLAIN으로 실행 계획 읽고 인덱스 전후 비교하기

1000행 실습으로 인덱스 전후 EXPLAIN과 실제 반환 행을 비교합니다. ANALYZE FORMAT=JSON의 실행 정보와 통계 갱신을 구분하고 결과·환경을 기록합니다.

MariaDB 강좌 / ALTER TABLE로 구조 변경하기: 열·제약 조건·변경 전 점검

MariaDB 강좌 / ALTER TABLE로 구조 변경하기: 열·제약 조건·변경 전 점검

ALTER TABLE을 새 코드 열 추가·기존 행 채우기·제약 적용으로 나누어 실습합니다. NULL·중복 사전 검사, MODIFY 속성 보존, 실제 실패 입력과 잠금·복구 계획을 설명합니다.

MariaDB / VIEW

MariaDB / VIEW

VIEW는 가상의 테이블입니다. 실제로 데이터를 저장하지 않고, 기존의 하나 이상의 테이블을 기반으로 한 쿼리 결과를 테이블처럼 사용할 수 있게 해줍니다. 뷰는 복잡한 쿼리를 단순화하고, 보안 및 데이터 관리에 유용하게 사용됩니다.

MariaDB / 연산자 / 비교 연산자

MariaDB / 연산자 / 비교 연산자

MariaDB의 비교 연산자에 대하여 알아보겠습니다. 예제로 사용하는 테이블은 아래의 데이터를 가진 T_1이라는 테이블입니다. 각 열의 데이터 형식은 다음과 같습니다. C_1 : 정수 C_2 : 문자 C_3 : 날짜 +----+------+------+------------+ | ID | C_1 | C_2 | C_3 | +----+------+------+------------+ | 1 | 1 | ...

MariaDB / 데이터베이스 저장 위치 변경하는 방법

MariaDB / 데이터베이스 저장 위치 변경하는 방법

MariaDB의 버전이 12.1이라고 할 때, 윈도우용 MariaDB의 기본 설치 위치는 C:\Program Files\MariaDB 12.1, 데이터베이스 저장 위치는 C:\Program Files\MariaDB 12.1\data입니다. 데이터베이스 저장 위치를 다음으로 변경해보겠습니다. D:\Database\MariaDB\data

MariaDB / 피벗 테이블 만드는 방법

MariaDB / 피벗 테이블 만드는 방법

피벗 테이블은 엑셀의 유용한 기능 중의 하나입니다. 예를 들어 아래의 왼쪽 데이터를 오른쪽 표처럼 만드는 것이 피벗 테이블입니다. 조건에 따라 그룹화시켜서 보기 좋게 데이터를 정리하는 것이라 보면 됩니다. MariaDB에서도 몇 가지 함수를 이용하여 피벗 테이블을 만들 수 있습니다.

MariaDB 강좌 / CREATE TABLE로 테이블 구조 정의하기

MariaDB 강좌 / CREATE TABLE로 테이블 구조 정의하기

상품 테이블을 예로 열·타입·기본값·기본키·저장 엔진을 정의하고 실제 생성 결과를 확인합니다. 기존 테이블 처리, IF NOT EXISTS의 한계와 안전한 반복 실습 방법도 설명합니다.

MariaDB / Query / LEFT JOIN, RIGHT JOIN, INNER JOIN

MariaDB / Query / LEFT JOIN, RIGHT JOIN, INNER JOIN

SQL의 JOIN은 여러 테이블을 한 번에 나타낼 때 사용합니다. 대표적인 JOIN 방식은 LEFT JOIN, RIGHT JOIN, INNER JOIN이 있습니다. 다음은 세 가지 JOIN의 차이를 알아보기 위한 간단한 예제 테이블입니다.

MariaDB 강좌 / 서버 설정·SQL 모드·시간대·로그 확인하기

MariaDB 강좌 / 서버 설정·SQL 모드·시간대·로그 확인하기

설정 파일·GLOBAL·SESSION의 차이를 기준으로 MariaDB 동작을 확인합니다. SQL 모드 오류와 TIMESTAMP·DATETIME 시간대 실습, 로그 조건 점검 및 변경 복구 절차를 제공합니다.

MariaDB 강좌 / 조회 조건·정렬·페이지 나누기를 개선하기

MariaDB 강좌 / 조회 조건·정렬·페이지 나누기를 개선하기

날짜 범위·정렬·키셋 페이지 조회를 같은 주문 데이터로 검증합니다. 함수 조건의 버전별 최적화, 동점 비교 방향, 포함 검색의 의미와 실제 계획·측정 기준을 설명합니다.