Choose SQL or SDK table operations
An SDK gives you a programming-language-specific API for invoking LanceDB’s functionality: create a table, insert rows, build an index, run a vector search, each its own method call. Every one of those functionalities also has a corresponding SQL operation. SQL covers a wider surface than any one SDK method and lets you chain operations together in a single statement — running a vector search and then joining the results with another table, for example — where SDK methods stay separate calls. Use SQL for joins, aggregations, window functions, analytical queries, and whenever you want to chain operations together. Use SDK table operations to build applications around table management, ingestion, filtering, and search, including applications that run with LanceDB Embedded, where SQL is not available. You can combine them. For example, create and populate a table with an SDK, then query it with SQL to prepare a training dataset. In Python, both workflows can use the same Enterprise connection. Both interfaces work with the same Lance tables, through the same Arrow schema. Applications in different languages can work with the same tables: connect to the same database and namespace, use the appropriate credentials, and account for read consistency when another process writes data. A fixed-size vector column stores vectors of a consistent dimension; see the data types reference for the supported element types and other column types.Common SQL operations
Several tasks are available through both interfaces, but the methods and SQL statements do not have identical coverage. SDK support also differs by language. These SQL reference pages describe the supported statements:
Computed columns store SQL
expressions whose values are populated by a refresh. They are separate
from registered Python Functions. See the computed-column and
materialized-view guides for their SDK and SQL
operations and Enterprise setup requirements.
Access LanceDB with SQL
There are three ways to run SQL against LanceDB Enterprise, all against the same Arrow Flight SQL endpoint, returning results as Arrow record batches:db.execute_query()on the Python SDK’s Enterprise connection, shown below, orConnection::execute_query_async()on the Rust SDK’s. These are LanceDB’s native SQL execution methods, and we recommend them for the best performance — see Adaptive query execution.- An Arrow Flight SQL client. Use this from TypeScript, whose LanceDB SDK does not expose a SQL execution method, or from any tool with its own Flight SQL integration. A LanceDB-specific SDK is not required to submit SQL.
- The LanceDB CLI. Open an interactive SQL shell, or run a statement or file, directly from the command line with no code at all.
Adaptive query execution
A standard Flight SQL client callsGetFlightInfo to plan a query and then
DoGet to fetch rows — two round trips every time, even when the server
could stream results back immediately.
execute_query() skips that extra round trip when it can. It calls DoGet
directly for a query with no parameters, or DoExchange when
binding parameters, and sends a
lancedb-sql-protocol: adaptive-v1 header. The server decides per query
whether to stream results on that same call or defer execution and return a
poll ticket; when it defers, execute_query() follows up with
PollFlightInfo automatically, with no extra code on your side. A generic
Flight SQL client does not send that header, so it always takes the slower
GetFlightInfo path.
Connection settings
Get the following values from your Enterprise administrator:- Database URI, such as
db://your-database. - API key and region.
- API endpoint for
host_override. - Flight SQL endpoint for
sql_host_override, including its scheme and port.
sql_host_override in Python, as in the example below. The
API and SQL endpoints may use different hosts or ports.
See Connect to Enterprise for other connection options.
SQL runs in Enterprise. Connections to a local directory or an object-storage
bucket use LanceDB Embedded and cannot execute SQL statements. Use
SDK table operations to query those tables.
Run a query in Python
Install the SDK withpip install lancedb. Replace the connection placeholders
with your Enterprise settings, including the region and both endpoint URLs.
This example reads an existing frames table, with a video_id and a
caption column describing each frame. Change the query to match your table.
execute_query() returns a blocking Arrow reader; see
Adaptive query execution for how it starts streaming. The example
above uses read_all() to collect the small result in memory. For larger
results, or to track and cancel a running query, see
Read and manage results. To submit a query and check its status
separately, use execute_query_async().
Parameters in SQL queries
Pass aparameters argument to bind values into a query, instead of
formatting them into the SQL string:
$1, $2, … for positional binding, or $name for named
binding:
- A list or tuple binds positionally, in order, to
$1,$2, … - A dict binds by key to
$name. - A one-row
pyarrow.RecordBatchorTablebinds its columns, by name and by position. - A 1-D NumPy or Arrow array becomes a single fixed-size-list value — use this form for a vector argument, as shown above.
parameters is a LanceDB-specific mechanism, separate from the Flight SQL
protocol’s PREPARE/bind step, which a deployment does not implement; see
Prepared statements below. Bind values with
parameters from the Python SDK; a generic Flight SQL client has no
equivalent.Prepared statements
A Flight SQL client can prepare a statement before it runs it. A deployment accepts that, but it keeps no plan between runs and does not accept the parameter values described above at this protocol layer. A Flight SQL client takes these steps, whatever it calls them:- Prepare: send the SQL and get back a handle and the result schema. The statement is parsed and planned at this point, so a syntax or name error surfaces here rather than at execution.
- Execute: run the handle and read the result. The deployment plans the statement again each time it runs, so reusing a handle does not save planning time.
- Close: release the handle.
$1 and $2 at this
protocol layer gets a “not implemented” error when it executes the
statement. Use parameters above instead; do not put
untrusted input directly into prepared statement text, since quoting it does
not protect the statement the way binding a parameter would.
Prepare a statement when your client requires it, or to get a statement’s
result schema and catch syntax and name errors without running it. Preparing
does not make a repeated statement faster. To give a reusable query a name,
create a view. To store its result for later queries,
create a materialized view.
Read and manage results
- Collect the results when you want a list, Arrow table or DataFrame that fits in memory. Use a result limit and select only the columns you need.
- Read batches when you want to process a large result incrementally. Reading batches avoids loading the entire result into application memory. It does not guarantee that the query can return its first batch immediately.
Asynchronous clients
An async client lets your application do other work while a request is waiting. That is separate from whether you collect results or read them in batches. Python offers synchronous and asynchronous clients; TypeScript uses async APIs. Check the SDK reference for the methods available in your client.Query handles and background jobs
An Enterprise SQL query handle lets you inspect query execution and request cancellation. Background operations such as index builds return a job handle instead. Use the API for the returned handle to check status and retrieve results.Query plans
Use query plans to understand how LanceDB reads data, applies filters and uses indexes.explain_plan() shows the planned operations. analyze_plan() runs
the query and adds measurements, including execution time and rows processed.
Inspect a plan or measure execution
Use the first to inspect query structure and the second to investigate work
actually performed. Operator names and available measurements can differ
between LanceDB Embedded and LanceDB Enterprise. SQL provides EXPLAIN and EXPLAIN ANALYZE.
Explain and analyze a query in Python, TypeScript or Rust
Explain and analyze a query in Python, TypeScript or Rust
These examples use three sample records and hand-written vectors, so no
embedding service is needed. Each program creates a temporary local table
and removes it when finished.This tiny table demonstrates the API, not a performance benchmark. The plan
uses a scan because the example has no vector index. The following larger
example illustrates how to read scans, filters and index operations.
- Python: Install with
pip install lancedb, save asquery_plan.py, then runpython query_plan.py. - TypeScript: Install with
npm install @lancedb/lancedb, save asquery_plan.mts, then runnode query_plan.mtswith Node.js 22.18 or later. - Rust: Download and extract the complete Rust examples, then run
cargo run --locked --bin query_planfrom the extracted directory.
Read scans, filters and result retrieval
The recorded example below uses 1.2 million rows sampled from the Wikipedia dataset. The counts describe that run. The table initially has no indexes, so the plan shows a scan. This Enterprise plan includesRemoteTake and hides storage paths
as uri=***. A local plan shows the dataset path instead, and fetches the result
rows with Lance’s Take or, depending on the library version, a second
LanceRead whose source is stream(_rowid).
Let’s examine a vector search query that:
- Filters rows where
identifieris between 0 and 1,000,000 - Returns the top 100 matches
- Projects specific columns:
chunk_index,title, andidentifier
Execution Plan Components
Read the recorded vector plan operator by operator
Read the recorded vector plan operator by operator
The execution plan reveals the sequence of operations performed to execute your query. Let’s examine each component:This plan demonstrates a basic search without index optimizations: it performs a full scan and filter before vector search.
- Initial data scan loading only specified columns to minimize I/O
- Unordered scan enabling parallel processing
- Apply requested filter on
identifiercolumn - Reduces the number of vectors that need KNN computation
- Computes squared L2 distances between query vector and all vectors that passed the filter
- Filters out null distance results
- Sorts by distance and takes top 100 results
- Coalesces output into batches targeting 1,024 rows in this plan
RemoteTakeis the deployment’s operator for this step; a LanceDB Embedded plan shows Lance’sTakeor aLanceReadover the selected row IDs, depending on the library version- Handles efficient data retrieval from remote storage locations
- Fetches specific rows and columns needed for the final output
- Optimizes network bandwidth by only retrieving required data
- Returns only requested columns and maintains column ordering
Non-vector queries
Query plans also cover filters and full-text search. Python can show both sub-plans of a hybrid query. Check where a filter runs and how many rows flow through each operator; a separateFilterExec does not by itself establish
that the plan is inefficient or that an index is missing.
FTS queries
Hybrid queries
For hybrid queries,explain_plan returns the reranker label followed by the vector and FTS sub-plans, indented for readability:
analyze_plan executes both sub-queries and labels them Vector Search Plan:
and FTS Search Plan:. Compare their filter placement and execution measurements.
analyze_plan executes the query and returns the physical plan annotated with
runtime metrics such as elapsed time, rows produced, bytes read and index comparisons.
Where explain_plan tells you what a query intends to do,
analysis shows the work performed.
The SQL equivalent is
EXPLAIN ANALYZE.Read execution measurements
Executes the query and provides detailed runtime metrics, including:- Operation duration (
_elapsed_compute_) - Data processing statistics (
_output_rows_,_bytes_read_) - Index effectiveness (
_index_comparisons_,_indices_loaded_) - Resource utilization (
_iops_,_requests_)
Measure the example query
Let’s useanalyze_plan to run the query and analyze the query performance, which will help us identify potential bottlenecks:
Performance Metrics Analysis
Inspect the recorded execution measurements
Inspect the recorded execution measurements
- Scanned 1,200,000 rows from the LanceDB table
- Read 1.86GB of data in 78 I/O operations
- Only loaded necessary columns (
vectorandidentifier) - Unordered scan for parallel processing
- Applied prefilter condition (
identifier > 0 AND identifier < 1000000) - Reduced dataset from 1.2M to 1,099,508 rows
- KNN search used L2 (Euclidean) distance metric
- Vector comparisons processed in 1076 batches
- KNN results sorted by distance (TopK with fetch=100)
- Null distances filtered out
- Batches coalesced to target size of 1024 rows
- Additional columns fetched for final results
- Remote take operation for 100 results
- Final projection of required columns
Distributed metrics on LanceDB Enterprise
Enterprise When you callanalyze_plan against a LanceDB Enterprise deployment that distributes its queries across a pool of workers, the query runs on those workers. By default the returned plan aggregates each operator’s metrics into a single value, matching the single-node output of LanceDB Embedded. Pass a distributed_metrics mode when you need to see how work was split across workers:
Python exposes this option through
distributed_metrics. In TypeScript, pass
the same mode to analyzePlan() as its distributedMetrics argument. Python
queries on LanceDB Embedded ignore the distributed mode and return single-node
measurements.
Use
per_worker or full sparingly; the plan output grows with the size of your worker pool. Stick with the default aggregate mode for routine tuning.Identify expensive operations
- The query computes distances for 1,099,508 vectors; inspect timings to assess its contribution to latency
- The scan reads 1.86 GB in this recorded run
- Full table scan due to lack of indices
- Compare this scan with an indexed plan using the same query
Compare the query after adding indexes
Compare the recorded indexed and unindexed plans
Compare the recorded indexed and unindexed plans
The recorded query after adding vector and scalar indexes produced this plan:
Optimized Plan Analysis
- Range filter using scalar index
- Only 2 index files and 562 scalar index parts loaded
- 2.3M index comparisons for matches
- IVF index with 20 probes
- Only 20 index parts loaded
- 25,893 vector comparisons
- 2,000 matching vectors
- Sorts by distance
- Limits to top 100 results
- Batches into groups of 1024
- Single output batch
- One remote take per row
- Returns specified columns: chunk_index, title, identifier, and distance
Performance Improvements
- Before: Full table scan of 1.2M rows, 1.86GB data
- After: Only 2 indices and 562 scalar index parts loaded
- Benefit: Eliminated table scans for prefilter
- Before: L2 calculations on 1,099,508 vectors
- After:
- About 97.6% fewer vector comparisons (25,893 versus 1,099,508)
- Decreased output batches from 1,076 to 20
- RemoteTake operation remains consistent
Choose a change to measure
1. Index Implementation
- Columns used in WHERE clauses
- Vector columns for similarity searches
- Join columns used in
merge_insert
Index Coverage Monitoring
Use
table.index_stats() to monitor index coverage.
Unindexed rows can add scan work. Use their count alongside query measurements
to decide when index maintenance is needed.2. Query Plan Optimization
Regularly analyze your query plans to identify and address performance
bottlenecks. The
analyze_plan output provides detailed metrics to guide
optimization efforts.3. Getting Started with Optimization
Keep the query, data and result limit fixed while changing one factor. Compare rows processed, bytes read, timings and returned results. For ANN changes, measure recall too: doing less work is useful only if retrieval quality meets your application’s needs. See vector indexes and scalar indexes for index choices.Vector and full-text search
SQL vector search support depends on the Enterprise server version and query form. Versions that supportORDER BY distance(vector, query_vector) LIMIT k
over a table scan can plan indexed or flat vector search. Check that your
server version supports this form, then use
EXPLAIN to inspect the query plan.
The SDK vector search API is also available in
LanceDB Enterprise and LanceDB Embedded. Use it when you prefer the SDK query builder or need to
search embedded tables.
For full-text search in SQL, use the fts() table
function or the contains_tokens
predicate.
SQL syntax and API references
The SQL reference documents statement syntax, supported expressions, and data types. For SDK methods and their arguments, use the Python, TypeScript, or Rust API reference.SQL Reference
Statement syntax, expressions, and data types for LanceDB SQL.
Next steps
Quickstart
Run a complete create, insert and search example.
Search
Choose vector, full-text or hybrid retrieval.
Filtering
Narrow results with predicates.
SDKs
Compare language clients and find their API references.