Devin.KR

SELECT WHERE 결과 예측 - 연산자 우선순위 NOT IN NULL LIKE CASE ORDER BY (SQLD 기본 7장)

개발자 조회 1

이 장에서 배우는 것

6장에서 NULL 과 3값 논리를 정리했다. 이제 2과목 SQL 이다. SELECT·WHERE 의 문법 자체는 『SQL 실전 기초』의 2단원 WHERE3단원 정렬에서 다뤘다. 이 장은 문법을 다시 설명하지 않고, 결과를 실행 전에 손으로 맞히는 연습에 집중한다. 7~12장은 모두 같은 예제 스키마를 쓴다.

  • AND·OR 우선순위, NULL 과 비교, NULL 이 섞인 IN·NOT IN 의 결과 행 수를 예측한다.
  • LIKE 의 와일드카드와 ESCAPE, 단순 CASE 가 NULL 을 못 잡는 이유, 자주 나오는 단일행 함수를 확인한다.
  • ORDER BY 에서 NULL 이 어디에 오는지가 제품마다 다르다는 점과, 별칭·위치 번호 정렬을 정리한다.

핵심 개념

"개발팀이거나 영업팀 중 연봉 4천 넘는 사람"이라는 요청을 받은 개발자가 괄호 없이 조건을 이어 썼다. 결과에 연봉 3,900 인 개발팀원이 섞여 나왔다. SQL 이 틀린 것이 아니라 요청을 SQL 로 옮길 때 우선순위를 놓쳤다. 시험의 결과 예측 문제는 대부분 이런 지점을 노린다.

논리 연산자 우선순위

괄호 → 비교 연산자 → NOTANDOR 순이다. A OR B AND CA OR (B AND C) 다. 헷갈리면 괄호를 쓴다. 괄호는 성능에 아무 영향이 없다.

NULL 과 비교 연산

=, <>, >, BETWEEN, LIKE 모두 한쪽이 NULL 이면 UNKNOWN 이고, WHERE 에서 버려진다. 그래서 bonus <> 0 은 보너스가 NULL 인 사람을 포함하지 않는다. NOT (bonus > 100) 도 NULL 인 사람을 포함하지 않는다. NOT 은 UNKNOWN 을 참으로 바꾸지 못한다.

SELECT 문의 처리 순서

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY 순으로 처리된다. 그래서 SELECT 에서 만든 별칭은 WHERE 에서 쓸 수 없고, ORDER BY 에서는 쓸 수 있다. 처리 순서의 자세한 설명은 『SQL 실전 기초』 1단원에 있다.

SQL 은 FROM 에서 시작해 WHERE, GROUP BY, HAVING 을 거쳐 SELECT 에서 별칭을 만들고 마지막에 ORDER BY 를 한다. 그래서 별칭은 ORDER BY 에서만 쓸 수 있다.

그림 · SELECT 문이 처리되는 순서와 별칭을 쓸 수 있는 곳 — SQL 은 FROM 에서 시작해 WHERE, GROUP BY, HAVING 을 거쳐 SELECT 에서 별칭을 만들고 마지막에 ORDER BY 를 한다. 그래서 별칭은 ORDER BY 에서만 쓸 수 있다.

자주 나오는 단일행 함수

분류함수기억할 점
문자SUBSTR, LENGTH, UPPER, LOWER, TRIM, LTRIM, RTRIM, REPLACESUBSTR 의 시작 위치는 1부터 센다
숫자ROUND, TRUNC, CEIL, FLOOR, MOD, ABS, SIGNROUND(값, -1) 은 일의 자리에서 반올림(Oracle)
NULL 관련COALESCE, NULLIF, NVL, NVL2, ISNULLNULLIF(a, b) 는 a = b 면 NULL, 아니면 a
조건CASE, DECODE단순 CASE 는 = 로 비교하므로 NULL 을 못 잡는다

예제 스키마

작은 회사의 팀·직원·판매·급여 등급이다. 7~12장이 모두 이 스키마를 쓴다. 일부러 뚫어 둔 구멍이 있다. 강예린은 팀이 없고(team_id NULL), 법무팀(40)은 직원이 없으며 도시도 NULL 이다. 보너스는 NULL, 0, 양수가 섞여 있고, 급여 4,300 과 3,900 이 두 명씩 같다.

