Files
lancedb/docs/web/curation/join.mdx
T
Jack YeandClaude Sonnet 5 de95ed3f6d docs: overhaul curation, feature engineering, security and deployment guides (#4748)
A broad documentation pass across the areas LanceDB Enterprise and
Embedded
diverge on most:

**Data Curation** -- GPU Function execution, Slurm scheduling (and what
Slurm is, for readers new to it), blob late materialization as its own
page, joins, aggregation/profiling, sampling, near-duplicate detection,
chunking, distributed shuffle, and IVF-partition clustering.

**Feature Engineering** -- computed columns and materialized views,
restructured to lead with the Function-based path and show the SQL form
side by side with Python throughout; a single Auto Refresh page instead
of
duplicating scheduling details on every refresh page; adaptive
execution;
checkpointing; GPU and Slurm placement for refresh workers; a Training
Cache page for the PyTorch data loader's automatic cache benefit.

**Security & Governance** -- an Audit Log reference (schema, access,
grants), Secrets promoted to an ordinary catalog object with their own
SQL
DDL and a vending/rotation guide, and privilege-table fixes verified
against the authorization source.

**Deployment & Maintenance** -- Horizontal Scaling and OpenTelemetry
moved
into a real Deployment area; Automated Cleanup folded into Intelligent
Auto-Maintenance (every other maintenance decision already lived there);
RTE Compaction and Advanced Conflict Resolution documented in depth.

**Landing page** -- the Enterprise feature comparison table reorganized
(several redundant or stale rows removed, several new areas added), and
the
"why Enterprise" bullets replaced with ones tied to a concrete page
(Performance, Scale, Fault tolerance, Multicloud deployment, Simplify
operations) instead of vague promises.

Plus a full pass addressing lancedb-gatekeeper's review: a real
test-safety
bug (a quickstart test could silently overwrite and delete a
deployment's
own `frames` table), a fabricated SQL statement with no backing
implementation, an actual dedup-guarantee bug in `take_row_ids`, and
dozens
of documentation-accuracy fixes verified against source rather than
taken
on faith -- each either fixed, or left with an explicit note on why it
needs a feature or deployment owner's sign-off instead of being guessed
at.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018PXMCbRH6i5w47TsjeEUMT

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-03 16:17:24 -08:00

179 lines
6.3 KiB
Plaintext

---
title: "Joining datasets"
sidebarTitle: "Join"
description: "Combine records with labels or metadata, and check for missing or duplicate matches."
icon: link
keywords: ["join", "labels", "metadata", "enrichment", "namespace"]
availability:
embedded: unavailable
enterprise: available
---
A SQL join combines records from two tables using a shared key. Use joins to
attach labels, metadata or model scores to your documents. Choose an inner join
to keep matching records, or a left join to retain records without a match.
## Example data {#example-data}
Connect with an [Enterprise SQL client](/basics/sql). The examples use a small
document table and a separate label table. Expand the setup below to create them.
<Accordion title="Create the example tables">
`curation_documents` has six records describing four documents.
`curation_labels` has one label for each of three documents. `row_id` identifies
a record; `doc_id` identifies the document it describes.
Run each statement separately in a scratch database or namespace. Create these
tables once for the whole section, using fresh names if they already exist.
<CodeGroup>
```sql SQL icon="database"
CREATE TABLE curation_documents (
row_id BIGINT, doc_id BIGINT, version BIGINT,
source VARCHAR, caption VARCHAR, score DOUBLE
);
INSERT INTO curation_documents VALUES
(1, 10, 1, 'web', NULL, 0.80),
(2, 10, 2, 'web', 'A cat on a chair', 0.95),
(3, 20, 1, 'books', 'A dog in a field', 0.90),
(4, 30, 1, 'web', NULL, 0.70),
(5, 30, 1, 'web', 'A cat by a window', 0.85),
(6, 40, 1, 'books', 'A bird in a tree', 0.60);
CREATE TABLE curation_labels (doc_id BIGINT, label VARCHAR);
INSERT INTO curation_labels VALUES
(10, 'cat'), (20, 'dog'), (30, 'cat');
```
</CodeGroup>
`score` is a sample quality score, not a search result score. Document 10 has
two versions. Document 30 has two records at the same version. Document 40
has no label. These cases let you check the effect of each operation.
</Accordion>
## Basic join {#join-type}
An inner join keeps matching records:
<CodeGroup>
```sql SQL icon="database"
SELECT d.row_id, d.doc_id, l.label
FROM curation_documents AS d
JOIN curation_labels AS l ON l.doc_id = d.doc_id
ORDER BY d.row_id;
```
</CodeGroup>
This returns **5 rows**: both records for document 10, one for document 20,
and both records for document 30. To produce one row per document,
[deduplicate the source records](/curation/windowing#dedup).
### Find records without a match
A left join keeps every record from the left table and fills missing right-side
values with `NULL`. Add `WHERE l.doc_id IS NULL` to return only unlabeled documents:
<CodeGroup>
```sql SQL icon="database"
SELECT DISTINCT d.doc_id
FROM curation_documents AS d
LEFT JOIN curation_labels AS l ON l.doc_id = d.doc_id
WHERE l.doc_id IS NULL
ORDER BY d.doc_id;
```
</CodeGroup>
The result is **document 40**. You might send it for annotation, retain it for
an unlabeled-data workflow, or exclude it from a task that requires labels.
## Handle duplicate keys {#labels}
This example joins `curation_documents.doc_id` to `curation_labels.doc_id`.
First check whether the label table contains more than one row per document:
<CodeGroup>
```sql SQL icon="database"
SELECT doc_id, count(*) AS labels
FROM curation_labels
GROUP BY doc_id
HAVING count(*) > 1;
```
</CodeGroup>
The example returns no rows. If your data returns duplicates, decide whether
they represent valid multiple labels or records that need consolidation.
A join does not enforce one-to-one matching.
## Call a Function across the join {#functions}
A [Function](/curation/create-function) can read columns from either side
of a join, the same as any other expression -- it runs once the join has
matched rows, not before. Register `caption_quality` first:
<CodeGroup>
```python Python icon="python"
from lancedb import udf
@udf
def caption_quality(caption: str) -> float:
... # score the caption, e.g. with a lightweight heuristic or model
db.create_function_async(caption_quality).wait()
```
```sql SQL icon="database"
SELECT d.row_id, l.label, caption_quality(d.caption) AS quality
FROM curation_documents AS d
JOIN curation_labels AS l ON l.doc_id = d.doc_id
WHERE d.caption IS NOT NULL
ORDER BY d.row_id;
```
</CodeGroup>
Scoring after the join, rather than before, means `caption_quality` only
runs on rows that actually matched a label -- a document with no label
never pays for the Function call.
## Join tables across namespaces {#namespaces}
Use a table's qualified path to address it in another
[namespace](/management/namespace). For example, a table named `images` in
`prod.media` is addressed as `prod.media.images`. The same matching-key and
join-type rules apply; your identity needs access to the source objects.
## Combine search results with metadata {#search}
A [SQL full-text search](/sql/dql/fts) returns rows that can be joined to other
tables. Join using an identifier retained in the search result, then check
whether the metadata has one or several matches per result.
For an SDK search, retain the matching IDs and combine them with metadata in
your application or save the selected records to a table for further SQL work.
See [SQL search interfaces](/basics/sql#no-vector-sql) for supported query forms.
## Join performance {#performance}
Check the row count, unmatched keys and duplicate matches before using a
joined dataset. Select only the columns needed for the next step, especially
when source records include large media or embedding columns.
Use [Query plans](/basics/sql#query-plans) to see how the query reads and
combines data. Check the plan to see whether the query uses indexes and how it performs the
join. An index on a key does not guarantee that the join will use it.
### Defer wide columns until after the join {#late-materialization}
When one side of a join carries a BlobV2 column, an embedding, or both,
join on a narrow key first and project the wide columns only in the
final select list, so the planner can defer reading them until after the
join has already narrowed down which rows survive. See
[Blob Late Materialization](/curation/blob-late-materialization) for the
mechanism, eligibility, and how to confirm it happened.
## Save the result or continue curating {#reference}
Keep the query for reuse or [save the selected data](/curation#next).
See [Joins](/sql/dql/join) for join types and exact SQL behavior.