サロゲートキーの扱いに終止符を打ちたい - 日々量産

サロゲートキーの扱いに終止符を打ちたい

データベーススペシャリストに落ちまくってるので権威性ゼロです。こんにちわ。誰も扱いに困ってないと思いますが、自分の中で決着がついたので。

いつものように散らかってます。すんません。書きなおす気力ないです。

要約: サロゲートキーを使うか・使わないかではない。必要なときは使えばよい

サロゲートキーがあってもよいケース

(というか、参考にした個人的にDBに強そうな方はちゃんと理由があってサロゲートキーを使え/使うなといっており、誰もサロゲートキーを常に使えとは言ってなかったでござる)

要はバランスだよね。困ることは大体これ!

書いたきっかけとしては、昔よくサロゲートキーを付けようみたいな話やフレームワークサロゲートキーありきの設計になっていて余計なものは付けたくない派としてはモヤモヤしてたのでモヤモヤを払いたかったからです。なのでサロゲートキーに対しては否定的な人が書いてるんだと思ってお読みください。

以降は駄文です。「サロゲートキーに関する様々な意見」以外は読むと疲れるだけな気がします。

サロゲートキー

言葉として、代理キー(Alternate key)や代替キー(Surrogate key)とも呼ばれることがあるかもしれませんが、一応区別します。今回はAlternate keyについては触れません。

サロゲートキーとは

わかりやすいのでこちらをどうぞ(記事では連番としているが、連番で無くてもユニークなIDなら良いとは思う)

wa3.i-3-i.info

メリット

  • レコードが1カラムで特定できるようになる
    • 複合キーを主キーとしなくてよくなる
    • INが使える。 WHERE pk IN (SELECT foreign_key FROM ...) みたいな記述ができる
    • SELECT/UPDATE/DELETEで簡単にアクセスできる
  • 比較するカラムが減るのでクエリが効率的になる

デメリット

  • カラムが増える
  • ストレージを消費する
    • サロゲートキーの分だけカラムが増えるのでレコードあたりのサイズが増える
    • サロゲートキーのインデックスの分だけ余分に必要(たいていはユニーク制約も欲しいので候補キーにユニークインデックスを貼ることになる)
  • 計算コストが増える
  • サロゲートキーは内部の都合で払い出すので内部でしか通用しない(それはそれでよい)

ただ、リソース消費については、昨今のマシンパワーを考えれば些細な事といえるかもしれません。デカすぎるテーブルは知らない。

DB設計の観点

論理設計

ここでサロゲートキーに関する話が出てくることは無いでしょう。

しかし一方で、ユーザIDといったエンティティに対するIDはこの段階でも検討すると思います。実体があるモノにはIDを振ったほうが管理が簡単になることが多いという感覚はあるのではないでしょうか。というか、ユーザIDとかは振らないと「何を持って一意に定めれば…」ってなりますからね。住所、氏名ではちょっと厳しいですし、引っ越しや結婚で変わるものを、システム上でキーとして扱うのは大変です。*1

社員番号だって何で振るんでしたっけ。管理上やりやすいからですよね。商品IDだって何で振るんでしたっけ。管理上やりやすいからですよね。

そんなわけで、サロゲートキーの意識がないままIDとしてを振っていることはよくあるのではないでしょうか。…ない?

そして「エンティティに対するID」はしれっとサロゲートキーの性質を満たしています。本来ないものがシステムの都合で振ったからナチュラルなキーとはいえないでしょう。まぁ名前だってマイナンバーだって元はナチュラルキーじゃなくて誰かが払い出したただの属性なんですけど。

サロゲートキーは複合キーがある状態で議論されることがほとんどなのでこれをサロゲートキーと呼ぶのかは議論がありそうですが、私は呼んでいいと思ってます。

物理設計

論理設計で出来上がったER図を眺めていると、結合条件がキツそうなテーブルがしばしば出てきます。

例えば製品の管理を行うマルチテナントなシステムを作るとしましょう。

  • マルチテナントなシステムを設計すると「テナント」単位で色々管理したくなります。あらゆるところでテナントIDが出てきます。
  • 「製品」と、製品を構成する「部品」があります。それぞれはIDでユニークに識別できます(同じ製品を別のテナントで作る場合でも、製品のIDは同じになるということです)
  • その組み合わせを管理する「関連テーブル」を持つことになります

