12월, 2025의 게시물 표시

집계 기초: SUM/COUNT로 잔액·기간별 적립/사용 리포트 만들기

포인트 시스템의 “진짜 쓸모”는 원장(point_history)에 기록된 사실을 사람이 이해할 수 있는 형태로 요약하는 데서 나옵니다. 화면에서는 잔액 한 줄로 보이지만, 그 뒤에는 적립/사용/만료/취소가 뒤섞인 수많은 기록이 존재합니다. 그래서 포인트 시스템에서 SQL의 핵심은 단순 조회(SELECT)보다 집계(aggregation) 입니다. 집계는 원장 데이터를 합치고(SUM), 개수를 세고(COUNT), 기준별로 묶어서(GROUP BY) “의미 있는 숫자”로 바꾸는 과정입니다. 이 단원의 목적: 원장 데이터로 ‘요약 결과’를 만드는 습관 만들기 이번 글에서는 아래 3가지를 목표로 합니다. SUM으로 잔액을 만들고, 기간별 합계를 안정적으로 계산한다 COUNT로 이벤트 건수를 집계해 “양”을 파악한다 집계를 할 때 자주 발생하는 실수(NULL, 숨김 데이터, 중복 기준)를 피한다 준비: 집계를 위한 최소 데이터 점검 집계는 데이터가 적으면 티가 안 나지만, 데이터가 쌓이면 작은 실수가 큰 오차로 확대됩니다. 그래서 집계 전에 “필수 조건”을 먼저 확인해 두면 좋습니다. amount가 NULL이 아닌가 숨김 데이터(hide=1)가 섞여 있다면 집계에서 제외할 것인가 부호 정책(적립 + / 차감 -)이 일관적인가 아래 예시는 hide 컬럼이 있다고 가정하고, 집계에서는 hide=0만 포함하는 패턴으로 진행합니다. SET @uid := '11111111-1111-1111-1111-111111111111'; 1) 잔액 계산의 기본: SUM(amount) 가장 기본적인 잔액 조회는 “해당 사용자의 amount를 전부 더하는 것”입니다. 이 방식의 장점은 단순함과 신뢰성입니다. 원장이 올바르면, 잔액은 항상 재현 가능합니다. SELECT uid, SUM(amount) AS balance FROM point_history WHERE uid = @uid AND hide = 0 GROUP BY uid; 여기서...

UPDATE/DELETE 기초: 포인트 원장을 망치지 않는 ‘정정(ADJUST)’과 ‘소프트 삭제’ 전략

포인트 시스템을 실제로 운영하면 “완벽하게 들어오는 데이터”만 존재하지 않습니다. 이벤트 지급이 잘못되거나, 고객 문의로 일부 포인트를 되돌려야 하거나, 테스트 데이터가 섞여 들어오거나, 배치 로직이 한 번 삐끗할 수도 있습니다. 이때 초보자가 가장 쉽게 선택하는 방법이 기존 행을 UPDATE로 고치거나 DELETE로 지워버리는 것 입니다. 하지만 포인트 원장은 “기록” 자체가 신뢰의 기반이기 때문에, 과거 기록을 조용히 바꾸거나 삭제하면 나중에 추적이 불가능해집니다. 오늘은 깔끔해 보이지만, 몇 달 뒤에는 “왜 잔액이 이렇게 되었지?”를 아무도 설명할 수 없는 상태가 될 수 있습니다. 이 단원의 목적: UPDATE/DELETE를 ‘원장 철학’에 맞게 쓰는 기준 세우기 UPDATE/DELETE는 강력하지만, 원장성 데이터를 다룰 때는 사용 기준이 필요합니다. 이번 글의 목표는 아래 3가지입니다. 포인트 시스템에서 UPDATE/DELETE를 써도 되는 경우와 피해야 하는 경우를 구분한다 기록을 고치지 않고도 결과를 바로잡는 정정(ADJUST) 패턴을 만든다 물리 삭제 대신 소프트 삭제(숨김 처리)를 적용해 추적 가능성을 유지한다 1) 원장 데이터는 “수정”보다 “추가”가 기본이다 포인트 원장은 회계의 장부와 비슷합니다. 장부의 기존 줄을 지우거나 고치는 대신, 잘못된 기록이 있었다면 반대되는 기록을 추가 해 결과를 맞춥니다. 이렇게 하면 “무슨 일이 있었는지”가 남기 때문에 나중에 확인이 가능합니다. 이 관점에서 UPDATE/DELETE는 ‘금지’가 아니라 ‘제한적으로만 사용’이 됩니다. 예를 들어 메모를 수정하거나, 테스트 데이터를 숨기는 정도는 가능하지만, amount나 action_type을 바꿔서 결과를 바꾸는 방식은 위험합니다. 2) UPDATE 기본: 특정 행을 ‘조건으로’ 정확히 집어서 수정한다 UPDATE에서 가장 중요한 건 WHERE 입니다. WHERE가 없으면 테이블 전체가 수정됩니다. 그래서 UPDATE는 항...

INSERT 기초: 적립/차감 이벤트를 ‘원장’에 일관되게 기록하는 패턴

포인트 시스템에서 INSERT는 단순한 데이터 입력이 아닙니다. 한 줄의 INSERT는 “어떤 일이 있었는지”를 원장에 남기는 행위이고, 이 기록은 나중에 정산, 고객 문의 대응, 장애 복구의 근거가 됩니다. 그래서 포인트 원장(point_history)에 데이터를 넣을 때는 “일단 들어가면 된다”보다 일관성 있게 들어가야 한다 가 더 중요합니다. 이번 글에서는 INSERT 문법 자체를 익히면서, 포인트 시스템에서 자주 발생하는 실수(부호 뒤집힘, ref_id 누락, created_at 불일치)를 줄이는 입력 패턴을 정리합니다. 이 단원의 목적: INSERT를 ‘기록 규칙’으로 만들기 많은 시스템에서 데이터 품질은 조회가 아니라 입력에서 결정됩니다. 포인트 원장에 기록되는 한 줄 한 줄이 흔들리면, 나중에 SUM으로 잔액을 계산할 때도, 특정 주문을 추적할 때도 “해석 비용”이 계속 발생합니다. 이번 글의 목표는 다음 3가지입니다. 포인트 원장에 반드시 들어가야 하는 최소 컬럼을 고정한다 적립/차감/만료/취소 같은 이벤트를 INSERT 템플릿으로 통일한다 중복/재시도 상황에서 안전하게 기록되는 방향(멱등성)을 염두에 둔다 1) 먼저, 포인트 원장에 “반드시” 들어가야 할 것 시스템마다 컬럼 구성은 다르지만, 원장이라는 성격을 유지하려면 아래 항목은 사실상 필수입니다. uid : 누구에게 발생한 일인지 action_type : 어떤 행위인지(적립/사용/만료/취소 등) amount : 얼마가 변했는지(부호 정책 포함) created_at : 언제 발생했는지 ref_id : 무엇(주문/이벤트/요청)에 의해 발생했는지(가능하면) 여기서 가장 많이 흔들리는 부분이 amount의 부호 정책 입니다. “적립은 +, 차감은 -”처럼 한 번 정했으면 끝까지 고정하는 편이, 조회 쿼리(특히 SUM)와 운영 점검이 쉬워집니다. 2) INSERT 기본 문법: 한 건 기록 가장 기본적인 형태는 아래처럼 한 줄을 넣는 것입니다. 연습할 때...

인덱스 기초: 포인트 내역 조회 속도를 결정하는 (uid, created_at) 인덱스의 의미

