> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pixeltable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Join tables to combine data

> Join multiple Pixeltable tables on shared keys to combine metadata, embeddings, and computed columns into unified, queryable result sets.

<a href="https://kaggle.com/kernels/welcome?src=https://github.com/pixeltable/pixeltable/blob/release/docs/release/howto/cookbooks/core/query-join-tables.ipynb" id="openKaggle" target="_blank" rel="noopener noreferrer"><img src="https://kaggle.com/static/images/open-in-kaggle.svg" alt="Open in Kaggle" style={{ display: 'inline', margin: '0px' }} noZoom /></a>  <a href="https://colab.research.google.com/github/pixeltable/pixeltable/blob/release/docs/release/howto/cookbooks/core/query-join-tables.ipynb" id="openColab" target="_blank" rel="noopener noreferrer"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab" style={{ display: 'inline', margin: '0px' }} noZoom /></a>  <a href="https://raw.githubusercontent.com/pixeltable/pixeltable/refs/tags/release/docs/release/howto/cookbooks/core/query-join-tables.ipynb" id="downloadNotebook" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/%E2%AC%87-Download%20Notebook-blue" alt="Download Notebook" style={{ display: 'inline', margin: '0px' }} noZoom /></a>

<Tip>This documentation page is also available as an interactive notebook. You can launch the notebook in
Kaggle or Colab, or download it for use with an IDE or local Jupyter installation, by clicking one of the
above links.</Tip>

