SQL 조회 — SELECT · JOIN · 서브쿼리 · 집계 E08

쿼리 한 줄을 종이 위에서 실행해 결과 표를 그려 내는 장이다. 문법 암기가 아니라 실행 순서가 답을 만든다

왜 이 장을 하나
SQL 조회 문제는 주어진 표에 쿼리를 손으로 돌려 결과를 적는 것이다. 실행 순서 6단계, JOIN 의 행 보존 규칙, NULL 의 세 규칙, 서브쿼리를 값으로 바꾸는 순서 — 이 네 가지만 손에 붙으면 매 회 1~3문항이 확실한 점수가 된다.
다루는 것SELECT 실행 순서 · JOIN · NULL · 서브쿼리
문항기출 13 · 예상 2 · 훈련 5
끝나면쿼리를 보고 결과 표를 그린다

01왜 이 장인가

왜 이걸 하나 — 2회에서 가장 약했던 묶음이 SQL 이다. 이 장이 그 5~15점을 되찾는 첫 장이다.

11회분 220문항 중 SQL 조회는 13문항이다. 11회 중 8회에 나왔고, 2026년 2회에는 3문항이 나왔다. 정의·조작·제어(E09)와 DB 이론(E10)까지 합치면 SQL·DB 묶음이 39문항, 18%다.

몇 문항무엇을 적나보기
실행 결과 — 행 수 · 값 하나8COUNT 의 값 하나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번
문법 빈칸1LIKE 패턴 · DESC2026-2회 6번
쿼리 작성1SELECT 문 한 줄 전부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회 문항은 이전 자료의 복원을 옮긴 것이라 문항 번호가 복원본마다 다를 수 있다.

02한눈에

왜 이걸 하나 — 시험 전날 이 절만 다시 본다. 답안에 적는 표기까지 여기 있다.

실행 순서FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. 쓰는 순서와 다르다. WHERE 는 묶기 전, HAVING 은 묶은 뒤. 별칭(AS)은 SELECT 에서 붙으므로 WHERE 에서는 못 쓴다.
JOIN 4종INNER — 짝 있는 행만. LEFT — 왼쪽 전부 + 짝 없으면 오른쪽 NULL. RIGHT — 오른쪽 전부. FULL — 양쪽 전부. CROSS — 모든 짝(m × n 행). FROM 에 쉼표로 두 테이블 + WHERE 등호는 INNER 와 같다.
집계와 NULLCOUNT(*) 는 행 수, COUNT(열) 은 NULL 을 뺀 개수. SUM · AVG · MAX · MIN 은 NULL 을 빼고 계산한다. 맞는 행이 하나도 없으면 COUNT 는 0, 나머지는 NULL.
NULL 비교= · > · < 로 NULL 과 비교하면 참도 거짓도 아니라 WHERE 를 통과하지 못한다. NULL 을 찾을 때는 IS NULL, 아닌 것은 IS NOT NULL.
서브쿼리 3꼴값 하나(= · > 비교) · 집합(IN · NOT IN · EXISTS) · 상관(바깥 행의 값을 안에서 쓴다 — 바깥 행마다 다시 계산). 안에서 밖으로 값으로 바꾼다.
LIKE% 는 0글자 이상, _ 는 딱 1글자. '이%' 시작, '%이' 끝, '%이%' 포함. 작은따옴표 필수.
정렬 · 중복ORDER BY 열 ASC(기본) · DESC. DISTINCT 는 중복 행 제거. UNION 은 합치며 중복 제거, UNION ALL 은 그대로. GROUP BY 결과는 묶음당 한 행.
답안 표기「결과를 쓰시오」면 열 이름 + 값. 「행 수」면 숫자 하나. 「표 형태」면 열 이름 줄과 값 줄을 나눠 적고, 열 순서는 SELECT 순서. 문자열은 작은따옴표, 키워드는 대문자.

03밑바닥

왜 이걸 하나 — 답이 왜 그 값인지 밑에서부터 세워야, 표가 바뀌어도 같은 절차로 푼다.

3-1. 쿼리는 행 집합을 깎아 내려간다

SELECT 문은 위에서 아래로 읽히지 않는다. 데이터베이스는 먼저 FROM 으로 표를 집고, WHERE 로 행을 버리고, GROUP BY 로 묶고, HAVING 으로 묶음을 버리고, 그제야 SELECT 의 열을 고르고, 마지막에 ORDER BY 로 줄을 세운다. 그래서 손으로 풀 때도 이 순서로 표를 깎아야 중간 행 수가 맞는다.

2026년 2회 6번의 학생 표(5행)에 아래 쿼리를 돌리는 과정을 단계별로 세어 보자.

SELECT 학년, COUNT(*) AS 인원 FROM 학생 WHERE 이름 LIKE '이%' GROUP BY 학년 HAVING COUNT(*) >= 2 ORDER BY 학년 DESC;
단계무엇을 하나남는 것
FROM 학생표를 집는다5행
WHERE 이름 LIKE '이%'이 로 시작하는 이름만이순신(3) · 이상(4) · 이정재(3) → 3행
GROUP BY 학년학년별로 묶는다3학년 묶음(2행) · 4학년 묶음(1행) → 묶음 2개
HAVING COUNT(*) >= 22행 미만 묶음을 버린다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 로 적는 것이 안전하다.

