--- title: Storage model description: How PizzaSQL maps tables, rows, schemas, and indexes onto PizzaKV keys, and what lives in memory versus on disk. --- PizzaSQL stores everything in **PizzaKV**, a key-value store, as JSON documents under a namespaced key scheme. There are no B-trees, no heap files, and no page format — just keys and values. ## Namespacing Every key is prefixed by the database name, so each database is an isolated namespace within the same PizzaKV store. Table and index names are lowercased in keys, making them case-insensitive at the storage layer. ## Key layout | Key pattern | Value | | --- | --- | | `:_sys:tables` | JSON array of table names (the catalog) | | `:_schema:` | JSON table schema (columns, primary key, `NextRowID`, `AutoIncrement`) | | `:_sys:rowid:
` | rowid counter state | | `:_data:
:` | a single row, as a JSON object | | `:indexes` | JSON array of index names | | `:index:` | JSON index definition (name, table, columns, unique) | | `:idx::` | index entry (value → rowids); **only ever cleared, never written** | The table schema is the source of truth for a table's columns and its `NextRowID` counter. Rows are keyed by their primary-key value; the rowid (`_rowid_`) is stored inside the JSON row itself. ## Rows are JSON A row is `map[string]interface{}` serialized as JSON. Consequences: - Numbers round-trip through JSON as floats, so integer columns are normalized back to `int64` after reading (see [Data types](/sql-reference/data-types/)). - `NULL` is JSON `null`. - BLOBs are strings. The storage layer reads a whole table by issuing a `reads :_data:
:` prefix scan and deserializing each value. ## The in-memory layer On top of PizzaKV, each `TableManager`/`SchemaManager` keeps caches: - **Row cache**: a table's rows, loaded on first `SELECT` and invalidated on write. - **Rowid map**: rowid → row, for index lookups. - **Index entry cache**: value → rowids for each warm index (see [Indexes](/engine/indexes/)). - **Schema cache**: table and index definitions. These caches are per database instance and are what make warm reads fast. They are derived state: after a restart they are rebuilt lazily from the durable keys. ## Rowid management The next rowid is tracked in the table schema's `NextRowID` field. On first use after startup, the engine scans the table's rows to recover `max(rowid) + 1` (so rowid state doesn't need its own write-ahead entry). `INTEGER PRIMARY KEY` aliases this rowid; other primary keys get a separate, invisible rowid counter. ## What is and isn't durable - **Durable**: table schemas, the catalog list, index definitions, and row data. - **Not durable** (derived/rebuilt): rowid counter (recovered from data), index entries, and all in-memory caches. This split is why index definitions survive a restart but their entries must be rebuilt, and why the first query after a cold start is slower than subsequent ones. ## Transactions and storage Transactions are handled at the SQL layer (an undo log), not by the key-value store. Each statement's writes go to PizzaKV immediately; `ROLLBACK` replays the undo log to restore prior values. There is no multi-key atomicity at the PizzaKV level — the undo log is what provides it for row writes (DDL is not covered; see [Transactions](/engine/transactions/)). ## The managed service's storage On database.pizza, each database is a PizzaSQL namespace inside a managed PizzaKV instance, provisioned and routed per organization. From your perspective the storage is opaque; the guarantees above still apply to your data, including the durability of schema/index definitions and row data.