SQL 기초
SQL 기초
PostgreSQL · MySQL · SQLite · SQL Server · Oracle 모두 핵심 문법은 비슷합니다. 한 번 배우면 평생 쓰는 도구가 됩니다.
1. SQL 에 대한 이야기
SQL 은 Structured Query Language 의 줄임말입니다. 1974 년 IBM 의 Donald Chamberlin 과 Raymond Boyce 가 SEQUEL 이라는 이름으로 발표했고, 상표 충돌로 SQL 로 개명됐습니다. 1986 년 ANSI, 1987 년 ISO 가 SQL-86 으로 표준화했고 이후 갱신이 이어집니다.
| 표준 | 시기 | 주요 추가 |
|---|---|---|
| SQL-86 / 89 | 1986–89 | 기본 문법 |
| SQL-92 | 1992 | JOIN 종류 · 표준화 강화 |
| SQL:1999 | 1999 | OLAP · 트리거 · CTE |
| SQL:2003 | 2003 | XML · 윈도우 함수 |
| SQL:2008 | 2008 | TRUNCATE · MERGE |
| SQL:2016 | 2016 | JSON |
DBMS 마다 표준 준수 정도와 방언이 다릅니다. PostgreSQL 의 ON CONFLICT 와 MySQL 의 ON DUPLICATE KEY UPDATE 처럼 구문 자체가 갈리는 자리도 있습니다.
2. DDL · DML · DCL · TCL
| 분류 | 의미 | 명령 |
|---|---|---|
| DDL (Data Definition) | 스키마 정의 | CREATE · ALTER · DROP · TRUNCATE |
| DML (Data Manipulation) | 데이터 조작 | SELECT · INSERT · UPDATE · DELETE |
| DCL (Data Control) | 권한 | GRANT · REVOKE |
| TCL (Transaction Control) | 트랜잭션 | BEGIN · COMMIT · ROLLBACK |
DDL 의 일부는 트랜잭션 안에서의 동작이 DBMS 마다 다릅니다. PostgreSQL 은 거의 모든 DDL 이 트랜잭션 안에서 롤백 가능하지만, MySQL · Oracle 은 일부 DDL 이 묵시적 커밋을 일으킵니다.
3. CREATE TABLE
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
age INT CHECK (age >= 0),
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users (email);
자료형은 DBMS 마다 약간 다릅니다. INT · BIGINT · TEXT · VARCHAR(n) · BOOLEAN · DATE · TIMESTAMP · TIMESTAMPTZ · JSONB 정도가 자주 등장합니다.
4. SELECT 의 절들
SELECT name, COUNT(*) AS cnt
FROM orders
WHERE status = 'paid'
AND created_at >= '2025-01-01'
GROUP BY name
HAVING COUNT(*) >= 5
ORDER BY cnt DESC
LIMIT 10
OFFSET 0;
논리적 처리 순서는 작성 순서와 다릅니다.
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT/OFFSET
이 순서를 알면 왜 WHERE 에서는 별칭(alias) 을 못 쓰고 ORDER BY 에서는 쓸 수 있는지 같은 함정이 풀립니다.
WHERE 절의 자주 쓰는 모양들.
WHERE age BETWEEN 18 AND 30
WHERE name LIKE 'Lee%' -- 'Lee' 로 시작
WHERE name ILIKE '%lee%' -- 대소문자 무시 (Postgres)
WHERE id IN (1, 2, 3)
WHERE id NOT IN (SELECT user_id FROM banned)
WHERE email IS NULL
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = users.id)
5. JOIN 5 가지
A 테이블 B 테이블
+---+------+ +-----+------+
| 1 | foo | | 1 | x |
| 2 | bar | | 3 | y |
+---+------+ +-----+------+
| JOIN | 결과 |
|---|---|
INNER JOIN |
양쪽 매칭만. (1, foo, x) |
LEFT JOIN |
왼쪽 전체, 오른쪽 없으면 NULL. (1, foo, x), (2, bar, NULL) |
RIGHT JOIN |
오른쪽 전체, 왼쪽 없으면 NULL. (1, foo, x), (NULL, NULL, y) |
FULL OUTER JOIN |
양쪽 전체 |
CROSS JOIN |
데카르트 곱. m × n 행 |
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';
USING (col) 은 같은 컬럼명일 때 짧게 쓰는 형태입니다.
6. CTE (WITH)
WITH paid_users AS (
SELECT DISTINCT user_id FROM orders WHERE status = 'paid'
)
SELECT u.name
FROM users u
JOIN paid_users p ON p.user_id = u.id;
재귀 CTE 는 트리·계층 처리에 유용합니다.
WITH RECURSIVE org AS (
SELECT id, name, manager_id, 1 AS level
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, o.level + 1
FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY level;
CTE 는 SQL:1999 표준이며 MySQL 은 8.0+ 에서 지원합니다.
7. INSERT · UPDATE · DELETE · UPSERT
INSERT INTO users (email, name) VALUES ('a@b.com', 'lee');
UPDATE users SET active = FALSE WHERE last_login < '2024-01-01';
DELETE FROM users WHERE active = FALSE;
-- UPSERT (Postgres)
INSERT INTO users (email, name) VALUES ('a@b.com', 'lee')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;
UPDATE · DELETE 시 WHERE 를 빠뜨리면 모든 행이 영향을 받습니다. 운영 DB 에서 가장 자주 일어나는 사고 자리입니다. 트랜잭션 안에서 같은 조건으로 SELECT 해 보고 BEGIN; UPDATE ...; COMMIT; 하는 습관이 도움이 됩니다.
8. 트랜잭션과 격리 수준
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 또는 ROLLBACK
ACID — 원자성·일관성·격리성·지속성. 격리 수준은 4 단계입니다.
READ UNCOMMITTED < READ COMMITTED < REPEATABLE READ < SERIALIZABLE
기본값은 DBMS 마다 다릅니다. Postgres 는 READ COMMITTED, MySQL/InnoDB 는 REPEATABLE READ.
9. NULL 의 3치 논리
NULL 은 "값 없음" 입니다. NULL 과의 비교는 TRUE / FALSE 가 아니라 UNKNOWN 이 결과입니다.
SELECT NULL = NULL; -- NULL (UNKNOWN)
SELECT NULL = 1; -- NULL
SELECT NULL <> NULL; -- NULL
SELECT 1 IS NULL; -- FALSE
SELECT NULL IS NULL; -- TRUE
WHERE 절은 TRUE 인 행만 통과시킵니다. WHERE x = NULL 은 영원히 행을 안 돌려줍니다. 항상 IS NULL 또는 IS NOT NULL 을 씁니다.
COUNT(*) 는 NULL 도 세지만 COUNT(col) 은 그 컬럼이 NULL 인 행을 뺍니다. AVG(col) 도 NULL 행은 분모·분자에서 모두 빠집니다.
10. 자주 걸리는 자리
WHERE 빠뜨린 UPDATE/DELETE — 운영 사고 단골입니다. 항상 SELECT 로 먼저 검증합니다.
GROUP BY 누락 컬럼 — SELECT 에 있는 비집계 컬럼은 GROUP BY 에도 적습니다.
ORDER BY 없는 LIMIT — 매번 다른 행이 나올 수 있습니다.
N+1 쿼리 — 애플리케이션 코드가 루프 안에서 같은 패턴의 쿼리를 반복하는 경우. JOIN 또는 IN 절로 한 번에 가져옵니다.
인덱스 안 타는 쿼리 — WHERE func(col) = ... 같은 함수·연산이 인덱스를 무력화합니다. 식 인덱스를 만들거나 조건을 바꿉니다.
SELECT * — 운영 코드에서는 컬럼을 명시합니다. 스키마 변경 시 무너지지 않게.
방언 차이 — NOW() vs CURRENT_TIMESTAMP, LIMIT vs TOP. 처음 만나면 검색합니다.
11. 한 번 시도해 보기
명령줄에서 한 번 시도해 보려면 Docker 한 줄로 PostgreSQL 을 띄울 수 있습니다.
docker run --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:16
docker exec -it pg psql -U postgres
SQLite 는 설치 부담이 거의 없습니다.
sqlite3 test.db
하고픈 말
SQL 은 30 년 넘은 언어인데도 신규 시스템에서 여전히 첫 선택지입니다. 표준이 잘 정의돼 있고 도구·자료가 두텁기 때문입니다. 한 번 익숙해지면 DBMS 가 바뀌어도 80% 는 그대로 쓸 수 있습니다.
Next
- postgres-first
- postgres-deep
PostgreSQL 공식 튜토리얼 · Use The Index, Luke! · SQLBolt · Codd 의 1970 년 논문 을 참고합니다.