쿼리 한 줄을 종이 위에서 실행해 결과 표를 그려 내는 장이다. 문법 암기가 아니라 실행 순서가 답을 만든다
왜 이걸 하나 — 2회에서 가장 약했던 묶음이 SQL 이다. 이 장이 그 5~15점을 되찾는 첫 장이다.
11회분 220문항 중 SQL 조회는 13문항이다. 11회 중 8회에 나왔고, 2026년 2회에는 3문항이 나왔다. 정의·조작·제어(E09)와 DB 이론(E10)까지 합치면 SQL·DB 묶음이 39문항, 18%다.
| 꼴 | 몇 문항 | 무엇을 적나 | 보기 |
|---|---|---|---|
| 실행 결과 — 행 수 · 값 하나 | 8 | COUNT 의 값 하나 | 2024-1회 18번 · 2024-3회 3번 · 2025-3회 11번 · 12번 · 2026-1회 9번 · 13번 · 2026-2회 11번 · 18번 |
| 실행 결과 — 표 | 3 | 열 이름과 행을 표 꼴로 | 2023-3회 8번 · 2024-1회 13번 · 2025-1회 7번 |
| 문법 빈칸 | 1 | LIKE 패턴 · DESC | 2026-2회 6번 |
| 쿼리 작성 | 1 | SELECT 문 한 줄 전부 | 2023-1회 16번 |
→ 13문항 중 11문항이 「실행 결과」다. 문법을 아는 것과 결과를 정확히 세는 것은 다른 일이고, 틀리는 곳은 거의 언제나 NULL · 중복 · AND/OR 우선순위 · 열 이름 누락이다.
E01 진단의 SQL 4문항 중 3문항이 이 장 것이었다. 2회에서 SQL 이 샜다면 그 원인은 문법이 아니라 표를 손으로 돌리는 절차가 없었기 때문일 가능성이 크다. 이 장은 그 절차를 03절에서 한 줄씩 세운다.
문항 수는 이 노트의 문제 데이터(`_제작/기출/*.json`)에서 ch=E08 인 것을 센 값이다(2026-09-14). 회차별 복원본은 grandlife.co.kr, 2023~2026년 1회 문항은 이전 자료의 복원을 옮긴 것이라 문항 번호가 복원본마다 다를 수 있다.
왜 이걸 하나 — 시험 전날 이 절만 다시 본다. 답안에 적는 표기까지 여기 있다.
왜 이걸 하나 — 답이 왜 그 값인지 밑에서부터 세워야, 표가 바뀌어도 같은 절차로 푼다.
SELECT 문은 위에서 아래로 읽히지 않는다. 데이터베이스는 먼저 FROM 으로 표를 집고, WHERE 로 행을 버리고, GROUP BY 로 묶고, HAVING 으로 묶음을 버리고, 그제야 SELECT 의 열을 고르고, 마지막에 ORDER BY 로 줄을 세운다. 그래서 손으로 풀 때도 이 순서로 표를 깎아야 중간 행 수가 맞는다.
2026년 2회 6번의 학생 표(5행)에 아래 쿼리를 돌리는 과정을 단계별로 세어 보자.
| 단계 | 무엇을 하나 | 남는 것 |
|---|---|---|
| FROM 학생 | 표를 집는다 | 5행 |
| WHERE 이름 LIKE '이%' | 이 로 시작하는 이름만 | 이순신(3) · 이상(4) · 이정재(3) → 3행 |
| GROUP BY 학년 | 학년별로 묶는다 | 3학년 묶음(2행) · 4학년 묶음(1행) → 묶음 2개 |
| HAVING COUNT(*) >= 2 | 2행 미만 묶음을 버린다 | 3학년 묶음만 → 1묶음 |
| SELECT 학년, COUNT(*) | 열을 고른다 | 3, 2 |
| ORDER BY 학년 DESC | 줄을 세운다 | 한 행이라 그대로 |
→ WHERE 와 HAVING 의 차이가 여기서 보인다. WHERE 는 행을 거르고 HAVING 은 묶음을 거른다. 그래서 HAVING 에는 COUNT · AVG 같은 집계가 올 수 있고 WHERE 에는 못 온다.
별칭도 같은 이유로 갈린다. AS 인원 은 SELECT 단계에서 붙는 이름이라 그보다 먼저 도는 WHERE 나 HAVING 에서는 존재하지 않는다. 문제에 HAVING 인원 >= 2 처럼 적혀 있다면 그 DBMS 가 허용하는지 여부와 별개로, 답안에는 HAVING COUNT(*) >= 2 로 적는 것이 안전하다.
두 표를 잇는 원리는 하나다. 먼저 모든 행의 짝을 만들고(m × n), ON 조건이 참인 짝만 남긴다. 이것이 INNER JOIN 이고, FROM 에 두 표를 쉼표로 놓고 WHERE 에 등호를 쓴 것도 같은 결과다.
OUTER 는 거기에 한 가지를 더한다. 짝을 못 찾은 행을 버리지 않고 상대편 열을 NULL 로 채워 남긴다. LEFT 는 왼쪽 표의 행을, RIGHT 는 오른쪽 표의 행을, FULL 은 양쪽을 남긴다.
2026년 2회 18번의 A(3행) · B(4행)로 RIGHT OUTER JOIN 의 결과를 그리면 이렇다.
| A.id | A.v | B.id | B.w | 어떻게 생겼나 |
|---|---|---|---|---|
| 1 | 10 | 1 | 100 | B 의 1 에 A 의 첫 행이 짝 |
| 1 | 20 | 1 | 100 | B 의 1 에 A 의 둘째 행도 짝 — B 행이 두 번 나온다 |
| 2 | 30 | 2 | 200 | 짝 하나 |
| NULL | NULL | 3 | 300 | A 에 3 이 없어 A 쪽이 NULL |
| NULL | NULL | 4 | 400 | A 에 4 가 없어 A 쪽이 NULL |
→ 5행이다. B 의 4행보다 많은 이유는 id 1 이 A 에 두 번 있어서고, 여기에 WHERE A.id IS NULL 을 걸면 아래 2행만 남아 답이 2 가 된다. 행 수를 셀 때는 한쪽에 같은 키가 여러 번 있는지를 먼저 본다.
같은 표로 LEFT 를 돌리면 A 의 3행이 각각 짝을 찾아 3행이고, INNER 도 3행이다. CROSS 는 조건 없이 3 × 4 = 12행이다. 2025년 3회 11번의 CROSS JOIN … WHERE A.NAME LIKE B.RULE 은 이 12행 같은 곱을 만든 뒤 LIKE 로 거르는 꼴이다.
NULL 은 0 도 빈 문자열도 아니다. 그래서 x > NULL 이나 x = NULL 은 참도 거짓도 아닌 모른다(UNKNOWN)가 되고, WHERE 는 참인 행만 통과시키므로 그 행은 빠진다. NULL 인 행을 잡으려면 IS NULL 뿐이다.
집계에서는 규칙 세 개가 전부다. COUNT(*) 는 행 자체를 세니 NULL 도 센다. COUNT(열) · SUM · AVG · MAX · MIN 은 그 열이 NULL 인 행을 계산에서 아예 뺀다. 맞는 행이 하나도 없으면 COUNT 는 0 이지만 AVG · SUM 은 NULL 이다.
2025년 3회 12번이 이 세 규칙을 한 문제에 담았다. 표 5행에 WHERE COL1 IN (2, 3) OR COL2 IN (3, 5) 는 5행을 다 통과시키지만, COUNT(COL2) 는 COL2 가 NULL 인 첫 행을 빼서 4 다. COUNT(*) 였다면 5 다.
세 번째 규칙이 상관 서브쿼리와 만나면 함정이 된다. 2026년 2회 11번에서 x = 10 인 행은 「자기보다 작은 x」가 없어 안쪽 AVG 가 NULL 이 되고, 10 > NULL 이 통과하지 못해 그 행이 빠진다. 「빈 집합의 평균은 0」이라고 생각하면 답이 4 로 틀린다.
서브쿼리는 세 꼴뿐이다. 값 하나를 돌려주면 = · > 로 비교하고, 여러 값이면 IN 으로 집합에 드는지 보고, 바깥 행의 값을 안에서 쓰면 상관 서브쿼리라 바깥 행마다 안쪽을 다시 계산한다. 어느 꼴이든 푸는 순서는 같다 — 가장 안쪽을 값으로 바꾸고, 그 값을 바깥에 넣는다.
2024년 3회 3번은 서브쿼리가 3겹이다. 가장 안쪽 GROUP BY project_id HAVING COUNT(*) < 2 → 20. 그 위 SELECT name … WHERE project_id IN (20) → Beta. 바깥 JOIN 에서 p.name IN ('Beta') → Charlie 1행. 세 겹이라도 한 겹씩 값으로 바꾸면 한 줄짜리 문제가 된다.
상관 서브쿼리는 표로 푼다. 바깥 행을 한 줄씩 놓고, 그 행의 값으로 안쪽을 계산한 결과를 옆 칸에 적고, 조건이 참인지를 마지막 칸에 적는다. 2026년 2회 11번을 이 표로 풀면 x = 10 · 20 · 30 · 40 네 줄에 AVG 가 NULL · 10 · 13.3 · 18.75 로 적히고, 참인 줄이 3개다.
KIND = 'A' AND SCORE >= 80 OR KIND = 'C' 는 (A 이면서 80 이상) 또는 (C) 다. 2024년 1회 18번의 답 1 은 이 우선순위에서 나왔고, 괄호를 옮긴 훈련 1번은 0 이 된다.왜 이걸 하나 — 03절의 절차를 기출 13문항과 예상 2문항에 그대로 대 본다. 회차순이라 꼴이 어떻게 반복되는지도 보인다.
푸는 법은 하나다. 표를 종이에 옮기고, 실행 순서대로 행을 지워 가며 남는 행 수를 적고, 마지막에 SELECT 열만 골라 답 칸에 옮긴다. 머릿속으로만 세지 않는다 — 2회에서 샌 점수는 거기서 나왔을 가능성이 가장 크다.
성적 테이블에서 과목이름별 최소점수·최대점수를 구하되, 평균 점수가 90 이상인 과목만 조회하는 SQL을 작성하시오. (컬럼 별칭: 최소점수, 최대점수)
SELECT 과목이름, MIN(점수) AS 최소점수, MAX(점수) AS 최대점수 FROM 성적 GROUP BY 과목이름 HAVING AVG(점수) >= 90;답 — SELECT 과목이름, MIN(점수) AS 최소점수, MAX(점수) AS 최대점수 FROM 성적 GROUP BY 과목이름 HAVING AVG(점수) >= 90;
원리 — "과목이름별"이라는 말이 나오면 GROUP BY 과목이름 이다. 같은 과목의 행을 한 묶음으로 접고, 묶음마다 MIN · MAX · AVG 같은 집계를 낸다. 묶음에 조건을 거는 자리는 WHERE 가 아니라 HAVING 이다 — WHERE 는 묶기 전 행 하나하나에, HAVING 은 묶은 뒤 집계값에 건다. "평균이 90 이상인 과목만"은 묶은 뒤의 조건이라 HAVING AVG(점수) >= 90 이다. AS 는 결과 열의 이름표(별칭)를 붙인다.
따라가기
헷갈리는 자리 — HAVING 자리에 WHERE AVG(점수) >= 90 을 쓰는 것. WHERE 안에는 집계 함수를 못 쓴다(오류). 별칭 이름을 문제가 준 그대로(최소점수 · 최대점수) 적어야 하고, "이상"은 >= 다(> 는 초과). GROUP BY 에 없는 열을 SELECT 에 넣으면 안 된다는 규칙도 같이 기억한다.
답의 SQL 을 sqlite 3.49 에서 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 열 이름이 실제와 다를 수 있다.
다음 두 테이블과 SQL문의 실행 결과를 순서대로 쓰시오.
| T1.A |
|---|
| 1 |
| 2 |
| 3 |
| 3 |
| T2.A |
|---|
| 2 |
| 4 |
답 — 열 A 에 4 / 3 / 2 / 1 네 행. UNION 은 중복을 지우고, DESC 는 큰 값부터다.
원리 — UNION 은 두 SELECT 의 결과를 위아래로 이어 붙이되 같은 행은 하나만 남긴다(집합의 합집합). 중복까지 다 남기려면 UNION ALL 이다. T1 의 3 이 두 번 있어도 하나로 접히고, T2 의 2 는 T1 에도 있으니 하나만 남는다. ORDER BY A DESC 는 합친 결과 전체를 A 기준 내림차순으로 정렬한다 — 정렬은 마지막에 한 번만 걸린다.
따라가기
헷갈리는 자리 — 3 을 두 번, 2 를 두 번 남겨 여섯 행으로 적는 것(UNION ALL 의 결과). DESC 를 오름차순으로 읽어 1 2 3 4 로 적는 것. 답을 표 꼴로 요구하면 열 이름 A 까지 적는다. UNION 을 쓰려면 두 SELECT 의 열 수와 자료형이 같아야 한다는 것도 같은 묶음의 규칙이다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 SQL을 실행한 결과를 쓰시오.
SAMPLE
| A | B |
|---|---|
| 1 | a |
| 2 | b |
| 3 | c |
답 — 열 B 에 a / b 두 행. 안쪽 SELECT 가 A ≤ 2 인 A 값 {1, 2} 를 만들고, 바깥은 A 가 그 안에 있는 행의 B 를 뽑는다.
원리 — 괄호 안 SELECT(서브쿼리)가 먼저 계산돼 값의 목록이 되고, A IN (목록) 은 "A 가 목록 중 하나와 같다"는 조건이다. 안쪽이 같은 테이블을 읽어도 상관없다 — 먼저 목록을 만들고 그다음 바깥이 그 목록으로 거른다고 두 단계로 읽으면 된다. 결과 열은 SELECT 뒤의 B 하나뿐이다.
따라가기
헷갈리는 자리 — 안쪽 SELECT 가 A 를 뽑는다고 답에 A 값 1, 2 를 적는 것. 결과 열은 바깥 SELECT 의 B 다. 조건이 < 2 였다면 a 한 행뿐이다 — 등호를 본다. IN 대신 = (SELECT …) 를 쓰면 서브쿼리가 두 행을 돌려줘 오류가 난다 — 여러 값에는 IN 이다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 SQL문의 실행 결과를 쓰시오.
RESULT
| ID | KIND | SCORE |
|---|---|---|
| 1 | A | 70 |
| 2 | B | 90 |
| 3 | A | 50 |
| 4 | C | 95 |
답 — 1. AND 가 OR 보다 먼저 묶이므로 "(KIND = 'A' 이고 SCORE ≥ 80) 또는 KIND = 'C'" 이고, 앞 조건은 없고 뒤 조건은 한 행이다.
원리 — SQL 에서 AND 는 OR 보다 우선순위가 높다. 괄호가 없으면 a AND b OR c 는 (a AND b) OR c 로 읽는다. 그래서 이 조건은 "A 이면서 80점 이상"인 행과 "C 인 행" 둘을 합친 것이다. A 의 점수는 70 과 50 이라 앞쪽은 0 행, C 는 4번 행 하나다. COUNT(*) 는 조건을 통과한 행 수다.
따라가기
헷갈리는 자리 — 왼쪽부터 묶어 KIND = 'A' AND (SCORE >= 80 OR KIND = 'C') 로 읽으면 0 행이 된다. 반대로 괄호를 (… OR KIND = 'C') 에 넣어 3 행으로 세는 것도 오답이다. AND 먼저, 그다음 OR — 이 우선순위 하나를 묻는 문제다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 테이블과 SQL문을 보고 출력값을 쓰시오.
employee
| employee_id | name | project_id |
|---|---|---|
| 1 | Alice | 10 |
| 2 | Bob | 10 |
| 3 | Charlie | 20 |
| 4 | Dana | 30 |
| 5 | Ethan | 30 |
project
| project_id | name |
|---|---|
| 10 | Alpha |
| 20 | Beta |
| 30 | Gamma |
답 — 1. 인원이 2 명 미만인 프로젝트는 20(Beta)뿐이고, 그 프로젝트에 속한 직원은 Charlie 한 명이다.
원리 — 서브쿼리가 세 겹이면 가장 안쪽부터 값을 만들어 바깥으로 올린다. 가장 안쪽은 employee 를 project_id 별로 묶어(GROUP BY) 인원이 2 명 미만인(HAVING COUNT(*) < 2) project_id 를 고른다. 가운데는 그 id 의 프로젝트 이름을 뽑는다. 바깥은 employee 와 project 를 project_id 로 이어 붙인(JOIN … ON) 뒤 이름이 그 목록에 있는 행만 센다. 각 층의 결과를 표로 적어 두면 헷갈리지 않는다.
따라가기
헷갈리는 자리 — HAVING COUNT(*) < 2 를 "2 명 이하"로 읽어 10 · 30 까지 넣는 것(<= 가 아니다). JOIN 결과가 다섯 행인 것을 답으로 적는 것 — WHERE 가 거른 뒤의 수다. 층마다 무엇을 돌려주는지(id → 이름 → 행 수)를 적는다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 두 테이블과 SQL문을 보고 조회 결과를 테이블 형태로 쓰시오.
직원
| 직원번호 | 이름 |
|---|---|
| 101 | 이순신 |
| 102 | 강감찬 |
| 103 | 김유신 |
보상
| 직원번호 | 성과급 |
|---|---|
| 101 | 1000 |
| 102 | 300 |
| 103 | 450 |
답 — 열 이름 · 성과급, 한 행 이순신 · 1000. 직원번호가 같은 행끼리 이어 붙인 뒤 성과급 500 이상만 남긴다.
원리 — FROM 직원 e, 보상 b 는 두 테이블을 전부 짝지은 뒤(3 × 3 = 9 행) WHERE e.직원번호 = b.직원번호 로 번호가 같은 짝만 남기는 옛 방식의 조인이다(JOIN … ON 과 같다). 남은 세 짝 중 b.성과급 >= 500 인 것은 101 번(1000)뿐이다. SELECT 에서 e.이름, b.성과급 두 열만 뽑으므로 결과 표는 열 둘, 행 하나다.
따라가기
헷갈리는 자리 — 답에 직원번호 열을 넣는 것(SELECT 에 없다). 450 을 "500 가까우니" 넣는 것 — 이상은 500 부터다. 표 꼴로 쓰라고 했으니 열 이름 줄을 먼저 적는다. 열 이름은 별칭이 없으니 원래 이름(이름 · 성과급)이고, e · b 같은 테이블 별칭은 결과에 안 나온다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 A, B 테이블과 SQL문을 보고 출력되는 행의 수를 쓰시오.
A 테이블
| NAME |
|---|
| Smith |
| Allen |
| Scott |
B 테이블
| RULE |
|---|
| S% |
| %T% |
답 — 4. 이름 셋 × 규칙 둘 = 여섯 짝 중 LIKE 가 맞는 짝은 Smith 둘, Scott 둘이다.
원리 — CROSS JOIN 은 두 테이블의 모든 짝을 만든다(3 × 2 = 6 행). LIKE 는 무늬 맞추기로, % 는 "아무 글자 0개 이상"이다. 'S%' 는 S 로 시작, '%T%' 는 어디든 T 가 들어 있음이다. 이 문제의 정답 4 는 대소문자를 구분하지 않는 채점(Smith 의 t · Scott 의 tt 가 T 에 맞음)을 전제로 한다 — MySQL · SQL Server 의 기본이 그렇다.
따라가기
헷갈리는 자리 — 대소문자를 구분하면(PostgreSQL · sqlite 기본) %T% 가 하나도 안 맞아 2 가 된다. 시험은 보통 구분하지 않는 쪽으로 낸다는 것을 알고 4 로 적되, 문제에 "대소문자 구분" 문구가 있으면 그것을 따른다. CROSS JOIN 의 행 수 6 을 그대로 적는 것도 오답이다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 SQL문의 실행 결과를 쓰시오. 테이블 A의 데이터가 다음과 같을 때 조회 결과를 구하시오.
A
| COL1 | COL2 |
|---|---|
| 2 | NULL |
| 3 | 6 |
| 2 | 3 |
| NULL | 3 |
| 4 | 5 |
답 — 4. WHERE 로 다섯 행이 다 걸리지만 COUNT(COL2) 는 NULL 을 세지 않아 4 다.
원리 — COUNT(*) 는 행 수를 세고, COUNT(열) 은 그 열이 NULL 이 아닌 행만 센다. NULL 은 "값 없음"이라 개수에 안 들어간다. WHERE 는 COL1 IN (2, 3) 또는 COL2 IN (3, 5) 인 행을 남기는데, NULL 은 어느 IN 에도 맞지 않지만 다른 쪽 조건으로 걸릴 수 있다. 행마다 두 조건을 따로 보고 하나라도 참이면 남긴다.
따라가기
헷갈리는 자리 — COUNT(*) 로 읽어 5 를 적는 것. 괄호 안이 열 이름이면 NULL 을 뺀다. (NULL, 3) 행을 "NULL 이 있으니 제외"로 보는 것 — COL2 = 3 으로 걸린다. NULL 규칙은 두 방향(조건에서는 거짓, 개수에서는 제외) 모두 본다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
STUDENT 테이블(총 200명, 학과 3개)에서: ① SELECT COUNT(*) FROM STUDENT; ② SELECT COUNT(DISTINCT 학과) FROM STUDENT; ③ SELECT COUNT(DISTINCT 학과) FROM STUDENT WHERE 학과='전산과'; 각각의 결과는?
답 — ① 200 ② 3 ③ 1. COUNT(*) 는 전체 행, COUNT(DISTINCT 학과) 는 서로 다른 학과 수, WHERE 로 한 학과만 남기면 그 수는 1 이다.
원리 — COUNT(*) 는 조건을 통과한 행의 수다(200 명 → 200). DISTINCT 는 중복을 지운다 — 200 행의 학과 값 중 서로 다른 것만 세면 학과 수 3 이다. ③ 은 WHERE 로 전산과 행만 남긴 뒤 그 안의 서로 다른 학과 값을 세니, 전산과 하나뿐이라 1 이다. 행 수(몇 명)와 종류 수(몇 학과)를 나누는 것이 전부다.
따라가기
헷갈리는 자리 — ③ 을 "전산과 학생 수"로 읽고 인원을 적는 것. DISTINCT 학과이므로 종류 수 1 이다. ② 를 200 으로 적는 것 — DISTINCT 가 있다. 세 문항이 COUNT 의 세 가지 쓰임을 정확히 나눠 묻는다. 학과 값에 NULL 인 학생이 있었다면 COUNT(DISTINCT 학과)는 NULL 을 안 세므로 ② 가 달라질 수 있다 — 문제는 그런 행이 없다고 본다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 두 테이블과 SQL문을 보고 반환되는 값을 쓰시오.
employee 테이블
| dep_id |
|---|
| 10 |
| 20 |
| 20 |
dept 테이블
| dept_id | budget |
|---|---|
| 10 | 100 |
| 20 | 300 |
| 30 | 200 |
답 — 2. 예산 평균 200 보다 큰 부서는 20(300)뿐이고, 그 부서 직원이 두 명이다.
원리 — 서브쿼리 SELECT AVG(budget) FROM dept 는 하나의 값(200)을 돌려주고, 바깥 조건은 d.budget > 200 이 된다. JOIN … ON e.dep_id = d.dept_id 는 직원 행마다 부서 정보를 붙인다 — 직원이 세 행이니 조인 결과도 세 행이고, 그중 부서 예산이 200 을 넘는 행만 센다. 부서 30 은 직원이 없어 조인에 아예 안 나온다.
따라가기
헷갈리는 자리 — 부서 수로 세어 1(부서 20 하나)을 적는 것. COUNT(*) 는 조인된 직원 행을 센다. 200 을 "이상"으로 보고 부서 30 까지 넣는 것 — > 는 초과이고, 30 은 직원이 없어 어차피 0 행이다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 [학생] 테이블을 이용해 이름이 ‘이’로 시작하는 학생들을 이름 기준 내림차순으로 정렬하려고 한다. [쿼리]의 빈칸 ①과 ②에 들어갈 알맞은 내용을 쓰시오.
[학생]
| 학번 | 이름 | 학년 | 학과 |
|---|---|---|---|
| 202101 | 이순신 | 3 | 컴퓨터공학 |
| 202102 | 김영희 | 1 | 전기공학 |
| 202103 | 이상 | 4 | 건축공학 |
| 202104 | 임꺽정 | 2 | 전자공학 |
| 202105 | 이정재 | 3 | 토목공학 |
[쿼리]
답 — ① '이%' ② DESC. "이로 시작"은 앞이 고정되고 뒤가 자유인 무늬, 내림차순은 DESC 다.
원리 — LIKE 의 % 는 "아무 글자 0개 이상"이므로 '이%' 는 첫 글자가 이인 모든 이름이다(이순신 · 이상 · 이정재). 문자열이라 작은따옴표로 감싼다. ORDER BY 이름 은 기본이 오름차순(ASC)이고 내림차순은 뒤에 DESC 를 붙인다. 이름이 한글이면 가나다 역순이다.
따라가기
헷갈리는 자리 — '%이%' 로 적는 것(어디든 이가 들어가면 임꺽정 · 김영희는 안 걸리지만 뜻이 다르다). 따옴표를 빼는 것. ② 에 DESCENDING 이나 "내림차순"을 적는 것 — 예약어는 DESC 다. 한 글자 자리에는 _ 를 쓴다는 것도 같은 묶음이다. 예를 들어 '이_' 는 두 글자 이름 이상만 걸리고, '%신' 은 신으로 끝나는 이순신만 걸린다 — %와 _ 의 자리로 무늬를 읽는다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 테이블과 SQL문을 보고 실행 결과를 쓰시오.
[A]
| id | x |
|---|---|
| 1 | 10 |
| 2 | 20 |
| 3 | 30 |
| 4 | 40 |
[B]
| id | y |
|---|---|
| 1 | 5 |
| 1 | 15 |
| 2 | 20 |
| 3 | 35 |
| 5 | 50 |
답 — 3. A 의 행마다 "자기보다 x 가 작은 행들의 id 에 해당하는 B.y 평균"을 구해 견주면, x 가 20 · 30 · 40 인 세 행이 남는다.
원리 — 안쪽 SELECT 가 바깥 행의 값(A.x)을 쓰므로 바깥 행마다 서브쿼리를 다시 계산해야 한다(상관 서브쿼리). 절차는 셋이다: 가장 안쪽에서 "지금 행보다 x 가 작은 A 행들의 id" 목록을 만들고, 가운데에서 B 중 그 id 인 행들의 y 평균을 내고, 바깥에서 지금 행의 x 가 그 평균보다 큰지 본다. 목록이 비면 평균은 NULL 이고 NULL 과의 비교는 참이 아니라 그 행은 빠진다.
따라가기
헷갈리는 자리 — 첫 행을 "비교할 것이 없으니 참"으로 보고 4 를 적는 것. NULL 비교는 거짓 취급이다. B 의 id 5 행(50)은 A 에 없는 id 라 한 번도 안 쓰인다는 것과, 상관 서브쿼리는 행마다 다시 센다는 것이 이 문제의 두 축이다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
다음 테이블과 SQL문의 실행 결과를 쓰시오.
[A]
| id | v |
|---|---|
| 1 | 10 |
| 1 | 20 |
| 2 | 30 |
[B]
| id | w |
|---|---|
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
| 4 | 400 |
답 — 2. RIGHT OUTER JOIN 은 B 의 행을 전부 남기므로 짝이 없는 B 의 id 3 · 4 가 A 쪽 NULL 로 나오고, 그 두 행만 A.id IS NULL 에 걸린다.
원리 — RIGHT OUTER JOIN 은 오른쪽 테이블(B)의 모든 행을 결과에 남기고, 왼쪽(A)에 짝이 있으면 붙이고 없으면 A 쪽 열을 NULL 로 채운다. 그래서 "B 에는 있는데 A 에는 없는 것"을 찾으려면 WHERE A.id IS NULL 을 건다 — 외부 조인의 대표 용법이다. A 의 id 1 이 두 행이라 B 의 id 1 은 두 번 나오지만 A.id 가 있으니 세지 않는다. NULL 은 = NULL 로 못 찾고 IS NULL 로만 찾는다.
따라가기
헷갈리는 자리 — 조인 결과 전체 5 행을 적는 것. INNER JOIN 으로 읽어 0 을 적는 것 — 내부 조인은 짝 없는 행이 아예 없어 IS NULL 이 하나도 안 걸린다. LEFT 였다면 A 쪽 전부가 남아 답이 0 이다 — 방향이 곧 답이다.
표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.
성적 테이블이 다음과 같을 때 쿼리 결과를 쓰시오.
답 — 열 이름 · 평균, 한 행 박C · 95. 이름별 평균이 85 를 넘는 사람은 박C 뿐이다.
원리 — GROUP BY 이름 은 같은 이름의 행을 묶고, AVG(점수) 는 묶음마다 평균을 낸다. HAVING AVG(점수) > 85 는 묶은 뒤 평균에 거는 조건이라 85 는 안 들어간다(초과). 김A 는 (90 + 80) / 2 = 85, 이B 는 (70 + 100) / 2 = 85 라 둘 다 탈락이고 박C 는 95 하나뿐이라 평균 95 다.
따라가기
헷갈리는 자리 — > 를 >= 로 읽어 세 사람을 다 남기는 것. "초과"와 "이상"은 다르다. 평균 열 이름을 AVG(점수)로 적는 것 — AS 평균 이 있으니 평균이다.
이전 자료의 예상 문제다. sqlite 3.49 에서 실행해 정답을 확인했다.
사원(사번, 이름, 부서코드): (1,김,A) (2,이,B) (3,박,NULL) / 부서(부서코드, 부서명): (A,영업) (B,개발)
① SELECT COUNT(*) FROM 사원 s INNER JOIN 부서 d ON s.부서코드 = d.부서코드;
② SELECT COUNT(*) FROM 사원 s LEFT JOIN 부서 d ON s.부서코드 = d.부서코드; 각각의 결과는?
답 — ① 2 ② 3. INNER JOIN 은 짝이 있는 행만, LEFT JOIN 은 왼쪽(사원) 행 전부를 남긴다.
원리 — 조인은 두 테이블의 행을 조건(부서코드가 같음)으로 짝짓는다. INNER JOIN 은 짝이 맞는 행만 결과에 넣는다 — 박의 부서코드가 NULL 이라 어느 부서와도 못 맞아 빠진다. LEFT JOIN 은 왼쪽 테이블의 모든 행을 남기고 짝이 없으면 오른쪽 열을 NULL 로 채운다 — 박도 남는다. NULL 은 어떤 값과도 같지 않으므로 NULL 끼리도 짝이 안 된다.
따라가기
헷갈리는 자리 — 부서 테이블에 없는 코드가 아니라 NULL 이라도 결과는 같다 — 짝이 없으면 INNER 에서 빠진다. RIGHT JOIN 이었다면 부서 쪽 전부가 남아 2 다.
이전 자료의 예상 문제다. sqlite 3.49 에서 실행해 정답을 확인했다.
왜 이걸 하나 — 아는 문제에서 깎이는 자리는 정해져 있다. 채점자의 눈으로 답안을 다시 본다.
| 자리 | 새는 꼴 | 적는 법 | 기출 |
|---|---|---|---|
| 결과의 꼴 | 행 수를 묻는데 값을 적는다, 값을 묻는데 행 수를 적는다 | 문제의 마지막 문장을 다시 읽는다 — 「행의 수」「반환되는 값」「결과를 쓰시오」「테이블 형태로」 | 2025-3회 11번 · 2025-1회 7번 |
| 열 이름 | 값만 적고 열 이름을 뺀다 | 「결과를 쓰시오」면 열 이름 줄부터 적는다. 별칭이 있으면 별칭이 열 이름이다 | 2024-1회 13번 · 2023-3회 8번 |
| 행 순서 | ORDER BY 를 무시하고 표 순서대로 적는다 | DESC 면 큰 것부터. ORDER BY 가 없으면 표 순서대로 적되, 순서가 채점 대상이 아닐 수 있다 | 2023-3회 8번 |
| NULL | NULL 을 0 으로 세거나, NULL 비교를 통과시킨다 | COUNT(열) · AVG 는 NULL 을 뺀다. 빈 집합 AVG 는 NULL 이고 비교는 통과 못 한다 | 2025-3회 12번 · 2026-2회 11번 |
| 중복 | UNION 에서 중복을 남기거나, RIGHT JOIN 에서 같은 키의 곱을 놓친다 | UNION 은 중복 제거, UNION ALL 은 유지. 한쪽에 같은 키가 n 개면 n 행이 된다 | 2023-3회 8번 · 2026-2회 18번 |
| AND · OR | 왼쪽부터 계산한다 | AND 를 먼저 괄호로 묶고 본다 | 2024-1회 18번 |
| 따옴표 · 대소문자 | LIKE 패턴에 따옴표를 빼거나, 키워드를 소문자로 적는다 | 문자열은 '이%' 처럼 작은따옴표까지, 키워드는 대문자 | 2026-2회 6번 |
| 빈칸 여러 개 | 첫 칸만 맞고 둘째 칸은 비워 둔다 | 소문항은 부분 점수가 있다. 모르는 칸도 가장 그럴듯한 것을 적는다 | 2026-2회 6번 · 2026-1회 9번 |
「결과를 쓰시오」 유형에서 열 이름을 빼는 것이 가장 흔한 감점이다. 채점자는 정답표의 표 꼴과 대조하므로, 열 이름 한 줄 + 값 줄을 기본 꼴로 삼고 행 수만 묻는 문제에서만 숫자 하나를 적는다.
왜 이걸 하나 — 기출과 같은 표에서 조건 하나만 바꾼 문제다. 답이 바뀌는 이유를 말할 수 있으면 원리가 손에 붙은 것이다.
5문항은 전부 04절의 표를 다시 쓴다. 괄호 위치, COUNT 의 괄호 안, JOIN 의 방향, LIKE 의 % 자리, 빈 집합 — 바뀐 것은 하나씩이고 답은 전부 바뀐다.
다음 테이블과 SQL문의 실행 결과를 쓰시오.
RESULT
| ID | KIND | SCORE |
|---|---|---|
| 1 | A | 70 |
| 2 | B | 90 |
| 3 | A | 50 |
| 4 | C | 95 |
답 — 0. 괄호로 OR 를 먼저 묶었으므로 "A 이면서 (80 이상이거나 C)" 인데, A 인 행은 70 · 50 이라 둘 다 탈락이다.
원리 — 2024년 1회 18번과 같은 표에 괄호만 넣은 변형이다. 괄호가 있으면 안의 OR 가 먼저 계산되고, 그다음 바깥 AND 가 걸린다. KIND 가 A 인 행에서만 두 번째 조건을 보는 셈인데, A 의 점수는 80 미만이고 A 이면서 동시에 C 일 수는 없으니 남는 행이 없다.
따라가기
이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.
다음 테이블과 SQL문의 실행 결과를 쓰시오.
A
| COL1 | COL2 |
|---|---|
| 2 | NULL |
| 3 | 6 |
| 2 | 3 |
| NULL | 3 |
| 4 | 5 |
답 — 5, 4, 4.25. 행 수는 5, COL1 이 NULL 이 아닌 행은 4, COL2 평균은 NULL 을 뺀 네 값의 평균이다.
원리 — COUNT(*) 는 NULL 과 상관없이 행을 센다. COUNT(COL1) 은 COL1 이 NULL 인 행을 빼고 센다. AVG(COL2) 도 NULL 을 빼고 "값이 있는 것들의 합 ÷ 값이 있는 개수"다 — NULL 을 0 으로 치지 않는다. 집계 함수는 * 를 빼면 전부 NULL 을 무시한다는 한 줄 규칙이다.
따라가기
이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.
다음 두 테이블과 SQL문의 실행 결과 행 수를 쓰시오.
A
| id | v |
|---|---|
| 1 | 10 |
| 1 | 20 |
| 2 | 30 |
B
| id | w |
|---|---|
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
| 4 | 400 |
답 — 3. LEFT OUTER JOIN 은 왼쪽 A 의 행을 전부 남기므로 A 의 행 수 3 이 그대로다.
원리 — 2026년 2회 18번(RIGHT)을 LEFT 로 바꾼 변형이다. LEFT 는 왼쪽 테이블이 기준이라 A 의 세 행이 모두 결과에 남고, 각 행의 id 가 B 에 있으면 그 값을 붙인다. A 의 id 1 · 1 · 2 는 전부 B 에 있어 NULL 도 안 생기고, B 의 3 · 4 는 A 에 없어 아예 안 나온다. 왼쪽 행 하나가 오른쪽 여러 행과 맞으면 그만큼 늘지만 여기서는 B 의 id 가 하나씩이라 3 이다.
따라가기
이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.
다음 테이블과 SQL문의 실행 결과를 쓰시오.
학생
| 학번 | 이름 | 학년 | 학과 |
|---|---|---|---|
| 202101 | 이순신 | 3 | 컴퓨터공학 |
| 202102 | 김영희 | 1 | 전기공학 |
| 202103 | 이상 | 4 | 건축공학 |
| 202104 | 임꺽정 | 2 | 전자공학 |
| 202105 | 이정재 | 3 | 토목공학 |
답 — 학년 · 인원 두 열, (3, 2) 와 (4, 1) 두 행. 이름에 이가 든 사람만 남겨 학년별로 세고 학년순으로 늘어놓는다.
원리 — 절이 실행되는 순서는 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY 다. WHERE LIKE '%이%' 로 먼저 행을 거르고(이순신 · 이상 · 이정재), 학년별로 묶어 COUNT 하고, HAVING 은 1 이상이라 아무것도 안 거르며, 마지막에 학년 오름차순으로 정렬한다. 김영희의 희는 이가 아니다.
따라가기
이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.
다음 테이블과 SQL문의 실행 결과를 쓰시오.
A
| id | x |
|---|---|
| 1 | 10 |
| 2 | 20 |
| 3 | 30 |
| 4 | 40 |
답 — 0. 서브쿼리에 맞는 행이 없어 AVG 가 NULL 이 되고, NULL 과 비교한 조건은 참이 될 수 없다.
원리 — AVG 는 값이 하나도 없으면 0 이 아니라 NULL 을 돌려준다. x > NULL 은 참도 거짓도 아닌 "모름"이라 WHERE 는 그 행을 버린다. 2026년 2회 11번의 첫 행(비교 대상이 없어 빠짐)이 같은 규칙이다. COUNT 만 예외로 값이 없을 때 0 을 돌려준다.
따라가기
이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.
파이썬에 sqlite 가 들어 있어 설치 없이 쿼리를 확인할 수 있다. 파워셸에서 아래를 그대로 치면 2026년 2회 18번의 답 2 가 찍힌다. 표와 쿼리만 바꿔 가며 손으로 센 값과 기계가 센 값을 맞춰 보는 것이 이 장의 훈련이다.
위 명령은 이 노트를 만든 컴퓨터의 파이썬 3.12(sqlite 3.49.1)에서 실행해 2 가 나온 것이다. RIGHT JOIN 은 sqlite 3.39 부터 지원한다. 이 장의 기출 13개 · 예상 2개 · 훈련 5개의 정답도 같은 방법으로 전부 돌려 확인했다(쿼리 작성 문항은 그 문장을 표본 표에 실행해 문법을 확인).
왜 이걸 하나 — 아래를 안 보고 적을 수 있어야 시험장에서 표를 돌릴 수 있다.