AD JB Toolbox 01

MariaDB 강좌 / 윈도 함수로 순번과 순위 계산하기

윈도 함수는 원래 결과 행을 유지하면서 정렬 순서·그룹에 따른 계산값을 덧붙입니다. ROW_NUMBER는 행마다 다른 순번을, RANK와 DENSE_RANK는 같은 기준값에 같은 순위를 주므로 동점 처리 규칙을 먼저 정해야 합니다.

순번과 순위의 차이 정하기

고객별 구매 금액이 같아도 화면의 행 번호는 각각 달라야 할 수 있고, 구매 순위는 공동 순위여야 할 수 있습니다. 동점을 처리하는 요구가 다른데 함수 이름만 바꾸면 안 됩니다. 집계로 고객당 금액을 먼저 만든 뒤 그 고객 행에 순위를 붙이겠습니다. 집계와 순위 단계를 나눌 때는 WITH로 공통 테이블 식 만들고 쿼리 나누기를 사용하고, 같은 행 순서에서 누적·이전값을 계산하는 경우는 윈도 함수로 누적 합계와 이전·다음 행 비교하기와 비교합니다.

함수 동점 처리 다음 순위
ROW_NUMBER 동점이어도 서로 다른 순번 1,2,3,4
RANK 같은 값은 같은 순위 1,1,3,3
DENSE_RANK 같은 값은 같은 순위 1,1,2,2

동점 고객이 있는 데이터 준비하기

학습용 MariaDB 서버의 shopcourse에 course_user로 접속합니다. 명령줄 클라이언트와 UTF-8 SQL 파일 실행 방법대로 코드를 UTF-8 파일에 저장하고 SOURCE C:/mariadb-lab/lesson.sql;처럼 실제 파일 경로로 실행할 수 있습니다. SQL 문은 운영체제 터미널이 아닌 MariaDB 연결 안에서 실행합니다.

먼저 SELECT VERSION(), DATABASE();와 SHOW TABLES;로 대상 서버·DB·이름 중복을 확인합니다. 아래 c32_customers, c32_products, c32_orders, c32_items는 이 글의 독립된 연습용 이름입니다. 이미 존재하면 정의와 행을 확인하고, 보관할 자료가 없는 이 글의 실습 테이블임이 확실할 때만 마지막 절의 정리 순서로 지운 뒤 다시 만듭니다. 기존 강좌의 동명 없는 테이블이나 운영 데이터는 삭제하지 않습니다.

