sqlite-zstd - SQLite の透過的な辞書ベースの行レベル圧縮

(Transparent dictionary-based row-level compression for SQLite)

Created at: 2020-02-29 20:33:26
Language: Rust
License: LGPL-3.0

sqlite-zstd

sqlite の透過的な辞書ベースの行レベル圧縮を提供する sqlite の拡張機能。これにより、基本的に、DB ファイル全体を圧縮する場合とほぼ同じように sqlite データベースのエントリを圧縮できますが、ランダム アクセスは維持されます。

いくつかの動機、ベンチマーク、とりとめのないことについては、アナウンスのブログ投稿も参照してください: https://phiresky.github.io/blog/2022/sqlite-zstd

サイズ比較表

データによっては、パフォーマンスをほぼ同じに保ちながら、データベースのサイズを 80% 削減できます (ディスクから読み取るデータが小さいため、パフォーマンスが向上することさえあります)。

ユースケースによっては、 https://github.com/mlin/sqlite_zstd_vfsなどの圧縮 VFS の方が適している場合があることに注意してください。これには非常に異なるトレードオフと機能がありますが、最終的な結果は似ています。

透過圧縮

  • zstd_enable_transparent(config)

    指定されたテーブルの指定された列の透過的な行レベルの圧縮を有効にします。

    圧縮する列が異なる同じテーブルで、この関数を複数回呼び出すことができます。

        SELECT
            zstd_enable_transparent('{"table": "objects", "column": "data1", "compression_level": 19, "dict_chooser": "''a''"}'),
            zstd_enable_transparent('{"table": "objects", "column": "data2", "compression_level": 19, "dict_chooser": "''a''"}')
    

    データは に移動されますが、 は SELECT、INSERT、UPDATE、および DELETE クエリを含む通常どおりクエリできるビューになります

    _table_name_zstd
    table_name
    この関数はそれ自体ではデータを圧縮しません
    zstd_incremental_maintenance
    。後で呼び出す必要があります。

    config
    構成を記述する json オブジェクトです。詳細については、 TransparentCompressConfigを参照してください。

    圧縮がアクティブな場合、次の違いが適用されます。

    • 圧縮された列には、宣言されたデータ型の親和性に応じて、
      blob
      またはデータのみが含まれる場合があります(たとえば、問題はありませんが、そうではありません)。
      text
      VARCHAR(10)
      int
    • どの行でも主キーを null にすることはできません。そうしないと、更新が期待どおりに機能しない可能性があります
    • sqlite3_changes() はクエリの変更に対して 0 を返します (こちらを参照)。
    • いずれにせよ、ブロブは完全にメモリにコピーされるため、SQLite ストリーミング ブロブ読み取り API はあまり役に立ちません。
    • を使用した圧縮テーブルを含むデータベースのアタッチ
      ATTACH 'foo.db'
      はサポートされていません。
    • DDL ステートメント (ALTER TABLE や CREATE INDEX など) は部分的にしかサポートされていません
  • zstd_incremental_maintenance(duration_seconds: float | null, db_load: float) -> bool

    指定された時間をかけて増分メンテナンス操作を実行します。これにより、辞書がトレーニングされ、TransparentCompressConfig で指定されたグループ化に基づいてデータが圧縮されます。

    duration_seconds
    : 指定された時間が 0 の場合、1 つのステップを実行して、できるだけ早く終了します。指定された時間が null の場合、保留中のすべてのメンテナンスが完了するまで実行されます。

    db_load
    : データベースが書き込みクエリでロックされる時間の比率を指定します。例: 0.5 に設定すると、各書き込み操作に 2 秒かかると、メンテナンス機能が 2 秒間スリープするため、他のプロセスはデータベースに対して書き込み操作を実行する時間ができます。1 に設定すると、メンテナンスはスリープしません。これは、インクリメンタル メンテナンス関数を他のロジックとは別のスレッドまたはプロセスで実行する場合にのみ役立つことに注意してください。期間とデータベース負荷の両方がベストエフォートであることに注意してください。データベースが一度にロックされたままになる時間について正確な保証はありません。

    実行すべき作業がさらにある場合は 1 を返し、すべてが適切に圧縮されている場合は 0 を返します。

    この関数の各呼び出しには、 に相当する起動時間のコストがあるため

    select * from table where dictid is null
    、期間が長いほど効率的であることに注意してください。

    この関数はいつでも安全に中断できます。圧縮作業の各チャンクはアトミック操作として実行されます。

    例:

    • zstd_incremental_maintenance(null, 1)
      : すべてをできるだけ速く圧縮します。データベースが現在使用されていない場合に便利です。
    • zstd_incremental_maintenance(60, 0.5)
      : 保留中のものを圧縮するために 60 秒を費やし、他のクエリを 50% の時間実行できるようにします。

    出力例:

    sqlite> select zstd_incremental_maintenance(null, 1);
      [2020-12-23T21:11:31Z WARN  sqlite_zstd::transparent] Warning: It is recommended to set `pragma busy_timeout=2000;` or higher
      [2020-12-23T21:11:40Z INFO  sqlite_zstd::transparent] events.data: Total 5.20GB to potentially compress.
      3[2020-12-23T21:13:22Z INFO  sqlite_zstd::transparent] Compressed 6730 rows with dictid=109. Total size of entries before: 163.77MB, afterwards: 2.12MB, (average: before=24.33kB, after=315B)
      [2020-12-23T21:13:43Z INFO  sqlite_zstd::transparent] Compressed 4505 rows with dictid=110. Total size of entries before: 69.28MB, afterwards: 1.60MB, (average: before=15.38kB, after=355B)
      [2020-12-23T21:14:06Z INFO  sqlite_zstd::transparent] Compressed 5228 rows with dictid=111. Total size of entries before: 91.97MB, afterwards: 1.41MB, (average: before=17.59kB, after=268B)
    

