20260430THU (DAY 26)

sungsimdangmascot·2026년 4월 30일

집계 함수

집계 함수는 여러 행의 데이터를 하나의 값으로 요약해주는 함수다. 예를 들어 선수 477명의 키가 각각 한 행씩 저장되어 있을 때, 그 477개의 값을 하나의 평균값으로 만들어주는 것이 집계 함수의 역할이다.

자주 쓰이는 집계 함수는 아래와 같다.

함수의미
AVG(열)평균값
SUM(열)합계
COUNT(열)행의 개수
MAX(열)최댓값
MIN(열)최솟값
SELECT AVG(height) FROM player;   -- 평균 키
SELECT SUM(height) FROM player;   -- 키의 합계
SELECT COUNT(height) FROM player; -- 키가 입력된 행의 수 (447)

NULL과 COUNT

NULL은 "값이 없음"을 의미한다. 숫자 0과는 다르다. 0은 값이 있는 것이고, NULL은 아예 값 자체가 존재하지 않는 것이다.

집계 함수는 기본적으로 NULL을 무시하고 계산한다. 그래서 키가 입력되지 않은 선수가 30명 있다면 COUNT(height)는 447이 나오고, COUNT(*)는 477이 나온다.

SELECT COUNT(*)                  FROM player; -- 477 (전체 행, NULL 포함)
SELECT COUNT(height)             FROM player; -- 447 (NULL인 행 제외)
SELECT COUNT(IFNULL(height, 0))  FROM player; -- 477 (NULL을 0으로 바꾼 뒤 카운트)

IFNULL(height, 0)은 height가 NULL이면 0으로 바꿔준다. 0은 실제 값이므로 COUNT에 포함되어 477이 나온다.


GROUP BY

GROUP BY는 지정한 열의 값이 같은 행끼리 묶는 기능이다. 예를 들어 선수 테이블에 FW, MF, DF, GK 선수들이 뒤섞여 있을 때, GROUP BY position을 쓰면 같은 포지션인 선수들끼리 하나의 그룹으로 묶인다. 결과적으로 포지션 종류만큼의 그룹이 생긴다.

이렇게 그룹이 만들어지면 각 그룹에 집계 함수를 적용할 수 있다. "포지션별 평균 키"처럼 그룹 단위의 계산이 가능해진다.

-- 포지션 종류 조회 방법 1 - DISTINCT 사용
SELECT DISTINCT position FROM player;

-- 포지션 종류 조회 방법 2 - GROUP BY 사용
SELECT position FROM player GROUP BY position;

GROUP BY를 쓸 때 SELECT에 올 수 있는 것

GROUP BY를 사용하면 표 전체가 몇 개의 그룹으로 압축된다. FW, MF, DF, GK 4개의 포지션이 있다면 결과는 4개의 행이 된다. 이 상태에서 SELECT *를 쓰면 문제가 생긴다. 각 그룹 안에는 여러 선수가 있는데, 4개의 행에 모든 선수 정보를 담을 수 없기 때문이다.

따라서 GROUP BY를 사용할 때 SELECT에는 두 가지만 쓸 수 있다.

  • 그룹화 기준이 된 열 (여기서는 position)
  • 집계 함수 (AVG, COUNT, SUM 등) — 그룹을 하나의 숫자로 요약한 값이므로 문제가 없다
SELECT *        FROM player GROUP BY position; -- 오류
SELECT position FROM player GROUP BY position; -- 정상
SELECT position, AVG(height) FROM player GROUP BY position; -- 정상

HAVING

HAVING은 GROUP BY로 만들어진 그룹에 조건을 거는 절이다. WHERE와 역할이 비슷해 보이지만 대상이 다르다.

  • WHERE는 그룹을 만들기 전에 개별 행을 걸러낸다.
  • HAVING은 그룹을 만든 후에 그룹 단위로 걸러낸다.

WHERE절에는 집계 함수를 쓸 수 없다. 아직 그룹이 만들어지지 않은 시점이기 때문이다. 그룹에 대한 조건은 반드시 HAVING에 써야 한다.

SELECT position, AVG(height)
FROM player
WHERE weight >= 80        -- 행 조건: 몸무게 80 이상인 선수만 남김
GROUP BY position          -- 포지션별로 그룹화
HAVING AVG(height) > 180; -- 그룹 조건: 평균 키가 180 초과인 그룹만 남김

실행 순서는 아래와 같다.

FROM - WHERE(행 필터링) - GROUP BY(그룹 생성) - HAVING(그룹 필터링) - SELECT - ORDER BY


그룹 연산 실습 - 문제 1번

문제: player_id가 2007로 시작하는 선수들 중 포지션별 평균 키를 조회하라.

SELECT position, AVG(height)
FROM player
WHERE player_id LIKE '2007%'
GROUP BY position;

LIKE는 문자열의 패턴을 비교하는 연산자다. =은 값이 정확히 일치해야 하지만, LIKE는 패턴이 맞으면 된다. %는 와일드카드로, 그 자리에 어떤 문자가 몇 개가 오든 상관없다는 의미다. '2007%'는 2007로 시작하는 모든 문자열을 의미한다.

WHERE로 2007로 시작하는 선수만 걸러낸 뒤, 그 선수들 내에서 포지션별로 묶어 평균 키를 계산한다.


그룹 연산 실습 - 문제 2번

문제: 포지션이 DF인 선수들의 평균 키를 팀별로 조회하고 오름차순 정렬하라.

SELECT team_id, AVG(height)
FROM player
WHERE position = 'DF'
GROUP BY team_id
ORDER BY AVG(height);

=은 값이 정확히 일치하는 행만 골라낸다. 패턴 비교가 필요 없으므로 LIKE 대신 =을 사용한다.

ORDER BY의 기본값은 오름차순(ASC)으로, 작은 값부터 큰 값 순서로 출력된다. 내림차순으로 바꾸려면 ORDER BY AVG(height) DESC로 작성한다.


문자 내장 함수

내장 함수는 SQL이 기본으로 제공하는 함수들이다. 문자열을 다루거나, 숫자를 계산하거나, 날짜를 처리하는 등 자주 필요한 기능들이 미리 만들어져 있다.

SUBSTRING — 문자열 자르기

문자열의 특정 위치부터 원하는 만큼 잘라내는 함수다. 위치는 1번부터 시작한다.

SELECT SUBSTRING('안녕하세요', 3);      -- '하세요' (3번째 글자부터 끝까지)
SELECT SUBSTRING('안녕하세요', 1, 2);   -- '안녕' (1번째 글자부터 2개)

형식은 SUBSTRING(문자열, 시작위치) 또는 SUBSTRING(문자열, 시작위치, 개수)다.

CHAR_LENGTH — 글자 수 세기

문자열이 몇 글자인지 반환하는 함수다. 공백도 한 글자로 카운트한다.

SELECT CHAR_LENGTH('안녕하세요'); -- 5
SELECT CHAR_LENGTH('나의 집');    -- 4 (공백 포함)

CONCAT — 문자열 이어붙이기

여러 문자열을 하나로 합쳐주는 함수다. 인수를 원하는 만큼 넣을 수 있고, 순서대로 연결된다.

SELECT CONCAT('a', 'b'); -- 'ab'

SELECT first_name, last_name,
       CONCAT(first_name, ' ', last_name) AS 풀네임
FROM employees;

이름과 성 사이에 공백 ' '을 넣어 자연스러운 풀네임을 만들 수 있다.


날짜 함수

날짜 함수는 날짜와 시간 데이터를 다루는 함수들이다. 데이터베이스에서 날짜는 단순한 문자열이 아니라 날짜 타입으로 저장되기 때문에, 연도나 월만 따로 꺼내거나 원하는 형식으로 바꾸려면 전용 함수가 필요하다.

SELECT NOW(),
   YEAR(NOW())              AS 년도,
   MONTH(NOW())             AS,
   DAY(NOW())               AS,
   DATE_FORMAT(NOW(), '%Y%m%d'),
   DATE_FORMAT(NOW(), '%Y-%m-%d');

NOW()는 현재 날짜와 시간을 반환한다. 예: 2025-04-30 14:23:11

YEAR(), MONTH(), DAY()는 날짜 값에서 연도, 월, 일을 숫자로 추출한다. 입사 연도별로 그룹화하거나 특정 연도만 필터링할 때 활용한다.

DATE_FORMAT(날짜, 형식)은 날짜를 원하는 문자 형식으로 출력한다. 형식에서 쓰이는 기호는 아래와 같다.

기호의미예시
%Y4자리 연도2025
%m2자리 월04
%d2자리 일30

'%Y%m%d'20250430, '%Y-%m-%d'2025-04-30으로 출력된다.


내장함수 실습 - 문제 1번

문제: last_name이 4글자 이하인 사원의 사원번호, 이름, 성을 조회하라.

SELECT employee_id, first_name, last_name
FROM employees
WHERE CHAR_LENGTH(last_name) <= 4;

CHAR_LENGTH로 성의 글자 수를 구하고, 4 이하인 행만 걸러낸다. King(4글자)은 통과하고, Johnson(7글자)은 탈락한다.


내장함수 실습 - 문제 2번

문제: 입사 연도별 평균 급여를 소수점 두 자리까지 반올림하여 입사 연도 오름차순으로 조회하라.

SELECT YEAR(hire_date) AS "입사 연도",
       ROUND(AVG(salary), 2) AS "평균 급여"
FROM employees
GROUP BY YEAR(hire_date)
ORDER BY "입사 연도";

ROUND(숫자, 자릿수)는 지정한 소수점 자리까지 반올림한다. ROUND(5432.7891, 2)의 결과는 5432.79다. 자릿수를 0으로 쓰면 정수로 반올림된다.

한글이나 띄어쓰기가 포함된 별칭은 큰따옴표로 감싸야 인식된다. ORDER BY에서도 SELECT에서 정의한 별칭을 그대로 사용할 수 있다.


서브쿼리

서브쿼리는 SQL 문장 안에 또 다른 SQL 문장을 넣는 것이다. 괄호로 감싸서 작성하며, 바깥쪽을 메인 쿼리, 안쪽을 서브쿼리라고 한다.

서브쿼리가 필요한 이유는 두 가지다.

첫째, 어떤 값을 직접 알 수 없고 먼저 조회해야 알 수 있을 때 사용한다. 정남일 선수의 팀 ID를 모르는 상태에서 그 팀의 선수를 조회하려면 팀 ID를 먼저 알아야 한다.

둘째, WHERE절에 집계 함수를 직접 쓸 수 없기 때문에 사용한다. WHERE height < AVG(height)는 오류가 발생한다. WHERE는 행을 한 줄씩 처리하는 시점에 실행되는데, 이 시점에는 아직 전체 평균이 계산되지 않았기 때문이다. 서브쿼리로 평균을 먼저 계산한 뒤 그 결과를 WHERE에 전달하면 문제가 해결된다.

