Binding inputs

Features defined with INPUT() are placeholders — they declare what a feature needs, not what value it has. Binding is how you provide those concrete values so the query can actually execute.

FeatureQL offers several binding operators depending on where your data comes from:

OperatorWhat it doesTypical use case
BIND_VALUE()Binds a single valueTesting, simple parameterization
BIND_VALUES()Binds an array of valuesPrototyping, small batch processing
BIND_COLUMNS()Binds columns from a table or queryProduction batch processing
@BIND_KEYSET()Binds entity keys from a predefined setOffline analytics at scale

How binding affects result cardinality

The number of rows you get back depends on how many values you bind and whether you bind features together or separately:

  • Single feature, n values → n rows.
  • Multiple features, single binding (using ROW()): n value pairs → n rows. The values are paired positionally.
  • Multiple features, separate bindings (requires FOR CROSS): n values × m values → n × m rows. FeatureQL produces the Cartesian product of all combinations.

Bind a single value

The simplest case: one value per input. Derived features are computed automatically from the bound values.

FeatureQL
WITH
    NAME := INPUT(VARCHAR),             -- Declare an input of type VARCHAR
    HELLO := 'Hello, ' || NAME || '!',
    NUMBER := INPUT(BIGINT),            -- Declare an input of type BIGINT
    OTHER_NUMBER := NUMBER * 2,
SELECT
    NAME,
    HELLO,
    NUMBER,
    OTHER_NUMBER
FOR CROSS
    NAME := BIND_VALUE('FeatureQL'),
    NUMBER := BIND_VALUE(5),
;
Result
NAME VARCHARHELLO VARCHARNUMBER BIGINTOTHER_NUMBER BIGINT
FeatureQLHello, FeatureQL!510

Bind multiple values

For batch workloads, bind an array of values to evaluate a feature across many inputs at once.

One feature

Pass an array to BIND_VALUES(). Each element produces one result row.

FeatureQL
WITH
    INPUT1 := INPUT(BIGINT),
    CALCULATED := INPUT1 + 1
SELECT
    INPUT1,
    CALCULATED,
FOR
    INPUT1 := BIND_VALUES(ARRAY[1, 3, 5]),
;
Result
INPUT1 BIGINTCALCULATED VARCHAR
12
34
56

Multiple features, paired

To bind several features together, wrap each set of values in a ROW(). The first row goes with the first row, second with second, and so on — no cross-product.

FeatureQL
WITH
    INPUT1 := INPUT(BIGINT),
    INPUT2 := INPUT(BIGINT),
    CALCULATED := INPUT1 + INPUT2
SELECT
    INPUT1,
    INPUT2,
    CALCULATED
FOR
    (INPUT1, INPUT2) := BIND_VALUES(ARRAY[ROW(1, 2), ROW(3, 4), ROW(5, 6)]),
;
Result
INPUT1 BIGINTINPUT2 BIGINTCALCULATED VARCHAR
123
347
5611

Multiple features, separate (Cartesian product)

When you bind features with separate BIND_VALUES() calls, FeatureQL evaluates every combination. Three values for each of two features produces 3 × 3 = 9 rows.

Because cross products can produce unexpectedly large result sets, FeatureQL requires the explicit FOR CROSS keyword instead of plain FOR. This makes the intent clear and prevents accidental Cartesian products when you meant to pair values. If you want pairing instead, use ROW() tuples in a single BIND_VALUES() as shown above.

FeatureQL
WITH
    FEATUREF1 := INPUT(BIGINT),
    FEATUREF2 := INPUT(BIGINT),
    FEATUREFF := FEATUREF1 + FEATUREF2,
SELECT
    FEATUREF1,
    FEATUREF2,
    FEATUREFF,
FOR CROSS
    FEATUREF1 := BIND_VALUES(ARRAY[1, 3, 5]),
    FEATUREF2 := BIND_VALUES(ARRAY[2, 4, 6]),
;
Result
FEATUREF1 BIGINTFEATUREF2 BIGINTFEATUREFF VARCHAR
123
145
167
325
347
369
527
549
5611

Bind to external data

When your input values live in a database table, use BIND_COLUMNS() to pull them in directly. First, let's create a sample table:

