AD JB Toolbox 01

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;
같은 카테고리의 다른 글
MariaDB 강좌 / NULL·기본값·NOT NULL·CHECK로 입력 조건 정하기

MariaDB 강좌 / NULL·기본값·NOT NULL·CHECK로 입력 조건 정하기

NULL·빈 문자열·0·입력 생략을 구분하고 DEFAULT·NOT NULL·CHECK의 역할을 실습합니다. nullable 열의 CHECK 예외, 실패 입력, 제약 이름과 설정 복원 방법을 설명합니다.

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

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

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

MariaDB 강좌 / NULL을 올바르게 비교하기: IS NULL·COALESCE

MariaDB 강좌 / NULL을 올바르게 비교하기: IS NULL·COALESCE

NULL·빈 문자열·0을 구분하고 IS NULL·IS NOT NULL과 COALESCE의 역할을 실습합니다. 일반 비교에서 빠지는 NULL 행, NULL-safe 비교와 NOT IN 목록의 함정을 설명합니다.

MariaDB / 샘플 데이터베이스 employees 설치하는 방법

MariaDB / 샘플 데이터베이스 employees 설치하는 방법

데이터베이스를 다루는 공부를 할 때 데이터가 들어있는 데이터베이스가 있으면 좋습니다. 데이터베이스를 직접 만드는 방법도 있지만, 공개된 샘플 데이터베이스를 이용하면 편합니다. 예를 들어 SQL Server라면 AdventureWorks라는 샘플 데이터베이스를 마이크로소프트에서 제공합니다. MariaDB라면 employees를 사용하면 됩니다.

MariaDB 강좌 / INNER JOIN으로 서로 연결된 데이터 조회하기

MariaDB 강좌 / INNER JOIN으로 서로 연결된 데이터 조회하기

고객과 주문을 INNER JOIN으로 연결하고 결과 한 행의 의미를 확인합니다. ON과 WHERE, 테이블 별칭, 일대다로 늘어나는 행과 조건 없는 곱집합을 비교합니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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