Skip to main content
Summary

단순 조회에서 의미 있는 조회로

앞 장에서는 SELECT 문으로 테이블의 데이터를 가져오는 방법을 배웠습니다. 하지만 실제로 데이터베이스를 사용할 때는 단순히 모든 행을 보는 것보다 다음과 같은 질문에 답해야 할 때가 많습니다.

  • 점수가 높은 학생부터 보고 싶다.

  • 전체 주문 건수는 몇 건인가?

  • 상품별 매출 합계는 얼마인가?

  • 주문이 3건 이상인 고객만 보고 싶다.

이런 질문에 답하려면 조회 결과를 정렬하고, 여러 행을 요약하고, 기준별로 그룹화할 수 있어야 합니다.

이번 장에서는 ORDER BY, 집계 함수, GROUP BY, HAVING을 배웁니다. 이 네 가지를 익히면 단순 조회를 넘어 데이터를 분석하는 쿼리를 작성할 수 있습니다.

정렬, 집계, 그룹화로 데이터 읽기

예제 주문 테이블 준비하기

이번 장에서는 orders 테이블을 사용하겠습니다. 앞 장의 students 테이블과 별도로, 집계와 그룹화를 연습하기 좋은 주문 데이터를 만들겠습니다.

USE mysql_book;

DROP TABLE IF EXISTS orders;

CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  customer_name VARCHAR(50) NOT NULL,
  product_name VARCHAR(100) NOT NULL,
  quantity INT NOT NULL,
  price INT NOT NULL,
  status VARCHAR(20) NOT NULL,
  ordered_at DATETIME NOT NULL
);

샘플 데이터를 넣습니다.

INSERT INTO orders (customer_name, product_name, quantity, price, status, ordered_at)
VALUES
  ('김민수', '키보드', 1, 50000, 'paid', '2026-05-01 10:00:00'),
  ('이서연', '마우스', 2, 20000, 'paid', '2026-05-01 11:00:00'),
  ('김민수', '모니터', 1, 250000, 'paid', '2026-05-02 09:30:00'),
  ('박지훈', '키보드', 1, 50000, 'canceled', '2026-05-02 15:00:00'),
  ('이서연', '키보드', 3, 50000, 'paid', '2026-05-03 14:20:00');

quantity는 수량, price는 상품 1개의 가격이라고 생각하면 됩니다. 실제 서비스에서는 회원, 주문, 주문 상품, 상품 테이블을 나누는 것이 더 좋지만, 지금은 정렬과 집계를 배우기 위해 한 테이블로 단순화했습니다.


ORDER BY로 조회 결과 정렬하기

ORDER BY는 조회 결과의 순서를 정합니다.

기본 형태는 다음과 같습니다.

SELECT 컬럼명
FROM 테이블명
ORDER BY 정렬기준;

예를 들어 주문일이 빠른 순서대로 주문 목록을 보고 싶다면 다음과 같이 작성합니다.

SELECT id, customer_name, product_name, ordered_at
FROM orders
ORDER BY ordered_at;

ORDER BY ordered_at은 ordered_at 값을 기준으로 오름차순 정렬하라는 뜻입니다.

오름차순은 작은 값에서 큰 값으로 정렬합니다. 날짜라면 오래된 날짜에서 최근 날짜로, 숫자라면 작은 숫자에서 큰 숫자로 정렬됩니다.

명확하게 쓰고 싶다면 ASC를 붙일 수 있습니다.

SELECT id, customer_name, product_name, ordered_at
FROM orders
ORDER BY ordered_at ASC;

ASC는 ascending의 줄임말로, 오름차순을 뜻합니다. 생략하면 기본값은 ASC입니다.

내림차순 정렬하기

최근 주문부터 보고 싶다면 내림차순을 사용합니다.

SELECT id, customer_name, product_name, ordered_at
FROM orders
ORDER BY ordered_at DESC;

DESC는 descending의 줄임말로, 내림차순을 뜻합니다.

가격이 높은 상품부터 보고 싶을 때도 같은 방식입니다.

SELECT id, product_name, price
FROM orders
ORDER BY price DESC;

정렬은 데이터를 바꾸지 않습니다. 테이블에 저장된 순서를 변경하는 것이 아니라, 조회해서 보여 주는 결과의 순서만 바꿉니다.

여러 기준으로 정렬하기

실무에서는 하나의 기준만으로 정렬하기 어려울 때가 많습니다.

예를 들어 주문일이 빠른 순서로 정렬하되, 같은 날짜와 시간 안에서는 가격이 높은 상품부터 보고 싶다고 해 보겠습니다.

SELECT id, customer_name, product_name, price, ordered_at
FROM orders
ORDER BY ordered_at ASC, price DESC;

이 쿼리는 다음 순서로 정렬합니다.

  1. 먼저 ordered_at을 오름차순으로 정렬합니다.

  2. ordered_at이 같은 행끼리는 price를 내림차순으로 정렬합니다.

즉, ORDER BY에 여러 기준을 적으면 앞에 있는 기준이 우선 적용됩니다.

고객 이름순으로 정렬하고, 같은 고객 안에서는 최근 주문부터 보고 싶다면 다음과 같이 작성할 수 있습니다.

SELECT customer_name, product_name, ordered_at
FROM orders
ORDER BY customer_name ASC, ordered_at DESC;

정렬 기준은 결과를 읽기 쉽게 만드는 데 매우 중요합니다. 특히 관리자 화면, 통계 화면, 리포트 화면에서는 거의 항상 정렬이 필요합니다.


집계 함수로 여러 행을 하나의 값으로 요약하기

집계 함수는 여러 행의 값을 계산해서 하나의 결과로 요약합니다.

대표적인 집계 함수는 다음과 같습니다.

함수의미
COUNT()행의 개수를 센다
SUM()합계를 구한다
AVG()평균을 구한다
MIN()최솟값을 구한다
MAX()최댓값을 구한다

전체 주문 건수를 알고 싶다면 COUNT()를 사용합니다.

SELECT COUNT(*) AS total_orders
FROM orders;

COUNT(*)는 조건에 맞는 행의 개수를 셉니다. 여기서는 orders 테이블의 전체 행 수를 셉니다.

AS total_orders는 결과 컬럼에 별칭을 붙이는 문법입니다. 별칭을 사용하면 결과를 읽기 쉬워집니다.

결과는 다음과 같은 형태가 됩니다.

total_orders
5

COUNT(*)와 COUNT(컬럼명)의 차이

COUNT(*)와 COUNT(컬럼명)은 비슷해 보이지만 차이가 있습니다.

SELECT COUNT(*)
FROM orders;

COUNT(*)는 행 자체의 개수를 셉니다. 특정 컬럼의 값이 NULL이어도 행이 존재하면 개수에 포함합니다.

반면 다음 쿼리는 customer_name 값이 NULL이 아닌 행만 셉니다.

SELECT COUNT(customer_name)
FROM orders;

대부분의 경우 전체 행 수를 세고 싶다면 COUNT(*)를 사용하면 됩니다.

SUM, AVG, MIN, MAX 사용하기

전체 판매 수량을 알고 싶다면 SUM()을 사용합니다.

SELECT SUM(quantity) AS total_quantity
FROM orders;

전체 매출을 구하려면 수량과 가격을 곱한 값을 합산해야 합니다.

SELECT SUM(quantity * price) AS total_sales
FROM orders;

quantity * price는 한 주문 행의 판매 금액입니다. SUM(quantity * price)는 모든 주문 행의 판매 금액을 합한 값입니다.

평균 주문 금액은 다음과 같이 구할 수 있습니다.

SELECT AVG(quantity * price) AS avg_order_amount
FROM orders;

가장 낮은 상품 가격과 가장 높은 상품 가격은 MIN(), MAX()로 구합니다.

SELECT
  MIN(price) AS min_price,
  MAX(price) AS max_price
FROM orders;

집계 함수는 데이터를 빠르게 요약할 때 사용합니다. 전체 행을 직접 눈으로 확인하지 않아도 규모와 경향을 파악할 수 있습니다.

WHERE와 집계 함수를 함께 사용하기

집계하기 전에 특정 행만 걸러야 할 때는 WHERE를 사용합니다.

예를 들어 결제 완료된 주문만 대상으로 전체 매출을 구해 보겠습니다.

SELECT SUM(quantity * price) AS paid_sales
FROM orders
WHERE status = 'paid';

이 쿼리는 먼저 status = 'paid'인 행만 남긴 뒤, 그 행들의 매출 합계를 계산합니다.

취소된 주문을 매출에 포함하면 실제 매출보다 크게 계산될 수 있습니다. 그래서 집계할 때는 어떤 데이터를 포함할지 먼저 정해야 합니다.

조건을 잘못 잡으면 결과도 잘못됩니다. 집계 쿼리에서는 WHERE 조건이 특히 중요합니다.


GROUP BY로 기준별 통계 만들기

GROUP BY는 같은 값을 가진 행들을 하나의 그룹으로 묶습니다.

예를 들어 고객별 주문 건수를 구하고 싶다면 고객 이름을 기준으로 묶으면 됩니다.

SELECT
  customer_name,
  COUNT(*) AS order_count
FROM orders
GROUP BY customer_name;

이 쿼리는 customer_name이 같은 행들을 하나로 묶고, 각 고객의 주문 건수를 계산합니다.

결과는 다음과 같은 형태가 됩니다.

customer_nameorder_count
김민수2
이서연2
박지훈1

GROUP BY를 사용하면 전체 데이터 하나의 요약값이 아니라, 그룹별 요약값을 만들 수 있습니다.

상품별 매출 구하기

상품별 판매 수량과 매출을 구해 보겠습니다.

SELECT
  product_name,
  SUM(quantity) AS total_quantity,
  SUM(quantity * price) AS total_sales
FROM orders
WHERE status = 'paid'
GROUP BY product_name;

이 쿼리의 흐름은 다음과 같습니다.

  1. orders 테이블에서 데이터를 가져옵니다.

  2. WHERE status = 'paid'로 결제 완료 주문만 남깁니다.

  3. product_name이 같은 행끼리 그룹으로 묶습니다.

  4. 각 상품 그룹마다 판매 수량과 매출을 계산합니다.

상품별 매출 결과를 매출이 높은 순서로 보고 싶다면 ORDER BY를 함께 사용합니다.

SELECT
  product_name,
  SUM(quantity * price) AS total_sales
FROM orders
WHERE status = 'paid'
GROUP BY product_name
ORDER BY total_sales DESC;

여기서 total_sales는 SUM(quantity * price)에 붙인 별칭입니다. MySQL에서는 ORDER BY에서 이 별칭을 사용할 수 있습니다.

날짜별 통계 만들기

날짜별 주문 건수도 자주 필요한 통계입니다.

SELECT
  DATE(ordered_at) AS order_date,
  COUNT(*) AS order_count,
  SUM(quantity * price) AS total_sales
FROM orders
WHERE status = 'paid'
GROUP BY DATE(ordered_at)
ORDER BY order_date ASC;

ordered_at 컬럼에는 날짜와 시간이 함께 들어 있습니다. 같은 날짜라도 시간이 다르면 서로 다른 값으로 취급되므로, 날짜별로 묶고 싶을 때는 DATE() 함수로 날짜 부분만 꺼내면 됩니다.

예를 들어 2026-05-01 14:30:00이라는 값이 있다면 DATE(ordered_at)은 2026-05-01만 사용합니다.


GROUP BY를 사용할 때 SELECT에 무엇을 쓸 수 있을까

GROUP BY를 사용할 때는 SELECT에 아무 컬럼이나 넣으면 안 됩니다.

기본 원칙은 간단합니다.

  • GROUP BY에 적은 컬럼

  • COUNT(), SUM(), AVG(), MIN(), MAX() 같은 집계 함수

이 두 종류를 중심으로 SELECT를 작성해야 합니다.

예를 들어 다음 쿼리는 자연스럽습니다.

SELECT
  customer_name,
  COUNT(*) AS order_count
FROM orders
GROUP BY customer_name;

customer_name으로 그룹을 묶었고, 각 그룹의 주문 수를 계산했습니다.

하지만 다음 쿼리는 조심해야 합니다.

SELECT
  customer_name,
  product_name,
  COUNT(*) AS order_count
FROM orders
GROUP BY customer_name;

고객별로 그룹을 묶었는데 product_name을 그대로 보여 달라고 했습니다. 한 고객이 여러 상품을 주문했다면 어떤 상품명을 보여 줘야 할지 애매합니다.

MySQL의 기본 SQL 모드에서는 이런 쿼리가 오류로 막히는 경우가 많습니다. 특히 ONLY_FULL_GROUP_BY가 켜져 있으면, 그룹 기준도 아니고 집계 함수도 아닌 컬럼을 함부로 선택할 수 없습니다. 이 설정은 실수를 줄여 주므로, 오류가 난다면 쿼리를 우회하려 하지 말고 "그룹 기준과 집계 대상이 맞는가?"를 먼저 확인하는 것이 좋습니다.

그룹화 쿼리에서는 "그룹의 기준이 무엇인지"와 "그룹 안에서 계산할 값이 무엇인지"를 분명히 구분해야 합니다.


HAVING으로 그룹 결과에 조건 걸기

WHERE는 그룹으로 묶기 전에 행을 필터링합니다.

반면 HAVING은 그룹으로 묶고 집계한 뒤, 그 결과를 필터링합니다.

예를 들어 주문 건수가 2건 이상인 고객만 보고 싶다고 해 보겠습니다.

SELECT
  customer_name,
  COUNT(*) AS order_count
FROM orders
GROUP BY customer_name
HAVING COUNT(*) >= 2;

이 쿼리는 고객별로 주문 건수를 계산한 뒤, 주문 건수가 2건 이상인 고객만 남깁니다.

COUNT(*) >= 2는 개별 행에 대한 조건이 아닙니다. 그룹별 집계 결과에 대한 조건입니다. 그래서 WHERE가 아니라 HAVING을 사용해야 합니다.

WHERE와 HAVING의 차이

WHERE와 HAVING은 모두 조건을 거는 문법입니다. 하지만 적용되는 시점이 다릅니다.

구분적용 대상적용 시점
WHERE개별 행그룹화 전
HAVING그룹 결과그룹화 후

예를 들어 결제 완료 주문만 대상으로 고객별 주문 건수를 구하고, 그중 주문 건수가 2건 이상인 고객만 보고 싶다면 WHERE와 HAVING을 함께 사용합니다.

SELECT
  customer_name,
  COUNT(*) AS paid_order_count
FROM orders
WHERE status = 'paid'
GROUP BY customer_name
HAVING COUNT(*) >= 2;

이 쿼리의 흐름은 다음과 같습니다.

  1. WHERE status = 'paid'로 결제 완료 주문만 남깁니다.

  2. 남은 행을 customer_name 기준으로 묶습니다.

  3. 고객별 주문 건수를 계산합니다.

  4. HAVING COUNT(*) >= 2로 주문 건수가 2건 이상인 그룹만 남깁니다.

만약 다음처럼 작성하면 오류가 나거나 의도와 다르게 동작할 수 있습니다.

SELECT
  customer_name,
  COUNT(*) AS order_count
FROM orders
WHERE COUNT(*) >= 2
GROUP BY customer_name;

WHERE 단계에서는 아직 COUNT(*)가 계산되지 않았습니다. 그래서 집계 함수 결과를 조건으로 걸 수 없습니다. 집계 결과에 조건을 걸 때는 HAVING을 사용해야 합니다.

SQL 문장의 작성 순서와 처리 흐름

집계 쿼리를 읽을 때 헷갈리는 이유는 SQL의 작성 순서와 실제 처리 흐름이 완전히 같지 않기 때문입니다.

보통 쿼리는 다음 순서로 작성합니다.

SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY

하지만 이해할 때는 다음 흐름으로 생각하면 쉽습니다.

  1. FROM으로 테이블을 정합니다.

  2. WHERE로 필요한 행만 남깁니다.

  3. GROUP BY로 행을 그룹으로 묶습니다.

  4. 집계 함수로 그룹별 값을 계산합니다.

  5. HAVING으로 그룹 결과를 걸러냅니다.

  6. SELECT에 지정한 형태로 결과를 보여 줍니다.

  7. ORDER BY로 최종 결과를 정렬합니다.

이 흐름을 기억하면 WHERE와 HAVING의 차이를 쉽게 이해할 수 있습니다.


자주 쓰는 집계 쿼리 패턴

고객별 매출 상위 순위를 보고 싶다면 다음과 같이 작성합니다.

SELECT
  customer_name,
  SUM(quantity * price) AS total_sales
FROM orders
WHERE status = 'paid'
GROUP BY customer_name
ORDER BY total_sales DESC;

상품별 판매 수량이 2개 이상인 상품만 보고 싶다면 다음과 같이 작성합니다.

SELECT
  product_name,
  SUM(quantity) AS total_quantity
FROM orders
WHERE status = 'paid'
GROUP BY product_name
HAVING SUM(quantity) >= 2
ORDER BY total_quantity DESC;

날짜별 매출이 100,000원 이상인 날짜만 보고 싶다면 다음과 같이 작성합니다.

SELECT
  DATE(ordered_at) AS order_date,
  SUM(quantity * price) AS total_sales
FROM orders
WHERE status = 'paid'
GROUP BY DATE(ordered_at)
HAVING SUM(quantity * price) >= 100000
ORDER BY order_date ASC;

이 세 쿼리는 모두 같은 구조를 가집니다.

  • 먼저 필요한 행을 고릅니다.

  • 기준별로 그룹을 만듭니다.

  • 그룹별 값을 계산합니다.

  • 필요한 그룹만 남깁니다.

  • 보기 좋은 순서로 정렬합니다.

복잡해 보이는 통계 쿼리도 이 흐름으로 나누면 이해하기 쉽습니다.

별칭으로 결과를 읽기 쉽게 만들기

집계 함수 결과는 그대로 두면 컬럼명이 길고 읽기 어렵습니다.

SELECT SUM(quantity * price)
FROM orders;

결과 컬럼명이 SUM(quantity * price)처럼 표시될 수 있습니다. 의미는 알 수 있지만 보고서나 화면에서 사용하기에는 불편합니다.

그래서 별칭을 붙이는 습관이 좋습니다.

SELECT SUM(quantity * price) AS total_sales
FROM orders;

별칭은 ORDER BY에서도 사용할 수 있습니다.

SELECT
  product_name,
  SUM(quantity * price) AS total_sales
FROM orders
GROUP BY product_name
ORDER BY total_sales DESC;

별칭을 잘 붙이면 쿼리 결과가 문서처럼 읽힙니다.

이번 장에서 기억할 것

ORDER BY는 조회 결과의 순서를 정합니다. ASC는 오름차순, DESC는 내림차순입니다. 여러 기준을 함께 사용할 수도 있습니다.

집계 함수는 여러 행을 하나의 값으로 요약합니다. COUNT()는 개수, SUM()은 합계, AVG()는 평균, MIN()과 MAX()는 최솟값과 최댓값을 구합니다.

GROUP BY는 같은 값을 가진 행들을 묶어 그룹별 통계를 만듭니다. 고객별 주문 수, 상품별 매출, 날짜별 판매량 같은 통계를 만들 때 사용합니다.

GROUP BY를 사용할 때 SELECT에는 그룹 기준 컬럼과 집계 함수 중심으로 작성해야 합니다. 어떤 그룹에서 어떤 값을 보여 주려는지 명확해야 합니다.

WHERE는 그룹화 전의 개별 행을 거릅니다. HAVING은 그룹화 후의 집계 결과를 거릅니다.

정렬, 집계, 그룹화는 데이터를 읽는 힘을 크게 높여 줍니다. 이제 단순히 행을 조회하는 수준을 넘어, 데이터에서 의미 있는 숫자를 뽑아낼 수 있습니다.

책 목록

처음부터 실무까지 배우는 MySQL

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기