콘텐츠로 이동

SQL 기초 — 데이터를 다루는 언어

설계 다음은 질문이다

5장에서 데이터베이스를 어떻게 설계하는지 배웠습니다.
이제 그 데이터베이스에 실제로 데이터를 넣고, 꺼내고, 수정하고, 삭제해야 합니다.

그 수단이 바로 SQL입니다.

SQL이란?

SQL = Structured Query Language

데이터베이스에게 명령을 내리는 언어입니다.
영어 문장처럼 읽히도록 설계되어 있습니다.

SELECT name FROM students WHERE age = 17;
-- "students에서, 나이가 17인 행의 name을 가져와라"

SQL은 어느 관계형 데이터베이스에서나 거의 동일하게 동작합니다.
MySQL, PostgreSQL, SQLite — 방언(dialect)이 약간 다를 뿐 핵심은 같습니다.

SQL의 분류

분류 이름 역할 대표 명령어
DDL Data Definition Language 구조 정의 CREATE, DROP, ALTER
DML Data Manipulation Language 데이터 조작 SELECT, INSERT, UPDATE, DELETE
DCL Data Control Language 권한 제어 GRANT, REVOKE

실무에서 가장 많이 쓰는 것은 DML입니다.
이 장에서는 DML을 집중적으로 다룹니다.

테이블 만들기 — CREATE TABLE

CREATE TABLE users (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    email       TEXT    NOT NULL UNIQUE,
    name        TEXT    NOT NULL,
    created_at  TEXT    DEFAULT (datetime('now'))
);