포인트 원장(point_history)은 시간이 지날수록 계속 쌓입니다. 초기에는 수천 건이라 체감이 없지만, 몇 달만 지나도 수십만 건, 몇 년이면 수천만 건이 될 수 있습니다. 이때 가장 먼저 느려지는 화면이 보통 “내 포인트 내역 보기”입니다. 조회가 느려지면 사용자는 새로고침을 반복하고, 그 과정에서 요청이 중복되거나(재시도), 운영팀은 “왜 이렇게 느려졌지?”라는 원인 분석에 시간을 쓰게 됩니다. 그래서 포인트 시스템에서는 처음부터 조회 패턴에 맞는 인덱스 를 설계해 두는 편이 안전합니다. 이 단원의 목적: 인덱스를 “추상 개념”이 아니라 “조회 패턴의 설계”로 이해하기 인덱스는 단순히 “빠르게 만드는 옵션”이 아닙니다. 어떤 인덱스를 만들지는 결국 어떤 질문을 자주 던질 것인가 에 대한 답입니다. 이 글에서는 포인트 내역의 대표 조회 패턴을 기준으로, 인덱스가 왜 필요한지와 어떤 조합이 현실적으로 효과적인지를 정리합니다. 1) 인덱스는 “찾기 위한 목차”다 테이블을 책에 비유하면, 전체 데이터를 훑는 것은 책을 처음부터 끝까지 읽는 것과 비슷합니다. 반면 인덱스는 “목차”나 “색인”처럼 원하는 페이지로 바로 이동하게 해줍니다. 데이터베이스가 인덱스를 사용하면, 조건에 맞는 행을 찾기 위해 테이블 전체를 읽는 대신(풀 스캔), 인덱스에서 필요한 범위만 탐색한 뒤 해당 행을 접근합니다. 특히 데이터가 커질수록 이 차이는 압도적으로 커집니다. 2) 포인트 내역의 대표 조회 패턴 3가지 인덱스는 “자주 쓰는 조회”를 기준으로 잡는 것이 기본입니다. 포인트 내역(point_history)에서 거의 고정적으로 반복되는 패턴은 아래 3가지입니다. 사용자 기준 : 특정 uid의 내역을 본다 기간 기준 : 최근 30일, 특정 달 등 created_at 구간으로 자른다 정렬 기준 : 최신순(created_at DESC, id DESC)으로 보여준다 그래서 인덱스 설계는 자연스럽게 (uid, created_at) 로 모입니다. 3...

키 설계 기초: PRIMARY KEY/UNIQUE로 ‘중복 적립’과 ‘중복 차감’을 구조적으로 막는 방법

포인트 시스템에서 가장 골치 아픈 문제는 “한 번만 처리되어야 하는 요청이 두 번 처리되는 것”입니다. 네트워크 재시도, 클라이언트 중복 클릭, 서버 타임아웃 후 재요청은 운영에서 흔한 일이고, 이때 코드만으로 완벽하게 막는 건 생각보다 어렵습니다. 그래서 많은 서비스는 마지막 방어선으로 키(Primary Key/Unique Key) 를 활용합니다. 키 설계가 잘 되면, 애플리케이션이 실수하더라도 DB가 중복을 거절 합니다. 이번 글에서는 “키는 그냥 ID 하나”라는 수준을 넘어서, 포인트 원장(히스토리)에서 실제로 필요한 멱등성(idempotency) 을 어떻게 만들지에 초점을 맞춥니다. 이 단원의 목적: “중복이 생길 수밖에 없는 현실”을 전제로 설계하기 포인트 기능은 보통 결제, 이벤트, 쿠폰, 고객센터 보정 등 다양한 경로에서 호출됩니다. 호출 경로가 늘어날수록 중복 처리 가능성도 늘어납니다. 이번 글의 목표는 아래 3가지입니다. PK/UNIQUE의 역할을 정확히 이해하고 “어디에 걸어야 하는지” 결정한다 포인트 원장에서 중복을 막는 대표 전략(요청 ID/ref_id)을 설계한다 실제로 중복 요청이 들어오는 상황을 가정해, DB 레벨에서 안전하게 막는다 1) PRIMARY KEY는 “행을 식별하는 유일한 주소”다 PRIMARY KEY(PK)는 테이블에서 한 행을 유일하게 식별합니다. 흔히 AUTO_INCREMENT id를 PK로 두는 이유는 단순합니다. 쉽고, 빠르고, 안정적 이기 때문입니다. 하지만 포인트 시스템에서는 “id가 유일하다”만으로는 부족합니다. id는 단지 “기록의 번호”일 뿐, “같은 요청이 두 번 들어왔다”는 문제를 막아주지 않습니다. 즉, PK는 필요조건이지만 중복 방지의 충분조건은 아닙니다. -- 기록 자체의 식별자(기본) PRIMARY KEY (id) 2) UNIQUE는 “같은 의미의 기록이 두 번 들어오는 것”을 막는다 UNIQUE는 “비즈니스적으로 같은 의미”의 중복을 차단하는 도구입니다. ...

데이터 타입과 제약조건: 포인트 시스템에서 “틀린 데이터”는 애초에 못 들어오게 막아야 한다

포인트 시스템을 운영하다 보면, 조회 쿼리를 아무리 잘 만들어도 해결되지 않는 문제가 있습니다. 바로 데이터 자체가 잘못 들어오는 경우 입니다. 한 번 잘못 적립되거나 중복 차감이 발생하면, 그 이후부터는 “정상 데이터 위에 예외 처리를 덧칠하는 작업”이 계속 늘어납니다. 반대로, 초기에 데이터 타입과 제약조건을 제대로 잡아두면 많은 문제가 입력 단계에서 차단 됩니다. 이 단원의 목적: SQL을 “조회 언어”가 아니라 “데이터 품질 언어”로 쓰기 SQL을 배우는 초반에는 SELECT/WHERE가 눈에 들어오지만, 실제 서비스에서는 스키마 설계(타입/제약조건) 가 시스템의 신뢰도를 좌우합니다. 이번 글의 목표는 다음 3가지입니다. 포인트 시스템에 맞는 타입을 고르고, 오버플로우/범위 문제를 예방한다 NOT NULL, DEFAULT, CHECK(가능한 DBMS 한정)로 “불완전한 입력”을 막는다 UNIQUE/FK로 “중복 적립”과 “없는 사용자에 대한 적립” 같은 사고를 차단한다 1) 숫자 타입: INT로 충분한가, BIGINT가 필요한가 포인트는 결국 숫자입니다. 그런데 숫자는 시간이 지나면서 쌓이고, 서비스가 커지면 “상상보다 빨리” 범위를 넘길 수 있습니다. 그래서 초기에 아래를 구분해서 생각하는 편이 안전합니다. amount(변동량) : 한 번의 적립/차감 크기 balance(잔액) : 누적 결과 id(일련번호) : 내역이 쌓이는 속도에 따라 매우 빠르게 증가 amount는 INT가 흔히 충분하지만, balance는 BIGINT가 안전하다 단건 적립이 수천~수만 포인트 수준이라면 amount는 INT로 충분한 경우가 많습니다. 하지만 balance는 장기간 누적될 수 있고, 이벤트나 정책 변경으로 규모가 커질 수 있습니다. 그래서 잔액(balance)을 별도 테이블에 둔다면 BIGINT 를 권장하는 이유가 여기에 있습니다. -- 내역 일련번호는 빠르게 늘어날 수 있으므로 BIGINT 계열이 흔히 쓰입니다. id BIGI...

WHERE로 “조건별 포인트 내역”을 정확하게 필터링하기: 필터가 흔들리면 데이터도 신뢰를 잃는다