서브쿼리는 항상 메인 쿼리보다 먼저 실행된다.

예시 1 - 정남일 선수가 소속된 팀의 선수 조회

SELECT player_name, team_id
FROM player
WHERE team_id = (
   SELECT team_id          -- 먼저 실행: 정남일의 팀 ID 조회 → K07
   FROM player
   WHERE player_name = '정남일'
);

서브쿼리가 K07을 반환하면, 메인 쿼리는 WHERE team_id = 'K07'로 동작한다.

예시 2 - 평균 키보다 작은 선수 조회

-- 오류 발생
SELECT player_name, height
FROM player
WHERE height < AVG(height); -- WHERE절에 집계함수 사용 불가

-- 서브쿼리로 해결
SELECT player_name, height
FROM player
WHERE height < (
   SELECT AVG(height)       -- 먼저 실행: 전체 평균 키 계산 → 179.31
   FROM player
);

ANY와 ALL

ANY와 ALL은 서브쿼리 결과가 여러 행일 때, 그 값들과 비교하는 방식을 정하는 연산자다.

ANY — 하나라도 만족하면 참

서브쿼리가 반환한 값 중 하나라도 조건을 만족하면 결과에 포함된다. = ANYIN과 동일하게 동작한다. > ANY는 서브쿼리 결과 중 가장 작은 값보다만 크면 조건을 만족한다.

WHERE salary = ANY(10000, 11000, 12000);
-- salary = 10000 OR salary = 11000 OR salary = 12000 과 같다
-- IN (10000, 11000, 12000)으로도 동일하게 표현 가능

ALL — 모두 만족해야 참

서브쿼리가 반환한 모든 값에 대해 조건을 만족해야 결과에 포함된다. > ALL은 서브쿼리 결과 중 가장 큰 값보다 커야 조건을 만족한다.

WHERE salary > ALL(10000, 11000, 12000);
-- salary > 10000 AND salary > 11000 AND salary > 12000 과 같다
-- 결국 12000보다 큰 값만 통과

서브쿼리 + IN — 경기장 조회

서브쿼리 결과가 여러 행일 때는 = 대신 IN을 사용한다. =은 오른쪽에 값이 하나일 때만 사용할 수 있다. 서브쿼리가 여러 행을 반환하는데 =을 쓰면 오류가 발생한다.

문제: 2012년 5월 1~2일 사이에 경기가 있는 경기장의 전체 정보를 조회하라.

SELECT *
FROM stadium
WHERE stadium_id IN (
   SELECT stadium_id                          -- 여러 행 반환 가능
   FROM schedule
   WHERE sche_date BETWEEN '20120501' AND '20120502'
);

BETWEEN A AND B는 A 이상 B 이하의 값을 골라내는 연산자다. 서브쿼리가 먼저 해당 날짜의 경기장 ID 목록을 반환하고, 메인 쿼리는 그 목록에 포함된 경기장 정보를 조회한다.


서브쿼리 + ANY — 키 비교

문제: K07팀 공격수(FW) 중 누구보다도 키가 큰 선수를 조회하라.

SELECT player_name, height, team_id
FROM player
WHERE height > ANY (
   SELECT height
   FROM player
   WHERE team_id = 'K07' AND position = 'FW'
)
ORDER BY height;

> ANY는 서브쿼리 결과 중 하나라도 조건을 만족하면 참이다. 즉 K07팀 FW 선수들의 키 목록 중 가장 작은 값보다만 크면 결과에 포함된다. 다시 말해 "K07팀 FW 중 가장 작은 선수보다 키가 큰 선수"를 조회하는 것과 같다.


서브쿼리 + ALL — 최대 키 비교

문제: K02팀 모든 선수보다 키가 큰 선수를 조회하라.

SELECT player_name, height, team_id
FROM player
WHERE height > ALL (
   SELECT height
   FROM player
   WHERE team_id = 'K02' AND height IS NOT NULL
)
ORDER BY height;

> ALL은 서브쿼리 결과 모두에 대해 조건을 만족해야 참이다. K02팀 선수들의 키 전체보다 커야 하므로, 결국 K02팀 최대 키인 192cm보다 큰 선수만 결과에 포함된다.

height IS NOT NULL 조건을 추가한 이유는 NULL 때문이다. NULL이 비교 대상에 포함되면 ALL 비교 결과가 NULL이 되어 조건을 만족하는 행이 없어질 수 있다. NULL이 있는 행을 미리 제외해야 비교가 정상적으로 작동한다.


핵심 요약

개념설명
COUNT(*)NULL 포함 전체 행 수를 센다
COUNT(열)NULL을 제외하고 행 수를 센다
IFNULL(열, 값)NULL을 지정한 값으로 바꾼다
GROUP BY지정한 열의 값이 같은 행끼리 묶는다
HAVINGGROUP BY로 만들어진 그룹에 조건을 건다
WHERE그룹 생성 전, 개별 행에 조건을 건다
SUBSTRING(문자열, 시작, 개수)문자열의 일부를 잘라낸다
CONCAT(a, b)두 문자열을 이어붙인다
서브쿼리먼저 실행되어 메인 쿼리에 값을 전달한다
IN (서브쿼리)서브쿼리 결과가 여러 행일 때 사용한다
> ANY (서브쿼리)서브쿼리 결과 중 최솟값보다 크면 참이다
> ALL (서브쿼리)서브쿼리 결과 중 최댓값보다 크면 참이다

집계 함수

