DBのプラむマリヌキヌはUUIDか敎数か実際に怜蚌しおみた

※この蚘事は自分が所属する組織で曞いた以䞋の蚘事のコピヌです。投皿した蚘事は個人の著䜜物ずしお自ブログにコピヌしお良いルヌルずしおいたす。

元蚘事: https://tech-blog.mitsucari.com/entry/2026/02/02/112938


こんにちは、ミツカリCTOの塚本こず、぀かびヌ(@tsukaby0) です。

先日、こんなツむヌトが少し話題になっおいたした。

芁玄するず

  • 3000䞇ナヌザヌ・10億APIヒット/日のBaaSでpkeyは意図的に連番だった
  • UUIDによるキャッシュヒット率の䜎䞋は倧芏暡システムでは恐怖
  • 128bit vs 32bitの差は最終的に8倍16倍に増幅される
  • 倖郚公開甚には別途 public_id カラムを䜜るのがおすすめ

こういったpkeyをintegerにするか、UUIDにするかは割ずよく聞く話ではありたす。私は過去アドテクノロゞヌ・Web広告界隈にいたしたが、そのような環境では採番の頻床が䞀般的なSaaSのアクセス量の比ではないので、UUIDが甚いられるこずが倚かったです。ただ、そうではないほずんどのナヌスケヌスではintegerが䞀般的です。

どうしおintegerにすべきか、詳现は元ツむヌトを読んでいただくずしお、知識や経隓ずしおはある皋床わかっおいた぀もりですが、実際にUUIDをpkeyにしお困ったずいうケヌスには遭遇しおいたせん。そのため、UUIDにするずどの皋床䞍利なのか怜蚌しおみるこずにしたした。

結論

  • Kenn Ejimaさんの䞻匵に同意であり、pkeyはintegerでよく、必芁に応じお公開甚のpublic_idを远加すればいい
  • public_idではなく、関数による導出もありだが、倖郚環境を考慮するずpublic_idずしお甚意した方が良い
  • 今回の怜蚌ではinteger vs UUID v7でキャッシュヒット率が 96.93% vs 2.78% ずなった
  • (党䜓的にかなり簡略化した怜蚌方法であり、メモリや実際のク゚リ等にかなり巊右されたす。䞀抂にこれだけ倧きな差が出るわけではないので泚意です)
  • 厳しい環境ではキャッシュヒット率が臎呜的になりうる

怜蚌環境

Dockerコンテナ䞊でPostgreSQLを動かし、ホスト偎からク゚リを実行したす。キャッシュミスを発生させるため、メモリを厳しく制限しおいたす。

コンテナの起動

# CPU 1コア、メモリ 128MB に制限したPostgreSQLコンテナを起動
# デヌタサむズ玄300MBに察しおキャッシュが小さいため、キャッシュミスが発生しやすい環境
#
# PostgreSQLのメモリ関連パラメヌタ:
# - shared_buffers: PostgreSQLがデヌタキャッシュに䜿う共有メモリ領域
# - work_mem: ゜ヌトやハッシュ操䜜で䜿う䜜業メモリク゚リごず
docker run -d \
  --name pg-bench \
  --cpus="1" \
  --memory="128m" \
  -e POSTGRES_PASSWORD=postgres \
  -p 15432:5432 \
  postgres:18 \
  -c shared_buffers=16MB \
  -c work_mem=4MB

# 以降のコマンドで䜿う環境倉数を蚭定
export PGPASSWORD=postgres

テヌブル䜜成

ホスト偎から psql でDBに接続し、怜蚌のセットアップを行いたす。PostgreSQL 18から uuidv7() がネむティブでサポヌトされたので嬉しいですね。

# 接続確認
psql -h localhost -p 15432 -U postgres --pset pager=off -c "SELECT version();"

--pset pager=off は結果をlessなどのペヌゞャヌで衚瀺せず、暙準出力に盎接出力するオプションです。スクリプトでの利甚やログ取埗時に䟿利です。

以䞋のSQLでテヌブルを䜜成したす。

psql -h localhost -p 15432 -U postgres --pset pager=off -f - <<'SQL'
-- integer版
CREATE TABLE users_int (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100),
  email VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);

-- UUID v7版PostgreSQL 18からネむティブサポヌト
CREATE TABLE users_uuid (
  id UUID PRIMARY KEY DEFAULT uuidv7(),
  name VARCHAR(100),
  email VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);
SQL

怜蚌

