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.
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 your local LanceDB instance: Or, connect to LanceDB Enterprise: In the Rust snippets, amake_users_reader helper is used to build Arrow input data.
Create the example table
We’ll start by creating a simple table withid, name, and login_count columns. All examples below use the same table.
Expected table contents:
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 guide for more details.
Choose a write method
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
Useupdate when you already know which target rows to modify and you do not need to compare against an incoming dataset.
Expected table contents:
Update rows with SQL expressions
Usevalues_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.
Expected table contents:
See the SQL queries page for more information on the supported SQL syntax.
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. Themerge_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.
target. prefix for the
existing table row and source. for the incoming row, for example
target.last_update < source.last_update.
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.Rust: build merge predicates with DataFusion expressionsIn 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.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.
Update matched rows only
This updates keys that already exist in the target table. Source rows with new keys are ignored. Expected table contents:Insert unmatched rows only
This inserts only brand-new keys from the source. Existing keys are left unchanged. Expected table contents:Update matched rows and insert unmatched rows
Use bothwhen_matched_update_all() and when_not_matched_insert_all() when you want to update existing keys and insert missing keys in one operation.
This is a conventional upsert.
Delete target rows that are missing from source
Usewhen_not_matched_by_source_delete() when you want to remove any target row that does not appear in the incoming source data.
Expected table contents:
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
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. Expected table contents:
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 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 at the Lance format level) and excluded from query results. Expected table contents:
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.
table.optimize() use a shorter retention window as follows: