MySQLのPrimaryKeyでUUIDv4を利用するのはやめた方がいい

# はじめに
「MySQLのPrimaryKeyはUUIDv4じゃなくて、UUIDv7とかULIDを使った方がいい」と耳にしました。
本記事では、UUIDv4とUUIDv7の違いを踏まえ、MySQLのデータ保存構造との相性について解説します。

# UUIDとは
UUID（Universally Unique IDentifier）は、128ビットの値で**空間と時間を超えた一意性**を保証する識別子であり（[RFC9562](https://datatracker.ietf.org/doc/html/rfc9562)）、被りの無い値を用いたい時に利用します。
2025年5月現在 v1-v8までの種類が存在し、主にUUIDv4とUUIDv7が広く用いられています（他のバージョンは下位互換だったり実験的な状態だったり）。
#### こんな値
```text
3494f92e-7c4f-9220-36b1-cda6df05255c
721a246e-a7fb-edd1-f45a-a86ce9214913
dfdcf7a6-6849-f547-147e-abbc7f2f7d1b
```

### UUIDv4
完全にランダム（または疑似乱数）で生成されるUUID
### UUIDv7
タイムスタンプを表すフィールドを含むUUID
最初の12桁が時間を表すフィールドになっており、1970年1月1日午前0時（UTC）からの経過ミリ秒数を16進数で表しています

![uuidv7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974522/ad36160a-73a9-4fc1-9618-c0013b3ce023.png)

:::note info
UUIDv4: 完全ランダム
UUIDv7: 時系列項目を含む
:::

## UUIDをPrimaryKeyに用いるモチベーション

MySQLでは一般的にAutoIncrementがPrimaryKey(PK)に用いられますが、UUIDには以下のような利点があります。

<details>

<summary>PKがUUIDの場合とAutoIncrementの場合の例</summary>

#### IDをAutoIncrementで生成した場合
| ID | Name | Price|
|:-:|:-:|:-:|
|  1 | みかん  | 100  |
|  2 |  りんご |  200 |
|  3 |  いちじく |  800 |

#### IDをUUIDで生成した場合
| ID | Name | Price|
|:-:|:-:|:-:|
| d837cb33-0462-b68b-40ae-5e1b4d4df04e  | みかん  | 100  |
|  976a3cd6-8860-de34-5285-76b8ad2b420b |  りんご |  200 |
|  ae648c87-029e-0100-1e15-3099e7180848 |  いちじく |  800 |

</details>



#### 1. 余計な情報が洩れにくい
例えばUserIDがAutoIncrementの場合、もしそのIDをユーザーが確認できると、自分が何番目のユーザーであるか（このサービスは現在何人が利用しているか）を認識することができてしまいます。
UUIDであれば明確な順序はないためこのような心配が軽減されます。

#### 2. テーブルの統合が容易
複数のテーブル（例：UserAとUserB）を後から結合する際、Auto IncrementだとID衝突の懸念がありますが、UUIDならほぼ衝突がありません。

#### 3. アプリケーション側でPKの値を決定できる
AutoIncrementは基本的にDBにレコードが生成される際に決定されます。その一方で、（UUIDに限りませんが）アプリケーション側でPKを決めることができれば、親レコードをデータベースへ保存する前に、そのレコードのPKを子レコードに設定することができます。

# MySQLのデータ保存方法がUUIDv4と相性が悪い
UUIDv4をDBのPKとして利用すべきでない理由は、MySQLのデータの保存方法と相性が悪いためです。
## MySQLのデータの保存方法
MySQLのストレージエンジンでは、データは「ページ」と呼ばれる単位で保存され、PKをソート順に並べたクラスタインデックスで管理されます（下図参照）。そして、データの読み込みはページ単位で行われ、書き込み時には対応するページに追加／更新し、容量オーバー時にはページ分割（スプリット）を行います。

:::note info
MySQLは以下の特徴を持つ
- PrimaryKeyの順番でデータを保存
- データの読み書きはPrimaryKeyの範囲（ページ）毎に行う
:::


![clusterdIndex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974522/002ece0e-1fc5-481b-81b2-f5a0b4439342.png)


## UUIDv4とUUIDv7の性能比較

MySQLには上記のような特徴があるため、INSERTの際に、UUIDv4（完全ランダム）であれば複数のページに分散して書き込み、UUIDv7（時系列性のあるランダム）であれば、ほぼ同じ範囲のページに書き込むことができます。
従って、**PKはUUIDv4ではなくUUIDv7を利用した方がIO効率が高い**のです。

![無題のプレゼンテーション(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974522/43f4702a-f7f9-49ce-a554-a69afd05d033.png)


### どのくらい効率が変わるのか？
[こちらの記事](https://kccoder.com/mysql/uuid-vs-int-insert-performance/)によると、約2800万のレコード挿入では、UUIDv4を用いた場合、UUIDv7に比べ70時間長く実行時間がかかるようです（下図）


```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
footer {
  FontColor black
  FontSize 17
}
</style>
footer uuidをUUIDv4, int(manual increment)をUUIDv7と読み替える
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974522/6f6a6806-38f9-4e4d-8196-1e133ce44414.jpeg>
@enduml
```

# まとめ
UUIDv4はMySQLのデータ保存方法とマッチしておらずクエリパフォーマンスが低下する点で利用を控えるべきであり、代わりに時系列性を持つ識別子であるUUIDv7（やULID）を利用するのがおすすめです。

# 参考文献

https://medium.com/@ciro-gomes-dev/uuidv4-vs-uuidv7-vs-ulid-choosing-the-right-identifier-for-database-performance-1f7d1a0fe0ba

https://zenn.dev/praha/articles/3c84e3818891c3

https://zenn.dev/reiwatravel/articles/9ce1050bf8509b

https://terra-drone.net/tech/postgresql%E3%81%A7auto-increment-vs-uuid-vs-ulid%E3%81%AE%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E6%AF%94%E8%BC%83/