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 강좌 / 두 연결로 격리 수준·잠금·교착 상태 확인하기

MariaDB 강좌 / 두 연결로 격리 수준·잠금·교착 상태 확인하기

별도 연결 A·B의 실행 순서대로 격리 수준, 행 잠금 대기와 교착 상태를 비교합니다. 초기화·재조회·오류 후 정리와 연결별 설정 복원까지 전체 코드로 확인합니다.

MariaDB 강좌 / 날짜와 시간 저장하기: DATE·DATETIME·TIMESTAMP

MariaDB 강좌 / 날짜와 시간 저장하기: DATE·DATETIME·TIMESTAMP

DATE·DATETIME·TIMESTAMP의 저장 의미와 시간대 처리를 비교합니다. 동일 입력의 한국 시간·UTC 조회 결과, 버전별 TIMESTAMP 범위, 날짜 오류와 설정 복원을 실습합니다.

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

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

고객별 금액의 ROW_NUMBER·RANK·DENSE_RANK를 비교하고 지역별 순위와 바깥 조회의 상위 N개 필터를 실습합니다. 공동 순위와 행 개수 제한의 차이를 결과로 확인합니다.

MariaDB 강좌 / mariadb-dump로 데이터베이스 백업하기

MariaDB 강좌 / mariadb-dump로 데이터베이스 백업하기

별도 주문 실습 DB를 준비해 mariadb-dump로 새 SQL 백업 파일을 만듭니다. 종료 코드·해시·문자 인코딩, InnoDB 스냅샷과 동시 DDL의 한계, 객체·권한·별도 복원 검증 기준을 설명합니다.

MariaDB 강좌 / ORDER BY·LIMIT로 정렬하고 조회 범위 제한하기

MariaDB 강좌 / ORDER BY·LIMIT로 정렬하고 조회 범위 제한하기

ORDER BY의 여러 열·방향과 LIMIT·OFFSET을 같은 가격의 상품으로 실습합니다. 동점 해소, 페이지 경계 변화, 마지막 정렬값을 이용한 조회와 NULL 배치를 설명합니다.

MariaDB 강좌 / Ubuntu에 MariaDB 설치하고 서버 실행하기

MariaDB 강좌 / Ubuntu에 MariaDB 설치하고 서버 실행하기

Ubuntu APT 설치 후 서버 실행·자동 시작·관리자 소켓 인증을 확인합니다. 로컬 TCP 학습 계정을 만들고 패키지 버전, 인증 오류와 서비스 로그를 확인하는 절차를 설명합니다.

MariaDB 강좌 / INSERT로 한 행과 여러 행 추가하기

MariaDB 강좌 / INSERT로 한 행과 여러 행 추가하기

INSERT의 열·값 대응, 한 행과 다중 행 입력, 자동 번호와 기본값을 상품 데이터로 실습합니다. 저장 결과와 오류 뒤 행 수를 확인하고 기존 데이터와 분리해 반복하는 방법을 설명합니다.

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

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

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

MariaDB 강좌 / 재귀 CTE로 계층 데이터 조회하고 순환 막기

MariaDB 강좌 / 재귀 CTE로 계층 데이터 조회하고 순환 막기

부모·자식 분류 데이터로 재귀 CTE의 앵커, 재귀 단계, 깊이와 경로를 실습합니다. 종료 조건 변경과 순환 데이터 테스트를 통해 잘린 결과와 무한 반복 위험을 구분합니다.

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

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

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