UUIDまたはキャッシュシーケンス?


人々が代理キーのUUIDを選ぶ理由の一つはスケーラビリティです.それはシーケンスのような中央の発電機と同期させることなくユニークな値を生成するため.UUIDを使用する他の理由がありますが、このポストはスケーラビリティに関するものです.ほとんどのデータベースでは、シーケンスジェネレータをキャッシュでスケーラブルにすることができます.これは、シーケンスを管理する中央カタログオブジェクトを次の値が必要なたびに読み込まなくてはならないことを意味します.データベースがシーケンスキャッシュを提供しない場合でも、これは非常には、アプリケーションから1つを模倣することは簡単ですが、シーケンスから読んで、50を乗算し、1から50までのローカルカウンタに追加するには、開始番号として使用します.これは、データベースは、真理値の中心点から次の値を1回だけごとに50次の値を一度に読み取ります.
あなたの挿入率がまだシーケンス番号世代でボトルネックに管理するならば、あなたはキャッシュを増やすことができます.32767個の挿入ごとに中央のシーケンスからの読み込みはおそらく無視できるオーバーヘッドです.なぜ私は32767を入れていますか?これはPostgreSQLのbigintの8バイトから2バイトかかります.したがって、キャッシュされた値が使用されていなくても(それはプールの接続が再利用されるときだけ起こるべきである)、bigintから1 E 14の数字のままである.あなたは、私がどこに行っているかを見ます:巨大なキャッシュでさえ、8バイトのシーケンスはスケーラビリティに十分です.

PostgreSQL
PostgreSQLでは、セッションごとにシーケンスキャッシュがあります.あなたが絶えず接続して、切断するならば、あなたは毎回キャッシュ範囲を浪費します.しかし、シーケンスの外でさえ、これはデータベースが使用されるべきではありません.接続はバックエンドでのプロセスの作成であり、トランザクションごとにそれを行うと、無駄なシーケンスキャッシュより大きな問題が発生します.アプリケーションサーバーは接続プールを使用し、データベースセッションを再利用します.プールからグラブした各セッションでnextval ()を読み込む、次のPythonの例を作りました.これは5つの同時スレッドと5つの接続に成長できる接続プールで行われます.
import sqlalchemy
from   sqlalchemy import Sequence
import threading

yb=sqlalchemy.create_engine('postgresql+psycopg2://franck:[email protected]:5433/yb_demo_northwind',pool_size=1,max_overflow=4)

def mythread():
 print(threading.current_thread().name)
 for i in range(100):
  nextid = yb.connect().execute(Sequence('myseq'))
  print(f'{nextid:6d} from {threading.current_thread().name:8s} {yb.pool.status()}');

yb.connect().execute('drop sequence if exists myseq; create sequence myseq cache 32767')

mythreads=[]
for i in range(5):
 t=threading.Thread(target=mythread)
 mythreads.append(t)
 t.start()

for i in mythreads:
 t.join()

exit();
以下は出力の始まりです.
     1 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     2 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     3 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     4 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     5 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     6 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 32768 from Thread-4 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     7 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 65535 from Thread-5 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 98302 from Thread-2 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
131069 from Thread-3 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 32769 from Thread-4 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     8 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 65536 from Thread-5 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
131070 from Thread-3 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 98303 from Thread-2 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 32770 from Thread-4 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
     9 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 65537 from Thread-5 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
131071 from Thread-3 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 98304 from Thread-2 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
 32771 from Thread-4 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
    10 from Thread-1 Pool size: 1  Connections in pool: 1 Current Overflow: 4 Current Checked out connections: 4
...
私は毎回別のセッションを使用している場合でも、彼らは接続プールから取得され、シーケンスキャッシュはまだ前のものからです.スレッド1は最初に開始し、プールから最初の接続を取得し、1 - 32767のキャッシュされた範囲内のシーケンスから数値を使用します.接続がリサイクルされるならば、それがすべてを使わないかもしれません、しかし、確かに数の浪費はキャッシュ価値ほど大きくありません.他のスレッドは、32768、65535で始まる追加接続を取得します.そして、この範囲の値を使用する.それで、この解決策で、あなたは時折2バイト範囲の小さい部分だけを浪費します.UUIDと比較して、UOIDと比較して心配することは何もありません(UUIDは16バイトです).
(ここでは、この例を使用したい場合はkaggleです).