-- 7~12장 공통 예제 스키마: 작은 회사의 팀·직원·판매·급여 등급 (가상 데이터)
CREATE TABLE team (
  team_id   INTEGER PRIMARY KEY,
  team_name TEXT NOT NULL,
  city      TEXT
);
CREATE TABLE staff (
  staff_id   INTEGER PRIMARY KEY,
  name       TEXT NOT NULL,
  team_id    INTEGER REFERENCES team(team_id),
  manager_id INTEGER REFERENCES staff(staff_id),
  salary     INTEGER NOT NULL,
  bonus      INTEGER,
  hired      TEXT NOT NULL
);
CREATE TABLE sale (
  sale_id    INTEGER PRIMARY KEY,
  staff_id   INTEGER NOT NULL REFERENCES staff(staff_id),
  sale_month TEXT NOT NULL,
  region     TEXT NOT NULL,
  product    TEXT NOT NULL,
  amount     INTEGER
);
CREATE TABLE grade (
  grade INTEGER PRIMARY KEY,
  low   INTEGER NOT NULL,
  high  INTEGER NOT NULL
);
INSERT INTO team VALUES
  (10, '개발', '서울'), (20, '영업', '부산'), (30, '디자인', '서울'), (40, '법무', NULL);
INSERT INTO staff VALUES
  (101, '한지수', 10,   NULL, 5200, NULL, '2019-03-02'),
  (102, '오민재', 10,   101,  4300, 300,  '2021-07-15'),
  (103, '박서윤', 10,   102,  3900, NULL, '2023-01-09'),
  (104, '이도현', 20,   101,  4300, 500,  '2020-11-30'),
  (105, '최하늘', 20,   104,  3600, 0,    '2024-02-19'),
  (106, '정우진', 30,   101,  4800, NULL, '2018-05-21'),
  (107, '강예린', NULL, 104,  3100, 200,  '2025-06-02'),
  (108, '윤태오', 30,   106,  3900, NULL, '2022-09-12');
INSERT INTO sale VALUES
  (1, 102, '2026-07', '서울', 'A', 100),
  (2, 103, '2026-07', '서울', 'B', 50),
  (3, 104, '2026-07', '부산', 'A', 70),
  (4, 105, '2026-07', '부산', 'A', 30),
  (5, 102, '2026-08', '서울', 'A', 120),
  (6, 103, '2026-08', '서울', 'B', NULL),
  (7, 104, '2026-08', '부산', 'B', 80),
  (8, 107, '2026-08', '부산', 'A', 40);
INSERT INTO grade VALUES (1, 0, 3499), (2, 3500, 4199), (3, 4200, 4999), (4, 5000, 99999);
staff_idnameteam_idmanager_idsalarybonushired
101한지수10NULL5200NULL2019-03-02
102오민재1010143003002021-07-15
103박서윤101023900NULL2023-01-09
104이도현2010143005002020-11-30
105최하늘20104360002024-02-19
106정우진301014800NULL2018-05-21
107강예린NULL10431002002025-06-02
108윤태오301063900NULL2022-09-12
team_idteam_namecity
10개발서울
20영업부산
30디자인서울
40법무NULL

SQL과 실행 결과

각 예제를 읽기 전에 결과 행 수를 먼저 적어 보고 비교하는 것을 권한다.

AND 가 OR 보다 먼저

SELECT name, team_id, salary FROM staff
WHERE team_id = 10 OR team_id = 20 AND salary > 4000
ORDER BY staff_id;

SELECT name, team_id, salary FROM staff
WHERE (team_id = 10 OR team_id = 20) AND salary > 4000
ORDER BY staff_id;

실행 결과:

name    team_id  salary
------  -------  ------
한지수  10       5200
오민재  10       4300
박서윤  10       3900
이도현  20       4300
name    team_id  salary
------  -------  ------
한지수  10       5200
오민재  10       4300
이도현  20       4300

첫 쿼리는 "개발팀 전원" 또는 "영업팀이면서 4,000 초과"이므로 박서윤(3,900)이 들어간다. 괄호를 친 두 번째 쿼리가 요청의 뜻이다.

NULL 과 비교하면 행이 빠진다

SELECT (SELECT COUNT(*) FROM staff WHERE bonus = NULL)      AS eq_null,
       (SELECT COUNT(*) FROM staff WHERE bonus IS NULL)     AS is_null,
       (SELECT COUNT(*) FROM staff WHERE bonus <> 0)        AS not_zero,
       (SELECT COUNT(*) FROM staff WHERE NOT (bonus > 100)) AS not_over_100;

실행 결과:

eq_null  is_null  not_zero  not_over_100
-------  -------  --------  ------------
0        4        3         1

보너스가 NULL 인 4명은 <> 0 에도, NOT (> 100) 에도 들어가지 않는다. 마지막 1명은 보너스 0 인 최하늘뿐이다.

IN, NOT IN 과 NULL

