非整数の主キーに関する考慮事項


16

環境

分散アプリケーションからのデータを保存するデータベース(PostgreSQL 9.6)を設計しています。アプリケーションの分散された性質のSERIALため、潜在的な競合状態のため、自動インクリメント整数()を主キーとして使用することはできません。

自然な解決策は、UUID、またはグローバルに一意の識別子を使用することです。Postgresには組み込みのUUIDtypeが付属しており、これがぴったりです。

私がUUIDで抱えている問題は、デバッグに関連しています。それは人間に優しい文字列です。識別子ff53e96d-5fd7-4450-bc99-111b91875ec5は何も教えてくれませんが、ACC-f8kJd9xKCdが一意であるとは限りませんが、ACCオブジェクトを扱っていることを教えてくれます。

プログラミングの観点からは、いくつかの異なるオブジェクトに関連するアプリケーションクエリをデバッグするのが一般的です。プログラマーACCORD(order)テーブルで(account)オブジェクトを誤って検索するとします。人間が読み取れる識別子を使用して、プログラマーは問題を即座に特定しますが、UUIDを使用して、何が問題なのかを理解するのに少し時間を費やします。

UUIDの「保証された」一意性は必要ありません。私はない、競合なしで鍵を生成するためのいくつかの部屋を必要とするが、UUIDは過剰です。また、最悪のシナリオでは、衝突が発生した場合、世界の終わりにはなりません(データベースがそれを拒否し、アプリケーションが回復できます)。したがって、トレードオフを考慮して、より小さくても人間に優しい識別子が私のユースケースにとって理想的なソリューションになるでしょう。

アプリケーションオブジェクトの特定

私が思いついた識別子の形式は次のとおりです。{domain}-{string}ここ{domain}で、はオブジェクトドメイン(アカウント、注文、製品)に置き換えられ{string}、ランダムに生成された文字列です。場合によっては{sub-domain}、ランダムな文字列の前にaを挿入することも理にかなっています。レッツは、の長さを無視{domain}し、{string}一意性を保証する目的のために。

インデックス作成/クエリのパフォーマンスに役立つ場合、形式のサイズを固定できます。

問題

知っています:

  • のような形式の主キーが必要ですACC-f8kJd9xKCd
  • これらの主キーは、いくつかのテーブルの一部になります。
  • これらすべてのキーは、6NFデータベースのいくつかの結合/関係で使用されます。
  • ほとんどのテーブルのサイズは、中規模から大規模(平均で最大100万行、最大で最大1億行)です。

パフォーマンスに関して、このキーを保存する最良の方法は何ですか?

以下に4つの解決策を示しますが、データベースに関する経験が少ないため、どれが最適かはわかりません。

考慮された解決策

