SQL 핵심 문법과 결과 판독
SELECT의 논리적 처리 순서와 NULL, 조인, 서브쿼리, 집계·윈도우 함수, 데이터 변경 문장을 종합한다.
1. 예제 스키마
이후 예시는 다음 두 테이블을 사용한다.
CREATE TABLE department (
dept_id INTEGER PRIMARY KEY,
dept_name VARCHAR(50) NOT NULL
);
CREATE TABLE employee (
emp_id INTEGER PRIMARY KEY,
emp_name VARCHAR(50) NOT NULL,
dept_id INTEGER,
salary DECIMAL(12, 2),
hire_date DATE,
manager_id INTEGER,
FOREIGN KEY (dept_id) REFERENCES department(dept_id),
FOREIGN KEY (manager_id) REFERENCES employee(emp_id)
);
2. SQL 명령의 분류
| 분류 | 목적 | 대표 명령 |
|---|---|---|
| DDL | 객체와 구조 정의 | CREATE, ALTER, DROP, TRUNCATE |
| DML | 데이터 조회·변경 | SELECT, INSERT, UPDATE, DELETE, MERGE |
| DCL | 권한 제어 | GRANT, REVOKE |
| TCL | 트랜잭션 제어 | COMMIT, ROLLBACK, SAVEPOINT |
COMMIT·ROLLBACK은 트랜잭션 상태를 제어하는 TCL로 구분한다.
3. SELECT의 논리적 처리 순서
SQL 문장을 작성하는 순서와 DBMS가 논리적으로 처리하는 순서는 다르다.
작성 순서
SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
논리 처리 순서
FROM/JOIN
↓
WHERE
↓
GROUP BY
↓
HAVING
↓
SELECT
↓
DISTINCT
↓
ORDER BY
↓
행 제한(FETCH/LIMIT 등)
따라서 일반적으로 SELECT에서 만든 별칭을 같은 단계보다 먼저 처리되는 WHERE에서 사용할 수 없다. ORDER BY에서는 별칭 사용을 허용하는 DBMS가 많다.
4. NULL과 3값 논리
NULL은 0이나 빈 문자열이 아니라 알 수 없음 또는 값 없음을 나타낸다. NULL과의 비교 결과는 TRUE나 FALSE가 아니라 UNKNOWN이 된다.
-- 잘못된 조건
WHERE manager_id = NULL
-- 올바른 조건
WHERE manager_id IS NULL
COUNT(*)는 결과 행 수를 센다.COUNT(column)은 해당 열이 NULL이 아닌 행만 센다.- 대부분의 집계함수는 NULL을 제외한다.
NOT IN의 목록이나 서브쿼리 결과에 NULL이 있으면 UNKNOWN 때문에 예상과 다른 결과가 나올 수 있다. 이때NOT EXISTS가 더 안전한 경우가 많다.
5. 조인
employee.dept_id ─────────► department.dept_id
FK PK
SELECT e.emp_name, d.dept_name
FROM employee e
JOIN department d
ON d.dept_id = e.dept_id;
| 조인 | 결과 |
|---|---|
| INNER JOIN | 양쪽 조인 조건을 만족하는 행만 반환 |
| LEFT OUTER JOIN | 왼쪽 행을 모두 보존하고, 일치하지 않는 오른쪽 열은 NULL |
| RIGHT OUTER JOIN | 오른쪽 행을 모두 보존 |
| FULL OUTER JOIN | 양쪽의 불일치 행까지 모두 보존 |
| CROSS JOIN | 모든 행 조합을 생성 |
| SELF JOIN | 같은 테이블을 서로 다른 별칭으로 조인 |
외부조인에서 오른쪽 테이블 조건을 WHERE에 잘못 두면 NULL 확장 행이 제거되어 내부조인처럼 동작할 수 있다.
-- 영업 부서가 아니어도 사원을 모두 유지하려면 조건을 ON에 둔다.
SELECT e.emp_name, d.dept_name
FROM employee e
LEFT JOIN department d
ON d.dept_id = e.dept_id
AND d.dept_name = '영업';
6. 서브쿼리와 집합 연산
-- 전체 평균보다 급여가 높은 사원
SELECT emp_name, salary
FROM employee
WHERE salary > (SELECT AVG(salary) FROM employee);
- 단일행 서브쿼리는
=,>,<등을 사용할 수 있다. - 다중행 서브쿼리는
IN,ANY,ALL,EXISTS를 사용한다. - 상관 서브쿼리는 바깥 행마다 내부 쿼리가 논리적으로 연관된다.
EXISTS는 서브쿼리 결과의 값보다 행 존재 여부를 판단한다.
집합 연산은 열 수와 대응 열의 자료형이 호환되어야 한다.
UNION: 합집합, 중복 제거UNION ALL: 합집합, 중복 보존INTERSECT: 교집합EXCEPT또는MINUS: 차집합
7. 그룹과 윈도우 함수
SELECT dept_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary
FROM employee
WHERE salary IS NOT NULL
GROUP BY dept_id
HAVING COUNT(*) >= 3;
WHERE는 그룹화 전 개별 행을, HAVING은 그룹화 후 그룹을 필터링한다.
윈도우 함수는 행을 하나로 축약하지 않고 각 행에 분석 결과를 붙인다.
SELECT emp_name,
dept_id,
salary,
RANK() OVER (
PARTITION BY dept_id
ORDER BY salary DESC
) AS salary_rank,
AVG(salary) OVER (
PARTITION BY dept_id
) AS dept_avg
FROM employee;
GROUP BY: 여러 행 → 그룹당 한 행
윈도우 함수: 여러 행 → 행 수 유지 + 분석 열 추가
8. 데이터 변경과 트랜잭션
START TRANSACTION;
UPDATE employee
SET salary = salary * 1.05
WHERE dept_id = 10;
SAVEPOINT after_raise;
DELETE FROM employee
WHERE emp_id = 9999;
ROLLBACK TO SAVEPOINT after_raise;
COMMIT;
실제 트랜잭션 시작·세이브포인트 문법과 DDL의 자동 커밋 여부는 DBMS마다 다를 수 있다.
9. 결과 집합을 직접 추적하는 SQL
EMP(id,dept,sal,manager)
(1,A,50,NULL)
(2,A,80,1)
(3,B,70,NULL)
SELECT dept, COUNT(*), COUNT(manager), AVG(sal)
FROM emp
GROUP BY dept;
A는 (2,1,65), B는 (1,0,70)이다. COUNT(*)는 행, COUNT(manager)는 NULL이 아닌 값만 센다.
10. NULL과 조인의 경계조건
-- 서브쿼리에 NULL이 있으면 기대와 달리 결과가 없을 수 있다.
SELECT * FROM A
WHERE x NOT IN (SELECT y FROM B);
-- 반례가 없는지 검사하는 방식이 안전하다.
SELECT * FROM A a
WHERE NOT EXISTS (
SELECT 1 FROM B b WHERE b.y=a.x
);
다만 바깥쪽 a.x가 NULL인 행까지 제외하려는 업무 규칙이라면 a.x IS NOT NULL을 별도로 명시해야 한다. NOT EXISTS와 NOT IN은 NULL 입력까지 언제나 동일한 의미가 아니다.
외부조인에서 보존되지 않는 쪽 조건을 WHERE에 두면 NULL 확장 행이 제거된다. 보존하려는 조건이라면 ON 절 배치를 검토한다.
11. 순위·누적과 재귀 CTE
| 점수 | RANK | DENSE_RANK | ROW_NUMBER |
|---|---|---|---|
| 100 | 1 | 1 | 1 |
| 90 | 2 | 2 | 2 |
| 90 | 2 | 2 | 3 |
| 80 | 4 | 3 | 4 |
동률 행의 ROW_NUMBER를 재현 가능하게 하려면 고유한 추가 정렬 키를 넣는다. 행 단위 누적은 ROWS UNBOUNDED PRECEDING을 명시하면 RANGE의 peer 처리와 구분할 수 있다.
WITH RECURSIVE n(x) AS (
SELECT 1
UNION ALL
SELECT x+1 FROM n WHERE x<4
)
SELECT SUM(x) FROM n; -- 10
12. 논리 처리 순서
FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
SELECT 별칭을 WHERE에서 일반적으로 바로 사용할 수 없는 이유와, HAVING이 집계 뒤에 적용되는 이유를 이 순서로 판단한다.
확인 문제
- COUNT(*)와 COUNT(col)의 차이는?
- NOT IN 서브쿼리에 NULL이 있으면 왜 주의해야 하는가?
- RANK와 DENSE_RANK의 동률 뒤 차이는?
- LEFT JOIN 오른쪽 조건을 WHERE에 두면?
- 일반 SQL 논리 처리 순서는?