"All orders for this customer, still open." "How many items in this cart." These are the reads an application does a thousand times a second, and in a relational database each one is a query with a planner behind it. kevy can keep the answer ready instead.
A key-value store is usually rejected for application data with a single objection: but I need to look things up by something other than the key. That objection is correct, and it is what secondary indexes are for.
An index is declared, not built. You name the key pattern and the field; the write path keeps it current. A filtered list becomes a lookup again — no planner, no scan, no query.
A view goes further and keeps an aggregate current on write, so a count or a total is read rather than computed. This is what most applications are actually asking their ORM for, and it is why their database is busy.
And a whole table can be declared at once. TABLE.DECLARE takes typed columns, secondary indexes and composite ORDER BY paths and compiles them to named indexes at declare time — kevy-sql does the same from the PG/MySQL schema file you already have. The engine still plans nothing and enforces no schema; joins and runtime SQL stay refused by name.
"All orders for customer 881" stays a lookup: declare an index per field, write normally, read by value.
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 1200IDX.CREATE idx:cust ON PREFIX order: FIELD customer TYPE i64 KIND range
IDX.CREATE idx:status ON PREFIX order: FIELD status TYPE str KIND rangeIDX.QUERY idx:cust EQ 881
-> 1) "0" # cursor
2) 1) "order:1001" 2) "881"
3) "order:1002" 4) "881"IDX.QUERY COMPOSE AND idx:cust EQ 881 idx:status EQ open
-> 1) "0"
2) 1) 1) "order:1001"Cost & limits Indexes are paid for on every write, not on read — the right trade for read-heavy serving, the wrong one for a write-heavy log. There are no joins, and there will not be: an index answers "which keys match these fields", not "join these two collections". If your read genuinely needs a join, keep it in Postgres — the RDS workloads page says which ones those are.
A filtered, ordered list maintained on the write path — the read never recomputes it, because it was never stale.
VIEW.CREATE v:open881 QUERY ( AND idx:cust EQ 881 idx:status EQ open ) ORDER BY idx:cust
-> OKThe parens are separate arguments.
VIEW.QUERY v:open881
-> 1) "0"
2) 1) "order:1001" 2) "881"Cost & limits A view is write-path work forever: every write to a matching key updates it, whether or not anyone reads it today. Declare views for the reads your application actually serves, and drop the ones it stops serving. The indexes a view composes must exist first.
The read path of a relational table — indexed WHERE, residual filter, ORDER BY, pagination, COUNT — compiled onto named indexes by one declaration. Or by your existing schema file.
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
-> OKRows stay ordinary hashes under the prefix — a missing column is NULL, and kevy-cli sql compile schema.sql emits this line from CREATE TABLE / CREATE INDEX.
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) 2IDX.QUERY orders.by_customer WHERE customer EQ 881 LIMIT 20 FIELDS status totalOne composite index answers it the way a relational composite index does — each customer's orders, largest first, no re-sort.
Cost & limits No runtime SQL and no joins. The server refuses SELECT as an unknown command; kevy-cli sql compile turns a PG/MySQL schema file into these declarations at build time and refuses JOIN, subqueries and GROUP BY by name, pointing at the recipe that replaces each. Uniqueness is verify-not-enforce, and constraints are recipes, not engine checks. With tiering on, index-only queries answer from RAM even when every row is cold — only the final FIELDS page reads cold rows, one read per row.