「いや、テナントと製品と部品を取りまとめて扱うべき単位があるはずでは?」という話はあると思いますが、一旦見なかったことにします。

そして「テナントXの製品Yでは部品Zが3個必要になったんだよね」となると、関連テーブルに「テナントID, 製品ID, 部品ID, 個数」と持つことになるでしょう。更新するときは忘れず3つのキーを指定することになります。「テナントXでは製品Yに部品Zを採用するのをやめるってよ!」となると、削除するときに3つのキーを指定することになります。

さて、ここでサロゲートキーを振りたくなるのかどうか?ということです。サロゲートキーがあれば更新・削除するときは1つのキーで済むようになることでしょう。

しかし現実はそう単純ではないでしょう。

「テナントXの製品Yで使われる部品の点数を知りたいんだよね」となったら結合が始まります。テナントIDと製品IDを指定してSUM(個数)とすればよさそうですね。おっと、部品ごとの個数が欲しいそうです。GROUP BY 部品IDして…と色々あります。サロゲートキーは案外使われないかもしれません。

それでもメリットが大きいと感じるのであれば、使えば良いのではないでしょうか。

…うーん、「関連テーブル」に対するサロゲートキーはちょっと例が悪かったかもしれません。

まぁとにかく必要なら付ければよいのでは、ということです。

アプリの観点

フレームワークの都合でサロゲートキーを採用する/しないがある

例えばDjango、Entity Framework Coreなどはサロゲートキーありきの設計を求められていました。最近は複合主キーを扱いやすくしていたり(EFCore 7)、扱えるようにする動きがあったり(Django)しますが…。

なので、サロゲートキーを前提としているフレームワークを使う場合はサロゲートキーを使うようにしたほうがよいです。単純に従ったほうが楽だからです。

フレームワークに逆らうと時間を溶かすわ、初学者への説明に時間割かれるわで良い事ありませんよ!!!

DBの論理設計するまえからどのフレームワークでいくか考えておいたり、DBアーキテクトに根回ししておきましょう。

サロゲートキーじゃないと条件漏れが怖い?

WHERE句に条件書き漏らしてバグった!複合キーのせいで条件たくさん書かされるのが悪い!なんで本番に出した時に発覚するんだ!社会が憎い!!

そんな経験ありませんか?私はあります。更新条件1個書き忘れて数百件更新がかかってたことを知った時は血の気が引いたものです。(処理結果の件数は見込みが分かるならちゃんと比較しようね!)

もしサロゲートキーがあるレコードの操作だったら、更新対象が明確なので記述も簡単になったでしょう。

しかし、サロゲートキーを振るべきだったか、というと私はそう思いません。

ちゃんとDBを使ったテストを書けばよかったのです。あるテナントに対する操作なら、テナントXとテナントYのレコードを足すように書くのです。そうすれば条件漏れてたときに想定している件数が増えるので気づけるのです。複合キーがN個あるなら2Nレコードを足すのが最低限といえるでしょう。まぁそれが難しいんですけどね。歯を食いしばってテストケース書いていきましょう。でも辛いよね。誰か書いてくれーと、いってもリポジトリ層で隠すのでアプリで使う分にはそこまででもなかったりします。最初の定義だけが辛いです。

SQLを書くのはアプリ開発チームだけの特権じゃない?運用のことを考えろ?サロゲートキーはあるべきだ?

そうかもしれません。必要ならそういう設計にしたらいいのではないでしょうか…

サロゲートキーがあっても結局使えないこともある

サロゲートキーはコンテキストの都合に弱い印象があります。そのコンテキスト(操作しようとしているユーザなど)でそのレコードを触ってもいいのか?という感じです。

例をあげましょう。例えばグループのメンバー管理をするDB設計を考えましょう。あるユーザがグループを作成でき、作成者が管理者となり、他のユーザをグループに参加させたり除名したりできるというものです。

これを無理やりサロゲートキーを使った設計にしてみます。

次に追加・取得・更新・削除 ができるようなHTTP API /groups/members/:group_member_idを設計をしたとしましょう。

さて、このAPIは誰でも叩けてよいでしょうか?よくありませんよね。

なので、グループの管理者であれば操作できるように、group_member_rolesでロールを割り当てる設計としています。RBACですね。(実際はロールに紐づく権限が別途あるだろという話は一旦脇に置いておきましょう)