export const quartoRawHtml = [`
<table>
<thead>
<tr>
<th>Scenario</th>
<th>Tables</th>
<th>Join on</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Customer orders</td>
<td style="vertical-align: middle;"><code>customers</code>, <code>orders</code></td>
<td style="vertical-align: middle;"><code>customer_id</code></td>
</tr>
<tr>
<td style="vertical-align: middle;">Product catalog</td>
<td style="vertical-align: middle;"><code>products</code>, <code>inventory</code></td>
<td style="vertical-align: middle;"><code>sku</code></td>
</tr>
<tr>
<td style="vertical-align: middle;">Media library</td>
<td style="vertical-align: middle;"><code>videos</code>, <code>captions</code></td>
<td style="vertical-align: middle;"><code>video_id</code></td>
</tr>
<tr>
<td style="vertical-align: middle;">User activity</td>
<td style="vertical-align: middle;"><code>users</code>, <code>events</code></td>
<td style="vertical-align: middle;"><code>user_id</code></td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">customer_id</th>
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">email</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">1</td>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">alice@example.com</td>
</tr>
<tr>
<td style="vertical-align: middle;">2</td>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">bob@example.com</td>
</tr>
<tr>
<td style="vertical-align: middle;">3</td>
<td style="vertical-align: middle;">Charlie</td>
<td style="vertical-align: middle;">charlie@example.com</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">order_id</th>
<th data-quarto-table-cell-role="th">customer_id</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">101</td>
<td style="vertical-align: middle;">1</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.</td>
</tr>
<tr>
<td style="vertical-align: middle;">102</td>
<td style="vertical-align: middle;">1</td>
<td style="vertical-align: middle;">Mouse</td>
<td style="vertical-align: middle;">29.</td>
</tr>
<tr>
<td style="vertical-align: middle;">103</td>
<td style="vertical-align: middle;">2</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.</td>
</tr>
<tr>
<td style="vertical-align: middle;">104</td>
<td style="vertical-align: middle;">4</td>
<td style="vertical-align: middle;">Monitor</td>
<td style="vertical-align: middle;">299.</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">Mouse</td>
<td style="vertical-align: middle;">29.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">Mouse</td>
<td style="vertical-align: middle;">29.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Charlie</td>
<td style="vertical-align: middle;">NaN</td>
<td style="vertical-align: middle;">NaN</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">email</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">alice@example.com</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">bob@example.com</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">total_spent</th>
<th data-quarto-table-cell-role="th">order_count</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">1028.</td>
<td style="vertical-align: middle;">2</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">79.</td>
<td style="vertical-align: middle;">1</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">price</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">Widget</td>
<td style="vertical-align: middle;">19.99</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">Widget</td>
<td style="vertical-align: middle;">19.99</td>
</tr>
<tr>
<td style="vertical-align: middle;">Charlie</td>
<td style="vertical-align: middle;">Widget</td>
<td style="vertical-align: middle;">19.99</td>
</tr>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">Gadget</td>
<td style="vertical-align: middle;">29.99</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">Gadget</td>
<td style="vertical-align: middle;">29.99</td>
</tr>
<tr>
<td style="vertical-align: middle;">Charlie</td>
<td style="vertical-align: middle;">Gadget</td>
<td style="vertical-align: middle;">29.99</td>
</tr>
</tbody>
</table>
`, `<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }
    .dataframe tbody tr th {
        vertical-align: top;
    }
    .dataframe thead th {
        text-align: right;
    }
</style>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">email</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">0</td>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">alice@example.com</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.0</td>
</tr>
<tr>
<td style="vertical-align: middle;">1</td>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">alice@example.com</td>
<td style="vertical-align: middle;">Mouse</td>
<td style="vertical-align: middle;">29.0</td>
</tr>
<tr>
<td style="vertical-align: middle;">2</td>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">bob@example.com</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.0</td>
</tr>
</tbody>
</table>
`, `
</div>`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">name</th>
<th data-quarto-table-cell-role="th">email</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">alice@example.com</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Alice</td>
<td style="vertical-align: middle;">alice@example.com</td>
<td style="vertical-align: middle;">Mouse</td>
<td style="vertical-align: middle;">29.</td>
</tr>
<tr>
<td style="vertical-align: middle;">Bob</td>
<td style="vertical-align: middle;">bob@example.com</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">order_id</th>
<th data-quarto-table-cell-role="th">customer_id</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">101</td>
<td style="vertical-align: middle;">1</td>
<td style="vertical-align: middle;">Laptop</td>
<td style="vertical-align: middle;">999.</td>
</tr>
<tr>
<td style="vertical-align: middle;">102</td>
<td style="vertical-align: middle;">1</td>
<td style="vertical-align: middle;">Mouse</td>
<td style="vertical-align: middle;">29.</td>
</tr>
</tbody>
</table>
`, `
<table class="dataframe" data-quarto-postprocess="true" data-border="1">
<thead>
<tr style="text-align: right;">
<th data-quarto-table-cell-role="th">order_id</th>
<th data-quarto-table-cell-role="th">customer_id</th>
<th data-quarto-table-cell-role="th">product</th>
<th data-quarto-table-cell-role="th">amount</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;">103</td>
<td style="vertical-align: middle;">2</td>
<td style="vertical-align: middle;">Keyboard</td>
<td style="vertical-align: middle;">79.</td>
</tr>
<tr>
<td style="vertical-align: middle;">104</td>
<td style="vertical-align: middle;">4</td>
<td style="vertical-align: middle;">Monitor</td>
<td style="vertical-align: middle;">299.</td>
</tr>
</tbody>
</table>
`, `
<table>
<thead>
<tr>
<th>Type</th>
<th>Keeps</th>
<th>Use when</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align: middle;"><code>'inner'</code></td>
<td style="vertical-align: middle;">Matching rows only</td>
<td style="vertical-align: middle;">You need data from both tables</td>
</tr>
<tr>
<td style="vertical-align: middle;"><code>'left'</code></td>
<td style="vertical-align: middle;">All left + matching right</td>
<td style="vertical-align: middle;">Keep all from first table</td>
</tr>
<tr>
<td style="vertical-align: middle;"><code>'full_outer'</code></td>
<td style="vertical-align: middle;">All from both</td>
<td style="vertical-align: middle;">Need complete picture</td>
</tr>
<tr>
<td style="vertical-align: middle;"><code>'cross'</code></td>
<td style="vertical-align: middle;">All combinations</td>
<td style="vertical-align: middle;">Cartesian product</td>
</tr>
</tbody>
</table>
`];

Combine data from multiple tables using inner, left, and cross joins.

## Problem

You have related data in separate tables and need to combine them for
analysis—customers with orders, products with inventory, or media with
metadata.

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[0] }} />

## Solution

**What’s in this recipe:**

* Inner join to match rows from both tables
* Left join to keep all rows from the first table
* Cross join for Cartesian product (all combinations)
* Join with filtering, aggregation, and saving results
* Paginate results with `limit()` and `offset`

Use `table1.join(table2, on=..., how=...)` to combine tables based on
matching columns.

### Setup

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
%pip install -qU pixeltable
```

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import pixeltable as pxt
import pixeltable.functions as pxtf

# Create a fresh directory
pxt.drop_dir('join_demo', force=True)
pxt.create_dir('join_demo')
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Connected to Pixeltable database at: postgresql+psycopg://postgres:@/pixeltable?host=/Users/pjlb/.pixeltable/pgdata
  Created directory 'join\_demo'.
  \<pixeltable.catalog.dir.Dir at 0x148e73850>
</pre>

### Create sample tables

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Create a customers table
customers = pxt.create_table(
    'join_demo/customers',
    {'customer_id': pxt.Int, 'name': pxt.String, 'email': pxt.String},
)

customers.insert(
    [
        {'customer_id': 1, 'name': 'Alice', 'email': 'alice@example.com'},
        {'customer_id': 2, 'name': 'Bob', 'email': 'bob@example.com'},
        {
            'customer_id': 3,
            'name': 'Charlie',
            'email': 'charlie@example.com',
        },
    ]
)

customers.collect()
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Created table 'customers'.
  Inserted 3 rows with 0 errors in 0.01 s (385.68 rows/s)
</pre>

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[1] }} />

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Create an orders table
orders = pxt.create_table(
    'join_demo/orders',
    {
        'order_id': pxt.Int,
        'customer_id': pxt.Int,
        'product': pxt.String,
        'amount': pxt.Float,
    },
)