準備ができたので、デヌタを入れ぀぀怜蚌を開始しおいきたす。

たずはひたすら実行しお、結果の考察は埌述したす。

怜蚌1INSERT性胜

300䞇件のINSERTにかかる時間を蚈枬したす。UUID v7もintegerも時系列順でB-treeに远蚘されるため、ペヌゞ分割の問題は発生したせん。差が出るずすればUUID生成コストずキヌサむズによるむンデックス曞き蟌み量の違いです。なお、UUID v4は時系列でないずいう問題を持っおおり、よりパフォヌマンス面で深刻な問題が発生したすが、今回は怜蚌察象ずしたせん。

# integer版
psql -h localhost -p 15432 -U postgres --pset pager=off -c "\timing" -c "
INSERT INTO users_int (name, email)
SELECT
  'user_' || i,
  'user_' || i || '@example.com'
FROM generate_series(1, 3000000) AS i;
"

# UUID版
psql -h localhost -p 15432 -U postgres --pset pager=off -c "\timing" -c "
INSERT INTO users_uuid (name, email)
SELECT
  'user_' || i,
  'user_' || i || '@example.com'
FROM generate_series(1, 3000000) AS i;
"
方匏所芁時間
integer9,309 ms
UUID v74,888 ms

怜蚌2ストレヌゞサむズ

テヌブルずむンデックスのサむズを比范したす。UUIDは128bit16バむト、integerは32bit4バむトなので、キヌサむズだけで4倍の差がありたす。今回は他にテヌブルを甚意しおいたせんが、実運甚䞊では倖郚キヌなどがあるため、4倍ではなく8倍、16倍ず増えおいく郚分です。

psql -h localhost -p 15432 -U postgres --pset pager=off -c "
SELECT
  relname as table_name,
  pg_size_pretty(pg_total_relation_size(relid)) as total_size,
  pg_size_pretty(pg_relation_size(relid)) as table_size,
  pg_size_pretty(pg_indexes_size(relid)) as index_size
FROM pg_stat_user_tables
WHERE relname LIKE 'users_%'
ORDER BY relname;
"
テヌブルテヌブルサむズむンデックスサむズ合蚈備考
users_int234 MB64 MB298 MB
users_uuid266 MB90 MB357 MB+20%

怜蚌3SELECT性胜単䞀行ランダム取埗

党デヌタぞの均䞀ランダムアクセスを行い、キャッシュ効率の差を怜蚌したす。

シンプルにpsqlでランダムなIDを指定しおSELECTを繰り返したす。UUID版は事前にID䞀芧をファむルに出力し、shufでランダム遞択したす。psql接続のオヌバヌヘッドは䞡者同じなので公平に比范できたす。ただし、psqlを郜床呌び出すのはかなり遅いので、理想的にはpgbenchなどを䜿った方が良いです が今回はずある理由により䜿えないのでこの方法で行きたす。理由は埌述したす。

# UUID䞀芧をファむルに出力
psql -h localhost -p 15432 -U postgres -tAc "SELECT id FROM users_uuid" > /tmp/uuids.txt
# コンテナ再起動でキャッシュクリア
docker restart pg-bench

# 統蚈情報をリセット
psql -h localhost -p 15432 -U postgres -c "SELECT pg_stat_reset();"

# integer版: ランダムIDで10000回SELECT
time for i in {1..10000}; do
  id=$((RANDOM % 3000000 + 1))
  psql -h localhost -p 15432 -U postgres -tAc "SELECT * FROM users_int WHERE id = $id" > /dev/null
done

# integer版のキャッシュヒット率を確認
psql -h localhost -p 15432 -U postgres --pset pager=off -c "
SELECT
  relname,
  heap_blks_hit as hits,
  heap_blks_read as reads,
  ROUND(heap_blks_hit::numeric / NULLIF(heap_blks_hit + heap_blks_read, 0) * 100, 2) as hit_ratio
FROM pg_statio_user_tables
WHERE relname = 'users_int';
"
# コンテナ再起動でキャッシュクリア
docker restart pg-bench

# 統蚈情報をリセット
psql -h localhost -p 15432 -U postgres -c "SELECT pg_stat_reset();"

# UUID版: ランダムUUIDで10000回SELECT
time for i in {1..10000}; do
  uuid=$(shuf -n 1 /tmp/uuids.txt)
  psql -h localhost -p 15432 -U postgres -tAc "SELECT * FROM users_uuid WHERE id = '$uuid'" > /dev/null
done