そうなるとロールに割り当たっているかどうかを確認したうえで、操作できるかどうかを判断する必要があります。実際には次のクエリが必要です。

  • group_member_idに紐づくgroup_membersのレコードを取得する
  • group_membersレコードのグループIDとユーザIDを使い、操作ユーザIDがグループの管理者(admin)かどうか検証する

※操作ユーザIDは名前の通りで、アプリのセッション情報なりで取得できるものとしています。

それを1発で書いてみるとこんな感じです。(1発で書く必要性は全くないですが、ある案件で事あるごとにクエリを何回も投げるなと調教されたせいでこんな発想になってしまった…)

SELECT gm.*
FROM group_members gm
WHERE gm.id = :group_member_id
  AND EXISTS (
    SELECT 1
    FROM group_members gm_admin
    JOIN group_member_roles gmr ON gm_admin.id = gmr.group_member_id
    WHERE
      gm_admin.group_id = gm.group_id -- <=この辺りは結局サロゲートキーを使えない
      AND gm_admin.user_id = :操作ユーザID
      AND gmr.role = 'admin'
  );

この要点はEXISTSのサブクエリで、操作している人のユーザIDからでは自身のgroup_member_rolesのレコードが引っ張れないというところです。また、group_idも一度group_member_idを使って取得する必要があり、若干煩雑になっています。

次にサロゲートキーを使わず複合主キーな設計にしてみましょう。

URLもサロゲートキーがなくなったので/groups/:group_id/members/:user_idとする必要があるでしょう。(REST APIの宗教によります)(:user_idではなくgroup_membersにサロゲートキーを設けて別のidにすべきという意見はあるかもしれません)

DBの変更があると大変ですね(白目)

そして上記と同等なクエリはこうなります。

SELECT gm.*
FROM group_members gm
WHERE gm.user_id = :user_id
  AND gm.group_id = :group_id
  AND EXISTS (
    SELECT 1
    FROM group_member_roles gmr
    WHERE gmr.group_id = gm.group_id
      AND gmr.user_id = :操作ユーザID
      AND gmr.role = 'admin'
  );

どうでしょうか。JOINがなくなった分少し読み解きやすい気もします。

サロゲートキーを使う設計を取っていたとしても、それが通用しない場面はこんな感じでちょこちょこ出てくるはずです。

サロゲートキーがあるとリレーションを辿るときに逆に大変になったり惑わされたりします。「あー、他のテーブルを結合しないと…」みたいな。

「じゃあずっと複合主キーでやっていくのかよ!うちは5カラムもあってやってられんわ!実装側に入力ミスさせるような設計してんのか!DB設計しただけでいい気になりやがって!!」

それは・・・まぁ・・・そうですね・・・サロゲートキーが必要ならそういう設計にしたらいいのではないでしょうか…

ただ、5カラムにわたる複合主キーは多分適切な概念を見いだせていないケースのようなきがします。業務分析などを見なおして扱おうとしているドメインに問題がないか疑ったほうがよいでしょう。

例えば、製造業向けのマルチテナントなシステムで、製品情報を扱うシステムを考えましょう。テナントは製造企業と考えてください。製品を作るときにはテナントが「型番」を割り振るものとします。候補キーとするべき項目は「型番」あたりがでるでしょうか。しかし「型番」はテナント毎に自由に決められるべきです。企業Aが製品Xという型番をつけているのに、企業Bが製品Xという型番をつけられなくなるのはよくないからです(もちろん被らないほうがよいという話もありますが、そもそも世の中全部の型番とかわかるわけないし…前提の都合が悪くなるので一旦脇に置きます)

なので「テナントID」と「型番」でユニークになるようにすべきでしょう。

でもこのままでは複合キーになってしまいます。それに型番は入力ミス等で変更したいこともあり不安定で、これがPKになっていると1個の更新のはずが他のテーブルも更新しないといけなくなりそうです。つまりシステム上それだと扱いにくいんですよね。そこでサロゲートキーとして「製品ID」を割り当て、「製品ID」はシステム上で一意に扱うようにすれば扱いやすそうです。もちろん、テナント内で型番が重複することはよくないですから「テナントID」と「型番」のユニーク制約を忘れずに。

こういった概念を適切に見つけてあげると、割と複合主キーが5つにもわたるということはあまりないです。そういう大規模なアプリを作ってないというのもあるかもしれませんが(200テーブルぐらい?)

サロゲートキーを使うか・使わないかを「設計全体」で決めるのではなく、必要なら使うという考えがよいのではないでしょうか、ということです。

…ちなみに「CASCADEがあるんだからそれを使えばいいじゃん」というのはあります。それは…はい、そうですね…使えばいいんじゃないですかね…(単純に私がCASCADEを使いこなせていないのと無駄に更新がかかりそうで不安なのであまり積極的に使う気にはなれないです)

「複合主キー許すまじ!サロゲートキーを一律に振れ!!」

こういう方は宗教上の理由です。好きにさせるしかありません。

でもお客との対話ではサロゲートキーの話は普通出てこないですからね。上記の「製品ID」もお客からしてみれば謎の存在ですからね。無駄な問答に付き合わされることでしょう。

サロゲートキーに関する様々な意見

DBの事情に詳しい先生方はどうでしょうか。私なんかより真っ当な意見があるでしょう。私は賢いので歴史から学びます。ほとんどここまで書いてから調べ始めたけど。

独断と偏見でピックアップしています。私も語っているぞという場合はコメントいただけますと幸いです…

ただし、どれもナチュラルキーや複合主キーに対するサロゲートキーという対比なのだけど、私は「サロゲートキーを使うか使わないかではなくね?必要なら使えばよくない?」という立場なのでちょっと感想がズレるかもしれない。

ミック先生

サロゲートキーについては否定的に見えますが、必要なら使えという意見ですね。

まず「テーブル設計のグレーゾーン~毒と薬は紙一重 (4)サロゲートキーVSナチュラルキー」という記事。

gihyo.jp

「そもそも途中で商品IDを使いまわす設計ってなんだよ!急に指し示すものを変えるなあああ!!」という感想が先に沸いてきたのだけど、外側から降ってきたIDや業務上の"番号"などはそういうものがありますね。我々下っ端SIerはそれらを尊重してあげないといけません…

とはいえ商品IDでは一意性が保たれないケースがあることがわかったのならば、候補キーとしては不適切ですから何かしなくてはいけません。

記事ではサロゲートキーカラム名では代理キー)を与えています。これで商品の購入履歴などを記録するときは商品IDの代わりに代理キーを記録すれば代理キー1つで一意に特定できるようにはなります。

また、注釈5や図11にある通り、開始時点を足しているので、素直に商品IDと開始時点を複合主キーにし、購入履歴などでは商品IDと購入日時を記録することで、同様に一意に特定できるようになります。この2つのキーは業務を分析して見えてきたナチュラルキーでもあります。(あと細かいですが、消しゴム追加時点で開始時点は2008になり、ガムテープの終了時点は2008になるよね…あと年単位だと大雑把かもしれません。)

要はこのどっちがいいかは選んでね。という感じに見えます。あとは記事に書かれている通り。

サロゲートキーを使えば、その保存に余計なディスク領域を消費しますし、論理的には不要な項目が付け加わることで、設計を不透明なものにするからです。必要に迫られたときだけ使うようにしましょう。

デメリットもあるので必要なら使いましょうと言う感じです。

また、同著者が出されている書籍「達人に学ぶDB設計 徹底指南書」(2012年)にて以下のようにありました。引用で失礼します。

極力代理キーの使用は避けて、自然キーによる解決を図るべきです。

ちゃんと業務と向き合い、サロゲートキーは必要になるまで使うなという感じですね。

Mikiya Okuno先生

否定的に見えます。(ドメインを分析すれば適切な候補キーとして出てくるが、どうしても出てこない時は仕方なくサロゲートキーを使う、という感じでしょうか)

nippondanji.blogspot.com

リレーショナルモデルの鬼という印象

リレーショナルモデルには「サロゲートキー(代理キー)」という概念はない。

はい。すみません。

本来はあくまでも適切なドメインを設計すべきであり、その上で適切に候補キーを選択すれば良いのである。(それはナチュラルキーと呼ばれるものになるだろう。)

私もそう思います。見つかった候補キーが複合キーだとしてもシステムのうえで何ら問題のない複合キーであるなら素直にそれを使えばよいかなと。

この記事だけみると「ナチュラルキーだろうがサロゲートキーだろうがちゃんと設計せい」という冷たい感じです。タイトルにある通りなんですが「ナチュラルキーに主キーを使うべきではない (これ?)」の記事に対する反論の記事なのでそう書かれた感じはあります。

