MariaDB / Query / GROUP BY

MariaDB의 GROUP BY는 같은 기준값을 가진 행을 묶어 그룹마다 하나의 결과 행을 만듭니다. COUNT·SUM·AVG 같은 집계 함수와 함께 쓰면 부서별 건수나 고객별 합계 등을 구할 수 있습니다. WHERE는 묶기 전의 개별 행을, HAVING은 묶은 뒤의 집계 결과를 걸러냅니다.

집계 기준과 실습 데이터 정하기

무엇을 한 그룹으로 볼지 먼저 정합니다. 팀별 결과가 필요하면 team, 팀과 월의 조합별 결과가 필요하면 team과 sales_month를 함께 묶습니다. 집계는 원본 행을 수정하거나 중복 데이터를 삭제하는 작업이 아니라 SELECT 결과를 만드는 작업입니다.

MariaDB 서버와 SQL 클라이언트, 접근 가능한 learning 실습 DB가 필요합니다. 다음 SQL을 한 번 실행한 뒤 같은 데이터베이스에서 뒤의 조회문을 실행하세요. 기존 자료와 겹치면 테이블을 덮어쓰지 말고 별도 실습 DB를 사용합니다. amount는 원리를 확인하는 가상의 수치이며 NULL은 아직 알 수 없는 값이라는 의미로 넣었습니다.

USE learning;
CREATE TABLE sales_group_demo (
    id INT PRIMARY KEY,
    team VARCHAR(10) NULL,
    sales_month CHAR(7) NOT NULL,
    amount DECIMAL(10,2) NULL
);
INSERT INTO sales_group_demo VALUES
    (1, 'A', '2025-01', 100),
    (2, 'A', '2025-01', 200),
    (3, 'A', '2025-02', 50),
    (4, 'B', '2025-01', 400),
    (5, 'B', '2025-02', NULL),
    (6, 'C', '2025-01', NULL);

총 6행이고 A팀은 3행, B팀은 2행, C팀은 1행입니다. 숫자 0과 NULL은 다릅니다. 월은 예제를 단순하게 하기 위해 YYYY-MM 문자열로 저장했으며, 실제 날짜 계산이 필요하면 날짜 자료형과 업무 기준에 맞게 설계해야 합니다.

그룹별 건수·합계·평균 구하기

SELECT team, COUNT(*) AS row_count, COUNT(amount) AS known_count,
       SUM(amount) AS total_amount, ROUND(AVG(amount), 2) AS avg_amount
FROM sales_group_demo
GROUP BY team
ORDER BY team;
team row_count known_count total_amount avg_amount
A 3 3 350.00 116.67
B 2 1 400.00 400.00
C 1 0 NULL NULL

COUNT(*)는 그룹의 모든 행을 세고 COUNT(amount)는 NULL이 아닌 amount만 셉니다. SUM과 AVG도 NULL을 제외하므로 B팀 평균은 400을 2로 나눈 200이 아닙니다. C팀처럼 모두 NULL이면 합계와 평균은 NULL입니다. 평균은 화면 비교를 위해 소수 둘째 자리로 반올림했습니다.

정의와 NULL 처리는 COUNT, SUM, AVG 문서를 참고하세요. 결과 순서를 명확히 하기 위해 ORDER BY도 지정했습니다. 집계 기준과 화면의 정렬 기준은 목적이 다를 수 있습니다.

WHERE와 HAVING의 차이 비교

먼저 금액이 100 이상인 개별 행만 집계해 보겠습니다. WHERE 조건은 그룹을 만들기 전에 적용되므로 A팀의 50과 NULL 금액 행은 계산에서 빠집니다.

SELECT team, COUNT(*) AS row_count, SUM(amount) AS total_amount
FROM sales_group_demo
WHERE amount >= 100
GROUP BY team
ORDER BY team;

A팀은 2행·합계 300.00, B팀은 1행·합계 400.00이고 C팀은 결과에서 사라집니다. 필터를 통과한 원본 행이 없어서 그룹 자체가 생기지 않은 것입니다. 여기에서 합계가 350 이상인 팀만 남기려면 집계 뒤에 HAVING을 적용합니다.

