GoSuda

Schottky: Go言語のためのゼロアロケーション、順序保持バイトキーエンコーディング

By Lemon Mint
views ...

LSMツリーまたはBツリーベースのキーバリューストアおよびデータベースインデックスを実装する際、複合フィールドを単一のバイトキーに結合しなければならないことがよくあります。

JSONやProtocol Buffersのような標準的なシリアライゼーション形式は、そのシリアライズされたバイト出力が符号なしバイト単位の比較(bytes.Compareまたはmemcmp)に必要な自然なソート順序を保持しないため、この目的に向けた設計にはなっていません。

Schottkyは、マルチタイプの複合タプルを順序保持バイトキーにエンコードするために設計されたGoのライブラリです。

1go get gosuda.org/schottky@latest

シリアライゼーション対ソートキー

正しいバイト単位のソートを保証するには、いくつかの低レベルなデータ表現の詳細に対処する必要があります:

  • 整数: 標準的な2の補数ビッグエンディアンエンコーディングは、最上位の符号ビットが原因で自然な順序が崩れます。符号なしバイト比較を正しく行うためには、符号ビットを反転させる必要があります。
  • 浮動小数点数: 符号ビットの調整、負の値に対する反転された順序付け、およびNaN-0の一貫した処理が必要です。
  • 可変長文字列およびバイトスライス: プレフィックスのソート順序を損なうことなく、フィールドの境界が保持されなければなりません。
  • 複合キーの要件: フィールドごとの独立したASC/DESC順序のサポート、切り離されたNULLS FIRST/LAST規則、厳密な辞書式順序の優先順位(前のフィールドが順序を決定する)、およびプレフィックススキャンとの互換性が必要です。

Schottkyは、存在タグと方向の向きを適用する前に、各値正規化されたペイロードに変換します。DESCフィールドの場合、ASCペイロードの各バイトはビット単位で反転(^b)されます。NULLの配置は専用の存在タグを介して処理され、ソート方向とは独立して動作します。

基本的な使用方法

次の例では、アカウントID(ASC, NULLS LAST)と名前(DESC, NULLS FIRST)で構成される複合キーを構築します:

 1package main
 2
 3import (
 4        "fmt"
 5
 6        "gosuda.org/schottky"
 7)
 8
 9func main() {
10        storage := make([]byte, 0, 128)
11        builder := schottky.NewBuilder(storage)
12
13        builder.Int64(42, schottky.AscNullsLast)
14        accountPrefixLen := builder.Len()
15
16        builder.String("Ada", schottky.DescNullsFirst)
17        key, err := builder.Key()
18
19        if err != nil {
20                panic(err)
21        }
22        accountPrefix := key[:accountPrefixLen]
23        fmt.Printf("key=%x\nprefix=%x\n", key, accountPrefix)
24}

Schottkyは4つの明示的なソート順序設定を提供します:

  • AscNullsFirst
  • AscNullsLast
  • DescNullsFirst
  • DescNullsLast

NULLの位置は暗黙的に推測されることはありません。無効なOrder値が渡された場合、ビルダーはErrInvalidOrderを記録し、これはKey()またはErr()を呼び出すときに返されます。

プレフィックススキャンと範囲境界

Schottkyの複合キーには、グローバルヘッダー、フィールド数メタデータ、型タグ、またはトレーラーが含まれていません。スキーマが事前にわかっていると仮定すると、フィールドのエンコーディングは単に連結されます。

このレイアウトのため、先行フィールドのエンコードされたバイトは、範囲スキャンのための有効なプレフィックスを形成します。上記の例では、accountPrefixをプレフィックスフィルターとして直接使用して、Account ID == 42であるすべてのレコードをスキャンできます。