実際、同著者が出されている書籍「理論から学ぶデータベース実践入門」のP.127からはじまる「IDを設計する考え方」には優しく書かれています。

引用するには長いので私が読み取った要点を並べます

  • IDは現実世界のものとIDとで1:1となるべきもの
  • ナチュラルキーとサロゲートキーはどちらも必要で使い分けるべき
  • IDとして成り立たないIDも存在し、それを自システムのIDとして扱うのは危険(例:ISBNの重複)
  • しかしIDとして成り立っているならナチュラルキーをIDと捉えても、何ら問題はないはずである(IDのライフサイクルには注意する)
  • 自システムの都合で自システム特有のIDを振るというのはサロゲートキーを使うことであり、必要なら使えばよく、何ら問題はないはずである(デメリットには注意する)

ナチュラルキーがあるのにそれを使えない理由をごまかそうとしてサロゲートキーを使うのは辞めなさい、みたいな感じですね(小並感)

jflute 先生

肯定的に見えます。

dbflute.seasar.org

もう少しざっくり言うと、"サロゲートキー+ユニーク制約" のデメリットと "複合主キー" のデメリットを天秤にかけたとき、DBだけじゃなくアプリも含めたシステム全体 で考えると、圧倒的に後者の方がつらいことが多く、前者のデメリットはフレームワークとか担保しやすくて深刻化しづらい、という感覚値を持っているからです。

まず、複合主キーがある前提で、そこにサロゲートキーを使うか、という話であると読み取れます。jflute 先生はサロゲートキーに肯定的な様子に見えます。

上記のお二方とは違い、RDBの面だけではなくアプリ面にも言及されている点が異なるでしょう。

複合主キーのデメリットに"条件落ち"があると示されております。これは私が上記で書いてた条件を書き忘れる系のやつですね。このあたりフレームワークによる負荷軽減ができるので明確なメリットになると思います。

また引用外で書かれていますが、複合主キーの代わりにサロゲートキーを採用する場合は複合キーにはユニーク制約を付ける、という話は大事だと思います。

soudai先生

肯定的に見えますが、全部のテーブルにつけるほどではないようです。

php-genba.shin1x1.com

こちらで52:40以降の話の中でサロゲートキーの話題が出ています。もちろんなんでもというわけではなく、

  • JOINで使うならつけたほうが早くなる(シーケンスなら4byteで済むので)
  • 並び替えに使う(都道府県なら北海道が一番上に来てほしい、など。)

というように、無意味なものには付けない程度で何らかの役に立つならつけてもいいんじゃない、という印象です。原理主義ではなく現場主義(でも原理も大事)という感じですね。

ただ個人的には並び替えは並び替え用の列もしくはテーブルを作るかもしれません。(都道府県ぐらいなら別にいいですが、客によってこういう順序で表示してほしい、というのがあるので…)

書籍: SQLアンチパターン (IDリクワイアド)

否定的に見えます。

有名な本ですね。IDリクワイアドはしばしばとにかくサロゲートキーを付けたがるパターンのことを差します。使っても良いケースとして以下が上げられています。

ううむ、自然キーが長すぎる場合というのは確かに。

うーん、どれをみても極端な方はいませんでしたね。

おまけ: ナチュラルキーを思い出す

サロゲートキーにばかり目がいっていたけど、みんなナチュラルキーについて触れているので少し考えてみましょう。

ナチュラルキーとは

わかりやすいのでこちらをどうぞ。

wa3.i-3-i.info

詳しくは調べてもらうとして、私の定義としては「与えられたデータから見つけた候補キー(だと思い込んでいるもの)」と考えています。

例えば話を聞いて、エンティティを見出し、これをテーブルにどう納めようか考えていると「お、これを主キーにすればええやん!」と思うことはあるでしょう。そして「実はこういう運用があって重複することがあるんです」なんて後出しジャンケンされた経験はないでしょうか。

ありませんか?ありますよね?そう、あるんですよ。どれだけ敗北してきたことか…。

ちなみに複合主キーである必要はないです。マイナンバーも扱う側はナチュラルキーと捉えられます。そんなの持ちたくないですけどね

ナチュラルキーを主キーにすべきではない」?

上記でも引用していますが、こんな記事があります

watanabek.cocolog-nifty.com

まず注意点はサロゲートキーに関しても複合主キーに関しても肯定的であること。必要なら使えばよかろうという感じですね。(うちの記事はサロゲートキーについて焦点を当てようとしていたので一応。)