3-2. JOIN 은 곱한 뒤 거른다

두 표를 잇는 원리는 하나다. 먼저 모든 행의 짝을 만들고(m × n), ON 조건이 참인 짝만 남긴다. 이것이 INNER JOIN 이고, FROM 에 두 표를 쉼표로 놓고 WHERE 에 등호를 쓴 것도 같은 결과다.

OUTER 는 거기에 한 가지를 더한다. 짝을 못 찾은 행을 버리지 않고 상대편 열을 NULL 로 채워 남긴다. LEFT 는 왼쪽 표의 행을, RIGHT 는 오른쪽 표의 행을, FULL 은 양쪽을 남긴다.

2026년 2회 18번의 A(3행) · B(4행)로 RIGHT OUTER JOIN 의 결과를 그리면 이렇다.

A.idA.vB.idB.w어떻게 생겼나
1101100B 의 1 에 A 의 첫 행이 짝
1201100B 의 1 에 A 의 둘째 행도 짝 — B 행이 두 번 나온다
2302200짝 하나
NULLNULL3300A 에 3 이 없어 A 쪽이 NULL
NULLNULL4400A 에 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 로 거르는 꼴이다.

3-3. NULL 은 값이 아니라 「모른다」다

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 로 틀린다.

3-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개다.

이 장을 통과했다고 말할 수 있는 기준은 넷이다. 첫째, 실행 순서 6단계를 안 보고 적는다. 둘째, 표 두 개를 주면 INNER · LEFT · RIGHT · CROSS 의 행 수를 손으로 센다. 셋째, NULL 세 규칙(비교는 통과 못 함 · COUNT(열)은 뺀다 · 빈 집합 AVG 는 NULL)을 말한다. 넷째, 서브쿼리를 안에서 밖으로 값으로 바꿔 적는다. 넷 중 하나라도 막히면 04절 기출을 풀기 전에 이 절로 돌아온다.
COUNT(열)도 행 수니까 COUNT(*)와 같다.COUNT(열) 은 그 열이 NULL 인 행을 세지 않는다. 2025년 3회 12번은 이 차이 하나로 4 와 5 가 갈렸다. 문제에 COUNT 가 보이면 괄호 안이 * 인지 열 이름인지부터 동그라미 친다.
AND 와 OR 는 왼쪽부터 차례로 계산된다.AND 가 먼저 묶인다. KIND = 'A' AND SCORE >= 80 OR KIND = 'C' 는 (A 이면서 80 이상) 또는 (C) 다. 2024년 1회 18번의 답 1 은 이 우선순위에서 나왔고, 괄호를 옮긴 훈련 1번은 0 이 된다.

04기출

왜 이걸 하나 — 03절의 절차를 기출 13문항과 예상 2문항에 그대로 대 본다. 회차순이라 꼴이 어떻게 반복되는지도 보인다.

푸는 법은 하나다. 표를 종이에 옮기고, 실행 순서대로 행을 지워 가며 남는 행 수를 적고, 마지막에 SELECT 열만 골라 답 칸에 옮긴다. 머릿속으로만 세지 않는다 — 2회에서 샌 점수는 거기서 나왔을 가능성이 가장 크다.

2023-1회 · 16번SQL

성적 테이블에서 과목이름별 최소점수·최대점수를 구하되, 평균 점수가 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 는 결과 열의 이름표(별칭)를 붙인다.

따라가기

  1. 무엇을 뽑나 → 과목이름, 최소, 최대 → SELECT 과목이름, MIN(점수) AS 최소점수, MAX(점수) AS 최대점수
  2. 어디서 → FROM 성적
  3. 무엇별로 → GROUP BY 과목이름
  4. 묶음 조건 → HAVING AVG(점수) >= 90

헷갈리는 자리 — HAVING 자리에 WHERE AVG(점수) >= 90 을 쓰는 것. WHERE 안에는 집계 함수를 못 쓴다(오류). 별칭 이름을 문제가 준 그대로(최소점수 · 최대점수) 적어야 하고, "이상"은 >= 다(> 는 초과). GROUP BY 에 없는 열을 SELECT 에 넣으면 안 된다는 규칙도 같이 기억한다.

답의 SQL 을 sqlite 3.49 에서 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 열 이름이 실제와 다를 수 있다.

2023-3회 · 8번SQL

다음 두 테이블과 SQL문의 실행 결과를 순서대로 쓰시오.

T1.A
1
2
3
3
T2.A
2
4
SELECT A FROM T1 UNION SELECT A FROM T2 ORDER BY A DESC;
정답4
3
2
1 (열 이름 A, 4행)

— 열 A 에 4 / 3 / 2 / 1 네 행. UNION 은 중복을 지우고, DESC 는 큰 값부터다.

원리UNION 은 두 SELECT 의 결과를 위아래로 이어 붙이되 같은 행은 하나만 남긴다(집합의 합집합). 중복까지 다 남기려면 UNION ALL 이다. T1 의 3 이 두 번 있어도 하나로 접히고, T2 의 2 는 T1 에도 있으니 하나만 남는다. ORDER BY A DESC 는 합친 결과 전체를 A 기준 내림차순으로 정렬한다 — 정렬은 마지막에 한 번만 걸린다.

따라가기

  1. T1: 1, 2, 3, 3 → T2: 2, 4
  2. UNION → {1, 2, 3, 4} (3 중복 · 2 중복 제거)
  3. DESC → 4, 3, 2, 1 (한 열 A, 네 행)

헷갈리는 자리 — 3 을 두 번, 2 를 두 번 남겨 여섯 행으로 적는 것(UNION ALL 의 결과). DESC 를 오름차순으로 읽어 1 2 3 4 로 적는 것. 답을 표 꼴로 요구하면 열 이름 A 까지 적는다. UNION 을 쓰려면 두 SELECT 의 열 수와 자료형이 같아야 한다는 것도 같은 묶음의 규칙이다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2024-1회 · 13번SQL

다음 SQL을 실행한 결과를 쓰시오.

SAMPLE

AB
1a
2b
3c
SELECT B FROM SAMPLE WHERE A IN ( SELECT A FROM SAMPLE WHERE A <= 2 );
정답열 이름 B, 값 a · b (2행)

— 열 B 에 a / b 두 행. 안쪽 SELECT 가 A ≤ 2 인 A 값 {1, 2} 를 만들고, 바깥은 A 가 그 안에 있는 행의 B 를 뽑는다.

원리 — 괄호 안 SELECT(서브쿼리)가 먼저 계산돼 값의 목록이 되고, A IN (목록) 은 "A 가 목록 중 하나와 같다"는 조건이다. 안쪽이 같은 테이블을 읽어도 상관없다 — 먼저 목록을 만들고 그다음 바깥이 그 목록으로 거른다고 두 단계로 읽으면 된다. 결과 열은 SELECT 뒤의 B 하나뿐이다.

따라가기

  1. 안쪽: A <= 2 인 행 → A = 1, 2 → 목록 (1, 2)
  2. 바깥: A IN (1, 2) 인 행 → 첫째 · 둘째 행
  3. 그 행들의 B → a, b

헷갈리는 자리 — 안쪽 SELECT 가 A 를 뽑는다고 답에 A 값 1, 2 를 적는 것. 결과 열은 바깥 SELECT 의 B 다. 조건이 < 2 였다면 a 한 행뿐이다 — 등호를 본다. IN 대신 = (SELECT …) 를 쓰면 서브쿼리가 두 행을 돌려줘 오류가 난다 — 여러 값에는 IN 이다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2024-1회 · 18번SQL

다음 SQL문의 실행 결과를 쓰시오.

RESULT

IDKINDSCORE
1A70
2B90
3A50
4C95
SELECT COUNT(*) FROM RESULT WHERE KIND = 'A' AND SCORE >= 80 OR KIND = 'C';
정답1

— 1. ANDOR 보다 먼저 묶이므로 "(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(*) 는 조건을 통과한 행 수다.

따라가기

  1. KIND = 'A' AND SCORE >= 80 → A 는 70 · 50 → 0 행
  2. OR KIND = 'C' → 4번 행 → 1 행
  3. COUNT(*) → 1

헷갈리는 자리 — 왼쪽부터 묶어 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) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2024-3회 · 3번SQL

다음 테이블과 SQL문을 보고 출력값을 쓰시오.

employee

employee_idnameproject_id
1Alice10
2Bob10
3Charlie20
4Dana30
5Ethan30

project

project_idname
10Alpha
20Beta
30Gamma
SELECT COUNT(*) FROM employee AS e JOIN project AS p ON e.project_id = p.project_id WHERE p.name IN ( SELECT name FROM project WHERE project_id IN ( SELECT project_id FROM employee GROUP BY project_id HAVING COUNT(*) < 2 ) );
정답1

— 1. 인원이 2 명 미만인 프로젝트는 20(Beta)뿐이고, 그 프로젝트에 속한 직원은 Charlie 한 명이다.

원리 — 서브쿼리가 세 겹이면 가장 안쪽부터 값을 만들어 바깥으로 올린다. 가장 안쪽은 employee 를 project_id 별로 묶어(GROUP BY) 인원이 2 명 미만인(HAVING COUNT(*) < 2) project_id 를 고른다. 가운데는 그 id 의 프로젝트 이름을 뽑는다. 바깥은 employee 와 project 를 project_id 로 이어 붙인(JOIN … ON) 뒤 이름이 그 목록에 있는 행만 센다. 각 층의 결과를 표로 적어 두면 헷갈리지 않는다.

따라가기

  1. 안쪽: 10 → 2명, 20 → 1명, 30 → 2명 → COUNT < 2 인 것은 20
  2. 가운데: project_id 20 의 name → Beta
  3. 바깥: JOIN 뒤 p.name = Beta 인 행 → Charlie 한 행 → 1

