--- title: "DataFrame operations" sidebarTitle: "DataFrame operations" description: "Every relational operation on a LanceDB DataFrame, with signatures and semantics." icon: table-list keywords: ["dataframe", "select", "filter", "join", "aggregate", "substrait"] --- A DataFrame is a lazy logical plan. Every operation returns a new plan; nothing executes until [`execute()`](#execution). Obtain one from a connection: ```python import lancedb from lancedb import col, lit from lancedb import sql_functions as F db = lancedb.connect("db://your-database", api_key="...") df = db.table("documents") ``` `db.table(name, *, namespace_path=None)` scans a table. It opens the table to read its schema, so the plan knows its columns before anything runs. ## Projection {#projection} | Operation | Effect | | --- | --- | | `select(*expressions)` | Project columns or expressions. At least one required. | | `with_column(name, expression)` | Add one derived column. | | `with_columns(**expressions)` | Add several, named by keyword. | | `drop(*columns)` | Remove columns by name. | | `with_column_renamed(old, new)` | Rename one column. | ```python df.select("id", col("score") * 2) df.with_columns(doubled=col("score") * 2, flag=col("score") > 0.5) ``` A bare string is a column reference, so `select("id")` and `select(col("id"))` mean the same thing. Use `col()` when you need methods or operators on it. ## Filtering {#filtering} `filter(*predicates)` keeps rows matching **every** predicate — multiple arguments are combined with `AND`: ```python df.filter(col("score") > 0.5, col("lang") == "en") df.filter((col("score") > 0.5) & (col("lang") == "en")) # equivalent ``` At least one predicate is required. For `OR`, combine with `|` inside a single argument. Python's `and` and `or` cannot be overloaded and will **not** do what you want on expressions. Use `&`, `|` and `~`, and parenthesise them — they bind more tightly than comparison operators, so `col("a") > 1 & col("b") < 2` parses as `col("a") > (1 & col("b")) < 2`. ## Aggregation {#aggregation} `aggregate(group_by, aggregates)` groups rows and computes aggregates over each group. ```python df.aggregate("lang", F.count(col("id"))) df.aggregate(["lang", "source"], [F.avg(col("score")), F.max(col("score"))]) df.aggregate(None, F.count(col("id"))) # whole relation, no grouping ``` Both arguments accept a single value or an iterable. `group_by=None` aggregates over the whole relation. The available aggregates are in [Expressions](/sql/expressions#aggregates). ## Ordering and slicing {#ordering} | Operation | Effect | | --- | --- | | `sort(*expressions)` | Order rows. | | `limit(count, offset=0)` | Take `count` rows after skipping `offset`. | | `distinct()` | Remove duplicate rows. | A bare expression sorts **ascending, nulls last**. For other orders use a `SortExpr`, which follows DataFusion's convention: when `nulls_first` is not given it defaults to the opposite of `ascending`, so descending sorts put nulls first. ```python df.sort(col("score")) # ascending, nulls last df.sort(col("score").sort(ascending=False)) ``` ## Joins {#joins} ```python df.join(other, on="id") df.join(other, left_on="doc_id", right_on="id", how="left") df.join(other, on=["tenant", "id"], how="inner") ``` Pass either `on` (same key names on both sides) or **both** `left_on` and `right_on`. Passing `on` together with either raises, as does passing neither. Keys may be a single name or a sequence. `how` defaults to `"inner"`. Both plans must come from the **same connection** and the **same default namespace**; otherwise the join raises before anything is submitted. The same applies to set operations. Use `alias(name)` to give a relation a name before joining it to itself. ## Set operations {#set-operations} | Operation | Effect | | --- | --- | | `union(other, distinct=False)` | Rows from both. | | `intersect(other, distinct=False)` | Rows present in both. | | `except_all(other, distinct=False)` | Rows in this plan and not the other. | All three default to **keeping duplicates**. Pass `distinct=True` for set semantics. Note this is the opposite default to SQL's bare `UNION`, which deduplicates — `union()` here corresponds to `UNION ALL`. ## Inspecting a plan {#inspecting} | | | | --- | --- | | `schema` | the plan's output schema, as a `pyarrow.Schema` | | `to_substrait()` | the logical plan as a Substrait protobuf | | `repr(df)` | a readable rendering of the plan | `schema` and `to_substrait()` work on a local connection, so a plan can be asserted against in tests without a server. ## Execution {#execution} | | Returns | | --- | --- | | `execute()` | a blocking Arrow `RecordBatchReader` | | `execute_async()` | a server-side query handle | On `AsyncConnection` both are coroutines and `execute()` yields an async reader. Execution requires a remote connection. A local one can build and serialize a plan but not run it. ```python reader = df.execute() table = reader.read_all() ```