SELECT (SELECT COUNT(*) FROM staff WHERE team_id IN (10, NULL))     AS in_10_null,
       (SELECT COUNT(*) FROM staff WHERE team_id NOT IN (20))       AS not_in_20,
       (SELECT COUNT(*) FROM staff WHERE team_id NOT IN (20, NULL)) AS not_in_20_null;

실행 결과:

in_10_null  not_in_20  not_in_20_null
----------  ---------  --------------
3           5          0

IN (10, NULL) 은 10 인 3명을 찾는다. OR 로 풀면 참 OR UNKNOWN = 참이기 때문이다. NOT IN (20) 은 5명이다. 팀이 NULL 인 강예린은 NULL <> 20 이 UNKNOWN 이라 빠진다. NOT IN (20, NULL) 은 0명이다. AND 로 풀면 모든 행에 UNKNOWN 이 하나씩 붙는다.

team_id NOT IN (20, NULL) 은 team_id <> 20 AND team_id <> NULL 이다. 뒤 조건이 늘 UNKNOWN 이라 어떤 행도 참이 될 수 없어 0행이 된다. NULL 을 뺀 NOT IN (20) 은 5행이었다(07-in-null).

그림 · NOT IN 목록에 NULL 이 하나라도 있으면 — team_id NOT IN (20, NULL) 은 team_id <> 20 AND team_id <> NULL 이다. 뒤 조건이 늘 UNKNOWN 이라 어떤 행도 참이 될 수 없어 0행이 된다. NULL 을 뺀 NOT IN (20) 은 5행이었다(07-in-null).

표 · NULL 이 섞인 조건의 실제 행 수 (staff 8명, 보너스 NULL 4명, 팀 NULL 1명)

조건행 수
bonus = NULL0NULL 과의 비교는 UNKNOWN
bonus IS NULL4NULL 검사는 IS NULL 로만
bonus <> 03NULL 4명은 빠진다
NOT (bonus > 100)1NOT UNKNOWN 도 UNKNOWN
team_id IN (10, NULL)310 과 같은 행은 참
team_id NOT IN (20)5팀 NULL 1명은 빠진다
team_id NOT IN (20, NULL)0참이 되는 행이 없다

LIKE 와 ESCAPE

WITH code(c) AS (VALUES ('A_01'), ('AB01'), ('A%02'), ('a_03'))
SELECT c,
       c LIKE 'A_%'              AS plain_underscore,
       c LIKE 'A\_%' ESCAPE '\'  AS escaped,
       c GLOB 'A_*'              AS glob_case_sensitive
FROM code;

실행 결과:

c     plain_underscore  escaped  glob_case_sensitive
----  ----------------  -------  -------------------
A_01  1                 1        1
AB01  1                 0        0
A%02  1                 0        0
a_03  1                 1        0

_ 는 "아무 글자 하나"라서 'A_%' 는 A 로 시작하고 두 글자 이상인 모든 값에 맞는다. 밑줄 문자 자체를 찾으려면 ESCAPE 로 지정한 문자를 앞에 붙인다. 네 번째 행 a_03 이 LIKE 에 걸린 이유는 아래 "표준과 구현의 차이"에서 설명한다.

단일행 함수

SELECT SUBSTR('SQLD시험대비', 5, 2) AS sub,
       LENGTH('SQLD시험')          AS len,
       UPPER('sqld')               AS up,
       '[' || TRIM('  a b  ') || ']' AS trimmed,
       REPLACE('2026-09-24', '-', '') AS replaced;
SELECT ROUND(45.678, 1)  AS r1,
       ROUND(45.678)     AS r0,
       ROUND(45.678, -1) AS r_neg,
       7 / 2            AS int_div,
       7 / 2.0          AS real_div,
       7 % 3            AS modulo;

실행 결과:

sub   len  up    trimmed  replaced
----  ---  ----  -------  --------
시험  6    SQLD  [a b]    20260924
r1    r0    r_neg  int_div  real_div  modulo
----  ----  -----  -------  --------  ------
45.7  46.0  46.0   3        3.5       1

SUBSTR 은 5번째 글자부터 2글자를 자른다. LENGTH 는 바이트가 아니라 글자 수를 센다. ROUND(45.678, -1) 이 50 이 아니라 46.0 인 것은 SQLite 가 음수 자릿수를 0 으로 다루기 때문이다. 정수끼리 나누면 정수 몫만 남는다(7 / 2 = 3). 이 동작은 제품마다 다르다.

단순 CASE 는 NULL 을 못 잡는다