헷갈리는 자리 — HAVING COUNT(*) < 2 를 "2 명 이하"로 읽어 10 · 30 까지 넣는 것(<= 가 아니다). JOIN 결과가 다섯 행인 것을 답으로 적는 것 — WHERE 가 거른 뒤의 수다. 층마다 무엇을 돌려주는지(id → 이름 → 행 수)를 적는다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2025-1회 · 7번SQL

다음 두 테이블과 SQL문을 보고 조회 결과를 테이블 형태로 쓰시오.

직원

직원번호이름
101이순신
102강감찬
103김유신

보상

직원번호성과급
1011000
102300
103450
SELECT e.이름, b.성과급 FROM 직원 e, 보상 b WHERE e.직원번호 = b.직원번호 AND b.성과급 >= 500;
정답이름 | 성과급
이순신 | 1000

— 열 이름 · 성과급, 한 행 이순신 · 1000. 직원번호가 같은 행끼리 이어 붙인 뒤 성과급 500 이상만 남긴다.

원리FROM 직원 e, 보상 b 는 두 테이블을 전부 짝지은 뒤(3 × 3 = 9 행) WHERE e.직원번호 = b.직원번호 로 번호가 같은 짝만 남기는 옛 방식의 조인이다(JOIN … ON 과 같다). 남은 세 짝 중 b.성과급 >= 500 인 것은 101 번(1000)뿐이다. SELECT 에서 e.이름, b.성과급 두 열만 뽑으므로 결과 표는 열 둘, 행 하나다.

따라가기

  1. 번호 같은 짝: 101 이순신 1000 · 102 강감찬 300 · 103 김유신 450
  2. 성과급 >= 500 → 101 만
  3. 결과: 이름 | 성과급 / 이순신 | 1000

헷갈리는 자리 — 답에 직원번호 열을 넣는 것(SELECT 에 없다). 450 을 "500 가까우니" 넣는 것 — 이상은 500 부터다. 표 꼴로 쓰라고 했으니 열 이름 줄을 먼저 적는다. 열 이름은 별칭이 없으니 원래 이름(이름 · 성과급)이고, e · b 같은 테이블 별칭은 결과에 안 나온다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2025-3회 · 11번SQL

다음 A, B 테이블과 SQL문을 보고 출력되는 행의 수를 쓰시오.

A 테이블

NAME
Smith
Allen
Scott

B 테이블

RULE
S%
%T%
SELECT COUNT(*) AS CNT FROM A CROSS JOIN B WHERE A.NAME LIKE B.RULE;
정답4

— 4. 이름 셋 × 규칙 둘 = 여섯 짝 중 LIKE 가 맞는 짝은 Smith 둘, Scott 둘이다.

원리CROSS JOIN 은 두 테이블의 모든 짝을 만든다(3 × 2 = 6 행). LIKE 는 무늬 맞추기로, % 는 "아무 글자 0개 이상"이다. 'S%' 는 S 로 시작, '%T%' 는 어디든 T 가 들어 있음이다. 이 문제의 정답 4 는 대소문자를 구분하지 않는 채점(Smith 의 t · Scott 의 tt 가 T 에 맞음)을 전제로 한다 — MySQL · SQL Server 의 기본이 그렇다.

따라가기

  1. Smith: S% 맞음, %T% 맞음(t) → 2
  2. Allen: S% 아님, %T% 아님 → 0
  3. Scott: S% 맞음, %T% 맞음(tt) → 2
  4. 4

헷갈리는 자리 — 대소문자를 구분하면(PostgreSQL · sqlite 기본) %T% 가 하나도 안 맞아 2 가 된다. 시험은 보통 구분하지 않는 쪽으로 낸다는 것을 알고 4 로 적되, 문제에 "대소문자 구분" 문구가 있으면 그것을 따른다. CROSS JOIN 의 행 수 6 을 그대로 적는 것도 오답이다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2025-3회 · 12번SQL

다음 SQL문의 실행 결과를 쓰시오. 테이블 A의 데이터가 다음과 같을 때 조회 결과를 구하시오.

A

COL1COL2
2NULL
36
23
NULL3
45
SELECT COUNT(COL2) FROM A WHERE COL1 IN (2, 3) OR COL2 IN (3, 5);
정답4

— 4. WHERE 로 다섯 행이 다 걸리지만 COUNT(COL2) 는 NULL 을 세지 않아 4 다.

원리COUNT(*) 는 행 수를 세고, COUNT(열) 은 그 열이 NULL 이 아닌 행만 센다. NULL 은 "값 없음"이라 개수에 안 들어간다. WHERE 는 COL1 IN (2, 3) 또는 COL2 IN (3, 5) 인 행을 남기는데, NULL 은 어느 IN 에도 맞지 않지만 다른 쪽 조건으로 걸릴 수 있다. 행마다 두 조건을 따로 보고 하나라도 참이면 남긴다.

따라가기

  1. (2, NULL): COL1 2 → 남음. COL2 NULL → 안 셈
  2. (3, 6): COL1 3 → 남음, 셈
  3. (2, 3): 남음, 셈
  4. (NULL, 3): COL2 3 → 남음, 셈
  5. (4, 5): COL2 5 → 남음, 셈 → COUNT(COL2) = 4

헷갈리는 자리 — COUNT(*) 로 읽어 5 를 적는 것. 괄호 안이 열 이름이면 NULL 을 뺀다. (NULL, 3) 행을 "NULL 이 있으니 제외"로 보는 것 — COL2 = 3 으로 걸린다. NULL 규칙은 두 방향(조건에서는 거짓, 개수에서는 제외) 모두 본다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2026-1회 · 9번SQL

STUDENT 테이블(총 200명, 학과 3개)에서: ① SELECT COUNT(*) FROM STUDENT;SELECT COUNT(DISTINCT 학과) FROM STUDENT;SELECT COUNT(DISTINCT 학과) FROM STUDENT WHERE 학과='전산과'; 각각의 결과는?

정답① 200 ② 3 ③ 1

— ① 200 ② 3 ③ 1. COUNT(*) 는 전체 행, COUNT(DISTINCT 학과) 는 서로 다른 학과 수, WHERE 로 한 학과만 남기면 그 수는 1 이다.

원리COUNT(*) 는 조건을 통과한 행의 수다(200 명 → 200). DISTINCT 는 중복을 지운다 — 200 행의 학과 값 중 서로 다른 것만 세면 학과 수 3 이다. ③ 은 WHERE 로 전산과 행만 남긴 뒤 그 안의 서로 다른 학과 값을 세니, 전산과 하나뿐이라 1 이다. 행 수(몇 명)와 종류 수(몇 학과)를 나누는 것이 전부다.

따라가기

  1. ① 전체 행 수 → 200
  2. ② 학과의 서로 다른 값 → 3
  3. ③ 전산과 행만 → 학과 값은 전산과 하나 → 1

헷갈리는 자리 — ③ 을 "전산과 학생 수"로 읽고 인원을 적는 것. DISTINCT 학과이므로 종류 수 1 이다. ② 를 200 으로 적는 것 — DISTINCT 가 있다. 세 문항이 COUNT 의 세 가지 쓰임을 정확히 나눠 묻는다. 학과 값에 NULL 인 학생이 있었다면 COUNT(DISTINCT 학과)는 NULL 을 안 세므로 ② 가 달라질 수 있다 — 문제는 그런 행이 없다고 본다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2026-1회 · 13번SQL

다음 두 테이블과 SQL문을 보고 반환되는 값을 쓰시오.

employee 테이블

dep_id
10
20
20

dept 테이블

dept_idbudget
10100
20300
30200
SELECT COUNT(*) FROM employee e JOIN dept d ON e.dep_id = d.dept_id WHERE d.budget > ( SELECT AVG(budget) FROM dept );
정답2

— 2. 예산 평균 200 보다 큰 부서는 20(300)뿐이고, 그 부서 직원이 두 명이다.

원리 — 서브쿼리 SELECT AVG(budget) FROM dept 는 하나의 값(200)을 돌려주고, 바깥 조건은 d.budget > 200 이 된다. JOIN … ON e.dep_id = d.dept_id 는 직원 행마다 부서 정보를 붙인다 — 직원이 세 행이니 조인 결과도 세 행이고, 그중 부서 예산이 200 을 넘는 행만 센다. 부서 30 은 직원이 없어 조인에 아예 안 나온다.

따라가기

  1. AVG(budget) = (100 + 300 + 200) / 3 = 200
  2. 조인: 직원 10 → 예산 100, 직원 20 → 300, 직원 20 → 300
  3. budget > 200 → 두 행 → 2

헷갈리는 자리 — 부서 수로 세어 1(부서 20 하나)을 적는 것. COUNT(*) 는 조인된 직원 행을 센다. 200 을 "이상"으로 보고 부서 30 까지 넣는 것 — > 는 초과이고, 30 은 직원이 없어 어차피 0 행이다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2026-2회 · 6번SQL

다음 [학생] 테이블을 이용해 이름이 ‘이’로 시작하는 학생들을 이름 기준 내림차순으로 정렬하려고 한다. [쿼리]의 빈칸 ①과 ②에 들어갈 알맞은 내용을 쓰시오.

[학생]

학번이름학년학과
202101이순신3컴퓨터공학
202102김영희1전기공학
202103이상4건축공학
202104임꺽정2전자공학
202105이정재3토목공학

[쿼리]

SELECT * FROM 학생 WHERE 이름 LIKE ① ORDER BY 이름 ②;
정답① '이%' ② DESC

— ① '이%'DESC. "이로 시작"은 앞이 고정되고 뒤가 자유인 무늬, 내림차순은 DESC 다.

원리LIKE% 는 "아무 글자 0개 이상"이므로 '이%' 는 첫 글자가 이인 모든 이름이다(이순신 · 이상 · 이정재). 문자열이라 작은따옴표로 감싼다. ORDER BY 이름 은 기본이 오름차순(ASC)이고 내림차순은 뒤에 DESC 를 붙인다. 이름이 한글이면 가나다 역순이다.

따라가기

  1. LIKE '이%' → 이순신, 이상, 이정재
  2. ORDER BY 이름 DESC → 이정재, 이순신, 이상

