2025-08-20 22:32

SQL 그룹 함수 완벽 정복 핸드북 데이터 집계부터 분석까지 모든 것

데이터베이스를 다루다 보면, 단순히 데이터를 저장하고 조회하는 것을 넘어 데이터가 가진 숨은 의미를 파악해야 하는 순간이 옵니다. 쇼핑몰의 일일 총매출액은 얼마일까? 우리 회사에서 가장 높은 연봉을 받는 직원은 누구일까? 부서별 평균 연봉은 어떻게 다를까?

이러한 질문들은 데이터 하나하나를 살펴보는 것만으로는 답하기 어렵습니다. 수백만 건의 데이터를 의미 있는 그룹으로 묶고, 각 그룹의 특징을 요약하는 과정이 필요합니다. 바로 이 역할을 수행하는 것이 SQL 그룹 함수(Group Function), 또는 **집계 함수(Aggregate Function)**입니다. 이 핸드북은 SQL 그룹 함수의 개념부터 실전 활용까지, 당신이 데이터 분석의 첫걸음을 뗄 수 있도록 돕는 완벽한 가이드가 될 것입니다.

1. 왜 그룹 함수가 필요한가?: 데이터의 숲을 보는 지혜

거대한 숲에 들어갔다고 상상해봅시다. 숲을 이해하기 위해 모든 나무의 이름, 나이, 키를 하나하나 외우는 것은 비효율적입니다. 대신 우리는 “이 숲에는 소나무가 가장 많다”, “평균 나무 높이는 15미터 정도이다”, “가장 오래된 나무는 500살이다” 와 같이 숲 전체를 요약하고 특징을 파악하려고 합니다.

데이터베이스의 테이블이 ‘숲’이라면, 테이블의 각 행(Row)은 ‘나무’에 해당합니다. 그룹 함수는 이 개별적인 나무(데이터)들을 모아 숲(데이터 그룹) 전체의 특징을 알려주는 강력한 도구입니다.

  • 개별 데이터: 3월 1일 A 고객이 3,000원짜리 커피를 구매했다.

  • 그룹 데이터: 3월 한 달간 총매출액은 5,000만 원이다.

이처럼 그룹 함수는 방대한 양의 상세 데이터를 비즈니스 의사결정에 유용한 의미 있는 정보로 변환하는 핵심적인 역할을 수행합니다.

2. 그룹 함수의 구조: GROUP BYHAVING의 마법

그룹 함수 자체는 간단하지만, 그 힘은 GROUP BY 절과 HAVING 절을 만났을 때 비로소 완성됩니다. 이 세 가지 요소가 어떻게 상호작용하는지 이해하는 것이 그룹 함수 정복의 핵심입니다.

가. 그룹 함수의 기본 종류

가장 널리 사용되는 그룹 함수는 다음과 같습니다.

함수설명예시
COUNT()행(row)의 개수를 센다.COUNT(*): 전체 직원 수
SUM()숫자 데이터의 합계를 구한다.SUM(salary): 전 직원 연봉 총합
AVG()숫자 데이터의 평균을 구한다.AVG(salary): 전 직원 평균 연봉

나. 데이터를 묶는 기준: GROUP BY

만약 단순히 전 직원의 평균 연봉이 궁금하다면 AVG(salary)만 사용하면 됩니다. 하지만 “각 부서별 평균 연봉”이 궁금하다면 어떨까요? 이때 필요한 것이 바로 GROUP BY 입니다.

GROUP BY는 특정 컬럼(column)의 값이 같은 행들을 하나의 그룹으로 묶어주는 역할을 합니다.

GROUP BY가 없는 경우:

SELECT AVG(salary) FROM employees;

이 쿼리는 전체 직원을 하나의 거대한 그룹으로 보고 평균 연봉 하나만을 반환합니다.

GROUP BY가 있는 경우:

SELECT department, AVG(salary)
FROM employees
GROUP BY department;

이 쿼리는 department 컬럼 값이 같은 행들(인사팀, 개발팀, 마케팅팀 등)을 각각 별개의 그룹으로 묶습니다. 그리고 각 그룹별로 AVG(salary)를 계산하여 부서와 해당 부서의 평균 연봉을 함께 보여줍니다.

