Devin.KR

SQL 기본 조회 - SELECT·WHERE·ORDER BY

개발자KR 조회 4

이 장에서 배우는 것

지금까지는 데이터를 어떻게 설계할지 다뤘다. 이 장부터는 이미 만들어진 데이터베이스에서 원하는 데이터를 꺼내는 방법을 배운다. SQL 연구소의 온라인 서점 데이터베이스를 놓고, 회원 목록이나 도서 목록에서 조건에 맞는 행만 골라 원하는 순서로 보는 문장을 직접 써 본다. SELECT 문은 SQL에서 가장 자주 쓰는 문장이고, 이 장에서 익히는 WHERE와 ORDER BY의 감각은 뒤에 나오는 조인·집계·서브쿼리에서도 그대로 이어진다.

  • DDL·DML·DCL이 각각 무엇을 다루는 명령인지 구분한다
  • SELECT 문을 쓰는 순서와 데이터베이스가 실제로 처리하는 순서가 다르다는 것을 이해한다
  • WHERE 절에서 비교 연산자, LIKE, IN, BETWEEN, NULL 판정을 상황에 맞게 쓴다
  • ORDER BY와 LIMIT으로 결과의 순서와 개수를 조정한다
  • member, book 테이블에서 조건을 조합한 조회 문장을 스스로 작성한다

문제 상황

SQL 연구소 서점의 데이터팀에 입사한 지 얼마 안 된 신입 사원 입장에서 생각해 보자. 고객센터와 상품기획(MD) 팀은 하루에도 몇 번씩 "이런 회원 목록 좀 뽑아 주세요", "이 가격대 도서 좀 보여 주세요" 같은 요청을 보낸다. 지금까지는 이런 요청이 오면 표(엑셀)로 정리된 전체 데이터를 내려받아 필터 기능으로 하나하나 걸러 왔다. 회원이나 도서가 몇백 건이면 버틸 만하지만, 데이터가 수만 건을 넘어가면 필터를 걸고 정렬하는 데만 시간이 오래 걸리고, 다음 날 다시 요청이 오면 같은 작업을 처음부터 반복해야 한다.

이번 주에 들어온 요청은 두 가지다. 하나는 고객센터에서 온 것으로, GOLD 이상 등급 회원 중 지역 정보가 비어 있는 회원, 즉 온라인 전용으로 추정되는 회원을 따로 관리하고 싶다는 요청이다. 다른 하나는 MD 팀에서 온 것으로, 가격이 일정 범위 안에 있고 제목에 특정 키워드가 들어간 도서를 최근 출간순으로 몇 건만 보고 싶다는 요청이다. 데이터베이스에 SELECT 문 하나만 던지면 몇 초 안에 끝나는 일인데, 이 장에서 그 문장을 쓰는 법을 배운다.

DDL·DML·DCL, SQL 명령을 세 갈래로 나누기

SQL 명령은 하는 일에 따라 크게 세 갈래로 나뉜다. 표(테이블)의 구조 자체를 만들고 고치고 지우는 명령을 DDL(Data Definition Language)이라 부른다. CREATE TABLE로 book 테이블을 만들고, ALTER TABLE로 열을 추가하고, DROP TABLE로 테이블을 지우는 것이 여기 속한다. 표 안의 데이터를 넣고 고치고 지우고 읽는 명령은 DML(Data Manipulation Language)이라 부른다. INSERT, UPDATE, DELETE, 그리고 이 장의 주인공인 SELECT가 여기 속한다. 누가 어떤 테이블에 접근할 수 있는지 권한을 다루는 명령은 DCL(Data Control Language)이라 부르며, GRANT와 REVOKE가 대표적이다.

SELECT는 데이터를 바꾸지 않고 읽기만 하기 때문에, 일부 책에서는 SELECT만 따로 떼어 DQL(Data Query Language)이라고 부르기도 한다. 이 장에서는 굳이 이름을 더 나누지 않고, SELECT를 DML 중에서도 데이터를 조회하는 명령으로만 기억해 두면 충분하다. book 테이블이 이미 만들어져 있고 데이터도 들어 있다는 전제로, 이번 장은 DDL이나 DCL은 다루지 않고 오직 SELECT로 조회하는 방법에 집중한다.

DDL·DML·DCL이 다루는 대상
분류다루는 대상대표 명령이 장에서 다루는지
DDL테이블 구조 정의CREATE, ALTER, DROP다루지 않음
DML테이블 안의 데이터INSERT, UPDATE, DELETE, SELECTSELECT만 다룸
DCL접근 권한GRANT, REVOKE다루지 않음