渡辺幸三先生の主張としては、「ナチュラルキー(自然キー。値が変化し得る一意キー)」と捉えているため、そんな不安定なものシステムのIDに使うんじゃねえよ!ということでしょう。

注釈にもちゃんと書いてあります。

値が確定していて変化することがあり得ないようなナチュラルキーであれば、主キーにしても事実上問題は生じない。そういうわけでこのルールは、正確に言えば「値の体系が変更されることがあり得るようなナチュラルキーは主キーにしてはいけない」となる

Mikiya Okuno先生はここを見落としてるのではないか…まぁタイトルから暴論と感じて筆を執ったとしてもしょうがない感はありますが。(そもそもこの記事をみての記事だったんだろうか)

なのでタイトルの「ナチュラルキーを主キーにすべきではない」というのは正しくなくて「値の体系が変更されることがあり得るようなナチュラルキーは主キーにしてはいけない」ということといえます。Mikiya Okuno先生の「IDとして成り立たないIDも存在し、それを自システムのIDとして扱うのは危険」という意見とも一致するでしょう。(私の理解が足りないかもだけど。)

おまけ: ID形式どうする問題

最近しばしば見かけるので。

まずID形式は要件とデータストアの性質と合わせるのが大事です。

一番わかりやすいのはGitHubのIssueとかPRがUUIDだったらどう思いますか?嫌ですよね?UUIDのほうが興奮する?

え、GitのコミットIDも連番になればいいのに?分散型の特性がなくなっちゃうので難しいですね…うーん…Subversion使おうか!!

ということで、要件に合わせてID形式を決めましょう。要件というとアレですが。

  • 制約はないか(シャーディングをしているorID生成の機能がないのでアプリ側で生成しなくてはならない、ロックがかかるのは困る、など)
  • ユーザが触れる値か(UUIDだと長すぎないか、短くするには連番がよいか、など)

それはそれとして、ID生成に悩んだらまずこちら。少し古いですが、ULIDをUUIDv7で読み替えれば今でも通用しますね。

qiita.com

素直に連番

素直にAUTO INCREMENTやBIGSERIALなどを使うのも手でしょう。

実装上はIDを先に発番できないという歯がゆい問題がありますし、シーケンスの採番でロックがかかる実装もあります。

また、シャーディング等でストア先を分散させる場合は使えません。読み書きにID値が必要だからです。そういえば最近流行の兆しがあるNewSQLなら何とかなりそうでは?と思い軽く調べてみました。

TiDBは振る舞いや互換性を取った時の性能面で難がありそうですね。

zenn.dev

Cockroach DBは簡単に歯抜けが起きるし、そもそも特定のノードに負荷が集中しないよう分散するようなIDがよいということでUUID(提供している関数ではUUIDv4)を推奨しているようでした。まぁ分散DBじゃそりゃそうか。

www.cockroachlabs.com

もちろん、RDBMS上だけで考えてましたが、連番だけを払い出す専用のシステムを組むのも手です。RedisのINCRBYとかで…

乱数ベースなID(UUID等)

最近UUIDv7がホットですね。

タイムスタンプ+乱数なので時系列で並び、マシン間でもほぼほぼ衝突はしないだろうという安心感はあります。

UUIDv4だろうがUUIDv7だろうが、良いことばかりではありません。

  • 乱数を使っている以上はわずかでも衝突する可能性があり得ます
    • IDの衝突が起きると何が起きるかを考えるとよいでしょう。とはいっても、せいぜいINSERTに失敗するぐらいでしょうから、ごくたまにエラーになるぐらいで何かが起きるということもないかもしれません。
  • 乱数の質が悪いと推測できる可能性があります(これをどう悪用するかという別の課題があるのでそこまで躍起になる人はいないとは思いますが)
  • UUIDは128bitです。16byteです。扱うデータによっては無視できないサイズです。

最初に言った通り、要件やデータストアの性質によります。これを使うに値するかはよく考えたほうがいいでしょう。

でも1テーブルあたり100万レコードぐらいしか扱わないなら今の時代なんでもいいと思う。ただ後から変えたり直すのは大変なのでな…

独自のID形式

無いなら作ればいいんです。

国内でも様々な事例がありますね。(これらの記事だけじゃないくらいいっぱいあります)

developer.smartnews.com

creators-note.chatwork.com

