kevy
kevy 6.3.0 · 主ストア

読み取りを、参照のまま保つ

「この顧客の、まだ open な注文をすべて」。「このカートに何点入っているか」。これらはアプリケーションが毎秒何千回も行う読み取りであり、リレーショナルデータベースでは、そのひとつひとつが、裏にプランナを抱えたクエリになります。kevy なら、答えを用意しておけます。

なぜ向いているのか

キーバリューストアをアプリケーションのデータに使うことは、たいていひとつの反論で退けられます。キー以外のもので引きたいのだが、と。その反論は正しく、セカンダリインデックスは、まさにそのためにあります。

インデックスは、構築するものではなく宣言するものです。キーのパターンとフィールドを指定すれば、書き込みパスが、それを最新に保ちます。絞り込んだ一覧は、ふたたび参照になります。プランナもスキャンもクエリもありません。

ビューはさらに進んで、書き込み時に集計を最新に保ちます。件数や合計は、計算されるのではなく読まれます。ほとんどのアプリケーションが ORM に実際に求めているのはこれであり、データベースが忙しい理由も、これです。

そして、テーブル全体を一度に宣言できます。TABLE.DECLARE は型付きカラム、セカンダリインデックス、複合 ORDER BY パスを受け取り、宣言の時点で名前つきインデックスへコンパイルします——kevy-sql は、手元の PG/MySQL スキーマファイルから同じことをします。エンジンは相変わらず何もプランせず、スキーマを課しません。join とランタイム SQL は、名前つきで拒否されたままです。

キーではなく、フィールドで引く

「顧客 881 の注文をすべて」が、参照のままです。引きたいフィールドごとにインデックスを宣言し、普通に書き、値で読みます。

データは、いままでどおり書く
HSET order:1001 customer 881 status open  total 4400
HSET order:1002 customer 881 status paid  total 8400
HSET order:1003 customer 902 status open  total 1200
引きたいフィールドごとに、インデックスを 1 つ
IDX.CREATE idx:cust   ON PREFIX order: FIELD customer TYPE i64 KIND range
IDX.CREATE idx:status ON PREFIX order: FIELD status   TYPE str KIND range
クエリになるはずだった読み取り
IDX.QUERY idx:cust EQ 881
-> 1) "0"                       # cursor
   2) 1) "order:1001"  2) "881"
      3) "order:1002"  4) "881"
条件を 2 つ、同時に
IDX.QUERY COMPOSE AND idx:cust EQ 881 idx:status EQ open
-> 1) "0"
   2) 1) 1) "order:1001"

コストと制約 インデックスの代金は、読み取りではなく書き込みのたびに支払います——読み取り主体の配信には正しく、書き込み主体のログには間違った取引です。結合はありませんし、今後も持ちません。インデックスが答えるのは「どのキーがこれらのフィールドに合致するか」であって、「この 2 つのコレクションを結合せよ」ではありません。読み取りに本当に結合が要るなら、Postgres に置いておいてください——どれがそれに当たるのかは、RDS ワークロードのページに書いてあります。

更新され続ける答えを、用意しておく

絞り込まれ、順序のついた一覧を、書き込みパスが維持します——古くなることが決してないので、読み取りが計算し直すこともありません。

同じインデックスの上に、ビューを宣言する
VIEW.CREATE v:open881 QUERY ( AND idx:cust EQ 881 idx:status EQ open )  ORDER BY idx:cust
-> OK

括弧は、それぞれ独立した引数です。

読む——ここでは何も計算されません
VIEW.QUERY  v:open881
-> 1) "0"
   2) 1) "order:1001"  2) "881"

コストと制約 ビューは書き込みパスの、終わらない仕事です。合致するキーへの書き込みは、今日それを読む人がいるかどうかに関係なく、毎回ビューを更新します。アプリケーションが実際に捌いている読み取りのためにビューを宣言し、捌かなくなったら落としてください。ビューが組み合わせるインデックスは、先に存在している必要があります。

テーブル全体を、一度で宣言する

リレーショナルなテーブルの読み取りパス——インデックスされた WHERE、残りのフィルタ、ORDER BY、ページング、COUNT——を、宣言 1 つで名前つきインデックスへコンパイルします。手元のスキーマファイルからでも。

カラムも、インデックスも、ソートパスも、宣言 1 つで
TABLE.DECLARE orders PREFIX order: PK id COLUMN id str COLUMN customer i64 COLUMN status str COLUMN total f64 INDEX status range VALUES total customer ORDERPATH by_customer ON customer THEN total DESC
-> OK

行はプレフィックス配下の普通のハッシュのままです——欠けたカラムは NULL。kevy-cli sql compile schema.sql が、CREATE TABLE / CREATE INDEX からこの行を出力します。

保存されたカラムでフィルタと集計——行は 1 つも読みません
IDX.QUERY orders.status EQ open FILTER total RANGE 2000 inf LIMIT 20
-> 1) "0"
   2) 1) "order:1001"  2) "open"

IDX.COUNT orders.status EQ open
-> (integer) 2
ORDER BY customer, total DESC の歩き方
IDX.QUERY orders.by_customer WHERE customer EQ 881 LIMIT 20 FIELDS status total

リレーショナルの複合インデックスと同じやり方で、複合インデックス 1 本が答えます——顧客ごとの注文を、大きい順に、再ソートなしで。

コストと制約 ランタイム SQL も join もありません。サーバーは SELECT を未知のコマンドとして拒否します。kevy-cli sql compile はビルド時に PG/MySQL のスキーマファイルを上の宣言に変え、JOIN、サブクエリ、GROUP BY を名前つきで拒否して、それぞれを置き換えるレシピを指し示します。一意性は強制ではなく検証で、制約はエンジンのチェックではなくレシピです。ティアリングをオンにすれば、index-only クエリは全行コールドでも RAM だけで答えます——コールドな行を読むのは最後の FIELDS ページだけ、1 行 1 回です。

次に

ガイド

テーブル

型付きカラムとインデックスを一度宣言すれば、テーブルのように引ける。

読む →

ガイド

kevy での設計

テーブルで考えることに慣れた頭で、キーで考える方法。

読む →

ガイド

セカンダリインデックス

どう構築され、何を要し、クエリプランをどう説明するのか。

読む →

リファレンス

RDS ワークロード

リレーショナルな全パターンと、ここでやる場合の正直なコスト。

読む →