IDを少しだけ語る

// Infrastructure

データベース設計において、レコードを一意に識別するIDの選定は重要な検討事項です。IDの生成方式には多様な選択肢があり、それぞれ特徴があります。この記事では、代表的なID生成方式の特徴と、それらが適している使用ケースについて少しだけ解説します。

そもそもIDとは?

IDとは、「Identifier(識別子)」の略語で、データベース内の各レコードを一意に識別するための値です。ユーザーID、オーダーID、商品IDなどがその具体例です。IDは、データベース内で重複することなく、それぞれのレコードを確実に区別できなければなりません。 同じテーブル内のIDはすべて一意である必要があるため、重複しない(または重複する可能性が極めて低い)IDを生成する方法が必要です。

代表的なIDの生成方式

  • シーケンシャルID (Auto Increment)
  • UUID
  • CUID
  • Snowflake ID
  • Object ID

シーケンシャルID (Auto Increment)

シーケンシャルIDは、最もシンプルで直感的なID生成方式の一つとして広く知られています。このIDは、データベースにおいて1から始まる連続した整数値を使用し、新しいレコードが追加されるたびに自動的に1ずつ増加していく仕組みを採用しています。例えば、最初のレコードには1が、次のレコードには2が、その次には3が割り当てられるという具合に、順序性を持って増加していきます。

シーケンシャルIDの基本的な流れ

また、実際のデータベーステーブルでの使用例を図示すると、以下のようになります。

ID (Auto Increment)ユーザー名登録日時
1田中太郎2025-01-01
2鈴木花子2025-01-02
3佐藤次郎2025-01-03
4山田優子2025-01-04

メリット

単純で理解しやすい

シーケンシャルIDは、その名前が示す通り、連続した数値を使用する非常にシンプルな仕組みです。開発者にとって直感的に理解しやすく、デバッグやトラブルシューティング時にも問題の特定が容易です。また、人間が目で見て順序関係を把握しやすいという利点もあります。

整数なので扱いやすい

整数型のIDは、多くのプログラミング言語やデータベースシステムで最も基本的なデータ型として扱われています。メモリ使用量が少なく、比較や演算が高速で、ソートも容易です。また、URLやAPIエンドポイントでの使用にも適しており、人間が読み書きしやすい形式となっています。

データベースのインデックスとして効率的

シーケンシャルIDは、データベースのインデックスとして非常に効率的に機能します。連続した整数値であるため、B-treeインデックスの構造に最適で、検索や範囲クエリのパフォーマンスが優れています。また、新しいレコードが常に最後に追加されるため、インデックスの断片化も最小限に抑えられます。

デメリット

分散システムでは競合は発生しやすい

分散システムやマイクロサービスアーキテクチャでは、複数のサーバーやデータベースインスタンスが同時にIDを生成する必要があります。このような環境では、シーケンシャルIDの生成時に競合が発生しやすく、一意性を保証することが困難になります。また、データベースの同期や複製においても、ID重複のリスクが高まります。

連番から作成順序が予測可能

シーケンシャルIDは連番であるため、レコードの作成順序が容易に推測できてしまいます。これはビジネスデータの機密性を損なう可能性があり、特にユーザー数やトランザクション数などの機密情報が推測可能になってしまう問題があります。また、競合他社による分析や、システムの脆弱性を突いた攻撃のリスクも高まります。

他のシステムとの統合時に重複の可能性がある

複数のシステムを統合する際、それぞれのシステムが独自のシーケンシャルIDを使用している場合、ID体系の統合が困難になります。例えば、異なるシステムで同じIDが使用されていた場合、データの統合時に重複が発生し、一意性が損なわれる可能性があります。また、システム間でIDの範囲を調整する必要が生じ、運用の複雑性が増加する要因となります。

使用に適したケース

シーケンシャルIDは、以下のようなケースで特に有効な選択肢となります。

  • 小規模な単一システムでの使用(例:ローカルアプリケーション、スタンドアロンのウェブサイトなど)
  • 順序性が重要なデータの管理(例:注文番号、請求書番号)
  • 高いパフォーマンスが要求される読み取り操作が多いシステム

UUID