SELECT name, bonus,
       CASE WHEN salary >= 4500 THEN '상'
            WHEN salary >= 4000 THEN '중'
            ELSE '하' END AS band,
       CASE bonus WHEN NULL THEN '없음' ELSE '있음' END AS simple_case,
       CASE WHEN bonus IS NULL THEN '없음' ELSE '있음' END AS searched_case
FROM staff WHERE team_id = 10 ORDER BY staff_id;

실행 결과:

name    bonus  band  simple_case  searched_case
------  -----  ----  -----------  -------------
한지수  NULL   상    있음         없음
오민재  300    중    있음         있음
박서윤  NULL   하    있음         없음

CASE bonus WHEN NULLbonus = NULL 로 비교하므로 절대 참이 되지 않는다. 그래서 보너스가 NULL 인 한지수·박서윤도 '있음'이 됐다. NULL 을 가르려면 검색 CASE(CASE WHEN bonus IS NULL)를 쓴다.

COALESCE 와 NULLIF

SELECT name, salary, bonus,
       salary + bonus              AS raw_sum,
       salary + COALESCE(bonus, 0) AS safe_sum,
       NULLIF(bonus, 0)            AS zero_as_null
FROM staff WHERE team_id IN (10, 20) ORDER BY staff_id;

실행 결과:

name    salary  bonus  raw_sum  safe_sum  zero_as_null
------  ------  -----  -------  --------  ------------
한지수  5200    NULL   NULL     5200      NULL
오민재  4300    300    4600     4600      300
박서윤  3900    NULL   NULL     3900      NULL
이도현  4300    500    4800     4800      500
최하늘  3600    0      3600     3600      NULL

NULL 의 정렬 위치

SELECT name, bonus FROM staff ORDER BY bonus, staff_id;

실행 결과:

name    bonus
------  -----
한지수  NULL
박서윤  NULL
정우진  NULL
윤태오  NULL
최하늘  0
강예린  200
오민재  300
이도현  500
SELECT name, bonus FROM staff ORDER BY bonus DESC, staff_id;

실행 결과:

name    bonus
------  -----
이도현  500
오민재  300
강예린  200
최하늘  0
한지수  NULL
박서윤  NULL
정우진  NULL
윤태오  NULL

SQLite 는 NULL 을 가장 작은 값으로 보고 오름차순 맨 앞, 내림차순 맨 뒤에 둔다. 위치를 직접 정하려면 NULLS FIRST·NULLS LAST 를 쓴다.

SELECT name, bonus FROM staff ORDER BY bonus NULLS LAST, staff_id;

실행 결과:

name    bonus
------  -----
최하늘  0
강예린  200
오민재  300
이도현  500
한지수  NULL
박서윤  NULL
정우진  NULL
윤태오  NULL

별칭, 위치 번호, SELECT 에 없는 컬럼으로 정렬

SELECT name, salary * 12 AS annual
FROM staff WHERE team_id = 10
ORDER BY annual DESC;

SELECT name, salary * 12 AS annual
FROM staff WHERE team_id = 10
ORDER BY 2;

SELECT name FROM staff WHERE team_id = 10 ORDER BY hired;

실행 결과:

name    annual
------  ------
한지수  62400
오민재  51600
박서윤  46800
name    annual
------  ------
박서윤  46800
오민재  51600
한지수  62400
name
------
한지수
오민재
박서윤

세 번째 쿼리처럼 SELECT 목록에 없는 hired 로도 정렬할 수 있다. 단, SELECT DISTINCT 나 GROUP BY 가 있으면 제약이 생긴다(8장).

표준과 구현의 차이

아래 Oracle·SQL Server 동작은 비교 설명이며 실행하지 않았다.

항목SQLite(실행)OracleSQL Server
NULL 정렬(오름차순)맨 앞맨 뒤(NULL 을 가장 큰 값으로 봄)맨 앞
NULLS FIRST / LAST지원지원없음(CASE 로 우회)
LIKE 대소문자ASCII 영문은 구분 안 함구분함데이터베이스 정렬 규칙(collation)에 따름. 기본 설정은 대개 구분 안 함
정수 나눗셈 7 / 233.53
문자열 연결||||, CONCAT+, CONCAT
NULL 대체COALESCE, IFNULLNVL, NVL2, COALESCEISNULL, COALESCE
나머지%MOD(7, 3)%
ROUND(45.678, -1)음수 자릿수를 0 으로 취급5050.000

[구현 차이] 위 LIKE 예제의 a_03 은 SQLite 가 영문 대소문자를 구분하지 않아서 걸렸다. Oracle 에서는 걸리지 않는다. 대소문자를 구분하는 비교가 필요하면 SQLite 에서는 GLOB(와일드카드가 *, ?)을 쓴다. 예제의 마지막 열이 그 결과다.