orders.insert(
    [
        {
            'order_id': 101,
            'customer_id': 1,
            'product': 'Laptop',
            'amount': 999.00,
        },
        {
            'order_id': 102,
            'customer_id': 1,
            'product': 'Mouse',
            'amount': 29.00,
        },
        {
            'order_id': 103,
            'customer_id': 2,
            'product': 'Keyboard',
            'amount': 79.00,
        },
        {
            'order_id': 104,
            'customer_id': 4,
            'product': 'Monitor',
            'amount': 299.00,
        },  # No matching customer
    ]
)

orders.collect()
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Created table 'orders'.
  Inserted 4 rows with 0 errors in 0.01 s (657.81 rows/s)
</pre>

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[2] }} />

### Inner join (matching rows only)

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Inner join: only rows that match in both tables
customers.join(
    orders, on=customers.customer_id == orders.customer_id, how='inner'
).select(customers.name, orders.product, orders.amount).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[3] }} />

### Left join (keep all from first table)

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Left join: all customers, with order data where available
# Charlie has no orders, so product/amount will be null
customers.join(
    orders, on=customers.customer_id == orders.customer_id, how='left'
).select(customers.name, orders.product, orders.amount).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[4] }} />

### Join with filtering

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Combine join with where clause to filter results
customers.join(
    orders, on=customers.customer_id == orders.customer_id, how='inner'
).where(orders.amount > 50).select(
    customers.name, customers.email, orders.product, orders.amount
).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[5] }} />

### Join with aggregation

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Join and aggregate: total spending per customer
customers.join(
    orders, on=customers.customer_id == orders.customer_id, how='inner'
).group_by(customers.name).select(
    customers.name,
    total_spent=pxtf.sum(orders.amount),
    order_count=pxtf.count(orders.order_id),
).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[6] }} />

### Cross join (all combinations)

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Cross join: every customer paired with every product (no 'on' condition)
products = pxt.create_table(
    'join_demo/products', {'product': pxt.String, 'price': pxt.Float}
)
products.insert(
    [
        {'product': 'Widget', 'price': 19.99},
        {'product': 'Gadget', 'price': 29.99},
    ]
)

customers.join(products, how='cross').select(
    customers.name, products.product, products.price
).collect()
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Created table 'products'.
  Inserted 2 rows with 0 errors in 0.00 s (422.52 rows/s)
</pre>

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[7] }} />

### Save join results to a new table

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Build a join query and collect as DataFrame
customer_orders_df = (
    customers.join(
        orders,
        on=customers.customer_id == orders.customer_id,
        how='inner',
    )
    .select(
        name=customers.name,
        email=customers.email,
        product=orders.product,
        amount=orders.amount,
    )
    .collect()
    .to_pandas()
)

customer_orders_df
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[8] }} />

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[9] }} />

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[10] }} />

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Create a new table from the DataFrame
orders_report = pxt.create_table(
    'join_demo/orders_report', source=customer_orders_df
)

orders_report.collect()
```

<pre style={{ 'margin': '-20px 20px 0px 20px', 'padding': '0px', 'background-color': 'transparent', 'color': 'black' }}>
  Created table 'orders\_report'.
  Inserted 3 rows with 0 errors in 0.01 s (500.32 rows/s)
</pre>

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[11] }} />

### Paginate results with limit and offset

Use `limit(n, offset=k)` to retrieve results in pages. This is useful
for displaying results incrementally or building paginated APIs.

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Page 1: first 2 rows
orders.order_by(orders.order_id).limit(2).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[12] }} />

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Page 2: next 2 rows (skip the first 2)
orders.order_by(orders.order_id).limit(2, offset=2).collect()
```

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[13] }} />

## Explanation

**Join types:**

<div style={{ 'margin': '0px 20px 0px 20px' }} dangerouslySetInnerHTML={{ __html: quartoRawHtml[14] }} />

**Join syntax:**

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Simple: join on column by name
t1.join(t2, on=t1.id)

# Explicit predicate
t1.join(t2, on=t1.customer_id == t2.customer_id)

# Composite key
t1.join(t2, on=(t1.pk1 == t2.pk1) & (t1.pk2 == t2.pk2))
```

**Aggregation functions:**

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
from pixeltable.functions import sum, count, mean, min, max

# Use as functions, not methods
total=sum(t.amount)
num_rows=count(t.id)
```

**Saving join results:**

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# Collect as DataFrame, then create table
df = query.select(name=t.col, ...).collect().to_pandas()
new_table = pxt.create_table('path', source=df)
```

**Pagination:**

```python theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
# limit(n) returns at most n rows
# limit(n, offset=k) skips the first k rows, then returns n
query.order_by(t.id).limit(10)           # rows 0-9
query.order_by(t.id).limit(10, offset=10) # rows 10-19
```

**Tips:**

* Use explicit predicates (`t1.col == t2.col`) for clarity
* Chain `.where()` after join to filter results
* Chain `.group_by()` for aggregations
* Use `'left'` join when the first table is your “main” table
* Use named columns in `.select(name=col)` for clean column names
* Always use `.order_by()` with pagination to get deterministic page
  ordering

## See also

* [Look up structured
  data](/howto/cookbooks/agents/pattern-data-lookup) -
  Use retrieval UDFs for lookups
* [Sample data for
  training](/howto/cookbooks/data/data-sampling) -
  Sample from joined results
