SQL 문법 치트시트
조인과 윈도우 함수부터 흔한 함정까지, 바로 쓰는 예시 포함
문법 104개
SELECT col1, col2 FROM t조회 골격SELECT id, name, created_at FROM users
컬럼 지정 조회, 운영에서는 SELECT * 지양
SELECT * FROM t WHERE id = 1조회 골격SELECT * FROM users WHERE id = 1
모든 컬럼 조회, 임시 확인용
SELECT DISTINCT col FROM t조회 골격SELECT DISTINCT city FROM users
중복 제거, 선택한 행 전체 기준
SELECT col AS alias FROM t조회 골격SELECT user_name AS name FROM users
컬럼이나 표현식에 별칭 지정
SELECT ... ORDER BY col DESC조회 골격SELECT id FROM orders ORDER BY created_at DESC
내림차순 정렬, ASC가 기본
SELECT ... ORDER BY a ASC, b DESC조회 골격SELECT id, score FROM exam ORDER BY score DESC, id ASC
다중 컬럼 정렬, 페이징 시 고유 컬럼 보조 정렬 필요
SELECT ... LIMIT 10 OFFSET 20조회 골격SELECT id FROM orders ORDER BY id LIMIT 10 OFFSET 20
페이징, 21~30번째 행
SELECT TOP 10 ... FROM t조회 골격SELECT TOP 10 id FROM orders ORDER BY id DESC
SQL Server의 상위 N행 문법
SELECT ... FETCH FIRST 10 ROWS ONLY조회 골격SELECT id FROM orders ORDER BY id FETCH FIRST 10 ROWS ONLY
표준 SQL 상위 N행, Oracle과 최신 PostgreSQL 지원
SELECT a FROM t1 UNION SELECT b FROM t2조회 골격SELECT email FROM users UNION SELECT email FROM leads
합치고 중복 제거, 컬럼 수와 타입이 일치해야 함
SELECT a FROM t1 UNION ALL SELECT b FROM t2조회 골격SELECT id FROM a UNION ALL SELECT id FROM b
중복 제거 없이 합침, UNION보다 훨씬 빠름
WITH cte AS (SELECT ...) SELECT * FROM cte조회 골격WITH paid AS (SELECT * FROM orders WHERE status = 1) SELECT COUNT(*) FROM paid
CTE, 복잡한 쿼리를 읽기 쉬운 단계로 분해
WITH RECURSIVE cte AS (... UNION ALL ...) SELECT * FROM cte조회 골격WITH RECURSIVE tree AS (SELECT id, parent_id FROM nodes WHERE parent_id IS NULL UNION ALL SELECT n.id, n.parent_id FROM nodes n JOIN tree t ON n.parent_id = t.id) SELECT * FROM tree
재귀 CTE, 트리와 그래프 구조 조회
SELECT CASE WHEN cond THEN a ELSE b END FROM t조회 골격SELECT CASE WHEN score >= 60 THEN 1 ELSE 0 END AS passed FROM exam
조건식, 결과 안에서 분기
SELECT CAST(col AS INT) FROM t조회 골격SELECT CAST(price AS DECIMAL(10,2)) FROM orders
명시적 형변환, 암시적 변환보다 안전
SELECT COALESCE(a, b, 0) FROM t조회 골격SELECT COALESCE(nickname, user_name) FROM users
첫 번째 NULL이 아닌 값, NULL 처리에 가장 흔함
SELECT CONCAT(a, b) FROM t조회 골격SELECT CONCAT(first_name, surname) FROM users
문자열 결합, PostgreSQL과 MySQL은 || 연산자도 지원
SELECT ROW_NUMBER() OVER (...) FROM t조회 골격SELECT ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users
행 번호 추가(윈도우 함수 그룹 참고)
WHERE col = value필터 조건WHERE status = 1
동등 비교 필터
WHERE col <> value필터 조건WHERE status <> 0
부등 비교, != 도 가능
WHERE col IN (1, 2, 3)필터 조건WHERE status IN (1, 2, 5)
여러 값 매칭, OR 나열보다 가독성 좋음
WHERE col NOT IN (SELECT ...)필터 조건WHERE id NOT IN (SELECT user_id FROM bans)
서브쿼리 제외, 서브쿼리에 NULL이 있으면 결과가 비어 버림
WHERE col BETWEEN 10 AND 20필터 조건WHERE created_at BETWEEN 2 AND 8
양끝 포함, 날짜에 쓰면 마지막 시각이 누락
WHERE name LIKE 'ab%'필터 조건WHERE email LIKE 'admin%'
접두 매칭, 인덱스 사용 가능
WHERE name LIKE '%ab%'필터 조건WHERE remark LIKE '%退货%'
포함 매칭, 앞쪽 와일드카드로 인덱스 사용 불가
WHERE col IS NULL필터 조건WHERE deleted_at IS NULL
NULL 판정은 IS NULL, = NULL은 절대 성립하지 않음
WHERE col IS NOT NULL필터 조건WHERE email IS NOT NULL
NULL 아님 판정
WHERE a = 1 AND (b = 2 OR c = 3)필터 조건WHERE status = 1 AND (type = 2 OR type = 3)
괄호가 우선순위 결정, AND가 OR보다 우선
WHERE EXISTS (SELECT 1 FROM b WHERE b.a_id = a.id)필터 조건WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
존재 판정, 보통 IN보다 빠르고 NULL 영향 없음
WHERE created_at >= '2026-01-01'필터 조건WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
시간 범위는 좌폐우개 권장, 컬럼에 함수를 씌우지 않음
SELECT ... FROM a INNER JOIN b ON a.id = b.a_id조인SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id
내부 조인, 양쪽 모두 일치하는 행만 유지
SELECT ... FROM a LEFT JOIN b ON a.id = b.a_id조인SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id
좌측 조인, 왼쪽 행 전부 유지, 오른쪽 없으면 NULL
SELECT ... FROM a FULL OUTER JOIN b ON ...조인SELECT * FROM a FULL OUTER JOIN b ON a.id = b.a_id
완전 외부 조인, 양쪽 미일치 행 모두 유지
SELECT ... FROM a CROSS JOIN b조인SELECT s.size, c.color FROM sizes s CROSS JOIN colors c
카티션 곱, 조합 행렬 생성에 사용
SELECT ... FROM a LEFT JOIN b ON ... WHERE b.id IS NULL조인SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL
왼쪽에만 있고 오른쪽에 없는 행 찾기(차집합)
SELECT ... FROM a JOIN b USING (id)조인SELECT * FROM users JOIN profiles USING (user_id)
같은 이름 컬럼으로 조인, 결과에 한 번만 표시
SELECT ... FROM a x JOIN a y ON x.pid = y.id조인SELECT e.name, m.name AS manager FROM emp e JOIN emp m ON e.mgr_id = m.id
셀프 조인, 계층 관계 조회
LEFT JOIN b ON ... AND b.status = 1조인SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 1
좌측 조인의 추가 조건은 ON에, WHERE에 쓰면 내부 조인으로 변질
SELECT col, COUNT(*) FROM t GROUP BY col집계와 그룹SELECT city, COUNT(*) FROM users GROUP BY city
컬럼별 그룹 집계
SELECT COUNT(*) FROM t집계와 그룹SELECT COUNT(*) FROM orders
행 수 집계, NULL 행 포함
SELECT COUNT(col) FROM t집계와 그룹SELECT COUNT(email) FROM users
NULL이 아닌 값만 집계, COUNT(*)와 다를 수 있음
SELECT COUNT(DISTINCT col) FROM t집계와 그룹SELECT COUNT(DISTINCT user_id) FROM orders
중복 제거 후 개수 집계
SELECT SUM(col), AVG(col), MIN(col), MAX(col) FROM t집계와 그룹SELECT SUM(amount), AVG(amount) FROM orders
합계, 평균, 최소, 최대, AVG는 NULL을 제외해 분모가 줄어듦
SELECT col FROM t GROUP BY col HAVING COUNT(*) > 1집계와 그룹SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1
그룹화 후 필터, 중복 찾기에 가장 흔함
SELECT GROUP_CONCAT(col) FROM t GROUP BY g집계와 그룹SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM users GROUP BY city
MySQL의 그룹 내 문자열 결합, PostgreSQL은 STRING_AGG
SELECT COUNT(*) FILTER (WHERE cond) FROM t집계와 그룹SELECT COUNT(*) FILTER (WHERE status = 1) AS paid FROM orders
조건부 집계, 한 번 스캔으로 여러 지표 계산, MySQL은 SUM(조건)
SELECT a, b, COUNT(*) FROM t GROUP BY ROLLUP (a, b)집계와 그룹SELECT city, status, COUNT(*) FROM orders GROUP BY ROLLUP (city, status)
소계와 합계 행 자동 추가
SELECT a, b FROM t GROUP BY a, b집계와 그룹SELECT city, status FROM orders GROUP BY city, status
다중 컬럼 그룹화, 비집계 SELECT 컬럼은 모두 GROUP BY에 포함
ROW_NUMBER() OVER (PARTITION BY g ORDER BY t DESC)윈도우 함수ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
그룹 내 순번, 중복 없음, 그룹별 최신 1건 패턴의 핵심
RANK() OVER (ORDER BY score DESC)윈도우 함수RANK() OVER (ORDER BY score DESC) AS r
동순위 시 건너뛰는 순위(1,2,2,4)
DENSE_RANK() OVER (ORDER BY score DESC)윈도우 함수DENSE_RANK() OVER (ORDER BY score DESC) AS r
건너뛰지 않는 순위(1,2,2,3)
LAG(col, 1) OVER (ORDER BY t)윈도우 함수LAG(amount, 1, 0) OVER (ORDER BY created_at) AS prev_amount
이전 행 값, 증감 계산
LEAD(col, 1) OVER (ORDER BY t)윈도우 함수LEAD(amount) OVER (ORDER BY created_at) AS next_amount
다음 행 값
SUM(x) OVER (ORDER BY t)윈도우 함수SUM(amount) OVER (ORDER BY created_at) AS running_total
누적 합계, 기본 프레임은 처음부터 현재 행까지
AVG(x) OVER (PARTITION BY g)윈도우 함수AVG(amount) OVER (PARTITION BY user_id) AS user_avg
그룹 내 평균, 행을 합치지 않고 상세와 함께 표시
NTILE(4) OVER (ORDER BY x)윈도우 함수NTILE(4) OVER (ORDER BY amount DESC) AS quartile
결과를 N개 구간으로 분할, 분위수와 층화 추출
FIRST_VALUE(x) OVER (PARTITION BY g ORDER BY t)윈도우 함수FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS first_amount
그룹 내 첫 행의 값
LAST_VALUE(x) OVER (... ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)윈도우 함수LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_amount
그룹 내 마지막 행, 프레임을 지정하지 않으면 현재 행이 나옴
SELECT * FROM (SELECT ..., ROW_NUMBER() OVER (...) rn FROM t) x WHERE rn = 1윈도우 함수SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn FROM orders) x WHERE rn = 1
그룹별 최신 1건의 표준 문법, 서브쿼리 한 겹 필요
WHERE rn = 1 -- 窗口函数不能直接写在 WHERE 里윈도우 함수SELECT * FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY g ORDER BY t) rn FROM t) x WHERE rn <= 3
윈도우 함수는 WHERE 단계에서 계산되지 않으므로 서브쿼리로 먼저 구체화
INSERT INTO t (a, b) VALUES (1, 2), (3, 4)데이터 변경INSERT INTO users (name, email) VALUES ('Ada', 'ada@example.com')
한 행 또는 여러 행 삽입, 묶어서 쓰면 더 빠름
INSERT INTO t (a, b) SELECT a, b FROM src데이터 변경INSERT INTO archive SELECT * FROM orders WHERE created_at < 2025
조회 결과를 일괄 삽입
INSERT ... ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col데이터 변경INSERT INTO s (id, v) VALUES (1, 10) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v
PostgreSQL upsert, 충돌 시 갱신 아니면 삽입
INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col)데이터 변경INSERT INTO s (id, v) VALUES (1, 10) ON DUPLICATE KEY UPDATE v = VALUES(v)
MySQL upsert 문법
UPDATE t SET col = value WHERE cond데이터 변경UPDATE users SET status = 1 WHERE id = 10
지정 행 갱신, WHERE 누락 시 전체 갱신
UPDATE t SET a = b.a FROM b WHERE t.id = b.id데이터 변경UPDATE orders o SET price = p.price FROM products p WHERE o.product_id = p.id
PostgreSQL 조인 갱신, MySQL은 JOIN, SQL Server는 FROM
DELETE FROM t WHERE cond데이터 변경DELETE FROM sessions WHERE expires_at < NOW()
조건에 맞는 행 삭제
TRUNCATE TABLE t데이터 변경TRUNCATE TABLE staging_rows
테이블 비우기, DELETE보다 빠르고 자동 증가 초기화, 대개 롤백 불가
MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE ...데이터 변경MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.v = s.v WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.v)
표준 SQL 병합, Oracle과 SQL Server 지원
REPLACE INTO t (id, v) VALUES (1, 2)데이터 변경REPLACE INTO s (id, v) VALUES (1, 10)
MySQL 전용: 삭제 후 삽입, 자동 증가가 바뀌고 트리거 동작도 다름
CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(50) NOT NULL)테이블 구조CREATE TABLE users (id BIGINT PRIMARY KEY, name VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT NOW())
테이블 생성, 기본 키와 NOT NULL은 기본 제약
CREATE TABLE t (..., CONSTRAINT fk FOREIGN KEY (a_id) REFERENCES a(id))테이블 구조CREATE TABLE orders (id BIGINT PRIMARY KEY, user_id BIGINT, CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id))
외래 키, 참조 무결성 보장하나 쓰기 성능에 영향
CREATE UNIQUE INDEX uk ON t (col)테이블 구조CREATE UNIQUE INDEX uk_email ON users (email)
고유 제약, 동시 쓰기 중복도 차단
ALTER TABLE t ADD COLUMN c TYPE테이블 구조ALTER TABLE users ADD COLUMN phone VARCHAR(20)
컬럼 추가, 대형 테이블에서 기본값 있는 NOT NULL은 잠금 유발
ALTER TABLE t DROP COLUMN c테이블 구조ALTER TABLE users DROP COLUMN phone
컬럼 삭제
ALTER TABLE t ALTER COLUMN c TYPE VARCHAR(100)테이블 구조ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(30)
컬럼 타입 변경, MySQL은 MODIFY COLUMN
ALTER TABLE t RENAME TO t2테이블 구조ALTER TABLE users RENAME TO app_users
테이블 이름 변경
DROP TABLE IF EXISTS t테이블 구조DROP TABLE IF EXISTS tmp_import
테이블 삭제, IF EXISTS로 재실행 오류 방지
CREATE VIEW v AS SELECT ...테이블 구조CREATE VIEW v_paid AS SELECT * FROM orders WHERE status = 1
뷰, 데이터가 아닌 쿼리 정의만 저장
CREATE TABLE t AS SELECT ...테이블 구조CREATE TABLE users_backup AS SELECT * FROM users
쿼리 결과로 테이블 생성, 제약과 인덱스는 복사되지 않음
CREATE INDEX idx ON t (col)인덱스와 실행 계획CREATE INDEX idx_created ON orders (created_at)
일반 인덱스, 조회는 빨라지고 쓰기는 느려짐
CREATE INDEX idx ON t (a, b)인덱스와 실행 계획CREATE INDEX idx_user_time ON orders (user_id, created_at)
복합 인덱스는 최좌측 접두 규칙, b만 조회하면 사용 불가
DROP INDEX idx인덱스와 실행 계획DROP INDEX idx_created
인덱스 삭제, SQL Server는 DROP INDEX table.index
EXPLAIN SELECT ...인덱스와 실행 계획EXPLAIN SELECT * FROM orders WHERE user_id = 1
실행 계획 확인, MySQL은 type과 key, PostgreSQL은 Seq Scan
EXPLAIN ANALYZE SELECT ...인덱스와 실행 계획EXPLAIN ANALYZE SELECT COUNT(*) FROM orders
실제 실행 후 시간과 행 수 제공, EXPLAIN보다 정확
CREATE INDEX idx ON t (a) INCLUDE (b)인덱스와 실행 계획CREATE INDEX idx_user ON orders (user_id) INCLUDE (amount)
커버링 인덱스, 조회 컬럼을 포함해 테이블 재조회 제거
BEGIN트랜잭션과 잠금BEGIN
트랜잭션 시작(START TRANSACTION도 가능)
COMMIT트랜잭션과 잠금COMMIT
트랜잭션 커밋
ROLLBACK트랜잭션과 잠금ROLLBACK
트랜잭션 롤백
SAVEPOINT sp1트랜잭션과 잠금SAVEPOINT sp1
세이브포인트 설정, 부분 롤백 가능
ROLLBACK TO SAVEPOINT sp1트랜잭션과 잠금ROLLBACK TO SAVEPOINT sp1
세이브포인트로 롤백, 이전 작업은 유지
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ트랜잭션과 잠금SET TRANSACTION ISOLATION LEVEL READ COMMITTED
격리 수준 설정
SELECT ... FOR UPDATE트랜잭션과 잠금SELECT * FROM accounts WHERE id = 1 FOR UPDATE
쓰기 잠금, 동시 갱신 방지, 트랜잭션 안에서만 사용
SELECT ... FOR UPDATE SKIP LOCKED트랜잭션과 잠금SELECT * FROM jobs WHERE status = 0 ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED
잠긴 행을 건너뜀, 작업 큐 점유의 표준 패턴
col = NULL -- 永远不成立흔한 함정SELECT * FROM t WHERE deleted_at = NULL
NULL은 미지이므로 어떤 값과 비교해도 미지, IS NULL을 사용
NOT IN (子查询含 NULL) -- 结果为空흔한 함정SELECT * FROM a WHERE id NOT IN (SELECT a_id FROM b)
서브쿼리에 NULL이 하나라도 있으면 NOT IN 결과가 비어 버림, NOT EXISTS 사용
WHERE id = '123' -- 数字列用字符串比较흔한 함정SELECT * FROM users WHERE phone = 13800000000
암시적 형변환은 인덱스를 무력화, 파라미터 타입을 컬럼과 일치
WHERE DATE(created_at) = '2026-01-01'흔한 함정WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'
컬럼에 함수를 쓰면 인덱스 무효, 범위 조건으로 재작성
WHERE a = 1 OR b = 2흔한 함정SELECT * FROM t WHERE a = 1 UNION SELECT * FROM t WHERE b = 2
OR가 여러 컬럼에 걸리면 단일 인덱스 사용 불가, 나눠서 UNION
LIMIT 20 OFFSET 100000흔한 함정SELECT * FROM t WHERE id > 100000 ORDER BY id LIMIT 20
깊은 오프셋은 앞부분을 모두 스캔 후 버림, 키셋 페이징으로 전환
DELETE FROM t -- 没有 WHERE흔한 함정DELETE FROM t WHERE created_at < 2024
WHERE 누락 시 전체 삭제, 같은 조건의 SELECT로 먼저 검증
SELECT ...
FROM t ORDER BY id -- 排序不稳定흔한 함정SELECT id, name FROM t ORDER BY score DESC, id ASC LIMIT 20
동순위 시 순서가 불확정해 페이징이 누락되거나 중복, 고유 컬럼 보조 정렬 필요
一个事务里改几十万行흔한 함정-- 改成每批 1000 行循环提交 DELETE FROM logs WHERE created_at < 2024 LIMIT 1000;
대형 트랜잭션은 잠금을 오래 잡고 롤백 세그먼트를 부풀림, 소량 배치로 분할
字符串比较受排序规则影响흔한 함정SELECT * FROM t WHERE name = 'Ada' -- 大小写敏感取决于 collation
MySQL 기본 콜레이션은 대소문자 무시, 다른 DB는 반대일 수 있음
뭔가 잘못됐거나 빠진 게 있나요?
피드백 보내기