집계 함수는 여러 행의 데이터를 하나의 값으로 요약해주는 함수다. 선수 477명의 키가 각각 한 행씩 저장되어 있을 때, 그 477개의 값을 하나의 평균값으로 만들어주는 것이 집계 함수의 역할이다.

함수의미
AVG(열)평균값
SUM(열)합계
COUNT(열)행의 개수
MAX(열)최댓값
MIN(열)최솟값
SELECT AVG(height) FROM player;
SELECT SUM(height) FROM player;
SELECT COUNT(height) FROM player; -- 447

NULL과 COUNT

NULL은 "값이 없음"을 의미한다. 숫자 0과는 다르다. 0은 값이 존재하는 것이고, NULL은 값 자체가 아예 없는 것이다. 집계 함수는 기본적으로 NULL을 무시하고 계산한다.

SELECT COUNT(*)                  FROM player; -- 477 (전체 행, NULL 포함)
SELECT COUNT(height)             FROM player; -- 447 (NULL인 행 제외)
SELECT COUNT(IFNULL(height, 0))  FROM player; -- 477 (NULL을 0으로 바꾼 뒤 카운트)

IFNULL(height, 0)은 height가 NULL이면 0으로 바꿔준다. 0은 실제 값이므로 COUNT에 포함되어 477이 나온다.


GROUP BY

GROUP BY는 지정한 열의 값이 같은 행끼리 묶는 기능이다. 선수 테이블에 FW, MF, DF, GK 선수들이 뒤섞여 있을 때, GROUP BY position을 쓰면 같은 포지션인 선수들끼리 하나의 그룹으로 묶인다. 결과적으로 포지션 종류만큼의 그룹이 생기고, 각 그룹에 집계 함수를 적용할 수 있다.

-- 포지션 종류 조회 방법 1 - DISTINCT 사용
SELECT DISTINCT position FROM player;

-- 포지션 종류 조회 방법 2 - GROUP BY 사용
SELECT position FROM player GROUP BY position;

GROUP BY를 쓸 때 SELECT에 올 수 있는 것

GROUP BY를 사용하면 표 전체가 그룹 수만큼 압축된다. 포지션이 4종류라면 결과는 4개의 행이 된다. 이 상태에서 SELECT *를 쓰면 오류가 발생한다. 4개의 행에 모든 선수의 정보를 담을 수 없기 때문이다.

따라서 GROUP BY를 사용할 때 SELECT에는 두 가지만 쓸 수 있다.

  • 그룹화 기준이 된 열 (여기서는 position)
  • 집계 함수 (AVG, COUNT, SUM 등)
SELECT *        FROM player GROUP BY position; -- 오류
SELECT position FROM player GROUP BY position; -- 정상
SELECT position, AVG(height) FROM player GROUP BY position; -- 정상

HAVING

HAVING은 GROUP BY로 만들어진 그룹에 조건을 거는 절이다. WHERE와 역할이 비슷해 보이지만 대상이 다르다.

  • WHERE는 그룹을 만들기 전에 개별 행을 걸러낸다.
  • HAVING은 그룹을 만든 후에 그룹 단위로 걸러낸다.

WHERE절에는 집계 함수를 쓸 수 없다. 아직 그룹이 만들어지지 않은 시점이기 때문이다. 그룹에 대한 조건은 반드시 HAVING에 써야 한다.

SELECT position, AVG(height)
FROM player
WHERE weight >= 80        -- 행 조건: 몸무게 80 이상인 선수만 남김
GROUP BY position         -- 포지션별로 그룹 생성
HAVING AVG(height) > 180; -- 그룹 조건: 평균 키가 180 초과인 그룹만 남김

실행 순서는 아래와 같다.

FROM - WHERE - GROUP BY - HAVING - SELECT - ORDER BY


그룹 연산 실습 - 문제 1번

문제: player_id가 2007로 시작하는 선수들 중 포지션별 평균 키를 조회하라.

SELECT position, AVG(height)
FROM player
WHERE player_id LIKE '2007%'
GROUP BY position;

LIKE는 문자열의 패턴을 비교하는 연산자다. =은 값이 정확히 일치해야 하지만, LIKE는 패턴이 맞으면 된다. %는 와일드카드로, 그 자리에 어떤 문자가 몇 개가 오든 상관없다는 의미다. WHERE로 2007로 시작하는 선수만 걸러낸 뒤, 그 선수들 안에서 포지션별로 묶어 평균 키를 계산한다.


그룹 연산 실습 - 문제 2번

문제: 포지션이 DF인 선수들의 평균 키를 팀별로 조회하고 오름차순 정렬하라.

SELECT team_id, AVG(height)
FROM player
WHERE position = 'DF'
GROUP BY team_id
ORDER BY AVG(height);

=은 값이 정확히 일치하는 행만 골라낸다. 패턴 비교가 필요 없으므로 LIKE 대신 =을 사용한다. ORDER BY의 기본값은 오름차순(ASC)으로 작은 값부터 큰 값 순서로 출력된다. 내림차순으로 바꾸려면 ORDER BY AVG(height) DESC로 작성한다.


문자 내장 함수

내장 함수는 SQL이 기본으로 제공하는 함수들이다. 문자열을 다루거나, 숫자를 계산하거나, 날짜를 처리하는 등 자주 필요한 기능들이 미리 만들어져 있다.

SUBSTRING — 문자열 자르기

문자열의 특정 위치부터 원하는 만큼 잘라내는 함수다. 위치는 1번부터 시작한다.

SELECT SUBSTRING('안녕하세요', 3);    -- '하세요' (3번째 글자부터 끝까지)
SELECT SUBSTRING('안녕하세요', 1, 2); -- '안녕' (1번째 글자부터 2개)