CREATE TABLE todos (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL,
    title      TEXT    NOT NULL,
    is_done    INTEGER DEFAULT 0,
    created_at TEXT    DEFAULT (datetime('now')),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

이 장에서는 SQLite 문법을 기준으로 합니다.
MySQL/PostgreSQL은 타입 이름이 약간 다릅니다 (VARCHAR, SERIAL, BOOLEAN 등).

SELECT — 데이터 조회

SELECT 컬럼명, ...
FROM   테이블명
WHERE  조건
ORDER BY 정렬기준
LIMIT  개수;

전체 조회

SELECT * FROM users;
-- *는 모든 컬럼을 의미

특정 컬럼만 조회

SELECT id, name FROM users;

WHERE — 조건 필터링

SELECT * FROM todos WHERE is_done = 0;
-- 완료되지 않은 할 일만

SELECT * FROM todos WHERE user_id = 1 AND is_done = 1;
-- user_id가 1이고 완료된 것만

자주 쓰는 조건 연산자

연산자 의미 예시
= 같다 WHERE id = 1
!= 또는 <> 다르다 WHERE name != '지민'
>, <, >=, <= 대소 비교 WHERE age >= 18
AND 둘 다 WHERE age >= 18 AND city = '서울'
OR 하나라도 WHERE city = '서울' OR city = '부산'
NOT 반대 WHERE NOT is_done = 1
BETWEEN 범위 WHERE age BETWEEN 16 AND 19
LIKE 패턴 매칭 WHERE email LIKE '%@gmail.com'
IN 목록 중 하나 WHERE city IN ('서울', '부산', '대구')
IS NULL 값 없음 WHERE deleted_at IS NULL

LIKE 패턴

WHERE name LIKE '김%'    -- '김'으로 시작하는 모든 이름
WHERE name LIKE '%민'    -- '민'으로 끝나는 모든 이름
WHERE name LIKE '%지%'   -- '지'가 들어있는 모든 이름

ORDER BY — 정렬

SELECT * FROM todos ORDER BY created_at DESC;
-- 최신순 (내림차순)

SELECT * FROM todos ORDER BY created_at ASC;
-- 오래된 순 (오름차순, 기본값)

SELECT * FROM todos ORDER BY user_id ASC, created_at DESC;
-- 먼저 user_id로 오름차순, 같으면 created_at 기준 최신순

LIMIT — 개수 제한

SELECT * FROM todos LIMIT 10;
-- 최대 10개만

SELECT * FROM todos LIMIT 10 OFFSET 20;
-- 21번째부터 10개 (페이지네이션)

집계 함수 — 통계 구하기

SELECT COUNT(*)   FROM todos;                    -- 전체 개수
SELECT COUNT(*)   FROM todos WHERE is_done = 1;  -- 완료된 개수
SELECT AVG(age)   FROM users;                    -- 나이 평균
SELECT MAX(age)   FROM users;                    -- 최고 나이
SELECT MIN(age)   FROM users;                    -- 최저 나이
SELECT SUM(price) FROM order_items;              -- 가격 합계

GROUP BY — 그룹별 집계

SELECT user_id, COUNT(*) AS todo_count
FROM   todos
GROUP BY user_id;
-- 사용자별 할 일 개수
결과:
user_id | todo_count
--------|----------
      1 |          5
      2 |          3
      3 |          8

HAVING — 그룹 필터링

WHERE는 행을 필터링하고, HAVING은 그룹을 필터링합니다.

SELECT user_id, COUNT(*) AS todo_count
FROM   todos
GROUP BY user_id
HAVING COUNT(*) >= 5;
-- 할 일이 5개 이상인 사용자만

JOIN — 테이블 연결

관계형 모델에서 데이터는 여러 테이블에 나뉘어 저장됩니다.
JOIN은 그 테이블들을 연결해서 하나의 결과로 만드는 방법입니다.

INNER JOIN

두 테이블에 모두 존재하는 행만 반환합니다.

SELECT todos.id, todos.title, users.name
FROM   todos
INNER JOIN users ON todos.user_id = users.id;
결과:
id | title      | name
---|------------|------
 1 | 운동하기   | 지민
 2 | 책 읽기    | 지민
 3 | 영어 공부  | 하나

LEFT JOIN

왼쪽 테이블의 모든 행을 반환합니다.
오른쪽에 매칭되는 값이 없으면 NULL로 채웁니다.

SELECT users.name, todos.title
FROM   users
LEFT JOIN todos ON users.id = todos.user_id;
-- 할 일이 없는 사용자도 결과에 포함 (todos 컬럼은 NULL)
결과:
name  | title
------|--------
지민  | 운동하기
지민  | 책 읽기
하나  | 영어 공부
준서  | NULL        ← 할 일이 없는 사용자도 포함

JOIN 선택 기준

모든 A를 보고 싶고, 연결된 B도 있으면 함께 → LEFT JOIN
반드시 양쪽 모두 있는 것만               → INNER JOIN

별칭(Alias) 사용

JOIN이 길어지면 별칭으로 줄입니다.

SELECT t.id, t.title, u.name
FROM   todos  AS t
INNER JOIN users AS u ON t.user_id = u.id
WHERE  u.id = 1;

INSERT — 데이터 삽입

INSERT INTO users (email, name)
VALUES ('jimin@ex.com', '지민');

INSERT INTO todos (user_id, title)
VALUES (1, '운동하기');

여러 행을 한 번에:

INSERT INTO users (email, name) VALUES
    ('jimin@ex.com', '지민'),
    ('hana@ex.com',  '하나'),
    ('jun@ex.com',   '준서');

UPDATE — 데이터 수정

UPDATE todos
SET    is_done = 1
WHERE  id = 3;
-- id가 3인 todo를 완료 상태로 변경
UPDATE users
SET    name = '김지민', email = 'new@ex.com'
WHERE  id = 1;
-- 여러 컬럼 동시 수정

주의: WHERE를 빠뜨리면 전체 행이 수정됩니다.

UPDATE todos SET is_done = 1;
-- 모든 할 일이 완료 상태로! (의도한 게 아니라면 재앙)

DELETE — 데이터 삭제

DELETE FROM todos WHERE id = 5;
-- id가 5인 행 삭제

DELETE FROM todos WHERE user_id = 1 AND is_done = 1;
-- user_id 1의 완료된 항목 전체 삭제

주의: WHERE를 빠뜨리면 테이블의 모든 행이 삭제됩니다.

소프트 삭제 (Soft Delete)

실무에서는 데이터를 실제로 지우지 않는 경우가 많습니다.

-- deleted_at 컬럼으로 삭제 시각을 기록하는 방식
ALTER TABLE todos ADD COLUMN deleted_at TEXT;

-- 삭제 대신:
UPDATE todos SET deleted_at = datetime('now') WHERE id = 5;

-- 조회 시 삭제된 것 제외:
SELECT * FROM todos WHERE deleted_at IS NULL;

실행 순서 — SQL은 위에서 아래로 읽히지 않는다

작성 순서와 실행 순서가 다릅니다.

작성 순서:          실행 순서:
SELECT    ①   →   FROM      ①
FROM      ②   →   JOIN      ②
WHERE     ③   →   WHERE     ③
GROUP BY  ④   →   GROUP BY  ④
HAVING    ⑤   →   HAVING    ⑤
ORDER BY  ⑥   →   SELECT    ⑥
LIMIT     ⑦   →   ORDER BY  ⑦
                  LIMIT     ⑧

이 순서를 알면 왜 WHERE에서 SELECT의 별칭을 쓸 수 없는지 이해됩니다.

-- 잘못된 예 — WHERE 실행 시점에 todo_count가 아직 없음
SELECT COUNT(*) AS todo_count FROM todos
WHERE todo_count > 5;  -- ← 오류

-- 올바른 예
SELECT user_id, COUNT(*) AS todo_count FROM todos
GROUP BY user_id
HAVING COUNT(*) > 5;  -- HAVING은 GROUP BY 이후 실행

실습 미션

미션 1: 기본 조회

아래 테이블이 있다고 가정합니다.

users: id, email, name, age, city
todos: id, user_id, title, is_done, created_at

다음 쿼리를 작성하세요.

1. 모든 사용자의 이름과 이메일을 조회하세요.
2. 서울에 사는 사용자만 조회하세요.
3. 나이가 18세 이상인 사용자를 나이 내림차순으로 조회하세요.
4. 완료되지 않은 할 일을 최신순으로 10개만 조회하세요.
5. 이름에 '민'이 들어있는 사용자를 조회하세요.

미션 2: 집계

1. 전체 사용자 수를 구하세요.
2. 완료된 할 일의 수를 구하세요.
3. 사용자별 할 일 개수를 구하세요 (user_id와 개수 출력).
4. 할 일이 3개 이상인 사용자만 골라서 개수 내림차순으로 출력하세요.

미션 3: JOIN

1. 모든 할 일을 조회할 때 작성자 이름도 함께 출력하세요.
2. 완료되지 않은 할 일을 조회할 때 작성자 이름과 이메일도 출력하세요.
3. 할 일이 아직 없는 사용자의 이름을 출력하세요. (힌트: LEFT JOIN + IS NULL)

미션 4: INSERT, UPDATE, DELETE

1. 사용자(email: 'test@ex.com', name: '테스트')를 추가하세요.
2. user_id = 1인 사용자의 완료된 할 일을 모두 삭제하세요.
3. id = 7인 할 일의 제목을 '영어 단어 30개'로 바꾸고 완료 상태로 수정하세요.

미션 5 (심화): 5장 ERD를 SQL로

5장에서 설계한 서비스의 ERD를 SQL로 변환하세요.

1. CREATE TABLE로 테이블을 만드세요.
2. 샘플 데이터를 INSERT하세요 (각 테이블 3행 이상).
3. 아래 질문에 답하는 SELECT 쿼리를 작성하세요:
   - 특정 사용자의 모든 데이터를 조회하세요.
   - 가장 많이 활동한 사용자는 누구인가요?

핵심 요약

명령 역할 주의점
SELECT 조회 WHERE, ORDER BY, LIMIT, GROUP BY, HAVING 활용
INSERT 삽입 컬럼 순서와 값 순서가 일치해야 함
UPDATE 수정 WHERE 필수 — 없으면 전체 수정
DELETE 삭제 WHERE 필수 — 없으면 전체 삭제
INNER JOIN 교집합 연결 ON 조건으로 FK ↔ PK 연결
LEFT JOIN 왼쪽 전체 연결 매칭 없으면 NULL

UPDATE와 DELETE에는 항상 WHERE를 붙이는 습관을 들이세요.
WHERE 없이 실행하면 전체 데이터가 바뀌거나 사라집니다.