6편 - SQLD 2과목 2장 : SQL 활용
제2장. SQL 활용
2.1 JOIN
2.1.1 JOIN의 개념
JOIN은 두 개 이상의 테이블에서 데이터를 조합하여 결과를 반환하는 기법이다.
2.1.2 EQUI JOIN (등가 조인)
-- Oracle 전통 방식 SELECT e.사원명, d.부서명 FROM 사원 e, 부서 d WHERE e.부서코드 = d.부서코드; -- ANSI 표준 방식 (INNER JOIN) SELECT e.사원명, d.부서명 FROM 사원 e INNER JOIN 부서 d ON e.부서코드 = d.부서코드; -- USING 절 (칼럼명이 동일할 때) SELECT e.사원명, d.부서명 FROM 사원 e JOIN 부서 d USING (부서코드); -- USING 절에서는 테이블 별칭 사용 불가! (부서코드 앞에 e. 또는 d. 못 씀) -- NATURAL JOIN (동일 칼럼명 자동 매핑) SELECT 사원명, 부서명 FROM 사원 NATURAL JOIN 부서; -- 주의: 동일 칼럼명이 여러 개면 모두 조인 조건에 포함됨
2.1.3 NON-EQUI JOIN (비등가 조인)
-- 등호(=)가 아닌 비교 연산자 사용 SELECT e.사원명, e.급여, g.등급 FROM 사원 e, 급여등급 g WHERE e.급여 BETWEEN g.최저급여 AND g.최고급여;
2.1.4 OUTER JOIN
-- LEFT OUTER JOIN: 왼쪽 테이블의 모든 행 보존 SELECT e.사원명, d.부서명 FROM 사원 e LEFT OUTER JOIN 부서 d ON e.부서코드 = d.부서코드; -- 부서가 없는 사원도 결과에 포함 (부서명은 NULL) -- Oracle 전통 방식 (+) SELECT e.사원명, d.부서명 FROM 사원 e, 부서 d WHERE e.부서코드 = d.부서코드(+); -- (+)는 데이터가 없는 쪽에 붙인다! -- RIGHT OUTER JOIN: 오른쪽 테이블의 모든 행 보존 SELECT e.사원명, d.부서명 FROM 사원 e RIGHT OUTER JOIN 부서 d ON e.부서코드 = d.부서코드; -- 사원이 없는 부서도 결과에 포함 -- FULL OUTER JOIN: 양쪽 모두 보존 SELECT e.사원명, d.부서명 FROM 사원 e FULL OUTER JOIN 부서 d ON e.부서코드 = d.부서코드; -- Oracle 전통 방식으로는 FULL OUTER JOIN 불가!
2.1.5 CROSS JOIN (교차 조인)
-- 카테시안 곱: M × N 건 생성 SELECT e.사원명, d.부서명 FROM 사원 e CROSS JOIN 부서 d; -- Oracle 전통 방식: WHERE 절 없이 조인 SELECT e.사원명, d.부서명 FROM 사원 e, 부서 d;
2.1.6 SELF JOIN (자기 조인)
-- 같은 테이블을 두 번 사용 SELECT e.사원명 AS 사원, m.사원명 AS 관리자 FROM 사원 e LEFT JOIN 사원 m ON e.관리자번호 = m.사원번호;
2.2 서브쿼리 (Subquery)
2.2.1 서브쿼리의 종류
| 위치 | 명칭 | 설명 |
|---|---|---|
| WHERE 절 | 서브쿼리 (중첩 서브쿼리) | 조건 비교에 사용 |
| FROM 절 | 인라인 뷰 (Inline View) | 가상 테이블로 사용 |
| SELECT 절 | 스칼라 서브쿼리 | 하나의 값만 반환 |
2.2.2 단일행 서브쿼리 vs 다중행 서브쿼리
-- 단일행 서브쿼리: 결과가 1건 → =, >, <, >=, <= 사용 SELECT * FROM 사원 WHERE 급여 > (SELECT AVG(급여) FROM 사원); -- 다중행 서브쿼리: 결과가 여러 건 → IN, ANY, ALL, EXISTS 사용 SELECT * FROM 사원 WHERE 부서코드 IN (SELECT 부서코드 FROM 부서 WHERE 지역 = '서울');
다중행 비교 연산자
| 연산자 | 설명 | 예시 |
|---|---|---|
| IN | 목록 중 하나와 일치 | WHERE 급여 IN (3000, 4000, 5000) |
| ANY/SOME | 목록 중 하나라도 만족 | WHERE 급여 > ANY (3000, 4000, 5000) → 급여 > 3000 |
| ALL | 목록 모두 만족 | WHERE 급여 > ALL (3000, 4000, 5000) → 급여 > 5000 |
| EXISTS | 서브쿼리 결과 존재 여부 | WHERE EXISTS (SELECT 1 FROM 주문 WHERE ...) |
시험 핵심:
> ANY= 최솟값보다 크면 됨> ALL= 최댓값보다 커야 함< ANY= 최댓값보다 작으면 됨< ALL= 최솟값보다 작아야 함
2.2.3 연관 서브쿼리 (Correlated Subquery)
-- 메인 쿼리의 칼럼을 서브쿼리에서 참조 SELECT e.사원명, e.급여, e.부서코드 FROM 사원 e WHERE e.급여 > (SELECT AVG(e2.급여) FROM 사원 e2 WHERE e2.부서코드 = e.부서코드); -- 자기 부서의 평균 급여보다 높은 사원
2.2.4 인라인 뷰 (Inline View)
-- FROM 절에 서브쿼리 SELECT a.부서코드, a.평균급여 FROM (SELECT 부서코드, AVG(급여) AS 평균급여 FROM 사원 GROUP BY 부서코드) a WHERE a.평균급여 >= 5000;
2.2.5 스칼라 서브쿼리 (Scalar Subquery)
-- SELECT 절에 서브쿼리 (반드시 1건, 1칼럼 반환) SELECT 사원명, 급여, (SELECT 부서명 FROM 부서 WHERE 부서코드 = e.부서코드) AS 부서명 FROM 사원 e;
2.3 집합 연산자
2.3.1 집합 연산자 종류
| 연산자 | 설명 | 중복 제거 |
|---|---|---|
| UNION | 합집합 | O (중복 제거) |
| UNION ALL | 합집합 | X (중복 포함, 성능 좋음) |
| INTERSECT | 교집합 | O |
| MINUS (Oracle) | 차집합 | O |
-- 두 쿼리의 칼럼 수와 데이터 타입이 일치해야 함 SELECT 사원번호, 사원명 FROM 정규사원 UNION SELECT 사원번호, 사원명 FROM 계약사원; -- UNION ALL은 정렬하지 않아 성능이 좋음 SELECT 사원번호, 사원명 FROM 정규사원 UNION ALL SELECT 사원번호, 사원명 FROM 계약사원; -- MINUS: 첫 번째 결과에서 두 번째 결과를 뺌 SELECT 부서코드 FROM 부서 MINUS SELECT DISTINCT 부서코드 FROM 사원; -- 사원이 없는 부서
2.4 그룹 함수
2.4.1 집계 함수 (Aggregate Function)
| 함수 | 설명 | NULL 처리 |
|---|---|---|
| COUNT(*) | 전체 행 수 | NULL 포함 |
| COUNT(칼럼) | 해당 칼럼의 NULL이 아닌 행 수 | NULL 제외 |
| SUM(칼럼) | 합계 | NULL 제외 |
| AVG(칼럼) | 평균 | NULL 제외 |
| MAX(칼럼) | 최대값 | NULL 제외 |
| MIN(칼럼) | 최소값 | NULL 제외 |
시험 핵심: AVG는 NULL을 제외하고 계산한다! 예: 값이 100, 200, NULL인 경우 → AVG = (100+200)/2 = 150 (3으로 나누지 않음)
SELECT 부서코드, COUNT(*) AS 인원수, SUM(급여) AS 급여합계, ROUND(AVG(급여), 0) AS 평균급여, MAX(급여) AS 최대급여, MIN(급여) AS 최소급여 FROM 사원 GROUP BY 부서코드;
2.4.2 GROUP BY
-- GROUP BY에 나열되지 않은 칼럼은 SELECT에서 집계 함수 없이 사용 불가! -- 에러: SELECT 부서코드, 사원명, SUM(급여) FROM 사원 GROUP BY 부서코드; -- 사원명이 GROUP BY에 없으므로 에러 -- 올바른 사용: SELECT 부서코드, COUNT(*), SUM(급여) FROM 사원 GROUP BY 부서코드;
2.4.3 HAVING
-- GROUP BY 결과에 대한 조건 (집계 함수 조건) SELECT 부서코드, AVG(급여) AS 평균급여 FROM 사원 GROUP BY 부서코드 HAVING AVG(급여) >= 5000; -- WHERE vs HAVING -- WHERE: 그룹화 전에 행 필터링 (집계 함수 사용 불가) -- HAVING: 그룹화 후에 그룹 필터링 (집계 함수 사용 가능)
2.4.4 고급 그룹 함수
(1) ROLLUP
-- 소계와 총계를 자동 생성 SELECT 부서코드, 직급, SUM(급여) FROM 사원 GROUP BY ROLLUP(부서코드, 직급); -- 결과: -- D01, 과장, 5000 -- D01, 대리, 3000 -- D01, NULL, 8000 ← 부서코드별 소계 -- D02, 과장, 6000 -- D02, NULL, 6000 ← 부서코드별 소계 -- NULL, NULL, 14000 ← 전체 총계 -- ROLLUP은 인자의 순서에 따라 결과가 달라진다! -- ROLLUP(A, B) → A별 소계 + 전체 총계 -- ROLLUP(B, A) → B별 소계 + 전체 총계
(2) CUBE
-- 모든 조합의 소계를 생성 SELECT 부서코드, 직급, SUM(급여) FROM 사원 GROUP BY CUBE(부서코드, 직급); -- ROLLUP 결과 + 직급별 소계까지 모두 포함
(3) GROUPING SETS
-- 특정 그룹만 선택적으로 생성 SELECT 부서코드, 직급, SUM(급여) FROM 사원 GROUP BY GROUPING SETS(부서코드, 직급); -- 부서코드별 합계와 직급별 합계만 각각 생성 (교차 소계 없음)
(4) GROUPING 함수
-- 소계/총계 행인지 구분 (0: 실제 그룹, 1: 소계/총계) SELECT CASE GROUPING(부서코드) WHEN 1 THEN '전체' ELSE 부서코드 END AS 부서, CASE GROUPING(직급) WHEN 1 THEN '소계' ELSE 직급 END AS 직급, SUM(급여) FROM 사원 GROUP BY ROLLUP(부서코드, 직급);
2.5 윈도우 함수 (Window Function / 분석 함수)
2.5.1 윈도우 함수 기본 구조
함수명(인자) OVER ( [PARTITION BY 칼럼] -- 그룹 분할 [ORDER BY 칼럼 [ASC|DESC]] -- 정렬 [WINDOWING 절] -- 범위 지정 )
2.5.2 순위 함수
SELECT 사원명, 부서코드, 급여, RANK() OVER (ORDER BY 급여 DESC) AS "RANK", DENSE_RANK() OVER (ORDER BY 급여 DESC) AS "DENSE_RANK", ROW_NUMBER() OVER (ORDER BY 급여 DESC) AS "ROW_NUMBER" FROM 사원;
| 급여 | RANK | DENSE_RANK | ROW_NUMBER |
|---|---|---|---|
| 5000 | 1 | 1 | 1 |
| 5000 | 1 | 1 | 2 |
| 4000 | 3 | 2 | 3 |
| 3000 | 4 | 3 | 4 |
차이점:
- RANK: 동일 순위 → 다음 순위 건너뜀 (1,1,3,4)
- DENSE_RANK: 동일 순위 → 다음 순위 연속 (1,1,2,3)
- ROW_NUMBER: 동일 값이어도 고유 번호 (1,2,3,4)
2.5.3 PARTITION BY
-- 부서별 급여 순위 SELECT 사원명, 부서코드, 급여, RANK() OVER (PARTITION BY 부서코드 ORDER BY 급여 DESC) AS 부서내순위 FROM 사원;
2.5.4 집계 윈도우 함수
-- 누적 합계 SELECT 사원명, 급여, SUM(급여) OVER (ORDER BY 급여) AS 누적합계 FROM 사원; -- 부서별 누적 합계 SELECT 사원명, 부서코드, 급여, SUM(급여) OVER (PARTITION BY 부서코드 ORDER BY 급여) AS 부서별누적합계 FROM 사원; -- 전체 합계 대비 비율 SELECT 사원명, 급여, ROUND(급여 / SUM(급여) OVER () * 100, 2) AS 비율 FROM 사원;
2.5.5 행 순서 함수
| 함수 | 설명 |
|---|---|
| LAG(칼럼, n, default) | 현재 행에서 n행 이전 값 |
| LEAD(칼럼, n, default) | 현재 행에서 n행 이후 값 |
| FIRST_VALUE(칼럼) | 파티션 내 첫 번째 값 |
| LAST_VALUE(칼럼) | 파티션 내 마지막 값 |
SELECT 사원명, 입사일, 급여, LAG(급여, 1, 0) OVER (ORDER BY 입사일) AS 이전사원급여, LEAD(급여, 1, 0) OVER (ORDER BY 입사일) AS 다음사원급여 FROM 사원;
2.5.6 비율 함수
| 함수 | 설명 |
|---|---|
| RATIO_TO_REPORT(칼럼) | 전체 합 대비 비율 |
| PERCENT_RANK() | 백분위 순위 (0~1) |
| CUME_DIST() | 누적 분포 |
| NTILE(n) | n개 그룹으로 균등 분할 |
-- 4개 그룹으로 분할 (4분위) SELECT 사원명, 급여, NTILE(4) OVER (ORDER BY 급여 DESC) AS 분위 FROM 사원;
2.6 계층형 쿼리 (Oracle)
2.6.1 기본 구조
SELECT LEVEL, LPAD(' ', (LEVEL-1)*2) || 사원명 AS 조직도, 사원번호, 관리자번호 FROM 사원 START WITH 관리자번호 IS NULL -- 루트 노드 조건 CONNECT BY PRIOR 사원번호 = 관리자번호; -- 부모-자식 관계 정의
2.6.2 핵심 키워드
| 키워드 | 설명 |
|---|---|
| LEVEL | 현재 노드의 깊이 (루트 = 1) |
| START WITH | 루트 노드를 지정하는 조건 |
| CONNECT BY PRIOR | 부모-자식 관계를 정의 |
| PRIOR | 이전 행(부모)을 참조 |
| ORDER SIBLINGS BY | 같은 레벨 내에서 정렬 |
| CONNECT_BY_ROOT 칼럼 | 루트 노드의 해당 칼럼 값 |
| SYS_CONNECT_BY_PATH(칼럼, 구분자) | 루트에서 현재까지의 경로 |
| CONNECT_BY_ISLEAF | 리프 노드 여부 (0/1) |
-- 순방향 전개 (부모 → 자식) CONNECT BY PRIOR 사원번호 = 관리자번호 -- 역방향 전개 (자식 → 부모) CONNECT BY PRIOR 관리자번호 = 사원번호 -- 경로 표시 SELECT SYS_CONNECT_BY_PATH(사원명, ' > ') AS 경로 FROM 사원 START WITH 관리자번호 IS NULL CONNECT BY PRIOR 사원번호 = 관리자번호; -- 결과: ' > 대표이사 > 영업이사 > 영업팀장'
2.7 DCL (Data Control Language)
2.7.1 GRANT (권한 부여)
-- 시스템 권한 부여 GRANT CREATE SESSION TO user1; GRANT CREATE TABLE TO user1; -- 객체 권한 부여 GRANT SELECT ON 사원 TO user1; GRANT INSERT, UPDATE ON 사원 TO user1; -- WITH GRANT OPTION: 받은 권한을 다른 사용자에게 부여 가능 GRANT SELECT ON 사원 TO user1 WITH GRANT OPTION;
2.7.2 REVOKE (권한 회수)
REVOKE SELECT ON 사원 FROM user1; -- WITH GRANT OPTION으로 부여한 경우: -- user1의 권한 회수 시 user1이 다른 사용자에게 부여한 권한도 함께 회수됨 (CASCADE)
2.7.3 ROLE
-- 롤 생성 및 권한 부여 CREATE ROLE 영업팀_롤; GRANT SELECT ON 고객 TO 영업팀_롤; GRANT SELECT ON 주문 TO 영업팀_롤; -- 사용자에게 롤 부여 GRANT 영업팀_롤 TO user1;
2.8 TCL (Transaction Control Language)
2.8.1 트랜잭션의 특성 (ACID)
| 특성 | 설명 |
|---|---|
| 원자성 (Atomicity) | 트랜잭션은 전부 실행되거나 전부 취소되어야 함 (All or Nothing) |
| 일관성 (Consistency) | 트랜잭션 실행 전후로 데이터베이스 상태가 일관되어야 함 |
| 고립성 (Isolation) | 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 함 |
| 지속성 (Durability) | 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야 함 |
2.8.2 COMMIT, ROLLBACK, SAVEPOINT
-- COMMIT: 변경사항을 확정 INSERT INTO 사원 VALUES (1001, '홍길동', ...); UPDATE 사원 SET 급여 = 5000 WHERE 사원번호 = 1002; COMMIT; -- ROLLBACK: 변경사항을 취소 DELETE FROM 사원 WHERE 부서코드 = 'D01'; ROLLBACK; -- DELETE가 취소됨 -- SAVEPOINT: 중간 저장점 INSERT INTO 사원 VALUES (1003, '이순신', ...); SAVEPOINT SP1; UPDATE 사원 SET 급여 = 3000 WHERE 사원번호 = 1003; SAVEPOINT SP2; DELETE FROM 사원 WHERE 사원번호 = 1001; ROLLBACK TO SP2; -- DELETE만 취소 ROLLBACK TO SP1; -- UPDATE까지 취소 COMMIT; -- INSERT만 확정
시험 핵심:
- DDL 문(CREATE, ALTER, DROP, TRUNCATE, RENAME) 실행 시 AUTO COMMIT 발생
- 정상적으로 세션 종료 시 AUTO COMMIT
- 비정상 종료 시 AUTO ROLLBACK
- SAVEPOINT 이후의 ROLLBACK TO는 해당 지점까지만 롤백
2.9 뷰 (VIEW)
-- 뷰 생성 CREATE OR REPLACE VIEW V_사원_급여 AS SELECT 사원번호, 사원명, 급여, 부서코드 FROM 사원 WHERE 급여 >= 3000; -- 뷰 사용 SELECT * FROM V_사원_급여; -- 뷰 삭제 DROP VIEW V_사원_급여;
뷰의 장점: 독립성(테이블 구조 변경 시 뷰만 수정), 편의성(복잡한 쿼리를 간단하게), 보안성(특정 칼럼만 노출)