sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

ID (UUID, GUID) の利用についての学習メモ

特に SQL などで、利用するユニークな採番(各データに番号を割り当てるプロセス)で、いわゆる ID を利用することがあります。

一般的には(単純な DB では)primary key に整数型で auto increment の設定を選択すると、DB 側で自動的に採番してくれるので、アプリケーションはあまり気にすることがありません。

ただし、この方法だと挿入と更新の操作は冪等 (idempotent) ではなくなるケースが発生することがあります(すべてのケースで起こるわけではない)。

冪等(べきとう):ある操作を1回行っても複数回行っても結果が同じであることをいう概念

冪等の例

具体的な例をとして、DB に採番を任せた場合を挙げることにします。関連する2,3つのテーブルにも同時にデータを入れたいとします。

ある人物の情報を記録する DB があるとします。User テーブルに(主たる)データを挿入して、Address テーブルにもデータを挿入する場合、User テーブルに登録したときのデータにある primary key が Address テーブルの登録にも必要ではないでしょうか。(または、参照したい)

CREATE TABLE Users (
    UserID INT AUTO_INCREMENT PRIMARY KEY,
    UserName VARCHAR(100) NOT NULL
);

CREATE TABLE Addresses (
    AddressID INT AUTO_INCREMENT PRIMARY KEY,
    UserID INT NOT NULL,
    Address VARCHAR(255) NOT NULL,
    FOREIGN KEY (UserID) REFERENCES Users(UserID)
);

-- Step 1: Users テーブルに新しいユーザーを挿入
INSERT INTO Users (UserName) VALUES ('John Doe');

-- Step 2: 採番された UserID を取得
SET @NewUserID = LAST_INSERT_ID();

-- Step 3: Addresses テーブルに関連するアドレスを挿入
INSERT INTO Addresses (UserID, Address) VALUES (@NewUserID, '123 Main St');

Step 2 のように採番された結果を待って ID を受け取り、残りのテーブルに割り振られた ID を利用して挿入の指示をしなくてはいけません。そのため、冪等ではなくなるケースがあります。

このケースでは、1つ目のテーブルで ID を採番したデータを挿入し、2つ目のテーブル Addresses にデータを挿入したタイミングで、なんらかのエラーが発生したとします。

1つ目の採番をしたデータはすでに登録しているはずなので、データの矛盾が生じてしまいます。このエラーの対処方法としては、トランザクション処理とロールバックが必要になったり INSERT ON DUPLICATE KEY UPDATE の書き方をしたりしますが、DB によって書き方が違ったり、冪等な処理の構文に非対応だったり、アプリケーションで採番しないことで、かえって複雑になってしまうことがあります。

一人で作っていると矛盾は作りづらいかもしれないけど、DB とアプリケーション側で構築する人が違ったりすると、すり合わせがめんどくさいことになる(ことがある)

結局、アプリケーション側で primary key を生成しておいたほうが単純だったよね、っていうケースはあります。

ただし、ケースバイケースの話であって、UUID だとパフォーマンスが低下する恐れがあります。DB が UUID を文字列のように扱うことになると INTEGER 型と比較して計算コストが高くなることがあります。SQLite では INTEGER PRIMARY KEY だと内部的に「ROWID」になっています。(参考「ROWIDs and the INTEGER PRIMARY KEY」なので、UUID を利用すると計算コストは高くなる可能性がある)

UUID と GUID

UUID は universally unique identifier のこと。なので、用語の示す意味的にも ID と同じようにふるまえることがわかります。基本はユニークな識別子です。

GUID とは Globally Unique IDentifier のこと。Microsoft Learn で「NewGuid」の説明を読んでみます。

The method creates a Version 4 Universally Unique Identifier (UUID) as described in RFC 4122, Sec. 4.4.

なので名前から UUID と GUID はすこし違うモノのように見ますが、実際は(現在は)UUID を代替する名前として GUID が使われています。(DB などでも GUID という用語が利用されることがあります)

C# の GUID の生成

Guid uuid = Guid.NewGuid();
Console.WriteLine(uuid.ToString());
c33db100-0779-4192-91af-043516a343d1

NanoID

UUID で基本的にはいいんだけど ID について調べていると nanoid はわりと知っていてもよいと思いました。

   public void Run(string[] args)
    {
        var defaultID = Nanoid.Generate();
        var shortID = Nanoid.Generate(size:10);

        Console.WriteLine($"ID: {defaultID}, shortID: {shortID}");

        var id1 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id2 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id3 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id4 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);
        var id5 = Nanoid.Generate("23456789" + "CFGHJMPQRVWX", size: 8);

        Console.WriteLine($"ID: {id1}");
        Console.WriteLine($"ID: {id2}");
        Console.WriteLine($"ID: {id3}");
        Console.WriteLine($"ID: {id4}");
        Console.WriteLine($"ID: {id5}");
    }

nanoid のよいところは、文字列の長さを指定したり、ID に使用できる文字を指定したりできるところです。なので、実際のユニークな ID のほかに、表示用の簡易的なユニーク ID を用意するときなんかは便利だと思います。

表示用の ID を用意する理由としては、人の読みづらい文字を回避した ID を用意すること。まぎらわしい文字、たとえば I 1 l みたいな違いは人の目には微妙ってこと。

一例として、OPEN LOCATION CODE などがあります。

TODO

今年の .NET 9 で UUID 7 がサポート予定になっているようです。

これは、気になるので加筆したい。

参考