Devin.KR
로그인

SQL SELECT 기본 문법 - 조회 처리 순서와 별칭 (SQL 초급 1단원)

개발자 조회 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 / MariaDBPostgreSQLOracleSQL 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 로 옮기는 순간 "인사팀 이라는 컬럼이 없다"는 오류를 만난다. 표준에서 큰따옴표는 식별자를 감싸는 기호이고 문자열은 홑따옴표다. 처음부터 홑따옴표로 쓴다.

스스로 확인하기

  1. emp 에서 사원번호와 이름만 조회하되, 결과 헤더를 각각 사번, 사원명 으로 바꿔라.
  2. 모든 사원의 이름과 월평균 급여(연봉을 12로 나눈 값)를 월급 이라는 이름으로 조회하라.
  3. 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단원에서 다룬다.