https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/#crayon-60fa2fbab27f7557869434
본 글은 위의 글을 참조하여 실제 데이터를 생성하여 테스트 해본 결과를 공유하는 것입니다.
UUID 개념은 구글링의 많은 문서가 있으므로 가볍게 다루고 위 solution을 이해하는데
필요한 부수적인 개념들을 중간중간 설명토록 하겠습니다.
uuid 장점
- 쉽게 추측가능한 sequence number와 다르게 보안우수
- 테이블에 한정되어있는 sequence number와 다르게 범용적으로 유일성을 보장해주는 방식
uuid 단점
- 36글자의 문자열
- 속도 이슈
uuid 는 36글자의 16진수로 이루어져 추측하기가 어려워 보안에 우수한 장점이 있습니다.
또한 테이블이나 DB 크게는 서버한대에 유일성이 국한되어있는 sequence number 와 다르게
다른 서버에서도 uuid는 겹칠일이 거의 없어 scale-out시 큰 장점이 된다는 점이 있습니다.
허나 pk로 사용하기에 36글자라는 큰 저장공간과 그로인한 인덱스 메모리 증가,
또한 정수타입의 인덱스 칼럼에 최적화되어있는 mysql, 오라클 db에서 uuid의 성능은 단점입니다.
참조한 글에서 이러한 uuid의 단점을 최대한으로 극복하는 seqeuntial uuid 개념의 해결책을 제안해주었는데
참조글에서 보여주지 않은 select쿼리 성능 테스트 결과와 jpa에서 사용하는 소스도 공유하도록 하겠습니다.
비교 기준은 UUID 버전1, seqeuntial uuid (uuid버전1을 커스터마이징), sequence number(auto_increment) 입니다.
seqeuntial uuid가 기존 uuid를 커스터마이징하여 단점을 극복한 해결방안입니다.
mysql 의 select UUID()로 생성한 uuid는 시간 기반의 uuid 버전1 입니다.
SELECT UUID();
uuid 버전1을 seqeuntial 하게 표현한 uuid 입니다.
CREATE DEFINER=`root`@`localhost` FUNCTION `ordered_uuid`(uuid BINARY(36))
RETURNS binary(16) DETERMINISTIC
RETURN UNHEX(CONCAT(SUBSTR(uuid, 15, 4),SUBSTR(uuid, 10, 4),SUBSTR(uuid, 1, 8),SUBSTR(uuid, 20, 4),SUBSTR(uuid, 25)));
(ordered_uuid 라는 이름으로customizing 한 함수를 만들었습니다.)
sequence number는 테이블 생성시 pk 칼럼을 int형 타입에 auto_increment 로 생성되는 값입니다.
테스트 데이터 생성
테이블 구성
create table members (
user_uniq_id binary(16) not null primary key,
email varchar(60) not null,
password varchar(60) not null,
signup_date datetime not null,
last_login_date datetime not null
);
create table members_rand (
user_uniq_id binary(16) not null primary key,
email varchar(60) not null,
password varchar(60) not null,
signup_date datetime not null,
last_login_date datetime not null
);
create table members_int (
id int(11) not null primary key auto_increment,
email varchar(60) not null,
password varchar(60) not null,
signup_date datetime not null,
last_login_date datetime not null
);
위와 같이 3개의 테이블을 만들어주었고요.
members 테이블에는 sequential_uuid가 들어가고, members_rand 테이블에는 mysql에서 제공하는 uuid
members_int테이블은 sequnce number입니다.
36글자를 char(36)으로 저장하지 않고 binary (16)으로 저장하여 칼럼 크기를 줄이겠습니다.
데이터는 133만개를 만들어 시작했습니다.
mysql 함수와 프로시저 만들어서 각 테이블에 130만개 데이터 만들어서 진행했습니다.
CREATE DEFINER=`root`@`localhost` FUNCTION `ordered_uuid`(uuid BINARY(36))
RETURNS binary(16) DETERMINISTIC
RETURN UNHEX(CONCAT(SUBSTR(uuid, 15, 4),SUBSTR(uuid, 10, 4),SUBSTR(uuid, 1, 8),SUBSTR(uuid, 20, 4),SUBSTR(uuid, 25)));
CREATE DEFINER=`root`@`localhost` FUNCTION `ordered_uuid2`(uuid BINARY(36))
RETURNS binary(16) DETERMINISTIC
RETURN UNHEX(REPLACE(uuid,'-',''));
DELIMITER $$
DROP PROCEDURE IF EXISTS loopInsert$$
CREATE PROCEDURE loopInsert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 1330000 DO
INSERT INTO MEMBERS (USER_UNIQ_ID, EMAIL, PASSWORD, signup_date, last_login_date) VALUES (ordered_uuid(uuid()),'ads@naver.com', '567sa1dsafsadfsasffdaadsffdaadsfasdfdfasd', now(), now());
SET i = i + 1;
END WHILE;
END$$
DELIMITER $$
DELIMITER $$
DROP PROCEDURE IF EXISTS loopInsert2$$
CREATE PROCEDURE loopInsert2()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 1330000 DO
INSERT INTO MEMBERS_rand (USER_UNIQ_ID, EMAIL, PASSWORD, signup_date, last_login_date) VALUES (ordered_uuid2(uuid()),'ads@naver.com', '567sa1dsafsadfsasffdaadsffdaadsfasdfdfasd', now(), now());
SET i = i + 1;
END WHILE;
END$$
DELIMITER $$
DELIMITER $$
DROP PROCEDURE IF EXISTS loopInsert3$$
CREATE PROCEDURE loopInsert3()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 1330000 DO
INSERT INTO MEMBERS_int (EMAIL, PASSWORD, signup_date, last_login_date) VALUES ('ads@naver.com', '567sa1dsafsadfsasffdaadsffdaadsfasdfdfasd', now(), now());
SET i = i + 1;
END WHILE;
END$$
DELIMITER $$
CALL loopInsert();
CALL loopInsert2();
CALL loopInsert3();
위 코드 읽어보시고 한번에 실행하지 마시고 테이블별로 따로 따로 실행하시면 될 것입니다.
FUNCTION을 보면 uuid의 -(대쉬)를 없앤 값에 UNHEX()함수를 사용해주었는데 UNHEX는 16진수의 수를
이진문자로 반환하여 리턴합니다. 따라서 uuid는 128비트의 숫자이이므로 byte값으로 16바이트입니다.
36글자의 uuid를 char(36)으로 저장하는 경우보다 20바이트를 줄일 수 있습니다.
실제 uuid 생성값 비교
sequential uuid 값
11ECC24777FD0EE4894000D861C2524E |
11ECC24777FD2F54894000D861C2524E |
11ECC24777FD59F5894000D861C2524E |
11ECC24777FD7F31894000D861C2524E |
11ECC24777FDA782894000D861C2524E |
11ECC24777FDD0EB894000D861C2524E |
11ECC24777FDF979894000D861C2524E |
11ECC24777FE21EF894000D861C2524E |
11ECC24777FE4928894000D861C2524E |
11ECC24777FE7080894000D861C2524E |
uuid 값(시간기반, uuid 버전1)
0031E325C25711EC894000D861C2524E |
0032053CC25611EC894000D861C2524E |
003206C0C25911EC894000D861C2524E |
00320D9AC25511EC894000D861C2524E |
00322569C25311EC894000D861C2524E |
00322A0BC25211EC894000D861C2524E |
0032308BC25A11EC894000D861C2524E |
0032374FC25811EC894000D861C2524E |
00323ECDC25411EC894000D861C2524E |
003249EBC25711EC894000D861C2524E |
sequential uuid와 일반 uuid를 보면 sequential uuid는 12번째 자리부터 값이 변하는데 일반 uuid는 4번째 자리부터
값이 변하는 걸 보니 sequential uuid가 더 잘 정렬됨을 확인 할 수 있습니다.
133만개의 데이터를 전체 인덱스 풀 스캔 했을때 select 성능 비교 입니다.
sequential uuid | select count(user_uniq_id) from members | 1 row(s) returned | 0.266 sec / 0.000 sec |
uuid | select count(user_uniq_id) from members_rand | 1 row(s) returned | 2.765 sec / 0.000 sec |
sequence number | select count(id) from members_int | 1 row(s) returned | 0.375 sec / 0.000 sec |
sequential uuid 처리시간(0.266 sec) < sequence number 처리시간(0.375 sec) < uuid 처리시간(2.765 sec)
sequential uuid의 select 성능이 일반 uuid에 비해 10배 좋고 sequence number 처리속도 보다
오히려 좋음을 볼 수 있습니다.
각 테이블의 특정값을 조회했을때 처리시간은 모두 0.000 sec 였습니다.
select HEX(user_uniq_id) from members where user_uniq_id=(select user_uniq_id from members where user_uniq_id =UNHEX('11ECC24777FD59F5894000D861C2524E'));
select HEX(user_uniq_id) from members_rand where user_uniq_id=(select user_uniq_id from members_rand where user_uniq_id =UNHEX('0032053CC25611EC894000D861C2524E'));
select id from members_int where id=1000;
아래는 133만개의 데이터가 있을때 10만개를 insert 했을 때, 성능 비교 입니다.
sequential uuid | CALL loopInsert(); | 1 row(s) affected | 195.360 sec |
uuid | CALL loopInsert2(); | 1 row(s) affected | 197.156 sec |
sequence number | CALL loopInsert3(); | 1 row(s) affected | 191.625 sec |
insert 성능은
sequence number 처리시간(191.625 sec) < sequential uuid 처리시간(195.360 sec) < uuid 처리시간(197.156 sec)
으로 sequence number 가 가장 좋은 성능을 나타냅니다.
데이터를 직접 만들어 테스트를 해보니 일반 uuid도 성능이 크게 나쁘지는 않지만 sequential uuid를 사용하는 것이
보다 괜찮은 것 같습니다.
JPA에서 sequentail UUID 사용
build.gradle
//UUID
implementation "com.fasterxml.uuid:java-uuid-generator:4.0.1"
entity설정
@Id
@Column(columnDefinition = "BINARY(16)")
private UUID userUniqId;
@PrePersist
public void createUserUniqId() {
//sequential uuid 생성
UUID uuid = Generators.timeBasedGenerator().generate();
String[] uuidArr = uuid.toString().split("-");
String uuidStr = uuidArr[2]+uuidArr[1]+uuidArr[0]+uuidArr[3]+uuidArr[4];
StringBuffer sb = new StringBuffer(uuidStr);
sb.insert(8, "-");
sb.insert(13, "-");
sb.insert(18, "-");
sb.insert(23, "-");
uuid = UUID.fromString(sb.toString());
this.userUniqId = uuid;
}
위와 같이 사용하시면 테스트에서 사용한 sequential uuid와 같은 방식으로 pk가 생성됩니다.
저 또한 이번에 uuid를 pk칼럼으로 사용하고자 개념을 알아보았는데
처음보는 개념이라 쉽게 받아들이기 어려워 여기저기 읽히지도 않는 구글의 많은 영문 포스팅을 읽어 보았는데
테스트를 직접해보니 성능단점을 어느정도 극복하고 보안의 우수성이라는 큰 장점으로 사용하기에 좋은 것 같습니다.
'DB' 카테고리의 다른 글
깃의 개념과 구조, 명령어 사용법(커밋,push, pull, 초기화, 브랜칭 생성 및 머지) (0) | 2021.12.06 |
---|---|
인덱스의 개념(클러스터, 논클러스터, B-tree)과 인덱스 성능 테스트 (0) | 2021.11.07 |
깃 비밀번호 인증 Deprecated, 새로운 방식인 토큰 인증 방식 (0) | 2021.08.22 |
[안드로이드 스튜디오] 깃허브 연동 (깃 커밋 & 푸시) (0) | 2021.06.14 |
[xcode] 깃허브 연동(커밋 & 푸시) (0) | 2021.06.01 |