Files
lancedb/docs/web/feature-engineering/add-computed-column.mdx
T
Xuanwo 8029428c71 feat: persist automatic Function refresh resource budgets (#4953)
## Depends on merged #4951

Phase 1 merged as `4d22c0ce2b6825c880c818e737b079db494b4053`, so this PR
now targets `main`. It reuses the canonical
`FunctionWorkerResourceBudget` introduced there rather than copying the
type.

For Function `add_columns` declarations, Python and Rust now persist
`auto_refresh_resource_budget` in computed-column metadata and send it
as `auto_refresh_resource_budget` in the remote request. CPU
milliseconds and memory bytes are required positive integers. Omitting
`gpu_count` is accepted as zero and the canonical outgoing JSON still
contains `"gpu_count": 0`. Unknown fields, missing CPU or memory, wrong
types, zero CPU or memory, negative values, and overflow are rejected.

The same budget is applied to every public output binding of a
multi-output Function declaration. Existing metadata is retained, and
SQL-expression declarations or local execution reject the budget instead
of silently ignoring it. Node is unchanged because its current
`addColumns` surface only accepts SQL computed expressions and has no
Function declaration path.

This change does not implement fleet call concurrency limits or enforce
the budget in the runtime. The Enterprise server must support the nested
Function worker resource-budget contract before these values can act as
execution limits.

## Validation

- Python extension build passed.
- Remote Function declaration and local unsupported-path rejection each
passed.
- Canonical metadata round-trip passed, including omitted GPU decoding
to zero and explicit `gpu_count: 0` on encode.
- Function multi-output remote wire test passed with `--features
remote`.
- Repository-scoped Python type check passed with 0 errors.
- All runtime source blobs match the native-tested source. The
post-native deltas are one documentation example line and an existing
routing-test stub update for the new optional argument; the affected
parameterized test passed 3/3 against the existing extension build. No
private test runners are part of this PR.
2026-11-11 11:23:17 +08:00

266 lines
9.6 KiB
Plaintext

---
title: "Create a computed column"
sidebarTitle: "Create Computed Column"
description: "Declare a column from a SQL expression. Nothing is evaluated until you refresh it."
icon: table-columns
keywords: ["computed columns", "add_columns", "transforms", "schema"]
availability:
embedded: unavailable
enterprise: available
---
import {
PySchemaAddSetup,
} from '/snippets/tables.mdx';
Lance can write a new column without rewriting the columns already in the
table. That matters most on a table that also holds large images, video or
embeddings, where rewriting everything just to add one small derived field
would be wasteful. See [Lance format](/basics/lance) for the storage model
this relies on.
A computed column is how LanceDB formalizes that capability for feature
engineering specifically: instead of writing the derived values immediately,
it is declared with an expression and starts with no values. Declaring it is
cheap on any table size, because nothing is evaluated until
[a refresh](/feature-engineering/refresh-column) asks for it.
<Note>
`computed` requires LanceDB Enterprise. Ordinary eager `add_columns`
transforms, without `computed`, remain available in LanceDB Embedded.
</Note>
## Fill a column with a Function {#functions}
<Badge color="red">Enterprise</Badge>
When the value cannot be expressed in SQL -- an embedding, a model call, an
image decode -- the column is filled by a registered
[function](/management/function) instead of a SQL expression. It needs a
deployment with Functions enabled.
Connect with an [Enterprise connection](/basics/catalog#enterprise).
Continuing the [`caption` Function](/curation/create-function#chain)
registered in Create a Function, against a `frames` table that already has
a `jpeg` column:
<CodeGroup>
```python Python icon="python"
version = db.create_function_async(caption).wait()
table.add_columns({"caption": version(jpeg=col("jpeg"))})
```
```sql SQL icon="database"
ALTER TABLE frames ADD COLUMN caption AS caption(jpeg) WITH (version = 'fv_1')
```
</CodeGroup>
A Function application is called with its parameters bound to source
columns through `col(...)`, keyed by the output column name. It commits a
new, empty table version the same way a SQL expression does, so the usual
[versioning](/management/table#versioning) applies.
Both a Function application and a SQL expression accept
`auto_refresh_enabled` and `auto_refresh_interval_seconds`, and both refresh
and toggle the same way; see [Auto Refresh](/enterprise/auto-refresh).
Python and Rust Function declarations can also set
`auto_refresh_resource_budget` to give their automatic refreshes an aggregate
CPU, memory, and optional GPU budget. CPU and memory are required. The nested
`gpu_count` defaults to zero, so omitting it requests CPU-only workers:
<CodeGroup>
```python Python icon="python"
table.add_columns(
{"caption": version(jpeg=col("jpeg"))},
auto_refresh_resource_budget={
"cpu_millis": 8000,
"memory_bytes": 16 * 1024**3,
},
)
```
```rust Rust icon="rust"
table
.add_columns()
.function(function_application)
.computed_options(ComputedColumnOptions {
auto_refresh_enabled: true,
auto_refresh_interval_seconds: None,
auto_refresh_resource_budget: Some(
FunctionWorkerResourceBudget::new(8000, 17_179_869_184),
),
})
.execute()
.await?;
```
</CodeGroup>
This setting applies to every output in the Function binding. SQL-expression
columns do not use Function workers, so they reject
`auto_refresh_resource_budget` instead of accepting an option that has no
effect. The separate `resource_budget` refresh option configures one explicit
refresh and does not change this automatic-refresh declaration. The Node.js
SDK does not currently expose a Function declaration through `addColumns`, so
this option is not added to an unsupported surface.
## Fill a column with a SQL expression {#expressions}
A value that SQL can compute doesn't need a Function. Declare it with
`computed` instead, as an expression evaluated over the row:
<CodeGroup>
```python Python icon="python"
table = db.create_table("computed_demo", [{"x": 1}, {"x": 2}])
table.add_columns(computed={"doubled": "x * 2"})
# AddColumnsResult(version=2)
```
```sql SQL icon="database"
ALTER TABLE computed_demo ADD COLUMN doubled AS x * 2
```
</CodeGroup>
That commits a new table version with the column present and empty. It is an
ordinary table version, so the usual [versioning](/management/table#versioning) applies,
including that you can travel back to before the column existed.
The expression can reference any column in the table, including another
computed column -- declaration order is dependency order, so it may
reference one declared earlier in the same mapping, but not one declared
after it:
<CodeGroup>
```python Python icon="python"
table.add_columns(computed={
"power_score": "cast(x as double) / 2.0",
"is_strong": "power_score > 0.5",
})
```
```sql SQL icon="database"
ALTER TABLE computed_demo ADD COLUMN power_score AS cast(x as double) / 2.0
ALTER TABLE computed_demo ADD COLUMN is_strong AS power_score > 0.5
```
</CodeGroup>
The functions available are the ones the
[SQL dialect](/sql/dql/functions) documents.
A literal in the expression has to fit the column it meets: `x / 2.0` is
refused when `x` is an integer column, so write `cast(x as double) / 2.0`.
<Warning>
Keep the expression to top-level columns. A column refresh can fail on an
expression that reads a field inside a struct, such as
`stats.magic + stats.strength`. Try a new expression on a small table first.
</Warning>
## Compute now or on refresh {#computed-vs-transforms}
`add_columns` takes either, and the difference matters:
- **SQL expressions in `transforms`** are evaluated *now*, for every row, as
part of the call. Good for a small table, or a column you want populated
immediately. A `FunctionApplication` passed through `transforms` instead
declares empty Function-bound columns, which a refresh fills; that needs a
deployment with Functions enabled, and the embedded library refuses it.
- **`computed`** stores the expression and evaluates nothing. The column is
committed empty and filled later by
[`refresh_column`](/feature-engineering/refresh-column).
`computed` and `transforms` cannot be combined in one call. Reach for `computed` when the table is
large enough that you would rather control when the work happens.
Automatic refresh is on by default for each computed column, and takes effect
when your Enterprise deployment enables its refresh scheduler. Until then,
call [`refresh_column`](/feature-engineering/refresh-column) yourself. Pass
`auto_refresh_enabled=False` to opt a column out, or
`auto_refresh_interval_seconds` to set how often it refreshes:
<CodeGroup>
```python Python icon="python"
table.add_columns(
computed={"doubled": "x * 2"},
auto_refresh_interval_seconds=300,
)
```
```sql SQL icon="database"
ALTER TABLE computed_demo ADD COLUMN doubled AS x * 2
WITH (auto_refresh_interval_seconds = 300)
```
</CodeGroup>
See [Auto Refresh](/enterprise/auto-refresh) for the
full behavior, including how to change the setting later.
An eager `transforms` call looks like this, starting from a small product
table:
<CodeGroup>
<CodeBlock filename="Python" language="Python" icon="python">
{PySchemaAddSetup}
</CodeBlock>
</CodeGroup>
<CodeGroup>
```python Python icon="python"
# Add a discounted price column (10% discount)
table.add_columns({"discounted_price": "cast((price * 0.9) as float)"})
```
```sql SQL icon="database"
ALTER TABLE schema_evolution_add_example ADD COLUMN discounted_price
AS cast((price * 0.9) as float)
```
</CodeGroup>
SQL's `ADD COLUMN ... AS` always declares the column empty and defers the
expression to a refresh, the same as `computed` does -- there is no SQL
form of eager `transforms`. The Python call above is the only way to get
discounted_price filled immediately; declaring it in SQL fills it only
once [`REFRESH COLUMN`](/sql/dml/refresh-column) runs.
For feature engineering on large existing tables, group related derived
features into one `add_columns` call instead of running many separate
writes: this creates one new table version for the schema change and
computes every new column from the existing rows in it, rather than growing
the table's version history with many small updates.
<Info>
On local tables, Python `add_columns` also accepts pandas or Polars
DataFrames, PyArrow Tables, RecordBatches, RecordBatchReaders, and iterables
of batches. Pass only the new columns, with exactly one row per current live
table row in storage scan order. DataFrame indexes are ignored, and reader
batch boundaries need not match table fragments. Keep the table unchanged
between computing the values and adding them. Readers are consumed once.
Data inputs cannot be combined with `computed` columns and are not supported
on remote tables. Python callables and batch UDFs are not accepted; compute
those values first, then pass the result as a DataFrame or reader.
</Info>
## Change columns used by a computed column {#input-constraints}
<Warning>
While a computed column's declaration references another column, that column
cannot be renamed, retyped, or dropped. Drop the computed column first
if you need to change one of its inputs.
</Warning>
This is worth knowing before deriving a column from something you expect to
reshape later. Dropping the computed column releases the constraint.
## Related guides {#next}
<CardGroup cols={2}>
<Card title="Refresh a computed column" icon="arrows-rotate" href="/feature-engineering/refresh-column">
Fill the rows that lack values.
</Card>
<Card title="Functions" icon="code" href="/management/function">
Fill a column with Python.
</Card>
</CardGroup>