UUID를 Primary Key로 사용하는 것은 이점이 많습니다.
구조상 중복 발생 확률이 매우 적으며(약 수백조 분의 일 확률),
길이는 일정하고 알파벳과 숫자로만 이루어져 있어 다루기도 쉽죠.
또한, 비즈니스적 요구로 uuid가 사용될 수도 있습니다.
예를 들어, 게시글 id나 주문 id등과 같은 값을 auto_increment가 아닌 uuid로 사용하게 한다면 자연스럽게 외부사용자가 총 생성 횟수를 유추할 수 없게 되고, 악성 사용자가 이전 혹은 다음 id를 쉽게 유추할 수 없도록 할 수 있습니다.
하지만, uuid를 Mysql(Innodb) 환경에서 쓸 때는 Insert, Select, Order등의 연산을 처리할 때 단순 long(integer) 형태의 sequencial pk를 쓸 때보다 생각보다 많은 성능 저하가 있는 것을 염두에 두고 사용해야 합니다.
실제로 제가 수백만건의 레코드를 가지고 있는 서비스를 운영할 당시,
uuid와 그냥 auto_increment를 사용할 때 사람이 인지할 수 있을 정도의 성능차가 있었으며,이를 개선하기 위해 꽤 많은 시간을 할애하였었습니다.
오늘은 이와 관련한 몇가지 테스트를 해보고자 합니다.
uuid를 키로 가지는 테이블의 성능과 auto_increment를 키로 가지는 테이블의 성능 비교를 위해 사용한 테이블의 DDL은 아래와 같습니다.
create table justid_local
(
id int(11) unsigned auto_increment primary key,
title text null,
created_at datetime default CURRENT_TIMESTAMP null
);
create table justuuid_local
(
uuid binary(16) default not null primary key,
title text null,
created_at datetime default CURRENT_TIMESTAMP null
);
100만건 Insert 시간 성능 테스트
1회차 | 2회차 | 3회차 |
---|---|---|
149.514s | 150.595s | 164.657s |
1회차 | 2회차 | 3회차 |
---|---|---|
160.347s | 176.548s | 178.298s |
100만건의 Bulk Sql Insert 작업을 수행하는데,
평균적으로 약 10% 정도의 Insert 성능 차이가 발생합니다. (UUID가 더 느립니다.)
물론 UUID V1을 사용했을 때에는 시간차가 거의 발생하지 않기 때문에,
uuid v4 사용으로 인한 시간차가 크다는 것을 알 수 있습니다.
단순 Hash 기반 Select의 성능 테스트.
SELECT * FROM justid_local WHERE id in (
1, 11, 43, 100, 12, 12000, 234443, 234453, 87000, 123000
);
SELECT * FROM justuuid_local WHERE uuid IN(
UNHEX(REPLACE('087a75e2-12b4-43d8-a9a6-d1a4a26daa1d', '-', '')),
UNHEX(REPLACE('0913c3de-ac18-40f6-8ba5-1cf3d0532a98', '-', '')),
UNHEX(REPLACE('00645bcf-1b50-4ed4-ae63-648421f96b3d', '-', '')),
UNHEX(REPLACE('0086577c-6e86-4f98-918a-f7a4ebfd14e8', '-', '')),
UNHEX(REPLACE('008688ae-fc7f-40bb-a899-0720e8c559c7', '-', '')),
UNHEX(REPLACE('01913cd3-1749-4db1-9077-b2d932a691fa', '-', '')),
UNHEX(REPLACE('02170e81-4c97-43f4-8dd1-e9394dac5ca5', '-', '')),
UNHEX(REPLACE('04cb4ec4-32f6-4c56-946f-d73da40e1e8d', '-', '')),
UNHEX(REPLACE('058a1854-8fb5-4943-b6e3-a1693a065a26', '-', '')),
UNHEX(REPLACE('0777878b-629b-4a85-992c-ea5583321ff5', '-', ''))
);
1회차 | 2회차 | 3회차 |
---|---|---|
42ms | 33ms | 35ms |
1회차 | 2회차 | 3회차 |
---|---|---|
51ms | 48ms | 41ms |
100만건의 컬럼에서 무작위 컬럼 10개를 Select 하는 데에는 25% 정도의 성능차가 발생합니다.
다만, (1)번의 경우와 유사하게 이는 UNHEX
와 Replace
함수 이용으로 인한 성능차 입니다. 물론 유저(클라이언트) 레이어 단에서는 HEX형태보다 String 형태로 UUID가 사용되는 경우가 많기 때문에 꼭 필요한 단계이기도 합니다.
컬럼의 순서와 연관되어 있는 Select관련 성능 테스트
SELECT * FROM justid_local where created_at like '2022%' Limit 3, 300;
SELECT * FROM justid_local where created_at like '2022%' Limit 3, 3000;
SELECT * FROM justid_local where created_at like '2022%' Limit 3, 3000;
SELECT * FROM justid_local where created_at like '2022%' Limit 3, 30000;
SELECT * FROM justid_local where created_at like '2022%' Limit 3, 300000;
SELECT * FROM justuuid_local where created_at like '2022%' Limit 3, 300;
SELECT * FROM justuuid_local where created_at like '2022%' Limit 3, 3000;
SELECT * FROM justuuid_local where created_at like '2022%' Limit 3, 3000;
SELECT * FROM justuuid_local where created_at like '2022%' Limit 3, 30000;
SELECT * FROM justuuid_local where created_at like '2022%' Limit 3, 300000;
1회차 | 2회차 | 3회차 | 4회차 | 5회차 | 6회차 | 7회차 | 8회차 | 9회차 | 10회차 |
---|---|---|---|---|---|---|---|---|---|
826ms | 828ms | 803ms | 982ms | 802ms | 804ms | 814ms | 815ms | 820ms | 787ms |
mean: 828ms
1회차 | 2회차 | 3회차 | 4회차 | 5회차 | 6회차 | 7회차 | 8회차 | 9회차 | 10회차 |
---|---|---|---|---|---|---|---|---|---|
934ms | 901ms | 897ms | 937ms | 918ms | 916ms | 1000ms | 917ms | 936ms | 892ms |
mean : 925ms
위 결과에서 볼 수 있듯 순서와 연관된 부분에서의 성능 차이는 약 11% 정도로,
그 차이가 더 확연하게 나타납니다.
이 차이는 uuid의 값을 이용해 순서와 연관된 sub 쿼리를 작성하였을 때 더욱 눈에 띄게 발생하게 됩니다.
앞서 UUID4 혹은 기타 함수 사용으로 인한 성능 차라고 말한 부분의 경우에도,
거의 대부분의 테스트에서 거의 항상 (3)번 항목과 유사하게 UUID가 더 느린 양상을 보였습니다.
물론, 적은 레코드 수나 적은 부하에서는 거의 차이가 발생하지 않거나 오히려 uuid 가 더 빠른 경우도 있었지만, 최소 수백만건의 레코드가 존재하는 환경이나 복잡하거나 오래걸리는 쿼리를 질의할 때는 최소 50% 확률 정도로 uuid가 더 느렸습니다.
그렇기 때문에 uuid가 auto_increment보다 거의 항상 느리다고 말해도 영 틀리지는 않을 것입니다.
만약 비즈니스적으로 n 번째 이후 컬럼, UUID를 이용한 filter 등의 기능 구현이 필요할 경우, auto_increment를 사용하는 id를 복합키로 사용하거나 auto_increment를 가지는 unique notnull column 를 추가하여 사용하는 것이 성능에 큰 도움이 됩니다.
.sql 파일 형태로 덤프된 500만건의 레코드를 Insert 하는 시간 비교
auto_increment | uuid |
---|---|
23 min, 47 sec, 163 ms | 23 min, 47 sec, 729 ms |
-> 이미 정해진 pk를 bulk로 insert하는 경우에는 성능차가 크지 않습니다.
UUID v1을 사용하는 신규 500만건 Insert 시간 비교
auto_increment | uuid |
---|---|
491.0724868774414 s | 488.1903250217438 s |
-> UUID V1()은 시간을 변수로 하여 UUID를 생성함으로 중복 발생 확률이 높고,
비슷한 시간대에 만들어졌을 경우 아래와 같이 값이 유사하다는 단점이 있어 주의하며
사용해야 합니다.
좋은 글 감사합니다!