半開区間[prefix, upper)の範囲スキャンの排他的上限を計算するには、PrefixUpperBoundを使用します:

 1upperStorage := make([]byte, 0, len(accountPrefix))
 2upper, finite, err := schottky.PrefixUpperBound(upperStorage, accountPrefix)
 3
 4if err != nil {
 5        panic(err)
 6}
 7
 8if finite {
 9        // 半開区間 [accountPrefix, upper) の範囲スキャン
10} else {
11        // 上限なしの開放範囲スキャン
12}

注意: Builder.Len()は、クリーンなフィールド境界で測定されなければなりません。フィールドの内部バイトストリームの内部でスライスを行うと、無効なプレフィックスが生成されます。

ゼロアロケーションとバッファ管理

キー生成は、データベースクリティカルパス上で頻繁に実行されます。ヒープアロケーションとバッファリサイズのおおもとのオーバーヘッドを排除するため、Builderは呼び出し元が提供したスライスの容量内で厳密に動作し、内部で再割り当てを行いません。

バッファの容量が不足した場合、部分的なバイトを書くことなくErrShortBufferが記録されます。フィールドの書き込みはアトミックであり、最初に遭遇したエラーはKey()またはErr()を介して確認されるまで保持されます。十分な容量を事前に提供することで、ゼロアロケーションエンコーディングが保証されます。

バッファサイズは、EncodedBytesSizeEncodedStringSizeEncodedDecimalSizeなどのヘルパー関数、または固定サイズ定数を使用して事前に計算できます。返されたキーは提供されたバッファを直接参照するため、メモリライフサイクルの管理は呼び出し元に委ねられます。

Decoderは対称的に動作します。入力キーから直接借用し、可変長フィールドに対して呼び出し元が提供する宛先バッファを要求し、末尾のバイトやスキーマの不一致を検出するためにRemaining() == 0を提供します。

サポートされているデータ型

  • 整数: 符号付きおよび符号なし(8ビットから64ビット)、Int128
  • 浮動小数点数および数値: Float32Float64、10進数テキスト
  • 基本型: バイナリ文字列、バイトスライス、ブール値、列挙型ランク
  • 日付と時刻: 日付、時刻、タイムゾーン付き時刻、タイムスタンプ、期間、カレンダー間隔
  • ネットワークと識別子: UUID、MAC、IP、IPプレフィックス、正規ネットワークプレフィックス、LSN
  • 複合構造: ネストされたタプル、範囲、および生の構造的エンコーディング
  • 照合(Collation): Unicode照合キーおよび外部正規トークン

SQL型のマッピングは、PostgreSQL 18のBツリーソート規則に準拠しています。データベースカタログまたは内部エンジン状態に依存する型は、外部正規トークンを渡すことによって処理されます。

文字列照合

Builder.Stringのデフォルトは、生のUTF-8バイナリ順序です。ロケールを意識したソートのために、Schottkyは並行安全で不変なCollatorを提供します:

  • 決定的照合: 照合の重みが同一である場合のタイブレーカーを提供するために、生のUTF-8バイトとともに照合キーをエンコードします。
  • 非決定的照合: 照合結果が等しい文字列を同一として扱い、生のバイトのタイブレーカーを省略します。

Unicodeおよびプロファイルのバージョンは、メタデータスキーマで追跡される必要があります。照合プロバイダーまたはプロファイル設定が変更された場合、既存のキーを再構築する必要があります。

スキーマ管理

Schottkyキーは生のヘッダーなしのバイトシーケンスであるため、スキーマレイヤーは以下を追跡する必要があります:

  1. フィールドの順序とデータ型。
  2. ソート方向(ASC/DESC)およびNULLの順序付け(NULLS FIRST/LAST)。
  3. 文字列の照合および正規化規則。
  4. Schottkyおよび照合プロファイルのバージョン。

異なるスキーマで生成されたキーを比較したり、一致しないスキーマに対してデコードしたりすると、順序の保証が損なわれます。

パフォーマンスとリンク

Go 1.27以降では、実験的なポータブルSIMDアクセラレーションをGOEXPERIMENT=simdを使用して有効にできます。スカラーパスとSIMDパスは、バイト単位で同一のキーを生成します。