In LanceDB Enterprise, tables are also created and dropped in SQL with
CREATE TABLE and DROP TABLE.Table structure and metadata
Create a table
data can be a list of objects, an Arrow table or reader, a pandas or polars
frame, or Pydantic models. Ingesting data covers each, plus
exist_ok and mode="overwrite" for re-runnable scripts.
Open and list tables
namespace_path to act inside a namespace
rather than at the database root.
For Python remote connections created with lancedb.connect("db://..."), use
db.table_exists(["documents"]) to check whether a table exists. For an async
remote connection, use await db.table_exists(["documents"]). Include namespace
segments before the table name, for example ["analytics", "documents"]. These
checks return False when the table is missing and propagate other errors,
including permission errors.
Local synchronous directory connections still raise NotImplementedError for
table_exists. On a connection from connect_namespace("dir", ...) or
connect_namespace("rest", ...), it fails with an AttributeError. For these
connections, open the table and handle the error, or look for its name in a
listing. A missing table raises ValueError on a local directory and
lance_namespace.TableNotFoundError on a connection from connect_namespace.
A listing returns one page of names at a time, so check every page. TypeScript
has no tableExists.
Rename and clone tables
Enterprise job type: on an Enterprise cluster that drops tables in the background,
the teardown runs as a job:
drop_table, which fences the table’s directory
with a marker and deletes its content, or drop_object, depending on how the
deployment stores its catalog. See
Object teardown.Drop a table
drop_table_async returns a
job rather than blocking when the deployment drops tables in the
background. In the embedded library, and on an Enterprise cluster that drops inline, the
returned job has already finished.
Table management APIs
Data types
A table’s schema is an Arrow schema: there is no separate LanceDB type system mapped onto storage, so a column is the same object to the query engine as it is to the format.
A vector column is a
fixed_size_list<float32, dim>, an ordinary Arrow type,
which is why it is selectable and joinable but why arithmetic over it is list
arithmetic rather than scalar.
See SQL data types for the full reference, including the
extension types and pseudo-columns.
Multimodal data
Store images, audio, video, and documents alongside structured columns and embeddings in the same versioned table. Useblob columns for these payloads. Small values stay inline in the data
file, up to 64 KiB by default, and larger values are stored outside it.
Blob columns
Early access Ablob column keeps metadata separate from the payload bytes, so queries can
filter rows without loading the blobs.
Most queries return descriptors, which contain metadata about the blob instead
of its bytes. You can
read the bytes in the query results or
fetch them by row ID.
In Python, plain to_pandas() scans can also return BlobFile handles when
pylance is installed.
When you write raw bytes, Lance chooses a storage layout based on the payload
size. A descriptor’s kind tells you which one was used:
Managed storage must be enabled separately. See
Managed blobs for details.
kind=5 identifies a Managed
object using an explicitly registered base.
Lance uses the default thresholds unless you customize them. Explicitly
configured thresholds are stored in the table schema and preserved on append.
Setup
Import the SDK and open a local database. The examples in this section usedb.
Define a blob schema
Declare a blob column withlancedb.blob() in Python and TypeScript, or
blob() in Rust:
Ingest blobs
Pass raw bytes for each blob value. In Python, a blob column acceptsbytes.
In TypeScript, it accepts a Buffer. The Python example creates two JPEGs
with EXIF metadata, which we’ll read using a blob handle below.
Read bytes in query results
Python and TypeScript can return blob bytes in query results without collecting_rowid. Rust uses fetch_blobs with
row IDs instead.
to_list(blob_mode="bytes") returns the image bytes from the photos table
above:
Table.to_pandas(), Table.to_arrow(), and the query builder
methods to_pandas(), to_arrow(), to_list(), and to_batches() accept a
blob_mode argument. Sync and async query builders support the same modes:
For supported local v2 blob projections,
to_pandas(blob_mode="bytes") and
to_pandas(blob_mode="descriptions") use native Arrow conversion without
pylance. Pandas conversions that need the Lance scanner, including plain-scan
"lazy" handles and legacy blobs, still require pylance. to_arrow(),
to_list(), to_batches(), and the fetch methods do not require pylance.
blob_mode="bytes" requires a LanceDB Embedded table snapshot, so Enterprise
and in-memory tables raise NotImplementedError. On an Enterprise table,
Table.to_pandas() and
Table.to_arrow() aren’t supported, and a query’s to_pandas() also raises in
the default "lazy" mode because it would open BlobFile handles. Query with
blob_mode="descriptions", then read bytes with fetch_blobs or
fetch_blob_files.
blob_mode="bytes" materializes top-level and struct-nested blob columns. It
raises ValueError when the selected columns include blobs
inside a list. Select those columns with blob_mode="descriptions", or leave
them out of the projection. Extra keyword arguments on
to_pandas(), such as split_blocks and self_destruct, are forwarded to
PyArrow.
to_batches(blob_mode="bytes") fetches blob v2 payloads one batch at a time
and returns them as Arrow large_binary values. Iterate over the reader and
call batch.to_pandas() to convert each batch to a DataFrame without collecting
all payloads at once.
For sync queries, call query.to_batches(batch_size=64, blob_mode="bytes")
and iterate with for. For async queries, call
await query.to_batches(max_batch_length=64, blob_mode="bytes") and iterate
with async for. Sync full-text and hybrid queries do not support
to_batches().
The default "lazy" mode returns descriptors and keeps automatic _rowid
columns visible for manual fetches. "descriptions" returns the five-field
descriptors. Both "bytes" and "descriptions" hide automatic row IDs;
explicitly requested row IDs remain visible.
Bytes mode has the same restrictions as to_arrow(blob_mode="bytes"),
including no legacy blobs, blobs inside lists, Enterprise tables, or namespace
QueryTable pushdown. If concurrent writers can overwrite the table, use a
separate reader checked out to a fixed version before querying. Otherwise,
an overwrite can invalidate the row IDs used to fetch bytes and raise
ValueError.
The lazy-handle calls below require pylance. The query example expects tbl_async,
query_vector, and a video column, which the photos table does not define.
blob_mode values:
toArray() and toArrow() accept a blobMode option:
With
"bytes", the rows and the bytes come from the same table version. If the
table changes while the query runs, the query runs again, up to three times:
Read blobs on demand
Query the rows you want and pass their_rowid values to a fetch method.
fetch_blob_files opens a seekable handle and loads bytes only when you read.
The Python example opens the JPEG from the ingest step and reads its EXIF
Make tag with Pillow. TypeScript and Rust read the first bytes from the handle.
fetch_blobsreturns the full bytes for each row ID in order. Null blobs produce null entries, and empty blobs produce empty byte strings.fetch_blob_filesloads bytes when you callread,read_range, orread_ranges. Range reads don’t move the file cursor. Null blobs produce null handles.fetch_blob_rangestakes(row_id, offset, length)requests and returns one slice per request, in order. A request list can repeat a row to read more than one slice of that blob.
fetch_blob_ranges. On a handle,
read_ranges reads more than one slice of one blob. Python takes
(offset, length) pairs, TypeScript takes { start, end } ranges with an
exclusive end, and Rust takes start..end ranges:
Notes and limits
Blob fetches fail if any requested row ID is missing or deleted. With stable row IDs enabled, IDs remain valid across compaction. A blob field can sit at the top level of the schema or inside a struct or list. Struct-nested blob columns use dotted paths such asclip.video in the fetch
APIs. Blobs inside lists can be written and queried as descriptors. A table
row ID does not address one list element, so blob_columns(), fetch_blobs,
fetch_blob_files, and blob_mode="bytes" can’t return those bytes.
See the Lance blob guide for the storage format.
Managed blobs (Blob v2)
Set the table schema metadatalance.blob.managed to true on a table with
lancedb.blob() columns to store small values inline and
larger values in managed objects under the table’s _blobs/ directory. Medium
values can share a packed object, and large values use a dedicated object.
The table owns these objects. You do not upload them separately, keep an object
catalog, or delete their files yourself. Compaction preserves each descriptor
and its payload object, so merging table fragments does not rewrite the blob
payloads.
Write and read blobs
This example uses a local connection. The same schema,add, query, and
fetch_blobs calls work with an Enterprise database connection:
fetch_blobs materializes the selected payloads in memory, so fetch bounded
batches when blobs are large. Row IDs belong to their table and snapshot. Keep
descriptors opaque in application code, because their representation can differ
between inline values and Managed objects.
Derived blobs and maintenance
Enterprise Functions can consume blob columns and return new blobs. Their output payloads are managed by the destination table. LanceDB Embedded tables can also derive blob columns with SQL computed columns. Deleting a row or replacing its blob does not immediately remove the old payload: retained table versions may still reference it. Garbage Collection reclaims objects after those references expire. A packed object remains while any of its contained blobs is still referenced.Legacy large-binary blobs
Uselancedb.blob() for new tables. The sections below
cover existing tables that use the V1 large-binary Blob API
(pa.large_binary() in Python, LargeBinary in TypeScript, and
DataType::LargeBinary in Rust with {"lance-encoding:blob": "true"} field
metadata). Changing a field’s metadata alone does not migrate an existing V1
column to Blob v2.
1. Define a blob schema
V1 blob columns use alarge_binary field marked with {"lance-encoding:blob": "true"} metadata:
2. Ingest large blobs
Write raw bytes to a V1 blob column:3. Convert blob tables to pandas
On LanceDB Embedded tables with V1 blob columns,to_pandas() in Python accepts
blob_mode ("lazy", "bytes", or "descriptions"). Enterprise tables raise
NotImplementedError, and "bytes" is not supported on in-memory tables.
Extra keyword arguments such as split_blocks or self_destruct are forwarded
to the PyArrow / Lance pandas conversion:
"lazy", "bytes",
and "descriptions". Vector, FTS, hybrid, and ordered queries cannot
materialize V1 blob columns through to_pandas(), so omit those columns from
the projection:
Store binary data
Use abinary column when the query should return the raw bytes. Declare it as pa.binary() in Python, Binary in TypeScript, and DataType::Binary in Rust.
1. Setup and imports
Import LanceDB and Arrow in your SDK.2. Prepare data
Encode each image or file into raw bytes before writing.3. Define the schema
Declare the schema explicitly so Arrow and LanceDB store the field asbinary instead of inferring a string or integer list.
4. Ingest data
Create the table from the rows and schema.Retrieve binary data
When you search the table, select thebinary column alongside your metadata columns.
Convert bytes back to objects
Decode the returned bytes into an image or media buffer in your application.Other modalities
For PDFs, audio recordings, and other files, keep searchable fields in separate columns so you can filter rows before fetching blob bytes.bfloat16
Sixteen-bit floats in the bfloat16 layout, stored asfixed_size_binary(2).
Useful as a vector element type when halving the memory matters more than the
precision does.
Schema evolution
Schema evolution enables non-breaking modifications to a database table’s structure (such as adding columns, altering data types, or dropping fields) to adapt to evolving data requirements without service interruptions. LanceDB supports ACID-compliant schema evolution through granular operations (add/alter/drop columns), allowing you to:- 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
In SQL, schema changes are
ALTER TABLE.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. On local tables,
the Python SDK also accepts precomputed values from DataFrames and Arrow readers;
see Create a computed column
for the supported inputs and row-order requirements.
Set up the example table
First, let’s create a sample table with product data to demonstrate schema evolution:Columns derived from existing data (SQL expressions over other columns) are
covered in Create a computed column,
because deriving values is a feature-engineering job rather than a schema one.
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: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: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, which is 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.
replace to true:
Canonical metadata keys
You can use any string as a metadata key. By convention, LanceDB treats keys prefixed withlancedb: as canonical. LanceDB Enterprise displays these keys in the UI, and agents can rely
on finding this information in a consistent place. Use them when they fit your use case:
These keys are conventions, not enforced constraints. LanceDB does not validate their values,
but following the conventions keeps your tables consistent with Enterprise tooling.
Drop columns
You can remove columns using thedrop_columns
method in Python, dropColumns in TypeScript/JavaScript, or drop_columns in Rust.
Set Up the example table
Create a table with temporary columns that we’ll remove:Drop single columns
Remove individual columns that are no longer needed:Drop multiple columns
Remove several columns at once for efficiency:Versioning
This page shows the core table-versioning APIs used in the code snippets for Python, TypeScript, and Rust. Each operation below maps directly to methods shown in the examples.Basic Versioning Example
Let’s create a table with sample data to demonstrate LanceDB’s versioning capabilities:Set Up the Table
First, let’s create a table with some sample data:Check Initial Version
After creating the table, let’s check the initial version information:Modify Data
When you modify data through operations like update or delete, LanceDB automatically creates new versions.Update Existing Data
Let’s update some existing records to see versioning in action:Add New Data
Now let’s add more records to the table:Check Version Changes
Let’s see how the versions have changed after our modifications:Rollback to Previous Versions
LanceDB supports fast rollbacks to any previous version without data duplication.View All Versions
First, let’s see all the versions we’ve created:Restore a Version Snapshot
Now let’s restore a captured version snapshot:Tag-Based Versioning
Numeric table versions likev3 or v17 are precise but hard to remember. Tags
let you attach human-readable labels (e.g., "prod", "baseline",
"q3-evaluation") to specific versions and check those out by name. They are
conceptually similar to git tags, and unlike numeric versions, tagged versions
are never pruned. A tagged version that is older than the retention window and
is not the latest stops cleanup instead (see the cleanup note at the bottom of this
page).
The tags API supports the standard CRUD operations (create, list, update, delete),
plus checking out by tag name.
Deleting a tag only removes the label, not the version it points to. After
deletion, the underlying table version becomes eligible for cleanup again.
Delete Data From the Table
Let’s demonstrate how deletions also create new versions:Go Back to Latest Version
First, let’s return to the latest version:Delete Data
Now let’s delete some data to see how it affects versioning:Version History and Operations
On a fresh table, the snippets in this guide produce this version sequence:v1: create table (create_table/createTable/create_table)v2: update rows (update)v3: add rows (add)v4: restore snapshot (restore) fromversion_after_mod/versionAfterModv5: delete rows (delete)
list_versions/listVersions, version, checkout, checkout_latest/checkoutLatest) do not create new versions.
The version metadata fields can differ by backend. Direct table-backed version listing exposes a
timestamp, while namespace-backed listing may expose fields such as manifest_path,
manifest_size, e_tag, and timestamp_millis. In deployments that use managed versioning,
prefer the table version APIs exposed by LanceDB Enterprise or the namespace service instead of
mixing in lower-level Lance file operations.
System OperationsSystem operations like
optimize(), index updates, and table compaction also increment table version numbers.
In LanceDB Embedded, optimize() can prune older versions based on its retention setting (cleanup_older_than, 7 days by default),
which is when old-version files are removed and disk space is reclaimed. A deployment prunes them in the background
when its configuration enables cleanup; optimize() is not available there.Tagged versions are never pruned. A version with a tag pointing at it is
retained regardless of age. When a tagged version is older than the retention window and
is not the latest, cleanup refuses to run instead of pruning around it: optimize() raises
an error naming the tag and prunes nothing. A tag on the latest version does not cause
this, but optimize() compacts before it prunes, and compacting commits a new version
whenever it merges files, so a tag on what was the latest version can become an older
one by the time cleanup runs; the error then comes after compacting, and the index
update does not run. To let cleanup proceed, move the tag
to a version inside the retention window, delete it, or widen the window.Privileges
A table’sOWNERSHIP, SELECT, INSERT, UPDATE and DELETE are
granted through the REST ACL API or SQL GRANT/REVOKE: