SQLD 4회 기출복원 풀이
목차 52
1과목 데이터 모델링의 이해 (문 1~10)
문 1. 데이터 독립성 보장을 위한 3단계 스키마 구조에 속하지 않는 것은?
- ① 개념 스키마(Conceptual Schema)
- ② 논리 스키마(Logical Schema)
- ③ 내부 스키마(Internal Schema)
- ④ 외부 스키마(External Schema)
정답 및 해설 보기
정답 ②
ANSI-SPARC 3단계 스키마는 외부 스키마(External, 사용자/응용 관점) · 개념 스키마(Conceptual, 조직 전체 통합 관점) · 내부 스키마(Internal, 물리적 저장 관점)로 구성된다. ①③④는 모두 3단계 정식 명칭이다. ② '논리 스키마'는 실무에서 개념 스키마를 가리켜 부르기도 하지만, 3단계 구조의 정식 구성 요소 명칭이 아니므로 속하지 않는다.
🔑 암기 3단계 스키마 — 외부 · 개념 · 내부. '논리'는 정식 명칭이 아니다.
문 2. 품질 좋은 데이터 모델의 특징에 대한 설명으로 가장 적절하지 않은 것은?
- ① 업무 수행에 필요한 모든 데이터가 데이터 모델에 정의되어 있어야 한다.
- ② 하나의 데이터베이스 내에서 동일한 데이터는 한 번만 기록되어야 한다.
- ③ 가장 이상적인 데이터 구조는 동일한 데이터를 조직 전반에서 한 번만 정의한 뒤, 이를 다른 영역에서 공통으로 활용하는 형태이다.
- ④ 데이터 모델은 업무규칙을 최소화하여 데이터 구조를 단순하게 설계해야 한다.
정답 및 해설 보기
정답 ④
①은 완전성(Completeness), ②는 중복 배제(Non-redundancy), ③은 데이터의 공유·재사용으로 모두 좋은 모델의 특징이다. ④는 함정이다. 데이터 모델은 현실의 업무규칙(Business Rule)을 정확하고 빠짐없이 반영해야 한다. 업무규칙을 '최소화'하는 것이 목표가 아니라, 규칙을 정확히 담아내는 것이 좋은 모델이다.
🔑 암기 좋은 모델 = 완전성 · 중복 배제 · 업무규칙의 정확한 반영. '규칙 최소화'는 함정.
문 3. 발생 시점에 따라 구분할 수 있는 엔터티의 유형으로 적절하지 않은 것은?
- ① 기본 엔터티
- ② 중심 엔터티
- ③ 유형 엔터티
- ④ 행위 엔터티
정답 및 해설 보기
정답 ③
발생 시점에 따른 엔터티 분류는 기본 엔터티(다른 엔터티의 도움 없이 독립적으로 생성) · 중심 엔터티(기본 엔터티에서 파생되어 업무의 중심이 됨) · 행위 엔터티(두 엔터티 이상의 행위에 의해 생성)의 세 가지다. ①②④가 여기에 해당한다. ③ '유형 엔터티'는 유형·개념·사건처럼 엔터티의 '형태/성격'에 따른 분류이지 '발생 시점' 분류가 아니므로 적절하지 않다.
🔑 암기 발생 시점 분류 = 기본 · 중심 · 행위.
문 4. 아래 데이터 모델에 대한 설명으로 가장 적절하지 않은 것은?

- ① 강좌 엔터티는 각 인스턴스를 유일하게 구분하기 위한 고유 식별자를 별도로 생성한다.
- ② 한 학생의 수강신청 내역에는 동일한 강좌가 여러 번 나타날 수 있다.
- ③ 학생 엔터티는 다른 엔터티로부터 식별자를 상속받지 않고 자체 식별자를 가지므로 기본 엔터티에 해당한다.
- ④ 수강신청 엔터티는 수강신청이라는 학생의 행동 발생 시 생성되므로 사건 엔터티로도 볼 수 있다.
정답 및 해설 보기
정답 ②
수강신청 엔터티의 주식별자는 학생번호와 강좌번호를 묶은 복합 식별자다. 복합 주식별자는 그 조합 전체가 유일해야 하므로, 한 학생(학생번호)은 동일한 강좌(강좌번호)를 단 한 번만 신청할 수 있다. 따라서 '동일한 강좌가 여러 번 나타날 수 있다'는 ②는 모델과 어긋난다. 재수강처럼 여러 번 신청을 허용하려면 수강년도·신청일련번호 같은 속성이 주식별자에 추가되어야 한다. ①③④는 모두 모델과 일치하는 설명이다.
🔑 암기 복합 주식별자 = 조합 전체가 유일. {학생번호, 강좌번호}면 한 학생-한 강좌 1건뿐.
문 5. 업무규칙을 명확히 하기 위해 기존 데이터에 없는 속성을 새롭게 정의하거나 변형한 속성은?
- ① 기본 속성(Basic Attribute)
- ② 설계 속성(Designed Attribute)
- ③ 파생 속성(Derived Attribute)
- ④ PK 속성(Primary Key Attribute)
정답 및 해설 보기
정답 ②
설계 속성(Designed Attribute)은 원래 업무 데이터에는 없었지만, 업무규칙을 명확히 하거나 데이터 관리를 쉽게 하기 위해 인위적으로 새롭게 정의·변형한 속성이다(상품코드·일련번호 등). ① 기본 속성은 업무에 원래 존재하는 속성이라 '기존에 없는'과 맞지 않는다. ③ 파생 속성은 다른 속성값에서 '계산'되어 나오는 값이라 '계산 결과'라는 특징이 강하다. ④ PK 속성은 생성 방식이 아닌 역할에 따른 분류다.
🔑 암기 설계 속성 = 업무규칙 명확화를 위해 인위적으로 새로 정의(코드·일련번호). 파생 속성 = 계산으로 도출.
문 6. 아래 ERD의 속성에 대한 설명으로 가장 적절한 것은?

- ① 시, 군·구, 동은 의미를 더 이상 세분화할 수 없으므로 단순 속성에 해당된다.
- ② 주소는 시, 군·구, 동으로 구성되어 있으므로 다중값 속성에 해당된다.
- ③ 연락처는 한 회원이 여러 개를 가질 수 있으므로 복합 속성으로 표현된다.
- ④ 회원명은 다른 속성을 사용하여 만들 수 있는 파생 속성에 해당된다.
정답 및 해설 보기
정답 ①
주소는 시·군구·동이라는 하위 속성으로 나뉘므로 복합 속성(Composite Attribute)이고, 그 하위인 시·군구·동 각각은 더 쪼갤 수 없는 단순 속성(Simple Attribute)이다. 그래서 ①이 옳다. ② 주소는 다중값이 아니라 복합 속성이다. ③ 연락처는 이중원(다중값 표기)이므로 복합 속성이 아니라 다중값 속성(Multi-valued Attribute)이다. ④ 회원명은 다른 속성에서 계산되지 않는 단순 속성이지 파생 속성이 아니다.
🔑 암기 복합 속성(하위로 분해 O) · 다중값 속성(이중원, 여러 값) · 단순 속성(분해 X) · 파생 속성(계산으로 도출).
문 7. 아래 ERD에 대한 설명으로 가장 적절하지 않은 것은?