형식은 SUBSTRING(문자열, 시작위치) 또는 SUBSTRING(문자열, 시작위치, 개수)다.

CHAR_LENGTH — 글자 수 세기

문자열이 몇 글자인지 반환하는 함수다. 공백도 한 글자로 카운트한다.

SELECT CHAR_LENGTH('안녕하세요'); -- 5
SELECT CHAR_LENGTH('나의 집');    -- 4 (공백 포함)

CONCAT — 문자열 이어붙이기

여러 문자열을 하나로 합쳐주는 함수다. 인수를 원하는 만큼 넣을 수 있고 순서대로 연결된다.

SELECT CONCAT('a', 'b'); -- 'ab'

SELECT first_name, last_name,
       CONCAT(first_name, ' ', last_name) AS 풀네임
FROM employees;

이름과 성 사이에 공백 ' '을 넣어 자연스러운 풀네임을 만들 수 있다.


날짜 함수

날짜 함수는 날짜와 시간 데이터를 다루는 함수들이다. 데이터베이스에서 날짜는 단순한 문자열이 아니라 날짜 타입으로 저장되기 때문에, 연도나 월만 따로 꺼내거나 원하는 형식으로 바꾸려면 전용 함수가 필요하다.

SELECT NOW(),
   YEAR(NOW())              AS 년도,
   MONTH(NOW())             AS,
   DAY(NOW())               AS,
   DATE_FORMAT(NOW(), '%Y%m%d'),
   DATE_FORMAT(NOW(), '%Y-%m-%d');

NOW()는 현재 날짜와 시간을 반환한다. 예: 2025-04-30 14:23:11

YEAR(), MONTH(), DAY()는 날짜 값에서 연도, 월, 일을 숫자로 추출한다. 입사 연도별로 그룹화하거나 특정 연도만 필터링할 때 활용한다.

DATE_FORMAT(날짜, 형식)은 날짜를 원하는 문자 형식으로 출력한다.

기호의미예시
%Y4자리 연도2025
%m2자리 월04
%d2자리 일30

'%Y%m%d'20250430, '%Y-%m-%d'2025-04-30으로 출력된다.


내장함수 실습 - 문제 1번

문제: last_name이 4글자 이하인 사원의 사원번호, 이름, 성을 조회하라.

SELECT employee_id, first_name, last_name
FROM employees
WHERE CHAR_LENGTH(last_name) <= 4;

CHAR_LENGTH로 성의 글자 수를 구하고 4 이하인 행만 걸러낸다. King(4글자)은 통과하고, Johnson(7글자)은 탈락한다.


내장함수 실습 - 문제 2번

문제: 입사 연도별 평균 급여를 소수점 두 자리까지 반올림하여 입사 연도 오름차순으로 조회하라.

SELECT YEAR(hire_date) AS "입사 연도",
       ROUND(AVG(salary), 2) AS "평균 급여"
FROM employees
GROUP BY YEAR(hire_date)
ORDER BY "입사 연도";

ROUND(숫자, 자릿수)는 지정한 소수점 자리까지 반올림한다. ROUND(5432.7891, 2)의 결과는 5432.79다. 한글이나 띄어쓰기가 포함된 별칭은 큰따옴표로 감싸야 하며, ORDER BY에서도 SELECT에서 정의한 별칭을 그대로 사용할 수 있다.


서브쿼리

서브쿼리는 SQL 문장 안에 또 다른 SQL 문장을 넣는 것이다. 괄호로 감싸서 작성하며, 바깥쪽을 메인 쿼리, 안쪽을 서브쿼리라고 한다. 서브쿼리는 항상 메인 쿼리보다 먼저 실행된다.

서브쿼리가 필요한 이유는 두 가지다.

첫째, 어떤 값을 직접 알 수 없고 먼저 조회해야 알 수 있을 때 사용한다. 정남일 선수의 팀 ID를 모르는 상태에서 그 팀의 선수를 조회하려면 팀 ID를 먼저 알아야 한다.

둘째, WHERE절에 집계 함수를 직접 쓸 수 없기 때문에 사용한다. WHERE는 행을 한 줄씩 처리하는 시점에 실행되는데, 이 시점에는 아직 전체 평균이 계산되지 않았기 때문에 WHERE height < AVG(height)는 오류가 발생한다. 서브쿼리로 평균을 먼저 계산한 뒤 그 결과를 WHERE에 전달하면 문제가 해결된다.

예시 1 - 정남일 선수가 소속된 팀의 선수 조회

-- 1단계: 정남일 선수의 팀 조회
SELECT team_id FROM player WHERE player_name = '정남일'; -- 결과: K07

-- 2단계: K07 소속 선수 조회
SELECT player_name, team_id FROM player WHERE team_id = 'K07';

-- 서브쿼리로 한 번에 처리
SELECT player_name, team_id
FROM player
WHERE team_id = (
   SELECT team_id          -- 먼저 실행: K07 반환
   FROM player
   WHERE player_name = '정남일'
);

서브쿼리가 K07을 반환하면, 메인 쿼리는 WHERE team_id = 'K07'로 동작한다.

예시 2 - 평균 키보다 작은 선수 조회

-- 오류 발생
SELECT player_name, height
FROM player
WHERE height < AVG(height); -- WHERE절에 집계함수 사용 불가

-- 서브쿼리로 해결
SELECT player_name, height
FROM player
WHERE height < (
   SELECT AVG(height)      -- 먼저 실행: 179.31 반환
   FROM player
);

