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;