UUID(Universally Unique Identifier)は、128ビットの長さを持つグローバルに一意な識別子です。UUIDは、時刻やランダムな値を組み合わせて生成され、異なるシステム間でも重複する可能性が極めて低い特徴を持っています。一般的なUUIDは、8-4-4-4-12文字の16進数で表現され、例えば「123e4567-e89b-12d3-a456-426614174000」のような形式となります。

UUIDには複数のバージョンが存在し、それぞれ異なる生成アルゴリズムを採用しています。最も一般的なのはバージョン4で、これはランダムまたは擬似ランダムな値を使用して生成されます。また、バージョン1は、生成時のタイムスタンプとノードIDを組み合わせて作成されます。

バージョン特徴
Version 12ed6657a-e927-11ed-9b5b-d7a2292d2e51タイムスタンプベース
Version 4550e8400-e29b-41d4-a716-446655440000ランダム生成
Version 5716c1eef-f73d-5c5d-95c4-8bdd9e33c749名前ベース (SHA-1)

メリット

グローバルな一意性

UUIDの最大の利点は、異なるシステムやデータベース間でも、生成されたIDが重複する可能性が極めて低いことです。これにより、分散システムやマイクロサービスアーキテクチャにおいても、中央調整なしで安全にIDを生成できます。

Version 4 UUIDの場合、1秒間に10億個のUUIDを生成し続けた場合でも、100年間で衝突が発生する確率は約50% とされています。これは実用上、ほぼ衝突が起きないと考えて問題ないレベルです。具体的な数値で表すと、Version 4 UUIDの場合、2¹²⁸(約3.4 x 10³⁸)個の異なる値を生成可能です。この数値の大きさを考えると、一般的なアプリケーションやシステムでUUID同士が衝突する可能性は、実質的にゼロと見なすことができます。

分散システムとの親和性

UUIDは分散システムでの使用に最適です。各サーバーやアプリケーションインスタンスが独立してIDを生成でき、同期や調整の必要性がありません。これにより、システムのスケーラビリティが向上し、運用の複雑さも軽減されます。

セキュリティ面での利点

UUIDはランダムな値を含むため、シーケンシャルIDと比較して、レコードの作成順序や総数を推測することが困難です。これにより、ビジネス情報の機密性が向上し、セキュリティリスクを軽減できます。

デメリット

サイズが大きい

UUIDは128ビット(一般的な文字列表現で36文字)と、整数型のIDと比較してサイズが大きくなります。これにより、ストレージ容量の増加や、インデックスのパフォーマンスへの影響が懸念されます。

可読性が低い

UUIDは長い16進数の文字列であり、人間にとって読みづらく、記憶しにくい形式です。デバッグやトラブルシューティング時に、IDの目視確認や手動での操作が困難になる可能性があります。

インデックスの効率が低下する可能性

UUIDはランダムな値であるため、B-treeインデックスにおいて効率的な構造を維持できない可能性があります。これにより、データベースのパフォーマンスに影響を与える可能性があります。

使用に適したケース

UUIDは、以下のようなケースで特に有効な選択肢となります・

  • 分散システムやマイクロサービスアーキテクチャ
  • データの同期や統合が必要なシステム
  • 高いセキュリティが要求される環境
  • 大規模なデータ移行や統合が予想されるシステム

CUID

CUID(Collision-resistant Unique IDentifier)は、衝突耐性を持つユニークな識別子を生成するために設計された比較的新しいID生成方式です。UUIDと同様にグローバルな一意性を持ちながら、より短い文字列長と優れたパフォーマンス特性を提供することを目的としています。

CUIDは、タイムスタンプ、カウンター、ランダム値などの要素を組み合わせて生成され、一般的に20-25文字程度の長さになります。例えば「ch72gsb320000udocl363eofy」のような形式となります。

メリット

効率的なサイズと性能

CUIDは、UUIDと比較してより短い文字列長を実現しながら、一意性を保証します。これにより、ストレージ効率が向上し、データベースのパフォーマンスも改善されます。

モノトニック性

CUIDは時間的な順序性を持つように設計されており、データベースのインデックス効率を最適化します。これにより、B-treeインデックスの効率的な構造が維持されます。

分散システムでの信頼性

複数のシステムやサーバーで同時に生成しても衝突する可能性が極めて低く、分散システムでの使用に適しています。

