C-1: SELECT 첫걸음 — 종이 설계를 진짜 SQL로
목차 29
안녕하세요, 홍순구 튜터입니다. 지난 시간까지 우리는 인스타그램 설계도를 종이 위에서 완성했어요. 회원·게시물·좋아요·팔로우를 표로 나누고, 키로 가리키고, 정규화로 잘게 쪼개고, 마지막엔 트랜잭션·NULL·조인의 원리까지 봤죠. 그런데 그 설계도는 아직 '그림'이에요. 실제로 동작하는 표는 하나도 없었어요.
오늘부터 그 그림이 화면에서 살아 움직여요. CREATE TABLE로 회원·게시물 표를 진짜로 만들고, SELECT로 데이터를 꺼내 보고, WHERE로 원하는 행만 골라내요. 지난 시간에 이름만 봤던 IS NULL도 오늘 직접 써 봐요. SQL이라는 언어의 첫걸음이라 어렵지 않으니 편하게 따라오세요.
오늘의 여정
① 명령어 지도 — SQL 은 네 갈래 (DDL · DML · TCL · DCL)
② CREATE TABLE — 종이 설계도를 진짜 표로 (회원 · 게시물)
③ SELECT · FROM — 표에서 데이터를 꺼내 보기
④ 다듬기 — 별칭 · 이어 붙이기(||) · 중복 제거(DISTINCT)
⑤ WHERE — 원하는 행만 (비교 · 논리 · 우선순위)
⑥ 범위·목록·패턴 — BETWEEN · IN · LIKE
⑦ IS NULL — 빈칸을 찾는 법 (지난 시간 NULL 의 실전)
⑧ ORDER BY — 결과를 줄 세우기
💡 오늘 수업의 핵심 — "종이에 그린 표를 CREATE TABLE로 만들고, SELECT·WHERE로 원하는 데이터만 꺼낸다"
🎯 학습 목표
- SQL이 DDL·DML·TCL·DCL 네 갈래로 나뉜다는 걸 알고,
CREATE TABLE로 회원·게시물 표를 실제로 만든다. (SQLD 2과목 'SQL 기본 — 명령어 분류·SELECT') SELECT·FROM·WHERE로 원하는 열과 행을 골라내고, 비교·논리 연산자와 연산자 우선순위를 이해한다.BETWEEN·IN·LIKE·IS NULL로 범위·목록·패턴·빈칸을 조건으로 걸고,ORDER BY로 결과를 정렬한다.
Step 1: "SQL이라는 언어의 지도 — DDL·DML·TCL·DCL"
본격적으로 표를 만들기 전에, SQL이라는 언어가 어떻게 생겼는지 지도부터 한 장 펼칠게요. SQL은 하나의 큰 명령어 덩어리가 아니에요. 하는 일에 따라 네 갈래로 나뉘어요. 이 분류는 SQLD에 꾸준히 나오니 이름과 역할을 짝지어 기억해 두세요. ★빈출
SQL 은 네 갈래
DDL (Data Definition Language) 표의 '틀'을 만든다
CREATE · ALTER · DROP · TRUNCATE
→ 오늘 Step 2 에서 CREATE TABLE 로 표를 만든다
DML (Data Manipulation Language) 표 안의 '데이터'를 다룬다
SELECT · INSERT · UPDATE · DELETE
→ 오늘은 SELECT(조회) 를 집중해서 본다
TCL (Transaction Control Language) 작업을 확정/취소한다
COMMIT · ROLLBACK · SAVEPOINT
→ 지난 시간에 본 트랜잭션이 바로 이 갈래
DCL (Data Control Language) 권한을 주고 뺏는다
GRANT · REVOKE
각 갈래를 한 줄로 정리하면 이래요. DDL은 표라는 그릇 자체를 만들고 바꾸고 없애요. DML은 그 그릇에 데이터를 넣고(INSERT) 꺼내고(SELECT) 고치고(UPDATE) 지워요(DELETE). TCL은 지난 시간에 배운 트랜잭션을 확정(COMMIT)하거나 되돌려요(ROLLBACK). DCL은 "누가 이 표를 볼 수 있는가" 같은 권한을 다뤄요.
⚠️ 함정 —
DELETE와TRUNCATE는 둘 다 데이터를 지우는 것 같지만 갈래가 달라요.DELETE는 행을 골라서 지우는 DML이고,TRUNCATE는 표를 통째로 비우는 DDL이에요. DDL이라 자동으로 확정(COMMIT)돼서ROLLBACK으로 되돌릴 수 없어요. "DELETE는 DML, TRUNCATE는 DDL"은 SQLD 단골 함정이에요.
오늘 우리가 쓸 명령어는 딱 두 갈래예요. Step 2에서 표를 만드는 DDL(CREATE TABLE), 그다음부터 끝까지는 데이터를 꺼내 보는 DML(SELECT)이에요.
💡 한 줄 정리
SQL은 표의 틀을 만드는 DDL, 데이터를 다루는 DML, 작업을 확정·취소하는 TCL, 권한을 다루는 DCL 네 갈래로 나뉜다.
🙋 학생 질문 — "튜터님, SELECT는 데이터를 바꾸지도 않는데 왜 DML이에요?"
좋은 의문이에요. SELECT는 데이터를 읽기만 하고 바꾸진 않아서, 어떤 책에서는 SELECT만 따로 떼어 DQL(Data Query Language)이라고 부르기도 해요. 하지만 SQLD 시험이나 실무에서는 보통 SELECT를 데이터를 '다루는' 명령으로 보고 DML에 함께 넣어요. 그래서 "DML = SELECT·INSERT·UPDATE·DELETE"로 외워 두면 충분해요. DQL이라는 이름이 보기에 나와도 당황하지 말고 "SELECT를 따로 부른 거구나" 하고 넘기면 돼요.
Step 2: "종이 설계를 진짜 표로 — CREATE TABLE"
이제 지도를 봤으니 첫 번째 갈래, DDL을 써 볼 차례예요. 지난 시간까지 종이 위에 그렸던 회원(Member)·게시물(Post) 표를 진짜로 만들어요. 표를 만드는 명령은 CREATE TABLE이에요.
-- sql/ddl/01_member_post.sql
-- 회원 테이블
CREATE TABLE member (
member_id NUMBER PRIMARY KEY, -- 회원 식별 번호 (인조 식별자)
username VARCHAR2(30) NOT NULL UNIQUE, -- 영문 아이디 (로그인용 · 중복 불가)
nickname VARCHAR2(30) NOT NULL, -- 한글 표시 이름
bio VARCHAR2(150) -- 자기소개 (값이 없을 수 있음 → NULL 허용)
);
-- 게시물 테이블
CREATE TABLE post (
post_id NUMBER PRIMARY KEY, -- 게시물 식별 번호 (인조 식별자)
member_id NUMBER NOT NULL -- 글쓴이 (회원 번호)
REFERENCES member(member_id), -- 외래키: member 를 가리킨다
caption VARCHAR2(500), -- 게시물 글 (#해시태그 포함)
created_at DATE DEFAULT SYSDATE NOT NULL -- 작성 시각
);
한 줄씩 뜯어볼게요. 먼저 컬럼 이름 옆에 붙은 게 데이터 타입이에요. 칸에 어떤 종류의 값이 들어갈지를 정해요.
NUMBER— 숫자.member_id,post_id같은 번호.VARCHAR2(30)— 가변 길이 문자. 괄호 안 숫자는 최대 길이예요.VARCHAR2(30)이면 최대 30글자.DATE— 날짜와 시각.
그다음 타입 뒤에 붙은 게 제약조건이에요. "이 칸은 이런 규칙을 지켜야 한다"는 약속이죠. 지난 시간 설계에서 정한 규칙을 여기서 실제로 거는 거예요.
| 제약조건 | 뜻 | 어디에 걸었나 |
|---|---|---|
PRIMARY KEY |
주식별자 — 행을 유일하게 가리키는 칸 (중복·NULL 불가) | member_id, post_id |
NOT NULL |
반드시 값이 있어야 함 (빈칸 금지) | username, nickname, member_id |
UNIQUE |
값이 중복되면 안 됨 | username(아이디는 겹치면 안 되니까) |
REFERENCES |
외래키 — 다른 표의 주식별자를 가리킴 | post.member_id → member.member_id |
DEFAULT |
값을 안 넣으면 기본값을 채움 | created_at(안 넣으면 지금 시각) |
bio에는 아무 제약도 없죠? 자기소개는 안 쓴 회원이 있을 수 있으니 빈칸을 허용한 거예요. 지난 시간에 배운 NULL 허용이 바로 이거예요. 반대로 username은 NOT NULL UNIQUE라서 반드시 있어야 하고 겹쳐도 안 돼요. 아이디니까요.
post의 member_id에 걸린 REFERENCES member(member_id)가 지난 시간에 배운 외래키예요. "이 게시물의 글쓴이는 반드시 회원 표에 있는 회원이어야 한다"는 규칙을 DB가 직접 지켜 줘요.
⚠️ 함정 — 표를 만드는 순서가 중요해요.
post가member를 외래키로 가리키니, 가리킬 대상인member를 먼저 만들어야 해요. 지울 때는 반대로 자식인post를 먼저 지워요. 순서를 바꾸면 "참조 중이라 못 만든다/못 지운다" 오류가 나요.
이 CREATE TABLE 두 개를 실행하면, 종이 위에만 있던 회원 표와 게시물 표가 데이터베이스 안에 진짜로 생겨요. 빈 그릇 두 개가 준비된 거예요. 이제 여기에 데이터를 채우고 꺼내 볼 차례예요.
💡 한 줄 정리
CREATE TABLE은 컬럼마다 데이터 타입(NUMBER·VARCHAR2·DATE)과 제약조건(PRIMARY KEY·NOT NULL·UNIQUE·REFERENCES·DEFAULT)을 정해 표의 틀을 만드는 DDL 명령이다.
🙋 학생 질문 — "튜터님, NUMBER나 VARCHAR2는 길이를 왜 정해 줘야 하나요? 그냥 다 넣으면 안 되나요?"
길이를 정하는 건 일종의 안전장치예요. nickname VARCHAR2(30)이라고 해 두면, 누가 실수로(또는 일부러) 3만 글자짜리 닉네임을 넣으려 할 때 DB가 "30글자까지만 약속했잖아요" 하고 막아 줘요. 데이터의 모양을 미리 정해 두면 이상한 값이 들어오는 걸 입구에서 거를 수 있고, DB가 저장 공간도 더 효율적으로 잡을 수 있어요. 길이 제한은 불편이 아니라 데이터를 깨끗하게 지키는 약속이라고 생각하면 돼요.
Step 3: "조회의 시작 — SELECT · FROM"
표를 만들었으니 이제 데이터가 들어 있다고 해 볼게요. 우리 실습 데이터에는 회원 12명과 게시물 50개가 들어 있어요. 이 데이터를 꺼내 보는 명령이 바로 SELECT예요. SQL에서 가장 많이 쓰는 명령이죠.
가장 기본 형태부터 볼게요. "회원 표의 모든 것을 보여 줘"는 이렇게 써요.
SELECT * FROM member;
SELECT는 "골라서 보여 줘", FROM은 "어느 표에서", *(별표)는 "모든 컬럼"이라는 뜻이에요. 그래서 이 한 줄은 "회원 표에서 모든 컬럼을 보여 줘"가 돼요. 실행하면 회원 12명이 한 명도 빠짐없이 조회돼서 돌아와요.
| member_id | username | nickname | bio |
|---|---|---|---|
| 1 | jaehoon | 김재훈 | 백엔드 공부 중 |
| 2 | minji | 이민지 | 사진과 커피 |
| 3 | seungwoo | 박승우 | (NULL) |
| ... | ... | ... | ... |
| 12 | jihun | 박지훈 | 음악과 코드 |
그런데 항상 모든 컬럼이 필요한 건 아니에요. 회원 목록에서 아이디와 이름만 보고 싶다면, * 대신 원하는 컬럼 이름만 적으면 돼요.
SELECT username, nickname FROM member;
이러면 member_id나 bio는 빼고, username과 nickname 두 칸만 골라서 12행이 나와요. 컬럼을 콤마로 나열한 순서대로 결과도 그 순서로 나와요.
🌟 실무 팁 — 실무에서는
SELECT *보다 필요한 컬럼만 콕 집어 적는 걸 권해요. 표에 컬럼이 수십 개일 때*로 다 끌어오면 안 쓰는 데이터까지 옮기느라 느려지고, 나중에 컬럼이 추가되면 결과가 예고 없이 바뀌어요. "필요한 것만 명시"가 안전한 습관이에요.
💡 한 줄 정리
SELECT 컬럼 FROM 표로 원하는 열을 골라 조회하며, *는 모든 컬럼을 뜻하고 컬럼 이름을 나열하면 그 칸만 그 순서로 나온다.
🙋 학생 질문 — "튜터님, bio가 비어 있는 박승우는 그 칸에 뭐가 보이나요?"
박승우(3번)는 자기소개를 안 썼으니 bio 칸이 NULL이에요. 조회하면 그 칸은 비어 있는 채로 나와요. 화면에 따라 아무것도 안 보이거나 (null) 같은 표시가 뜨기도 해요. 중요한 건 그게 빈 글자('')가 아니라 '값이 없음(NULL)'이라는 점이에요. 지난 시간에 배웠듯 NULL과 빈 글자는 달라요. 이 NULL인 회원만 골라내는 법은 잠시 뒤 Step 7에서 직접 해 볼 거예요.
Step 4: "조회 결과 다듬기 — 별칭 · 이어 붙이기 · DISTINCT"
SELECT로 데이터를 꺼낼 수 있게 됐으니, 이번엔 결과를 좀 더 보기 좋게 다듬는 세 가지 도구를 볼게요. 별칭, 이어 붙이기, 중복 제거예요.
먼저 별칭(alias)이에요. 결과에 나오는 컬럼 이름을 바로 다른 이름으로 바꿔 보여 줘요. AS 다음에 새 이름을 적어요.
SELECT username AS id, nickname AS name FROM member;
이러면 결과의 컬럼 머리글이 username·nickname이 아니라 id·name으로 바뀌어 나와요. AS는 생략해도 되지만, 붙여 두면 읽기 편해요. 한글로 별칭을 달고 싶으면 큰따옴표로 감싸요. nickname AS "이름"처럼요.
다음은 이어 붙이기예요. 두 막대 기호 ||로 문자열을 이어 붙여요. 닉네임 뒤에 '님'을 붙여 보면 이래요.
SELECT nickname || '님' AS greeting FROM member;
| greeting |
|---|
| 김재훈님 |
| 이민지님 |
| 박승우님 |
||는 양쪽 값을 그냥 한 줄로 이어 줘요. '님'처럼 따옴표로 감싼 글자는 적은 그대로 들어가요. 숫자를 다루는 산술 연산자(+ - * /)도 있는데, 회원·게시물 데이터로 숫자 계산을 본격적으로 하는 건 함수를 배우는 다음 시간에 더 다뤄요.
마지막은 중복 제거(DISTINCT)예요. 게시물 표에서 글쓴이 번호만 뽑으면 어떻게 될까요?
SELECT DISTINCT member_id FROM post;
게시물은 50개지만, 같은 사람이 여러 개 쓴 경우가 많죠. 그래서 글쓴이 번호에는 중복이 잔뜩 있어요. DISTINCT를 붙이면 중복을 걷어내고 서로 다른 값만 남겨요. 실행하면 게시물을 쓴 회원이 9명이라, 9개 번호만 돌아와요. (12명 중 3명은 게시물을 한 개도 안 썼거든요.)
⚠️ 함정 —
DISTINCT는 바로 뒤 한 컬럼이 아니라SELECT에 적은 컬럼 조합 전체를 기준으로 중복을 따져요.SELECT DISTINCT member_id, caption이면 (member_id, caption) 쌍이 똑같아야 중복으로 쳐요. 컬럼을 더 적으면 그만큼 중복이 덜 걸러진다는 점을 기억하세요.
💡 한 줄 정리
AS로 컬럼에 별칭을 달고, ||로 문자열을 이어 붙이며, DISTINCT로 SELECT한 컬럼 조합 전체 기준의 중복을 제거한다.
🙋 학생 질문 — "튜터님, DISTINCT는 결과만 깔끔해 보이는데 그냥 항상 붙이면 안 되나요?"
깔끔해 보인다고 습관처럼 붙이는 건 권하지 않아요. DISTINCT는 중복을 걸러내려고 DB가 결과를 한 번 더 정렬하거나 비교하는 일을 해요. 데이터가 많으면 그만큼 느려져요. 정말로 중복이 생기는 조회에서, 그 중복을 없애야 할 때만 쓰는 게 맞아요. 중복이 애초에 없는 조회(예: 주식별자만 뽑을 때)에 DISTINCT를 붙이면 효과는 없고 일만 늘어요. "필요할 때만"이 원칙이에요.
Step 5: "원하는 행만 — WHERE 비교·논리·우선순위"
지금까지는 표의 모든 행을 가져왔어요. 하지만 보통은 "조건에 맞는 행만" 보고 싶죠. 그 조건을 거는 게 WHERE예요. FROM 다음에 WHERE 조건을 붙이면, 조건이 참인 행만 남아요.
조건에는 비교 연산자를 써요. =(같다), <> 또는 !=(다르다), > < >= <=(크고 작음)이에요. 회원 번호가 8보다 큰 회원만 보면 이래요.
SELECT * FROM member WHERE member_id > 8;
9·10·11·12번, 4명이 돌아와요. 조건에 안 맞는 1~8번은 빠져요.
조건을 여러 개 엮을 땐 논리 연산자 AND(그리고)·OR(또는)·NOT(아니다)을 써요. "1번 회원이 쓴 게시물 중 6월 1일 이후 것"은 두 조건을 AND로 엮어요.
SELECT * FROM post
WHERE member_id = 1 AND created_at >= DATE '2026-06-01';
날짜는 DATE '2026-06-01'처럼 적어요. 이 조건은 둘 다 참인 행만 남기니 4건이 나와요. 반대로 게시물이 하나도 없는 7번 또는 8번 회원의 게시물을 찾으면요?
SELECT * FROM post WHERE member_id = 7 OR member_id = 8;
둘 다 게시물을 안 썼으니 결과는 0건, 한 행도 안 나와요. 조건에 맞는 게 없으면 빈 결과가 정상이에요.
여기서 SQLD 단골 함정이 하나 나와요. 바로 연산자 우선순위예요. AND가 OR보다 먼저 묶여요. 마치 곱셈을 덧셈보다 먼저 하는 것과 같아요.
연산자 우선순위: AND 가 OR 보다 먼저 (곱셈이 덧셈보다 먼저이듯)
member_id = 1 OR member_id = 2 AND created_at >= DATE '2026-06-05'
└──────────┬──────────┘
이 AND 가 먼저 묶인다
괄호 없이 읽으면: member_id = 1 또는 (member_id = 2 그리고 6/5 이후)
이 차이를 직접 보면 확 와닿아요.
-- 괄호 없음: AND 가 먼저 묶임
SELECT * FROM post
WHERE member_id = 1 OR member_id = 2 AND created_at >= DATE '2026-06-05';
-- 괄호로 OR 를 먼저 묶음
SELECT * FROM post
WHERE (member_id = 1 OR member_id = 2) AND created_at >= DATE '2026-06-05';
위(괄호 없음)는 10건, 아래(괄호 있음)는 5건이 나와요. 같은 조건 같은데 결과가 두 배 차이죠. 괄호 없는 위쪽은 "1번 게시물 전부, 또는 2번 중 6/5 이후"로 읽혀서 1번 게시물 7개가 통째로 들어와요. 괄호 친 아래쪽은 "1번이나 2번이면서 6/5 이후"라 양쪽 다 날짜 조건을 받아요. ★빈출
⚠️ 함정 — 의도한 게 어느 쪽이든,
AND와OR를 섞을 땐 괄호로 묶어 의도를 분명히 하세요. 우선순위를 외우는 것보다, 괄호로 묶어 두는 게 실수도 없고 읽는 사람도 편해요.
💡 한 줄 정리
WHERE는 비교(=·<>·>)·논리(AND·OR·NOT) 연산자로 조건에 맞는 행만 남기며, AND가 OR보다 먼저 묶이므로 섞어 쓸 땐 괄호로 의도를 분명히 한다.
🙋 학생 질문 — "튜터님, 조건에 맞는 게 하나도 없으면 오류가 나나요?"
아니에요, 오류가 아니에요. 7번 또는 8번 게시물을 찾았을 때 0건이 나온 것처럼, 조건에 맞는 행이 없으면 그냥 '빈 결과'가 돌아와요. 이건 정상이에요. "찾아봤는데 해당하는 게 없었다"는 답을 정확히 준 거예요. 오류는 SQL 문법이 틀렸거나 없는 컬럼을 가리킬 때 나는 거고, 결과가 0건인 건 조회가 성공적으로 끝난 정상 상태예요. 둘을 구분하는 게 중요해요.
Step 6: "범위·목록·패턴 — BETWEEN · IN · LIKE"
WHERE에 자주 쓰는 편리한 조건 세 가지를 더 볼게요. 범위는 BETWEEN, 목록은 IN, 패턴은 LIKE예요. 셋 다 AND·OR로 길게 쓸 걸 짧게 줄여 줘요.
먼저 BETWEEN — 범위예요. "6월 1일부터 6월 10일까지" 같은 구간을 잡아요.
SELECT * FROM post
WHERE created_at BETWEEN DATE '2026-06-01' AND DATE '2026-06-10';
이 기간에 올라온 게시물 22건이 나와요. 여기서 꼭 기억할 점은 BETWEEN a AND b가 양쪽 끝을 포함한다는 거예요. 6월 1일과 6월 10일에 올라온 게시물도 결과에 들어와요. ★빈출
다음은 IN — 목록이에요. "1번, 3번, 5번 회원"처럼 값 몇 개 중 하나면 통과예요.
SELECT * FROM member WHERE member_id IN (1, 3, 5);
3명이 나와요. 이건 사실 member_id = 1 OR member_id = 3 OR member_id = 5를 짧게 줄인 거예요. 값이 많아질수록 IN이 훨씬 깔끔해요.
마지막은 LIKE — 패턴이에요. 정확히 같은 값이 아니라 "이런 모양"을 찾을 때 써요. 두 기호를 기억하면 돼요. %는 '아무 문자 0개 이상', _(밑줄)는 '정확히 한 문자'예요. 아이디가 j로 시작하는 회원을 찾아볼게요.
SELECT * FROM member WHERE username LIKE 'j%';
'j%'는 "j로 시작하고 뒤는 아무거나"예요. jaehoon·jisoo·jihun 3명이 나와요. 게시물 글에서 '여행'이 들어간 것도 찾을 수 있어요.
SELECT * FROM post WHERE caption LIKE '%여행%';
'%여행%'는 "앞뒤 아무거나, 중간에 여행 포함"이라 글 어디에든 '여행'이 있으면 걸려요. 4건이 나와요.
🌟 이렇게 미리 보기 —
LIKE로는 "여행이 들어간 글"까지는 찾지만, "글에서 #해시태그만 똑 떼어내기"는 못 해요. 그건 글자를 자르고 뽑아내는 문자 함수가 필요한데, 다음 시간에caption속 해시태그를 함수로 추출해 볼 거예요.
💡 한 줄 정리
BETWEEN a AND b는 양끝을 포함한 범위, IN (...)은 여러 값 중 하나(=OR 축약), LIKE는 %(0개 이상)·_(한 문자) 패턴으로 조건을 건다.
🙋 학생 질문 — "튜터님, LIKE에서 % 하나랑 _ 하나는 정확히 뭐가 다른가요?"
글자 개수를 세는 방식이 달라요. %는 글자가 몇 개든(0개여도) 상관없이 다 받아들여요. _는 정확히 한 글자를 뜻해요. 예를 들어 'j%'는 j 뒤에 글자가 0개든 10개든 다 맞으니 jisoo도 jihun도 걸려요. 반면 'j_'는 j 다음에 딱 한 글자만 있는 두 글자짜리만 맞아서, jisoo 같은 긴 아이디는 안 걸려요. "%는 길이 자유, _는 한 칸 고정"으로 구분하면 돼요.
Step 7: "빈칸을 찾아라 — IS NULL"
지난 시간에 NULL을 배우면서 약속한 게 있었죠. "NULL은 빈 글자가 아니라 '값이 없음·알 수 없음'이고, NULL인지는 =이 아니라 IS NULL로 확인한다"고요. 오늘 그걸 진짜로 써 봐요.
자기소개(bio)를 안 쓴 회원, 즉 bio가 NULL인 회원을 찾아볼게요.
SELECT * FROM member WHERE bio IS NULL;
박승우·최도연·한나래·이민수 4명이 나와요. 반대로 자기소개를 쓴 회원은 IS NOT NULL로 찾아요.
SELECT * FROM member WHERE bio IS NOT NULL;
이러면 나머지 8명이 나와요. 4명 + 8명 = 12명, 딱 맞죠.
그런데 만약 지난 시간 약속을 잊고 =로 NULL을 찾으면 어떻게 될까요?
SELECT * FROM member WHERE bio = NULL;
결과는 0건이에요. 한 명도 안 걸려요. bio가 비어 있는 회원이 분명히 4명 있는데도요. NULL은 '알 수 없음'이라, bio = NULL은 "알 수 없는 값과 같은가?"를 묻는 셈이라 결과가 참도 거짓도 아닌 '알 수 없음'이 돼 버려요. 그래서 한 행도 통과하지 못해요. ★빈출
⚠️ 함정 —
= NULL이나!= NULL은 문법 오류는 아니라서 조용히 0건을 돌려줘요. 오류가 안 나니 더 위험해요. NULL을 다룰 땐 무조건IS NULL/IS NOT NULL이에요. 이건 SQLD에 거의 매번 나오는 함정이에요.
🌟 이렇게 미리 보기 — 지금은 NULL인 회원을 '찾는' 것까지 했어요. 그런데 화면에 보여 줄 땐 빈칸 대신 "소개 없음" 같은 글자로 바꿔 주고 싶을 때가 많죠. NULL을 다른 값으로 바꿔 채우는 건 다음 시간에 NULL 처리 함수로 다뤄요.
💡 한 줄 정리
NULL 여부는 =이 아니라 IS NULL·IS NOT NULL로 확인하며, = NULL은 오류 없이 항상 0건을 돌려주므로 더 조심해야 한다.
🙋 학생 질문 — "튜터님, bio = '' (빈 따옴표)로 찾으면 NULL인 회원이 걸리나요?"
Oracle에서는 안 걸려요. 그리고 사실 Oracle은 빈 글자('')를 NULL과 똑같이 취급하는 독특한 동작이 있어요. 그래서 bio = ''도 bio = NULL처럼 0건이 나와요. 어느 쪽이든 NULL을 =으로 비교하는 셈이라 한 명도 못 찾아요. 결론은 같아요. 빈칸을 찾을 땐 따옴표든 NULL이든 =을 쓰지 말고 IS NULL을 쓰는 거예요. 이 빈 글자와 NULL의 관계는 다른 데이터베이스에선 또 다르게 동작하는데, 그 차이는 나중에 방언을 비교할 때 짚어 볼게요.
Step 8: "줄 세우기 — ORDER BY"
마지막으로 결과를 정렬하는 ORDER BY예요. 지금까지 조회 결과는 DB가 주는 순서대로 나왔는데, 그 순서는 보장되지 않아요. 원하는 순서로 줄을 세우려면 ORDER BY를 SELECT 맨 끝에 붙여요.
회원을 아이디 순으로 정렬해 볼게요.
SELECT * FROM member ORDER BY username;
username 기준으로 영문 알파벳 순서(오름차순)로 줄을 서요. 맨 위가 doyeon, haru, jaehoon... 순이에요. 이번엔 같은 회원들을 닉네임 기준으로 정렬하면요?
SELECT * FROM member ORDER BY nickname;
이번엔 한글 가나다순이라 맨 위가 강지수, 김유나, 김재훈... 순으로 완전히 달라져요. 같은 12명인데 정렬 기준이 영문이냐 한글이냐에 따라 순서가 갈리는 거예요.
같은 12 명, 정렬 기준에 따라 순서가 다르다
ORDER BY username (영문) ORDER BY nickname (한글 가나다)
doyeon 강지수
haru 김유나
jaehoon 김재훈
... ...
방향도 정할 수 있어요. 기본은 오름차순 ASC(작은 값부터), 거꾸로는 DESC(큰 값부터)예요. 정렬 기준을 콤마로 여러 개 줄 수도 있어요. "회원 번호 오름차순으로 묶되, 같은 회원 안에서는 최신 글부터"는 이렇게 써요.
SELECT post_id, member_id, created_at FROM post
ORDER BY member_id ASC, created_at DESC;
먼저 member_id로 줄을 세우고, 같은 회원끼리는 created_at을 내림차순으로 정렬해요. 그래서 1번 회원 안에서는 가장 최근에 올린 글(6월 8일)이 맨 위, 가장 오래된 글(5월 6일)이 맨 아래로 와요.
⚠️ 함정 —
bio처럼 NULL이 섞인 컬럼으로 정렬하면, NULL이 맨 위로 갈지 맨 아래로 갈지 헷갈려요. Oracle은 기본적으로 오름차순에서 NULL을 맨 뒤에 둬요. 이 위치를 직접 정하는 방법(NULLS FIRST/NULLS LAST)은 다음 시간에 NULL 처리와 함께 다뤄요.
💡 한 줄 정리
ORDER BY 컬럼 [ASC|DESC]로 결과를 정렬하며, 콤마로 정렬 기준을 여러 개 주면 앞 기준으로 묶은 뒤 그 안에서 다음 기준으로 정렬한다.
🙋 학생 질문 — "튜터님, ORDER BY를 안 쓰면 항상 입력한 순서대로 나오는 거 아닌가요?"
그렇게 보일 때가 많지만, 보장되지 않아요. 데이터베이스는 데이터를 가장 효율적으로 꺼내려고 내부 사정에 따라 순서를 바꿀 수 있어요. 지금은 데이터가 적어서 우연히 번호순으로 나오는 것처럼 보여도, 데이터가 많아지거나 조건이 복잡해지면 순서가 달라질 수 있어요. 그래서 "이 순서로 보여야 한다"가 중요하면 반드시 ORDER BY로 명시해야 해요. 순서를 운에 맡기지 않는 게 핵심이에요.
마무리
오늘 종이 위 설계도가 드디어 화면에서 살아 움직였어요. CREATE TABLE로 회원·게시물 표를 진짜로 만들고, SELECT로 데이터를 꺼내고, WHERE·BETWEEN·IN·LIKE·IS NULL로 원하는 행만 골라내고, ORDER BY로 줄을 세웠죠. SQL이라는 언어의 첫 문장들을 직접 써 본 거예요. 오늘 다룬 건 전부 데이터를 '읽는' 조회였는데, 이 SELECT가 앞으로 배울 함수·집계·조인의 토대가 돼요.
오늘 배운 핵심 세 가지
- 💡 하나 — SQL은 DDL·DML·TCL·DCL 네 갈래로 나뉘고,
CREATE TABLE(DDL)은 데이터 타입과 제약조건(PRIMARY KEY·NOT NULL·UNIQUE·REFERENCES)으로 표의 틀을 만든다. - 💡 둘 —
SELECT 컬럼 FROM 표 WHERE 조건이 조회의 기본이고, 비교·논리 연산자에서AND가OR보다 먼저 묶이므로 섞을 땐 괄호로 의도를 분명히 한다. - 💡 셋 —
BETWEEN(양끝 포함)·IN(OR 축약)·LIKE(%·_패턴)로 조건을 짧게 걸고, NULL은IS NULL로만 찾으며,ORDER BY로 결과를 정렬한다.
다음 시간 예고
오늘은 데이터를 '있는 그대로' 꺼냈어요. 다음 시간엔 꺼낸 데이터를 '가공'해요. 게시물 글(caption) 속에 섞여 있는 #해시태그를 함수로 똑 떼어내고, 가입 날짜를 "2026년 6월"처럼 보기 좋게 바꾸고, 글자를 자르고 이어 붙이는 단일행 함수들을 배워요. 오늘 LIKE로 "여행이 들어간 글"까지 찾았다면, 다음 시간엔 그 글에서 해시태그만 정확히 뽑아내는 데까지 가요.
과제
오늘 배운 SELECT·WHERE·정렬을 직접 손으로 써 보는 과제예요. 머릿속으로만 짐작하지 말고, 결과가 몇 행 나올지 먼저 예상한 뒤 확인해 보세요.
[기초] 원하는 회원만 골라 정렬하기
회원 표에서 member_id가 5 이하인 회원의 username과 nickname만 조회하는 SQL을 써 보세요. (가) 이때 모든 컬럼을 가져오는 * 대신 두 컬럼만 적어야 하는 이유를 한 줄로 적어 보세요. (나) 결과가 몇 명 나올지 예상해 보세요. (다) 여기에 nickname 기준 가나다순 정렬을 붙이려면 어떤 절을 어디에 추가해야 하는지 SQL로 완성해 보세요.
[응용] 범위·패턴·빈칸 조건 걸기
게시물·회원 표에서 다음 셋을 각각 조회하는 SQL을 써 보세요. (가) 작성일(created_at)이 2026년 5월에 올라온 게시물 (BETWEEN 사용, 양끝 포함에 주의). (나) 글(caption)에 '사진'이 들어간 게시물 (LIKE 사용). (다) 자기소개(bio)가 비어 있는 회원. 그리고 (다)에서 bio = NULL로 쓰면 왜 한 명도 안 걸리는지, 지난 시간에 배운 NULL의 성질로 설명해 보세요.
[심화] 우선순위 함정과 표 설계
(가) member_id가 1 또는 2이면서 2026년 6월 5일 이후에 올라온 게시물을 조회하려고 합니다. WHERE member_id = 1 OR member_id = 2 AND created_at >= DATE '2026-06-05'라고 괄호 없이 쓴 것과, (member_id = 1 OR member_id = 2)로 괄호를 묶은 것의 결과가 왜 다른지 연산자 우선순위로 설명해 보세요. (나) 만약 회원 표에 '가입일'을 기록할 joined_at 칸을 새로 넣는다면, 어떤 데이터 타입과 제약조건(NULL 허용 여부·기본값 등)을 줄지 CREATE TABLE 관점에서 설계하고 이유를 붙여 보세요.
생각해볼 주제
1. SELECT *는 편한데 왜 실무에선 지양할까
조회할 때 SELECT *는 모든 컬럼을 한 번에 가져와서 타이핑이 적고 편해요. 그런데 실무에서는 필요한 컬럼만 골라 적으라고 권하는 경우가 많아요. 표에 컬럼이 수십 개일 때, 또는 나중에 컬럼이 추가·삭제될 때 *가 어떤 문제를 일으킬지 생각해 보세요. 편함과 안전함 사이에서 무엇을 얻고 무엇을 잃는지 따져 보면 좋아요.
2. = NULL은 왜 한 건도 못 잡을까
bio = NULL은 문법 오류도 아닌데 항상 0건을 돌려줘요. 지난 시간에 배운 'NULL = 알 수 없음'과 비교의 결과가 참·거짓이 아닌 '알 수 없음'이 된다는 점(3치 논리)을 떠올리며, 왜 =으로는 NULL을 절대 못 찾는지, 그래서 IS NULL이라는 별도 문법이 왜 필요한지 자기 말로 설명해 보세요.
3. 연산자 우선순위, 외울까 괄호로 감쌀까
AND가 OR보다 먼저 묶인다는 우선순위를 정확히 외우는 것과, 헷갈릴 땐 무조건 괄호로 묶어 의도를 드러내는 것 중 어느 쪽이 더 나은 습관일까요? 외우기에 의존할 때의 위험과, 괄호를 항상 쓰는 것의 장단점(가독성·실수 방지 대 장황함)을 함께 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다, 왜 그렇게 쓰는지 흐름을 따라와 주세요. SQL을 외우는 게 아니라, 조건을 어떻게 표현하는지 익숙해지는 게 목표예요.
🎯 [과제 1 예시답안] 원하는 회원만 골라 정렬하기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) * 대신 컬럼 명시 이유 |
30% | 필요 없는 데이터를 안 옮겨 빠르고, 결과가 명확하다 |
| (나) 결과 행 수 예상 | 30% | member_id 1~5번 → 5명 |
| (다) 정렬 붙이기 | 40% | ORDER BY nickname을 SELECT 맨 끝에 추가 |
풀이 예시
(가) 왜 * 대신 두 컬럼만 적나
SELECT *는 member_id·bio까지 다 끌어와요. 지금 화면엔 아이디와 이름만 필요한데 안 쓰는 칸까지 옮기는 셈이라 비효율적이에요. 필요한 컬럼만 적으면 데이터를 덜 옮겨 빠르고, "이 조회가 무엇을 보여 주는지"도 한눈에 명확해져요.
(나)·(다) 완성한 SQL과 결과
SELECT username, nickname FROM member
WHERE member_id <= 5
ORDER BY nickname;
member_id가 5 이하인 회원은 1~5번, 5명이에요. 여기에 ORDER BY nickname을 붙이면 닉네임 가나다순으로 줄을 서요.
| username | nickname |
|---|---|
| jaehoon | 김재훈 |
| seungwoo | 박승우 |
| minji | 이민지 |
| haru | 정하루 |
| doyeon | 최도연 |
번호순(1~5)으로 넣었지만 결과는 닉네임 가나다순(김재훈→박승우→이민지→정하루→최도연)으로 나와요. ORDER BY가 결과 순서를 다시 정한 거예요.
💡 튜터의 한마디 — WHERE로 행을 고르고, 보여 줄 컬럼을 고르고, ORDER BY로 순서를 정하는 이 세 가지가 조회의 기본 골격이에요. SELECT 컬럼 → FROM 표 → WHERE 조건 → ORDER BY 정렬 순서로 적는 흐름을 익혀 두세요.
🎯 [과제 2 예시답안] 범위·패턴·빈칸 조건 걸기
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
(가) 5월 게시물 BETWEEN |
30% | 양끝 포함 → 23건 |
(나) '사진' 포함 LIKE |
30% | '%사진%' → 7건 |
(다) 빈 자기소개 IS NULL |
40% | 4명 / bio = NULL이 안 되는 이유 |
풀이 예시
(가) 2026년 5월 게시물
SELECT * FROM post
WHERE created_at BETWEEN DATE '2026-05-01' AND DATE '2026-05-31';
5월 1일부터 31일까지 올라온 게시물 23건이 나와요. BETWEEN은 양끝을 포함하니 5월 1일과 5월 31일에 올라온 글도 결과에 들어가요.
(나) 글에 '사진'이 들어간 게시물
SELECT * FROM post WHERE caption LIKE '%사진%';
'%사진%'는 글 어디에든 '사진'이 있으면 걸려요. '필름 카메라 #사진', '봄 사진 #봄', '바다 사진 한 장'처럼 7건이 나와요.
(다) 자기소개가 비어 있는 회원
SELECT * FROM member WHERE bio IS NULL;
박승우·최도연·한나래·이민수 4명이 나와요. 여기서 bio = NULL로 쓰면 한 명도 안 걸려요. NULL은 '알 수 없음'이라, bio = NULL은 "알 수 없는 값과 같은가?"를 묻는 셈이 되어 결과가 참도 거짓도 아닌 '알 수 없음'이 돼 버리거든요. 그래서 어떤 행도 통과하지 못해요. 빈칸을 찾을 땐 반드시 IS NULL이에요.
💡 튜터의 한마디 — BETWEEN·IN·LIKE는 AND·OR로 길게 쓸 조건을 짧게 줄여 줘요. 특히 BETWEEN의 '양끝 포함'과 NULL을 =으로 못 찾는다는 두 가지는 시험에 자주 나오니 꼭 챙기세요.
🎯 [과제 3 예시답안] 우선순위 함정과 표 설계
채점 포인트
| 항목 | 배점 | 핵심 |
|---|---|---|
| (가) 우선순위로 결과 차이 설명 | 50% | AND가 OR보다 먼저 묶임 → 괄호 유무로 결과가 갈림 |
(나) joined_at 칸 설계 |
50% | 데이터 타입(DATE)·제약(NOT NULL·DEFAULT) + 이유 |
풀이 예시
(가) 괄호 유무로 결과가 갈리는 이유
AND가 OR보다 먼저 묶여요. 괄호 없이 쓰면 이렇게 읽혀요.
-- 무괄호: member_id = 1 OR (member_id = 2 그리고 6/5 이후)
SELECT * FROM post
WHERE member_id = 1 OR member_id = 2 AND created_at >= DATE '2026-06-05';
이 경우 "1번 회원의 게시물 전부"와 "2번 회원 중 6월 5일 이후"가 더해져요. 1번 게시물이 통째로 들어와서 결과가 넓어져요. 반대로 괄호로 묶으면요.
-- 괄호: (1번 또는 2번) 그리고 6/5 이후
SELECT * FROM post
WHERE (member_id = 1 OR member_id = 2) AND created_at >= DATE '2026-06-05';
이번엔 1번이든 2번이든 모두 날짜 조건을 받아요. 그래서 무괄호는 더 많이, 괄호는 더 적게 나와요. 의도가 어느 쪽이든 괄호로 묶어 분명히 하는 게 안전해요.
(나) joined_at 칸 설계
-- 회원 표에 가입일을 추가한다면
joined_at DATE DEFAULT SYSDATE NOT NULL
| 항목 | 선택 | 이유 |
|---|---|---|
| 데이터 타입 | DATE |
가입 날짜·시각을 담는 칸이니까 |
NOT NULL |
적용 | 모든 회원은 반드시 가입 시점이 있음 — 비면 안 됨 |
DEFAULT SYSDATE |
적용 | 값을 안 넣으면 가입한 지금 시각이 자동으로 들어감 |
가입일은 '아직 값이 없는 게 자연스러운' 칸이 아니에요. 회원이 만들어졌다는 건 그 시점이 분명히 있다는 뜻이라, bio와 달리 NOT NULL로 막는 게 맞아요.
💡 튜터의 한마디 — (가)는 "외울까 괄호 칠까"의 문제예요. 우선순위를 정확히 알더라도, 읽는 사람을 위해 괄호로 의도를 드러내는 게 좋은 습관이에요. (나)처럼 새 칸을 설계할 땐 "이 값이 없으면 곤란한가"를 기준으로 NULL 허용 여부를 정하면 돼요.
🤔 [생각해볼 주제 1] SELECT *는 편한데 왜 실무에선 지양할까
문제 상황 요약
SELECT *는 타이핑이 적고 편한데, 실무에서는 필요한 컬럼만 적으라고 권하는 경우가 많아요. 무엇을 얻고 무엇을 잃는지가 핵심이에요.
튜터의 가이드 및 해설
SELECT *의 문제는 크게 셋이에요. 첫째, 안 쓰는 컬럼까지 데이터를 옮겨서 느려져요. 표에 컬럼이 수십 개거나 큰 텍스트 칸이 있으면 차이가 커요. 둘째, 나중에 컬럼이 추가·삭제되면 조회 결과의 모양이 예고 없이 바뀌어, 그 결과에 기대던 코드가 깨질 수 있어요. 셋째, 쿼리만 봐서는 "이 조회가 무엇을 가져오는지" 한눈에 안 보여요.
반대로 *가 편한 경우도 있어요. 임시로 데이터를 살펴볼 때, 표 구조를 빠르게 확인할 때처럼요. 결국 "탐색용으로는 *, 코드에 담아 둘 조회는 컬럼 명시"가 균형점이에요.
🎯 SQLD는 이렇게 나온다
SELECT * 자체를 직접 묻는 문제는 드물지만, 조회 결과의 컬럼 순서·개수를 따지는 문제에서 *와 컬럼 명시의 차이가 깔려 있어요. "결과 컬럼이 무엇이 몇 개 나오는가"를 정확히 셀 줄 알아야 해요.
💡 실무에선
운영 코드의 조회는 거의 항상 컬럼을 명시해요. 성능과 더불어, 표 구조가 바뀌어도 조회가 안전하게 버티도록 하기 위해서예요.
🤔 [생각해볼 주제 2] = NULL은 왜 한 건도 못 잡을까
문제 상황 요약
bio = NULL은 문법 오류도 아닌데 항상 0건을 돌려줘요. 빈 자기소개를 가진 회원이 분명히 있는데도요. 왜 =으로는 NULL을 못 찾는지가 핵심이에요.
튜터의 가이드 및 해설
지난 시간에 NULL은 '값이 없음·알 수 없음'이라고 배웠죠. 보통 비교는 참(TRUE) 아니면 거짓(FALSE)인데, NULL이 끼면 결과가 하나 더 생겨요. 바로 '알 수 없음(UNKNOWN)'이에요. 참·거짓·알 수 없음 셋으로 따지는 걸 3치 논리라고 해요.
bio = NULL은 "알 수 없는 값과 같은가?"를 묻는 셈이라 답이 '알 수 없음'이 돼요. WHERE는 조건이 참인 행만 남기는데, '알 수 없음'은 참이 아니라서 한 행도 안 걸려요. 그래서 NULL을 다룰 땐 =이 아니라 전용 표현 IS NULL·IS NOT NULL을 써야 해요. 이건 '같은가'가 아니라 'NULL인 상태인가'를 직접 확인하는 문법이에요.
🎯 SQLD는 이렇게 나온다
NULL과 비교 연산의 결과를 묻는 문제는 거의 매 회차 나와요. = NULL·!= NULL이 항상 빈 결과라는 점, NULL 확인은 IS NULL뿐이라는 점, 그리고 WHERE 조건이 '알 수 없음'이면 그 행이 제외된다는 점을 함께 기억하세요.
💡 실무에선
빈칸을 거를 땐 IS NULL, 그리고 NULL을 다른 값으로 바꿔 보여 줄 땐 NULL 처리 함수를 써요. 그 함수들은 다음 시간에 다뤄요.
🤔 [생각해볼 주제 3] 연산자 우선순위, 외울까 괄호로 감쌀까
문제 상황 요약
AND가 OR보다 먼저 묶인다는 우선순위를 정확히 외우는 것과, 헷갈릴 땐 무조건 괄호로 묶는 것 중 어느 쪽이 더 나은 습관일까요?
튜터의 가이드 및 해설
우선순위를 외우는 건 기본이에요. 산술이 비교보다, 비교가 AND보다, AND가 OR보다 먼저 묶여요. 시험에서 괄호 없는 조건의 결과를 예측하려면 이걸 알아야 해요.
하지만 코드를 쓸 때는 외우기에만 기대지 않는 게 좋아요. 사람은 실수하고, 내 코드를 읽는 동료는 내 우선순위 기억을 믿어야 하거든요. 괄호로 묶어 두면 의도가 글자로 드러나서, 우선순위를 헷갈려도 결과가 흔들리지 않고 읽는 사람도 편해요. 단점이라면 괄호가 늘어 살짝 장황해지는 정도인데, 그 대가로 얻는 안전함이 훨씬 커요. 정리하면 "우선순위는 알되, 쓸 땐 괄호로 드러낸다"가 좋은 균형이에요.
🎯 SQLD는 이렇게 나온다
괄호 없는 AND/OR 혼합 조건을 주고 결과 행을 예측하게 하는 문제가 단골이에요. 이때는 AND를 먼저 묶어서 읽어야 정답이 나와요. 우선순위를 모르면 함정에 그대로 걸려요.
💡 실무에선
운영 쿼리에서는 AND와 OR를 섞을 때 거의 항상 괄호로 묶어요. 한 줄의 모호함이 잘못된 데이터 조회로 이어질 수 있으니, 의도를 분명히 드러내는 쪽을 택해요.