- Iterate Safely: Modify schemas in production with versioned datasets and backward compatibility
- Scale Seamlessly: Handle ML model iterations, regulatory changes, or feature additions
- Optimize Continuously: Remove unused fields or enforce new constraints without downtime
Schema evolution operations
LanceDB supports four primary schema evolution operations:- Adding new columns: Extend your table with additional attributes
- Altering existing columns: Change column names, data types, or nullability
- Updating field metadata: Attach or change per-column Arrow metadata
- Dropping columns: Remove unnecessary columns from your schema
version. Run these operations from a mutable table handle; if you checked out an
older version for reads, call checkout_latest / checkoutLatest before modifying the schema.
Add new columns
You can add new columns to a table with theadd_columns
method in Python, addColumns in TypeScript/JavaScript, or add_columns in Rust.
New columns are populated based on SQL expressions you provide.
Set up the example table
First, let’s create a sample table with product data to demonstrate schema evolution:Add derived columns
You can add new columns that are derived from existing data using SQL expressions. For feature engineering on large existing tables, group related derived features into oneadd_columns operation instead of running many separate writes. This creates one
new table version for the schema change and computes the new columns from the existing
rows, which avoids growing the table’s version history with many small updates.
The same call can add multiple derived columns at once. For example, if you are
building several lightweight features from existing product fields, pass all of the
new column expressions together:
LanceDB
add_columns does not currently accept Python callables, batch UDFs, or
PyArrow RecordBatch iterators for populating new columns. New column values must be
defined with SQL expressions, or added as NULL columns from an Arrow field or schema.
If your transformation cannot be expressed in SQL, compute the values outside
add_columns before writing them back through another workflow.Add columns with default values
Add boolean columns with default values for status tracking:Add nullable columns
Add timestamp columns that can contain NULL values:Declare computed columns
You can also declare a column whose values are defined by a SQL expression but not evaluated at commit time. LanceDB stores the expression in the column’s field metadata, commits the column with no values, and fills the rows on a later refresh. The column’s type and its input columns are derived from the expression, so you do not pass a data type. Use this form when you want to add a derived column to a large table without paying the cost of computing every row up front. Declaring a computed column costs the same on an empty table as on a large one, because no values are written at declaration time. Regularadd_columns transforms, in contrast,
evaluate the SQL expression against every existing row and write the results
in the same commit.
add,update,merge_insert, and SQLINSERTare refused for the declared column.- The declared column cannot be renamed, retyped, or dropped.
- An input column named in the expression cannot be renamed, retyped, or dropped while the declaration reads it.
- Volatile expressions (for example, expressions whose value can change between calls) are refused at declaration time.
Computed columns work on both local tables and LanceDB Enterprise. On
Enterprise the declaration is sent to the server, which plans the
expression against the published contract; refresh runs as a server-side
backfill job (see the next section).
add_columns cannot mix a regular transform with a computed column in the
same call. Declare computed columns in a separate add_columns call from
any evaluated transforms.Refresh a computed column
A declared computed column starts empty. Callrefresh_column (Python and
Rust) or refreshColumn (TypeScript) to evaluate the expression and fill
every row that still has no value:
refresh_column when nothing new needs
filling is a no-op that costs one scan of the unfilled rows. Because refresh
never revisits a filled row, mutating an input after the fact does not change
the stored value — to recompute, drop the column and declare it again.
The blocking form is refused when the table uses an LSM write specification,
and is refused on LanceDB Enterprise because a remote refresh runs as a
server job that does not report a fill count. On Enterprise, submit the
refresh with the async form below instead.
Run the refresh in the background
If you don’t want to block on the refresh, call the async variant to get back a job handle. On local tables the job runs as an in-process task; on LanceDB Enterprise the call submits a server-side backfill job and returns a handle that tracks it. Wait for it or poll its status when convenient.wait returns.
On LanceDB Enterprise, a successful wait also advances the submitting
table handle’s read-freshness baseline so subsequent reads see the refreshed
rows — unless a checkout has pinned the handle to a specific version by the
time the job completes.
Alter existing columns
You can alter columns using thealter_columns
method in Python, alterColumns in TypeScript/JavaScript, or alter_columns in Rust. This allows you to:
- Rename a column
- Change a column’s data type
- Modify nullability (whether a column can contain NULL values)
Set up the example table
Create a table with a custom schema to demonstrate column alterations:Rename columns
Change column names to better reflect their purpose:Change data types
Convert column data types for better performance or compatibility:Make columns nullable
You can alter columns to contain NULL values: Changing a column to nullable affects future writes and merges too: missing values are accepted only when the target column is nullable.Multiple changes at once
Apply several alterations in a single operation:Expression-based type changes
For transformations that are not simple casts (for example, converting"$100" to an integer), use a SQL-expression column add, then drop and rename:
Alter embedding types and dimensions
It’s quite common to need to change an embedding column’s schema, in case a new model becomes available with a different embedding dimension.- In Python, the example shows an in-place type update when the cast is compatible.
- In TypeScript and Rust, the example shows a dimension change (
384 -> 1024), which cannot be cast in-place.
Update field metadata
Each column in a LanceDB table can carry a small key/value map of Arrow field metadata — useful for annotating columns with units, provenance, PII flags, embedding model versions, or any other schema-level context your application needs. Useupdate_field_metadata
in Python, updateFieldMetadata
in TypeScript/JavaScript, or update_field_metadata in Rust to add, change, or remove these
key/value pairs without rewriting the column data. Each call commits a new table version and returns
the new version.
Each update targets one field by dot-path: top-level columns are addressed by name (for
example "embedding"), and nested fields by their full path (for example "address.zip"). By
default, the keys you pass are merged into the field’s existing metadata — keys you do not
mention are preserved, and passing None (Python) or null (TypeScript) deletes a key. Set
replace: true to swap the field’s entire metadata map instead of merging.
To overwrite a field’s metadata entirely instead of merging, set replace to true:
Drop columns
You can remove columns using thedrop_columns
method in Python, dropColumns in TypeScript/JavaScript, or drop_columns in Rust.