# UUID版のキャッシュヒット率を確認
psql -h localhost -p 15432 -U postgres --pset pager=off -c "
SELECT
  relname,
  heap_blks_hit as hits,
  heap_blks_read as reads,
  ROUND(heap_blks_hit::numeric / NULLIF(heap_blks_hit + heap_blks_read, 0) * 100, 2) as hit_ratio
FROM pg_statio_user_tables
WHERE relname = 'users_uuid';
"
方匏ク゚リ数ヒット数ディスク読み取りヒット率1ク゚リあたりreads
integer10,0009,69330796.93%0.03
UUID v76,3001806,2862.78%1.00

※ psql経由だずかなり遅いので、UUID v7方匏は途䞭で打ち切りたした。そのため6,300回しかク゚リできおいたせん。ただキャッシュヒット率の蚈算にはそれほど問題ないはずです

クリヌンアップ

怜蚌が終わったらコンテナを削陀したす。

docker stop pg-bench && docker rm pg-bench
rm -f /tmp/uuids.txt

結論ず考察

今回の怜蚌結果をたずめるず

怜蚌項目integerUUID v7å·®
INSERT (300䞇件)9.3秒4.9秒UUIDが速い※埌述
ストレヌゞサむズ298 MB357 MB+20%
キャッシュヒット率96.93%2.78%箄35倍の差
1ク゚リあたりディスク読み取り0.03回1.00回33倍

INSERTでUUIDが速かったのは蚈枬や実行環境の問題がありそうです。今回の怜蚌は䜕回かやり方を倉えながら実斜したのですが、他の実行ではintegerの方が若干速かった20%皋床です。

ストレヌゞサむズは +20% です。UUIDの方が倧きいのは圓たり前ですね。ただ、これは怜蚌方法がよくありたせんでした。nameやemailカラムを入れおしたったので、その分の圱響で正確な差を出せおいたせん。それらの列がない方が良かったですね。

本呜であるキャッシュヒット率には劇的な差が出たした。

UUIDは1ク゚リあたり玄35倍のディスク読み取りが発生しおいたす。 これはKenn Ejimaさんが指摘しおいた「UUIDによるキャッシュ効率の䜎䞋」を裏付ける結果です。

UUIDのキヌサむズ128bitはinteger32bitの4倍あるため、同じペヌゞに栌玍できる行数が少なくなりたす。その結果、ランダムアクセス時にキャッシュに必芁なデヌタが乗っおいる確率が䞋がり、ディスクI/Oが増加したす。

掚奚構成に぀いお

Kenn Ejimaさんのツむヌトでは掚奚の構成が投皿されおいたしたが、これに同意したす。

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,           -- 内郚甚64bit連番
  public_id VARCHAR(16) UNIQUE        -- 倖郚甚random_hex(16)
);

この構成であればツむヌトの通り、シャヌディングを回避しお長く構成を倉えずに維持できるず思いたす。

public_idを関数等で郜床導出するか、固定で入れるか

public_idカラムを甚意せずに、integerのpkeyから䜕らかのハッシュ関数や暗号化で郜床public_idを導出する方法もありたす。

しかし、DBにpublic_idカラムずしお倀を持たせる方が実務䞊は䟿利です。

  • BIツヌルやデヌタ分析基盀での利甚: Metabase、Redash、BigQueryなどでデヌタを参照する際、関数で導出する方匏だずツヌル偎で同じ関数を実装する必芁がありたす。カラムずしお存圚すれば単玔なJOINやフィルタで枈みたす。
  • 倖郚システム連携: Webhook、API連携、ログ解析などで倖郚システムがpublic_idを参照する堎合、DBに倀があればSQLで盎接怜玢できたす。
  • デバッグのしやすさ: 本番障害時に「このpublic_idのナヌザヌを調べたい」ずなった時、カラムがあれば即座にク゚リできたす。
  • むンデックスの効率: カラムに察しおむンデックスを貌れば、public_idでの怜玢も高速です。関数むンデックスも可胜ですが、管理が耇雑になりたす。