SELECT 문의 작성 순서와 실행 순서

SELECT 문은 보통 SELECT, FROM, WHERE, ORDER BY, LIMIT 순서로 쓴다. 그런데 데이터베이스가 이 문장을 처리하는 순서는 쓰는 순서와 다르다. 데이터베이스는 먼저 FROM으로 어느 테이블에서 데이터를 가져올지 정하고, WHERE로 조건에 맞지 않는 행을 걸러낸 뒤, 그 결과에서 SELECT로 보여 줄 열만 고른다. 마지막으로 ORDER BY로 정렬하고 LIMIT으로 개수를 자른다. 즉 눈에 보이는 글 순서는 SELECT가 맨 앞이지만, 실제로는 FROM과 WHERE가 먼저 처리된다.

이 차이를 알아 두면 실수를 줄일 수 있다. 예를 들어 SELECT 절에서 price * 0.9 AS discounted처럼 별칭(alias)을 만들었다고 해도, WHERE 절은 SELECT보다 먼저 처리되기 때문에 WHERE discounted < 10000처럼 그 별칭을 바로 쓸 수 없다. WHERE 절에는 원래 열 이름인 price를 그대로 써야 한다. 반대로 ORDER BY는 SELECT 이후에 처리되므로 SELECT에서 만든 별칭을 ORDER BY에서는 사용할 수 있다.

SELECT 문은 SELECT부터 쓰지만 데이터베이스는 FROM과 WHERE부터 처리한다

조건과 정렬, WHERE·LIKE·IN·BETWEEN·NULL·ORDER BY·LIMIT

WHERE 절에는 =, <>, <, >, <=, >= 같은 비교 연산자와 AND, OR, NOT을 쓸 수 있다. 여러 값 중 하나와 같은지 확인할 때는 OR를 여러 번 쓰는 대신 IN을 쓰면 짧아진다. 예를 들어 grade = 'GOLD' OR grade = 'VIP'는 grade IN ('GOLD', 'VIP')로 줄일 수 있다. 값의 범위를 확인할 때는 BETWEEN을 쓰는데, price BETWEEN 12000 AND 25000은 12000과 25000을 포함한 범위를 뜻한다. 문자열의 일부만 맞는지 확인할 때는 LIKE를 쓰며, %는 길이에 상관없이 아무 문자열이나 대응하고 _는 정확히 한 글자에 대응한다. title LIKE '%입문%'은 제목 어딘가에 '입문'이 들어간 도서를 찾는다.

NULL은 값이 없다는 뜻이 아니라 '모른다' 또는 '해당 없음'을 뜻하는 특별한 상태다. member 테이블의 region처럼 값이 비어 있을 수 있는 열은 =로 비교할 수 없다. NULL과 무엇을 비교하든 결과는 참도 거짓도 아닌 '알 수 없음'이 되기 때문에, region이 비어 있는 행을 찾으려면 region = NULL이 아니라 region IS NULL을 써야 한다. 반대로 값이 채워진 행만 보려면 region IS NOT NULL을 쓴다.

region이 비어 있는 행은 region = NULL이 아니라 region IS NULL로 찾아야 한다

ORDER BY는 결과를 정렬하는 절이다. 기본은 오름차순(ASC)이며, 내림차순으로 보려면 열 이름 뒤에 DESC를 붙인다. 정렬 기준을 여러 개 쓸 수도 있는데, ORDER BY grade DESC, joined_on DESC처럼 쓰면 등급이 같은 회원끼리는 가입일이 최근인 순서로 다시 정렬한다. LIMIT은 결과 행의 개수를 제한하는 절로, LIMIT 5는 정렬이 끝난 결과에서 앞의 5건만 남긴다. ORDER BY 없이 LIMIT만 쓰면 어떤 5건이 나올지 보장되지 않으므로, 순서가 중요한 조회에서는 반드시 ORDER BY와 함께 써야 한다. 참고로 표준 SQL이나 오라클에서는 LIMIT 대신 FETCH FIRST 5 ROWS ONLY 같은 구문을 쓰기도 하지만, SQLite와 MySQL은 LIMIT을 그대로 쓴다.

완성 코드

앞서 나온 두 가지 요청과, 도서 데이터의 빈 값을 확인하는 조회까지 한 스크립트에 담았다. SQL 연구소 콘솔이나 sqlite3에서 그대로 실행할 수 있다.

-- SQL 연구소 온라인 서점: 이번 주 CS/MD 요청 조회 스크립트

