icon

안동민 개발노트

4장 : SQL 기초 — DML

SELECT 기본


SELECT는 데이터를 조회하는 명령이며, 개발자가 가장 자주 사용하는 SQL입니다. SELECT 구문의 구조를 정확히 이해하면 복잡한 쿼리도 체계적으로 작성할 수 있습니다.

전체 SQL 작업 중 SELECT 쿼리가 차지하는 비율은 약 7080%에 달합니다. 나머지 INSERT, UPDATE, DELETE를 합쳐도 2030%에 불과합니다. 그만큼 SELECT는 데이터베이스 활용의 핵심입니다.


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는 거의 마지막에 실행됩니다.

SQL 실행 순서
작성 순서        실행 순서 (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
-- 모든 컬럼, 모든 행 조회
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 * vs 컬럼 명시
┌─────────────────────────────────────────────────────────┐
│ 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: 범위 검색

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: 목록 검색

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: 패턴 매칭

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 '%노트북%' → 인덱스 사용 불가

인덱스를 사용하려면 LIKE 앞부분이 고정되어야 합니다.
전문 검색이 필요하면 Full-Text Index를 사용합니다.

IS NULL / IS NOT NULL

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을 반환합니다.

NULL의 3가지 의미
1. 값이 없음 (Unknown) — 학생의 전화번호를 모름
2. 해당 없음 (Not Applicable) — 미혼인 사람의 배우자 이름
3. 아직 입력하지 않음 (Not Yet) — 아직 채점하지 않은 시험 점수

NULL과 연산

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, 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: 중복 제거

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 표현식
-- 검색형 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, NVL, IFNULL
-- 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와 형변환
-- 표준: 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으로 분리 (각각 인덱스 활용 가능)
WHERE 조건 작성 원칙
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 기본 핵심 요약
┌────────────────────────────────────────────────────────┐
│ 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                            │
└────────────────────────────────────────────────────────┘

다음 절에서는 결과의 정렬과 제한을 다루겠습니다.

목차