채번 UPDATE 를 주문 INSERT 와 같은 트랜잭션에 두면 주문이 실패해 ROLLBACK 할 때 번호도 함께 돌아옵니다.
UPDATE seq_no SET last_no = last_no + 1 WHERE biz = 'ORD' AND ymd = '20260930';
SELECT last_no FROM seq_no WHERE biz = 'ORD' AND ymd = '20260930';
ROLLBACK;
SELECT last_no FROM seq_no WHERE biz = 'ORD' AND ymd = '20260930';LAST_NO
-------
3
(1행)LAST_NO
-------
2
(1행)UPDATE 직후에는 3 이었지만 ROLLBACK 하자 2 로 돌아왔습니다. 번호에 구멍이 생기지 않습니다.
채번만 먼저 커밋해서 잠금을 빨리 푸는 방식도 있습니다. 이 경우는 주문이 실패해도 번호가 남습니다.
UPDATE seq_no SET last_no = last_no + 1 WHERE biz = 'ORD' AND ymd = '20260930';
COMMIT;
-- (여기서 주문 INSERT 가 실패해 ROLLBACK 했다고 가정)
ROLLBACK;
SELECT last_no FROM seq_no WHERE biz = 'ORD' AND ymd = '20260930';
SELECT COUNT(*) AS order_cnt FROM order_h WHERE ord_date = DATE '2026-09-30';LAST_NO
-------
3
(1행)ORDER_CNT
---------
1
(1행)카운터는 3 인데 30일 주문은 1건뿐이라 0003 번 주문이 없는 구멍이 생겼습니다.
| 방식 | 번호 구멍 | 잠금 대기 |
|---|---|---|
| 본 트랜잭션과 묶음 | 없음 | 채번 후 주문 처리가 끝날 때까지 길어짐 |
| 채번만 따로 커밋 | 주문 실패 시 생김 | 짧음 |
팁구멍을 허용하는지가 업무 요구입니다. 세금계산서처럼 번호가 비면 안 되는 경우는 묶고 트랜잭션을 최대한 짧게 씁니다. 주문번호처럼 비어도 되는 경우는 분리해서 대기를 줄입니다.
UPDATE 가 잠금을 잡기 전에 읽기부터 하고 싶다면 SELECT 에 FOR UPDATE 를 붙여 행을 먼저 잠급니다. H2 는 이 문법을 실행합니다.
SELECT last_no FROM seq_no WHERE biz = 'ORD' AND ymd = '20260930' FOR UPDATE;
UPDATE seq_no SET last_no = last_no + 1 WHERE biz = 'ORD' AND ymd = '20260930';
COMMIT;
SELECT last_no FROM seq_no WHERE biz = 'ORD' AND ymd = '20260930';LAST_NO
-------
3
(1행)LAST_NO
-------
4
(1행)읽은 3 에서 UPDATE 로 4 가 되었습니다. 잠금이 SELECT 시점부터 걸려 있어 사이에 다른 세션이 끼어들지 못합니다.
UPDATE 와 값 읽기 사이의 틈을 없애는 DB별 문법입니다. 아래 셋 중 Oracle 의 RETURNING 과 MSSQL 의 OUTPUT 은 H2 에서 실행하지 않았습니다.
-- Oracle · Tibero: PL/SQL 안에서 변수로 받는다
UPDATE seq_no SET last_no = last_no + 1
WHERE biz = 'ORD' AND ymd = '20260930'
RETURNING last_no INTO :n;-- MySQL: 올린 값을 LAST_INSERT_ID 에 실어 세션에서 바로 읽는다
INSERT IGNORE INTO seq_no VALUES ('ORD', '20260930', 0);
UPDATE seq_no SET last_no = LAST_INSERT_ID(last_no + 1) WHERE biz = 'ORD' AND ymd = '20260930';
SELECT LAST_INSERT_ID();-- MSSQL: OUTPUT 으로 올라간 값을 결과로 돌려준다
UPDATE seq_no SET last_no = last_no + 1
OUTPUT inserted.last_no
WHERE biz = 'ORD' AND ymd = '20260930';MySQL 의 LAST_INSERT_ID 는 세션마다 따로 저장되므로 다른 세션이 끼어들어도 내 값이 바뀌지 않습니다. 03 에서 MySQL 모드로 실행하면 7 이던 값이 8 로 올라 LAST_INSERT_ID 로 8 을 받습니다. 다만 H2 가 MySQL 과 같다는 근거는 아닙니다.
소스: 03_dialects.sql. last_no 를 7 로 두고 세 모드로 같은 번호 ORD-20260930-0007 을 만듭니다.
| 항목 | Oracle · Tibero | MySQL | MSSQL |
|---|---|---|---|
| 문자열 연결 | a || b | CONCAT(a, b) | a + b |
| 0 채움 | LPAD(n, 4, '0') | LPAD(n, 4, '0') | RIGHT('0000' + CAST(n AS VARCHAR(4)), 4) |
| 날짜 8자리 | TO_CHAR(d, 'YYYYMMDD') | DATE_FORMAT(d, '%Y%m%d') | CONVERT(CHAR(8), d, 112) |
MSSQL 은 LPAD 가 없어서 0 을 앞에 붙인 뒤 오른쪽 4글자를 자르는 방식을 씁니다. H2 가 실행하는 것은 Oracle 모드의 TO_CHAR, LPAD, 연결과 MySQL 모드의 CONCAT, LPAD, MSSQL 모드의 RIGHT 와 + 입니다. MySQL 의 DATE_FORMAT 과 MSSQL 의 CONVERT 스타일 번호 112 는 H2 미지원이라 문법 검토만입니다.
SET MODE MSSQLServer;
SELECT
'ORD-' + ymd + '-' + RIGHT('0000' + CAST(last_no AS VARCHAR(4)), 4) AS order_no
FROM seq_no;ORDER_NO
-----------------
ORD-20260930-0008
(1행)MSSQL 모드 결과가 0008 인 것은 소스 3번 구간의 UPDATE 가 last_no 를 7 에서 8 로 올렸기 때문입니다.
표준 모드에서도 RIGHT('0000' || CAST(n AS VARCHAR), 4) 가 동작해 공통 방법이 됩니다(소스 5번).
주의LPAD 는 자릿수를 넘으면 오른쪽을 자릅니다. Oracle·MySQL 의 LPAD(10000, 4, '0') 은 1000 이 되고, MSSQL 의 RIGHT 도 뒤 4글자만 남깁니다. 하루 1만 건이 넘을 수 있으면 자릿수를 늘려 둡니다.