헷갈리는 자리'%이%' 로 적는 것(어디든 이가 들어가면 임꺽정 · 김영희는 안 걸리지만 뜻이 다르다). 따옴표를 빼는 것. ② 에 DESCENDING 이나 "내림차순"을 적는 것 — 예약어는 DESC 다. 한 글자 자리에는 _ 를 쓴다는 것도 같은 묶음이다. 예를 들어 '이_' 는 두 글자 이름 이상만 걸리고, '%신' 은 신으로 끝나는 이순신만 걸린다 — %와 _ 의 자리로 무늬를 읽는다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2026-2회 · 11번SQL

다음 테이블과 SQL문을 보고 실행 결과를 쓰시오.

[A]

idx
110
220
330
440

[B]

idy
15
115
220
335
550
SELECT COUNT(*) FROM A WHERE A.x > ( SELECT AVG(B.y) FROM B WHERE B.id IN ( SELECT A2.id FROM A AS A2 WHERE A2.x < A.x ) );
정답3

— 3. A 의 행마다 "자기보다 x 가 작은 행들의 id 에 해당하는 B.y 평균"을 구해 견주면, x 가 20 · 30 · 40 인 세 행이 남는다.

원리 — 안쪽 SELECT 가 바깥 행의 값(A.x)을 쓰므로 바깥 행마다 서브쿼리를 다시 계산해야 한다(상관 서브쿼리). 절차는 셋이다: 가장 안쪽에서 "지금 행보다 x 가 작은 A 행들의 id" 목록을 만들고, 가운데에서 B 중 그 id 인 행들의 y 평균을 내고, 바깥에서 지금 행의 x 가 그 평균보다 큰지 본다. 목록이 비면 평균은 NULL 이고 NULL 과의 비교는 참이 아니라 그 행은 빠진다.

따라가기

  1. x = 10: 더 작은 x 없음 → 목록 비어 AVG NULL → 제외
  2. x = 20: 목록 {1} → B.y 5, 15 → 평균 10 → 20 > 10 ✓
  3. x = 30: 목록 {1, 2} → 5, 15, 20 → 평균 13.3 → ✓
  4. x = 40: 목록 {1, 2, 3} → 5, 15, 20, 35 → 평균 18.75 → ✓ → 3

헷갈리는 자리 — 첫 행을 "비교할 것이 없으니 참"으로 보고 4 를 적는 것. NULL 비교는 거짓 취급이다. B 의 id 5 행(50)은 A 에 없는 id 라 한 번도 안 쓰인다는 것과, 상관 서브쿼리는 행마다 다시 센다는 것이 이 문제의 두 축이다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

2026-2회 · 18번SQL

다음 테이블과 SQL문의 실행 결과를 쓰시오.

[A]

idv
110
120
230

[B]

idw
1100
2200
3300
4400
SELECT COUNT(*) FROM A RIGHT OUTER JOIN B ON A.id = B.id WHERE A.id IS NULL;
정답2

— 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 로만 찾는다.

따라가기

  1. B id 1 ↔ A 두 행 → 2행(A.id 1)
  2. B id 2 ↔ A 한 행 → 1행(A.id 2)
  3. B id 3 · 4 ↔ 짝 없음 → A.id NULL → 2행
  4. WHERE A.id IS NULL → 2

헷갈리는 자리 — 조인 결과 전체 5 행을 적는 것. INNER JOIN 으로 읽어 0 을 적는 것 — 내부 조인은 짝 없는 행이 아예 없어 IS NULL 이 하나도 안 걸린다. LEFT 였다면 A 쪽 전부가 남아 답이 0 이다 — 방향이 곧 답이다.

표를 sqlite 3.49 에 넣고 같은 SQL 을 실행해 결과를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 표 · 값이 실제와 다를 수 있다.

예상 1회 · 10번SQL빈출 원리: GROUP BY + HAVING

성적 테이블이 다음과 같을 때 쿼리 결과를 쓰시오.

이름 과목 점수 SELECT 이름, AVG(점수) AS 평균 김A 수학 90 FROM 성적 김A 영어 80 GROUP BY 이름 이B 수학 70 HAVING AVG(점수) > 85; 이B 영어 100 박C 수학 95
정답박C | 95

— 열 이름 · 평균, 한 행 박C · 95. 이름별 평균이 85 를 넘는 사람은 박C 뿐이다.

원리GROUP BY 이름 은 같은 이름의 행을 묶고, AVG(점수) 는 묶음마다 평균을 낸다. HAVING AVG(점수) > 85 는 묶은 뒤 평균에 거는 조건이라 85 는 안 들어간다(초과). 김A 는 (90 + 80) / 2 = 85, 이B 는 (70 + 100) / 2 = 85 라 둘 다 탈락이고 박C 는 95 하나뿐이라 평균 95 다.

따라가기

  1. 김A → 85(탈락), 이B → 85(탈락), 박C → 95
  2. HAVING > 85 → 박C 만
  3. 결과 박C | 95 (열 이름은 이름 · 평균)

