EasyDebug.NET

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는 반대일 수 있음

뭔가 잘못됐거나 빠진 게 있나요?

피드백 보내기
작성자의 블로그
아이디어 보내기