Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Membership and full joins

Use a semi join to keep rows whose keys appear in another dataset, or an anti join to find keys that are absent. Both return only the left columns. They are useful for checking allowed IDs, finding missing records and filtering against a lookup table without multiplying rows.

These examples require the optional engine.

Keep matching rows or find missing ones

import std.dataframe as df

def main() -> Result[(), df.DataFrameError | AllocError | IoError]:
    orders = df.frame([
        df.column_i64("id", [1, 2, 2, 3]?)?,
        df.column_str("order", ["one", "two-a", "two-b", "three"]?)?
    ]?)?
    allowed = df.frame([df.column_i64("id", [2, 2]?)?]?)?
    keys = [df.col("id")?]?
    order = [df.SortKey(df.col("order")?)]?
    matching = orders.lazy()?.join(
        allowed.lazy()?, keys, keys, how=df.JoinKind.Semi
    )?.sort(order)?.collect()?
    missing = orders.lazy()?.join(
        allowed.lazy()?, keys, keys, how=df.JoinKind.Anti
    )?.sort(order)?.collect()?
    print(matching.values_str("order")?)?
    print(missing.values_str("order")?)?
    Ok(())
["two-a", "two-b"]
["one", "three"]

The duplicate allowed ID does not duplicate matching orders. Each left row is kept at most once; duplicate left rows would still be separate rows. No right-side columns or suffixes appear in the result.

Null keys do not match by default, so a null left key goes into the anti result. Set nulls_equal=True only when null should be an ordinary matching key. Sorting above makes presentation deterministic.

Reconcile both sides with a full join

A full join also keeps rows found only on the right. Both key columns remain, so explicitly derive a common display key if that is what your report needs.

import std.dataframe as df

def main() -> Result[(), df.DataFrameError | AllocError | IoError]:
    before = df.frame([
        df.column_i64("id", [1, 2]?)?,
        df.column_i64("price", [10, 20]?)?
    ]?)?
    after = df.frame([
        df.column_i64("id", [2, 3]?)?,
        df.column_i64("price", [25, 30]?)?
    ]?)?
    keys = [df.col("id")?]?
    joined = before.lazy()?.join(after.lazy()?, keys, keys, how=df.JoinKind.Full)?
    display_id = df.col("id")?.fill_null(df.col("id_right")?)?.alias("item")?
    result = joined.with_columns([display_id]?)?.sort([
        df.SortKey(df.col("item")?)
    ]?)?.collect()?
    print(result.values_i64("item")?)?
    print(result.values_i64_nullable("price")?)?
    print(result.values_i64_nullable("price_right")?)?
    Ok(())
[1, 2, 3]
[Option[i64].Some(10), Option[i64].Some(20), Option[i64].Nothing]
[Option[i64].Nothing, Option[i64].Some(25), Option[i64].Some(30)]

Full joins preserve duplicate matching pairs, just like inner and left joins. They do not verify key uniqueness. The nulls above describe missing sides; they are not query failures. See the join contracts for composite keys, null matching, name collisions and ordering.

Continue with Match nearby observations when a lookup should use the most recent or nearest key rather than an identical key.