MariaDB 강좌 / 서브쿼리·EXISTS·NOT EXISTS로 조건 표현하기
서브쿼리는 다른 SQL 문 안에서 계산 결과나 행 집합을 제공하는 조회입니다. EXISTS·NOT EXISTS는 연결된 행의 존재 여부를 검사하므로, 주문이 있는 고객을 고객당 한 번 고르거나 paid 주문이 없는 고객을 찾을 때 유용합니다.
값·목록·존재 중 무엇이 필요한지 정하기
평균 가격보다 비싼 상품은 숫자 하나가 필요하고, paid 주문이 있는 고객은 해당 주문의 존재 여부만 필요합니다. 서브쿼리를 쓴다는 이유만으로 모든 문제를 같은 형태로 해결하지 않습니다. 결과 모양과 바깥 쿼리의 한 행 단위를 먼저 정합니다. NOT IN과 NULL의 조합은 NULL을 올바르게 비교하기: IS NULL·COALESCE에서 확인할 수 있습니다. 조건의 존재 여부를 고르는 일과 결과 목록을 세로로 합치는 UNION·UNION ALL로 조회 결과 합치기는 목적이 다릅니다.
| 형태 | 기대 결과 | 중요 조건 |
|---|---|---|
| 스칼라 서브쿼리 | 한 열·한 행의 값 | 여러 행이면 오류 |
| IN 서브쿼리 | 값 목록 | NULL 포함 여부 확인 |
| EXISTS | 일치 행이 있는지 | 바깥 키와 연결 조건 필요 |
| NOT EXISTS | 일치 행이 없는지 | 무엇의 부재인지 상태 조건 정의 |
고객과 주문이 다른 사례를 준비하기
학습용 MariaDB 서버의 shopcourse에 course_user로 접속합니다. 명령줄 클라이언트와 UTF-8 SQL 파일 실행 방법대로 코드를 UTF-8 파일에 저장하고 SOURCE C:/mariadb-lab/lesson.sql;처럼 실제 파일 경로로 실행할 수 있습니다. SQL 문은 운영체제 터미널이 아닌 MariaDB 연결 안에서 실행합니다.
먼저 SELECT VERSION(), DATABASE();와 SHOW TABLES;로 대상 서버·DB·이름 중복을 확인합니다. 아래 c29_customers, c29_products, c29_orders, c29_items는 이 글의 독립된 연습용 이름입니다. 이미 존재하면 정의와 행을 확인하고, 보관할 자료가 없는 이 글의 실습 테이블임이 확실할 때만 마지막 절의 정리 순서로 지운 뒤 다시 만듭니다. 기존 강좌의 동명 없는 테이블이나 운영 데이터는 삭제하지 않습니다.
USE shopcourse; CREATE TABLE c29_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 c29_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 c29_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_c29_order_customer FOREIGN KEY (customer_id) REFERENCES c29_customers(customer_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c29_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_c29_item_order FOREIGN KEY (order_id) REFERENCES c29_orders(order_id) ON DELETE RESTRICT, CONSTRAINT fk_c29_item_product FOREIGN KEY (product_id) REFERENCES c29_products(product_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; INSERT INTO c29_customers VALUES (1,'김민수','서울'),(2,'이서연','부산'), (3,'박지우','서울'),(4,'최하늘','대전'); INSERT INTO c29_products VALUES (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50), (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5); INSERT INTO c29_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 c29_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);
고객 1은 paid 주문 두 건, 고객 2는 paid 주문 한 건, 고객 3은 canceled 주문만, 고객 4는 주문이 없습니다. JOIN처럼 주문별 행을 반환하는 대신 고객당 한 번만 반환해야 하는 상황으로 비교하겠습니다.
한 값의 서브쿼리로 평균 가격과 비교하기
SELECT product_id,product_name FROM c29_products WHERE price > (SELECT AVG(price) FROM c29_products) ORDER BY product_id;
상품 가격 평균은 9500이고 이를 넘는 스탠드 4번만 반환됩니다. AVG는 조건에 맞는 상품 집합을 한 값으로 요약합니다. 스칼라 위치에 여러 상품 가격을 그대로 반환하는 서브쿼리를 쓰면 둘 이상의 행이 반환된다는 오류가 납니다. 무조건 LIMIT 1로 감추기보다 어떤 값을 비교하려는지 정해야 합니다.
EXISTS로 고객당 한 번만 선택하기
SELECT c.customer_id,c.customer_name FROM c29_customers c WHERE EXISTS (SELECT 1 FROM c29_orders o WHERE o.customer_id=c.customer_id AND o.status='paid') ORDER BY c.customer_id;
고객 1·2가 각각 한 번 반환됩니다. 안쪽 SELECT 1은 존재 여부를 검사하기 위한 값이며 주문 금액을 계산하는 뜻이 아닙니다. 고객 1의 주문 두 건이 있어도 바깥 고객 행이 두 번 늘어나지 않습니다. o.customer_id=c.customer_id가 빠지면 전체 데이터에 paid 주문이 하나라도 있는지만 검사하게 됩니다.
상관 서브쿼리는 바깥 행의 키를 참조합니다. 이해를 위한 논리적 설명과 실제 실행 계획은 다를 수 있으므로 무조건 바깥 행 수만큼 전체 테이블을 재검색한다고 단정하지 않습니다. EXISTS와 NOT EXISTS 공식 설명을 참고하세요.
NOT EXISTS로 원하는 조건의 부재 확인하기
SELECT c.customer_id,c.customer_name FROM c29_customers c WHERE NOT EXISTS (SELECT 1 FROM c29_orders o WHERE o.customer_id=c.customer_id AND o.status='paid') ORDER BY c.customer_id;
paid 주문이 없는 고객은 3·4번입니다. 취소 주문만 있는 고객과 주문 자체가 없는 고객을 모두 포함합니다. 모든 상태의 주문이 없는 고객만 필요하면 상태 조건을 제거하고, 그 경우 4번만 반환됩니다. 없는 것의 정의를 문장으로 적으면 필터를 잘못 넣는 일을 줄일 수 있습니다.
NOT IN은 안쪽 목록에 NULL이 섞이면 일반 비교가 알 수 없는 결과가 되어 예상한 행이 빠질 수 있습니다. NOT EXISTS와 무조건 동치라고 가정하지 말고, 비교 열의 NULL 허용 여부와 바깥 NULL 행을 포함할지 판단합니다. EXISTS를 썼다고 모든 쿼리가 JOIN보다 빠르다는 보장도 없습니다.
서브쿼리의 반환 형태와 연결 키 점검하기
안쪽 조회를 먼저 따로 실행해 값·목록·중복·NULL을 확인하고 바깥 행의 예시 하나와 연결해 봅니다. 결과가 모두 같으면 상관 조건이 빠지지 않았는지 살펴보세요. 단순한 존재 판정에 필요 없는 열·정렬을 넣지 않고, 성능은 실제 데이터와 실행 계획으로 평가합니다.
반복하기 전 현재 DB와 실제 정의·행을 확인하고 필요한 자료를 백업합니다. 보관할 데이터가 없는 이 글의 전용 실습 객체만 아래 순서로 정리합니다. 외래키가 있으면 자식부터 지웁니다. DROP TABLE은 구조와 데이터를 모두 없애며 일반적인 ROLLBACK으로 복원되지 않으므로 취소 가능한 DML 실습과 분리합니다.
DROP TABLE shopcourse.c29_items; DROP TABLE shopcourse.c29_orders; DROP TABLE shopcourse.c29_products; DROP TABLE shopcourse.c29_customers;