1.文字列として保存(VARCHAR

(Postgresはの間に違いはありませんCHAR(n)VARCHAR(n)、私は無視していますCHAR)。

いくつかの調査の後VARCHAR、特に結合操作での文字列比較は、を使用するよりも遅いことがわかりましたINTEGER。これは理にかなっていますが、この規模で心配する必要があるのでしょうか?

2.バイナリとして保存(bytea

Postgresとは異なり、MySQLにはネイティブUUIDタイプがありません。BINARY36 バイトのフィールドではなく、16バイトのフィールドを使用してUUIDを保存する方法を説明する投稿がいくつかありますVARCHAR。これらの投稿は、キーをバイナリとして保存するというアイデアを与えてくれました(byteaPostgresで)。

これによりサイズを節約できますが、パフォーマンスに関心があります。どの比較が高速であるかについての説明、つまりバイナリまたは文字列の説明を見つけることができなかった。バイナリ比較の方が速いと思います。もしそうであれば、プログラマは毎回データをエンコード/デコードする必要がありbyteaますがVARCHAR、おそらくの場合よりも優れています。

私は間違っているかもしれないが、私は両方だと思うbyteaVARCHAR、バイト(または文字単位)による(平等)のバイトを比較します。この段階的な比較を「スキップ」し、単に「全体」を比較する方法はありますか?(私はそうは思いませんが、チェックの費用はかかりません)。

として保存するのbyteaが最善の解決策だと思いますが、私が無視している他の選択肢があるのではないかと思います。また、ソリューション1で述べたのと同じ懸念が当てはまります。比較のオーバーヘッドは心配するほど十分ですか?

「クリエイティブ」ソリューション

動作する2つの非常に「創造的な」ソリューションを思い付きました。どの程度であるかわかりません(つまり、テーブル内で数千行以上にスケーリングするのが難しい場合)。

3. UUID「ラベル」を付けて保存する

UUIDを使用しない主な理由は、プログラマーがアプリケーションをよりよくデバッグできるようにするためです。しかし、両方を使用できる場合:データベースはすべてのキーをUUIDs としてのみ格納しますが、クエリが実行される前/後にオブジェクトをラップします。

たとえば、プログラマはを要求しACC-{UUID}、データベースはそのACC-部分を無視し、結果を取得して、すべてをとして返します{domain}-{UUID}

おそらく、ストアドプロシージャまたは関数を使用したハッカーでこれが可能になるかもしれませんが、いくつかの質問が思い浮かびます。

  • これ(各クエリでドメインを削除/追加する)はかなりのオーバーヘッドですか?
  • これも可能ですか?

ストアドプロシージャや関数を使用したことがないため、これが可能かどうかもわかりません。誰かが光を当てることはできますか?プログラマと保存されたデータの間に透明なレイヤーを追加できれば、それは完璧なソリューションのようです。

4.(私のお気に入り)IPv6として保存 cidr

はい、あなたはそれを正しく読みました。IPv6アドレス形式は私の問題を完全に解決することがわかりました

  • 最初の数オクテットでドメインとサブドメインを追加し、残りをランダム文字列として使用できます。
  • 衝突確率は OKです。(ただし、2 ^ 128は使用しませんが、それでも大丈夫です。)
  • 等値比較は(できれば)最適化されているため、単にを使用するよりもパフォーマンスが向上する可能性がありますbytea
  • containsドメインとその階層がどのように表されるかに応じて、実際にいくつかの興味深い比較を実行できます。

たとえば0000、ドメイン「製品」を表すためにコードを使用するとします。キー0000:0db8:85a3:0000:0000:8a2e:0370:7334は製品を表します0db8:85a3:0000:0000:8a2e:0370:7334

ここでの主な質問はbytea、と比較して、cidrデータ型を使用する上で主な利点または欠点はありますか?


5
いくつの分散ノードが可能ですか?事前にその数(および名前)を知っていますか?複合(複数列)PKを検討しましたか?ドメイン(最初の質問に依存)、および単純なシリアル列は、最小で最も単純で最速かもしれません...
Erwin Brandstetter

@Philありがとう!@ErwinBrandstetterアプリケーションに関しては、負荷に応じて自動スケーリングするように設計されているため、事前に情報がほとんどありません。(ドメイン、UUID)をPKとして使用することを考えましたが、これは全面的に「ドメイン」を繰り返しますが、ドメインはまだvarchar他の多くの問題の1つです。pgのドメインについては知りませんでした。特定のクエリが正しいオブジェクトを使用しているかどうかを検証するためにドメインが使用されているように見えますが、それでも整数以外のインデックスを持つことに依存しています。serialここで使用する「安全な」方法があるかどうかはわかりません(1つのロックステップなし)。
レナートシケイ

1
ドメインは必ずしもである必要はありませんvarchar。それをFK integer型にして、ルックアップテーブルを追加することを検討してください。そうすることで、人間が読みやすくなりPK、挿入/更新の異常(存在しないドメインを置く)からコンポジットを保護できます。
yemet


1
次のような形式の主キーが必要ですACC-f8kJd9xKCd」←それは古き良き複合PRIMARY KEYの仕事のようです。
MDCCL

回答:


5

を使用して ltree

IPV6が動作する場合、素晴らしい。「ACC」はサポートしていません。ltreeします。

ラベルパスは、ドットで区切られたゼロ個以上のラベルのシーケンスです。たとえば、L1.L2.L3は、階層ツリーのルートから特定のノードまでのパスを表します。ラベルパスの長さは65kB未満でなければなりませんが、2kB未満に保つことが望ましいです。実際には、これは大きな制限ではありません。たとえば、DMOZカタログ(http://www.dmoz.org)の最長ラベルパスは約240バイトです。

このように使用します

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;

サンプルデータを作成します。

SELECT x, (
  CASE WHEN x%7=0 THEN 'ACC'
    WHEN x%3=0 THEN 'XYZ'
    ELSE 'COM'
  END ||'.'|| md5(x::text)
  )::ltree
FROM generate_series(1,10000) AS t(x);

CREATE INDEX ON foo USING GIST (ltree);
ANALYZE foo;


  x  |                ltree                 
-----+--------------------------------------
   1 | COM.c4ca4238a0b923820dcc509a6f75849b
   2 | COM.c81e728d9d4c2f636f067f89cc14862c
   3 | XYZ.eccbc87e4b5ce2fe28308fd9f2a7baf3
   4 | COM.a87ff679a2f3e71d9181a67b7542122c
   5 | COM.e4da3b7fbbce2345d7772b0674a318d5
   6 | XYZ.1679091c5a880faf6fb5e6087eb1b2dc
   7 | ACC.8f14e45fceea167a5a36dedd4bea2543
   8 | COM.c9f0f895fb98ab9159f51fd0297e236d

そしてヴィオラ..

                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=103.23..234.91 rows=1414 width=57) (actual time=0.422..0.908 rows=1428 loops=1)
   Recheck Cond: ('ACC'::ltree @> ltree)
   Heap Blocks: exact=114
   ->  Bitmap Index Scan on foo_ltree_idx  (cost=0.00..102.88 rows=1414 width=0) (actual time=0.389..0.389 rows=1428 loops=1)
         Index Cond: ('ACC'::ltree @> ltree)
 Planning time: 0.133 ms
 Execution time: 1.033 ms
(7 rows)

詳細と演算子については、ドキュメントを参照してください

製品IDを作成している場合は、ltreeを使用します。それらを作成するために何かが必要な場合は、UUIDを使用します。


1

byteaとのパフォーマンス比較について。ネットワークの比較は、3つのステップで行われます。最初はネットワーク部分の共通ビット、次にネットワーク部分の長さ、そしてマスクされていないアドレス全体です。参照:network_cmp_internal

そのため、memcmpに移行するbyteaよりも少し遅くなります。1つの行を検索する1,000万行のテーブルで簡単なテストを実行しました。

  • 数値ID(整数)を使用すると、1000ミリ秒かかりました。
  • cidrを使用すると1300msかかりました。
  • byteaを使用すると1250msかかりました。

byteaとcidrには多くの違いがあるとは言えません(ギャップは一定のままですが)ただ追加のifステートメント-10mのタプルにとってそれほど悪くないことを推測してください。

それがお役に立てば幸いです-何を選んだのでしょうか。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.