본문 바로가기
DB

uuid 생성 전략 성능차이 테스트 (sequential uuid, jpa에서 sequential uuid)

by 코딩공장공장장 2022. 4. 23.

https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/#crayon-60fa2fbab27f7557869434

 

Storing UUID Values in MySQL

Karthik Appigatla revisits a post Peter Zaitsev wrote on UUIDs (Universal Unique IDs), rearranging the timestamp and talks about storing UUID Values.

www.percona.com

 

본 글은 위의 글을 참조하여 실제 데이터를 생성하여 테스트 해본 결과를 공유하는 것입니다. 

 

UUID 개념은 구글링의 많은 문서가 있으므로 가볍게 다루고 위 solution을 이해하는데

 

필요한 부수적인 개념들을 중간중간 설명토록 하겠습니다.

 

 

uuid 장점

  1. 쉽게 추측가능한 sequence number와 다르게 보안우수
  2. 테이블에 한정되어있는 sequence number와 다르게 범용적으로 유일성을 보장해주는 방식

uuid 단점

  1. 36글자의 문자열
  2. 속도 이슈

 

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칼럼으로 사용하고자 개념을 알아보았는데 

 

처음보는 개념이라 쉽게 받아들이기 어려워 여기저기 읽히지도 않는 구글의 많은 영문 포스팅을 읽어 보았는데 

 

테스트를 직접해보니 성능단점을 어느정도 극복하고 보안의 우수성이라는 큰 장점으로 사용하기에 좋은 것 같습니다. 

반응형