ANY와 ALL

ANY와 ALL은 서브쿼리 결과가 여러 행일 때, 그 값들과 비교하는 방식을 정하는 연산자다.

ANY — 하나라도 만족하면 참

서브쿼리가 반환한 값 중 하나라도 조건을 만족하면 결과에 포함된다. = ANYIN과 동일하게 동작한다. > ANY는 서브쿼리 결과 중 가장 작은 값보다만 크면 조건을 만족한다.

WHERE salary = ANY(10000, 11000, 12000);
-- 아래와 동일한 의미
WHERE salary IN (10000, 11000, 12000);
-- 아래와도 동일한 의미
WHERE salary = 10000 OR salary = 11000 OR salary = 12000;

ALL — 모두 만족해야 참

서브쿼리가 반환한 모든 값에 대해 조건을 만족해야 결과에 포함된다. > ALL은 서브쿼리 결과 중 가장 큰 값보다 커야 조건을 만족한다.

WHERE salary > ALL(10000, 11000, 12000);
-- 아래와 동일한 의미
WHERE salary > 10000 AND salary > 11000 AND salary > 12000;
-- 결국 가장 큰 값인 12000보다 큰 값만 통과

서브쿼리 + IN — 경기장 조회

서브쿼리 결과가 여러 행일 때는 = 대신 IN을 사용한다. =은 오른쪽에 값이 하나일 때만 사용할 수 있다. 서브쿼리가 여러 행을 반환하는데 =을 쓰면 오류가 발생한다.

문제: 2012년 5월 1~2일 사이에 경기가 있는 경기장의 전체 정보를 조회하라.

SELECT *
FROM stadium
WHERE stadium_id IN (
   SELECT stadium_id
   FROM schedule
   WHERE sche_date BETWEEN '20120501' AND '20120502'
);

BETWEEN A AND B는 A 이상 B 이하의 값을 골라내는 연산자다. 서브쿼리가 먼저 해당 날짜의 경기장 ID 목록을 반환하고, 메인 쿼리는 그 목록에 포함된 경기장 정보를 조회한다.


서브쿼리 + ANY / ALL

문제 1: K07팀 공격수(FW) 중 누구보다도 키가 큰 선수를 조회하라.

SELECT player_name, height, team_id
FROM player
WHERE height > ANY (
   SELECT height
   FROM player
   WHERE team_id = 'K07' AND position = 'FW'
)
ORDER BY height;

> ANY는 서브쿼리 결과 중 가장 작은 값보다만 크면 조건을 만족한다. K07팀 FW 중 가장 키가 작은 선수보다만 크면 결과에 포함된다. 확인용으로 SELECT MIN(height)를 따로 조회하면 어떤 기준으로 필터링됐는지 검증할 수 있다.

문제 2: K02팀 모든 선수보다 키가 큰 선수를 조회하라.

SELECT player_name, height, team_id
FROM player
WHERE height > ALL (
   SELECT height
   FROM player
   WHERE team_id = 'K02' AND height IS NOT NULL
)
ORDER BY height;

> ALL은 서브쿼리 결과 전체보다 커야 조건을 만족한다. K02팀의 최대 키인 192cm보다 큰 선수만 결과에 포함된다. height IS NOT NULL 조건을 추가한 이유는, NULL이 비교 대상에 포함되면 ALL 비교 결과가 NULL이 되어 조건을 만족하는 행이 없어지기 때문이다.


서브쿼리 실습 1

문제 1: 지역명이 서울이거나 수원인 팀에 소속된 선수들의 이름과 포지션을 조회하라.

SELECT player_name, position, team_id
FROM player
WHERE team_id IN (
   SELECT team_id
   FROM team
   WHERE region_name IN ('서울', '수원')
);

team 테이블과 player 테이블이 분리되어 있다. 먼저 서울·수원에 있는 팀의 team_id를 구하고, 그 team_id에 해당하는 선수를 player 테이블에서 조회한다. 서브쿼리 결과가 여러 행이므로 IN을 사용한다.

문제 2: last_name이 Abel인 사원보다 급여가 높은 사원들의 성, 이름을 조회하라.

SELECT first_name, last_name, salary
FROM employees
WHERE salary > (
   SELECT salary
   FROM employees
   WHERE last_name = 'Abel'  -- 단일 행 반환
);

Abel 사원의 급여는 하나의 값(단일 행)이므로 =>처럼 단순 비교 연산자를 사용할 수 있다.

문제 3: K08팀의 모든 공격수보다 키가 큰 선수를 조회하라.

SELECT player_name, height
FROM player
WHERE height > ALL (
   SELECT height
   FROM player
   WHERE team_id = 'K08' AND position = 'FW'
);

K08팀 FW 중 가장 큰 키보다 더 큰 선수만 결과에 포함된다. SELECT MAX(height)로 확인하면 기준값을 검증할 수 있다.


다중 컬럼 서브쿼리

다중 컬럼 서브쿼리는 WHERE절에서 두 개 이상의 열을 동시에 비교하는 방식이다. 하나의 열만으로는 조건을 완전히 표현할 수 없을 때 사용한다.

예를 들어 "포지션별로 가장 키가 작은 선수"를 조회하려면 두 가지 조건이 동시에 맞아야 한다.

  • 포지션이 일치해야 한다.
  • 그 포지션의 최솟값과 키가 일치해야 한다.

