kevy4.0

ビュー(VIEW.* / view_*

ビューとは、宣言済みインデックスの名前付き合成です。インデックスの形(shape)からなるAND/OR/DIFFの木に、順序づけ用のインデックスを添えたものを、1つの単位としてクエリできます。評価はクエリごとに行う(virtual)か、書き込みのたびに増分維持する(materialized、任意でtop-K有界)かを選べます。ビューは「ホットリスト」クエリ——WHERE state = 'ready' AND pri BETWEEN 0 AND 100 ORDER BY pri DESC LIMIT 10——に対するエンジンの答えであり、クエリ時のスキャンではなく、宣言されたアクセスパスとして提供されます。

IDX.CREATE j_pri  ON PREFIX job: FIELD pri  TYPE i64 KIND range
IDX.CREATE j_state ON PREFIX job: FIELD state TYPE str KIND range

VIEW.CREATE ready_jobs
    QUERY ( AND j_pri RANGE 0 100 j_state EQ ready )
    ORDER BY j_pri DESC
    MODE materialized TOPK 100
VIEW.QUERY ready_jobs LIMIT 10

クイックスタート(サーバー)

ビューが合成するものは、すべて先に存在していなければなりません。葉もORDER BY用インデックスも、同じプレフィックスドメイン上で宣言されたIDX.*インデックスです(indexes.md)。

kevy-cli -p 6004 IDX.CREATE j_pri ON PREFIX job: FIELD pri TYPE i64 KIND range
kevy-cli -p 6004 IDX.CREATE j_state ON PREFIX job: FIELD state TYPE str KIND range
kevy-cli -p 6004 VIEW.CREATE ready_jobs QUERY ( AND j_pri RANGE 0 100 j_state EQ ready ) ORDER BY j_pri DESC MODE materialized TOPK 100
kevy-cli -p 6004 HSET job:1 pri 10 state ready
kevy-cli -p 6004 HSET job:2 pri 99 state blocked
kevy-cli -p 6004 VIEW.QUERY ready_jobs LIMIT 10

応答はkey, order-valueの組を、ビューの順序で、再開用のCURSORとともにページングします。木の文法は次のとおりです(1行、括弧つき)。

tree     = '(' AND|OR|DIFF sub sub ')' | leaf
leaf     = <index> RANGE <min> <max> | <index> EQ <value>

補助verb:VIEW.LIST(カタログ + モード + 形)、VIEW.EXPLAIN name(葉ごとのカーディナリティつきの木)、VIEW.VERIFY name(members / bytes / order-exclusions——bytesとorder-exclusionsはmaterializedのときだけです。virtualなビューは何も保存しないので両方0を報告し、しかもそれをVERIFYすると新規のeval_treeをまるごと1回支払います。virtualなビューが高くつく唯一の場所がここです)、VIEW.REBUILD name(materializedの再構築を強制する——答えは保存されます)、VIEW.DROP name

構造上の3つのルール

  1. 構成要素は名前付きインデックスである。 葉は形(RANGE min max | EQ v。CREATE時に参照先インデックスの型へ強制されます)を持ちます。ビュー層は自前の述語を一切持ちません。木は深さ3以下、葉は4つ以下です。AND/ORはエンジンが並べ替えることがあり、DIFFは左マイナス右で固定です。
  2. ビューが保存するのはメンバーシップと順序だけであり、フィールドの値は決して保存しません。ORDER BY <index>がソートキーを供給します。順序インデックスに存在しない行は除外されます(数えられ、VIEW.VERIFYで可視になります)。
  3. hydrationは参照解決であって、クエリではない。 VIA <template>(たとえばuser:{key.1}{key}はメンバーキー、{key.N}はその:区切りN番目のセグメント)が各メンバーをターゲットキーへ写像します。VIEW.QUERY … FIELDS f…は、2回目の内部ファンアウトで、それらのターゲットを所有するシャード上でフィールドを読みます。ターゲットが存在しなければ行のフィールドはnilになります。ターゲットに述語を付けることはできません。

モードとコストモデル

選び方。書き込みの多いドメイン上のダッシュボードtop-100ならMODE materialized TOPK 100です(メモリが有界、非候補をO(1)で棄却)。ほどほどのドメイン上でときどきページングされるレポートならMODE virtualです(書き込み税ゼロ、常に最新)。VIEW.EXPLAINは葉ごとのカーディナリティを見せます——この判断のための選択率データです。

DIFF:宣言されたanti-join

DIFFは左マイナス右であり、可換ではありません。エンジンが子を決して並べ替えない、唯一の木ノードです。定番の用途は「資格はあるが、除外されていないもの」です。

VIEW.CREATE assignable
    QUERY ( DIFF j_pri RANGE 0 100 j_state EQ quarantined )
    ORDER BY j_pri DESC
    MODE virtual

——稼働中の優先度帯にあるジョブすべてから、隔離されたものを引いたもの、ということです。

SQLで言えばこれはWHERE … AND NOT EXISTS (…)の形にあたります。ただしクエリごとのサブクエリではなく、宣言されたアクセスパスとして提供されます(この語彙の残りはrds-workloads.mdが写像します)。

整合性

インデックスと同じ包絡線です(indexes.md)。トリガーとなった書き込みとシャード単位でアトミックであり、シャードをまたぐ場合はグローバルスナップショットなしにマージされます(SCANクラス)。

組み込み

index cargoフィーチャ(デフォルトで有効)の背後にある型付きAPIです。木は値として渡すので、プロセス内にテキスト文法は登場しません。そしてすべての呼び出しがKevyResultを返します(4.0の単一エラー通貨)。

use kevy_embedded::{
    Config, IndexKind, IndexValType, IndexValue, Store, ViewLeaf,
    ViewMode, ViewTree,
};

fn main() -> kevy_embedded::KevyResult<()> {
    let store = Store::open(Config::default())?;
    store.idx_create(b"j_pri", b"job:", b"pri", IndexValType::I64,
                     IndexKind::Range)?;
    store.idx_create(b"j_state", b"job:", b"state", IndexValType::Str,
                     IndexKind::Range)?;

    let tree = ViewTree::And(
        Box::new(ViewTree::Leaf(ViewLeaf {
            index: b"j_pri".to_vec(),
            min: IndexValue::I64(0),
            max: IndexValue::I64(100),
        })),
        Box::new(ViewTree::Leaf(ViewLeaf {
            index: b"j_state".to_vec(),
            min: IndexValue::Str(b"ready".to_vec()),
            max: IndexValue::Str(b"ready".to_vec()),   // EQ = same min/max
        })),
    );
    store.view_create(b"ready_jobs", tree, b"j_pri", /*desc*/ true,
                      ViewMode::Materialized { top_k: 100 })?;

    store.hset(b"job:1", &[
        (b"pri".as_slice(), b"10".as_slice()),
        (b"state".as_slice(), b"ready".as_slice()),
    ])?;

    // One page: (Vec<(key, order_value)>, Option<resume_cursor>).
    let (rows, _next) = store.view_query(b"ready_jobs", None, 10)?;
    for (key, val) in &rows {
        println!("{} {val:?}", String::from_utf8_lossy(key));
    }
    Ok(())
}

パフォーマンス

計測された包絡線です。clampはbench/viewgate.shにあり、100万行の実サーバーに対して走ります。

メンバーあたりのメモリは概算でorder_value_width + key_len + 48バイトで、VIEW.VERIFYがライブで報告します。

関連項目