- ① ㉡은 교수가 없는 학교가 존재할 수 있다는 것을 의미한다.
- ② ㉢은 하나의 강의에 여러 명의 담당 교수가 존재할 수 있다는 것을 의미한다.
- ③ ㉢은 강의를 하지 않는 교수가 존재할 수 있다는 것을 의미한다.
- ④ ㉠은 ㉡과 ㉢을 합친 것과 동일한 의미는 아니다.
정답 및 해설 보기
정답 ②
㉢ 관계(교수–강의)는 한 명의 교수가 0개 이상의 강의를 담당하고, 하나의 강의는 반드시 한 명의 교수가 담당하는 구조다. 즉 강의 입장에서는 담당 교수가 정확히 1명이므로 '하나의 강의에 여러 명의 담당 교수가 존재할 수 있다'는 ②는 표기와 정반대다. ① ㉡(학교–교수)에서 교수 쪽이 선택적이면 교수가 없는 학교도 가능하다. ③ ㉢에서 교수 쪽이 선택적 참여이면 강의를 하지 않는 교수도 가능하다. ④ 두 직접 관계의 단순 결합이 별도 관계와 같다고 단정할 수 없다.
🔑 암기 까마귀발 — |(필수 1) · O(선택 0) · <(다수). 강의 쪽이 1이면 강의당 교수는 1명뿐.
문 8. 식별자에 대한 설명으로 가장 적절하지 않은 것은?
- ① 주식별자는 유일성, 최소성, 불변성, 존재성을 만족하는 대표 식별자이다.
- ② 보조식별자는 다른 엔터티와 참조 관계로 사용될 수 있으나, 인스턴스를 식별할 수는 없다.
- ③ 업무 프로세스에 존재하며 가공되지 않은 식별자를 본질식별자라고 한다.
- ④ 업무 프로세스에 존재하지는 않지만, 본질식별자의 구성이 복잡하여 인위적으로 만든 식별자를 인조식별자라고 한다.
정답 및 해설 보기
정답 ②
보조식별자(Alternate Key)는 주식별자는 아니지만 그 엔터티에서 유일성이 보장되는 식별자다(예: 이메일·전화번호). 유일성을 가지므로 인스턴스를 식별할 수 있다. 따라서 '인스턴스를 식별할 수는 없다'는 ②가 틀렸다. ① 주식별자 4대 특징(유일성·최소성·불변성·존재성)은 정확하다. ③ 본질식별자(가공되지 않은 업무상 키), ④ 인조식별자(인위적으로 만든 키)의 설명도 옳다.
🔑 암기 주식별자 4특징 = 유일성 · 최소성 · 불변성 · 존재성. 보조식별자도 유일성을 가져 식별 가능.
문 9. 주어진 거래 엔터티에 대해 3차 정규화까지 수행한 스키마 구조로 가장 적절한 것은? 🎯 고난도
[거래 엔터티] 거래번호 · 거래일자 · 고객명 · 고객등급 · 할인율 · 상품코드 · 상품명 · 단가 · 수량
[함수 종속성(FD)]
-
{거래번호, 상품코드} → {수량}
-
{거래번호} → {거래일자, 고객명, 고객등급}
-
{상품코드} → {상품명, 단가}
-
{고객등급} → {할인율}
-
① {거래번호, 거래일자, 고객명, 고객등급, 할인율}, {상품코드, 상품명, 단가}, {거래번호, 상품코드, 수량}
-
② {거래번호, 거래일자, 고객명, 고객등급}, {상품코드, 상품명, 단가, 수량, 할인율}
-
③ {거래번호, 거래일자, 고객명, 고객등급}, {고객등급, 할인율}, {상품코드, 상품명, 단가}, {거래번호, 상품코드, 수량}
-
④ {거래번호, 거래일자, 고객명, 고객등급, 할인율}, {상품코드, 상품명, 단가, 수량}
정답 및 해설 보기
정답 ③
함수 종속성을 따라 분해한다. {거래번호}→{거래일자,고객명,고객등급}·{상품코드}→{상품명,단가}는 복합키의 일부에만 종속되는 부분 종속이므로 별도 테이블로 분리한다(2NF). {고객등급}→{할인율}은 거래번호→고객등급→할인율로 이어지는 이행 종속이므로 다시 분리한다(3NF). 그 결과 {거래번호, 거래일자, 고객명, 고객등급} · {고객등급, 할인율} · {상품코드, 상품명, 단가} · {거래번호, 상품코드, 수량}의 네 릴레이션이 된다. ③이 정확히 이 구조다. ①④는 할인율이 거래 테이블에 남아 이행 종속이 그대로다. ②는 부분 종속(상품·수량)이 한 테이블에 뭉쳐 2NF조차 위반이다.
🔑 암기 부분 종속 제거 = 2NF, 이행 종속 제거 = 3NF. 고객등급→할인율을 떼어내야 3NF.
문 10. 데이터 모델링의 정규화에 대한 설명으로 가장 적절하지 않은 것은?
- ① 정규화는 중복 데이터를 제거하여 데이터베이스 크기를 줄이고 데이터 일관성을 유지한다.
- ② 정규화는 데이터를 조회할 때 조인으로 인한 성능 저하가 예상되는 경우 주로 적용된다.
- ③ 반정규화는 조회 속도를 향상시키지만, 데이터 모델의 유연성은 낮아지게 한다.
- ④ 과도한 반정규화 적용 시 데이터 무결성이 깨질 수 있다.
정답 및 해설 보기
정답 ②
조회 시 조인으로 인한 성능 저하가 예상될 때 적용하는 기법은 정규화가 아니라 반정규화(Denormalization)다. 정규화는 오히려 테이블을 분해하여 조인을 늘리는 방향이다. 따라서 목적과 수단을 반대로 설명한 ②가 틀렸다. ① 정규화의 목적(중복 제거·일관성), ③ 반정규화의 효과(조회 속도↑·유연성↓), ④ 과도한 반정규화의 위험(무결성 저하)은 모두 옳다.
⚠️ 함정 '성능 저하가 예상될 때 → 정규화'는 반대. 그 자리는 반정규화다.
2과목 SQL 기본 및 활용 (문 11~50)
문 11. 아래 SQL의 실행 결과를 순서대로 나열한 것은?
[TBL]
| COL1 | COL2 |
|---|---|
| A | 10 |
| B | 20 |
| A | 20 |
| B | 10 |
| C | 10 |
| A | 20 |
| A | 30 |
| A | 40 |
-- (가)
SELECT SUM(MAX(COL2))
FROM TBL
GROUP BY COL1
HAVING COUNT(*) >= 2;
-- (나)
SELECT SUM(MIN(COL2))
FROM TBL
GROUP BY COL1
HAVING COUNT(DISTINCT COL2) >= 2;
- ① 50, 20
- ② 40, 30
- ③ 60, 20
- ④ 60, 10
정답 및 해설 보기
정답 ③
그룹은 A(10·20·20·30·40), B(20·10), C(10)이다. (가) HAVING COUNT(*) >= 2는 행 수 2개 이상인 A·B만 통과(C는 1건 탈락)하고, 각 그룹의 MAX는 A=40·B=20이므로 그 합은 60이다. (나) HAVING COUNT(DISTINCT COL2) >= 2는 서로 다른 값이 2개 이상인 A(4개)·B(2개)가 통과하고, MIN은 A=10·B=10이므로 합은 20이다. 실행하면 60, 20을 반환한다.
⚠️ 함정 SUM(MAX(...))는 그룹별 MAX를 먼저 구한 뒤 그 값들을 다시 합산하는 중첩 집계다. COUNT(*)와 COUNT(DISTINCT)로 통과 그룹이 갈린다.
문 12. 아래 SQL의 실행 결과를 순서대로 나열한 것은?
SELECT FLOOR(4.0) FROM DUAL;
SELECT ROUND(3.9) FROM DUAL;
SELECT TRUNC(3.8) FROM DUAL;
- ① 3, 3, 3
- ② 3, 4, 4
- ③ 4, 4, 3
- ④ 4, 4, 4
정답 및 해설 보기
정답 ③
FLOOR(4.0)은 내림이라 4, ROUND(3.9)는 반올림이라 4, TRUNC(3.8)은 소수점 이하 절삭이라 3이다. 실행하면 4, 4, 3을 반환한다.
🔑 암기 FLOOR=내림(바닥) · ROUND=반올림 · TRUNC=절삭(버림). TRUNC와 FLOOR는 음수에서 갈린다(TRUNC(-3.8)=-3, FLOOR(-3.8)=-4).
문 13. 아래 SQL의 실행 결과는?
SELECT COALESCE(NULL, 'He', NULL, 'llo', 'World') AS R1
FROM DUAL;
- ① He
- ② Hello
- ③ HelloWorld
- ④ NULL
정답 및 해설 보기
정답 ①
COALESCE는 인자를 왼쪽부터 훑어 처음 만나는 NULL이 아닌 값을 반환한다. 첫 인자 NULL은 건너뛰고 두 번째 'He'에서 멈춘다. 뒤의 'llo'·'World'는 평가하지 않으므로 결과는 'He'다(문자열을 이어 붙이지 않는다).
🔑 암기 COALESCE = 왼쪽부터 첫 비(非)NULL 값. NVL(A, B)의 다중 인자 확장판.
문 14. 아래 SQL의 실행 결과는?
[월별매출]
| 지점 | 월 | 매출 |
|---|---|---|
| 서울 | 11 | 980 |
| 대전 | 11 | 700 |
| 부산 | 11 | 500 |
| 서울 | 12 | 1220 |
| 대전 | 12 | 800 |
| 부산 | 12 | NULL |
SELECT 지점, AVG(매출) AS 평균매출
FROM 월별매출
WHERE 매출 > (
SELECT MIN(매출)
FROM 월별매출
WHERE 월 = 11
)
GROUP BY 지점
HAVING COUNT(DISTINCT 월) = 2
ORDER BY 평균매출 DESC;
①
| 지점 | 평균매출 |
|---|---|
| 서울 | 980 |
| 대전 | 700 |
②
| 지점 | 평균매출 |
|---|---|
| 서울 | 1100 |
③
| 지점 | 평균매출 |
|---|---|
| 서울 | 1100 |
| 대전 | 750 |
| 부산 | 500 |
④
| 지점 | 평균매출 |
|---|---|
| 서울 | 1100 |
| 대전 | 750 |
정답 및 해설 보기
정답 ④
서브쿼리 MIN(매출) WHERE 월=11은 11월 매출(980·700·500)의 최솟값 500이다. WHERE 매출 > 500이면 서울(980·1220)·대전(700·800)만 남고, 부산은 500이 경계와 같아 제외되고 NULL 매출도 비교에서 빠진다. HAVING COUNT(DISTINCT 월) = 2는 11·12월 두 달치가 모두 남은 서울·대전이 통과한다. 평균은 서울 (980+1220)/2=1100, 대전 (700+800)/2=750이고, ORDER BY 평균매출 DESC로 서울·대전 순으로 정렬된다. 실행하면 이 두 행을 반환한다.
⚠️ 함정 >(초과)라 경계값 500인 부산이 빠지고, 집계 대상에서 NULL은 제외된다. HAVING으로 두 달치가 안 남은 부산이 한 번 더 걸러진다.
문 15. 아래 SQL의 실행 결과를 순서대로 나열한 것은? (단, DBMS는 오라클로 가정함)
[TAB]
| COL1 |
|---|
| 10 |
| 20 |
| 30 |
| 40 |
| 50 |
SELECT COL1 FROM TAB WHERE COL1 = 100;
SELECT MIN(COL1) FROM TAB WHERE COL1 = 100;
- ① 공집합, 공집합
- ② 공집합, NULL
- ③ NULL, 공집합
- ④ NULL, NULL
정답 및 해설 보기
정답 ②
첫 쿼리는 COL1 = 100을 만족하는 행이 없어 결과가 0건, 즉 공집합이다. 둘째 쿼리는 같은 공집합을 집계 함수 MIN에 넣은 것인데, COUNT를 제외한 집계 함수(MIN·MAX·SUM·AVG)는 공집합에 대해 NULL 1행을 반환한다. 따라서 공집합, NULL이다.
🔑 암기 WHERE만 통과 못하면 공집합(0건). 집계 함수 + 공집합 → NULL 1행. 단 COUNT(*)만 0을 반환.
문 16. 아래 테이블을 참고할 때 실행 결과가 다른 하나는?
[테이블 생성]
CREATE TABLE CUSTOMER (
C_ID NUMBER,
CNAME VARCHAR2(10)
);
[CUSTOMER]
| C_ID | CNAME |
|---|---|
| 100 | James |
| 101 | Frank |
| 102 | Matthew |
| 103 | Linda |
| 104 | Ariana |
①
SELECT *
FROM CUSTOMER
WHERE CNAME LIKE '%a%';
②
SELECT *
FROM CUSTOMER
WHERE CNAME LIKE '%_a%';
③
SELECT *
FROM CUSTOMER
WHERE CNAME LIKE '%a_%';
④
SELECT *
FROM CUSTOMER
WHERE CNAME LIKE '%_%';
정답 및 해설 보기
정답 ③
%는 0글자 이상, _는 정확히 한 글자다. ① %a%는 'a'가 어디든 포함되면 되므로 5명 모두, ② %_a%는 'a' 앞에 한 글자 이상이면 되므로 5명 모두, ④ %_%는 한 글자 이상이면 되므로 5명 모두 매치된다. ③ %a_%는 'a' 뒤에 한 글자 이상이 와야 하므로, 'a'로 끝나는 Linda가 탈락해 4건만 나온다. 실행하면 ③만 4건이고 나머지는 5건이다.
🔑 암기 %=0글자 이상 · _=정확히 한 글자. 'a로 끝남'은 %a_%(뒤에 글자 필요)에서 빠진다.
문 17. 아래 테이블을 참고할 때 실행 결과가 다른 하나는?
[EMP]
| EMP_ID | EMP_NAME | DEPTNO |
|---|---|---|
| 9970 | Alice | 10 |
| 9971 | Ben | 20 |
| 9972 | Chloe | 10 |
| 9973 | David | 20 |
| 9974 | Emma | NULL |
| 9975 | Frank | NULL |
-- (가)
SELECT EMP_ID, DEPTNO, CASE
WHEN DEPTNO = '10' THEN 'HR'
WHEN DEPTNO = '20' THEN 'Sales'
WHEN DEPTNO IS NULL THEN 'etc'
END AS R1 FROM EMP;
-- (나)
SELECT EMP_ID, DEPTNO, CASE DEPTNO
WHEN '10' THEN 'HR'
WHEN '20' THEN 'Sales'
ELSE 'etc'
END AS R2 FROM EMP;
-- (다)
SELECT EMP_ID, DEPTNO, CASE DEPTNO
WHEN '10' THEN 'HR'
WHEN '20' THEN 'Sales'
ELSE NULLIF(DEPTNO, 'etc')
END AS R3 FROM EMP;
-- (라)
SELECT EMP_ID, DEPTNO,
DECODE(DEPTNO, '10', 'HR', '20', 'Sales', 'etc') AS R4 FROM EMP;
- ① (가)
- ② (나)
- ③ (다)
- ④ (라)
정답 및 해설 보기
정답 ③
(가)는 WHEN DEPTNO IS NULL THEN 'etc'로 NULL을 명시 처리, (나)는 단순 CASE의 ELSE로, (라)는 DECODE의 마지막 기본값으로 NULL을 모두 'etc'로 만든다. 반면 (다)는 ELSE가 NULLIF(DEPTNO, 'etc')인데 DEPTNO가 NULL인 행은 NULLIF(NULL, 'etc') = NULL을 반환한다. 그래서 Emma·Frank가 'etc'가 아닌 NULL이 되어 (다)만 결과가 다르다.
⚠️ 함정 NULLIF(A, B)는 A=B면 NULL, 다르면 A. NULLIF(NULL, 'etc')는 첫 인자 NULL을 그대로 돌려준다.
문 18. 아래의 연산자를 동시에 적용할 때 우선순위가 가장 높은 연산자는? (단, DBMS는 오라클로 가정함)
BETWEEN, NOT, AND, OR
- ① BETWEEN
- ② NOT
- ③ AND
- ④ OR
정답 및 해설 보기
정답 ①
WHERE 절 연산자 우선순위는 비교·범위 연산자(BETWEEN·LIKE·IN·= 등) → NOT → AND → OR 순이다. 네 개 중에서는 BETWEEN(비교 연산자)이 가장 높다.
🔑 암기 우선순위 — 비교/범위(BETWEEN 등) > NOT > AND > OR.
문 19. 아래 테이블을 참고할 때 실행 결과가 다른 하나는?
[T1]
| ID | C1 | C2 | C3 |
|---|---|---|---|
| M-100 | A | 30 | 10 |
| M-101 | B | 30 | 10 |
| M-102 | A | 40 | 15 |
| M-103 | B | 40 | 15 |
| M-104 | AB | 50 | 20 |
| M-105 | AA | 50 | 20 |
①
SELECT ID
FROM T1
WHERE
C1 IN ('A', 'B')
AND (C2 >= 40 AND C3 <= 20);
②
SELECT ID
FROM T1
WHERE
C1 IN ('A', 'B')
AND NOT (C2 < 40 AND C3 > 20);
③
SELECT ID
FROM T1
WHERE
C1 LIKE '_'
AND (C2 >= 40 AND C3 <= 20);
④
SELECT ID
FROM T1
WHERE
(C1 = 'A' OR C1 = 'B')
AND C2 >= 40 AND C3 <= 20;
정답 및 해설 보기
정답 ②
①③④는 결국 'C1이 A 또는 B'이면서 C2 >= 40 AND C3 <= 20을 요구한다. 이 조건을 만족하는 행은 M-102·M-103 두 건이다(C1 LIKE '_'는 한 글자라 A·B만, 'AB'·'AA'는 제외). ②는 NOT (C2 < 40 AND C3 > 20)인데 이 테이블엔 C2<40이면서 C3>20인 행이 없어 괄호 안이 항상 거짓 → NOT으로 항상 참이 된다. 따라서 C1 IN ('A','B')인 4건(M-100~M-103) 전부가 나와 ②만 결과가 다르다. 실행하면 ①③④는 2건, ②는 4건이다.
⚠️ 함정 LIKE '_'는 정확히 한 글자(여기선 A·B). ②의 NOT(...)은 안쪽 조건이 항상 거짓이라 필터가 사실상 사라진다.
문 20. 주문 테이블의 '제품ID' 칼럼에는 제품ID가 'A001', 'A002', 'A003'인 행이 각각 10개, 15개, 20개가 존재한다. 이를 참고할 때 아래 SQL의 실행 결과는? (단, 주문 테이블의 총 행의 개수는 45개로 가정함)
SELECT COUNT(DISTINCT 제품ID)
FROM 주문;
SELECT COUNT(*)
FROM 주문
WHERE 제품ID IN (NULL, 'A001');
- ① 3, 0
- ② 3, 10
- ③ 3, NULL
- ④ 3, 오류 발생
정답 및 해설 보기
정답 ②
COUNT(DISTINCT 제품ID)는 서로 다른 값(A001·A002·A003) 3개를 센다. 둘째 쿼리의 IN (NULL, 'A001')은 제품ID = NULL OR 제품ID = 'A001'과 같은데, NULL과의 비교는 UNKNOWN이라 무시되고 = 'A001'만 작동한다. 따라서 A001인 10건을 세어 오류 없이 10을 반환한다. 결과는 3, 10이다.
⚠️ 함정 IN 리스트 안의 NULL은 오류가 아니라 무시될 뿐이다(나머지 값으로 매치). NOT IN과는 정반대로 동작한다.
문 21. 각 부서에 속한 모든 사원들의 급여가 회사 전체 평균 급여보다 높은 경우, 해당 부서의 부서명과 평균 급여를 구하는 SQL로 가장 적절한 것은? 🎯 고난도
①
SELECT D_NAME AS 부서명, AVG(SALARY) AS 평균급여
FROM EMP
WHERE SALARY > (SELECT AVG(SALARY) FROM EMP)
GROUP BY D_NAME;
②
SELECT D_NAME AS 부서명, AVG(SALARY) AS 평균급여
FROM EMP
GROUP BY D_NAME
HAVING AVG(SALARY) > (SELECT AVG(SALARY) FROM EMP);
③
SELECT D_NAME AS 부서명, AVG(SALARY) AS 평균급여
FROM EMP
GROUP BY D_NAME
HAVING MIN(SALARY) >= (SELECT AVG(SALARY) FROM EMP);
④
SELECT D_NAME AS 부서명, AVG(SALARY) AS 평균급여
FROM EMP
GROUP BY D_NAME
HAVING MIN(SALARY) > (SELECT AVG(SALARY) FROM EMP);
정답 및 해설 보기
정답 ④
'부서의 모든 사원 급여 > 전체 평균'은 곧 '그 부서에서 가장 낮은 급여 > 전체 평균'이므로 HAVING MIN(SALARY) > (전체 평균)이 정확하다. ① WHERE로 미리 거르면 평균보다 낮은 사원이 빠진 채 부서 평균이 계산돼 '모든 사원' 조건과 의미가 달라진다. ② HAVING AVG(SALARY) >는 '부서 평균'이 높은 것이라 '모든 사원'과 다르다. ③ >=는 '높은(초과)'이 아니라 '이상'이라 전체 평균과 같은 값까지 포함돼 어긋난다.
⚠️ 함정 '모든 ~보다 높은' = MIN(...) >(초과). '이상(>=)'과 명확히 구분.
문 22. 아래 SQL이 실행되는 논리적 순서로 올바르게 나열된 것은?
SELECT DEPT_ID, AVG(SALARY) AS AVG_SALARY
FROM EMP
WHERE SALARY >= 4000
GROUP BY DEPT_ID
HAVING AVG(SALARY) >= 5000
ORDER BY AVG_SALARY DESC;
- ① SELECT - FROM - WHERE - GROUP BY - HAVING - ORDER BY
- ② SELECT - FROM - WHERE - GROUP BY - ORDER BY - HAVING
- ③ FROM - WHERE - GROUP BY - HAVING - ORDER BY - SELECT
- ④ FROM - WHERE - GROUP BY - HAVING - SELECT - ORDER BY
정답 및 해설 보기
정답 ④
SQL의 논리적 실행 순서는 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY다. ④가 정확하다. ③은 SELECT가 ORDER BY보다 뒤라 틀리다 — ORDER BY가 SELECT의 별칭(AVG_SALARY)을 쓰려면 SELECT가 먼저 평가돼야 한다.
🔑 암기 실행 순서 FROM-WHERE-GROUP BY-HAVING-SELECT-ORDER BY. 작성 순서(SELECT 먼저)와 다르다.
문 23. 아래 테이블을 참고할 때 실행 결과가 다른 하나는? 🎯 고난도
[TBL]
| C1 |
|---|
| AAAA |
| BBB |
| CC |
| D |
| NULL |
①
SELECT * FROM TBL
WHERE C1 LIKE '%_%';
②
SELECT * FROM TBL
WHERE REGEXP_LIKE(C1, '.+');
③
SELECT * FROM TBL
WHERE NVL(LENGTH(C1), 0) >= 1;
④
SELECT * FROM TBL
WHERE NVL(TRIM(C1), ' ') LIKE '_%';
정답 및 해설 보기
정답 ④
①②③은 NULL을 잡지 못한다 — LIKE·REGEXP_LIKE는 NULL을 매치하지 못하고, ③도 LENGTH(NULL)이 NULL이라 NVL(...,0)=0이 되어 >= 1에서 탈락한다. 그래서 AAAA·BBB·CC·D 4건이다. ④는 NVL(TRIM(C1), ' ')로 NULL을 공백 한 글자로 치환한 뒤 LIKE '_%'(한 글자 이상)로 검사하므로 NULL 행까지 매치되어 5건이다. 실행하면 ④만 5건이고 나머지는 4건이다.
⚠️ 함정 NULL은 LIKE·REGEXP_LIKE·LENGTH로 안 잡히지만, NVL로 비(非)NULL 값으로 치환하면 잡힌다.
문 24. T1, T2 테이블에 각각 20개의 행이 존재할 때 아래 SQL의 실행 결과는?
SELECT COUNT(*) FROM T1, T2;
- ① 20
- ② 40
- ③ 400
- ④ 오류 발생
정답 및 해설 보기
정답 ③
FROM 절에 두 테이블을 콤마로 나열하고 조인 조건이 없으면 카티션 곱(Cartesian Product, 크로스 조인)이 된다. T1의 모든 행과 T2의 모든 행이 결합되어 20 × 20 = 400건이 되고, COUNT(*)는 400을 반환한다.
⚠️ 함정 조인 조건 누락 = 카티션 곱. 건수가 두 테이블 행 수의 곱으로 폭증한다.
문 25. 월 매출이 1000 이상인 지점들을 대상으로 지역별 월 매출의 합계 금액이 낮은 순으로 정렬하려고 할 때 아래 SQL에서 고쳐야 할 부분은?
SELECT 지점명, SUM(월매출) -- (가)
FROM 지점별매출
WHERE 월매출 >= 1000 -- (나)
GROUP BY 지점명 -- (다)
ORDER BY SUM(월매출) DESC; -- (라)
- ① (가)
- ② (나)
- ③ (다)
- ④ (라)
정답 및 해설 보기
정답 ④
요구사항은 합계 금액을 '낮은 순', 즉 오름차순(ASC)으로 정렬하는 것이다. (라)는 DESC(내림차순, 높은 순)라 요구와 반대다. DESC를 ASC로 바꾸거나 생략해야 한다. (가) 합계 출력, (나) 1000 이상 필터, (다) 지점명 그룹화는 모두 요구와 일치한다.
🔑 암기 ASC=오름차순(낮은→높은, 기본값) · DESC=내림차순(높은→낮은).
문 26. 오류가 발생하는 SQL은? (단, DEPT의 칼럼은 DEPT_NO, DEPT_NAME, SAL로 가정함)
①
SELECT DEPT_NO, SAL
FROM DEPT
ORDER BY DEPT_NAME;
②
SELECT DEPT_NO, SUM(SAL)
FROM DEPT
GROUP BY DEPT_NO
ORDER BY DEPT_NAME;
③
SELECT DEPT_NO, SUM(SAL)
FROM DEPT
GROUP BY DEPT_NO
HAVING COUNT(*) >= 5;
④
SELECT DEPT_NO, SUM(SAL) AS SAL
FROM DEPT
GROUP BY DEPT_NO
HAVING SUM(SAL) > 2000
ORDER BY COUNT(*) DESC;
정답 및 해설 보기
정답 ②
GROUP BY DEPT_NO로 묶으면 SELECT·ORDER BY에는 GROUP BY 컬럼이나 집계 함수만 올 수 있다. ②는 ORDER BY DEPT_NAME인데 DEPT_NAME은 GROUP BY 컬럼도 집계 함수도 아니라 'ORA-00979: GROUP BY 표현식이 아닙니다' 오류가 난다. ① GROUP BY가 없어 ORDER BY DEPT_NAME이 정상, ③ HAVING COUNT()·④ ORDER BY COUNT()는 집계라 정상이다.
⚠️ 함정 GROUP BY 사용 시 SELECT·ORDER BY에는 'GROUP BY 컬럼 또는 집계 함수'만 허용.
문 27. 아래 SQL의 실행 결과는?
[TAB1]
| C1 | C2 |
|---|---|
| A | 1 |
| B | NULL |
| C | 3 |
[TAB2]
| C1 | C2 |
|---|---|
| A | 1 |
| B | 4 |
| C | 3 |
SELECT C1 FROM TAB1 WHERE C2 = 2;
SELECT C1 FROM TAB2 WHERE C2 IS NULL;
- ① 공집합, 공집합
- ② 공집합, NULL
- ③ NULL, 공집합
- ④ NULL, NULL
정답 및 해설 보기
정답 ①
첫 쿼리는 TAB1에 C2 = 2인 행이 없어 0건(공집합)이다. 둘째 쿼리는 TAB2에 C2 IS NULL인 행이 없어(1·4·3뿐) 역시 0건(공집합)이다. 두 쿼리 모두 집계 함수가 없어 NULL이 아니라 공집합을 반환한다. 따라서 공집합, 공집합이다.
🔑 암기 WHERE 조건 0건 + 집계 함수 없음 → 공집합(NULL 아님). NULL은 집계 함수에 공집합이 들어갈 때 나온다.
문 28. 아래 실행 결과를 출력하는 SQL로 가장 적절한 것은?
[TBL]
| COL1 | COL2 |
|---|---|
| 1 | 10 |
| 2 | 10 |
| 3 | 20 |
| 4 | 20 |
| 5 | 30 |
[실행 결과]
| R1 |
|---|
| 3 |
①
SELECT COUNT(COL2) AS R1
FROM TBL;
②
SELECT COUNT(DISTINCT(COL2)) AS R1
FROM TBL;
③
SELECT COUNT(COL2) AS R1
FROM TBL
GROUP BY COL2;
④
SELECT COUNT(DISTINCT(COL2)) AS R1
FROM TBL
GROUP BY COL2;
정답 및 해설 보기
정답 ②
실행 결과가 단일 행 '3'이다. ② COUNT(DISTINCT(COL2))는 중복 제거한 COL2 값(10·20·30) 3개를 세어 한 줄로 3을 반환한다. ① COUNT(COL2)는 NULL이 아닌 행 수라 5다. ③④는 GROUP BY COL2라 그룹별로 여러 행이 나와 단일 행 결과와 맞지 않는다.
🔑 암기 COUNT(DISTINCT 컬럼) = 중복 제거한 값의 개수(단일 행).
문 29. 아래 SQL의 실행 결과는?
[T1]
| ID | C1 |
|---|---|
| 1 | AAA |
| 5 | BBB |
| 10 | CCC |
[T2]
| ID | X1 |
|---|---|
| 10 | AAA |
| 20 | BBB |
| 30 | CCC |
| 40 | DDD |
| 50 | EEE |
SELECT SUM(cnt)
FROM (
SELECT COUNT(*) AS cnt
FROM T1 NATURAL JOIN T2
UNION ALL
SELECT COUNT(*) AS cnt
FROM T1
WHERE ID NOT IN (SELECT ID FROM T2)
);
- ① 3
- ② 4
- ③ 5
- ④ 6
정답 및 해설 보기
정답 ①
NATURAL JOIN은 이름이 같은 컬럼 ID로 INNER JOIN한다. T1.ID(1·5·10)와 T2.ID(10·20·30·40·50)의 공통은 10 하나뿐이라 첫 줄 COUNT는 1이다. UNION ALL 아래는 T1에서 ID가 T2에 없는 행(1·5)으로 COUNT는 2다. 인라인 뷰가 1·2 두 행을 만들고 SUM(cnt)로 합치면 3이다. 실행하면 3을 반환한다.
🔑 암기 NATURAL JOIN = 동일명 컬럼으로 INNER JOIN. NOT IN 서브쿼리에 NULL이 없으면(T2.ID 모두 값 있음) 정상 작동.
문 30. 아래 SQL의 실행 결과는?
[TBL1]
| C1 | C2 |
|---|---|
| 10 | 20 |
| 30 | 20 |
| 30 | NULL |
| NULL | 10 |
[TBL2]
| C1 | C2 |
|---|---|
| 30 | NULL |
| 40 | 30 |
| 40 | 10 |
SELECT A.C2 AS A_C2, B.C2 AS B_C2
FROM TBL1 A
INNER JOIN TBL2 B
ON A.C1 = B.C1;
①
| A_C2 | B_C2 |
|---|---|
| NULL | NULL |
②
| A_C2 | B_C2 |
|---|---|
| 20 | NULL |
③
| A_C2 | B_C2 |
|---|---|
| 20 | NULL |
| NULL | NULL |
④
| A_C2 | B_C2 |
|---|---|
| NULL | 20 |
| NULL | NULL |
정답 및 해설 보기
정답 ③
조인 키 A.C1 = B.C1에서 NULL은 어느 값과도 같다고 판정되지 않으므로(NULL = NULL은 UNKNOWN) C1이 NULL인 TBL1의 마지막 행은 조인에서 빠진다. TBL2에서 C1=30인 행은 (30, NULL) 하나다. TBL1의 C1=30인 두 행 (30, 20)·(30, NULL)이 이와 매치되어 (A_C2=20, B_C2=NULL)·(A_C2=NULL, B_C2=NULL) 두 행이 나온다(C1=10·40은 짝이 없음). 실행하면 이 2행을 반환한다.
⚠️ 함정 INNER JOIN의 조인 키에서 NULL = NULL은 참이 아니라 UNKNOWN이라, NULL 키 행은 조인되지 않는다.
문 31. 아래 실행 결과를 출력하는 SQL은?
[TAB1]
| ID | NAME |
|---|---|
| 10 | Amy |
| 20 | Bill |
| 30 | Chris |
[TAB2]
| ID | AGE |
|---|---|
| 20 | 37 |
| 30 | 41 |
| 40 | 53 |
[실행 결과]
| ID | NAME | AGE |
|---|---|---|
| 20 | Bill | 37 |
| 30 | Chris | 41 |
| 40 | NULL | 53 |
①
SELECT *
FROM TAB1 INNER JOIN TAB2 USING(ID);
②
SELECT *
FROM TAB1 LEFT OUTER JOIN TAB2 USING(ID);
③
SELECT *
FROM TAB1 RIGHT OUTER JOIN TAB2 USING(ID);
④
SELECT *
FROM TAB1 FULL OUTER JOIN TAB2 USING(ID);
정답 및 해설 보기
정답 ③
실행 결과에 ID=40 행이 NAME=NULL로 포함돼 있다. 40은 TAB2(오른쪽)에만 있는 ID이므로, 오른쪽 테이블을 모두 보존하는 RIGHT OUTER JOIN이다. 반대로 TAB1에만 있는 ID=10은 결과에 없으니 왼쪽은 보존되지 않았다. INNER였다면 공통 20·30만, FULL이었다면 10까지 모두 나왔을 것이다.
🔑 암기 결과에 한쪽 전용 행이 NULL을 달고 남으면 그쪽을 보존하는 OUTER JOIN. 오른쪽만 남으면 RIGHT.
문 32. 아래 SQL의 실행 결과는?
[학생]
| 학생ID | 학생명 |
|---|---|
| 1000 | 김길수 |
| 1001 | 박진영 |
| 1002 | 박춘식 |
| 1003 | 최영자 |
[수상자명단]
| 학생ID | 소속학과 | 연락처 |
|---|---|---|
| 1001 | 통계학과 | 010-123-4567 |
| 1002 | 통계학과 | 010-234-5678 |
| 1003 | 철학과 | 010-345-6789 |
| 1005 | 국문학과 | 02-1000-1234 |
| 1006 | 철학과 | 02-1000-2345 |
SELECT COUNT(*)
FROM 학생
JOIN 수상자명단
ON 학생.학생ID = 수상자명단.학생ID
WHERE 수상자명단.소속학과
IN ('통계학과', '철학과')
AND 수상자명단.학생ID
NOT IN (SELECT 학생ID
FROM 수상자명단
WHERE 소속학과 = '국문학과');
- ① 3
- ② 4
- ③ 5
- ④ 6
정답 및 해설 보기
정답 ①
학생(1000~1003)과 수상자명단을 학생ID로 조인하면 공통은 1001·1002·1003이다. WHERE 소속학과 IN ('통계학과','철학과')에서 1001(통계)·1002(통계)·1003(철학)이 모두 통과한다. NOT IN (국문학과 학생ID = 1005)에서 셋 다 1005가 아니므로 그대로 남는다. 따라서 COUNT(*)는 3이다. 실행하면 3을 반환한다.
⚠️ 함정 NOT IN 서브쿼리 결과(1005)에 NULL이 없어 정상 동작한다. 조인 단계에서 학생에 없는 1005·1006은 이미 빠진다.
문 33. 아래 SQL의 실행 결과는? 🎯 고난도
[T1]
| C1 |
|---|
| 30 |
| 10 |
| 40 |
| 50 |
| 20 |
[T2]
| C1 |
|---|
| 30 |
| 40 |
| NULL |
SELECT COUNT(*)
FROM T1
WHERE C1 NOT IN (
SELECT C1
FROM T2
WHERE C1 <= (
SELECT MAX(C1) FROM T2)
OR C1 IS NULL
);
- ① 0
- ② 1
- ③ 2
- ④ 3
정답 및 해설 보기
정답 ①
서브쿼리는 T2에서 C1 <= MAX(C1)(=40)이거나 C1 IS NULL인 행을 고르므로 30·40·NULL이 모두 포함된다. 바깥 쿼리 C1 NOT IN (30, 40, NULL)은 리스트에 NULL이 끼어 있어 모든 비교가 UNKNOWN이 되고, 결국 어떤 행도 통과하지 못해 0건이 된다. 실행하면 0을 반환한다.
⚠️ 함정 NOT IN 리스트에 NULL이 하나라도 있으면 결과는 항상 0건이다. SQLD 최다 함정.
문 34. 아래 SQL에 대한 설명으로 가장 적절한 것은?
[테이블 생성]
- 지점 (지점ID, 지점명, 전화번호)
- 제품 (제품ID, 제품명, 가격)
- 제품판매 (지점ID, 제품ID, 판매일자, 판매수량)
SELECT 지점.지점ID, SUM(판매수량) AS 총판매수량
FROM 지점, 제품, 제품판매
WHERE 지점.지점ID = 제품판매.지점ID
AND 제품.제품ID = 제품판매.제품ID
GROUP BY 지점.지점ID
HAVING MAX(판매수량) > 20
ORDER BY 2;
- ① 판매수량이 20을 초과한 판매를 한 기록이 있는 지점의 지점ID와 판매수량의 총합을 구하고, 총판매수량이 큰 순서대로 정렬한다.
- ② 판매수량이 20을 초과한 판매를 한 기록이 있는 지점의 지점ID와 판매수량의 총합을 구하고, 총판매수량이 작은 순서대로 정렬한다.
- ③ 21회 이상 제품 판매를 한 지점의 지점ID와 판매수량의 총합을 구하고, 총판매수량이 큰 순서대로 정렬한다.
- ④ 21회 이상 제품 판매를 한 지점의 지점ID와 판매수량의 총합을 구하고, 총판매수량이 작은 순서대로 정렬한다.
정답 및 해설 보기
정답 ②
HAVING MAX(판매수량) > 20은 '한 번에 20개를 초과해 판매한 기록이 있는' 지점을 고른다(판매 횟수가 아니라 단일 판매수량). SUM(판매수량)으로 총합을 구하고, ORDER BY 2는 SELECT의 두 번째 컬럼인 총판매수량 기준 정렬인데 ASC/DESC가 없어 기본 오름차순(작은 순)이다. 따라서 ②가 정확하다. ③④의 '21회 이상 판매'는 COUNT에 대한 조건이라 MAX 조건과 다르고, ①은 정렬 방향이 반대다.
⚠️ 함정 HAVING MAX(...) > 20은 '한 건이라도 20 초과'(총합·횟수 아님). ORDER BY 2는 SELECT 두 번째 컬럼, 기본 ASC.
문 35. 집합 연산자(Set Operation) 중에서 수학의 차집합의 기능을 수행하는 연산자로 가장 적절한 것은?
- ① MINUS
- ② SUBTRACT
- ③ INTERSECT
- ④ DIFFERENCE
정답 및 해설 보기
정답 ①
차집합(앞 쿼리에는 있고 뒤 쿼리에는 없는 행)을 구하는 연산자는 오라클의 MINUS(표준 SQL·일부 DBMS에서는 EXCEPT)다. ② SUBTRACT·④ DIFFERENCE는 수학 용어일 뿐 SQL 집합 연산자가 아니다. ③ INTERSECT는 교집합이다.
🔑 암기 합집합 UNION·UNION ALL · 교집합 INTERSECT · 차집합 MINUS(= EXCEPT).
문 36. 아래 SQL의 실행 결과는?
[TBL1]
| K_ID | ID | AMT |
|---|---|---|
| 1 | A001 | 1000 |
| 2 | A002 | 2000 |
| 3 | A001 | 1600 |
| 4 | A002 | 2500 |
| 5 | A003 | 1300 |
[TBL2]
| ID | NAME |
|---|---|
| A001 | Andrew |
| A002 | James |
| A003 | Linda |
SELECT NAME
FROM TBL2
WHERE ID IN (SELECT ID FROM TBL1
WHERE AMT > 1500);
①
| NAME |
|---|
| Andrew |
②
| NAME |
|---|
| Andrew |
| James |
③
| NAME |
|---|
| Andrew |
| Linda |
④
| NAME |
|---|
| Andrew |
| James |
| Linda |
정답 및 해설 보기
정답 ②
서브쿼리 TBL1 WHERE AMT > 1500은 A001(1600)·A002(2000)·A002(2500) 행을 골라 ID 집합 {A001, A002}를 만든다. 바깥 쿼리는 TBL2에서 ID가 A001·A002인 행의 NAME을 찾으므로 Andrew·James다(A003 Linda는 AMT 1300뿐이라 제외). 실행하면 Andrew, James 두 건이다.
🔑 암기 IN (서브쿼리)는 서브쿼리부터 평가해 값 목록을 만든 뒤 바깥 조건에 대입한다.
문 37. 아래 테이블을 참고할 때 SQL의 실행 결과가 다른 하나는? 🎯 고난도
[SQL]
CREATE TABLE TBL (
C1 VARCHAR2(5)
);
INSERT INTO TBL VALUES('A');
INSERT INTO TBL VALUES('B');
INSERT INTO TBL VALUES('C');
INSERT INTO TBL VALUES(NULL);
COMMIT;
①
SELECT * FROM TBL
WHERE C1 NOT IN ('A', 'B');
②
SELECT * FROM TBL
WHERE C1 IN ('C', NULL);
③
SELECT * FROM TBL T1
WHERE NOT EXISTS (
SELECT 1 FROM TBL T2
WHERE T1.C1 = T2.C1);
④
SELECT * FROM TBL T1
WHERE EXISTS (
SELECT 1 FROM TBL T2
WHERE T1.C1 = T2.C1 AND T2.C1
IN ('C', NULL));
정답 및 해설 보기
정답 ③
①②④는 모두 'C' 한 건을 반환한다. ① NOT IN ('A','B')는 C만 통과(NULL은 비교가 UNKNOWN이라 탈락), ② IN ('C', NULL)은 NULL이 무시되어 C1='C'와 같음, ④ EXISTS ... T2.C1 IN ('C', NULL)도 결국 C1='C'일 때만 참이다. 반면 ③ NOT EXISTS (... T1.C1 = T2.C1)은 A·B·C는 자기 자신과 매치되어 EXISTS가 참 → NOT EXISTS 거짓으로 빠지고, NULL 행만 T1.C1 = T2.C1이 UNKNOWN이라 매치되는 T2가 없어 NOT EXISTS가 참이 된다. 그래서 ③만 NULL 행을 반환해 결과가 다르다.
⚠️ 함정 IN/NOT IN은 NULL 비교를 UNKNOWN으로 흘리지만, NOT EXISTS는 '매치되는 행이 없음'을 참으로 보아 NULL 행을 살린다.
문 38. SELF JOIN에 대한 설명으로 가장 적절하지 않은 것은?
- ① SELF JOIN은 하나의 테이블이 자기 자신과 조인할 때 사용한다.
- ② SELF JOIN에서는 테이블에 별칭(Alias)을 부여하여 사용한다.
- ③ SELF JOIN은 서로 다른 두 테이블 간의 관계를 표현할 때 사용한다.
- ④ SELF JOIN은 하나의 테이블에서 두 개의 칼럼이 연관 관계가 있을 때 사용한다.
정답 및 해설 보기
정답 ③
SELF JOIN은 하나의 테이블을 자기 자신과 조인하는 기법으로, 같은 테이블을 별칭 두 개로 구분해 사용한다(①②④). ③ '서로 다른 두 테이블 간의 관계'는 일반 조인(INNER·OUTER 등)에 대한 설명이라 SELF JOIN과 어긋난다.
🔑 암기 SELF JOIN = 한 테이블을 별칭 두 개로 자기 자신과 조인(사원–관리자처럼 한 테이블 내 관계).
문 39. 대상 문자열에서 정규표현식 패턴과 일치하는 부분 문자열을 반환하는 함수로 가장 적절한 것은?
- ① REGEXP_LIKE
- ② REGEXP_INSTR
- ③ REGEXP_SUBSTR
- ④ REGEXP_REPLACE
정답 및 해설 보기
정답 ③
REGEXP_SUBSTR은 패턴에 일치하는 '부분 문자열' 자체를 잘라 반환한다. ① REGEXP_LIKE는 일치 여부(참/거짓), ② REGEXP_INSTR은 일치 시작 위치(숫자), ④ REGEXP_REPLACE는 일치 부분을 다른 문자열로 치환한다.
🔑 암기 SUBSTR=부분 문자열 추출 · LIKE=판별 · INSTR=위치 · REPLACE=치환.
문 40. 아래 테이블을 참고할 때 조건에 해당되는 사원명과 관리자명을 조회하는 SQL로 가장 적절한 것은?
[조건] 전체 사원의 이름과 사원의 관리자 이름을 모두 출력한다(단, 관리자가 없는 사원은 출력하지 않음).
[사원]
| 사원ID | 사원명 | 관리자ID | 관리자명 |
|---|---|---|---|
| 100 | 김민수 | NULL | NULL |
| 101 | 김철수 | 100 | 김민수 |
| 102 | 박영희 | 101 | 김철수 |
| 103 | 박진영 | 102 | 박영희 |
| 104 | 정영식 | 103 | 박진영 |
| 105 | 최철민 | 104 | 정영식 |
| 106 | 최미영 | 105 | 최철민 |
①
SELECT 사원명, 관리자명 FROM 사원
START WITH 관리자ID = 100
CONNECT BY PRIOR 관리자ID = 사원ID;
②
SELECT 사원명, 관리자명 FROM 사원
START WITH 사원ID = 101
CONNECT BY PRIOR 관리자ID = 사원ID;
③
SELECT 사원명, 관리자명 FROM 사원
START WITH 관리자ID IS NULL
CONNECT BY PRIOR 사원ID = 관리자ID;
④
SELECT 사원명, 관리자명 FROM 사원
START WITH 관리자ID = 100
CONNECT BY PRIOR 사원ID = 관리자ID;
정답 및 해설 보기
정답 ④
조건은 관리자가 없는 사원(김민수, 관리자ID NULL)을 빼고 그 아래 직원을 모두 출력하는 것이다. ④는 START WITH 관리자ID = 100으로 김철수(관리자ID=100)부터 시작하고, CONNECT BY PRIOR 사원ID = 관리자ID(순방향)로 김철수→박영희→…→최미영까지 김민수를 제외한 전 직원을 전개한다. ③은 START WITH 관리자ID IS NULL이라 김민수부터 시작해 그를 포함하므로 조건에 어긋난다. ①② PRIOR 관리자ID = 사원ID는 자식에서 부모로 올라가는 역방향이라 하향 전개가 되지 않는다.
🔑 암기 순방향(부모→자식) = CONNECT BY PRIOR 부모키 = 자식키. PRIOR가 붙는 컬럼이 '먼저 읽은(상위)' 쪽이다.
문 41. 아래 SQL의 실행 결과는? 🎯 고난도
SELECT
REGEXP_SUBSTR('xxcccdyy', '[^c]c{2,3}')
FROM DUAL;
- ① ccc
- ② xccc
- ③ cccd
- ④ xxc
정답 및 해설 보기
정답 ②
패턴 [^c]c{2,3}은 'c가 아닌 한 글자' 뒤에 'c가 2~3회 반복'되는 부분을 찾는다. 'xxcccdyy'를 왼쪽부터 훑으면 두 번째 'x'(c가 아님)에 이어 'ccc'(c 3개)가 와서 'xccc'가 첫 일치다. REGEXP_SUBSTR은 첫 일치 부분 문자열을 반환하므로 결과는 'xccc'다. 실행하면 'xccc'를 반환한다.
🔑 암기 [^c]=c 제외 한 글자 · c{2,3}=c가 2~3회. REGEXP_SUBSTR은 최장 일치(greedy)로 첫 조각을 반환.
문 42. 실행 결과가 다른 하나는?
①
SELECT ENAME, DEPTNO DNO, EMPNO
FROM EMP
ORDER BY 1, DNO ASC, EMPNO DESC;
②
SELECT ENAME, DEPTNO, EMPNO
FROM EMP
ORDER BY ENAME, DEPTNO, 3 DESC;
③
SELECT ENAME, DEPTNO DNO, EMPNO
FROM EMP
ORDER BY ENAME DESC, DNO, 3 DESC;
④
SELECT ENAME, DEPTNO DNO, EMPNO
FROM EMP
ORDER BY 1, DNO, 3 DESC;
정답 및 해설 보기
정답 ③
SELECT 컬럼 순서는 (1: ENAME, 2: DEPTNO/DNO, 3: EMPNO)다. ①②④는 모두 ENAME 오름차순 → DEPTNO 오름차순 → EMPNO 내림차순으로 같다(1=ENAME, DNO=DEPTNO 별칭, 3=EMPNO). ③만 첫 기준이 ENAME DESC(내림차순)라 정렬 결과가 다르다.
🔑 암기 ORDER BY에는 컬럼명·별칭·SELECT 순번(숫자)을 섞어 쓸 수 있다. 첫 정렬 기준의 방향이 전체를 가른다.
문 43. 아래에서 설명하는 트랜잭션의 특징으로 가장 적절한 것은?
트랜잭션은 독립적으로 수행되며, 하나의 트랜잭션이 실행되는 동안 다른 트랜잭션이 그 작업에 영향을 주거나 중간 결과를 볼 수 없도록 보장한다.
- ① 원자성(Atomicity)
- ② 일관성(Consistency)
- ③ 고립성(Isolation)
- ④ 영속성(Durability)
정답 및 해설 보기
정답 ③
'독립적으로 수행', '서로 영향을 주지 않음', '중간 결과를 볼 수 없음'은 트랜잭션을 격리시키는 성질, 즉 고립성(Isolation)이다. ① 원자성은 All or Nothing, ② 일관성은 트랜잭션 전후 무결성 유지, ④ 영속성은 성공 시 영구 반영이다.
🔑 암기 ACID — 원자성 · 일관성 · 고립성(독립 수행·간섭 배제) · 영속성.
문 44. DELETE 명령어에 대한 설명으로 가장 적절하지 않은 것은?
- ① DML 명령어에 포함되며, 자동커밋 모드로 실행된다.
- ② COMMIT 전에는 ROLLBACK이 가능하지만, COMMIT 후에는 ROLLBACK이 불가능하다.
- ③ WHERE 절을 사용하여 특정 행만 삭제할 수 있다.
- ④ 작업취소를 위한 로그 데이터를 생성하기 때문에 TRUNCATE보다 속도가 느리다.
정답 및 해설 보기
정답 ①
DELETE는 DML이 맞지만 '자동커밋 모드로 실행'된다는 ①이 틀렸다. DML(INSERT·UPDATE·DELETE)은 COMMIT 전까지 반영되지 않고 ROLLBACK이 가능하다. 자동커밋되는 것은 DDL(CREATE·ALTER·DROP·TRUNCATE)·DCL(GRANT·REVOKE)이다. ②③④는 DELETE의 정확한 특징이다.
⚠️ 함정 DML = 수동 COMMIT(롤백 가능) / DDL·DCL = 자동 COMMIT. TRUNCATE(DDL)는 로그를 거의 남기지 않아 DELETE보다 빠르다.
문 45. 주어진 테이블을 참고할 때 아래 실행 결과를 출력하는 SQL로 가장 적절한 것은?
[TBL]
| EMP_ID | EMP_NAME | SALARY |
|---|---|---|
| 1 | James | 3550 |
| 2 | Linda | 3550 |
| 3 | Andrew | 3800 |
| 4 | Frank | 3800 |
| 5 | Kevin | 3100 |
| 6 | Maria | 3200 |
[실행 결과]
| RANK | EMP_NAME | SALARY |
|---|---|---|
| 1 | Andrew | 3800 |
| 1 | Frank | 3800 |
| 2 | James | 3550 |
| 2 | Linda | 3550 |
| 3 | Maria | 3200 |
| 4 | Kevin | 3100 |
①
SELECT ROW_NUMBER() OVER
(ORDER BY SALARY DESC) AS RANK,
EMP_NAME, SALARY
FROM TBL;
②
SELECT RANK() OVER
(ORDER BY SALARY DESC) AS RANK,
EMP_NAME, SALARY
FROM TBL;
③
SELECT DENSE_RANK() OVER
(ORDER BY SALARY DESC) AS RANK,
EMP_NAME, SALARY
FROM TBL;
④
SELECT PERCENT_RANK() OVER
(ORDER BY SALARY DESC) AS RANK,
EMP_NAME, SALARY
FROM TBL;
정답 및 해설 보기
정답 ③
실행 결과는 급여 내림차순으로 3800(공동 1위), 3550(공동 2위), 3200(3위), 3100(4위)이다. 공동 순위가 있어도 다음 순위가 건너뛰지 않고 촘촘히 이어지므로 DENSE_RANK다. RANK였다면 1·1·3·3·5·6으로 건너뛰고, ROW_NUMBER였다면 공동 없이 1~6, PERCENT_RANK는 0~1 사이 비율이 나온다.
🔑 암기 RANK(공동 뒤 건너뜀) · DENSE_RANK(안 건너뜀) · ROW_NUMBER(공동 없이 일련번호).
문 46. 아래 SQL의 실행 결과는? 🎯 고난도
[T1]
| 회원ID | 회원명 |
|---|---|
| 101 | 김철수 |
| 102 | 이영희 |
| 103 | 박영식 |
[T2]
| 회원ID | 주문제품 | 주문금액 |
|---|---|---|
| 101 | 사과 | 3000 |
| 102 | 바나나 | 2500 |
| 102 | 바나나 | 3500 |
| 103 | 토마토 | 4000 |
| 103 | 토마토 | 2000 |
| 103 | 오이 | 1500 |
SELECT DISTINCT T1.회원ID, T1.회원명
FROM T1
JOIN (
SELECT 회원ID,
주문금액,
DENSE_RANK() OVER (
ORDER BY 주문금액 DESC) AS 순위
FROM T2
) T2 ON T1.회원ID = T2.회원ID
WHERE T2.순위 <= 2
ORDER BY T1.회원ID;
①
| 회원ID | 회원명 |
|---|---|
| 103 | 박영식 |
②
| 회원ID | 회원명 |
|---|---|
| 101 | 김철수 |
| 102 | 이영희 |
③
| 회원ID | 회원명 |
|---|---|
| 102 | 이영희 |
| 103 | 박영식 |
④
| 회원ID | 회원명 |
|---|---|
| 101 | 김철수 |
| 103 | 박영식 |
정답 및 해설 보기
정답 ③
인라인 뷰는 T2 전체 주문을 주문금액 내림차순으로 DENSE_RANK를 매긴다 — 4000(1위)·3500(2위)·3000(3위)·2500(4위)·2000(5위)·1500(6위). WHERE 순위 <= 2로 4000(103)·3500(102)만 남는다. 이를 T1과 조인해 DISTINCT 회원ID·회원명을 뽑으면 102 이영희·103 박영식이고, ORDER BY 회원ID로 그 순서가 된다. 실행하면 이 두 행을 반환한다.
🔑 암기 인라인 뷰에서 DENSE_RANK로 순위를 매긴 뒤 바깥에서 순위 <= N으로 상위 N을 거른다.
문 47. 아래 실행 결과를 참고할 때 SQL의 빈칸 ㉠에 들어갈 내용으로 가장 적절한 것은?
SELECT A.제품, A.지점, FLOOR(AVG(A.판매수량))
AS 평균판매량
FROM 제품판매량 A
GROUP BY ㉠ ;
[실행 결과]
| 제품 | 지점 | 평균판매량 |
|---|---|---|
| 노트북 | 서울 | 15 |
| 노트북 | 인천 | 14 |
| 노트북 | 부산 | 10 |
| 모니터 | 서울 | 11 |
| 모니터 | 인천 | 13 |
| 모니터 | 부산 | 13 |
| 키보드 | 서울 | 9 |
| 키보드 | 인천 | 10 |
| 키보드 | 부산 | 8 |
| NULL | NULL | 11 |
- ① ROLLUP(제품)
- ② ROLLUP(제품, 지점)
- ③ ROLLUP((제품, 지점))
- ④ ROLLUP((제품, 지점), 제품)
정답 및 해설 보기
정답 ③
실행 결과는 (제품, 지점)별 상세 행과 마지막 전체 총계 (NULL, NULL)만 있고, '제품별 소계'(예: 노트북, NULL) 행이 없다. ROLLUP((제품, 지점))처럼 괄호로 묶으면 (제품, 지점)을 한 단위로 취급해 '(제품, 지점) 집계 + 전체 총계'만 생성하므로 결과와 일치한다. ② ROLLUP(제품, 지점)은 제품별 소계까지 만들어 결과보다 행이 많다.
🔑 암기 ROLLUP((A, B)) = (A, B) 상세 + 전체 총계(소계 없음). ROLLUP(A, B) = (A, B) + A 소계 + 총계.
문 48. 아래 SQL의 실행 결과는? (단, DBMS는 오라클로 가정함) 🎯 고난도
[사원보너스]
| 부서ID | 사원ID | 사원명 | 보너스 |
|---|---|---|---|
| A001 | 9976 | Alex | 850 |
| A001 | 9977 | Bill | 1050 |
| A001 | 9978 | Chris | 600 |
| A001 | 9978 | Chris | 800 |
| A002 | 9979 | Emma | 750 |
| A002 | 9979 | Emma | 850 |
| A003 | 9980 | George | 450 |
| A003 | 9981 | Helen | 900 |
SELECT
부서ID,
MAX(보너스) KEEP (DENSE_RANK LAST
ORDER BY 사원ID) AS 보너스2
FROM 사원보너스
GROUP BY 부서ID;
①
| 부서ID | 보너스2 |
|---|---|
| A001 | 800 |
| A002 | 850 |
| A003 | 900 |
②
| 부서ID | 보너스2 |
|---|---|
| A001 | 1050 |
| A002 | 850 |
| A003 | 900 |
③
| 부서ID | 보너스2 |
|---|---|
| A001 | 850 |
| A002 | 850 |
| A003 | 450 |
④
| 부서ID | 보너스2 |
|---|---|
| A001 | 850 |
| A002 | 850 |
| A003 | 900 |
정답 및 해설 보기
정답 ①
KEEP (DENSE_RANK LAST ORDER BY 사원ID)는 부서별로 사원ID가 가장 큰(LAST) 행 집합을 기준으로 삼고, 그중 MAX(보너스)를 취한다. A001은 사원ID 최대 9978의 보너스(600·800) 중 800, A002는 9979의 보너스(750·850) 중 850, A003은 9981의 보너스 900이다. 실행하면 800·850·900을 반환한다.
🔑 암기 KEEP (DENSE_RANK LAST ORDER BY x) = x가 가장 큰 행 집합 기준 집계. FIRST면 가장 작은 행 기준.
문 49. 아래 실행 결과를 출력하는 SQL은? 🎯 고난도
[실행 결과]
| 부서명 | 직급 | 급여총액 |
|---|---|---|
| NULL | NULL | 36500 |
| NULL | 부장 | 15000 |
| NULL | 과장 | 12000 |
| NULL | 대리 | 9500 |
| 인사부 | NULL | 19500 |
| 인사부 | 부장 | 8000 |
| 인사부 | 과장 | 6500 |
| 인사부 | 대리 | 5000 |
| 총무부 | NULL | 17000 |
| 총무부 | 부장 | 7000 |
| 총무부 | 과장 | 5500 |
| 총무부 | 대리 | 4500 |
①
SELECT 부서명, 직급, SUM(급여) AS 급여총액
FROM 직원급여
GROUP BY CUBE(부서명, 직급);
②
SELECT 부서명, 직급, SUM(급여) AS 급여총액
FROM 직원급여
GROUP BY ROLLUP(부서명, 직급);
③
SELECT 부서명, 직급, SUM(급여) AS 급여총액
FROM 직원급여
GROUP BY GROUPING SETS(부서명, 직급);
④
SELECT 부서명, 직급, SUM(급여) AS 급여총액
FROM 직원급여
GROUP BY GROUPING SETS((부서명, 직급));
정답 및 해설 보기
정답 ①
실행 결과에는 (부서명, 직급) 상세, (부서명, NULL) 부서별 소계, (NULL, 직급) 직급별 소계, (NULL, NULL) 전체 총계가 모두 들어 있다. 이렇게 인자로 준 컬럼들의 '모든 조합'에 대한 소계·총계를 만드는 것은 CUBE다. ② ROLLUP은 직급별 소계가 없고, ③ GROUPING SETS(부서명, 직급)은 상세·총계 구성이 다르며, ④ GROUPING SETS((부서명, 직급))은 상세 한 묶음만 만든다.
🔑 암기 CUBE(A, B) = A·B 모든 조합 소계 + 총계. ROLLUP(A, B) = 계층적((A,B)·A·총계).
문 50. 아래 SQL의 실행 결과는?
CREATE TABLE TBL(COL1 NUMBER, COL2 NUMBER);
INSERT INTO TBL VALUES (10, 20);
INSERT INTO TBL VALUES (20, 30);
INSERT INTO TBL VALUES (30, 40);
SAVEPOINT S1;
UPDATE TBL SET COL1 = 40
WHERE COL2 >= 30;
SAVEPOINT S2;
INSERT INTO TBL VALUES (40, 20);
SAVEPOINT S2;
DELETE FROM TBL
WHERE COL2 = 30;
ROLLBACK TO SAVEPOINT S2;
COMMIT;
SELECT COUNT(*) FROM TBL
WHERE COL1 = 40;
- ① 0
- ② 1
- ③ 2
- ④ 3
정답 및 해설 보기
정답 ④
순서대로 따라간다. INSERT 3건 후 UPDATE COL1=40 WHERE COL2>=30으로 (20,30)·(30,40)이 (40,30)·(40,40)이 된다. 이후 INSERT (40,20)로 (40,20)이 추가된다. 여기서 SAVEPOINT S2가 다시 선언되어 S2가 이 시점(INSERT 직후)으로 갱신된다. DELETE WHERE COL2=30으로 (40,30)이 지워지지만, ROLLBACK TO SAVEPOINT S2는 가장 최근 S2(INSERT 직후)로 되돌아가 DELETE만 취소한다. 최종적으로 COL1=40인 행은 (40,30)·(40,40)·(40,20) 세 건이다. 실행하면 3을 반환한다.
⚠️ 함정 같은 이름의 SAVEPOINT를 다시 선언하면 그 위치로 갱신된다. ROLLBACK TO S2는 두 번째 S2(INSERT 뒤)로 복귀하므로 그 뒤의 DELETE만 취소된다.
합격까지
SQLD, 약점 유형이 보이나요?
초개인화 학습앱 Klue로 틀린 유형을 집중 공략하고, 에듀윌 온라인강의로 개념까지 정리하세요.