포인트 시스템에서 조회는 단순히 “보여주기”가 아닙니다. 포인트는 금액에 준하는 성격을 가지기 때문에, 내역 조회가 조금만 애매해져도 운영에서 바로 문제로 이어집니다. 특히 사용자가 가장 자주 쓰는 기능이 기간/유형/상태 같은 필터인데, 이 필터가 틀리면 “내 포인트가 왜 이래?”라는 불신으로 연결됩니다. 이 단원의 목적: “질문을 정확한 조건으로 번역하는 능력” 만들기 WHERE는 SQL의 필터링(조건) 문법이지만, 실제로는 사용자의 질문을 데이터베이스 언어로 번역 하는 과정입니다. 이번 글의 목표는 세 가지입니다. 포인트 조회 화면에서 자주 나오는 요구사항을 WHERE로 안정적으로 구현한다 AND/OR 조합에서 발생하는 실수를 “괄호”로 확실히 제어한다 운영 데이터가 커져도 느려지지 않게, 조건을 인덱스 친화적으로 작성하는 감각을 익힌다 준비: 실습 데이터(이전 글의 데이터가 있다면 생략 가능) -- uid 1명 기준 예시 SET @uid := '11111111-1111-1111-1111-111111111111'; -- 최근 30일 기준으로 몇 건 더 섞어두면 필터 연습이 쉬워집니다. INSERT INTO point_history (uid, point_type, action_type, amount, ref_id, created_at) VALUES (@uid, 'FREE', 'CHARGE', 150, 'EVENT_20251201', NOW() - INTERVAL 13 DAY), (@uid, 'PAID', 'USE', -80, 'USE_7010', NOW() - INTERVAL 12 DAY), (@uid, 'FREE', 'ADJUST', 30, 'ADMIN_0001', NOW() - INTERVAL 11 DAY); 1) WHERE의 이론: “조건식은 참/거짓을 만든다” WHERE 뒤에 오는...

SELECT 기초로 “내 포인트 잔액/내역” 조회하기: 문법보다 ‘질문’을 먼저 만든다

SQL을 처음 배울 때 가장 빠른 길은 문법을 외우는 게 아니라, “데이터베이스에 어떤 질문을 던질 것인지”를 먼저 정하는 것입니다. 포인트 시스템에서 가장 자주 나오는 질문은 딱 2개입니다. 지금 잔액이 얼마인가? 어떤 내역 때문에 이렇게 되었는가? 이번 글은 SELECT의 핵심 문법(컬럼 선택, 별칭, 정렬, 제한)을 익히면서 위 2개 질문을 실제 쿼리로 만드는 데 집중합니다. 준비: 예시 데이터(작게 넣고 바로 확인하기) 실습을 위해 사용자 1명과 포인트 내역 몇 건을 넣습니다. 부호 정책은 “적립 +, 차감 -”로 통일합니다. -- 사용자 1명 INSERT INTO users (uid, created_at) VALUES ('11111111-1111-1111-1111-111111111111', NOW()); -- 내역 5건 (적립 3, 사용 1, 만료 1) INSERT INTO point_history (uid, point_type, action_type, amount, ref_id, created_at) VALUES ('11111111-1111-1111-1111-111111111111', 'FREE', 'CHARGE', 1000, 'EVENT_20251214', NOW() - INTERVAL 10 DAY), ('11111111-1111-1111-1111-111111111111', 'PAID', 'CHARGE', 500, 'ORDER_9001', NOW() - INTERVAL 7 DAY), ('11111111-1111-1111-1111-111111111111', 'FREE', 'USE', -300, 'USE_7001', NOW() - INTERVAL 5 DAY), ('11111111-1111-1111-1111-111111111111', ...

포인트 시스템에 필요한 최소 테이블 목록 설계: “나중에 고치기 어려운 것”부터 잡는다

포인트 기능을 만들 때 가장 흔한 시행착오는 “일단 구현하고 나중에 정리하자”입니다. 그런데 포인트는 금액에 준하는 데이터라, 설계가 흔들리면 운영에서 바로 비용으로 돌아옵니다. 이번 글에서는 SQL 문법보다 한 단계 앞에서, 포인트 시스템에 필요한 최소 테이블 을 확정하고, 각 테이블이 어떤 질문(조회/검증)에 답해야 하는지까지 정리합니다. 포인트 데이터는 2가지 질문을 반드시 견뎌야 한다 왜 잔액이 이 숫자인가? (원장/히스토리로 추적 가능해야 함) 지금 잔액이 얼마인가? (빠르게 조회되어야 함) 첫 번째 질문은 “감사(추적)”의 영역이고, 두 번째는 “성능”의 영역입니다. 이 둘을 동시에 만족시키는 가장 단단한 출발점은 원장(내역) 테이블을 중심 으로 두고, 필요하면 잔액 테이블을 추가하는 방식입니다. 최소 테이블 2개 + 선택 테이블 1개 처음부터 많은 테이블을 만들 필요는 없습니다. 아래 2개는 필수, 1개는 성능 목적의 선택입니다. 구분 테이블 필요 이유 대표 조회 필수 users uid(사용자 식별) 기준을 고정 회원별 잔액/내역 조회 필수 point_history (원장) 적립/차감/만료/취소의 “사실”을 남김 기간별 내역, 합계, 감사 추적 선택 point_balance “지금 잔액”을 즉시 응답하기 위한 캐시성 테이블 uid로 잔액 1건 조회 1) users: 최소 컬럼만으로 시작해도 된다 여기서 중요한 건 회원정보가 아니라 uid가 일관되게 유지되는 것 입니다. uid는 문자열(UUID)일 수도 있고 정수형 id일 수도 있습니다. 어떤 것을 쓰든 “한 번 정하면 바꾸기 어렵다”는 점만 기억하면 됩니다. CREATE TABLE users ( uid CHAR(36) PRIMARY KEY, -- UUID 가정 created_at DATETIME NOT NULL ); 2) point_history(원장): 포인트 시스템의 중심 포인트는 잔액보다 “내역”이 핵심입...