[중요 규칙] GROUP BY를 사용할 때, SELECT 절에는 다음 두 종류의 표현식만 올 수 있습니다.

  1. GROUP BY 절에 명시된 컬럼 (예: department)

  2. 그룹 함수 (예: AVG(salary), COUNT(*))

만약 GROUP BY department로 묶었는데 SELECT name 처럼 그룹 기준이 아닌 일반 컬럼을 조회하면, 어떤 name을 보여줘야 할지 데이터베이스가 알 수 없기 때문에 오류가 발생합니다. (개발팀 그룹에 속한 여러 직원의 이름 중 무엇을 대표로 보여줘야 할까요?)

다. 그룹을 필터링하는 기준: HAVING

WHERE 절이 개별 행을 필터링하는 조건이라면, HAVING 절은 GROUP BY로 만들어진 그룹 자체를 필터링하는 조건입니다.

비유하자면, WHERE는 식당에 들어오는 손님 개개인을 검사하는 것(예: “미성년자는 출입 금지”)이고, HAVING은 각 테이블(그룹)의 전체 주문 금액을 보고 조건을 거는 것(예: “총주문액이 5만 원 이상인 테이블만 이벤트 참여 가능”)과 같습니다.

예시: 평균 연봉이 6,000만 원 이상인 부서만 조회하기

SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING AVG(salary) >= 6000;

이 쿼리는 먼저 부서별로 그룹을 짓고 평균 연봉을 계산한 뒤(GROUP BY), 그 결과 그룹들 중에서 평균 연봉이 6,000 이상인 그룹만 남겨서 보여줍니다. 이 조건(AVG(salary) >= 6000)은 그룹이 만들어진 후에만 판단할 수 있으므로 WHERE 절에서는 사용할 수 없습니다.

라. SQL 쿼리 실행 순서

WHERE, GROUP BY, HAVING이 함께 쓰일 때 SQL의 내부적인 실행 순서를 이해하면 복잡한 쿼리를 작성하는 데 큰 도움이 됩니다.

  1. FROM: 어떤 테이블에서 데이터를 가져올지 결정합니다.

  2. WHERE: 개별 행에 대한 조건을 적용하여 대상 데이터를 필터링합니다.

  3. GROUP BY: WHERE 절을 통과한 행들을 지정된 컬럼 기준으로 그룹화합니다.

  4. HAVING: GROUP BY로 생성된 그룹에 대한 조건을 적용하여 그룹을 필터링합니다.

  5. SELECT: 최종적으로 남은 그룹들의 데이터를 조회합니다.

  6. ORDER BY: 결과를 정렬합니다.

3. 실전! 그룹 함수 사용법 마스터하기

간단한 employees 테이블을 예제로 각 그룹 함수를 실제로 어떻게 사용하는지 알아봅시다.

employees 테이블 예시

| id | name | department | salary | hire_date | | :— | :--- | :--- | :--- | :--- | | 101 | Alice | 개발팀 | 8000 | 2021-01-10 | | 102 | Bob | 개발팀 | 7500 | 2022-03-15 | | 103 | Charlie | 마케팅팀 | 5500 | 2020-05-20 | | 104 | David | 마케팅팀 | 6000 | 2021-08-01 | | 105 | Eve | 인사팀 | 5000 | 2023-02-11 | | 106 | Frank | 개발팀 | 9000 | 2019-11-30 |

가. COUNT(): 개수 세기

COUNT는 가장 기본적이면서도 활용도가 높은 함수입니다.

  • 전체 직원 수 세기
SELECT COUNT(*) FROM employees;
-- 결과: 6