USE shopcourse;
CREATE TABLE c32_customers (
 customer_id INT NOT NULL PRIMARY KEY,
 customer_name VARCHAR(50) NOT NULL,
 city VARCHAR(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c32_products (
 product_id INT NOT NULL PRIMARY KEY,
 product_name VARCHAR(100) NOT NULL,
 category VARCHAR(30) NOT NULL,
 price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
 stock INT NOT NULL CHECK (stock >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c32_orders (
 order_id INT NOT NULL PRIMARY KEY,
 customer_id INT NOT NULL,
 ordered_at DATETIME NOT NULL,
 status VARCHAR(20) NOT NULL,
 shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 1000,
 CONSTRAINT fk_c32_order_customer FOREIGN KEY (customer_id)
  REFERENCES c32_customers(customer_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c32_items (
 order_id INT NOT NULL,
 product_id INT NOT NULL,
 quantity INT NOT NULL CHECK (quantity > 0),
 unit_price DECIMAL(10,2) NOT NULL CHECK (unit_price >= 0),
 PRIMARY KEY (order_id, product_id),
 CONSTRAINT fk_c32_item_order FOREIGN KEY (order_id)
  REFERENCES c32_orders(order_id) ON DELETE RESTRICT,
 CONSTRAINT fk_c32_item_product FOREIGN KEY (product_id)
  REFERENCES c32_products(product_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO c32_customers VALUES
 (1,'김민수','서울'),(2,'이서연','부산'),
 (3,'박지우','서울'),(4,'최하늘','대전');
INSERT INTO c32_products VALUES
 (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50),
 (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5);
INSERT INTO c32_orders
 (order_id,customer_id,ordered_at,status,shipping_fee) VALUES
 (101,1,'2026-01-10 10:00:00','paid',1000),
 (102,1,'2026-01-20 11:00:00','paid',1000),
 (103,2,'2026-02-05 12:00:00','paid',1000),
 (104,3,'2026-02-10 13:00:00','canceled',1000);
INSERT INTO c32_items VALUES
 (101,1,2,3500),(101,2,3,1500),(102,3,1,8000),
 (103,1,1,3500),(103,3,2,8000),(104,2,1,1500);

paid 상품 금액 합계는 고객 1과 2가 각각 19500.00으로 같습니다. 고객 3은 취소 주문만, 고객 4는 주문이 없어 둘 다 paid 금액을 0으로 정의합니다. 주문 없는 고객도 포함할지, 취소 금액을 뺄지는 이 순위의 업무 정책입니다.

고객별 금액을 만든 뒤 세 함수를 비교하기

WITH customer_totals AS (
 SELECT c.customer_id,c.customer_name,
 COALESCE(SUM(CASE WHEN o.status='paid'
  THEN i.quantity*i.unit_price ELSE 0 END),0) AS goods_total
 FROM c32_customers c LEFT JOIN c32_orders o
  ON o.customer_id=c.customer_id
 LEFT JOIN c32_items i ON i.order_id=o.order_id
 GROUP BY c.customer_id,c.customer_name
)
SELECT customer_id,customer_name,goods_total,
 ROW_NUMBER() OVER(ORDER BY goods_total DESC,customer_id ASC) AS row_no,
 RANK() OVER(ORDER BY goods_total DESC) AS amount_rank,
 DENSE_RANK() OVER(ORDER BY goods_total DESC) AS dense_amount_rank
FROM customer_totals ORDER BY goods_total DESC,customer_id ASC;
고객 금액 row_no amount_rank dense_amount_rank
1 19500.00 1 1 1
2 19500.00 2 1 1
3 0.00 3 3 2
4 0.00 4 3 2

ROW_NUMBER에는 customer_id를 보조 기준으로 넣어 같은 금액에서 누가 먼저인지 정했습니다. RANK와 DENSE_RANK에는 금액만 넣어 공동 순위를 유지합니다. 순위 함수에도 고객 번호를 넣으면 금액이 같아도 다른 정렬 조합이 되어 동점이 사라집니다. ROW_NUMBER 문법과 윈도 함수 개요를 참고하세요.

OVER의 정렬과 최종 결과 정렬 구분하기

OVER(ORDER BY ...)는 함수가 순번·순위를 계산할 기준이며, 최종 SELECT의 ORDER BY는 화면에 출력할 행 순서입니다. OVER에 정렬이 있다는 이유로 최종 출력 순서까지 보장된다고 가정하지 않습니다. 필요하면 두 기준을 모두 명시합니다.

PARTITION BY는 윈도 계산을 다시 시작할 그룹을 지정합니다. 지역별 순위라면 city를 집계 결과에 포함하고 PARTITION BY city를 검토할 수 있습니다. GROUP BY처럼 고객 행을 줄이는 것이 아니라 각 그룹 안에서 계산 범위를 정한다는 점이 다릅니다.

다음 전체 조회는 같은 고객별 금액을 지역별로 나누어 순번을 다시 시작합니다. 기존 c32 실습 테이블을 그대로 사용하며 최종 결과는 고객 번호로 정렬합니다.

WITH customer_totals AS (
SELECT c.customer_id, c.customer_name, c.city,
       COALESCE(SUM(CASE WHEN o.status='paid'
         THEN i.quantity*i.unit_price ELSE 0 END),0) AS goods_total
FROM c32_customers c
LEFT JOIN c32_orders o ON o.customer_id=c.customer_id
LEFT JOIN c32_items i ON i.order_id=o.order_id
GROUP BY c.customer_id,c.customer_name,c.city
)
SELECT customer_id,city,goods_total,
       ROW_NUMBER() OVER (PARTITION BY city
         ORDER BY goods_total DESC,customer_id ASC) AS regional_row_no
FROM customer_totals ORDER BY customer_id;
customer_id goods_total regional_row_no
1 19500.00 1
2 19500.00 1
3 0.00 2
4 0.00 1

고객 1·3은 같은 서울 그룹이므로 순번이 1·2입니다. 고객 2·4는 서로 다른 지역의 첫 행이어서 각각 1입니다. PARTITION BY를 제거하면 전체 고객 네 명에 대한 순번이 됩니다. 비교 후 city 기준을 되돌리세요.

상위 순위 필터는 바깥 조회에서 적용하기

같은 SELECT의 WHERE에서 방금 만든 row_no를 바로 사용할 수 없습니다. CTE나 파생 테이블로 순위 결과를 한 번 감싼 뒤 바깥 WHERE에서 row_no <= 2처럼 제한합니다. 상위 두 행과 공동 1위 전체는 다른 요구이므로 ROW_NUMBER와 RANK 중 어떤 값을 필터할지도 정해야 합니다.

상품 상세 행에 바로 순위를 붙이면 고객당 한 번의 순위가 아니라 상세 행의 순위가 됩니다. 고객별 금액 합계를 먼저 만드는 단계와 순위 부여 단계를 구분하면 행 단위가 바뀌는 실수를 줄일 수 있습니다.

WITH customer_totals AS (
SELECT c.customer_id, c.customer_name, c.city,
       COALESCE(SUM(CASE WHEN o.status='paid'
         THEN i.quantity*i.unit_price ELSE 0 END),0) AS goods_total
FROM c32_customers c
LEFT JOIN c32_orders o ON o.customer_id=c.customer_id
LEFT JOIN c32_items i ON i.order_id=o.order_id
GROUP BY c.customer_id,c.customer_name,c.city
), ranked AS (
 SELECT customer_id,customer_name,goods_total,
        ROW_NUMBER() OVER (ORDER BY goods_total DESC,customer_id) AS row_no,
        RANK() OVER (ORDER BY goods_total DESC) AS amount_rank
 FROM customer_totals
)
SELECT customer_id,goods_total,row_no,amount_rank
FROM ranked WHERE row_no <= 2 ORDER BY row_no;

결과는 고객 1·2이며 row_no는 1·2, amount_rank는 둘 다 1입니다. 같은 쿼리에서 조건만 row_no <= 1로 바꾸면 한 행, amount_rank = 1로 바꾸면 공동 1위 두 행이 남습니다. 두 결과의 요구 사항이 다르므로 “상위 한 행”과 “1위 전체”를 구분하고 비교 후 row_no <= 2로 되돌립니다.

동점·보조 기준·대상 집합 확인하기

금액이 같은 고객을 일부러 넣어 공동 순위와 다음 순위를 비교합니다. 고객 번호 방향을 바꿨을 때 ROW_NUMBER만 동점 안에서 바뀌는지, 취소·미구매 고객을 포함한 정책이 맞는지도 확인하세요. 순위 숫자는 저장된 고유 고객 번호를 대체하는 식별 키가 아닙니다.

반복하기 전 현재 DB와 실제 정의·행을 확인하고 필요한 자료를 백업합니다. 보관할 데이터가 없는 이 글의 전용 실습 객체만 아래 순서로 정리합니다. 외래키가 있으면 자식부터 지웁니다. DROP TABLE은 구조와 데이터를 모두 없애며 일반적인 ROLLBACK으로 복원되지 않으므로 취소 가능한 DML 실습과 분리합니다.

DROP TABLE shopcourse.c32_items;
DROP TABLE shopcourse.c32_orders;
DROP TABLE shopcourse.c32_products;
DROP TABLE shopcourse.c32_customers;
같은 카테고리의 다른 글
MariaDB / VIEW

MariaDB / VIEW

VIEW는 가상의 테이블입니다. 실제로 데이터를 저장하지 않고, 기존의 하나 이상의 테이블을 기반으로 한 쿼리 결과를 테이블처럼 사용할 수 있게 해줍니다. 뷰는 복잡한 쿼리를 단순화하고, 보안 및 데이터 관리에 유용하게 사용됩니다.

MariaDB / 연산자 / 비교 연산자

MariaDB / 연산자 / 비교 연산자

MariaDB의 비교 연산자에 대하여 알아보겠습니다. 예제로 사용하는 테이블은 아래의 데이터를 가진 T_1이라는 테이블입니다. 각 열의 데이터 형식은 다음과 같습니다. C_1 : 정수 C_2 : 문자 C_3 : 날짜 +----+------+------+------------+ | ID | C_1 | C_2 | C_3 | +----+------+------+------------+ | 1 | 1 | ...

MariaDB / Query / LEFT JOIN, RIGHT JOIN, INNER JOIN

MariaDB / Query / LEFT JOIN, RIGHT JOIN, INNER JOIN

SQL의 JOIN은 여러 테이블을 한 번에 나타낼 때 사용합니다. 대표적인 JOIN 방식은 LEFT JOIN, RIGHT JOIN, INNER JOIN이 있습니다. 다음은 세 가지 JOIN의 차이를 알아보기 위한 간단한 예제 테이블입니다.

MariaDB / 피벗 테이블 만드는 방법

MariaDB / 피벗 테이블 만드는 방법

피벗 테이블은 엑셀의 유용한 기능 중의 하나입니다. 예를 들어 아래의 왼쪽 데이터를 오른쪽 표처럼 만드는 것이 피벗 테이블입니다. 조건에 따라 그룹화시켜서 보기 좋게 데이터를 정리하는 것이라 보면 됩니다. MariaDB에서도 몇 가지 함수를 이용하여 피벗 테이블을 만들 수 있습니다.

MariaDB 강좌 / CASE로 조건별 값과 집계 만들기

MariaDB 강좌 / CASE로 조건별 값과 집계 만들기

CASE 조건의 순서와 ELSE를 상품 재고 상태로 실습합니다. SUM·COUNT 안의 조건부 집계, 0과 NULL의 차이, WHERE로 그룹을 없애는 경우와 0을 표시하는 경우를 설명합니다.

MariaDB 강좌 / INNER JOIN으로 서로 연결된 데이터 조회하기

MariaDB 강좌 / INNER JOIN으로 서로 연결된 데이터 조회하기

고객과 주문을 INNER JOIN으로 연결하고 결과 한 행의 의미를 확인합니다. ON과 WHERE, 테이블 별칭, 일대다로 늘어나는 행과 조건 없는 곱집합을 비교합니다.

MariaDB 강좌 / NULL을 올바르게 비교하기: IS NULL·COALESCE

MariaDB 강좌 / NULL을 올바르게 비교하기: IS NULL·COALESCE

NULL·빈 문자열·0을 구분하고 IS NULL·IS NOT NULL과 COALESCE의 역할을 실습합니다. 일반 비교에서 빠지는 NULL 행, NULL-safe 비교와 NOT IN 목록의 함정을 설명합니다.

MariaDB 강좌 / WITH로 공통 테이블 식 만들고 쿼리 나누기

MariaDB 강좌 / WITH로 공통 테이블 식 만들고 쿼리 나누기

WITH의 공통 테이블 식으로 결제 주문·주문별 합계·고객 연결을 단계별로 나눕니다. 중간 결과 검증, 한 문장 안의 유효 범위와 성능을 자동 보장하지 않는 이유를 설명합니다.

MariaDB 강좌 / 서버 설정·SQL 모드·시간대·로그 확인하기

MariaDB 강좌 / 서버 설정·SQL 모드·시간대·로그 확인하기

설정 파일·GLOBAL·SESSION의 차이를 기준으로 MariaDB 동작을 확인합니다. SQL 모드 오류와 TIMESTAMP·DATETIME 시간대 실습, 로그 조건 점검 및 변경 복구 절차를 제공합니다.

MariaDB 강좌 / 저장 프로시저·함수·트리거의 차이와 사용 방법

MariaDB 강좌 / 저장 프로시저·함수·트리거의 차이와 사용 방법

프로시저·함수·트리거의 호출 방식과 권한 차이를 재고·금액·변경 기록 예제로 비교합니다. 전체 생성 SQL과 실행 결과, 롤백·확정·오류 확인 및 객체 정리 방법을 제공합니다.