SQL의 문법적 순서는 다음과 같지만, DBMS가 실제로 실행하는 내부 처리 순서는 약간 다릅니다(예: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT 순으로 처리된다고 이해하면 됩니다).
SELECT [DISTINCT] select_list
FROM table_source
[JOIN ...]
[WHERE predicate]
[GROUP BY grouping_columns]
[HAVING group_predicate]
[WINDOW ...] -- 일부 DB에서 사용
[ORDER BY ...]
[LIMIT n [OFFSET m] | FETCH FIRST n ROWS ONLY]
컬럼, 리터럴, 함수, 표현식을 지정할 수 있음.
컬럼별 별칭: expr AS alias 또는 expr alias
SELECT id, username AS name, CONCAT(first_name, ' ', last_name) full_name
FROM users;
*(와일드카드): 모든 컬럼 선택 — 디버깅 외에는 사용 지양(네트워크·파싱 비용, 명확성 문제).
DISTINCT: 중복 제거
SELECT DISTINCT country FROM users;
TOP(SQL Server), LIMIT(MySQL/Postgres) 차이 존재 — 아래 LIMIT 참고.
테이블, 뷰, 파생 테이블(서브쿼리), CTE(공통표현식 WITH) 사용 가능.
테이블 별칭 권장:
FROM users u
JOIN orders o ON u.id = o.user_id
파생 테이블(derived table):
SELECT t.user_id, t.cnt FROM (
SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id
) t WHERE t.cnt > 10;
INNER JOIN: 일치하는 행만
LEFT (OUTER) JOIN: 왼쪽 테이블의 모든 행 + 일치하는 오른쪽 행(없으면 NULL)
RIGHT (OUTER) JOIN: 오른쪽 우선(왼쪽과 반대)
FULL OUTER JOIN: 양쪽 모두(모든 RDBMS가 지원하지 않음; MySQL 과거 버전은 미지원—UNION으로 대체)
CROSS JOIN: 카티시안 곱
예:
SELECT u.id, u.username, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
조인 성능 핵심: 조인 키에 적절한 인덱스가 있어야 효율적. 조인 순서와 인덱스 사용을 EXPLAIN으로 확인.
WHERE는 행 필터링. 인덱스 사용 가능 여부(= sargable)에 크게 좌우됩니다.
인덱스 활용을 방해하는 패턴:
컬럼에 함수 적용: WHERE LOWER(name) = 'kim' (인덱스 미사용) — 대신 WHERE name = 'Kim' COLLATE ... 또는 function-based index 사용(지원 시).
선행 와일드카드: WHERE col LIKE '%abc' — 인덱스 무효, LIKE 'abc%'는 인덱스 사용 가능.
NULL 처리: IS NULL, IS NOT NULL 사용.
GROUP BY는 집계 단위(집단)를 만들고, HAVING은 그룹 필터(집계 결과 기반)를 수행.
예:
SELECT user_id, COUNT(*) AS cnt
FROM orders
WHERE status = 'paid'
GROUP BY user_id
HAVING COUNT(*) >= 10;
MySQL 주의: ONLY_FULL_GROUP_BY 모드가 꺼져 있으면 SELECT에 그룹화되지 않은 컬럼 사용 가능하지만(비결정적 결과) 권장하지 않음 — 정식 SQL은 그룹화되지 않은 컬럼의 사용을 허용하지 않습니다.
ORDER BY column [ASC|DESC]로 정렬.
LIMIT n OFFSET m (MySQL/Postgres) — OFFSET이 큰 경우 성능 저하(전체 스캔 후 건너뜀).
키셋(무거운 OFFSET 회피) 페이징(권장):
SELECT * FROM orders WHERE user_id = ? AND created_at < ? ORDER BY created_at DESC LIMIT 20;
SQL Server: TOP n 또는 OFFSET ... FETCH.
MySQL 8+: FETCH FIRST n ROWS ONLY도 지원(표준적 표현).
스칼라 서브쿼리: 한 행·한 칼럼 반환 — SELECT (SELECT MAX(...))
행 서브쿼리: (a,b) = (SELECT ..)
테이블 서브쿼리(From에 사용): 파생테이블
상관 서브쿼리(correlated): 외부 쿼리의 컬럼을 참조 — 실행 비용 높음
SELECT u.id, u.username,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;
EXISTS vs IN
EXISTS(SELECT 1 FROM t2 WHERE t2.x = t1.x) — 존재 여부 체크(일치하는 행 찾자마자 종료).
IN (SELECT col FROM t2) — 내부 결과를 집합으로 만든 뒤 비교; NULL/중복 처리 주의.
성능은 데이터 분포·인덱스에 따라 달라짐. 보통 EXISTS가 유리할 때가 많음(특히 서브쿼리가 상관 서브쿼리일 때).
PARTITION BY / ORDER BY와 함께 집계 아닌 행 단위의 누적/순위 계산 가능(예: ROW_NUMBER(), RANK(), SUM() OVER(...)).
예: 사용자별 최신 3개 주문
SELECT *
FROM (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn
FROM orders o
) t
WHERE rn <= 3;
MySQL 8.0 이상, Postgres, Oracle 등 최근 RDBMS는 지원.
가독성·재사용성 향상, 재귀 쿼리 가능(WITH RECURSIVE).
재귀 CTE 예(조직도 ancestor 탐색):
WITH RECURSIVE mgrs AS (
SELECT id, manager_id, name FROM employees WHERE id = 1
UNION ALL
SELECT e.id, e.manager_id, e.name
FROM employees e JOIN mgrs m ON e.manager_id = m.id
)
SELECT * FROM mgrs;
UNION (중복 제거), UNION ALL (중복 허용).
INTERSECT, EXCEPT는 DBMS마다 지원 여부가 다름(일부 버전의 MySQL은 최근에 추가). 사용 전 DB 버전 확인.
인덱스 설계: WHERE·JOIN·ORDER BY·GROUP BY에 자주 사용되는 컬럼에 인덱스.
복합 인덱스는 좌측(left-most) 규칙을 따름.
커버링 인덱스: 인덱스만으로 쿼리 해결 가능하면 매우 빠름(index-only scan).
EXPLAIN 사용: 쿼리 플랜(사용 인덱스, 예상 스캔 행수 등) 확인.
MySQL: EXPLAIN FORMAT=JSON SELECT ...
SARGability: 컬럼에 직접 조건을 걸어 인덱스를 활용하라(함수 적용·표현식은 인덱스 무효화 가능).
대량 데이터 페이징: OFFSET 큰 페이징은 비효율 → 키셋 페이징 권장.
통계와 옵티마이저: 최신 통계 없으면 비효율 플랜 선택 — ANALYZE TABLE 등으로 통계 갱신.
쿼리 리팩토링: 복잡한 상관 서브쿼리를 JOIN으로 바꾸면 성능 개선될 때가 많음.
SELECT는 기본적으로 락을 걸지 않음(읽기).
FOR UPDATE: 행을 잠구고 이후 트랜잭션에서 수정할 때 사용(예: 잔액 처리).
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
LOCK IN SHARE MODE(MySQL) / FOR SHARE(다른 DB) 등 공유락 옵션 존재.
SKIP LOCKED로 이미 잠긴 행을 건너뛸 수 있음(작업 분산 시 유용).
절대 문자열 연결로 쿼리 만들지 말고 Prepared Statements / Parameterized queries 사용.
최소 권한 원칙: DB 사용자 계정에 필요한 최소 권한만 부여.
기본 조회
SELECT id, username FROM users WHERE email = 'a@example.com';
조인 + 정렬 + 페이징
SELECT u.id, u.username, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.active = 1
ORDER BY o.created_at DESC
LIMIT 20 OFFSET 0;
집계 + HAVING
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5
ORDER BY cnt DESC;
상위 N개 그룹별(윈도우)
SELECT user_id, order_id, total
FROM (
SELECT user_id, id AS order_id, total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn
FROM orders
) t
WHERE rn <= 3;
키셋 페이징(권장)
-- 1페이지
SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20;
-- 다음 페이지(마지막 created_at 값이 last_ts라면)
SELECT * FROM orders WHERE user_id = 123 AND created_at < :last_ts ORDER BY created_at DESC LIMIT 20;
재귀 CTE(예: 트리 구조)
WITH RECURSIVE subtree AS (
SELECT id, parent_id, name FROM categories WHERE id = 2
UNION ALL
SELECT c.id, c.parent_id, c.name FROM categories c JOIN subtree s ON c.parent_id = s.id
)
SELECT * FROM subtree;
EXPLAIN / EXPLAIN ANALYZE(DBMS 지원 시)
slow query log(느린 쿼리 로그)
performance_schema / pg_stat_statements 등 통계 뷰
프로파일링(애플리케이션 레벨) — 쿼리 실행 시간 측정
SELECT * 지양 — 필요한 컬럼만 선택.
인덱스는 쿼리 패턴 기반으로 설계.
복잡한 로직은 CTE로 분리해 가독성 확보.
큰 OFFSET 페이지는 피하고 키셋 페이징 사용.
서브쿼리 vs JOIN 성능 비교 — EXPLAIN으로 확인.
항상 파라미터화하여 SQL 인젝션 방지.
쿼리는 작고 명확하게, 필요시 캐싱(use Redis, memcached).