-- 1) GOLD, VIP 등급이면서 지역 정보가 없는 회원 목록 (온라인 전용 CS 대상 후보)
SELECT id, email, name, grade, region
FROM member
WHERE grade IN ('GOLD', 'VIP')
  AND region IS NULL
ORDER BY joined_on DESC;

-- 2) 가격 12,000원~25,000원, 제목에 '입문'이 들어간 도서 중 최근 출간순 5건
SELECT id, title, price, published_on
FROM book
WHERE price BETWEEN 12000 AND 25000
  AND title LIKE '%입문%'
ORDER BY published_on DESC
LIMIT 5;

-- 3) 페이지 수가 등록되지 않은 도서 확인 (데이터 보정 후보)
SELECT id, title, pages
FROM book
WHERE pages IS NULL
ORDER BY id
LIMIT 10;

줄별 해설

  • 첫 번째 문장은 member 테이블에서 시작한다. WHERE grade IN ('GOLD', 'VIP')는 등급이 GOLD 또는 VIP인 행만 남기고, AND region IS NULL은 그중에서도 지역 정보가 비어 있는 행만 다시 걸러낸다. 두 조건을 AND로 묶었으므로 둘 다 만족해야 결과에 남는다.
  • ORDER BY joined_on DESC는 가입일이 최근인 회원부터 보여 준다. SELECT 절에서 id, email, name, grade, region 다섯 열만 골랐지만, ORDER BY에는 SELECT에 없는 joined_on을 써도 된다. ORDER BY는 원본 테이블의 어떤 열이든 기준으로 삼을 수 있기 때문이다.
  • 두 번째 문장은 book 테이블에서 price BETWEEN 12000 AND 25000으로 가격 범위를 정하고, title LIKE '%입문%'로 제목에 '입문'이라는 글자가 들어간 도서만 남긴다. 두 조건 모두 AND로 묶여 있으므로 가격과 제목 조건을 동시에 만족해야 한다.
  • ORDER BY published_on DESC LIMIT 5는 출간일이 늦은(최근인) 도서부터 정렬한 뒤 앞의 5건만 잘라낸다. LIMIT은 ORDER BY로 정렬이 끝난 결과에 적용되므로, 이 순서를 지켜야 '최근 5건'이라는 의도가 정확히 반영된다.
  • 세 번째 문장은 WHERE pages IS NULL로 pages 값이 등록되지 않은 도서를 찾는다. 전자책이나 예약판매 도서처럼 아직 쪽수가 확정되지 않은 항목을 데이터 보정 후보로 뽑을 때 쓰는 조회다. ORDER BY id LIMIT 10은 id 순으로 최대 10건만 확인한다.

실행 결과

아래는 sqlite3 콘솔에서 .headers on, .mode column을 설정한 뒤 위 스크립트를 실행했다고 가정한 예시 결과다. 실제 값은 서점 데이터의 내용에 따라 달라진다.

-- 1) GOLD/VIP 등급이면서 지역 정보가 없는 회원
id   email               name    grade  region
---  ------------------  ------  -----  ------
203  grace@example.com   그레이스  GOLD
177  hoon@example.com    이훈     VIP

-- 2) 가격 12000~25000원, 제목에 '입문' 포함, 최근 출간순 5건
id   title                     price  published_on
---  ------------------------  -----  ------------
591  SQL 입문                   18000  2024-03-10
430  관계형 데이터베이스 입문      21000  2023-11-02
508  파이썬 데이터 분석 입문      16500  2023-08-21
299  통계학 입문                 13000  2023-05-14
112  프로그래밍 입문             24000  2023-02-01

-- 3) 페이지 수가 등록되지 않은 도서 (일부만 표시)
id   title              pages
---  -----------------  -----
14   초판 미확정 도서 A
27   전자책 전용 에세이
35   예약판매 도서

실무에서 자주 틀리는 것

region = NULL로 빈 값을 찾으려는 실수

NULL은 값이 아니라 '모름'이므로 =로 비교하면 항상 알 수 없음이 되어 결과가 아예 나오지 않는다.

-- 틀린 코드: 결과가 0건으로 나온다
SELECT id, email FROM member WHERE region = NULL;

-- 고친 코드
SELECT id, email FROM member WHERE region IS NULL;

LIKE에서 % 위치를 빠뜨리는 실수

%를 앞뒤에 붙이지 않으면 정확히 그 글자와 완전히 같은 값만 찾게 되어, 제목 일부에 키워드가 들어간 도서를 놓친다.

-- 틀린 코드: title이 정확히 '입문'인 도서만 찾는다
SELECT id, title FROM book WHERE title LIKE '입문';

