SQL SELECT 기본 문법 - 조회 처리 순서와 별칭 (SQL 초급 1단원)
이 단원에서 배우는 것
SQL 초급 커리큘럼의 첫 단원이다. 앞 단원은 없다. 대신 여기서 만드는 dept(부서)와 emp(사원) 두 테이블을 6단원까지 그대로 쓴다. 아래 실습 스크립트는 한 번 실행해 두고 시작한다. 이번 단원의 주제는 조회문의 뼈대인 SELECT ... FROM 과, 그 문장이 실제로 어떤 순서로 처리되는가이다.
- SELECT 문을 적는 순서와 데이터베이스가 처리하는 순서가 왜 다른지 설명한다.
- 컬럼과 테이블에 별칭을 붙이고, 별칭을 쓸 수 있는 절과 없는 절을 구분한다.
DISTINCT로 중복을 제거하고, NULL 이 어떻게 묶이는지 확인한다.
개념
SELECT 문은 "무엇을 보여줄지"를 맨 앞에 적는다. 그런데 데이터베이스 입장에서는 무엇을 보여줄지 정하기 전에 어디서 읽을지, 어떤 행만 남길지를 먼저 정해야 한다. 그래서 적는 순서와 처리되는 순서가 어긋난다. 이 어긋남을 모르면 "별칭을 분명히 만들었는데 WHERE 에서 못 쓴다" 같은 오류 앞에서 원인을 못 찾는다.
표준 SQL 이 정한 논리적 처리 순서는 다음과 같다. 이 순서는 앞으로 여섯 단원 내내 판단 기준이 된다.
FROM -- 1. 읽을 테이블을 정한다
WHERE -- 2. 행 단위로 걸러낸다 (2단원)
GROUP BY -- 3. 남은 행을 그룹으로 묶는다 (6단원)
HAVING -- 4. 그룹 단위로 걸러낸다 (6단원)
SELECT -- 5. 보여줄 컬럼을 고르고 별칭을 붙인다
ORDER BY -- 6. 마지막으로 정렬한다 (3단원)
여기서 두 가지가 바로 따라 나온다. 첫째, SELECT 절에서 만든 별칭은 WHERE·GROUP BY·HAVING 에서 쓸 수 없다. 그 절들이 먼저 처리되는 시점에는 별칭이 아직 존재하지 않기 때문이다. 둘째, ORDER BY 는 SELECT 다음이므로 별칭을 쓸 수 있다.
덧붙이면 이것은 논리적 순서다. 옵티마이저는 결과만 같으면 실제 읽는 순서를 얼마든지 바꾼다. 하지만 문법이 되느냐 안 되느냐는 언제나 위 순서로 판단하면 맞다.
표준 SQL 문법과 예제
실습 테이블 만들기
부서와 사원, 두 개다. salary 는 연봉을 만원 단위로 저장한다. bonus, dept_id, mgr_id 에는 일부러 NULL 을 섞어 두었다. 2단원의 NULL 실습에 그대로 쓰기 위해서다. 표준 SQL 문법 위주로 썼으므로 MySQL·MariaDB·PostgreSQL·Oracle 에는 그대로 들어간다. SQL Server 만 날짜 리터럴 표기가 달라 한 군데 손봐야 하는데, 아래에서 설명한다.
CREATE TABLE dept (
dept_id INT NOT NULL,
dept_name VARCHAR(30) NOT NULL,
location VARCHAR(30),
PRIMARY KEY (dept_id)
);
CREATE TABLE emp (
emp_id INT NOT NULL,
emp_name VARCHAR(30) NOT NULL,
dept_id INT,
job VARCHAR(20) NOT NULL,
salary DECIMAL(10,2) NOT NULL,
bonus DECIMAL(10,2),
hire_date DATE NOT NULL,
mgr_id INT,
PRIMARY KEY (emp_id)
);
INSERT INTO dept (dept_id, dept_name, location) VALUES
(10, '인사팀', '서울'),
(20, '개발팀', '서울'),
(30, '영업팀', '부산'),
(40, '재무팀', '대전'),
(50, '총무팀', '서울');
INSERT INTO emp (emp_id, emp_name, dept_id, job, salary, bonus, hire_date, mgr_id) VALUES
(1001, '김유신', 20, '개발자', 5200.00, NULL, DATE '2018-03-02', 1005),
(1002, '이순신', 20, '개발자', 4800.00, 300.00, DATE '2019-07-15', 1005),
(1003, '강감찬', 30, '영업', 3900.00, 500.00, DATE '2020-01-06', 1006),
(1004, '을지문덕', 30, '영업', 4100.00, NULL, DATE '2017-11-20', 1006),
(1005, '유관순', 20, '팀장', 7200.00, 1000.00, DATE '2015-04-01', NULL),
(1006, '안중근', 30, '팀장', 6800.00, 900.00, DATE '2016-09-13', NULL),
(1007, '윤봉길', 10, '인사', 3600.00, NULL, DATE '2021-05-04', NULL),
(1008, '신채호', 10, '인사', 3300.00, 200.00, DATE '2022-02-14', 1007),
(1009, '홍범도', NULL, '개발자', 4400.00, NULL, DATE '2023-08-01', 1005),
(1010, '김구', 40, '재무', 5000.00, NULL, DATE '2019-12-02', NULL);
사원이 한 명도 없는 부서(50 총무팀)와, 부서가 정해지지 않은 사원(1009 홍범도)이 한 건씩 들어 있다. 6단원에서 이 두 건이 결과를 어떻게 바꾸는지 보게 된다. 참고로 DATE '2018-03-02' 는 표준 날짜 리터럴이고 MySQL·MariaDB·PostgreSQL·Oracle 이 모두 받는다. SQL Server 는 이 표기 자체를 지원하지 않아 Incorrect syntax near '2018-03-02' 로 죽으므로, DATE 를 떼고 '2018-03-02' 라고만 쓴다. MySQL 도 이 짧은 형태를 받는다.
가장 단순한 조회
SELECT emp_id, emp_name, salary
FROM emp;
컬럼 목록을 * 로 바꾸면 전체 컬럼이 나온다. 확인용으로는 편하지만 운영 코드에는 쓰지 않는다. 이유는 아래 함정 절에서 다룬다.
별칭(alias)
별칭은 두 군데에 붙인다. 컬럼에 붙이면 결과 헤더 이름이 바뀌고, 테이블에 붙이면 그 뒤로 짧게 참조할 수 있다. 컬럼 별칭에는 AS 를 쓰고, 테이블 별칭에는 쓰지 않는 습관을 들이면 어느 DB 에서나 안전하다.
SELECT e.emp_id AS 사번,
e.emp_name AS 이름,
e.salary AS 현재연봉,
e.salary * 1.05 AS 인상후연봉
FROM emp e;
계산식이나 함수 결과에는 별칭을 반드시 붙인다. 붙이지 않으면 헤더가 salary * 1.05 같은 식 그대로 나와서, 애플리케이션에서 컬럼명으로 값을 꺼낼 때 깨진다.
중복 제거
SELECT DISTINCT job
FROM emp;
+--------+
| job |
+--------+
| 개발자 |
| 팀장 |
| 영업 |
| 인사 |
| 재무 |
+--------+
5 rows in set (0.00 sec)
DISTINCT 는 SELECT 절 전체에 걸린다. SELECT DISTINCT job, dept_id 는 job 만 중복 제거하는 것이 아니라 (job, dept_id) 조합으로 중복을 제거한다. 그리고 ORDER BY 를 붙이지 않은 결과의 행 순서는 어떤 DB 에서도 보장되지 않는다. 위 출력의 순서도 우연히 그렇게 나온 것일 뿐이다.
DB별 차이
| 항목 | MySQL / MariaDB | PostgreSQL | Oracle | SQL Server |
|---|---|---|---|---|
| 테이블 별칭에 AS | 가능 | 가능 | 불가 (FROM emp e 만) | 가능 |
FROM 절 생략 (SELECT 1) | 가능 | 가능 | 23ai 미만은 FROM dual 필요 | 가능 |
| 공백·예약어가 든 별칭 | 백틱 또는 큰따옴표 | 큰따옴표 | 큰따옴표 | 큰따옴표 또는 대괄호 |
| 문자열에 큰따옴표 | 기본 허용(비표준) | 불가(식별자로 해석) | 불가 | 불가(기본 설정) |
| 따옴표 없는 식별자 처리 | 테이블명은 OS 파일시스템을 따름(리눅스는 대소문자 구분) | 소문자로 접음 | 대문자로 접음 | 콜레이션을 따름 |
실무에서 자주 틀리는 것
1. SELECT 의 별칭을 WHERE 에서 참조한다
-- 오류. WHERE 는 SELECT 보다 먼저 처리된다
SELECT salary * 1.05 AS new_salary
FROM emp
WHERE new_salary > 5000;
MySQL 은 ERROR 1054 (42S22): Unknown column 'new_salary' in 'where clause' 를 낸다. 해결책은 조건식을 다시 쓰거나 한 겹 감싸는 것이다. 다만 감싸서 계산 결과에 조건을 걸면 인덱스를 못 쓴다. 가능하면 원본 컬럼 쪽으로 식을 옮긴다.
-- 나쁨: 계산 결과에 조건 → salary 인덱스를 못 쓴다
SELECT * FROM (SELECT emp_id, salary * 1.05 AS new_salary FROM emp) t
WHERE t.new_salary > 5000;
-- 좋음: 조건을 원본 컬럼으로 옮긴다
SELECT emp_id, salary * 1.05 AS new_salary
FROM emp
WHERE salary > 5000 / 1.05;
2. 운영 코드에 SELECT * 를 쓴다
세 가지가 한꺼번에 걸린다. 나중에 컬럼이 추가되면 결과 컬럼 개수가 바뀌어 애플리케이션 매핑이 깨진다. 안 쓰는 TEXT·BLOB 컬럼까지 읽어 네트워크와 버퍼 풀을 낭비한다. 필요한 컬럼만 담은 인덱스로 끝낼 수 있는 조회가 테이블 본체까지 읽게 된다. 조회 화면 하나에 필요한 컬럼은 보통 서너 개다. 그 서너 개만 적는다.
3. 문자열을 큰따옴표로 감싼다
WHERE dept_name = "인사팀" 은 MySQL 기본 설정에서 잘 돌아간다. 그래서 그대로 두었다가 PostgreSQL 이나 Oracle 로 옮기는 순간 "인사팀 이라는 컬럼이 없다"는 오류를 만난다. 표준에서 큰따옴표는 식별자를 감싸는 기호이고 문자열은 홑따옴표다. 처음부터 홑따옴표로 쓴다.
스스로 확인하기
- emp 에서 사원번호와 이름만 조회하되, 결과 헤더를 각각
사번,사원명으로 바꿔라. - 모든 사원의 이름과 월평균 급여(연봉을 12로 나눈 값)를
월급이라는 이름으로 조회하라. - emp 에 실제로 존재하는 부서번호 목록을 중복 없이 조회하라. 결과가 몇 행인지, 왜 그런지 설명하라.
-- 1
SELECT emp_id AS 사번, emp_name AS 사원명
FROM emp;
-- 2
SELECT emp_name, salary / 12 AS 월급
FROM emp;
-- 3
SELECT DISTINCT dept_id
FROM emp;
-- 5행. 10, 20, 30, 40 네 개와 NULL 한 개다.
-- DISTINCT 는 NULL 들도 서로 같은 값으로 보고 하나로 묶는다.
-- 정작 값 비교에서는 NULL = NULL 이 참이 아니다. 이 어긋남은 2단원에서 다룬다.