基本機能

  • zstd_compress(data: text|blob, level: int = 3, dictionary: blob | int | null = null, compact: bool = false) -> blob

    指定されたデータを圧縮レベル (1 ~ 22、デフォルト 3) で圧縮します。

    • 辞書が blob の場合、直接使用されます
    • 辞書が int i の場合、機能的には次と同等です。
      zstd_compress(data, level, (select dict from _zstd_dict where id = i))
    • ディクショナリが存在しない、null、または -1 の場合、データはディクショナリなしで圧縮されます。

    コンパクトが真の場合、出力はマジック ヘッダーなし、チェックサムなし、および dictid なしになります。これにより、辞書を使用しない場合は 4 バイト、辞書を使用する場合は 8 バイトが節約されます。これは、標準ツールを使用して通常の zstd アーカイブとしてデータをデコードできないことも意味します。同じ圧縮引数を decompress 関数にも渡す必要があります。

  • zstd_decompress(data: blob, is_text: bool, dictionary: blob | int | null = null, compact: bool = false) -> text|blob

    指定されたデータを解凍します。辞書が間違っている場合、結果は未定義です

    • 辞書が blob の場合、直接使用されます
    • 辞書が int i の場合、機能的には と同等
      zstd_decompress(data, (select dict from _zstd_dict where id = i))
      です。
    • ディクショナリが存在しない、null、または -1 の場合、データはディクショナリなしで圧縮されたと見なされます。

    ディクショナリを int として渡すことをお勧めします。これは、ディクショナリを一度だけ準備する必要があるためです。

    is_text は、データをテキストとして出力するか、blob として出力するかを指定します。テキストとして出力する場合、エンコーディングは sqlite データベースのエンコーディングに依存することに注意してください。sqlite-zstd は UTF-8 でのみテストされています。

    圧縮関数もコンパクトで呼び出された場合は、コンパクトを指定する必要があります。

  • zstd_train_dict(agg, dict_size: int, sample_count: int) -> blob

    指定された集計データの sample_count サンプルで zstd 辞書をトレーニングするための集計関数 (sum() や count() など)

    使用例:

    select zstd_train_dict(tbl.data, 100000, 1000) from tbl
    1000 サンプルでトレーニングされたサイズ 100kB の辞書を返します。
    tbl

    推奨されるサンプル数は、ターゲット ディクショナリ サイズの 100 倍です。例として、次のように「最適な」サンプル数で 100kB の dict をトレーニングできます。

    select zstd_train_dict(data, 100000, (select (100000 * 100 / avg(length(data))) as sample_count from tbl))
                    as dict from tbl

    dict_size と sample_count は定数と見なされることに注意してください。

  • zstd_train_dict_and_save(agg, dict_size: int, sample_count: int) -> int

    と同じ

    zstd_train_dict
    ですが、辞書が
    _zstd_dicts
    テーブルに保存され、ID が返されます。

