「Object ID」は、文脈ごとに別のものを指します。MongoDBでは_idの既定値として使われる12バイトの値、SQL ServerではOBJECT_IDという組み込み関数、ArcGISではテーブルの属性フィールド、Rubyでは処理系がオブジェクトに振る番号、そしてAPI設計では外部に公開するリソース識別子です。検索結果に無関係なページが混ざるのは、この語の指す対象がもともと複数あるためです。
この記事では、まず語の切り分けとID・UID・GUID・UUIDの呼び分けを済ませ、そのうえで連番・UUIDv4・UUIDv7・Snowflake型・プレフィックス付きランダム値を選び分ける基準を、各仕様の原文と手元での実行結果から示します。
まとめ
- Object IDは単一の技術用語ではありません。MongoDBのObjectId(12バイト・16進24桁)、SQL ServerのOBJECT_ID関数、API設計の公開識別子は互いに無関係です。
- 採番方式は「DBに問い合わせずにIDを作りたいか」「時系列順に並べたいか」「64ビットに収めたいか」の3問でほぼ絞れます。単一DBで外部に出さないなら連番で足ります。
- 時刻順のID(UUIDv7・Snowflake型)は単一ノードのB木には向きますが、Spannerのようにキー範囲で分散するDBでは書き込みが1台に集中します。Spannerの公式ガイドはUUIDv4を推奨しています。
- Snowflake型の64ビットIDをJSONの数値で返すと、JavaScriptでは下の桁が化けます。Discordは全IDを文字列で返しています。
- 連番をUUIDに変えても、Sqidsで難読化しても、IDOR(オブジェクトレベル認可の不備)は塞がりません。必要なのはリソース単位の認可チェックです。
まず、自分の探している「Object ID」がどれなのかを切り分けます。
Object IDが指すものの切り分け
同じ「Object ID」と書かれていても、以下は互いに変換も互換もありません。実装に入る前に、自分が扱っているのがどれかを確定させます。
MongoDBのObjectIdを構成する12バイト
MongoDBのObjectId()リファレンスによると、ObjectIdは先頭4バイトが生成時刻(Unixエポックからの秒数)、続く5バイトがクライアントプロセスごとに1回生成される乱数、末尾3バイトがランダムな値から始まる増分カウンタです。5バイトの乱数部は、プロセスが再起動したときや、そのプロセスの接続先のプライマリノードが変わったときに作り直されます。同じアプリから発行したIDでも、再起動の前後で中間の10桁が変わるのはこのためです。
生成時刻はmongoshならObjectId.getTimestamp()で取り出せます。言語を問わず、16進文字列の先頭8桁を整数に戻せば同じ値になります。
<?php
$oid = '6890f1a53c2d6b1a2e9b4f1a'; // 16進24桁 = 12バイト
$sec = hexdec(substr($oid, 0, 8)); // 先頭4バイト = Unix秒
echo gmdate('Y-m-d H:i:s', $sec), " UTC\n"; // 2025-08-04 17:45:09 UTC
ただし並び順は厳密ではありません。BSON型の解説は「ObjectIds are approximately ordered by creation time, but are not perfectly ordered」と書き、理由として時刻の分解能が1秒しかないこと、生成するクライアントごとに時計がずれうることを挙げています。_idでのソートは「おおむね作成順」であり、作成順の証明には使えません。MongoDB自体の仕組みはMongoDBとは?ドキュメント指向データベースの仕組み・利点・使い方入門【9.0対応】で扱っています。
SQL Server・ArcGIS・Rubyでの用法
SQL ServerのOBJECT_IDは、テーブルやビューなどスキーマスコープのオブジェクトについて、データベース内のオブジェクト識別番号を返す関数です。DDLトリガーのようなスキーマスコープ外のオブジェクトは対象外で、主にテーブルの存在確認に使われます。ArcGISのObjectIDは、ジオデータベースが各行に自動で割り当てる管理用の一意番号です。Rubyのobject_idは実行中のオブジェクトに処理系が与える番号で、一意なのは現在のプロセス内だけです。
この3つは製品や処理系が付与するもので、設計者が方式を選ぶ余地はありません。採番方式を自分で決めたいなら、探しているのは次のリソース識別子です。
API設計における公開リソース識別子
API設計でのオブジェクトIDは、レスポンスに載せてクライアントへ渡し、以後そのリソースを指すために使う値です。型は文字列のことも整数のこともあります。Stripeのpi_で始まる支払いID、DiscordのギルドID、GitHubのIssue番号がこれにあたり、形式も長さも提供側が決めます。データベースの主キーをそのまま出すか、別の値を出すかは設計判断です。この記事の後半は、この意味のIDを扱います。
ID・UID・GUID・UUID・ULIDの呼び分け
これらは並列の選択肢ではなく、標準を持つ名前と総称が混ざっています。標準の有無で並べると関係がはっきりします。
| 呼称 | 実体 | 標準 | 文字数 |
|---|---|---|---|
| UUID | 128ビットの識別子 | RFC 9562 | 36 |
| GUID | UUIDのMicrosoft系の呼称 | RFC 9562に準拠 | 36 |
| ULID | 128ビットの時刻順識別子 | ULID Spec | 26 |
| ObjectId | MongoDBの12バイト値 | MongoDB仕様 | 24 |
| Snowflake ID | 64ビットの時刻順整数 | なし(各社実装) | 19〜20桁 |
| UID・グローバルID | 文脈依存の総称 | なし | 可変 |
UUIDとGUIDは呼び名が違うだけで、.NETのSystem.GuidもRFC 9562の書式に従います。違いが出るのはバイト列にしたときで、Guid.ToByteArray()は先頭の4バイト組と続く2バイト組2つを逆順で返すため、文字列表現のバイト順と一致しません。.NET 8以降はbigEndianを受け取る多重定義があるので、DBへバイナリで格納する経路ではそちらを使います。UIDとグローバルIDには規格がありません。グローバルIDは「システムをまたいで一意」という性質の呼び名で、生成方法を指す言葉ではないため、要件定義でこの語が出たら中身を先に確定させます。UUIDの構造と版ごとの違いはUUIDとは?RFC 9562の構造とv1〜v8の使い分け・実装判断を解説にまとめています。
採番方式の選び方
方式ごとに、生成する場所・時系列順に並ぶか・推測できるか・主キーの幅が変わります。
| 方式 | 生成主体 | 時系列順 | 推測 | 主キー幅 |
|---|---|---|---|---|
| 連番 | DB | 並ぶ | 容易 | 8バイト |
| UUIDv4 | アプリ | 並ばない | 困難 | 16バイト |
| UUIDv7・ULID | アプリ | 並ぶ | 困難 | 16バイト |
| Snowflake型 | 採番ノード | 並ぶ | 時刻は読める | 8バイト |
| プレフィックス付き乱数 | アプリ | 並ばない | 困難 | 可変 |
最初の分かれ目は「DBに問い合わせずにアプリケーション側でIDを作りたいか」で、作りたいなら連番は外れます。次が「時系列順に並ぶ必要があるか」、最後が「64ビットの整数に収めたいか」です。3問目がYesならSnowflake型、NoならUUIDv7かULIDになります。
連番の適性と限界
MySQLのAUTO_INCREMENTやPostgreSQLのシーケンスで採番する方式です。主キーが8バイトのBIGINTで済み、挿入が常にB木の右端に入るのでページの中間分割も起きにくくなります。単一のDBで完結し、IDを外部に出さない業務システムなら、避ける理由はほとんどありません。
限界は2つあります。MySQLのAUTO_INCREMENTはINSERTした時点で値が決まるため、書き込む前にIDを知ることができません。PostgreSQLのシーケンスならnextval()でINSERT前に値を取れますが、その場合もDBへの問い合わせは必要です。オフライン端末でレコードを作る処理や、クライアントが生成したIDで再送を判定する冪等性とは?読み方・意味からAPI・IaCでの担保方法まで実装者向けに解説のような制御とは噛み合いません。もう1つは、書き込み先のDBを複数に分けた時点で、一意性を別の仕組みで担保する必要が出ることです。
UUIDv4のランダム性と索引の断片化
UUIDv4は、版とバリアントの6ビットを除く122ビットが乱数で、どのプロセスからも調整なしに発行できます。代わりに、RFC 9562自身が「UUID versions that are not time ordered, such as UUIDv4 … have poor database-index locality」と認めるとおり、連続して発行した値が索引上の離れた位置に入ります。MySQLのInnoDBのように主キー順にデータを並べるエンジンでは、挿入のたびにB木の別ページへ書き込みが飛びます。乱数源はCSPRNGが前提で、RFC 9562の6.9はその使用をSHOULDとしています。PHPならrandom_bytes()が該当し、mt_rand()やuniqid()は該当しません。
UUIDv7とULIDによる時系列順序の回復
UUIDv7は先頭48ビットにUnixエポックからのミリ秒を置き、残りを版・バリアント・乱数に使います。先頭が時刻なので、文字列を辞書順に並べればミリ秒単位で時刻順に並び、B木の右端に挿入が寄ります。ただし並ぶのは各サーバーが刻んだ時刻の順で、サーバー間の時計のずれや、時刻同期で時計が巻き戻った場合は実際の生成順とずれます。RFC 9562の6.1も、時計が戻る場合の扱いを実装側で決めるよう求めています。注意点は同一ミリ秒内の順序で、RFC 9562の6.2は「Take care to ensure UUIDs generated in batches are also monotonic」として別途の工夫を求めています。最小限の自作実装ではここが抜けやすいため、ramsey/uuidのUuid::uuid7()のような単調性を実装済みのライブラリか、PostgreSQL 18のuuidv7()を使ってください。
ULIDはUUIDとは別系統の仕様で、48ビットの時刻と80ビットの乱数をCrockford Base32で符号化した26文字です。UUIDより10文字短く、辞書順ソートに対応する点は同じです。言語別の生成方法とDB性能の差はUUIDv7とは?UUIDv4との違い・言語別の生成方法と主キー設計の注意点で詳しく扱っています。
Snowflake型の64ビットIDとJavaScriptの精度限界
Twitterが2010年に公開したSnowflakeは、「we need to keep our ids under 64bits」という要件から、41ビットのミリ秒時刻、10ビットのマシンID、12ビットの連番を1つの整数に詰めます。マシンIDを事前に割り振っておけば、採番ノード同士が通信せずに発行でき、BIGINT列にそのまま入ります。順序の保証は緩く、READMEは「k-sorted」を約束する範囲を「1s, but shooting for 10’s of ms」としています。初版は退役済みで、現在のリポジトリは設計資料として読むものです。
この方式を採用するなら、2つの前提を先に決める必要があります。1つはマシンIDの割り当てで、同じIDのノードが2台動けば衝突します。もう1つがJSONでの受け渡しです。Discordの開発者ドキュメントは自社のSnowflake(42ビット時刻・5ビットのワーカー・5ビットのプロセス・12ビットの連番)について「they are always returned as strings in the HTTP API to prevent integer overflows in some languages」と書いています。JavaScriptの数値が誤差なく表せる整数は2の53乗-1までなので、19桁のIDは数値のままでは壊れます。
// Node.js v26.5.0 で実行
JSON.parse('{"id":308994132968210433}').id // 308994132968210400(下3桁が化ける)
JSON.parse('{"id":"308994132968210433"}').id // '308994132968210433'
const id = 308994132968210433n;
new Date(Number((id >> 22n) + 1420070400000n)).toISOString()
// '2017-05-02T15:52:22.133Z'(DiscordエポックからIDの生成時刻を復元)
化けた値はエラーにならず、別のIDとして処理が進みます。Snowflake型をAPIで返すなら、最初から文字列型で定義してください。後から数値を文字列に変えるのは、全クライアントに影響する破壊的変更です。IDに時刻が入ることを利用したカーソル方式のページ送りはページネーション設計とは?オフセットとカーソルの選び分けと実装を解説で扱っています。
プレフィックス付きランダム値の運用上の利点
Stripeのpi_やcus_のように、種別を示す接頭辞と乱数を組み合わせる方式です。ログやサポート問い合わせでIDだけを見たときに、何のリソースかが分かります。
<?php
function publicId(string $prefix, int $randomBytes = 10): string {
$alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // I,L,O,U を除外
$bits = '';
foreach (str_split(random_bytes($randomBytes)) as $ch) {
$bits .= str_pad(decbin(ord($ch)), 8, '0', STR_PAD_LEFT);
}
$out = '';
foreach (str_split($bits, 5) as $chunk) {
$out .= $alphabet[bindec(str_pad($chunk, 5, '0'))];
}
return $prefix . '_' . $out;
}
echo publicId('ord'); // ord_6K6VHDYM01FHBRXM
既定の引数(乱数10バイト=80ビット・接頭辞3文字)なら、文字列は接頭辞込み20文字です。関数自体は一意性を保証しないので、保存先の公開ID列に一意制約を付け、重複エラーが出たら値を作り直して再挿入します。時系列順には並ばないため、並び順が要るなら作成日時の列を持つか、内部の主キーにUUIDv7を使います。
分散データベースでは時刻順IDが裏目に出る条件
ここまでの「時刻順に並ぶIDはB木に優しい」という話は、1台のサーバーが索引を持つ前提です。キーの範囲ごとにデータを複数サーバーへ割り振る分散データベースでは、評価が逆転します。
Google CloudのSpannerのスキーマ設計ガイドは「A common cause of hotspots is using a key that monotonically increases or decreases, such as a timestamp」と書き、単調に増えるキーは新しい行を同じキー範囲=同じサーバーへ送り込むと説明しています。推奨は「We recommend using UUID Version 4, because it uses random values in the bit sequence」で、バージョン1を避ける理由に「they store the timestamp in the high order bits」を挙げています。ガイドはUUIDv7に触れていませんが、上位ビットに時刻を置く点はv7もSnowflake型も同じなので、同じ理屈が当てはまります。
CockroachDBのUUIDの解説も、UUIDを使うならUUIDv4を強く推奨すると書き、それ以外の版は「largely untested with CockroachDB」で、ホットスポットを避けるには性能試験が必要だとしています。
SpannerやCockroachDBのようにキー範囲で分散するDBで書き込みが多い表なら、主キーの先頭に時刻順のIDを置かないでください。UUIDv4を使うか、整数が必要ならSpannerが用意しているビット反転シーケンス(連番のビットを逆順にして値を散らす機能)を使います。作成日時の列に索引を張るだけでも同じ問題が起きる点に注意してください。Spannerの索引は内部的にはテーブルなので、ガイドは単調に増える列への非インターリーブ索引も避けるよう求めており、親の行の下に置くインターリーブ索引(例:ユーザーIDと最終アクセス日時の組)を代わりに示しています。Spannerの仕組み自体はCloud Spannerとは?分散リレーショナルDBの仕組み・TrueTimeと採用判断を実装者目線で解説で整理しています。逆に、MySQLやPostgreSQLを1台のプライマリで動かすならこの問題は起きず、UUIDv7が素直な選択です。
内部主キーと公開IDの分離
外部に出すIDとデータベースの主キーは分けてください。主キーをそのまま公開していると、採番方式を連番からUUIDv7へ変えるといった、公開済みのIDの値や型が変わる内部変更がそのままAPIの破壊的変更になります。公開IDを別の列に持っていれば、主キー側を作り直しても利用者に見える値は変わりません。
Stripeの仕様に見る公開IDの扱い
Stripeは、APIアップグレードガイドの下位互換の変更の一覧に「Changing the length or format of opaque strings, such as object IDs」を挙げ、「This includes adding or removing fixed prefixes (such as ch_ on charge IDs)」と明記していました。あわせて、IDを255文字まで扱えるようにし、MySQLならVARCHAR(255) COLLATE utf8_binに格納するよう求めています(2022年5月のアーカイブで確認。2026年11月時点の現行ページは手順中心に書き換えられ、この一覧はありません)。
読み取るべきは、接頭辞は人が読むための手がかりで、プログラムが解釈する構造ではないという線引きです。利用側がch_という文字列で分岐する実装は、提供側から見れば仕様外です。自社APIに置き換えるなら、公開IDは長さも形式も後から変えられる不透明な文字列として定義し、主キーは別に持ちます。なおutf8_binは文字セットutf8の照合順序で、そのutf8はMySQL 8.4ではutf8mb3の非推奨の別名です。新しく作る列は文字セットutf8mb4・照合順序utf8mb4_binにします。主キーの選定が後から効く他の論点はDB設計のアンチパターン一覧|SQLアンチパターン第2版27章と現場の定番失敗で整理しています。
Sqidsによる連番の難読化と秘匿性の限界
主キーの連番を残したまま、公開時だけ短い英数字に変換する方法もあります。代表がSqids(旧Hashids)で、YouTubeの動画IDのような見た目を連番から作れます。ただしSqidsの公式FAQは、向かない用途として「Any data that’s sensitive」を挙げ、「Generated IDs are not hashes and could be decoded back into numbers」と書いています。旧Hashidsのsaltについても「There is no encryption of any kind」として削除しました。
FAQ自身が、ユーザーIDに使うと復号されたときに利用者数が分かってしまうと例示しています。件数や登録順を知られたくないなら、変換ではなく、公開ID列に乱数を別途保存する方式を選んでください。
ログインIDやメールアドレスを主キーにした場合の変更コスト
ログインIDやメールアドレスのように、利用者が決めて利用者が変えられる値は主キーに向きません。変更を許した時点で、その値を参照している全テーブルの書き換えが必要になるからです。RFC 9562の6.13も、郵便番号や免許証番号のように「変わらない」と思われた値が後から変更を迫られるのはスキーマ設計でよくある問題だと注意しています。主キーは意味を持たない値で別に採番し、ログインIDやメールアドレスは一意制約を付けた通常の列として持ちます。
UUIDを主キーにするときのデータベース側の実装
方式を決めたら、格納方法で性能が変わります。とくにMySQLは、文字列のまま持つか16バイトのバイナリで持つかで差が出ます。
MySQLでのBINARY(16)格納
MySQL 8.4のマニュアルは、InnoDBの二次索引の各レコードが主キーの列を含むと述べ、「If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key」と明記しています。主キーの幅は、その表に張ったすべての二次索引に掛け算で効きます。100万行の表に二次索引を3本張ると、主キーの値はクラスタ化索引と合わせて4か所に載ります。主キーの値の部分だけを、行形式DYNAMIC(MySQL 8.4の既定)を前提に単純合計すると次のとおりです(1MB=100万バイト)。
| 主キーの型 | 1行あたり | 4か所の合計 |
|---|---|---|
| CHAR(36)(UUID文字列) | 36バイト | 144MB |
| BINARY(16) | 16バイト | 64MB |
| BIGINT | 8バイト | 32MB |
実際の索引はレコードヘッダーやページの充填率を含むので、この値がそのままディスク使用量にはなりません。見るべきは比率で、UUIDを文字列で持つとBINARY(16)の2.25倍、BIGINTの4.5倍になります。文字セットがutf8mb4でも36バイトで済むのは、InnoDBの行形式の解説のとおり、COMPACTとDYNAMICではCHAR(N)を末尾の空白を詰めてNバイトに収めようとするためで、旧形式のREDUNDANTではCHARを固定長のまま格納します。RFC 9562の6.13も、文字列での格納は128ビットの値に288ビットを使う冗長な形式だとして、可能ならバイナリで持つことをSHOULDとしています。変換にはUUID_TO_BIN()を使います。第2引数swap_flagの1は、時刻が下位から並ぶUUIDv1を索引向きに並べ替えるためのものなので、先頭が時刻のUUIDv7では0(既定)のままにします。MySQLのUUID()関数が返すのはバージョン1なので、UUIDv7はアプリ側で生成します。
CREATE TABLE orders (
id BINARY(16) NOT NULL PRIMARY KEY,
public_id VARCHAR(255) COLLATE utf8mb4_bin NOT NULL,
created_at DATETIME(3) NOT NULL,
UNIQUE KEY uk_public_id (public_id)
);
INSERT INTO orders (id, public_id, created_at)
VALUES (UUID_TO_BIN('019fd224-292d-7b6e-9b2f-7d57af5203f6'), 'ord_6K6VHDYM01FHBRXM', NOW(3));
SELECT BIN_TO_UUID(id) AS id FROM orders;
列の型選びの基本はデータベースの属性とは?カラム・フィールド・項目の違いと種類・制約をSQL例で解説でも扱っています。既存システムで文字列主キーからの移行や型変更を伴う場合は、データベース設計・移行支援のように索引の再構築と切り替え手順まで含めて計画すると、停止時間を見積もれます。
PostgreSQLのuuid型とuuidv7関数
PostgreSQLのuuid型は内部が16バイトなので、MySQLのような変換は要りません。2025年9月25日公開のPostgreSQL 18のリリースノートに「Add UUID version 7 generation function uuidv7() … This UUID value is temporally sortable」とあるとおり、DB側でUUIDv7を生成できるようになりました。同時にuuidv4()が別名として加わり、従来のgen_random_uuid()も使えます。
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
public_id text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
PostgreSQL 17以前にuuidv7()はないので、アプリ側で生成した値を渡すか拡張を入れます。
連番IDの秘匿ではIDORが塞がらない理由
「連番は推測されるのでUUIDに変えれば安全」という説明は広く流通していますが、対策としては不十分です。OWASP API Security Top 10 2023の第1位API1:2023は「Object IDs can be anything from sequential integers, UUIDs, or generic strings. Regardless of the data type, they are easy to identify in the request target」と述べています。IDは推測されなくても、共有URL、ログ、リファラ、スクリーンショットから漏れます。漏れたUUIDは、連番と同じように他人のリソースを指せます。
OWASPの対策は、ユーザーのポリシーに基づく認可機構の実装、レコードにアクセスする全関数での認可チェック、ランダムで予測できないIDの使用、認可機構を検証するテストの4つです。ランダムなIDは3番目にすぎず、認可チェックの代わりにはなりません。優先順位は、全エンドポイントでリソース単位の認可を書き、他人のIDを指定したリクエストが拒否されることをテストで固定し、そのうえでIDを推測困難にする、の順です。認可チェックを書かないままUUIDへ移す改修は、脆弱性をそのまま残します。実装面の対策は認可バイパスとは?IDOR・権限昇格の仕組みとサーバ側で止める実装対策を解説で詳しく扱っています。
よくある質問
Object IDとUUIDは同じものですか?
違います。UUIDはRFC 9562で定義された128ビットの識別子の仕様名で、Object IDは文脈で指す対象が変わる呼び方です。MongoDBのObjectIdは12バイトで、16バイトのUUIDとは長さも構造も違い、互換性はありません。API設計で「オブジェクトIDをどうするか」と言うときは、UUIDを含む複数の採番方式から選ぶという意味です。
UUIDは32文字ですか、36文字ですか?
文字列表現は36文字です。16進8桁・4桁・4桁・4桁・12桁をハイフンでつなぐので、16進32桁とハイフン4本で36文字になります。「32文字」は16進の桁数だけを数えたものです。CHAR(32)の列ではハイフン込みの値が収まらないため、文字列ならCHAR(36)、バイナリならMySQLはBINARY(16)、PostgreSQLはuuid型を使います。
UUIDが重複することはありますか?
正しく実装しても、有限の乱数から作る以上、確率的な衝突はゼロではありません。ただし実際に問題になるのは乱数生成器の不備です。UUIDv4の乱数部は122ビットですが、CSPRNGでない生成器、たとえばPHPのmt_rand()やuniqid()を使うと実効的な幅が大きく狭まります。PHPのマニュアルはuniqid()について「does not guarantee the uniqueness of the return value」と明記しています。重複を疑ったら、まず自分のコードが使っている乱数源を確認してください。
グローバルIDとユニークIDは何が違いますか?
一意性が保証される範囲の広さです。ユニークIDは1つのテーブルやシステムの中で一意であればよく、グローバルIDはシステムやデータベースをまたいで一意であることを求めます。連番はテーブル内ではユニークですが、別のDBの連番と衝突するのでグローバルではありません。RFC 9562は「true global uniqueness is impossible to guarantee without a shared knowledge scheme」と断ったうえで、同じ文で「a shared knowledge scheme is not required by a UUID to provide uniqueness for practical implementation purposes」と続けています。理論上の絶対保証ではないが、実用上は共有の仕組みなしで足りるという位置づけです。
IDの作り方や決め方が分からないとき、どこから手をつければよいですか?
決めるものを2つに分けるところからです。内部の主キーは「採番方式の選び方」の3問(DBに問い合わせずに作りたいか、時系列順に並べたいか、64ビットに収めたいか)で決めます。そのIDを外部に見せるかどうかは別に判断し、見せるなら公開ID列を追加します。ログインIDやメールアドレスはどちらにも使わず、一意制約付きの通常の列にします。