Prepared statements

Prepared statements are the final step for taking features to production. They pre-compile a feature and its entire dependency graph — including all data source calls and cross-source joins — into a single optimized query. This eliminates parsing and resolution overhead at request time, giving you predictable latency for real-time serving.

Defining the base features

A prepared statement wraps an existing feature. Start by creating the features you want to serve:

FeatureQL
CREATE OR REPLACE FEATURES AS
SELECT
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A := INPUT(BIGINT),
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B := INPUT(BIGINT),
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C := INPUT(BIGINT),
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_D := FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A + FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B,
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_E := FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B + FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C,
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R := FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_D + FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_E
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_ACREATEDFeature created as not exists
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_BCREATEDFeature created as not exists
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_CCREATEDFeature created as not exists
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_DCREATEDFeature created as not exists
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_ECREATEDFeature created as not exists
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_RCREATEDFeature created as not exists

Here, FEATURE_R depends on FEATURE_D and FEATURE_E, which in turn depend on the three inputs FEATURE_A, FEATURE_B, and FEATURE_C. The prepared statement will resolve this entire dependency chain at compile time.

In a real deployment, these features would typically include EXTERNAL_REDIS(), EXTERNAL_COLUMNS(), or EXTERNAL_HTTP() calls — the prepared statement compiles all of that into a single execution plan.

Creating the prepared statement

Use PREPARED_STATEMENT() to compile a feature for real-time serving:

FeatureQL
CREATE OR REPLACE FEATURE FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS AS
PREPARED_STATEMENT(
    FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R
    USING INPUTS
        (FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A, FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B),
        FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C
);
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PSCREATEDFeature created as not exists

The USING INPUTS clause declares how inputs are grouped. Inputs in the same parenthesized group (FEATURE_A, FEATURE_B) are paired row-by-row (like BIND_VALUES with an array of rows). Inputs in separate groups create a cross-product. This grouping controls how the prepared statement handles multiple input rows.

You can inspect the compiled result with DESCRIBE:

FeatureQL
DESCRIBE FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS
Result
OUTPUT VARCHAR
[{name: FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS, function: PREPARED_STATEMENT, params: {base: {kind: IDENTIFIER}, inputs: {kind: ARRAY}, positions: {kind: ARRAY}}, status: DEV, position: 0.0, meta: {}, properties: {}, unnamed_dependencies: {}, id: 2T9UB2NUA23E, feature_type: {}, input: [], signature: NULL, dependencies: {}, restrictions: [], inputs: [], formula: NULL, computed_properties: {prepared_statement_details: {query_sql: WITH\nCTE_2Z25ZAE6_0 AS (\n SELECT\n source_UNNAMED_HVTDUSTQ.\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\" AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \n source_UNNAMED_HVTDUSTQ.\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\" AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\", \n source_UNNAMED_8YY5DTNS.\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\" AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\"\n FROM (\n \tSELECT 1\n ) source_CONSTANTS\n CROSS JOIN (\n \tWITH\n\tCTE_JRVYMYHN_0 AS (\n\t(\n\t SELECT t.*\n\t FROM (SELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\" FROM INPUT_TABLE_1)\n\t AS t(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\")\n\t )\n\t), \n\tCTE_JRVYMYHN_1 AS (\n\tSELECT\n\t\tCAST(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\" AS BIGINT) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \n\t\tCAST(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\" AS BIGINT) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\"\n\tFROM CTE_JRVYMYHN_0\n\t)\n\tSELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\"\n\tFROM CTE_JRVYMYHN_1\n ) source_UNNAMED_HVTDUSTQ\n CROSS JOIN (\n \tWITH\n\tCTE_2QMB49D2_0 AS (\n\t(\n\t SELECT t.*\n\t FROM (SELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\" FROM INPUT_TABLE_2)\n\t AS t(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\")\n\t )\n\t), \n\tCTE_2QMB49D2_1 AS (\n\tSELECT\n\t\tCAST(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\" AS BIGINT) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\"\n\tFROM CTE_2QMB49D2_0\n\t)\n\tSELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\"\n\tFROM CTE_2QMB49D2_1\n ) source_UNNAMED_8YY5DTNS\n), \nCTE_2Z25ZAE6_1 AS (\nSELECT\n\tCAST(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\" AS BIGINT) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \n\tCAST(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\" AS BIGINT) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\", \n\tCAST(\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\" AS BIGINT) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\"\nFROM CTE_2Z25ZAE6_0\n), \nCTE_2Z25ZAE6_2 AS (\nSELECT\n\t((\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\" + \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\") + (\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\" + \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\")) AS \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS\", \n\t\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \n\t\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\", \n\t\"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\"\nFROM CTE_2Z25ZAE6_1\n)\nSELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS\"\nFROM CTE_2Z25ZAE6_2\n, input_schema: {INPUT_TABLE_1: [[FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A, BIGINT], [FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B, BIGINT]], INPUT_TABLE_2: [[FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C, BIGINT]]}, output_schema: [[FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A, BIGINT], [FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B, BIGINT], [FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C, BIGINT], [FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS, BIGINT]], query_featureql: \n WITH\n \n \n (FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A, FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B) :=\n BIND_SQL(SELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A\", \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B\" FROM INPUT_TABLE_1)\n ,\n \n \n FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C :=\n BIND_SQL(SELECT \"FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\" FROM INPUT_TABLE_2)\n ,\n \n PREPARED_STATEMENT_AS_MACRO := MACRO(\n FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R\n USING INPUTS FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A, FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B, FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C\n )\n SELECT\n \n FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A,\n \n FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B,\n \n FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C,\n \n PREPARED_STATEMENT_AS_MACRO(FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A, FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B, FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C) AS FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS\n }}}]

Calling a prepared statement

From Python

import requests

def call_prepared_statement(feature_name: str, inputs: dict[str, list]) -> dict:
    response = requests.post(
        "http://localhost:10090/v1/featureql",
        json={
            "statement": {
                "id": feature_name,
                "inputs": inputs
            }
        },
        headers={
            "Content-Type": "application/json",
            "Authorization": "Bearer YOUR_ACCESS_TOKEN",
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()
python

From curl

curl -X POST "http://localhost:10090/v1/featureql" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -d '{
    "statement": {
      "id": "FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_R_PS",
      "inputs": {
        "FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_A": [1, 2],
        "FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_B": [10, 20],
        "FM.TUTORIALS.PREPARED_STATEMENTS.FEATURE_C": [100]
      }
    }
  }'
bash

The inputs object maps each input feature name to an array of values. Inputs in the same group must have arrays of equal length (they are paired positionally). Inputs in different groups can have different lengths (cross-product).

Last update at: 2026/03/03 16:47:38
Last updated: 2026-03-03 16:48:19