コンパイル中

このプロジェクトは 2 つのモードでビルドできます: (a) Rust ライブラリとして、(b) 純粋な SQLite 拡張 (を使用

--features build_extension
) として。

GitHub リリースから SQLite 拡張バイナリを取得できます。別の方法として、拡張機能を手動でビルドすることもできます。

cargo build --release --features build_extension
# should give you target/release/libsqlite_zstd.so

使用法

このライブラリは、SQLite 拡張機能または Rust ライブラリとしてロードできます。sqlite 拡張機能は永続的ではないため、データベースに接続するたびにロードする必要があることに注意してください。

このライブラリの生産準備は整っていますか?

私は自分のデータでそれを信頼しません(まだ)。すべてのバックアップがあることを確認してください。また、圧縮されていないデータをコピーして移行することはもちろん問題なく動作するはずですが、将来の更新の下位互換性についても保証していません。

Sqlite CLI

REPLにロードするか:

$ sqlite3 file.db
SQLite version 3.34.0 2020-12-01 16:14:00
sqlite> .load .../libsqlite_zstd.so
[2020-12-23T21:30:02Z INFO  sqlite_zstd::create_extension] [sqlite-zstd] initialized
sqlite>

または、次のようにします。

sqlite3 -cmd '.load libsqlite_zstd.so' 'select * from foo'

C API

int success = sqlite3_load_extension(db, "libsqlite_zstd.so", NULL, NULL);

詳しくはこちらをご覧ください。

さび

推奨される方法は

sqlite_zstd
、プロジェクトに依存関係として追加し、次を使用してロードすることです

let conn: rusqlite::Connection;
sqlite_zstd::load(&conn)?;

または、他の拡張機能と同じように拡張機能をロードできます。

let conn: rusqlite::Connection;
conn.load_extension("libsqlite_zstd.so", None)?;

詳しくはこちらをご覧ください。

冗長性/デバッグ

ログレベルを変更するには、環境変数を設定して

SQLITE_ZSTD_LOG=error
ロギングを少なくし、ロギング
SQLITE_ZSTD_LOG=debug
を増やします。

将来の仕事 / アイデア / Todo

  • 辞書なしで初期費用を調べる
  • 圧縮された列のインデックスを正しく処理します (ビューの代わりに生成された列、おそらく vtables を試してください。sqlite 開発者に尋ねてください)
  • パフォーマンスのために異なるスレッドで圧縮を行います (たとえば、zstd で .multithread(1) を使用しますか?)
  • タイプ アフィニティは int パス スルーに干渉し
    insert into compressed (col) values (1)
    ます - 列のタイプがテキストとして宣言されている場合、integer ではなく typeof(col) = text になります - これにより、解凍が失敗し、「文字列を取得しましたが、zstd 圧縮データは常に blob です」 "
  • 圧縮された列の型を blob などに変更するか、整数のパススルーを許可しない