PowerFX Elegant Exclusion

In data manipulation, a “Set Difference” operation—returning items from Set A that are not in Set B—is a fundamental
building block. SQL has EXCEPT, C# has .Except(), and Python has
set.difference().

PowerFX, however, lacks a native Except(Table1, Table2) function.

This omission becomes painfully obvious when building “Pick Once” UIs, such as:

  • Rostering: Assigning an employee to a shift (excluding those already assigned).
  • Inventory: Selecting a serial number to ship (excluding those already shipped).
  • Configuration: Adding a category to a project (excluding categories already added).

Developers often stumble into performance pitfalls trying to solve this. This article details the definitive,
high-performance pattern to manually implement Except in PowerFX.

The Trap: Naive Approaches

When trying to exclude items, developers usually try two things that don’t work well.

1. The “Row-by-Row” Lookup

The intuitive approach is to filter the source table by checking the destination table for every single row.

// DO NOT DO THIS
Filter(
    'AllOptions',
    IsBlank(LookUp('UsedOptions', OptionID = ThisRecord.OptionID))
)

Why this fails: It causes O(N²) complexity (1,000 queries for 1,000 items) and hits
delegation limits immediately.

2. The Direct “Not In” Filter

After the first failure, the next logical step seems to be:

// OFTEN FAILS
Filter(
    'AllOptions',
    Not(OptionID in colExcludedIDs)
)

Why this fails: While syntactically clean, Not(... in ...) is frequently
non-delegable against Dataverse or SharePoint. The server doesn’t know how to interpret “not in
this local collection,” causing the app to either download the entire dataset locally (slow) or silently fail to
filter correctly beyond the 500/2000 row limit.

The Solution: The “Materialize & Filter” Pattern

To implement Except efficiently and reliably, we must separate the read operation from
the filter operation. We snapshot the exclusion list into memory first, then use
AddColumns to perform the check locally.

Step 1: Materialize the Exclusion Set (The “Right” Side)

We need a flat, lightweight collection of just the keys we want to exclude. We do this before the UI renders
(e.g., OnSelect of the button that opens the modal).

ClearCollect(
    colExcludedIDs,
    Distinct(
        AddColumns(
            Filter(
                'Assignments', // The table containing used items
                ParentID = SelectedParent.ID // Optional: Contextual filter
            ),
            ExclusionKey,
            Text(RelatedItem.ID) // Extract the Foreign Key ID
        ),
        ExclusionKey
    )
);

Analysis:

  • Contextual Filtering: We first filter the assignments table to the relevant scope.
  • Flattening: We extract the foreign key (ExclusionKey). This is crucial. We don’t
    want to cache entire records, just the unique identifiers needed for the exclusion check.
  • Result: colExcludedIDs is a single-column table of IDs.

Step 2: Filter the Source (The “Left” Side)

Now, in the `Items` property of the ComboBox, we perform the exclusion.

Filter(
    AddColumns(
        'MasterList', // The table of all available options
        IsAlreadyUsed,
        If(
            // Check if this item's ID exists in our exclusion collection
            ItemID in colExcludedIDs,
            true,
            false
        )
    ),
    IsAlreadyUsed = false
)

Analysis:

  • AddColumns vs LookUp: Instead of looking up records, we use
    AddColumns to attach a boolean flag (IsAlreadyUsed) to each candidate row.
  • The in Operator: This is the secret sauce. Checking
    Value in Collection is an O(1) or O(log N) operation in memory.
    It is orders of magnitude faster than a database lookup.
  • Final Filter: The outer Filter simply keeps rows where the flag is
    false.

Summary

Until Microsoft introduces a native Except() function, this pattern is the industry standard for set
difference in PowerFX.

Feature Naive Approach Direct “Not In” Materialize & Filter Pattern
Complexity O(N²) O(N) O(N)
Delegation Fails Fails Safe
Reliability Low Low High

By treating the exclusion list as a temporary, local dataset, you ensure your apps remain snappy and scalable,
regardless of how complex your data relationships become.

Recent Comments

Recent Comments