
特に 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 などがあります。
UUID 7(旧:TODO)
今年の .NET 9 で UUID 7 がサポート予定になっているようです。
これは、気になるので加筆したい。2025年04月27日加筆
Console.WriteLine(Guid.CreateVersion7());
予定通り実装されました。
特徴を整理:
- UUIDv4 と同じように安全なランダム性を持つ
- タイムスタンプが先頭にあるので、UUIDを生成順にソートすると時間順になる
- UUIDv7は、最初の48ビットをミリ秒単位のUnixタイムスタンプ
- 次の6ビットをバージョンとバリアント
- 残りの74ビットは乱数
なので、実はタイムスタンプを持っているので、タイムスタンプを取り出すことも可能みたいです。
データの並びがわかりやすくなるので便利だと思っていたけど、確かにそうかと思った。