FeatureQL
/* SQL */
CREATE SCHEMA IF NOT exists bnd;
--
CREATE OR REPLACE TABLE bnd.table_example AS
SELECT unnest as col_key, unnest+1 as val1, unnest*2 as val2 FROM unnest(ARRAY[1,2,3]);
--
SELECT CAST(COUNT(*) AS INTEGER) AS Count FROM bnd.table_example;
Result
Count BIGINT
3

Then bind columns from that table to features. The query evaluates once per row in the source table.

FeatureQL
WITH
    INPUT1 := INPUT(BIGINT),
    INPUT2 := INPUT(BIGINT),
    ADD_INPUTS := INPUT1 + INPUT2,
SELECT
    INPUT1,
    INPUT2,
    ADD_INPUTS,
FOR
    (INPUT1, INPUT2) := BIND_COLUMNS(val1, val2 FROM TABLE(bnd.table_example)),
Result
INPUT1 BIGINTINPUT2 BIGINTADD_INPUTS BIGINT
224
347
4610

Nested bindings for multi-entity queries

When a query computes features at one entity grain but those features depend on data from finer-grained entities, you need values for the sub-entity keys too. Use the NESTED keyword to supply those sub-entity bindings without affecting the outer result cardinality.

FOR
    CUSTOMER_ID := BIND_COLUMNS(id FROM TABLE(dim_customers)),   -- outer grain: one row per customer
    NESTED ORDER_ID := BIND_COLUMNS(id FROM TABLE(fct_orders)),  -- sub-entity: all order keys
    NESTED ITEM_ID  := BIND_COLUMNS(id FROM TABLE(fct_items)),   -- sub-entity: all item keys
sql

The outer binding (CUSTOMER_ID) drives the result rows. The NESTED bindings provide the key pools that the RELATED() and EXTEND() subqueries inside each outer row can look up.

What counts as a sub-entity key

A NESTED binding is appropriate when the INPUT() feature is only consumed inside a RELATED() or EXTEND() subquery, or as the BIND TO join key of an EXTERNAL_COLUMNS() mapping. If you bind such an input directly with a non-NESTED binding, FeatureQL would cross-product it with the outer entity — producing one result row per customer–order pair instead of one per customer.

NESTED bindings and FOR CROSS

NESTED bindings do not count toward the Cartesian-product requirement. A query with one non-nested BIND_COLUMNS and two NESTED bindings uses plain FOR, not FOR CROSS:

FOR                                             -- plain FOR: only one non-nested binding
    CUSTOMER_ID := BIND_COLUMNS(...),
    NESTED ORDER_ID := BIND_COLUMNS(...),
    NESTED TICKET_ID := BIND_COLUMNS(...),
sql

FOR CROSS is only required when there are two or more non-nested multi-value bindings that would form a cross product.

Using persisted library features with nested sub-entities

When your namespace contains persisted features that use RELATED() (e.g. CUSTOMER_AVG_DAYS_PAID, CUSTOMER_OPEN_TICKET_COUNT), those features internally reference sub-entity keys such as INVOICE_ID and TICKET_ID. The outer SELECT only returns customer-grain features, but the sub-entity key pools still need to be provided:

SELECT
    CUSTOMER_ID,
    STATUS := CUSTOMER_HEALTH_STATUS   -- persisted feature; depends on INVOICE_ID and TICKET_ID internally
FROM FM.MY_NAMESPACE
FOR
    CUSTOMER_ID := BIND_COLUMNS(id FROM TABLE(dim_customers)),
    NESTED INVOICE_ID := BIND_COLUMNS(id FROM TABLE(fct_invoices)),
    NESTED TICKET_ID  := BIND_COLUMNS(id FROM TABLE(fct_support_tickets)),
sql

FeatureQL detects that INVOICE_ID and TICKET_ID are only consumed inside RELATED/EXTEND subqueries and treats them as sub-grain join keys — not as outer-grain inputs.

Using sets of keys

For analytics at scale, you can use predefined sets of entity keys with the metaprogramming function @BIND_KEYSET(). This is especially useful for offline batch analytics where you want to evaluate features for all entities matching certain business criteria.

More details in the bind keyset section.

Last update at: 2026/05/26 17:22:09