SELECT team, COUNT(*) AS row_count, SUM(amount) AS total_amount
FROM sales_group_demo
WHERE amount >= 100
GROUP BY team
HAVING SUM(amount) >= 350
ORDER BY total_amount DESC, team;

결과는 B팀 한 행입니다. 비교를 위해 HAVING의 350만 300으로 바꾸면 A팀도 나타납니다. 확인 뒤 원래 값으로 되돌릴 수 있습니다. 반대로 WHERE에 SUM(amount)를 넣어 개별 행 조건처럼 사용하면 집계 순서에 맞지 않습니다. SELECT의 절 구성은 MariaDB SELECT 문서를 참고하세요.

여러 열로 그룹을 나누기

SELECT team, sales_month, SUM(amount) AS total_amount
FROM sales_group_demo
GROUP BY team, sales_month
ORDER BY team, sales_month;

이 쿼리는 A·2025-01의 합계 300.00, A·2025-02의 50.00, B·2025-01의 400.00, B·2025-02의 NULL, C·2025-01의 NULL로 총 5행을 만듭니다. 두 열 값이 모두 같은 행끼리 묶이므로 팀별로만 묶은 3행과 결과 단위가 달라집니다.

문자열 그룹은 문자셋·콜레이션의 비교 규칙에 영향을 받습니다. 콜레이션은 문자열의 비교·정렬 규칙이며 대소문자나 뒤쪽 공백 등을 같게 취급할 수 있습니다. 화면에서 조금 달라 보이는 이름이 같은 그룹이 되는 경우에는 저장된 값과 비교 규칙을 함께 확인하세요. 기본 동작과 제한은 GROUP BY 문서에 설명되어 있습니다.

빈 결과와 그룹 밖의 열 선택 주의

SELECT team, COUNT(*) AS row_count
FROM sales_group_demo
WHERE amount > 1000
GROUP BY team;

SELECT COUNT(*) AS row_count, SUM(amount) AS total_amount
FROM sales_group_demo
WHERE amount > 1000;

첫 조회는 조건에 맞는 행이 없으므로 결과 행이 없습니다. 두 번째는 GROUP BY 없는 전체 집계여서 row_count 0과 total_amount NULL인 한 행을 반환합니다. 조회 결과가 한 행 있다는 사실과 집계할 원본 데이터가 있다는 사실을 구분해야 합니다.

SELECT에 team과 집계값 외에 id 같은 개별 행 값을 무작정 추가하지 마세요. 한 그룹 안에 id가 여러 개이므로 어느 id를 보여 줄지 명확하지 않습니다. ONLY_FULL_GROUP_BY SQL 모드에서는 이런 조회가 거절되며, 해당 모드가 꺼져 있어 쿼리가 실행되더라도 의미가 올바르다는 뜻은 아닙니다. SQL 모드 문서에서 동작을 확인할 수 있습니다.

필요한 개별 행을 고르려면 최소·최대값 자체가 필요한지, 합계와 함께 특정 원본 행까지 필요한지를 먼저 구분해야 합니다. 단순히 MIN(id)를 추가하면 최소 번호를 얻을 뿐 다른 열도 그 행의 값으로 함께 선택되는 것은 아닙니다. 또한 NULL인 그룹 키는 하나의 그룹으로 모일 수 있으므로, 알 수 없는 분류를 별도 상태로 표시할지도 정해야 합니다.

중복 없는 팀 이름만 필요하다면 DISTINCT처럼 목적에 맞는 조회를 검토할 수 있습니다. GROUP BY를 사용했다고 원본 데이터의 중복이 제거되거나 저장 내용이 바뀌는 것은 아닙니다. 통계 결과가 이상하면 그룹 기준, WHERE로 빠진 행, NULL 처리와 JOIN 때문에 늘어난 원본 행 수를 차례대로 확인하세요.