-- 고친 코드: 제목 어딘가에 '입문'이 들어간 도서를 찾는다
SELECT id, title FROM book WHERE title LIKE '%입문%';

ORDER BY 없이 LIMIT만 써서 최신순이라 착각하는 실수

ORDER BY가 없으면 데이터베이스가 어떤 순서로 행을 돌려줄지 보장하지 않는다. '최근 5건'처럼 순서가 의미 있는 요청에는 반드시 정렬 기준을 함께 써야 한다.

-- 틀린 코드: 최신순이라는 보장이 없다
SELECT id, title, published_on FROM book LIMIT 5;

-- 고친 코드
SELECT id, title, published_on FROM book
ORDER BY published_on DESC
LIMIT 5;

AND와 OR을 섞을 때 괄호를 빠뜨리는 실수

AND는 OR보다 먼저 계산된다. 괄호 없이 섞어 쓰면 의도한 것과 다른 조건이 만들어질 수 있다.

-- 틀린 코드: grade = 'VIP'인 행은 region 조건과 상관없이 모두 포함된다
SELECT id, grade, region FROM member
WHERE grade = 'VIP' OR grade = 'GOLD' AND region IS NULL;

-- 고친 코드: 괄호로 의도한 조건을 명확히 한다
SELECT id, grade, region FROM member
WHERE (grade = 'VIP' OR grade = 'GOLD') AND region IS NULL;

한눈에 보기

WHERE·ORDER BY·LIMIT에서 자주 쓰는 구문
구문의미예시
IN여러 값 중 하나와 같은지 확인grade IN ('GOLD', 'VIP')
BETWEEN두 값을 포함한 범위 확인price BETWEEN 12000 AND 25000
LIKE문자열 일부가 맞는지 확인(%,_)title LIKE '%입문%'
IS NULL / IS NOT NULL값이 비어 있는지/채워졌는지 확인region IS NULL

연습 문제

  1. price가 10,000원 미만이거나 30,000원을 초과하는 도서의 title, price를 price 오름차순으로 조회하는 문장을 작성하라.
  2. email이 'naver.com'으로 끝나고 grade가 SILVER, GOLD, VIP 중 하나인 회원의 id, email, grade, region을 joined_on 내림차순으로 조회하는 문장을 작성하라. region이 NULL인 회원도 결과에 포함되는지 확인하라.
  3. 2023-01-01부터 2023-12-31 사이에 출간되었고 title에 '개론'이 들어간 도서를 published_on 오름차순으로 최대 3건만 조회하는 문장을 작성하라.
  4. pages 값이 등록되지 않은 도서를 id 오름차순으로 조회하여 데이터 보정이 필요한 도서 후보 목록을 만드는 문장을 작성하라.

정답과 해설

1번

SELECT title, price
FROM book
WHERE price < 10000 OR price > 30000
ORDER BY price ASC;

두 조건을 OR로 묶었으므로 둘 중 하나만 만족해도 결과에 포함된다. price는 하나의 값만 가지므로 10,000원 미만 조건과 30,000원 초과 조건이 동시에 참이 되는 행은 없어 괄호 없이 써도 의도한 결과가 나온다.

2번

SELECT id, email, grade, region
FROM member
WHERE email LIKE '%naver.com'
  AND grade IN ('SILVER', 'GOLD', 'VIP')
ORDER BY joined_on DESC;

email LIKE '%naver.com'은 naver.com으로 끝나는 이메일을 찾는다. region 조건을 따로 걸지 않았으므로 region이 NULL인 회원도 grade와 email 조건만 만족하면 결과에 그대로 포함된다. NULL은 WHERE에서 값을 제외하는 조건을 걸지 않는 한 자동으로 빠지지 않는다.

3번

SELECT id, title, published_on
FROM book
WHERE published_on BETWEEN '2023-01-01' AND '2023-12-31'
  AND title LIKE '%개론%'
ORDER BY published_on ASC
LIMIT 3;

BETWEEN은 양 끝값을 포함하므로 2023-01-01과 2023-12-31에 출간된 도서도 결과에 포함된다. ORDER BY를 published_on ASC로 두어야 '가장 이른 3건'이라는 의도가 LIMIT 3과 맞아떨어진다.

4번

SELECT id, title, pages
FROM book
WHERE pages IS NULL
ORDER BY id ASC;

pages = NULL로는 어떤 행도 찾을 수 없으므로 반드시 IS NULL을 써야 한다. LIMIT을 지정하지 않았으므로 조건에 맞는 도서가 모두 나오며, id 순으로 정렬해 두면 데이터 보정 작업을 순서대로 처리하기 편하다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.