4장 : SQL 기초 — DML
SELECT 기본
SELECT는 데이터를 조회하는 명령이며, 개발자가 가장 자주 사용하는 SQL입니다. SELECT 구문의 구조를 정확히 이해하면 복잡한 쿼리도 체계적으로 작성할 수 있습니다.
전체 SQL 작업 중 SELECT 쿼리가 차지하는 비율은 약 7080%에 달합니다. 나머지 INSERT, UPDATE, DELETE를 합쳐도 2030%에 불과합니다. 그만큼 SELECT는 데이터베이스 활용의 핵심입니다.
SELECT 구문 구조
SELECT [DISTINCT] 컬럼 또는 표현식 -- 5. 출력할 컬럼 선택
FROM 테이블 -- 1. 대상 테이블 결정
[WHERE 조건] -- 2. 행 필터링
[GROUP BY 컬럼] -- 3. 그룹화
[HAVING 조건] -- 4. 그룹 필터링
[ORDER BY 컬럼 [ASC|DESC]] -- 6. 정렬
[LIMIT 숫자 [OFFSET 숫자]] -- 7. 결과 제한작성 순서 vs 실행 순서
SQL은 작성 순서와 실행 순서가 다릅니다. FROM이 가장 먼저 실행되고, SELECT는 거의 마지막에 실행됩니다.
작성 순서 실행 순서 (DBMS 내부)
──────── ────────────────────
SELECT → 1. FROM : 어떤 테이블에서?
FROM → 2. WHERE : 어떤 행을?
WHERE → 3. GROUP BY : 어떻게 묶어서?
GROUP BY → 4. HAVING : 그룹 중 어떤 것을?
HAVING → 5. SELECT : 어떤 컬럼을?
ORDER BY → 6. DISTINCT : 중복 제거
LIMIT → 7. ORDER BY : 어떤 순서로?
8. LIMIT : 몇 개까지?이 순서를 알면 다음 질문에 답할 수 있습니다.
Q: WHERE에서 별칭(AS)을 쓸 수 없는 이유는?
A: WHERE(2번)이 SELECT(5번)보다 먼저 실행되므로, 별칭이 아직 없음
Q: HAVING에서 집계 함수를 쓸 수 있는 이유는?
A: HAVING(4번)은 GROUP BY(3번) 이후 실행되어 그룹이 이미 형성됨
Q: ORDER BY에서 별칭을 쓸 수 있는 이유는?
A: ORDER BY(7번)는 SELECT(5번) 이후 실행되므로 별칭이 존재함가장 간단한 SELECT
-- 모든 컬럼, 모든 행 조회
SELECT * FROM products;
-- 특정 컬럼만 조회
SELECT name, price FROM products;
-- 상수, 계산식도 가능
SELECT name, price, price * 1.1 AS 부가세포함가격 FROM products;
-- 테이블 없이도 가능 (DBMS에 따라)
SELECT 1 + 1; -- PostgreSQL, MySQL
SELECT 1 + 1 FROM DUAL; -- Oracle (DUAL: 더미 테이블)
SELECT CURRENT_DATE; -- 오늘 날짜SELECT * 은 피해야 하는가
┌─────────────────────────────────────────────────────────┐
│ SELECT * 의 문제점 │
│ 1. 불필요한 컬럼까지 읽어 I/O 낭비 │
│ 2. 인덱스만으로 응답 가능한 쿼리도 테이블 접근 필요 │
│ 3. 테이블 구조 변경 시 애플리케이션 코드가 깨질 수 있음│
│ 4. 어떤 데이터를 사용하는지 코드에서 명확하지 않음 │
├─────────────────────────────────────────────────────────┤
│ SELECT * 이 괜찮은 경우 │
│ * 탐색적 조회: 데이터 구조를 파악할 때 │
│ * 임시 쿼리: 개발/디버깅 중 │
│ * 소규모 테이블: 컬럼이 몇 개 없을 때 │
└─────────────────────────────────────────────────────────┘
실무: 운영 코드에서는 필요한 컬럼만 명시적으로 나열
개발: 탐색 시에는 SELECT * 자유롭게 사용조건 검색 (WHERE)
WHERE 절은 FROM에서 가져온 행들 중에서 조건을 만족하는 행만 필터링합니다.
비교 연산자
-- 기본 비교
SELECT * FROM products WHERE price > 10000; -- 초과
SELECT * FROM products WHERE price >= 10000; -- 이상
SELECT * FROM products WHERE price < 50000; -- 미만
SELECT * FROM products WHERE price <= 50000; -- 이하
SELECT * FROM products WHERE category = '전자기기'; -- 같음
SELECT * FROM products WHERE stock != 0; -- 다름 (또는 <>)BETWEEN: 범위 검색
-- 가격이 10000 이상 50000 이하 (양쪽 포함)
SELECT * FROM products WHERE price BETWEEN 10000 AND 50000;
-- 동일한 표현
SELECT * FROM products WHERE price >= 10000 AND price <= 50000;
-- 날짜 범위
SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';BETWEEN은 양쪽 경계를 포함(이상, 이하)합니다. 날짜 검색에서 자주 사용됩니다.
IN: 목록 검색
-- 특정 값 목록에 포함되는지 확인
SELECT * FROM products
WHERE category IN ('전자기기', '도서', '식품');
-- 동일한 표현 (OR로 풀어쓰기)
SELECT * FROM products
WHERE category = '전자기기' OR category = '도서' OR category = '식품';
-- NOT IN: 목록에 없는 것
SELECT * FROM products
WHERE category NOT IN ('전자기기', '도서');IN은 값의 개수가 많을 때 OR보다 간결합니다. 서브쿼리와 결합하면 더 강력해집니다.
LIKE: 패턴 매칭
-- %: 0개 이상의 임의 문자
SELECT * FROM products WHERE name LIKE '삼성%'; -- '삼성'으로 시작
SELECT * FROM products WHERE name LIKE '%노트북%'; -- '노트북' 포함
SELECT * FROM products WHERE name LIKE '%Pro'; -- 'Pro'로 끝남
-- _: 정확히 1개의 임의 문자
SELECT * FROM products WHERE name LIKE '___'; -- 정확히 3글자
SELECT * FROM products WHERE name LIKE '_철_'; -- 가운데 '철'
-- 이스케이프: %나 _를 문자 그대로 검색
SELECT * FROM products WHERE name LIKE '%50\%%' ESCAPE '\';
-- '50%' 를 포함하는 이름 검색LIKE '삼성%' → 인덱스 사용 가능 (앞부분 고정)
LIKE '%노트북' → 인덱스 사용 불가 (앞부분 와일드카드)
LIKE '%노트북%' → 인덱스 사용 불가
인덱스를 사용하려면 LIKE 앞부분이 고정되어야 합니다.
전문 검색이 필요하면 Full-Text Index를 사용합니다.IS NULL / IS NOT NULL
-- NULL 검사는 반드시 IS NULL / IS NOT NULL 사용
SELECT * FROM products WHERE category IS NULL;
SELECT * FROM products WHERE category IS NOT NULL;
-- 주의: 아래는 항상 결과가 0행!
SELECT * FROM products WHERE category = NULL; -- 잘못됨!
SELECT * FROM products WHERE category != NULL; -- 잘못됨!NULL 비교 주의: WHERE category = NULL은 항상 거짓입니다. NULL은 "알 수 없는 값"이므로, NULL과의 모든 비교는 UNKNOWN을 반환합니다.
1. 값이 없음 (Unknown) — 학생의 전화번호를 모름
2. 해당 없음 (Not Applicable) — 미혼인 사람의 배우자 이름
3. 아직 입력하지 않음 (Not Yet) — 아직 채점하지 않은 시험 점수NULL과 연산
NULL + 100 = NULL (숫자 연산)
NULL || '안녕' = NULL (문자열 연결, Oracle)
NULL = NULL = UNKNOWN (비교 연산)
NULL != NULL = UNKNOWN (비교 연산)
NULL AND TRUE = UNKNOWN (논리 연산)
NULL OR TRUE = TRUE (OR만 예외적으로 TRUE 가능)논리 연산자
WHERE 절에서 여러 조건을 결합할 때 AND, OR, NOT을 사용합니다.
-- AND: 모든 조건을 만족
SELECT * FROM products
WHERE category = '전자기기' AND price < 100000;
-- OR: 하나라도 만족
SELECT * FROM products
WHERE category = '전자기기' OR category = '도서';
-- NOT: 조건의 부정
SELECT * FROM products WHERE NOT category = '식품';
SELECT * FROM products WHERE category != '식품'; -- 동일
-- 복합 조건 (괄호로 우선순위 명확하게!)
SELECT * FROM products
WHERE (category = '전자기기' OR category = '도서')
AND price < 50000;연산자 우선순위
NOT > AND > OR
예시
WHERE A OR B AND C
= WHERE A OR (B AND C) ← AND가 먼저!
의도가 (A OR B) AND C 이었다면?
→ WHERE (A OR B) AND C ← 괄호 필수!
권장: 복합 조건에서는 항상 괄호를 사용하여 의도를 명확히DISTINCT와 별칭
DISTINCT: 중복 제거
-- 중복 제거: 유일한 카테고리만 조회
SELECT DISTINCT category FROM products;
-- 여러 컬럼에 DISTINCT: 조합이 유일한 행
SELECT DISTINCT category, brand FROM products;
-- category와 brand의 조합이 같은 행은 하나만 남음
-- COUNT와 DISTINCT 결합
SELECT COUNT(DISTINCT category) AS 카테고리수 FROM products;별칭 (AS)
-- 컬럼 별칭
SELECT name AS 상품명, price AS 가격 FROM products;
-- AS 생략 가능
SELECT name 상품명, price 가격 FROM products;
-- 계산식에 별칭
SELECT name, price * 0.9 AS 할인가격 FROM products;
SELECT name, price * stock AS 총재고가치 FROM products;
-- 공백 포함 별칭 (따옴표 필요)
SELECT name AS "상품 이름", price AS "정가(원)" FROM products;
-- 테이블 별칭 (조인에서 필수)
SELECT p.name, p.price
FROM products p
WHERE p.stock > 0;조건식과 CASE
WHERE 절 외에도 SELECT 절에서 조건에 따라 다른 값을 출력할 수 있습니다.
-- 검색형 CASE: 조건식 사용
SELECT name, price,
CASE
WHEN price >= 100000 THEN '고가'
WHEN price >= 50000 THEN '중가'
WHEN price >= 10000 THEN '저가'
ELSE '초저가'
END AS 가격등급
FROM products;
-- 단순형 CASE: 단일 컬럼의 값 비교
SELECT name, category,
CASE category
WHEN '전자기기' THEN 'Electronics'
WHEN '도서' THEN 'Books'
WHEN '식품' THEN 'Food'
ELSE 'Other'
END AS category_en
FROM products;NULL 처리 함수
-- COALESCE: 첫 번째 NOT NULL 값 반환 (표준 SQL)
SELECT name, COALESCE(phone, email, '연락처 없음') AS contact
FROM users;
-- NVL: NULL이면 대체값 반환 (Oracle)
SELECT name, NVL(phone, '미등록') AS phone FROM users;
-- IFNULL: MySQL
SELECT name, IFNULL(phone, '미등록') AS phone FROM users;
-- NULLIF: 두 값이 같으면 NULL, 다르면 첫 번째 값
SELECT NULLIF(price, 0) AS safe_price FROM products;
-- price가 0이면 NULL 반환 (0으로 나누기 방지에 유용)산술 연산과 표현식
SELECT 절에서 직접 계산할 수 있습니다.
-- 컬럼 간 사칙연산
SELECT name,
price,
stock,
price * stock AS 총재고가치
FROM products;
-- 정수 나눗셈 주의 (DBMS에 따라 다름)
SELECT 7 / 2; -- PostgreSQL: 3 (정수 나눗셈)
SELECT 7 / 2; -- MySQL: 3.5000
SELECT 7.0 / 2; -- PostgreSQL: 3.5 (하나라도 실수면 실수)
SELECT 7 / 2.0; -- 3.5
-- 나머지, 절대값
SELECT MOD(7, 2); -- 1 (나머지)
SELECT ABS(-100); -- 100 (절대값)
SELECT ROUND(3.756, 1); -- 3.8 (소수점 첫째 자리까지)
SELECT CEIL(3.1); -- 4 (올림)
SELECT FLOOR(3.9); -- 3 (내림)문자열 함수
-- 길이
SELECT LENGTH('Hello'); -- 5 (PostgreSQL, MySQL)
SELECT LEN('Hello'); -- 5 (SQL Server)
-- 부분 문자열
SELECT SUBSTR('Hello World', 1, 5); -- 'Hello' (Oracle, PostgreSQL)
SELECT SUBSTRING('Hello World', 1, 5); -- 'Hello' (MySQL, SQL Server)
-- 대소문자 변환
SELECT UPPER('hello'); -- 'HELLO'
SELECT LOWER('HELLO'); -- 'hello'
-- 공백 제거
SELECT TRIM(' hello '); -- 'hello'
SELECT LTRIM(' hello'); -- 'hello'
SELECT RTRIM('hello '); -- 'hello'
-- 문자열 연결
SELECT CONCAT('Hello', ' ', 'World'); -- 'Hello World'
SELECT 'Hello' || ' ' || 'World'; -- 'Hello World' (Oracle, PostgreSQL)
-- 치환
SELECT REPLACE('Hello World', 'World', 'SQL'); -- 'Hello SQL'
-- 위치 찾기
SELECT POSITION('World' IN 'Hello World'); -- 7 (PostgreSQL)
SELECT INSTR('Hello World', 'World'); -- 7 (Oracle, MySQL)날짜 함수
-- 현재 날짜/시간
SELECT CURRENT_DATE; -- 2024-06-15
SELECT CURRENT_TIMESTAMP; -- 2024-06-15 14:30:00
SELECT NOW(); -- MySQL, PostgreSQL
-- 날짜 부분 추출
SELECT EXTRACT(YEAR FROM order_date) AS 주문년도
FROM orders;
SELECT EXTRACT(MONTH FROM order_date) AS 주문월
FROM orders;
-- 날짜 연산
SELECT order_date + INTERVAL '7' DAY AS 일주일후 -- PostgreSQL
FROM orders;
SELECT DATE_ADD(order_date, INTERVAL 7 DAY) AS 일주일후 -- MySQL
FROM orders;
-- 날짜 차이
SELECT AGE(NOW(), created_at) AS 경과시간 FROM users; -- PostgreSQL
SELECT DATEDIFF(NOW(), created_at) AS 경과일 FROM users; -- MySQL형변환 (CAST)
-- 표준: CAST
SELECT CAST(price AS VARCHAR(10)) FROM products;
SELECT CAST('123' AS INTEGER);
SELECT CAST(order_date AS VARCHAR(10)) FROM orders;
-- PostgreSQL 축약::
SELECT price::VARCHAR FROM products;
-- 암시적 형변환 경고
SELECT * FROM products WHERE price = '10000';
-- 문자열 '10000'이 자동으로 숫자로 변환됨
-- 인덱스가 무효화될 수 있으므로 명시적 형변환 권장WHERE 조건의 성능 고려
WHERE 절의 작성 방식에 따라 성능이 크게 달라집니다.
-- 1. 컬럼에 함수 적용 → 인덱스 무효화
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- 개선: WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
-- 2. 컬럼에 연산 적용 → 인덱스 무효화
SELECT * FROM products WHERE price * 1.1 > 50000;
-- 개선: WHERE price > 50000 / 1.1
-- 3. 타입 불일치 → 암시적 형변환으로 인덱스 무효화
SELECT * FROM users WHERE phone = 01012345678; -- phone이 VARCHAR인 경우
-- 개선: WHERE phone = '01012345678'
-- 4. OR 조건 → 인덱스 활용 어려움
SELECT * FROM products WHERE category = 'A' OR price > 50000;
-- 개선: UNION으로 분리 (각각 인덱스 활용 가능)1. 컬럼을 가공하지 않는다 (함수, 연산 적용 금지)
2. 데이터 타입을 맞춘다 (암시적 형변환 방지)
3. OR보다 IN을 사용한다 (옵티마이저가 더 잘 처리)
4. 부정 조건(!=, NOT IN)은 인덱스를 사용하지 못한다
5. 가능하면 범위 조건보다 등치 조건(=)을 앞에 배치한다실전 예제
-- 전자기기 카테고리에서
-- 재고가 있고 (stock > 0)
-- 가격이 10만원 이하인 상품을
-- 가격 높은 순으로 조회
SELECT name AS 상품명,
price AS 가격,
stock AS 재고,
price * stock AS 재고가치,
CASE
WHEN stock >= 100 THEN '충분'
WHEN stock >= 10 THEN '보통'
ELSE '부족'
END AS 재고상태
FROM products
WHERE category = '전자기기'
AND stock > 0
AND price <= 100000
ORDER BY price DESC;-- 활성 사용자 중에서
-- 이메일이 gmail인 사용자를
-- 가입일 최신 순으로 5명만 조회
SELECT user_id,
username,
email,
COALESCE(phone, '미등록') AS phone,
created_at AS 가입일
FROM users
WHERE status = 'active'
AND email LIKE '%@gmail.com'
AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 5;핵심 정리
┌────────────────────────────────────────────────────────┐
│ SELECT 실행 순서 │
│ FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY │
│ (작성 순서와 다름!) │
├────────────────────────────────────────────────────────┤
│ WHERE 조건 │
│ 비교: =, !=, >, <, >=, <= │
│ 범위: BETWEEN A AND B (양쪽 포함) │
│ 목록: IN (값1, 값2, ...) │
│ 패턴: LIKE '삼성%' (_ = 1글자, % = 0개 이상) │
│ NULL: IS NULL / IS NOT NULL (= NULL은 안 됨!) │
├────────────────────────────────────────────────────────┤
│ 논리 연산자: NOT > AND > OR (괄호로 명확하게) │
├────────────────────────────────────────────────────────┤
│ DISTINCT: 중복 제거 │
│ AS: 컬럼/테이블 별칭 │
│ CASE: 조건에 따른 값 분기 │
│ COALESCE: NULL 대체 │
├────────────────────────────────────────────────────────┤
│ 실무 규칙 │
│ SELECT *는 탐색용, 운영 코드에서는 컬럼 명시 │
│ 복합 조건에서 괄호 필수 │
│ NULL 비교는 반드시 IS NULL │
└────────────────────────────────────────────────────────┘다음 절에서는 결과의 정렬과 제한을 다루겠습니다.