Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 76 additions & 4 deletions site/guides/performance.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,76 @@
- page_size
- memory mapping
- in-memory index
- chunk_size (?)
# Performance tuning

This page describes the knobs sqlite-vec exposes for tuning the on-disk and
in-memory performance of `vec0` tables.

## `chunk_size` table option

`vec0` stores vectors in fixed-size chunks. Each chunk holds up to
`chunk_size` vectors in a contiguous run, plus a per-chunk rowid array and a
bit-packed validity bitmap. sqlite-vec allocates a new chunk for a partition
only when the current chunk is full, and a chunk is reclaimed only when every
slot in it has been deleted or moved.

The `chunk_size` is a table-level option set with `CREATE VIRTUAL TABLE`:

```sql
CREATE VIRTUAL TABLE items USING vec0(
embedding float[1536],
chunk_size=64
);
```

### Contract

- **Default**: `1024`.
- **Range**: `8` to `4096`, inclusive.
- **Constraint**: must be divisible by `8`.
- **Scope**: per vec0 table. Two `vec0` tables in the same database may
declare different `chunk_size` values; the choice does not leak between
tables.
- **Immutability**: the option is set at `CREATE VIRTUAL TABLE` time and is
fixed for the lifetime of the table. To change it, drop and recreate the
table.

Invalid values raise an error at `CREATE VIRTUAL TABLE` time:

```sql
-- too large
CREATE VIRTUAL TABLE v USING vec0(a float[4], chunk_size=8200);
-- vec0 constructor error: chunk_size too large

-- not divisible by 8
CREATE VIRTUAL TABLE v USING vec0(a float[4], chunk_size=7);
-- vec0 constructor error: chunk_size must be a multiple of 8

-- malformed
CREATE VIRTUAL TABLE v USING vec0(a float[4], chunk_sizex=100);
-- Unknown table option: chunk_sizex
```

### Choosing a value

A smaller `chunk_size` reduces the empty-slot waste at the tail of the last
chunk in a partition. A larger `chunk_size` reduces the per-chunk metadata
overhead (the rowid array and validity bitmap are sized per chunk, not per
vector).

| Workload characteristic | Recommended `chunk_size` |
| -------------------------------------------- | ------------------------ |
| Dense, large corpus (>10k vectors/partition) | `1024` (default) |
| Sparse per-partition corpus (<1k) | `64`–`128` |
| Small test corpus at minimum size | `8` (minimum) |

The on-disk savings from a smaller `chunk_size` come entirely from the
unused slots at the end of the last chunk in each partition. Once a
partition is dense, reducing `chunk_size` no longer saves space and only
increases the metadata overhead.

### Per-table shadow storage

Each `vec0` table owns its own shadow tables (`<name>_chunks`,
`<name>_info`, `<name>_rowids`, `<name>_vector_chunks00`,
`<name>_auxiliary`). With a different `chunk_size`, the shadow tables for
each table differ in row count and bitmap width, but they do not interact.
Creating two `vec0` tables with different `chunk_size` values in the same
database does not cause shared state or contention between them.
84 changes: 84 additions & 0 deletions tests/test-loadable.py
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,90 @@ def test_vec0_constructor():
db.execute("create virtual table v using vec0(4)")


def test_chunk_size_64_knn_and_per_table_isolation():
# chunk_size=64 is a useful mid-range option that:
# - accepts insert of a corpus that crosses the 64-row chunk boundary,
# - returns the same top-k ordering as default-chunk_size on the same
# vector set, and
# - keeps per-table shadow state isolated when a second vec0 table in
# the same database uses a different chunk_size.
db.execute("drop table if exists t_cs64")
db.execute("drop table if exists t_cs1024")

# chunk_size=64 is accepted and the table is creatable.
db.execute(
"create virtual table t_cs64 using vec0("
"row_id text primary key, embedding float[4] distance_metric=cosine, chunk_size=64)"
)

# Insert 65 rows: forces the second chunk to materialize at the 64-row
# boundary. Each row uses a distinct primary key.
for i in range(65):
db.execute(
"insert into t_cs64(row_id, embedding) values (?, ?)",
[f"id_{i}", _f32([(i % 13) * 0.1, (i % 7) * 0.05, i * 0.001, 1.0])],
)

# The shadow `chunks` table must record at least two chunks at chunk_size=64.
chunk_count = db.execute(
"select count(*) from t_cs64_chunks where size = 64"
).fetchone()[0]
assert chunk_count >= 2, (
f"expected at least 2 chunks at chunk_size=64, got {chunk_count}"
)

# KNN over chunk_size=64 returns the same top-k ordering as chunk_size=1024
# on the same vector set. KNN correctness is preserved across small chunk
# boundaries — chunk_size is a storage-only knob.
db.execute(
"create virtual table t_cs1024 using vec0("
"row_id text primary key, embedding float[4] distance_metric=cosine, chunk_size=1024)"
)
for i in range(65):
db.execute(
"insert into t_cs1024(row_id, embedding) values (?, ?)",
[f"id_{i}", _f32([(i % 13) * 0.1, (i % 7) * 0.05, i * 0.001, 1.0])],
)

query = _f32([0.1, 0.05, 0.001, 1.0])
cs64_top = [
r[0]
for r in db.execute(
"select row_id from t_cs64 where embedding match ? and k = 5 order by distance",
[query],
).fetchall()
]
cs1024_top = [
r[0]
for r in db.execute(
"select row_id from t_cs1024 where embedding match ? and k = 5 order by distance",
[query],
).fetchall()
]
assert cs64_top == cs1024_top, (
f"top-k ordering diverged between chunk_size=64 and chunk_size=1024: "
f"{cs64_top} vs {cs1024_top}"
)

# chunk_size is per-table — the shadow tables reflect each table's choice
# independently, with no cross-table leakage.
cs64_size = db.execute(
"select distinct size from t_cs64_chunks"
).fetchall()
cs1024_size = db.execute(
"select distinct size from t_cs1024_chunks"
).fetchall()
assert cs64_size == [(64,)], (
f"t_cs64 shadow table should report only size=64, got {cs64_size}"
)
assert cs1024_size == [(1024,)], (
f"t_cs1024 shadow table should report only size=1024, got {cs1024_size}"
)

db.execute("drop table t_cs64")
db.execute("drop table t_cs1024")


def test_vec0_indexed_by_flat():
db.execute("drop table if exists t_ibf")
db.execute("drop table if exists t_ibf2")
Expand Down