AD JB Toolbox 01

MariaDB 강좌 / 윈도 함수로 누적 합계와 이전·다음 행 비교하기

SUM의 윈도 계산은 행을 유지하면서 누적 합계를 만들고 LAG·LEAD는 정렬 기준상 이전·다음 행의 값을 읽습니다. 날짜가 같은 행의 순서와 누적 범위인 프레임을 명시해야 매번 같은 의미의 누적값을 얻을 수 있습니다.

누적할 행의 단위와 시간 기준 정하기

주문별 누적 매출과 날짜별 누적 매출은 다릅니다. 여기서는 paid 주문 한 건마다 상품 금액을 합산한 뒤 주문 시각·번호 순으로 누적합니다. 배송비·취소·환불을 어떻게 반영할지는 별도 정책이며 이 예제는 paid 상세의 수량×주문 당시 단가만 합산합니다. 행별 계산과 그룹별 계산의 차이는 윈도 함수로 순번과 순위 계산하기, “이전 행”과 실제 날짜 차이의 차이는 날짜 계산하기: 기간·날짜 차이·표시 형식과 함께 판단할 수 있습니다.

계산 범위 첫째·마지막 행
누적 SUM 처음부터 현재 행까지 첫 행은 자기 금액
LAG 정렬상 앞 행 앞 행이 없으면 NULL
LEAD 정렬상 뒤 행 뒤 행이 없으면 NULL

시간 순서가 정해진 주문 데이터 준비하기

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

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