COUNT(*)NULL` 값을 포함한 모든 행의 수를 계산합니다.

  • 부서별 직원 수 세기
SELECT department, COUNT(*) AS num_employees
FROM employees
GROUP BY department;
**결과:** | department | num_employees | | :--- | :--- | | 개발팀 | 3 | | 마케팅팀 | 2 | | 인사팀 | 1 |
  • 직원이 2명 이상인 부서만 조회하기

    SELECT department, COUNT(*)
    FROM employees
    GROUP BY department
    HAVING COUNT(*) >= 2;

    결과: | department | COUNT(*) | | :--- | :--- | | 개발팀 | 3 | | 마케팅팀 | 2 |

나. SUM() & AVG(): 합계와 평균

숫자 데이터를 요약하는 데 필수적인 함수입니다.

  • 전체 연봉 총액과 평균 연봉

    SELECT SUM(salary), AVG(salary) FROM employees;
    -- 결과: 41000, 6833.33
  • 부서별 연봉 총액과 평균 연봉

    SELECT department, SUM(salary), AVG(salary)
    FROM employees
    GROUP BY department;

    결과: | department | SUM(salary) | AVG(salary) | | :--- | :--- | :--- | | 개발팀 | 24500 | 8166.67 | | 마케팅팀 | 11500 | 5750.00 | | 인사팀 | 5000 | 5000.00 |

다. MAX() & MIN(): 최댓값과 최솟값

  • 가장 높은 연봉과 가장 낮은 연봉 조회

    SELECT MAX(salary), MIN(salary) FROM employees;
    -- 결과: 9000, 5000
  • 부서별 최고 연봉자와 최저 연봉 조회

    SELECT department, MAX(salary), MIN(salary)
    FROM employees
    GROUP BY department;

    결과: | department | MAX(salary) | MIN(salary) | | :--- | :--- | :--- | | 개발팀 | 9000 | 7500 | | 마케팅팀 | 6000 | 5500 | | 인사팀 | 5000 | 5000 |

4. 심화 학습: 그룹 함수의 숨겨진 기능들

기본적인 사용법에 익숙해졌다면, 이제 분석의 깊이를 더해줄 몇 가지 심화 기법을 알아볼 차례입니다.

가. 여러 컬럼으로 그룹화하기

GROUP BY 절에 여러 컬럼을 지정하여 더 세부적인 그룹을 만들 수 있습니다. 예를 들어, 부서별 그리고 입사 연도별 직원 수를 알고 싶다면 다음과 같이 쿼리를 작성할 수 있습니다.

SELECT
    department,
    EXTRACT(YEAR FROM hire_date) AS hire_year,
    COUNT(*)
FROM employees
GROUP BY department, EXTRACT(YEAR FROM hire_date)
ORDER BY department, hire_year;

이 쿼리는 먼저 부서로 그룹을 나누고, 그 안에서 다시 입사 연도별로 그룹을 나누어 각 하위 그룹의 직원 수를 계산합니다.

쿼리 설명

이 쿼리의 목적은 **“각 부서별로, 그리고 같은 부서 내에서는 입사 연도별로 직원이 몇 명인지”**를 파악하는 것입니다.

쿼리는 데이터베이스 내부적으로 다음과 같은 순서로 실행됩니다.

1단계: FROM employees
  • 가장 먼저, employees 테이블에서 모든 데이터를 가져옵니다. 아직 아무런 필터링이나 그룹화가 적용되지 않은 원본 데이터 상태입니다.
2단계: GROUP BY department, EXTRACT(YEAR FROM hire_date)
  • 이제 가져온 데이터를 그룹으로 묶습니다. GROUP BY에 두 개의 조건이 있으므로, 두 조건이 모두 동일한 데이터끼리 하나의 그룹으로 묶입니다.
    • 첫 번째 기준: department (부서)
    • 두 번째 기준: EXTRACT(YEAR FROM hire_date) (입사일에서 ‘연도’만 추출한 값)
  • 예를 들어, ‘개발팀’이면서 ‘2021년’에 입사한 직원들이 하나의 그룹이 되고, ‘마케팅팀’이면서 ‘2021년’에 입사한 직원들은 또 다른 그룹이 됩니다 employees 테이블 예시 데이터로 그룹을 만들면 다음과 같이 6개의 그룹이 생성됩니다.
  • 개발팀 - 2019년 (Frank)
  • 개발팀 - 2021년 (Alice)
  • 개발팀 - 2022년 (Bob)
  • 마케팅팀 - 2020년 (Charlie)
  • 마케팅팀 - 2021년 (David)
  • 인사팀 - 2023년 (Eve)
3단계: SELECT department, ..., COUNT(*)
  • 이제 각 그룹별로 어떤 정보를 보여줄지 결정합니다.
    • department: 해당 그룹의 부서 이름을 보여줍니다.
    • EXTRACT(YEAR FROM hire_date) AS hire_year: 해당 그룹의 입사 연도를 hire_year라는 별명으로 보여줍니다.
    • COUNT(*): 각 그룹에 속한 행(직원)의 개수를 셉니다. 예시 데이터에서는 모든 그룹에 직원이 1명씩 있으므로 COUNT(*)는 모두 1이 됩니다
4단계: ORDER BY department, hire_year
  • 마지막으로, 위에서 만들어진 결과 데이터를 정렬합니다.
    • 첫 번째 정렬 기준: department (부서 이름 가나다순)
    • 두 번째 정렬 기준: hire_year (입사 연도 오름차순)
  • 따라서 ‘개발팀’이 먼저 나오고, 개발팀 내에서는 입사 연도가 빠른 순서대로 정렬됩니다. 그 다음 ‘마케팅팀’, ‘인사팀’ 순으로 결과가 표시됩니다. 이 모든 단계를 거쳐, “부서별, 입사 연도별 직원 수”라는 의미 있는 통계 데이터를 얻게 되는 것입니다.

나. NULL 값 처리 방식

그룹 함수는 NULL 값을 다루는 방식이 조금씩 다릅니다.

  • COUNT(*): NULL을 포함한 모든 행의 수를 셉니다.

  • COUNT(컬럼명): 해당 컬럼의 값이 NULL이 아닌 행의 수만 셉니다.

  • SUM(), AVG(), MAX(), MIN(): 계산 과정에서 NULL 값을 자동으로 무시합니다.

예를 들어, 5명의 연봉 데이터가 (5000, 6000, 7000, 8000, NULL)일 때, AVG(salary)(5000+6000+7000+8000) / 4 를 계산합니다. NULL 값을 0으로 취급하여 5로 나누지 않는다는 점에 유의해야 합니다.

다. ROLLUPCUBE: 소계와 총계 자동 계산

GROUP BY의 확장 기능인 ROLLUPCUBE를 사용하면 소계와 총계를 더 쉽게 계산할 수 있습니다. (모든 DBMS에서 지원하지는 않을 수 있습니다.)

  • ROLLUP: 계층 구조의 소계를 계산합니다. GROUP BY ROLLUP(A, B)(A, B) 그룹별 집계, (A) 그룹별 소계, 그리고 전체 총계를 모두 보여줍니다.

  • CUBE: 가능한 모든 조합의 소계를 계산합니다. GROUP BY CUBE(A, B)(A, B), (A), (B) 그룹별 집계와 전체 총계를 모두 보여줍니다.

이 기능들은 복잡한 보고서나 다차원 분석을 수행할 때 매우 유용합니다.

5. 결론: 데이터를 요약하는 기술, 분석의 첫걸음

SQL 그룹 함수는 단순히 쿼리 문법의 일부가 아닙니다. 그것은 데이터를 바라보는 관점을 바꾸는 철학입니다. 개별 데이터의 홍수 속에서 길을 잃지 않고, 의미 있는 패턴과 인사이트를 발견하게 해주는 나침반과도 같습니다.

COUNT, SUM, AVG, MAX, MIN과 같은 기본 함수부터 GROUP BY를 통한 그룹화, HAVING을 통한 그룹 필터링까지, 오늘 배운 내용을 꾸준히 연습하고 실제 데이터에 적용해보세요. 처음에는 어색할 수 있지만, 익숙해질수록 당신은 데이터를 단순한 기록의 집합이 아닌, 살아있는 정보의 원천으로 다룰 수 있게 될 것입니다. 데이터 분석가로의 여정, 그 첫걸음을 성공적으로 내디딘 것을 축하합니다!

레퍼런스(References)

그룹 함수