포인트 시스템으로 배우는 SQL 로드맵: 적립·차감·만료를 끝까지 완성하는 20단계 계획

SQL을 “문법”으로만 배우면, 실제 서비스에 붙일 때 어디서부터 막히는 경우가 많습니다. 반대로 작은 기능을 끝까지 완성 해 보면 SELECT 하나를 배우더라도 “왜 이 쿼리가 필요한지”가 분명해집니다. 이 연재는 포인트(적립/사용/만료/취소) 시스템을 목표로, SQL 기초를 실전 감각으로 쌓아가는 로드맵입니다. 왜 하필 ‘포인트 시스템’인가 포인트는 단순히 숫자 하나를 더하고 빼는 문제가 아닙니다. 운영에서 반드시 부딪히는 이슈가 다 들어 있습니다. 중복 적립 을 어떻게 막을까? 동시에 여러 요청 이 들어오면 잔액이 꼬이지 않을까? 만료(Expire) 는 언제/어떤 기준으로 처리할까? 사용 취소 나 환불 이 생기면 어떻게 되돌릴까? 데이터가 쌓이면 조회 성능 은 어떻게 유지할까? 결국 이 주제 하나로 테이블 설계 , 쿼리 작성 , 인덱스 , 트랜잭션 , 운영 검증 까지 자연스럽게 연결됩니다. 이번 연재에서 만들 결과물 연재가 끝나면 아래를 스스로 설명하고 구현할 수 있는 상태가 목표입니다. 포인트 히스토리 기반 으로 잔액과 내역을 정확하게 조회한다 중복/오류 를 막는 제약조건과 키 설계를 적용한다 동시성 에서 안전하게 적립/차감을 처리한다(트랜잭션/잠금) 만료 배치 쿼리를 만들고 운영 점검 쿼리로 검증한다 성능을 위해 인덱스 와 조회 패턴을 설계한다 포인트 시스템 최소 모델(처음엔 이것만으로 충분) 포인트는 “현재 잔액”보다 왜 그렇게 되었는지(내역) 가 더 중요합니다. 그래서 기본은 ‘히스토리(원장)’ 테이블입니다. -- 예시: point_history (가장 단순한 형태) -- DBMS에 따라 타입/문법은 조금씩 다를 수 있습니다. CREATE TABLE point_history ( id BIGINT PRIMARY KEY AUTO_INCREMENT, uid CHAR(36) NOT NULL, -- 사용자 식별자(UUID 가정) point_type ENUM('PAID','FREE'...