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 강좌 / WHERE로 조건 지정하기: 비교·AND·OR·IN·BETWEEN·LIKE

MariaDB 강좌 / WHERE로 조건 지정하기: 비교·AND·OR·IN·BETWEEN·LIKE

상품 가격·분류·재고·이름으로 WHERE 조건을 실습합니다. AND·OR 괄호 차이, IN 목록, BETWEEN 경계, LIKE 와일드카드와 실제 퍼센트 검색을 예상 행 번호로 확인합니다.

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

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

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

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

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

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

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

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

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

MariaDB / 사용자 관리하는 방법

MariaDB / 사용자 관리하는 방법

MariaDB에서 사용자를 만드는 방법, 수정하는 방법, 삭제하는 방법, 권한을 부여하고 제거하는 방법을 알아봅니다.

MariaDB 강좌 / 윈도 함수로 누적 합계와 이전·다음 행 비교하기

MariaDB 강좌 / 윈도 함수로 누적 합계와 이전·다음 행 비교하기

누적 합계와 LAG·LEAD를 계산하고 동점 데이터로 ROWS·RANGE를 비교합니다. 날짜 필터를 누적 전후에 적용한 결과와 정렬·기간 경계를 확인합니다.

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

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

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

MariaDB / VIEW

MariaDB / VIEW

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

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

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

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

MariaDB 강좌 / Ubuntu에 MariaDB 설치하고 서버 실행하기

MariaDB 강좌 / Ubuntu에 MariaDB 설치하고 서버 실행하기

Ubuntu APT 설치 후 서버 실행·자동 시작·관리자 소켓 인증을 확인합니다. 로컬 TCP 학습 계정을 만들고 패키지 버전, 인증 오류와 서비스 로그를 확인하는 절차를 설명합니다.