デメリット

比較的新しい技術

UUIDと比較して歴史が浅く、広く採用されているとは言えない状況です。そのため、ライブラリのサポートや実績が限定的である可能性があります。

標準化されていない

UUIDのような標準規格ではないため、実装によって生成されるIDの形式や特性が異なる可能性があります。

使用に適したケース

CUIDは、以下のようなケースで特に有効な選択肢となります。

  • 高いパフォーマンスが要求される分散システム
  • UUIDのサイズが問題となるケース
  • 順序性と一意性の両方が重要なアプリケーション
  • モダンな開発環境での新規プロジェクト

Snowflake ID

Snowflake IDは、Twitterが開発した分散システムで使用される一意な識別子生成方式です。64ビットの整数値として表現され、タイムスタンプ、ワーカーID、シーケンス番号などの情報を組み合わせて生成されます。

Snowflake IDの構造は以下のように分解されます。

  • 41ビット: タイムスタンプ(ミリ秒単位)
  • 10ビット: マシンID(データセンターIDとワーカーID)
  • 12ビット: シーケンス番号
ビットフィールドビット数例の値説明
タイムスタンプ41ビット17050584140002024-01-12 22:20:14 UTC
マシンID10ビット42サーバー/ワーカーの識別子
シーケンス12ビット123同一ミリ秒内の連番

これらの要素を組み合わせると、最終的なSnowflake IDは「7944731075280576635」のような64ビット整数として表現されます。

生成例

以下がSnowflake IDの生成例です。

package main

import (
    "fmt"
    "sync"
    "time"
)

type Snowflake struct {
    mutex       sync.Mutex
    sequence    int64
    lastTS      int64
    machineID   int64
}

func NewSnowflake(machineID int64) *Snowflake {
    return &Snowflake{
        machineID: machineID & 0x3FF, // マシンIDは10ビットを使用
    }
}

func (s *Snowflake) Generate() int64 {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    // 現在のタイムスタンプをミリ秒単位で取得
    ts := time.Now().UnixNano() / 1000000

    // システム時計が巻き戻された場合はエラー
    if ts < s.lastTS {
        panic("システム時計が巻き戻されました!")
    }

    // 同じミリ秒内の場合、シーケンス番号をインクリメント
    if ts == s.lastTS {
        s.sequence = (s.sequence + 1) & 0xFFF // シーケンス番号は12ビットを使用
        if s.sequence == 0 {
            // シーケンス番号が枯渇した場合、次のミリ秒まで待機
            ts = s.waitNextMillis(ts)
        }
    } else {
        s.sequence = 0
    }

    s.lastTS = ts

    // IDの生成:
    // - 41ビット: タイムスタンプ
    // - 10ビット: マシンID
    // - 12ビット: シーケンス番号
    id := (ts << 22) | (s.machineID << 12) | s.sequence
    return id
}

func (s *Snowflake) waitNextMillis(ts int64) int64 {
    for ts <= s.lastTS {
        ts = time.Now().UnixNano() / 1000000
    }
    return ts
}

func main() {
    // マシンID 1で新しいSnowflakeインスタンスを作成
    sf := NewSnowflake(1)

    // IDを生成してみる
    id := sf.Generate()
    fmt.Printf("生成されたID: %d\n", id)
}

このコードは、Goで実装したSnowflake IDジェネレーターの例です。machineIDを指定してインスタンスを作成し、Generate()メソッドを呼び出すことで一意のIDを生成できます。ミューテックスを使用してスレッドセーフな実装となっており、分散システムでの使用を想定しています。

メリット

時系列での順序性

タイムスタンプがIDの一部となっているため、生成されたIDは時系列で自然に並びます。これにより、時系列でのデータ取得や並び替えが効率的に行えます。

コンパクトなサイズ

64ビットの整数値として表現されるため、UUIDと比較してストレージ効率が高く、インデックスのパフォーマンスも優れています。

高いスループット

1ミリ秒あたり4096個(12ビットのシーケンス番号分)のIDを生成できるため、高負荷な環境でも十分な性能を発揮します。

デメリット

時刻同期の重要性

