Skip to main content
Applies to: Embedded ✅, Enterprise ✅ Early access in Enterprise Use merge insert when incoming records should be compared with existing rows. Combine update and insert policies for an upsert. Use append when every incoming row should simply be added.

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. Use merge_insert("id") in Python or the corresponding SDK builder. 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. Choose a key that identifies the same record in the source and target. Check for duplicate and null keys before merging. A primary-key declaration does not enforce uniqueness on ordinary writes, and append operations do not deduplicate records for you. A scalar index on the key can help matching. Keep it current and measure the actual workload rather than assuming every merge uses an index. Enterprise may reject a merge when the unindexed scan exceeds its configured limit; inspect that error and the index state before retrying.

Choose a merge policy

The following examples are independent. Each starts from the table shown in its code, so the expected results do not depend on running the previous example. Python blocks include their own temporary connection. For a full TypeScript or Rust program, see Complete write workflow.

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 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.
This is a conventional upsert.
Expected table contents:

Delete target rows that are missing from source

Use when_not_matched_by_source_delete() only when the incoming data is the complete set for the target scope you intend to synchronize. An incremental batch is usually not a complete source: deleting everything absent from it could remove unrelated target records. Inspect the unmatched target keys first.
Expected table contents: Here, IDs 2 and 3 are updated and ID 1 is deleted. This policy synchronizes the target to the supplied source set.

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.

Check the merged records

Inspect the keys that should have changed, confirm that unrelated rows remain, and check inserted or deleted counts when the SDK returns them. Repeating a merge is only safe when its keys, conditions and update values produce the intended result on retry.

Embeddings and expression predicates

When a table uses an embedding definition, behavior depends on which source and vector fields the incoming records provide. Do not assume a partial metadata update recomputes embeddings. Inspect the resulting vector when changing the input used to generate it. Rust’s when_matched_update_all_expr and when_not_matched_by_source_delete_expr accept DataFusion expressions for LanceDB Embedded tables. Use the string predicate variants for Enterprise tables.

Complete write workflow

Each program creates a temporary local database, performs the five operations, prints the result, and removes its temporary data when finished.
  • Python: install lancedb, save the code as ingestion.py, and run python ingestion.py.
  • TypeScript: install @lancedb/lancedb, save the code as ingestion.mts, and run node ingestion.mts with Node.js 22.18 or later.
  • Rust: download the complete Rust project, extract it, and run cargo run --locked. It includes src/main.rs, the Cargo manifest and a lockfile for the example’s dependencies. The code is shown below.
After the operations, Alice has 10 logins, Bobby has 21, and Charlie has 6. Dana is selected for deletion and is absent from the final result.