USE shopcourse;
CREATE TABLE c33_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 c33_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 c33_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_c33_order_customer FOREIGN KEY (customer_id)
  REFERENCES c33_customers(customer_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE c33_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_c33_item_order FOREIGN KEY (order_id)
  REFERENCES c33_orders(order_id) ON DELETE RESTRICT,
 CONSTRAINT fk_c33_item_product FOREIGN KEY (product_id)
  REFERENCES c33_products(product_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO c33_customers VALUES
 (1,'김민수','서울'),(2,'이서연','부산'),
 (3,'박지우','서울'),(4,'최하늘','대전');
INSERT INTO c33_products VALUES
 (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50),
 (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5);
INSERT INTO c33_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 c33_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월 10일 101번, 1월 20일 102번, 2월 5일 103번입니다. 날짜가 같을 때도 순서를 결정할 수 있도록 order_id를 보조 기준에 둡니다. 실제 시스템에서 주문 번호가 시간 순서를 보장하는지와 무관하게, 같은 시각의 출력 순서를 명시하는 역할입니다.

ROWS 프레임으로 주문별 누적 금액 계산하기

WITH order_totals AS (
 SELECT o.order_id,o.ordered_at,SUM(i.quantity*i.unit_price) AS goods_total
 FROM c33_orders o JOIN c33_items i ON i.order_id=o.order_id
 WHERE o.status='paid' GROUP BY o.order_id,o.ordered_at
)
SELECT order_id,goods_total,
 SUM(goods_total) OVER(ORDER BY ordered_at,order_id
  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
 LAG(goods_total) OVER(ORDER BY ordered_at,order_id) AS previous_total,
 LEAD(goods_total) OVER(ORDER BY ordered_at,order_id) AS next_total
FROM order_totals ORDER BY ordered_at,order_id;
order_id 금액 누적 이전 다음
101 11500.00 11500.00 NULL 8000.00
102 8000.00 19500.00 11500.00 19500.00
103 19500.00 39000.00 8000.00 NULL

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW는 정렬된 첫 행부터 현재 행까지 포함합니다. 프레임을 생략했을 때의 기본 범위나 같은 정렬값의 처리에 기대기보다 누적 의도를 명시합니다. 같은 날짜가 많은 데이터에서는 ROWS와 RANGE의 동점 범위가 특히 다를 수 있습니다. 윈도 계산과 프레임 개요를 참고하세요.

동점 처리는 테이블을 바꾸지 않는 다음 독립 예제로 비교할 수 있습니다. 501·502번은 시각이 같고 금액만 다릅니다.

WITH sample AS (
 SELECT 501 AS order_id,CAST('2026-01-10 10:00:00' AS DATETIME) AS ordered_at,10 AS amount
 UNION ALL SELECT 502,'2026-01-10 10:00:00',20
 UNION ALL SELECT 503,'2026-01-11 10:00:00',5
)
SELECT order_id,amount,
 SUM(amount) OVER (ORDER BY ordered_at,order_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_unique,
 SUM(amount) OVER (ORDER BY ordered_at,order_id
   RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS range_unique,
 SUM(amount) OVER (ORDER BY ordered_at
   RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS range_time
FROM sample ORDER BY ordered_at,order_id;
order_id amount rows_unique range_unique range_time
501 10 10 10 30
502 20 30 30 30
503 5 35 35 35

첫 두 누적 열은 정렬 기준이 시각·고유 번호로 같고 프레임만 다릅니다. 고유 번호 때문에 동점이 없어 둘 다 10·30·35입니다. 세 번째 열은 RANGE에서 번호 기준만 제거했으므로 같은 시각의 두 행을 함께 포함해 30·30·35가 됩니다. 프레임을 ROWS로 바꾸면서 고유한 보조 기준까지 생략하면 동점 안의 행 순서를 단정할 수 없습니다.

이전·다음 값과 변화량의 의미 확인하기

LAG의 이전은 시간 차이가 일정한 직전 날짜가 아니라 정렬상 앞 행입니다. 주문이 없었던 날짜를 자동으로 만들어 전일 값으로 가져오는 기능이 아닙니다. 전일 대비 지표가 필요하면 날짜별 집계와 누락 날짜를 채우는 기준을 따로 준비해야 합니다. LAG의 행 위치 기준을 확인하세요.

현재 금액에서 이전 금액을 빼면 두 주문의 금액 차이를 구할 수 있지만 첫 행은 이전 값이 없어 NULL입니다. 무조건 COALESCE로 0을 넣으면 첫 주문의 증가액을 그 금액 전체로 정의하는 셈이므로 보고서의 기준을 먼저 정합니다. 결측과 실제 0은 같은 정보가 아닙니다.

필터를 언제 적용하는지에 따라 누적 범위 달라지기

WHERE로 2월 주문만 먼저 남긴 다음 누적하면 2월 안에서 누적이 시작됩니다. 전체 기간을 누적한 결과에서 2월 행만 보여 주려면 윈도 계산을 안쪽 CTE에서 끝내고 바깥에서 날짜 조건을 적용해야 합니다. 두 방식 모두 문법상 가능하지만 지표의 시작점이 다릅니다.

상세 행을 주문 단위로 먼저 합치지 않으면 한 주문의 여러 줄마다 누적이 진행됩니다. 누적 결과를 검토할 때는 기본 단위·대상 상태·정렬 키·프레임·필터 위치 다섯 가지를 함께 기록하면 재현하기 쉽습니다.

아래 첫 쿼리는 전체 paid 주문을 누적한 다음 2월만 표시합니다. 둘째 쿼리는 같은 날짜 조건을 윈도 계산 전으로 옮깁니다. 기존 c33 테이블을 사용하며 원본 데이터는 바꾸지 않습니다.

WITH order_totals AS (
SELECT o.order_id,o.ordered_at,SUM(i.quantity*i.unit_price) AS goods_total
FROM c33_orders o JOIN c33_items i ON i.order_id=o.order_id
WHERE o.status='paid' GROUP BY o.order_id,o.ordered_at
), running AS (
 SELECT order_id,ordered_at,goods_total,
        SUM(goods_total) OVER (ORDER BY ordered_at,order_id
          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
 FROM order_totals
)
SELECT order_id,goods_total,running_total FROM running
WHERE ordered_at >= '2026-02-01' AND ordered_at < '2026-03-01'
ORDER BY ordered_at,order_id;
WITH order_totals AS (
SELECT o.order_id,o.ordered_at,SUM(i.quantity*i.unit_price) AS goods_total
FROM c33_orders o JOIN c33_items i ON i.order_id=o.order_id
WHERE o.status='paid' GROUP BY o.order_id,o.ordered_at
)
SELECT order_id,goods_total,
       SUM(goods_total) OVER (ORDER BY ordered_at,order_id
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM order_totals
WHERE ordered_at >= '2026-02-01' AND ordered_at < '2026-03-01'
ORDER BY ordered_at,order_id;
필터 시점 order_id goods_total running_total
누적 후 103 19500.00 39000.00
누적 전 103 19500.00 19500.00

두 결과 모두 103번 한 행이지만 누적의 시작점이 다릅니다. 전체 누적을 보여 주는 보고서와 월 안에서 다시 시작하는 보고서 중 어느 의미가 필요한지 먼저 정하세요.

정렬과 경계 행을 검증하고 정리하기

작은 데이터에서 직접 11500, 19500, 39000의 누적을 계산해 대조합니다. 첫 행의 LAG와 마지막 행의 LEAD가 NULL인지, 날짜가 같은 테스트 행을 추가했을 때 보조 기준이 일관적인지 확인하세요. 최종 ORDER BY도 따로 명시하고 인덱스의 우연한 순서에 의존하지 않습니다.

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

DROP TABLE shopcourse.c33_items;
DROP TABLE shopcourse.c33_orders;
DROP TABLE shopcourse.c33_products;
DROP TABLE shopcourse.c33_customers;
같은 카테고리의 다른 글
MariaDB 강좌 / 중복 데이터를 줄이는 정규화와 주문 데이터베이스 설계

MariaDB 강좌 / 중복 데이터를 줄이는 정규화와 주문 데이터베이스 설계

주문 상세에 반복한 고객 정보의 변경 오류를 관찰하고 키별 사실을 네 테이블로 나눕니다. 부분·이행 종속, 거래 당시 단가와 스냅샷 보존, 무결성 제약과 성능의 별도 검토를 설명합니다.

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

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

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

MariaDB 강좌 / 로컬·원격 접속과 연결 오류 해결하기

MariaDB 강좌 / 로컬·원격 접속과 연결 오류 해결하기

로컬 TCP 연결을 명시하고 서버 정보·현재 계정·실제 테이블 권한을 확인합니다. 연결 거절·인증·DB 오류를 나누어 진단하고 원격 수신 주소·계정·TLS의 적용 조건을 설명합니다.

MariaDB 강좌 / WHERE로 조건 지정하기: 비교·AND·OR·IN·BETWEEN·LIKE

MariaDB 강좌 / WHERE로 조건 지정하기: 비교·AND·OR·IN·BETWEEN·LIKE

상품 가격·분류·재고·이름으로 WHERE 조건을 실습합니다. AND·OR 괄호 차이, IN 목록, BETWEEN 경계, LIKE 와일드카드와 실제 퍼센트 검색을 예상 행 번호로 확인합니다.

MariaDB 강좌 / SELECT로 열 선택하기: 별칭·계산식·DISTINCT

MariaDB 강좌 / SELECT로 열 선택하기: 별칭·계산식·DISTINCT

SELECT의 열 선택과 AS 별칭, 행별 계산식, DISTINCT의 열 조합 기준을 실습합니다. 원본 데이터와 출력 결과를 구분하고 WHERE에서 별칭을 사용할 때의 주의점을 설명합니다.

MariaDB 강좌 / 문자열 타입 선택하기: CHAR·VARCHAR·TEXT

MariaDB 강좌 / 문자열 타입 선택하기: CHAR·VARCHAR·TEXT

CHAR·VARCHAR·TEXT를 저장 목적에 따라 선택하고 문자 수·바이트 수·끝 공백을 비교합니다. 한글과 이모지 입력, 길이 초과 오류, 정렬 규칙과 긴 본문 저장 시 주의점을 설명합니다.

MariaDB 강좌 / 첫 실습: 상품 테이블을 만들고 데이터를 넣어 조회하기

MariaDB 강좌 / 첫 실습: 상품 테이블을 만들고 데이터를 넣어 조회하기

상품 세 건을 저장하는 전체 SQL로 테이블 생성·입력·조회 과정을 실습합니다. 구조와 행 수, 가격·재고 조건의 예상 결과를 확인하고 반복 입력 오류와 실습 테이블 초기화 범위를 구분합니다.

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

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

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

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

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

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

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

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

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