FeatureQL for the Impatient
If you know SQL, this tutorial shows how FeatureQL turns business logic into named features that you can compose, persist, and reuse. FOR binds the data universe (every key the graph may read), not a SQL WHERE. Grain is declared, not a primary-key check.
You will build one customer segment, persist its source features, and use it in a report.
Build a customer segment, step by step
The goal: define IS_LONG_TENURED — a feature that takes a customer ID and returns whether the customer signed up before 2024.
Step 1: Declare the input
This query starts with an input: the customer ID for which you want to compute features.
DEFINE
customer_id := INPUT(BIGINT) -- Define the input
RETURN
customer_id -- The feature you want to return
FOR
customer_id := BIND_VALUES(ARRAY(100, 101)) -- Bind concrete values to the input
;| CUSTOMER_ID BIGINT |
|---|
| 100 |
| 101 |
No tables, no FROM clause. You declared an input (CUSTOMER_ID), selected it, and bound two values to evaluate. That's the core pattern: define features, bind inputs, get results.
Step 2: Connect to real data
Create some mock customer data. FeatureQL can embed raw SQL when setup or an outer query needs it:
CREATE SCHEMA IF NOT exists impatient;
--
DROP TABLE IF EXISTS impatient.dim_customers;
--
CREATE TABLE impatient.dim_customers (
customer_id BIGINT,
preferred_city_id VARCHAR,
name VARCHAR,
time_create TIMESTAMP
);
--
INSERT INTO impatient.dim_customers
SELECT *
FROM (
VALUES
(CAST(100 AS BIGINT), 'BCN', 'John Doe', TIMESTAMP '2023-09-18 14:30:00'),
(CAST(101 AS BIGINT), 'BCN', 'Jane Doe', TIMESTAMP '2024-12-08 09:45:00'),
(CAST(102 AS BIGINT), 'BCN', 'Jack Doe', TIMESTAMP '2025-02-03 12:00:02')
) AS t(customer_id, preferred_city_id, name, time_create);
--
SELECT CAST(COUNT() AS INTEGER) AS Count FROM impatient.dim_customers;| Count BIGINT |
|---|
| 3 |
Now map table columns to features with EXTERNAL_COLUMNS(). The BIND TO clause tells FeatureQL which column is the lookup key:
DEFINE
customer_id := INPUT(BIGINT),
data.customers := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO customer_id,
name VARCHAR,
time_create TIMESTAMP
FROM TABLE(impatient.dim_customers)
),
customer_name := data.customers[name],
customer_time_create := data.customers[time_create]
RETURN
customer_id,
customer_name,
customer_time_create
FOR
customer_id := BIND_VALUES(ARRAY(100, 101)) -- We need to choose the values we want to bind to the input
;| CUSTOMER_ID BIGINT | CUSTOMER_NAME VARCHAR | CUSTOMER_TIME_CREATE TIMESTAMP(3) |
|---|---|---|
| 100 | John Doe | 2023-09-18T14:30:00 |
| 101 | Jane Doe | 2024-12-08T09:45:00 |
Notice: you didn't write a JOIN. You declared which columns you need and which key to look them up by. FeatureQL generates the join for you.
Step 3: Add business logic
One line of logic on top of the data mapping. A customer is long-tenured if they signed up before 2024:
DEFINE
customer_id := INPUT(BIGINT),
data.customers := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO customer_id,
name VARCHAR,
time_create TIMESTAMP
FROM TABLE(impatient.dim_customers)
),
customer_name := data.customers[name],
customer_time_create := data.customers[time_create],
is_long_tenured := customer_time_create < '2024-01-01'::TIMESTAMP
RETURN
customer_id,
customer_name,
customer_time_create,
is_long_tenured
FOR
customer_id := BIND_COLUMNS(
customer_id
FROM TABLE(impatient.dim_customers)
)
;| CUSTOMER_ID BIGINT | CUSTOMER_NAME VARCHAR | CUSTOMER_TIME_CREATE TIMESTAMP(3) | IS_LONG_TENURED BOOLEAN |
|---|---|---|---|
| 100 | John Doe | 2023-09-18T14:30:00 | true |
| 101 | Jane Doe | 2024-12-08T09:45:00 | false |
| 102 | Jack Doe | 2025-02-03T12:00:02 | false |
IS_LONG_TENURED is a pure function: it depends on CUSTOMER_TIME_CREATE, which depends on DATA.CUSTOMERS, which depends on CUSTOMER_ID. FeatureQL resolves this dependency chain automatically — you never specify evaluation order.
Also notice: the FOR clause switched from BIND_VALUES() (an explicit list) to BIND_COLUMNS() (an entire table column). The features stay the same; only the binding strategy changes.
Persist and reuse
The features above were local — defined in the query. In practice, core features like CUSTOMER_ID and CUSTOMER_NAME would be persisted in the registry so the entire organization can reuse them:
CREATE OR REPLACE FEATURES IN FM.TUTORIALS.IMPATIENT AS
PERSIST
customer_id := INPUT(BIGINT),
data.customers := EXTERNAL_COLUMNS(
customer_id BIGINT BIND TO customer_id,
name VARCHAR,
time_create TIMESTAMP
FROM TABLE(impatient.dim_customers)
),
customer_name := data.customers[name],
customer_time_create := data.customers[time_create]
;| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.IMPATIENT.CUSTOMER_ID | CREATED | Feature created as not exists |
| FM.TUTORIALS.IMPATIENT.DATA.CUSTOMERS | CREATED | Feature created as not exists |
| FM.TUTORIALS.IMPATIENT.CUSTOMER_NAME | CREATED | Feature created as not exists |
| FM.TUTORIALS.IMPATIENT.CUSTOMER_TIME_CREATE | CREATED | Feature created as not exists |
Now anyone can build on top without redefining the data mapping. An analyst just writes their business logic:
DEFINE
is_long_tenured := customer_time_create < '2024-01-01'::TIMESTAMP
RETURN
customer_id,
is_long_tenured
FROM FM.TUTORIALS.IMPATIENT
FOR
customer_id := BIND_COLUMNS(
customer_id
FROM TABLE(impatient.dim_customers)
)
;| FM.TUTORIALS.IMPATIENT.CUSTOMER_ID BIGINT | IS_LONG_TENURED BOOLEAN |
|---|---|
| 100 | true |
| 101 | false |
| 102 | false |
The FROM FM.TUTORIALS.IMPATIENT clause loads the persisted features, DEFINE adds a local feature, and FOR binds the input. FeatureQL resolves the data mapping and dependencies.
From features to dashboards
Features produce fine-grained, per-entity results. For dashboards and reports, you typically need aggregations. FeatureQL handles this with GROUP BY as part of the feature definition:
DEFINE
is_long_tenured := data.customers[time_create] < '2024-01-01'::TIMESTAMP
RETURN
is_long_tenured,
num_customers := COUNT() GROUP BY (is_long_tenured), -- Note that group by (is) part of the feature definition
FROM FM.TUTORIALS.IMPATIENT
FOR
customer_id := BIND_COLUMNS(
customer_id
FROM TABLE(impatient.dim_customers)
)
;| IS_LONG_TENURED BOOLEAN | NUM_CUSTOMERS BIGINT |
|---|---|
| false | 2 |
| true | 1 |
For existing BI tools that generate their own SQL, you can embed FeatureQL inside SQL. The BI tool writes the outer GROUP BY and WHERE clauses; FeatureQL handles the feature computation:
WITH
CUSTOMERS_DATA AS FEATUREQL(
DEFINE
IS_LONG_TENURED := CUSTOMER_TIME_CREATE < '2024-01-01'::TIMESTAMP,
RETURN
CUSTOMER_ID,
IS_LONG_TENURED,
FROM FM.TUTORIALS.IMPATIENT
FOR
CUSTOMER_ID := BIND_COLUMNS(customer_id FROM TABLE(impatient.dim_customers))
;
)
SELECT
IS_LONG_TENURED,
COUNT() AS NUM_CUSTOMERS
FROM CUSTOMERS_DATA
GROUP BY (IS_LONG_TENURED);| IS_LONG_TENURED BOOLEAN | NUM_CUSTOMERS BIGINT |
|---|---|
| false | 2 |
| true | 1 |
This means you can adopt FeatureQL incrementally — your existing SQL tools keep working, they just get better data.
Where to go next
- Hello World — Learn the language fundamentals one concept at a time
- E-commerce tutorial — Model customers, orders, items, and cross-entity relationships
- Mapping data sources — How to connect FeatureQL to your tables
- Array of rows — Work with nested related data
- Metaprogramming — Compute literals and query text at compile time
- Batch analytics — Using FeatureMesh as a semantic layer for your data warehouse
- Real-time serving — Serving features from Redis, JDBC, and HTTP in production
- Python library — Run FeatureQL against your own data
- FeatureQL with AI — Give the same tools to an assistant