導出方匏だず䞍䟿な具䜓䟋ずしお、以䞋のようなシナリオが考えられたす実際に近いものを䜓隓しおいたす。

  • 営業「ID public_id=foo のお客様からバグの問い合わせがあったのですが、確認お願いできたすか」
  • 開発者1「分かりたした、調べたす。えヌず fooっおどのIDだっけ・・・デコヌド関数を実行しないず・・・」
  • PdM「GA4のデヌタでUserID public_id=barを蚘録しおいるず思うのだけど、そのナヌザヌのDBのデヌタずGA4のデヌタをJOINしお分析するレポヌトをBI環境に䜜っおください」
  • 開発者2「分かりたした。frontendにはpublic_idを返しおいるからidは返しおいないんだよね。BIにあるDB等のデヌタをJOINしないず ああ、BI環境でdecodeしないず・・・」
  • 埌日
  • 開発者12「KPTでこの問題を共有したす調査やデバッグがやりづらいです」
  • 開発者3「匊瀟の運甚管理システム䞊でdecodeする機胜を䜜ろうあず、いちいちdecodeしなくおいいようにpublic_idを衚瀺する機胜を䜜ろうBI環境はBIにデヌタを入れる前のETLでdecodeする凊理を入れよう」

想像するず面倒ですね。初めからpublic_idがあれば解決する話です。

UUIDを䜿う堎面

冒頭でWeb広告ずいう話をしたしたが、UUIDがpkeyでも良いケヌスは䞀定存圚したす。䟋えば関連ツむヌトで以䞋が参考になりたす。

必ずしもUUID pkeyが悪ずいうわけではないですが、ほずんどのナヌスケヌスでは䞍芁なので、もし利甚する堎合は同僚に説明したり、コヌドにコメントを残しおなぜUUIDが必芁なのかを明蚘するず良いでしょう。

怜蚌で苊劎したこず

今回の怜蚌では、「キャッシュヒット率の差」を明確に瀺すたでに䜕床も方法を倉曎したした。あたり誰かの圹に立぀気はしたせんが、䞀応苊劎した点を蚘茉しおおきたす。

1. メモリ蚭定の調敎

最初は memory=512MB や memory=256MB で実行しおいたのですが、OSのペヌゞキャッシュが効きすぎおキャッシュヒット率が䞡者ずも100%になっおしたいたした。次に memory=64MB, shared_buffers=16MB で詊したしたが、OOMが発生したした。最終的に memory=128MB, shared_buffers=16MB に萜ち着きたした。

こういうケヌスもあるので、我々が保守するDBで䞍芁にUUID pkeyが䜿われおいたずしおも、臎呜的でないケヌスもあり、無芖しおも良い堎合もあるず思いたす。

2. pgbenchの制玄

pgbenchはPostgreSQL暙準のベンチマヌクツヌルで、カスタムSQLスクリプトを高速に繰り返し実行できたす。integer版では \set id random(1, 3000000) のように倉数にランダムな敎数をセットできるため、玔粋なPKルックアップの性胜を簡単に枬定できたす。

しかし、pgbenchには倖郚ファむルから倀を読み蟌む機胜がありたせん。UUID版では300䞇件のUUID䞀芧から1぀を遞んで倉数にセットしたいのですが、これができたせん。

そのため、pgbenchを諊めシンプルにpsqlでルヌプする方匏に倉曎したした。

  • integer版: $((RANDOM % 3000000 + 1))で盎接ID生成
  • UUID版: 事前にファむル出力したUUID䞀芧からshufでランダム遞択

psql接続のオヌバヌヘッドは倧きいですが、䞡者同条件なので公平に比范でき、玔粋なPK怜玢のキャッシュ効率を蚈枬できたした。

おわり

今回の怜蚌は完璧ではないです。䟋えば実際にはデヌタに察しお満遍なくランダムアクセスされるケヌスよりは、䞀郚のデヌタにアクセスが集䞭するケヌスが考えられたす。そのため、UUIDでもキャッシュヒット率が問題にならないケヌスはありたす。たた、そもそもアクセスは倚いがデヌタ量が少なく、十分にキャッシュヒットする可胜性もありたす。より厳密な怜蚌をする堎合は、今回のような郜床psqlコマンドを実行する方法ではなく、䜕らかのシナリオベヌスの負荷テストツヌルを䜿っお怜蚌した方が良いでしょう。

今埌においおも問題ない堎合は無理に今あるUUIDカラムをintegerカラムに倉える必芁はないでしょうが、新しくシステムを䜜る堎合は基本的には特別な理由がない限りはUUIDは避けるべきず蚀えるでしょう。

今回の怜蚌のおかげで、この知識は私に刻たれたず思うので、今埌も自信を持っおintegerのpkeyをベヌスにしお行きたいず思いたす。

珟圚、ミツカリではIT゚ンゞニアを募集しおいたす。興味のある方はぜひお気軜にご連絡ください