헷갈리는 자리 — > 를 >= 로 읽어 세 사람을 다 남기는 것. "초과"와 "이상"은 다르다. 평균 열 이름을 AVG(점수)로 적는 것 — AS 평균 이 있으니 평균이다.

이전 자료의 예상 문제다. sqlite 3.49 에서 실행해 정답을 확인했다.

예상 2회 · 10번SQL빈출 원리: INNER vs LEFT JOIN

사원(사번, 이름, 부서코드): (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

— ① 2 ② 3. INNER JOIN 은 짝이 있는 행만, LEFT JOIN 은 왼쪽(사원) 행 전부를 남긴다.

원리 — 조인은 두 테이블의 행을 조건(부서코드가 같음)으로 짝짓는다. INNER JOIN 은 짝이 맞는 행만 결과에 넣는다 — 박의 부서코드가 NULL 이라 어느 부서와도 못 맞아 빠진다. LEFT JOIN 은 왼쪽 테이블의 모든 행을 남기고 짝이 없으면 오른쪽 열을 NULL 로 채운다 — 박도 남는다. NULL 은 어떤 값과도 같지 않으므로 NULL 끼리도 짝이 안 된다.

따라가기

  1. 김(A) ↔ 영업, 이(B) ↔ 개발, 박(NULL) ↔ 없음
  2. INNER → 김 · 이 → 2
  3. LEFT → 김 · 이 · 박(부서 NULL) → 3

헷갈리는 자리 — 부서 테이블에 없는 코드가 아니라 NULL 이라도 결과는 같다 — 짝이 없으면 INNER 에서 빠진다. RIGHT JOIN 이었다면 부서 쪽 전부가 남아 2 다.

이전 자료의 예상 문제다. sqlite 3.49 에서 실행해 정답을 확인했다.

05틀리는 자리

왜 이걸 하나 — 아는 문제에서 깎이는 자리는 정해져 있다. 채점자의 눈으로 답안을 다시 본다.

자리새는 꼴적는 법기출
결과의 꼴행 수를 묻는데 값을 적는다, 값을 묻는데 행 수를 적는다문제의 마지막 문장을 다시 읽는다 — 「행의 수」「반환되는 값」「결과를 쓰시오」「테이블 형태로」2025-3회 11번 · 2025-1회 7번
열 이름값만 적고 열 이름을 뺀다「결과를 쓰시오」면 열 이름 줄부터 적는다. 별칭이 있으면 별칭이 열 이름이다2024-1회 13번 · 2023-3회 8번
행 순서ORDER BY 를 무시하고 표 순서대로 적는다DESC 면 큰 것부터. ORDER BY 가 없으면 표 순서대로 적되, 순서가 채점 대상이 아닐 수 있다2023-3회 8번
NULLNULL 을 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번

「결과를 쓰시오」 유형에서 열 이름을 빼는 것이 가장 흔한 감점이다. 채점자는 정답표의 표 꼴과 대조하므로, 열 이름 한 줄 + 값 줄을 기본 꼴로 삼고 행 수만 묻는 문제에서만 숫자 하나를 적는다.

06훈련

왜 이걸 하나 — 기출과 같은 표에서 조건 하나만 바꾼 문제다. 답이 바뀌는 이유를 말할 수 있으면 원리가 손에 붙은 것이다.

5문항은 전부 04절의 표를 다시 쓴다. 괄호 위치, COUNT 의 괄호 안, JOIN 의 방향, LIKE 의 % 자리, 빈 집합 — 바뀐 것은 하나씩이고 답은 전부 바뀐다.

훈련 SQL · 1번SQL

다음 테이블과 SQL문의 실행 결과를 쓰시오.

RESULT

IDKINDSCORE
1A70
2B90
3A50
4C95
SELECT COUNT(*) FROM RESULT WHERE KIND = 'A' AND (SCORE >= 80 OR KIND = 'C');
정답0

— 0. 괄호로 OR 를 먼저 묶었으므로 "A 이면서 (80 이상이거나 C)" 인데, A 인 행은 70 · 50 이라 둘 다 탈락이다.

원리 — 2024년 1회 18번과 같은 표에 괄호만 넣은 변형이다. 괄호가 있으면 안의 OR 가 먼저 계산되고, 그다음 바깥 AND 가 걸린다. KIND 가 A 인 행에서만 두 번째 조건을 보는 셈인데, A 의 점수는 80 미만이고 A 이면서 동시에 C 일 수는 없으니 남는 행이 없다.

따라가기

  1. KIND = 'A' 인 행: 1번(70), 3번(50)
  2. 괄호 안 (SCORE >= 80 OR KIND = 'C') → 둘 다 거짓
  3. COUNT(*) → 0

이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.

훈련 SQL · 2번SQL

다음 테이블과 SQL문의 실행 결과를 쓰시오.

A

COL1COL2
2NULL
36
23
NULL3
45
SELECT COUNT(*), COUNT(COL1), AVG(COL2) FROM A;
정답5, 4, 4.25

— 5, 4, 4.25. 행 수는 5, COL1 이 NULL 이 아닌 행은 4, COL2 평균은 NULL 을 뺀 네 값의 평균이다.

원리COUNT(*) 는 NULL 과 상관없이 행을 센다. COUNT(COL1) 은 COL1 이 NULL 인 행을 빼고 센다. AVG(COL2) 도 NULL 을 빼고 "값이 있는 것들의 합 ÷ 값이 있는 개수"다 — NULL 을 0 으로 치지 않는다. 집계 함수는 * 를 빼면 전부 NULL 을 무시한다는 한 줄 규칙이다.

따라가기

  1. COUNT(*) = 5
  2. COL1: 2, 3, 2, NULL, 4 → NULL 제외 4
  3. COL2: NULL, 6, 3, 3, 5 → (6 + 3 + 3 + 5) / 4 = 17 / 4 = 4.25

이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.

훈련 SQL · 3번SQL

다음 두 테이블과 SQL문의 실행 결과 행 수를 쓰시오.

A

idv
110
120
230

B

idw
1100
2200
3300
4400
SELECT COUNT(*) FROM A LEFT OUTER JOIN B ON A.id = B.id;
정답3

— 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 이다.

따라가기

  1. A(1, 10) ↔ B(1, 100), A(1, 20) ↔ B(1, 100), A(2, 30) ↔ B(2, 200)
  2. 결과 3 행 → 3

이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.

훈련 SQL · 4번SQL

다음 테이블과 SQL문의 실행 결과를 쓰시오.

학생

학번이름학년학과
202101이순신3컴퓨터공학
202102김영희1전기공학
202103이상4건축공학
202104임꺽정2전자공학
202105이정재3토목공학
SELECT 학년, COUNT(*) AS 인원 FROM 학생 WHERE 이름 LIKE '%이%' GROUP BY 학년 HAVING COUNT(*) >= 1 ORDER BY 학년;
정답학년 | 인원
3 | 2
4 | 1

— 학년 · 인원 두 열, (3, 2) 와 (4, 1) 두 행. 이름에 이가 든 사람만 남겨 학년별로 세고 학년순으로 늘어놓는다.

원리 — 절이 실행되는 순서는 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY 다. WHERE LIKE '%이%' 로 먼저 행을 거르고(이순신 · 이상 · 이정재), 학년별로 묶어 COUNT 하고, HAVING 은 1 이상이라 아무것도 안 거르며, 마지막에 학년 오름차순으로 정렬한다. 김영희의 희는 이가 아니다.

따라가기

  1. WHERE → 이순신(3), 이상(4), 이정재(3)
  2. GROUP BY 학년 → 3학년 2명, 4학년 1명
  3. ORDER BY 학년 → 3 | 2, 4 | 1

이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.

훈련 SQL · 5번SQL

다음 테이블과 SQL문의 실행 결과를 쓰시오.

A

idx
110
220
330
440
SELECT COUNT(*) FROM A WHERE x > (SELECT AVG(x) FROM A WHERE x > 100);
정답0

— 0. 서브쿼리에 맞는 행이 없어 AVG 가 NULL 이 되고, NULL 과 비교한 조건은 참이 될 수 없다.

원리AVG 는 값이 하나도 없으면 0 이 아니라 NULL 을 돌려준다. x > NULL 은 참도 거짓도 아닌 "모름"이라 WHERE 는 그 행을 버린다. 2026년 2회 11번의 첫 행(비교 대상이 없어 빠짐)이 같은 규칙이다. COUNT 만 예외로 값이 없을 때 0 을 돌려준다.

따라가기

  1. x > 100 인 행 없음 → AVG(x) = NULL
  2. 모든 행에서 x > NULL → 모름 → 버림
  3. COUNT(*) → 0

이 노트에서 만든 변형 문제다. 표를 sqlite 3.49 에 넣고 실행해 정답을 확인했다.

직접 돌려 보는 법

파이썬에 sqlite 가 들어 있어 설치 없이 쿼리를 확인할 수 있다. 파워셸에서 아래를 그대로 치면 2026년 2회 18번의 답 2 가 찍힌다. 표와 쿼리만 바꿔 가며 손으로 센 값과 기계가 센 값을 맞춰 보는 것이 이 장의 훈련이다.

python -c "import sqlite3; c=sqlite3.connect(':memory:'); c.executescript('create table A(id,v); insert into A values(1,10),(1,20),(2,30); create table B(id,w); insert into B values(1,100),(2,200),(3,300),(4,400);'); print(c.execute('SELECT COUNT(*) FROM A RIGHT OUTER JOIN B ON A.id=B.id WHERE A.id IS NULL').fetchone()[0])"

위 명령은 이 노트를 만든 컴퓨터의 파이썬 3.12(sqlite 3.49.1)에서 실행해 2 가 나온 것이다. RIGHT JOIN 은 sqlite 3.39 부터 지원한다. 이 장의 기출 13개 · 예상 2개 · 훈련 5개의 정답도 같은 방법으로 전부 돌려 확인했다(쿼리 작성 문항은 그 문장을 표본 표에 실행해 문법을 확인).

07백지 재현

왜 이걸 하나 — 아래를 안 보고 적을 수 있어야 시험장에서 표를 돌릴 수 있다.