上記はsnowflakeベースでしたね。なんとなくID生成器に手を出すところは次をこだわる傾向があるようです。

  • 時系列順
  • 短さ(概ね64bit長。UUIDの128bitの半分)
  • シーケンスを使うので衝突しない(コントロール可能)

非推奨: IDに接頭語をつける

まず、IDに意味を持たせるのはよくないです。サロゲートキーはレコードを特定するためのものであるべきですから、それ以外の役割を持たせてはいけません。

なぜならその役割が変わった時にIDを変えないといけない可能性がでるからです。IDは不変であるべきでしょう。

とはいえ、業務でお世話になったので紹介だけ。

接頭語を付けるというのは単純で、例えば、ユーザなら文字列でU001、製品ならP001などの形でIDを払いだします。

これでログの値を見たり、取違いが起きて変なデータがテーブルに入っても値から「なんで製品変更履歴テーブルの製品IDにU001が入ってるんだ?」という感じで、すぐに原因がわかる…という場合があります。

でもそれだけです。運用時のトラブルでこういう手掛かりがあるだけでも助かるんですけどね。まぁ市民権は得られないでしょう。でもどうせ外に出ないなら自由に採番してもいいのよ?

非推奨: ユーザに決めさせる

英数字16桁まで、みたいな。昔ながらのシステムではしばしばありますが、これはちょっと危険です。

  • 再取得の仕様についての配慮はしましょう。
  • 外部に出すときに問題のないか確認しましょう(よろしくないワードを含んでいると色々配慮が必要になることも)

それでもユーザに自由な名前を選ばせたいという場面はあります。その時はそういう要件なのだから単純に「ユーザ名」などとして扱い、それとは別に「(内部用の)ユーザID」を払い出せばよいでしょう。

例えば、Twitter(新X)は@hogehogeというアカウント名を撮れますが、その裏ではアカウント固有のIDを割り当てていますし、フォロワーなどの情報はこの固有のIDで紐づいています。だからアカウント名が変わっても勝手にフォローが外れるようなことはありません(…よね?今はどうなんだ?)

おまけ:主キーは公開してもよい?もとい露出した情報から意図しない情報の読み取りを防ぐには?

何が問題?

主キーは別に公開してもいいじゃろ、と思っているんですが、次の記事をみて一部「うーん」と思ったので。

zenn.dev

主キーは原則外部に公開することは望ましくありません。

私は別にいいじゃんと思ってます。上記のREST APIの例でも自然キーをそのまま使ってますが、普段からやってます。

あるユーザのプロフィール画面のURLややりとりされるデータを考えてみてください。Webサービスを作る以上は結局アクセスに必要なユーザIDなどの情報は必ず露出することになります。それが主キーであるか否かは別にどうでもよいはずです。

なので主キーを公開すべきではない、という話ではなく、露出している情報から意図していなかった情報を読み取られることのはずです。

シーケンスの場合

とはいえ、シーケンス(連番)は推測しやすいですから、そういった危険はあります。

例えばユーザアカウントを2つ取りユーザIDが1000,1001と振られたら色々推測できます。

  • 1000ユーザぐらい居そう
  • 1000,1001の間に登録はなかったな、そんなに頻繁に登録されていなさそうだ。1日置けば1日当たりの登録者数も分かりそうだ
  • 1000なら1002のユーザもいるだろう。あわよくば権限がないと見れないはずのデータが見れるかも

推測されて起き得る問題を考えてみましょう。

  • 意図せず見られてしまうのでは…
    • そもそも権限管理をしっかりしなさい
    • ランダムなID体系を使っていても解決にはなりません。万が一アクセスされて困るなら管理をしなさい
  • 現在の値からアクティブユーザ数などの情報を読みとられてしまうのでは…
    • 困るならシーケンスではなく隠せるID体系にする
    • もしくは内部用と外部用でIDを2つ作り、内部用はシーケンスでシステム内部で使う、外部用は好きに採番してURLなどへ露出してもよい形式にする(上記のZennの記事でも書いてあります)

シーケンスではない場合(UUID等)

内容次第ですが、まず推測しにくくなります。よかったですね。

*1:「メールアドレスはユニーク制約であるべきだしIDにしてもよくない…?」だって?やめろ!メールアドレスは変えられるんだし、1人で何個も持てるんだから!メールアドレスにユーザを見出す考え方はいつか痛い目みるぞ!