docs: restructure the Documentation tab around catalog objects

The tab opened on Quickstart, buried "What is LanceDB?" inside it, and put
everything else under a single Guides group that had to be expanded before any
section was reachable. Model training sat outside it for no reason.

Reorganize around what a user does, in order: Introduction, Catalog, Ingestion,
Search, Feature engineering, Training, Maintenance, SQL. Guides is gone as a
level, so each section is one click.

New pages: catalog overview and database, and a compaction page under
Maintenance. tables/update covered four separate operations, so it becomes the
Ingestion section — insert, merge insert, update, delete — with insert drawing
on the large-dataset material from tables/create. Its old URL redirects.

Agentic skills move to their own tab after API Reference.
This commit is contained in:
Jack Ye
2026-09-09 21:33:24 -07:00
parent 96bfdcd937
commit 44f4b959ca
13 changed files with 943 additions and 525 deletions
+94
View File
@@ -0,0 +1,94 @@
---
title: "Database"
sidebarTitle: "Database"
description: "The root of a catalog. Connecting to one, and the operations it holds."
icon: database
---
A database is the root of a catalog. A connection opens exactly one, and every
other object is reached through it.
## Connecting {#connecting}
```python
import lancedb
db = lancedb.connect("./.lancedb") # a local directory
db = lancedb.connect("s3://bucket/path") # object storage
db = lancedb.connect("db://my-database", api_key="...") # Cloud or Enterprise
```
The URI decides what kind of database you get, and that decision reaches
further than it looks. A local or object-store URI opens the data directly from
your process. A `db://` URI talks to a deployment, which is what makes
server-side execution — [functions](/feature-engineering/functions), remote refreshes,
[SQL](/sql) — available, and what makes a few local-only features unavailable.
`connect_async` returns an `AsyncConnection` with the same surface as
coroutines.
### Storage options {#storage-options}
`storage_options` configures the underlying store — credentials, endpoints, and
per-table defaults for tables created through this connection:
```python
db = lancedb.connect(
"s3://bucket/path",
storage_options={"new_table_enable_stable_row_ids": "true"},
)
```
<Warning>
`new_table_enable_stable_row_ids` is worth setting deliberately. Stable row ids
cannot be enabled after a table exists, and a
[materialized view](/feature-engineering/materialized-views) requires its source to have
them. A table created without them cannot later become a view's source without
being recreated.
</Warning>
See [Storage](/storage) for the full set.
## Operations {#operations}
| | |
| --- | --- |
| `db.table_names()` | list the tables in the database |
| `db.create_table(name, data)` | create a table — see [Table](/tables) |
| `db.open_table(name)` | open an existing table |
| `db.drop_table(name)` | drop one table |
| `db.drop_database()` | drop everything in the database |
Each accepts `namespace_path` to act inside a [namespace](/namespaces)
rather than at the root.
```python
db.table_names()
tbl = db.open_table("documents")
db.drop_table("documents")
```
<Warning>
`drop_database()` removes every table in the database. There is no confirmation
and no undo — table [versioning](/tables/versioning) does not survive the
table being dropped.
</Warning>
## Read consistency {#consistency}
`read_consistency_interval` controls how often a connection checks for versions
committed by other writers. The default reads a snapshot taken when the table
was opened, so a long-lived connection will not see other processes' writes
until it reopens.
Set it when several processes write the same table and readers must observe
those writes:
```python
from datetime import timedelta
db = lancedb.connect(uri, read_consistency_interval=timedelta(seconds=5))
```
The cost is a metadata check on that interval. See
[Consistency](/tables/consistency).
+58
View File
@@ -0,0 +1,58 @@
---
title: "Catalog"
sidebarTitle: "Overview"
description: "What lives in a LanceDB catalog: databases, namespaces, tables, views, and functions."
icon: sitemap
---
A catalog is what LanceDB knows about. Everything you create lives in one and is
addressed through it — the objects below, arranged in a hierarchy.
This section covers what each object is and the basic operations on it. Where an
object has depth — how to search a table, how to refresh a view — the page links
to the section that covers it rather than repeating it.
## The objects {#objects}
| Object | What it is |
| --- | --- |
| [Database](/catalog/database) | The root. A connection opens one. |
| [Namespace](/namespaces) | A named grouping of tables inside a database. |
| [Table](/tables) | Rows and columns, versioned. The thing most operations act on. |
| [Materialized view](/feature-engineering/materialized-views) | A table whose contents come from a query, computed and stored. |
| [View](/catalog/view) | A saved query. Computed when read, not stored. |
| [Function](/feature-engineering/functions) | Registered code, applied to produce column values. |
## How they nest {#hierarchy}
```
database
└── namespace
├── table
├── materialized view (a table, defined by a query)
├── view (a query, not stored)
└── function
```
A namespace can contain further namespaces, so the hierarchy is as deep as your
organisation needs. A table is reached by its path through that hierarchy.
## Views and materialized views are different objects {#views}
The names are close enough to mislead. Both are defined by a query over other
tables; the difference is when that query runs.
- A **materialized view** stores its rows. A refresh computes them, and reads
are reads of stored data — fast, and as current as the last refresh.
- A **view** stores only the query. Every read runs it, so a view is never stale
and never faster than the query it wraps.
Reach for a materialized view when the query is expensive and the data changes
less often than it is read. Reach for a view when correctness matters more than
latency, or when the query is cheap.
## What is not here {#elsewhere}
Indexes belong to a table but are covered under [Maintenance](/indexing),
because creating one is a decision about performance rather than about
structure.
+96 -79
View File
@@ -26,90 +26,75 @@
}
},
"favicon": "/static/favicon.ico",
"redirects": [
{
"source": "/tables/update",
"destination": "/ingestion/update"
}
],
"navigation": {
"tabs": [
{
"tab": "Documentation",
"groups": [
{
"group": "Get started",
"group": "Introduction",
"pages": [
"quickstart",
{
"group": "What is LanceDB?",
"pages": [
"index",
"lance",
"tables-and-namespaces"
]
},
"performance"
"index",
"quickstart"
]
},
{
"group": "Model training",
"pages": [
"training/why-lancedb",
"training/index",
"training/torch",
"training/object-detection",
"training/vlm-finetuning"
]
},
{
"group": "Guides",
"group": "Catalog",
"pages": [
"catalog/index",
"catalog/database",
{
"group": "Table operations",
"pages": [
"tables/index",
"tables/create",
"tables/multimodal",
"tables/schema",
"tables/update",
"tables/versioning",
"tables/branching",
"tables/consistency"
]
},
{
"group": "Namespaces",
"group": "Namespace",
"pages": [
"namespaces/index",
"namespaces/usage"
]
},
{
"group": "Embeddings",
"group": "Table",
"pages": [
"embedding/index",
"embedding/quickstart"
]
},
{
"group": "Indexing",
"pages": [
"indexing/index",
"indexing/vector-index",
"indexing/fts-index",
"indexing/scalar-index",
"indexing/quantization",
"indexing/reindexing"
]
},
{
"group": "Search",
"pages": [
"search/index",
"search/vector-search",
"search/multivector-search",
"search/full-text-search",
"search/fts-examples",
"search/hybrid-search",
"search/filtering",
"search/optimize-queries"
"tables/index",
"tables/create",
"tables/schema",
"tables/multimodal"
]
},
"feature-engineering/materialized-views",
"feature-engineering/functions",
"lance",
"tables-and-namespaces"
]
},
{
"group": "Ingestion",
"pages": [
"ingestion/index",
"ingestion/insert",
"ingestion/merge-insert",
"ingestion/update",
"ingestion/delete",
"tables/versioning",
"tables/branching",
"tables/consistency"
]
},
{
"group": "Search",
"pages": [
"search/index",
"search/vector-search",
"search/multivector-search",
"search/full-text-search",
"search/fts-examples",
"search/hybrid-search",
"search/filtering",
"search/optimize-queries",
{
"group": "Reranking",
"pages": [
@@ -123,19 +108,10 @@
]
},
{
"group": "Storage",
"group": "Embedding",
"pages": [
"storage/index",
"storage/configuration",
"storage/monitoring"
]
},
{
"group": "Build with AI agents",
"expanded": true,
"pages": [
"build-with-ai-agents",
"agent-branch-experiments"
"embedding/index",
"embedding/quickstart"
]
}
]
@@ -144,9 +120,36 @@
"group": "Feature engineering",
"pages": [
"feature-engineering/index",
"feature-engineering/computed-columns",
"feature-engineering/materialized-views",
"feature-engineering/functions"
"feature-engineering/computed-columns"
]
},
{
"group": "Training",
"pages": [
"training/why-lancedb",
"training/index",
"training/torch"
]
},
{
"group": "Maintenance",
"pages": [
"indexing/index",
"indexing/vector-index",
"indexing/fts-index",
"indexing/scalar-index",
"indexing/quantization",
"indexing/reindexing",
"maintenance/compaction",
{
"group": "Storage",
"pages": [
"storage/index",
"storage/configuration",
"storage/monitoring"
]
},
"performance"
]
},
{
@@ -266,7 +269,9 @@
"tutorials/agents/multimodal-agent/index"
]
},
"tutorials/feature-engineering/index"
"tutorials/feature-engineering/index",
"training/object-detection",
"training/vlm-finetuning"
]
}
]
@@ -316,6 +321,18 @@
]
}
]
},
{
"tab": "Agentic Skills",
"groups": [
{
"group": "Agentic Skills",
"pages": [
"build-with-ai-agents",
"agent-branch-experiments"
]
}
]
}
]
},
+73
View File
@@ -0,0 +1,73 @@
---
title: "Delete rows"
sidebarTitle: "Delete"
description: "Remove rows by predicate, and reclaim their space."
icon: trash
---
These examples use the table set up in
[Ingesting data](/ingestion), and the connection made there.
import {
PyDeleteOperation as DeleteOperation,
PyUpdateOptimizeCleanup as UpdateOptimizeCleanup,
TsDeleteOperation,
TsUpdateOptimizeCleanup,
RsDeleteOperation,
RsUpdateOptimizeCleanup,
} from '/snippets/tables.mdx';
## Delete rows {#delete-rows}
Delete operations **soft delete** rows that match a given condition.
The underlying data is not immediately removed, but is marked
for deletion (in the [deletion files](https://lance.org/format/table/#deletion-files) at the Lance format level) and excluded from query results.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{DeleteOperation}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsDeleteOperation}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsDeleteOperation}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 20 |
<Warning>
**Deleting rows removes them from the index**
When rows are deleted, those rows are also excluded from the index segments, so indexed queries will not return them either. If ALL the rows are deleted (i.e., the table is emptied), ensure that you recreate the index after ingesting new data.
</Warning>
To permanently remove deleted rows, you can optimize the table, which will run compaction and cleans up the soft-deleted rows, which frees up storage space.
- In LanceDB OSS, compaction and cleanup are manual. Run `table.optimize()` regularly to free up disk space.
- In LanceDB Enterprise, files aren't cleaned up by default. You can configure automatic compaction and cleanup behavior at cluster setup time to suit your organization's retention policy.
By default, table cleanup removes data up to 7 days ago. If you need to reclaim space from deleted rows more aggressively, manually call `table.optimize()` use a shorter retention window as follows:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateOptimizeCleanup}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateOptimizeCleanup}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateOptimizeCleanup}
</CodeBlock>
</CodeGroup>
+107
View File
@@ -0,0 +1,107 @@
---
title: "Ingesting data"
sidebarTitle: "Overview"
description: "The four write operations, and the example table the rest of this section uses."
icon: inbox
---
import {
PyUpdateConnectEnterprise as UpdateConnectEnterprise,
PyUpdateConnectLocal as UpdateConnectLocal,
PyUpdateExampleTableSetup as UpdateExampleTableSetup,
TsUpdateConnectEnterprise,
TsUpdateConnectLocal,
TsUpdateExampleTableSetup,
RsUpdateConnectEnterprise,
RsUpdateConnectLocal,
RsUpdateExampleTableSetup,
RsUpdateMakeUsersReader,
} from '/snippets/tables.mdx';
## Choose a write method {#choose-a-write-method}
| Family | Method | Use this when... |
| --------------- | --------------- | ---------------- |
| [`add`](/ingestion/insert) | `add(data)` | You are appending new rows and do not need to match against existing ones. |
| `update` | `update(where=..., values=...)` | You want to edit rows that already exist, using a SQL filter. |
| `merge_insert` | `.when_matched_update_all()` | You have incoming rows and want to update keys that already exist in the table. |
| `merge_insert` | `.when_not_matched_insert_all()` | You have incoming rows and want to insert keys that do not exist yet. |
| `merge_insert` | `.when_matched_update_all()` + `.when_not_matched_insert_all()` | You want both behaviors together (often called **upsert**: update existing keys **and** insert missing keys in the same operation). |
| `merge_insert` | `.when_not_matched_by_source_delete(...)` | You want to remove target rows that are missing from the incoming source set. |
Write operations return status metadata. For example, `update` reports updated row count and
committed `version`, `delete` reports deleted row count and `version`, and `merge_insert` reports
inserted, updated, deleted, retry-attempt, and `version` fields. Writes require a mutable table
handle; after checking out an older version for reads, call `checkout_latest` / `checkoutLatest`
before modifying data.
The committed `version` advances even for writes that affect zero rows, such as a delete predicate
that matches nothing.
## Connect to LanceDB {#connect-to-lancedb}
Connect to your local LanceDB instance:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateConnectLocal}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateConnectLocal}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateConnectLocal}
</CodeBlock>
</CodeGroup>
Or, connect to LanceDB Enterprise:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateConnectEnterprise}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateConnectEnterprise}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateConnectEnterprise}
</CodeBlock>
</CodeGroup>
In the Rust snippets, a `make_users_reader` helper is used to build Arrow input data.
<CodeGroup>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateMakeUsersReader}
</CodeBlock>
</CodeGroup>
## Create the example table {#create-the-example-table}
We'll start by creating a simple table with `id`, `name`, and `login_count` columns. All examples below use the same table.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateExampleTableSetup}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateExampleTableSetup}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateExampleTableSetup}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 20 |
The example above shows a PyArrow schema. You can just as well create the table using other
table creation patterns (Pandas, Polars, Pydantic, iterators, etc.) -- see [Creating a table](/tables/create) for more details.
+120
View File
@@ -0,0 +1,120 @@
---
title: "Insert data"
sidebarTitle: "Insert"
description: "Append rows to a table, including datasets too large to hold in memory."
icon: plus
---
Rows are appended with `table.add()`. It accepts the same shapes
[table creation](/tables/create) does — lists of objects, Arrow tables, pandas
and polars frames, Pydantic models — and commits a new table version.
These examples use the table set up in [Ingesting data](/ingestion).
import {
PyAddFromDataset as AddFromDataset,
PyCreateTableFromIterator as CreateTableFromIterator,
TsCreateTableFromIterator as TsCreateTableFromIterator,
RsCreateTableFromIterator as RsCreateTableFromIterator,
TsAddProgress,
} from '/snippets/tables.mdx';
## Ingesting large datasets {#large-datasets}
When ingesting large datasets, use `table.add()` on an existing table rather than
passing all data to `create_table()`. The `add()` method auto-parallelizes large
writes, while `create_table(name, data)` does not.
<Tip>
For best performance with large datasets, create an empty table first and then call
`table.add()`. This enables automatic write parallelism for materialized data sources.
</Tip>
### From files (Parquet, CSV, etc.) {#from-files-parquet-csv-etc}
<Badge color="green">Python Only</Badge>
For file-based data, pass a `pyarrow.dataset.Dataset` to `table.add()`. This streams
data from disk without loading the entire dataset into memory.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{AddFromDataset}
</CodeBlock>
</CodeGroup>
<Note>
`pa.dataset()` input is currently Python-only. TypeScript and Rust support for
file-based dataset ingestion is tracked in
[lancedb#3173](https://github.com/lancedb/lancedb/issues/3173).
</Note>
### From iterators (custom batch generation) {#from-iterators-custom-batch-generation}
When you need custom batch logic — generating embeddings on the fly, transforming
rows from an external source, etc. — use an iterator of `RecordBatch` objects.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{CreateTableFromIterator}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsCreateTableFromIterator}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsCreateTableFromIterator}
</CodeBlock>
</CodeGroup>
Use this pattern when:
- Your source data already arrives in Arrow batches, readers, datasets, or streams.
- Materializing the entire ingest as one giant in-memory list or array would be too expensive.
- You want to control chunk size explicitly during ingestion.
Python can also consume iterators of other supported types like Pandas DataFrames or Python lists.
### Write parallelism {#write-parallelism}
<Note title="Automatic parallelism">
For materialized data (`pa.Table`, `pd.DataFrame`, `pa.dataset()`), LanceDB
automatically parallelizes large writes — no configuration needed. Auto-parallelism
targets approximately 1M rows or 2GB per write partition.
For streaming sources (iterators, `RecordBatchReader`), LanceDB cannot determine
total size upfront. A `parallelism` parameter to control this manually is planned
but not yet exposed in Python or TypeScript
([tracking issue](https://github.com/lancedb/lancedb/issues/3173)).
</Note>
### Tracking ingestion progress {#tracking-ingestion-progress}
<Badge color="green">TypeScript Only</Badge>
For long-running writes, pass a `progress` callback to `table.add()` to surface
per-batch progress in your UI, logs, or metrics pipeline. The callback fires
once per batch written and once more with `done: true` when the write completes.
Each invocation receives a `WriteProgress` object:
| Field | Description |
|:------|:------------|
| `outputRows` | Rows written so far. |
| `outputBytes` | Bytes written so far. |
| `totalRows` | Expected total rows when the input source reports one. Always set on the final callback. |
| `elapsedSeconds` | Wall-clock seconds since the write started. |
| `activeTasks` | Parallel write tasks currently in flight. |
| `totalTasks` | Total parallel write tasks (the write parallelism). |
| `done` | `true` only for the final callback. |
<CodeGroup>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsAddProgress}
</CodeBlock>
</CodeGroup>
A few things to know before you wire this up:
- Back-pressures the writer: callback invocations are serialized and run inline with each batch, so a slow callback will slow the write rather than drop updates. Every batch update is delivered, and the final `done: true` callback always fires (even on error or cancellation). Keep the callback cheap — offload heavy work to a queue you drain elsewhere.
- Errors swallowed: anything your callback throws is logged with `console.warn` and won't abort the write, so keep the callback side-effect-only and don't rely on it for control flow.
- Row totals: `totalRows` is only populated when the input source can report it up front (for example, a materialized `arrow.Table`). For streaming sources it stays `undefined` until the final callback, where it falls back to the actual rows written.
+210
View File
@@ -0,0 +1,210 @@
---
title: "Merge insert"
sidebarTitle: "Merge insert"
description: "Reconcile incoming rows against a key: update what matches, insert what does not."
icon: code-merge
---
These examples use the table set up in
[Ingesting data](/ingestion), and the connection made there.
import {
PyMergeMatchedUpdateOnly as MergeMatchedUpdateOnly,
PyInsertIfNotExists as InsertIfNotExists,
PyMergeUpdateInsert as MergeUpdateInsert,
PyMergeDeleteMissingBySource as MergeDeleteMissingBySource,
PyMergePartialColumns as MergePartialColumns,
TsMergeMatchedUpdateOnly,
TsInsertIfNotExists,
TsMergeUpdateInsert,
TsMergeDeleteMissingBySource,
TsMergePartialColumns,
RsMergeMatchedUpdateOnly,
RsInsertIfNotExists,
RsMergeUpdateInsert,
RsMergeDeleteMissingBySource,
RsMergePartialColumns,
} from '/snippets/tables.mdx';
## Merge incoming rows by key {#merge-incoming-rows-by-key}
Merging is different from updating because it involves comparing incoming rows to existing rows by key, and then choosing what to do based on whether the key exists in the target table or not.
The `merge_insert(""..."")` method lets you do this.
In merge operations, rows are split into three groups:
- **Matched**: key exists in both source and target.
- **Not matched**: key exists only in source.
- **Not matched by source**: key exists only in target.
Conditional merge clauses can compare old and new values. Use the `target.` prefix for the
existing table row and `source.` for the incoming row, for example
`target.last_update < source.last_update`.
<Tip>
**Use scalar indexes to speed up merge insert**
The merge insert command performs a join between the input data and the target table `on` the key you provide. This requires scanning that entire column, which can be expensive for large tables. To speed up this operation, create a scalar index on the join column, which will allow LanceDB to find matches without scanning the whole table.
Read more about scalar indices in the [Scalar Index](/indexing/scalar-index/) guide.
</Tip>
<Info>
If you see this HTTP 400 error from `merge_insert`: `Bad request: Merge insert cannot be performed because the number of unindexed rows exceeds the maximum of 10000`. Verify that the scalar index on the join column is up to date before retrying.
</Info>
<Note>
**Rust: build merge predicates with DataFusion expressions**
In the Rust SDK, you can pass a `datafusion_expr::Expr` directly instead of a SQL string by using
`when_matched_update_all_expr` and `when_not_matched_by_source_delete_expr` on `MergeInsertBuilder`.
This is useful when you are constructing predicates programmatically and want to avoid building a SQL string.
These methods are only supported on local tables. Calling them against a remote table returns a
`NotSupported` error — use the SQL string variants (`when_matched_update_all` /
`when_not_matched_by_source_delete`) for remote tables.
</Note>
Like the create table and add APIs, the merge insert API will automatically compute embeddings based on the [embedding registry](/embedding/index#embedding-registry) if the table has an embedding definition in its schema.
During `merge_insert`, if the input data doesn't contain the source column (i.e., the original field used to generate embeddings, such as text for a text embedding model or `image_uri` for an image model), or if a vector value is already provided, LanceDB skips embedding generation for that row. Embeddings are only auto-generated when that source field is present in the incoming data, **and** the vector field is empty.
Primary keys in LanceDB are metadata used by operations such as `merge_insert`; they are not
enforced as uniqueness constraints on ordinary writes. Keep using `merge_insert` or your own
deduplication logic when you need key-based upsert semantics.
### Update matched rows only {#update-matched-rows-only}
This updates keys that already exist in the target table. Source rows with new keys are ignored.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergeMatchedUpdateOnly}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergeMatchedUpdateOnly}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergeMatchedUpdateOnly}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 21 |
### Insert unmatched rows only {#insert-unmatched-rows-only}
This inserts only brand-new keys from the source. Existing keys are left unchanged.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{InsertIfNotExists}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsInsertIfNotExists}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsInsertIfNotExists}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 20 |
| 3 | Charlie | 5 |
### Update matched rows and insert unmatched rows {#update-matched-rows-and-insert-unmatched-rows}
Use both `when_matched_update_all()` and `when_not_matched_insert_all()` when you want to update existing keys and insert missing keys in one operation.
<Info>
This is a conventional **upsert**.
</Info>
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergeUpdateInsert}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergeUpdateInsert}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergeUpdateInsert}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 21 |
| 3 | Charlie | 5 |
### Delete target rows that are missing from source {#delete-target-rows-that-are-missing-from-source}
Use `when_not_matched_by_source_delete()` when you want to remove any target row that does not appear in the incoming source data.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergeDeleteMissingBySource}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergeDeleteMissingBySource}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergeDeleteMissingBySource}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 2 | Bobby | 21 |
| 3 | Charlie | 5 |
In the example above, LanceDB matches rows by `id`. Rows with `id=2` and `id=3` exist in both the table and incoming data, so they are updated. Row `id=1` exists only in the target, so it is deleted.
### Use partial columns in merge updates {#use-partial-columns-in-merge-updates}
Merge updates do not require you to provide values for all columns. You can provide only a subset of columns in source rows. For matched rows, only the provided columns are updated.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergePartialColumns}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergePartialColumns}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergePartialColumns}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 20 |
| 3 | Charlie | null |
Note that in the example above, when `merge_insert` creates a new row, any missing columns are written as `null`. If a missing column is non-nullable in your schema, the insert will fail.
+78
View File
@@ -0,0 +1,78 @@
---
title: "Update rows"
sidebarTitle: "Update"
description: "Change values in rows that already exist, by predicate or SQL expression."
icon: pen
---
These examples use the table set up in
[Ingesting data](/ingestion), and the connection made there.
import {
PyUpdateOperation as UpdateOperation,
PyUpdateUsingSql as UpdateUsingSql,
TsUpdateOperation,
TsUpdateUsingSql,
RsUpdateOperation,
RsUpdateUsingSql,
} from '/snippets/tables.mdx';
## Update rows {#update-rows}
Use `update` when you already know which target rows to modify and you do not need to compare against an incoming dataset.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateOperation}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateOperation}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateOperation}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 20 |
<Warning title="Warning">
Updating nested columns is not yet supported.
</Warning>
## Update rows with SQL expressions {#update-rows-with-sql-expressions}
Use `values_sql` when you want to use SQL-like expressions to update rows. This is useful for operations like incrementing a counter, or setting a column value based on another column.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateUsingSql}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateUsingSql}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateUsingSql}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 21 |
<Note title="SQL syntax">
See the [SQL queries](/search/sql/) page for more information on the supported SQL syntax.
</Note>
When rows are updated, they are moved out of any existing index. The row will still show up in search queries, but the query will not be as fast as it would be if the row was in the index. If you update a large proportion of rows, consider triggering an index rebuild afterwards.
+101
View File
@@ -0,0 +1,101 @@
---
title: "Compaction and cleanup"
sidebarTitle: "Compaction"
description: "Merge small files, prune old versions, and fold new rows into existing indexes."
icon: broom
---
Every write commits a new table version, and versions accumulate: small files
from frequent appends, superseded files from updates and deletes, and rows added
since an index was built. `optimize()` addresses all three.
It is modelled after `VACUUM` in PostgreSQL — routine housekeeping that a table
needs periodically rather than a repair for something broken.
## What it does {#what-it-does}
```python
table.optimize()
```
Three operations, in one call:
| | |
| --- | --- |
| **Compaction** | merges small files into larger ones |
| **Prune** | removes versions older than the retention window |
| **Index** | folds newly added rows into existing indexes |
The third is the one people miss. An [index](/indexing) does not
automatically cover rows appended after it was built — those rows are still
searchable, but by scanning rather than through the index. Running `optimize()`
is what brings them under it.
## Retention {#retention}
```python
from datetime import timedelta
table.optimize(cleanup_older_than=timedelta(days=30))
```
`cleanup_older_than` defaults to **7 days**. Versions older than that are
removed, along with the files only they referenced.
The latest version is never removed, whatever you pass — `timedelta(0)` prunes
everything except it.
<Warning>
Pruning is what makes [time travel](/tables/versioning) finite. A version
outside the retention window is gone, and a tag or branch pointing at it no
longer resolves.
If you rely on reading old versions, set the window to cover the range you need
before running this, not after.
</Warning>
## Files from failed transactions {#unverified}
A write that fails partway can leave files that look like an operation still in
progress. `optimize()` will not remove them until they are at least seven days
old, because it cannot tell them apart from a live write.
```python
table.optimize(delete_unverified=True)
```
<Warning>
`delete_unverified=True` removes them regardless of age. Only set it when you
can guarantee **no other process is writing to the table** — otherwise it can
delete files belonging to a write that is genuinely in progress, and corrupt the
table.
</Warning>
## Retraining indexes {#retrain}
```python
table.optimize(retrain=True)
```
Folding new rows into an index preserves the structure the index was trained on.
When the data has shifted enough that the structure itself is a poor fit — a
vector index whose clusters no longer reflect the distribution — `retrain=True`
rebuilds it rather than extending it.
More expensive than a normal optimize, and worth it after a large ingest that
changed the shape of the data rather than just its volume.
## When to run it {#when}
There is no background compactor: `optimize()` runs when you call it.
Frequent small appends are what make it necessary, because each one commits
files. A table written in a few large batches needs it rarely; one taking
continuous small writes needs it on a schedule.
<Note>
Compaction rewrites files, which conflicts with a concurrent
[computed-column refresh](/feature-engineering/computed-columns) or
[view refresh](/feature-engineering/materialized-views) planned against the
version it replaces. Sequence them rather than running both at once.
</Note>
+1 -1
View File
@@ -223,7 +223,7 @@ How you apply a validated change depends on the type of work:
### Upsert selected branch rows into `main` {#upsert-selected-branch-rows-into-main}
If the result you want to retain is a set of inserted or updated rows, use
[`merge_insert`](/tables/update#merge-incoming-rows-by-key) to write them to
[`merge_insert`](/ingestion/merge-insert#merge-incoming-rows-by-key) to write them to
`main`. Despite its name, `merge_insert` is a row-ingestion operation: it
matches incoming rows by key and does not merge branch histories.
+2 -2
View File
@@ -117,7 +117,7 @@ You can change this behavior with two parameters that resolve the conflict in di
<Note>
`exist_ok` / `existOk` does not append the provided data to an existing table. Use
[`table.add()`](/tables/update) for that. If you need to ensure a table exists *and*
[`table.add()`](/ingestion/insert) for that. If you need to ensure a table exists *and*
contains specific rows, prefer the [empty-table-then-add pattern](#create-empty-table).
</Note>
@@ -398,7 +398,7 @@ that has been extended to support LanceDB specific types like `Vector`.
</CodeGroup>
Once the empty table has been created, you can append to it or modify its contents,
as explained in the [updating and modifying tables](/tables/update) section.
as explained in [Ingesting data](/ingestion).
## Open an existing table {#open-an-existing-table}
+3 -3
View File
@@ -649,11 +649,11 @@ advanced features of LanceDB. Below are some suggested next pages.
Learn the different approaches to creating and ingesting data into LanceDB tables from various sources.
</Card>
<Card
title="Update and modify tables"
title="Ingest and modify data"
icon="clone"
href="/tables/update/"
href="/ingestion"
>
Learn how to update and modify existing LanceDB tables and their data.
Insert, merge, update and delete rows in an existing table.
</Card>
<Card
title="Schema & data evolution"
-440
View File
@@ -1,440 +0,0 @@
---
title: "Updating and Modifying Table Data"
sidebarTitle: "Update/modify data"
description: "Learn how to update, merge, and delete rows in a LanceDB table."
icon: "clone"
---
import {
PyUpdateConnectEnterprise as UpdateConnectEnterprise,
PyUpdateConnectLocal as UpdateConnectLocal,
PyUpdateExampleTableSetup as UpdateExampleTableSetup,
PyUpdateOperation as UpdateOperation,
PyUpdateUsingSql as UpdateUsingSql,
PyMergeMatchedUpdateOnly as MergeMatchedUpdateOnly,
PyInsertIfNotExists as InsertIfNotExists,
PyMergeUpdateInsert as MergeUpdateInsert,
PyMergeDeleteMissingBySource as MergeDeleteMissingBySource,
PyMergePartialColumns as MergePartialColumns,
PyDeleteOperation as DeleteOperation,
PyUpdateOptimizeCleanup as UpdateOptimizeCleanup,
TsUpdateConnectEnterprise,
TsUpdateConnectLocal,
TsUpdateExampleTableSetup,
TsUpdateOperation,
TsUpdateUsingSql,
TsMergeMatchedUpdateOnly,
TsInsertIfNotExists,
TsMergeUpdateInsert,
TsMergeDeleteMissingBySource,
TsMergePartialColumns,
TsDeleteOperation,
TsUpdateOptimizeCleanup,
RsUpdateConnectEnterprise,
RsUpdateConnectLocal,
RsUpdateExampleTableSetup,
RsUpdateMakeUsersReader,
RsUpdateOperation,
RsUpdateUsingSql,
RsMergeMatchedUpdateOnly,
RsInsertIfNotExists,
RsMergeUpdateInsert,
RsMergeDeleteMissingBySource,
RsMergePartialColumns,
RsDeleteOperation,
RsUpdateOptimizeCleanup,
} from '/snippets/tables.mdx';
Updating or modifying data involves changing rows in an existing table.
LanceDB provides two families of write operations that can modify data in a table:
- `update(...)`: mutate existing rows that match a SQL filter.
- `merge_insert(...)`: compare incoming rows to existing rows by key, then choose what to do for each case.
The `update` method is simpler to use when you already know which rows you want to modify and you do not need to compare against an incoming dataset. The `merge_insert` method is more powerful when you have a new dataset that you want to merge into an existing table, and you want LanceDB to handle the logic of comparing against existing rows by key.
Let's look at an example that demonstrates these operations in practice.
## Connect to LanceDB {#connect-to-lancedb}
Connect to your local LanceDB instance:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateConnectLocal}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateConnectLocal}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateConnectLocal}
</CodeBlock>
</CodeGroup>
Or, connect to LanceDB Enterprise:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateConnectEnterprise}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateConnectEnterprise}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateConnectEnterprise}
</CodeBlock>
</CodeGroup>
In the Rust snippets, a `make_users_reader` helper is used to build Arrow input data.
<CodeGroup>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateMakeUsersReader}
</CodeBlock>
</CodeGroup>
## Create the example table {#create-the-example-table}
We'll start by creating a simple table with `id`, `name`, and `login_count` columns. All examples below use the same table.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateExampleTableSetup}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateExampleTableSetup}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateExampleTableSetup}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 20 |
The example above shows a PyArrow schema. You can just as well create the table using other
table creation patterns (Pandas, Polars, Pydantic, iterators, etc.) -- see the [ingestion](/tables/create/) guide for more details.
## Choose a write method {#choose-a-write-method}
| Family | Method | Use this when... |
| --------------- | --------------- | ---------------- |
| `update` | `update(where=..., values=...)` | You want to edit rows that already exist, using a SQL filter. |
| `merge_insert` | `.when_matched_update_all()` | You have incoming rows and want to update keys that already exist in the table. |
| `merge_insert` | `.when_not_matched_insert_all()` | You have incoming rows and want to insert keys that do not exist yet. |
| `merge_insert` | `.when_matched_update_all()` + `.when_not_matched_insert_all()` | You want both behaviors together (often called **upsert**: update existing keys **and** insert missing keys in the same operation). |
| `merge_insert` | `.when_not_matched_by_source_delete(...)` | You want to remove target rows that are missing from the incoming source set. |
Write operations return status metadata. For example, `update` reports updated row count and
committed `version`, `delete` reports deleted row count and `version`, and `merge_insert` reports
inserted, updated, deleted, retry-attempt, and `version` fields. Writes require a mutable table
handle; after checking out an older version for reads, call `checkout_latest` / `checkoutLatest`
before modifying data.
The committed `version` advances even for writes that affect zero rows, such as a delete predicate
that matches nothing.
## Update rows {#update-rows}
Use `update` when you already know which target rows to modify and you do not need to compare against an incoming dataset.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateOperation}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateOperation}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateOperation}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 20 |
<Warning title="Warning">
Updating nested columns is not yet supported.
</Warning>
## Update rows with SQL expressions {#update-rows-with-sql-expressions}
Use `values_sql` when you want to use SQL-like expressions to update rows. This is useful for operations like incrementing a counter, or setting a column value based on another column.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateUsingSql}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateUsingSql}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateUsingSql}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 21 |
<Note title="SQL syntax">
See the [SQL queries](/search/sql/) page for more information on the supported SQL syntax.
</Note>
When rows are updated, they are moved out of any existing index. The row will still show up in search queries, but the query will not be as fast as it would be if the row was in the index. If you update a large proportion of rows, consider triggering an index rebuild afterwards.
## Merge incoming rows by key {#merge-incoming-rows-by-key}
Merging is different from updating because it involves comparing incoming rows to existing rows by key, and then choosing what to do based on whether the key exists in the target table or not.
The `merge_insert(""..."")` method lets you do this.
In merge operations, rows are split into three groups:
- **Matched**: key exists in both source and target.
- **Not matched**: key exists only in source.
- **Not matched by source**: key exists only in target.
Conditional merge clauses can compare old and new values. Use the `target.` prefix for the
existing table row and `source.` for the incoming row, for example
`target.last_update < source.last_update`.
<Tip>
**Use scalar indexes to speed up merge insert**
The merge insert command performs a join between the input data and the target table `on` the key you provide. This requires scanning that entire column, which can be expensive for large tables. To speed up this operation, create a scalar index on the join column, which will allow LanceDB to find matches without scanning the whole table.
Read more about scalar indices in the [Scalar Index](/indexing/scalar-index/) guide.
</Tip>
<Info>
If you see this HTTP 400 error from `merge_insert`: `Bad request: Merge insert cannot be performed because the number of unindexed rows exceeds the maximum of 10000`. Verify that the scalar index on the join column is up to date before retrying.
</Info>
<Note>
**Rust: build merge predicates with DataFusion expressions**
In the Rust SDK, you can pass a `datafusion_expr::Expr` directly instead of a SQL string by using
`when_matched_update_all_expr` and `when_not_matched_by_source_delete_expr` on `MergeInsertBuilder`.
This is useful when you are constructing predicates programmatically and want to avoid building a SQL string.
These methods are only supported on local tables. Calling them against a remote table returns a
`NotSupported` error — use the SQL string variants (`when_matched_update_all` /
`when_not_matched_by_source_delete`) for remote tables.
</Note>
Like the create table and add APIs, the merge insert API will automatically compute embeddings based on the [embedding registry](/embedding/index#embedding-registry) if the table has an embedding definition in its schema.
During `merge_insert`, if the input data doesn't contain the source column (i.e., the original field used to generate embeddings, such as text for a text embedding model or `image_uri` for an image model), or if a vector value is already provided, LanceDB skips embedding generation for that row. Embeddings are only auto-generated when that source field is present in the incoming data, **and** the vector field is empty.
Primary keys in LanceDB are metadata used by operations such as `merge_insert`; they are not
enforced as uniqueness constraints on ordinary writes. Keep using `merge_insert` or your own
deduplication logic when you need key-based upsert semantics.
### Update matched rows only {#update-matched-rows-only}
This updates keys that already exist in the target table. Source rows with new keys are ignored.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergeMatchedUpdateOnly}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergeMatchedUpdateOnly}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergeMatchedUpdateOnly}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 21 |
### Insert unmatched rows only {#insert-unmatched-rows-only}
This inserts only brand-new keys from the source. Existing keys are left unchanged.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{InsertIfNotExists}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsInsertIfNotExists}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsInsertIfNotExists}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 20 |
| 3 | Charlie | 5 |
### Update matched rows and insert unmatched rows {#update-matched-rows-and-insert-unmatched-rows}
Use both `when_matched_update_all()` and `when_not_matched_insert_all()` when you want to update existing keys and insert missing keys in one operation.
<Info>
This is a conventional **upsert**.
</Info>
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergeUpdateInsert}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergeUpdateInsert}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergeUpdateInsert}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 21 |
| 3 | Charlie | 5 |
### Delete target rows that are missing from source {#delete-target-rows-that-are-missing-from-source}
Use `when_not_matched_by_source_delete()` when you want to remove any target row that does not appear in the incoming source data.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergeDeleteMissingBySource}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergeDeleteMissingBySource}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergeDeleteMissingBySource}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 2 | Bobby | 21 |
| 3 | Charlie | 5 |
In the example above, LanceDB matches rows by `id`. Rows with `id=2` and `id=3` exist in both the table and incoming data, so they are updated. Row `id=1` exists only in the target, so it is deleted.
### Use partial columns in merge updates {#use-partial-columns-in-merge-updates}
Merge updates do not require you to provide values for all columns. You can provide only a subset of columns in source rows. For matched rows, only the provided columns are updated.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{MergePartialColumns}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsMergePartialColumns}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsMergePartialColumns}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bobby | 20 |
| 3 | Charlie | null |
Note that in the example above, when `merge_insert` creates a new row, any missing columns are written as `null`. If a missing column is non-nullable in your schema, the insert will fail.
## Delete rows {#delete-rows}
Delete operations **soft delete** rows that match a given condition.
The underlying data is not immediately removed, but is marked
for deletion (in the [deletion files](https://lance.org/format/table/#deletion-files) at the Lance format level) and excluded from query results.
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{DeleteOperation}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsDeleteOperation}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsDeleteOperation}
</CodeBlock>
</CodeGroup>
Expected table contents:
| id | name | login_count |
| --- | --- | --- |
| 1 | Alice | 10 |
| 2 | Bob | 20 |
<Warning>
**Deleting rows removes them from the index**
When rows are deleted, those rows are also excluded from the index segments, so indexed queries will not return them either. If ALL the rows are deleted (i.e., the table is emptied), ensure that you recreate the index after ingesting new data.
</Warning>
To permanently remove deleted rows, you can optimize the table, which will run compaction and cleans up the soft-deleted rows, which frees up storage space.
- In LanceDB OSS, compaction and cleanup are manual. Run `table.optimize()` regularly to free up disk space.
- In LanceDB Enterprise, files aren't cleaned up by default. You can configure automatic compaction and cleanup behavior at cluster setup time to suit your organization's retention policy.
By default, table cleanup removes data up to 7 days ago. If you need to reclaim space from deleted rows more aggressively, manually call `table.optimize()` use a shorter retention window as follows:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{UpdateOptimizeCleanup}
</CodeBlock>
<CodeBlock filename="TypeScript" language="TypeScript" icon="square-js">
{TsUpdateOptimizeCleanup}
</CodeBlock>
<CodeBlock filename="Rust" language="Rust" icon="rust">
{RsUpdateOptimizeCleanup}
</CodeBlock>
</CodeGroup>