이 두 조건을 각각 따로 비교하면 "FW 포지션이면서 전체 최솟값인 선수"처럼 잘못된 결과가 나올 수 있다. 다중 컬럼 서브쿼리를 사용하면 두 값을 짝으로 묶어 동시에 비교한다.

-- 포지션별 최솟값 조회 (서브쿼리로 사용할 결과)
SELECT position, MIN(height)
FROM player
WHERE position IS NOT NULL
GROUP BY position;

-- 다중 컬럼 서브쿼리로 포지션별 최소 키 선수 조회
SELECT position, player_name, height
FROM player
WHERE (position, height) IN (
   SELECT position, MIN(height)
   FROM player
   WHERE position IS NOT NULL
   GROUP BY position
);

(position, height) IN (...)은 position과 height가 짝으로 일치하는 행만 골라낸다. 예를 들어 서브쿼리 결과에 ('FW', 168)이 있다면, position이 FW이면서 동시에 height가 168인 행만 통과한다.

다중 컬럼 서브쿼리 실습: 각 팀별로 최소 키를 가진 선수의 팀 ID, 이름, 키를 조회하라. 포지션이 없는 선수는 제외한다.

SELECT team_id, player_name, height
FROM player
WHERE (team_id, height) IN (
   SELECT team_id, MIN(height)
   FROM player
   WHERE position IS NOT NULL
   GROUP BY team_id
)
ORDER BY team_id;

스칼라 서브쿼리

스칼라 서브쿼리는 SELECT절 안에 쓰는 서브쿼리다. 반드시 단 하나의 행, 단 하나의 열을 반환해야 한다. 결과가 여러 행이면 오류가 발생한다.

각 사원의 이름과 급여를 조회하면서, 전체 평균 급여도 함께 출력하고 싶을 때 사용한다. 전체 평균은 하나의 값이므로 스칼라 서브쿼리로 가져올 수 있다.

SELECT first_name,
       salary,
       (SELECT AVG(salary) FROM employees) AS "전체 평균 급여"
FROM employees;

FROM employees로 사원 수만큼 행이 나오는데, 스칼라 서브쿼리의 결과인 전체 평균 급여는 모든 행에 동일한 값으로 반복해서 출력된다.


인라인 뷰

인라인 뷰는 FROM절 안에 쓰는 서브쿼리다. 서브쿼리의 결과를 하나의 임시 테이블처럼 사용한다. 반드시 별칭을 붙여야 한다.

SELECT *
FROM (
   SELECT *
   FROM player
   WHERE team_id = 'K01'  -- 먼저 K01팀 선수만 추출
) AS sub
WHERE position = 'GK';    -- 그 중에서 GK만 조회

위 코드는 사실 아래처럼 한 번에 작성해도 동일한 결과가 나온다.

SELECT *
FROM player
WHERE team_id = 'K01' AND position = 'GK';

인라인 뷰는 서브쿼리 없이는 해결할 수 없는 복잡한 경우에 주로 사용한다. 단순한 경우에는 WHERE에 조건을 직접 쓰는 것이 더 효율적이다.


DML — 데이터 조작

DML은 Data Manipulation Language의 약자로, 테이블 안의 데이터를 추가, 수정, 삭제하는 명령어들이다.

테이블 생성 (DDL)

데이터를 넣기 전에 먼저 테이블을 만들어야 한다. 테이블을 만드는 것은 DDL(Data Definition Language)에 해당한다.

CREATE TABLE tbl_member(
   member_name VARCHAR(100),  -- 문자형 컬럼
   member_age  INT            -- 숫자형 컬럼
);

INSERT — 데이터 추가

테이블에 새로운 행을 추가한다.

-- 열 이름을 명시하는 방법 (권장)
INSERT INTO tbl_member(member_name, member_age)
VALUES('짱구', 5);

-- 열 이름을 생략하는 방법 (테이블의 열 순서대로 값을 입력해야 함)
INSERT INTO tbl_member
VALUES('김철수', 20);

열 이름을 명시하는 방법이 더 안전하다. 나중에 테이블 구조가 바뀌어도 오류가 발생하지 않는다.

UPDATE — 데이터 수정

기존 행의 값을 바꾼다.

-- WHERE 없이 쓰면 모든 행이 수정된다 (주의)
UPDATE tbl_member
SET member_name = '유리';

-- WHERE로 수정할 행을 지정해야 한다
UPDATE tbl_member
SET member_name = '맹구'
WHERE member_age = 20;

WHERE 없이 UPDATE를 실행하면 테이블의 모든 행이 수정된다. 반드시 WHERE로 대상을 지정해야 한다.

DELETE — 데이터 삭제

특정 행 또는 모든 행을 삭제한다.

-- 특정 행 삭제
DELETE FROM tbl_member
WHERE member_name = '유리';

-- 모든 행 삭제 (테이블 구조는 남음)
DELETE FROM tbl_member;

-- 모든 행 삭제 (DELETE보다 빠름, 복구 불가)
TRUNCATE TABLE tbl_member;

DELETETRUNCATE의 차이는 아래와 같다.

구분DELETETRUNCATE
속도느림 (행 단위로 처리)빠름 (전체를 한 번에 처리)
복구가능불가
WHERE사용 가능사용 불가

DDL — 테이블 구조 정의

DDL은 Data Definition Language의 약자로, 테이블 자체를 만들고 수정하고 삭제하는 명령어들이다.

-- 테이블 생성
CREATE TABLE tbl_user(
   name     VARCHAR(100),
   user_age INT
);

-- 테이블명 수정
ALTER TABLE tbl_user RENAME TO user;

-- 컬럼 추가
ALTER TABLE user ADD gender CHAR(4);

