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:
| Operator | What it does | Typical use case |
|---|---|---|
BIND_VALUE() | Binds a single value | Testing, simple parameterization |
BIND_VALUES() | Binds an array of values | Prototyping, small batch processing |
BIND_COLUMNS() | Binds columns from a table or query | Production batch processing |
@BIND_KEYSET() | Binds entity keys from a predefined set | Offline 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.
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),
;| NAME VARCHAR | HELLO VARCHAR | NUMBER BIGINT | OTHER_NUMBER BIGINT |
|---|---|---|---|
| FeatureQL | Hello, FeatureQL! | 5 | 10 |
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.
WITH
INPUT1 := INPUT(BIGINT),
CALCULATED := INPUT1 + 1
SELECT
INPUT1,
CALCULATED,
FOR
INPUT1 := BIND_VALUES(ARRAY[1, 3, 5]),
;| INPUT1 BIGINT | CALCULATED VARCHAR |
|---|---|
| 1 | 2 |
| 3 | 4 |
| 5 | 6 |
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.
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)]),
;| INPUT1 BIGINT | INPUT2 BIGINT | CALCULATED VARCHAR |
|---|---|---|
| 1 | 2 | 3 |
| 3 | 4 | 7 |
| 5 | 6 | 11 |
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.
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]),
;| FEATUREF1 BIGINT | FEATUREF2 BIGINT | FEATUREFF VARCHAR |
|---|---|---|
| 1 | 2 | 3 |
| 1 | 4 | 5 |
| 1 | 6 | 7 |
| 3 | 2 | 5 |
| 3 | 4 | 7 |
| 3 | 6 | 9 |
| 5 | 2 | 7 |
| 5 | 4 | 9 |
| 5 | 6 | 11 |
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:
/* 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;| Count BIGINT |
|---|
| 3 |
Then bind columns from that table to features. The query evaluates once per row in the source table.
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)),| INPUT1 BIGINT | INPUT2 BIGINT | ADD_INPUTS BIGINT |
|---|---|---|
| 2 | 2 | 4 |
| 3 | 4 | 7 |
| 4 | 6 | 10 |
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 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(...), 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)), 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.