システム間の時刻同期が重要で、時刻が逆行すると一意性が保証できなくなる可能性があります。また、システムの時刻が大きく遅れている場合、生成されるIDが過去の値となってしまい、データの整合性に影響を与える可能性があります。このため、NTPなどを使用した適切な時刻同期の仕組みが不可欠です。

設定の複雑さ

マシンIDの割り当てや管理に中央管理が必要で、これらの設定と運用には注意が必要です。

使用に適したケース

Snowflake IDは、以下のようなケースで特に有効な選択肢となります。

  • 大規模な分散システム
  • 高いスループットが要求されるアプリケーション
  • 時系列データの管理が重要なシステム
  • 効率的なストレージ利用が必要なケース

Object ID

Object IDは、MongoDBで使用される12バイトの一意識別子です。これは、タイムスタンプ、マシン識別子、プロセスID、カウンターを組み合わせて生成されます。

Object IDの構造は以下のように分解されます。

  • 4バイト: UNIXエポックからの秒数
  • 3バイト: マシン識別子
  • 2バイト: プロセスID
  • 3バイト: カウンター

以下は、Object IDの構造と実際の値の例です。

フィールドバイト数例の値説明
タイムスタンプ465A14A8E2024-01-12 22:20:14 UTC
マシン識別子3ABC123ホストのハッシュ値
プロセスID21234MongoDBプロセスのPID
カウンター3FFFFFFインクリメンタルな値

これらのフィールドを組み合わせることで、「65A14A8EABC1231234FFFFFF」のような24文字の16進数文字列として表現されます。この形式は、MongoDBのドキュメントを一意に識別するための標準的な方式として広く使用されています。

メリット

自動生成と一意性の保証

MongoDBのドライバーによって自動的に生成され、分散システムでの一意性が保証されます。

時系列での順序性

タイムスタンプが含まれているため、生成時刻に基づいた自然な順序付けが可能です。

コンパクトな表現

12バイトという比較的小さいサイズながら、十分な情報を格納できます。

デメリット

MongoDB依存

MongoDBに特化した設計であるため、他のデータベースシステムでの使用には適していない場合があります。

可読性の低さ

16進数表現のため、人間にとって読みづらく、デバッグや運用時の可読性が低くなります。

使用に適したケース

Object IDは、以下のようなケースで特に有効な選択肢となります。

  • MongoDBを使用するアプリケーション
  • 自動生成されるIDが必要なケース
  • 分散システムでの一意性が重要な場合
  • 時系列データの管理が必要なシステム

それぞれのIDについてまとめる

以上で見てきた各ID生成方式には、それぞれ特徴的な性質があり、使用するケースによって適切な選択が異なります。以下の表では、主要なID生成方式の特性を比較し、その違いを明確にしています。これにより、システムの要件に応じた最適なID生成方式の選定が可能となります。

特性シーケンシャルIDSnowflake IDObject IDUUIDCUID
サイズ通常64ビット整数64ビット整数12バイト(96ビット)16バイト(128ビット)25文字
順序性完全な順序性あり時系列での順序性あり時系列での順序性ありなし時系列での順序性あり
生成方式自動インクリメントタイムスタンプ + マシンID + シーケンスタイムスタンプ + マシンID + プロセスID + カウンターランダム値または時間ベースタイムスタンプ + カウンター + フィンガープリント
分散システム向け×
スループット低~中高(1ms/4096個)
主な用途単一データベース、小規模システム大規模分散システムMongoDBを使用するシステム汎用的な一意識別子分散システム、ウェブアプリケーション
メリットシンプル、実装が容易高スループット、コンパクト自動生成、一意性保証衝突確率が極めて低い、標準規格衝突回避、順序性、URL安全
デメリットスケーラビリティの制現時刻同期が重要MongoDB依存、可読性低サイズが大きい、順序性なし比較的新しい規格

まとめ

本記事では、代表的なID生成方式について、その特徴と使用ケースを見てきました。シーケンシャルIDの単純さから、SnowflakeやObject IDの分散システム向けの特性まで、それぞれのIDタイプには固有の長所と短所があります。システムの要件、規模、そして運用環境を考慮しながら、最適なID生成方式を選択することが重要です。

P.S.

最近Snowflakeを採用したので、ついでに記事にしたまで。何番煎じだろうか?

参考文献