-- 컬럼명 수정
ALTER TABLE user RENAME COLUMN user_age TO age;

-- 컬럼 타입 수정
ALTER TABLE user MODIFY gender VARCHAR(10);

-- 테이블 삭제
DROP TABLE user;

ALTER TABLE은 이미 만들어진 테이블의 구조를 변경할 때 사용한다. DROP TABLE은 테이블 자체를 삭제하며, 안에 있던 데이터도 함께 사라진다.


제약조건

제약조건은 테이블에 저장되는 데이터의 규칙을 정하는 것이다. 잘못된 데이터가 들어오는 것을 데이터베이스 수준에서 막아준다.

PRIMARY KEY (기본키)

기본키는 각 행을 유일하게 식별하는 열이다. 두 가지 규칙이 있다.

  • 중복된 값을 허용하지 않는다.
  • NULL을 허용하지 않는다.
CREATE TABLE product(
   number INT PRIMARY KEY,
   name   VARCHAR(100),
   price  INT
);

INSERT INTO product VALUES(1, '아이폰', 120000);     -- 정상
INSERT INTO product VALUES(1, '갤럭시 워치', 30000); -- 오류! 중복된 PK
INSERT INTO product VALUES(NULL, '갤럭시탭', 500000); -- 오류! NULL 불가

AUTO_INCREMENT

기본키에 AUTO_INCREMENT를 설정하면 데이터를 추가할 때 번호를 자동으로 1씩 증가시켜준다. 직접 번호를 입력하지 않아도 된다.

CREATE TABLE product(
   number INT PRIMARY KEY AUTO_INCREMENT,
   name   VARCHAR(100),
   price  INT
);

INSERT INTO product(name, price) VALUES('아이폰', 120000); -- number = 1 자동 부여
INSERT INTO product(name, price) VALUES('갤럭시', 120000); -- number = 2 자동 부여

FOREIGN KEY (외래키)

외래키는 다른 테이블의 기본키를 참조하는 열이다. 두 테이블 간의 관계를 연결하고, 존재하지 않는 값이 들어오는 것을 막아준다.

CREATE TABLE school(
   school_id   INT,
   school_name VARCHAR(100),
   CONSTRAINT pk_school PRIMARY KEY(school_id)
);

CREATE TABLE student(
   student_id   INT,
   student_name VARCHAR(100),
   student_age  INT,
   school_id    INT,  -- school 테이블의 school_id를 참조
   CONSTRAINT pk_student PRIMARY KEY(student_id),
   CONSTRAINT fk_student FOREIGN KEY(school_id)
      REFERENCES school(school_id)
      ON DELETE CASCADE
);

REFERENCES school(school_id)는 student 테이블의 school_id가 school 테이블의 school_id를 참조한다는 의미다. 존재하지 않는 school_id를 student에 넣으려 하면 오류가 발생한다.

INSERT INTO school VALUES(1, 'DBMS학교');
INSERT INTO school VALUES(2, 'Java학교');

INSERT INTO student VALUES(1, '김철수', 17, 1); -- 정상 (school_id 1 존재)
INSERT INTO student VALUES(2, '짱구',   15, 5); -- 오류! school_id 5 없음

ON DELETE CASCADE

ON DELETE CASCADE는 참조하는 테이블의 행이 삭제될 때, 그것을 참조하고 있는 행도 자동으로 함께 삭제되도록 하는 옵션이다. 예를 들어 school 테이블에서 학교를 삭제하면, 그 학교에 소속된 학생들도 자동으로 삭제된다. 이를 통해 참조 무결성을 유지한다.

참조 무결성이란 외래키로 연결된 두 테이블 사이에서, 참조 대상이 없는 데이터가 존재하지 않도록 보장하는 것이다. 학생이 존재하지 않는 학교를 참조하는 상황이 발생하지 않도록 막아준다.


핵심 요약

개념설명
COUNT(*)NULL 포함 전체 행 수를 센다
COUNT(열)NULL을 제외하고 행 수를 센다
IFNULL(열, 값)NULL을 지정한 값으로 바꾼다
GROUP BY지정한 열의 값이 같은 행끼리 묶는다
HAVINGGROUP BY로 만들어진 그룹에 조건을 건다
서브쿼리먼저 실행되어 메인 쿼리에 값을 전달한다
IN (서브쿼리)서브쿼리 결과가 여러 행일 때 사용한다
> ANY서브쿼리 결과 중 최솟값보다 크면 참이다
> ALL서브쿼리 결과 중 최댓값보다 크면 참이다
다중 컬럼 서브쿼리두 열을 짝으로 묶어 동시에 비교한다
스칼라 서브쿼리SELECT절에 쓰며 단일 행, 단일 열을 반환한다
인라인 뷰FROM절에 쓰며 결과를 임시 테이블처럼 사용한다
INSERT테이블에 새로운 행을 추가한다
UPDATE기존 행의 값을 수정한다. WHERE 없이 쓰면 전체가 수정된다
DELETE특정 행을 삭제한다. WHERE 없이 쓰면 전체가 삭제된다
TRUNCATE모든 행을 빠르게 삭제한다. 복구가 불가능하다
ALTER TABLE테이블 구조를 변경한다
PRIMARY KEY중복과 NULL을 허용하지 않는 고유 식별자다
FOREIGN KEY다른 테이블의 기본키를 참조하는 열이다
ON DELETE CASCADE참조 대상이 삭제되면 참조하는 행도 자동으로 삭제된다
profile
성심당마스코트

0개의 댓글