[구현 차이] 시험은 NULL 정렬 위치를 Oracle 기준으로 묻는 경우가 많다. "Oracle 에서 NULL 은 오름차순 맨 뒤, 내림차순 맨 앞"을 기억하고, SQL Server 와 SQLite 는 반대라는 것까지 함께 기억한다.

시험에서 헷갈리는 지점

판단 1. "WHERE NOT team_id = 10 은 팀이 10 이 아닌 모든 직원(팀이 없는 직원 포함)을 돌려준다"

틀렸다. 팀이 NULL 인 직원은 team_id = 10 이 UNKNOWN 이고, NOT UNKNOWN 도 UNKNOWN 이라 빠진다. 포함하려면 OR team_id IS NULL 을 더한다.

판단 2. "BETWEEN 0 AND 300 은 0 과 300 을 포함한다"

맞다. 양 끝을 포함한다. BETWEEN 300 AND 0 처럼 작은 값을 뒤에 쓰면 0행이 된다.

판단 3. "ORDER BY 에서 SELECT 절의 별칭을 쓸 수 있으므로 WHERE 에서도 쓸 수 있다"

틀렸다. WHERE 는 SELECT 보다 먼저 처리되므로 별칭이 아직 없다. ORDER BY 는 SELECT 뒤에 처리되므로 쓸 수 있다.

연습 문제

모두 예제 스키마 기준이다. 실행하기 전에 결과를 먼저 적는다.

  1. SELECT COUNT(*) FROM staff WHERE NOT team_id = 10 의 결과는?
  2. WHERE bonus BETWEEN 0 AND 300 에 해당하는 직원 이름을 직원 번호 순으로 쓰라.
  3. 직원 101, 104, 105 에 대해 COALESCE(NULLIF(bonus, 0), -1) 의 값을 쓰라.
  4. WHERE team_id = 30 OR bonus > 250 AND salary < 4500 을 만족하는 직원 이름을 직원 번호 순으로 쓰라.
  5. ORDER BY bonus DESC, name 으로 정렬했을 때 첫 3명을 SQLite 기준과 Oracle 기준으로 각각 쓰라.

정답과 해설

1. 4. 팀 20 의 2명과 팀 30 의 2명이다. 팀이 NULL 인 강예린은 빠진다.

SELECT COUNT(*) AS n FROM staff WHERE NOT team_id = 10;

실행 결과:

n
-
4

2. 오민재(300), 최하늘(0), 강예린(200). NULL 인 4명은 BETWEEN 에서 UNKNOWN 이다.

SELECT name FROM staff WHERE bonus BETWEEN 0 AND 300 ORDER BY staff_id;

실행 결과:

name
------
오민재
최하늘
강예린

3. 101 은 NULL → NULLIF 도 NULL → -1. 104 는 500. 105 는 0 → NULLIF 가 NULL 로 바꿈 → -1.

SELECT staff_id, COALESCE(NULLIF(bonus, 0), -1) AS b
FROM staff WHERE staff_id IN (101, 104, 105) ORDER BY staff_id;

실행 결과:

staff_id  b
--------  ---
101       -1
104       500
105       -1

4. team_id = 30 OR (bonus > 250 AND salary < 4500) 이다. 팀 30 의 정우진·윤태오, 그리고 보너스 300·500 에 급여 4,300 인 오민재·이도현. 직원 번호 순으로 오민재, 이도현, 정우진, 윤태오.

SELECT name FROM staff
WHERE team_id = 30 OR bonus > 250 AND salary < 4500
ORDER BY staff_id;

실행 결과:

name
------
오민재
이도현
정우진
윤태오

5. SQLite 는 내림차순에서 NULL 이 맨 뒤라 이도현, 오민재, 강예린이다. Oracle 은 내림차순에서 NULL 이 맨 앞이라 NULL 4명 중 이름순 첫 3명인 박서윤, 윤태오, 정우진이다. Oracle 결과는 SQLite 에서 NULLS FIRST 를 붙여 재현했다(Oracle 자체를 실행한 것은 아니다).

SELECT name, bonus FROM staff ORDER BY bonus DESC, name LIMIT 3;
SELECT name, bonus FROM staff ORDER BY bonus DESC NULLS FIRST, name LIMIT 3;

실행 결과:

name    bonus
------  -----
이도현  500
오민재  300
강예린  200
name    bonus
------  -----
박서윤  NULL
윤태오  NULL
정우진  NULL

다음 장에서는 여러 행을 하나로 줄이는 집계와 GROUP BY 의 결과를 예측한다.

참고 자료

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.