ユガシテ
私はPostgreSQLを言いました、しかし、私は完全にPostgresと互換性があるyugabytedbデータベースに接続していました.同じクエリ層が使用されますが、ストレージ層が異なります.yugabyteにおいて、すべてのシーケンスは、複製因子によって複製されて、ノード衝突またはネットワーク失敗のイベントでまだ利用可能である分散テーブル(「SystemSound Postgres」.シーケンスのリーダータブレットが別のノードにあるかもしれないので、それを読む待ち時間はより高いです.そして、他のノードに複製されるので、quorumに書く待ち時間もモノリスPostgreSQLより高いです.分散データベースでの待ち時間への説明された含意のために、データベースを訪問しなければならないのに十分に大きいキャッシュを構成するのを強く勧めて、新しい価値を得ることの潜在的な罰を被ります.キャッシュを明示的に定義していない場合でも、パフォーマンスが良好であることを確認するには、YugabyteDBのデフォルト値をPostgreSQLで1の代わりに100に増やし、ysql_sequence_cache_minval flagで設定できます.挿入率が十分に速くキャッシュが非常に速く使い尽くされるならば、それは非常に高く行くことを勧められます.UUIDが乱数であるので、すべての数がUGIDを使用して、ギャップについて心配しないでください.
彼らの名前をdespites、シーケンスジェネレータは、数字のギャップシリーズではなく、ユニークな数字を提供するために効率的です.主要キー、代理キーについて言うにはたくさんあります.私は、10月にJoker 2021でこれについて議論します:SQL primary key, surrogate key, composite keys, foreign keys... and JPA
もちろん、PostgreSQLをクエリ層として使用しているyugabytedbでは、pgcryptoがデフォルトでcreate extensionまで準備ができているので、gentoo random ocuid ()でUUIDを生成できます.UUID OSSPをインストールすることもできます.
しかし、分散されたデータベースのキャッシュされたシーケンスのもう一つの利点を見ましょう.同じセッションから生成された場合でも、すべての行を配布したい場合があります.そして、プライマリキーをハッシュシェイディングで定義するならば、これはケースです.
このように:
create table t1 (
 id bigint generated always as identity
 ( start with 1 cache 32767 ),
 value text,
 constraint t1_id primary key ( id HASH ));
または、一緒に挿入された行をクラスタ化することを好むかもしれません.
create table t1 (
 id bigint generated always as identity
 ( start with 1 cache 32767 ),
 value text,
 constraint t1_id primary key ( id ASC ));
あなたも、範囲を定義することができます.同じ範囲にある場合、キャッシュ範囲は別の場所に行を置きます.それは選択であり、また、Automatic Tablet Splittingに依存することができます.

要するに
複数のオプションがあります.UUIDを使用する理由は、サイズやパフォーマンスとは無関係です.例えば、ログのUUIDを見ても、セキュリティ監査の問題が発生することはありませんが、シーケンス内で生成された番号を持つことで、お客様のビジネス情報に関するいくつかの推測が可能になります.これは、高い数から始めることによって軽減されることができます、そして、それはキャッシュによって非常にランダムにされます、しかし、とにかく、セキュリティポリシーはそれを許しません.
しかし、懸念がスケーラビリティを明確にあなたの目標を定義する必要があります.一見して、キャッシュはすべてのセッションで共有されなければならない状態を維持しなければならず、それからスケーラブルではないようです.しかし、大きなキャッシュで、シーケンスを照会して、更新することは、まれな活動になります.適切な構成では、シーケンスのすべての利点を得ることができます.数の上昇する性質は、それが好まれるならば、バルク負荷を加速するために一緒に列をクラスタリングするのを助けます、あるいは、それが多くのノードに負荷を分配するのが好ましいならば、ハッシュShardingによって広げられることができます.
サイズも問題.主キーと外部キーの16バイトはbigintの8バイトに比べて大きい.そして、整数を比較するか、増やすような操作は、UUIDを十分にランダムに生成するのと比較するとき、非常に単純なCPU操作です.
次のテストはUUIDとシーケンスのパフォーマンスの違いを示します.これはPostgreSQLやyugabytedbで実行できます:
create extension pgcrypto;
\timing on
create sequence myseq cache 32767;
select count(nextval('myseq') ) from generate_series(1,10000000);
select count(gen_random_uuid()) from generate_series(1,10000000);
この大きなキャッシュでは、1秒あたり300万個のユニークな数字が生成されますが、UUIDに対して1秒間に100万個以下となります.

シーケンスキャッシュサイズについて考えたことがないなら、シーケンスがスケーラブルではないと思う良いチャンスがあります.問題はSQLシーケンスではなく、しばしばあまりにも小さいデフォルト値を維持することです.