FK-Resolved Dependencies
We're not aware of another platform that does this declaratively. InfluxDB, TimescaleDB, Apache Flink, KDB+, Snowflake, Databricks, and Palantir Foundry all support time-series storage and (in most cases) temporal joins. None of them auto-segment a derivation when an FK relationship changes — on each of those platforms, that requires custom pipeline code to detect change points, split time ranges, re-resolve targets, and stitch results back together. GroveStreams handles it as a single SQL expression on the variable definition.
Overview
Standard expression variables point directly at a specific stream (component + stream UID). FK-resolved dependencies replace that static pointer with a SQL statement that dynamically resolves which stream to use at derivation time.This enables two powerful capabilities:
- Temporal relationship resolution — When an FK relationship changes over time (a meter moves from Customer A to Customer B), the engine automatically segments the derivation range and uses the correct target for each period.
- Fan-in aggregation — When a SQL resolves to multiple targets (all meters connected to a customer), the engine loads all target streams, aggregates them (SUM, AVG, MIN, MAX), and feeds a single result into the expression.
employee and salaries as two tables joined by emp_no, create a
single employee component template with a salary temporal stream.
Each salary change becomes a new sample at its effective date — GS handles the history natively.
Result: no FK needed, no fan-in aggregation, no walkChain segmentation, no join in the query.
SELECT _component_name, salary FROM employee returns the current salary; adding
Range(ed=<past-date>) returns what the salary was at that point. The JDBC import
is a bridge for legacy data — it preserves source-table shape so you can start querying
immediately, but designing fresh in GS usually means fewer tables and simpler queries.
How It Works
The Data Model
FK-resolved dependencies involve two distinct streams that serve different roles:| Stream | Role |
| Link stream | A stream on the source component whose value is the target component's
primary key (component id). It defines the relationship — who is connected
to whom. The derivation engine reads the link stream's history to detect when relationships
changed over time.
The link stream's type matches the target's PK type (STRING for string keys, LONG for numeric keys, etc.). This mirrors standard database FK behavior — a FK column holds the PK value of the referenced row. Example: A meter's customerId stream stores the customer component's id
(e.g. "C123") this meter is currently assigned to. A salaries component's
emp_no stream stores the employee's numeric PK
(e.g. 10001 as LONG).
Legacy UUID form: prior to 2026-04, link streams were required to be STRING and held the target's component UUID directly. That form still works in orgs where the target components' _component_id was auto-set to the UUID (the GS default when
no explicit id is provided).
|
| Value stream | A stream on the target (resolved) component that provides the actual data
used in the expression. This is the stream whose data becomes the variable's value in the formula.
Example: The customer's energy_rate stream — its values are what the expression engine
sees when evaluating the rate variable.
|
The Resolution SQL bridges these two streams: it follows the link stream to find the target component, and specifies which value stream to load from that component.
The required data model pattern:
- An entity (component) has a link stream whose value is the
primary key (component id) of a related entity. The link stream's type matches the
target's PK type: STRING for string keys, LONG for numeric keys.
For example, a meter component has a
customerIdstream (STRING) storing the customer's id. A JDBC-imported salaries component has anemp_nostream (LONG) storing the numeric employee PK.
Important: It is highly recommended that link streams be Regular (random) streams rather than interval streams. Regular streams only store data points when the value actually changes, making them ideal for relationship tracking. Interval streams can be used but are not recommended because they store a value per interval, which requires the engine to walk backward through the history in chunks to detect relationship changes. - The link stream's FK target template is configured (via
REFERENCES targetTemplatein DDL, or the Stream Editor) to tell the system which template the linked entity belongs to. - The target entity (e.g., the customer) has one or more value streams (e.g.,
energy_rate,credit_limit) that the derivation expression needs. The value stream ID is specified in the Resolution SQL.
Resolution SQL Variables
Two variables are injected into the namespace of the resolution SQL before it executes. Pick whichever matches the shape of your link stream or join column:@_component_uid |
The UUID of the component being derived. Use this when the link
stream on the source component holds target UUIDs (legacy form), or when joining against
the target's _component_uid column directly.
Example: SELECT customerUid, 'rate' FROM meter WHERE _component_uid = @_component_uid |
@_component_id |
The component id (human-readable PK) of the component being derived.
Use this for standard DB-style FK resolution where the link stream holds the target's PK
value (STRING or numeric).
Example: SELECT _component_uid, 'kwh' FROM meter WHERE customerId = @_component_id
— a fan-in from a customer to all meters pointing at that customer's id. |
Resolution Flow
Single-Target Resolution (Standard FK Dep)
When the SQL resolves to exactly one target component, the engine loads the value stream from that component and uses its data as the variable's value in the expression. This is the most common case — a meter resolving to its one customer, a sensor to its one production line.Multi-Target Resolution (Fan-In)
When the SQL resolves to multiple targets, the Fan Aggregator controls how they combine:| Fan Aggregator | Behavior |
NONE |
Validation error if more than one target resolves. This is the default — it enforces single-target resolution. |
SUM |
Sums all targets' values at each time point. Gaps (null values) are excluded from the sum. |
AVG |
Averages all targets' values at each time point. Only non-gap values participate. Average = SUM / count of non-gap values. |
MIN |
Takes the minimum value across all targets at each time point. |
MAX |
Takes the maximum value across all targets at each time point. |
CARDINALITYCOUNT |
Returns the total number of resolved target value streams. This is a constant value at every time point — it does not depend on whether targets have data or gaps. Use this as a denominator for coverage or per-unit calculations. |
NONNULLCARDINALITYCOUNT |
Returns the number of resolved target value streams that have non-null data at each time point (after fill-forward, if enabled). This varies per time point — targets with gaps contribute zero to the count. Use this for data quality and completeness calculations. |
Temporal Segmentation
When an FK relationship changes over time, the derivation engine splits the derivation range into segments — one per stable relationship period — and derives each segment independently.Single FK Dep
Consider a meter that was connected to Customer A from January through June, then moved to Customer B in July:- Loads the
customerUidlink stream's history to detect relationship changes - Finds the change point at July
- Creates two segments: Jan–Jun and Jul–Dec
- Derives each segment using the correct customer's value stream data
- Stitches the results into one continuous derived stream
Multiple FK Deps
When an expression has multiple FK variables (e.g., both a customer rate and a supplier margin), the engine intersects all variables' segment boundaries. It finds time ranges where all FK relationships are stable, then derives each unified segment with the correct targets for every variable.Example: A meter with two FK deps — one to a customer that changes in April, one to a supplier that changes in July:
| Period | Customer | Supplier | Segment |
| Jan – Mar | Customer A | Supplier 1 | Segment 1 |
| Apr – Jun | Customer B | Supplier 1 | Segment 2 |
| Jul – Dec | Customer B | Supplier 2 | Segment 3 |
Multi-Hop Chains
FK resolution supports chains of any depth. For example, meter → customer → supplier. At each hop, the engine loads the link stream's history to detect when that relationship changed. Change points at any level in the chain create segment boundaries. The value stream is always loaded from the final resolved component in the chain.Fan-In Aggregation
Fan-in is a reverse FK pattern. Instead of "which customer does this meter belong to?" (one-to-one), fan-in asks "which meters belong to this customer?" (one-to-many) and aggregates their data.How Fan-In Works
Fan-In with Fill Forward
Fill Forward is applied to each target stream individually before aggregation. This ensures that targets with sparse data (e.g., a meter that reports hourly while others report every 15 minutes) contribute their last known value to every time point, rather than creating gaps in the aggregation.Fan-In and Gap Handling
At each time point, only non-gap (non-null) values participate in the aggregation:- SUM — sums all non-null values
- AVG — sums non-null values and divides by the count of non-null values
- MIN / MAX — considers only non-null values
- If all targets are null at a time point, the aggregated result is null
Cardinality Counts
Two fan aggregator types return counts of resolved targets rather than aggregated values:- CARDINALITYCOUNT — the total number of resolved target value streams. This is a constant at every time point. For example, if 5 meters resolve to a customer, CARDINALITYCOUNT = 5 at every interval.
- NONNULLCARDINALITYCOUNT — the number of resolved target value streams that have non-null data at each time point (after fill-forward, if enabled). This varies per interval — if 2 of the 5 meters have gaps at a given interval, NONNULLCARDINALITYCOUNT = 3 at that interval.
Example — Data Coverage Calculation:
A customer has 5 meters reporting kWh every hour, with a derived stream that calculates daily data coverage. The expression uses three fan-in variables, all resolving the same SQL but with different settings:
| Variable | Cycle | Function | Fan Aggregator | Result |
|---|---|---|---|---|
nongap |
day | NONGAPCOUNT | SUM | Total non-gap hourly intervals across all meters (e.g., 108) |
total |
day | INTVLCOUNT | SUM | Total possible hourly intervals across all meters (5 × 24 = 120) |
meter_count |
CARDINALITYCOUNT | Number of meters (5) |
nongap / total * 100 → 90% coverage
Or:
nongap / (meter_count * 24) * 100 → same result, using cardinality as the denominator
Note that
nongap and total use the existing rollup function field (NONGAPCOUNT, INTVLCOUNT)
combined with a SUM fan aggregator to sum those counts across all targets. This is the same as using
NONGAPCOUNT and INTVLCOUNT on non-FK variables — the fan aggregator simply controls how the N targets' values combine.
Fan-in vs. Aggregation Derivation: Fan-in aggregates streams as a variable inside an expression, so the aggregated result can be combined with other variables in a formula. Aggregation Derivation is a separate derivation type that aggregates streams directly into a result stream with no expression. Use fan-in when you need the aggregated value in a calculation. Use Aggregation Derivation when you just need the aggregate itself.
Resolution SQL Patterns
The Resolution SQL field accepts TEQ (Temporal Entity Query) SELECT statements. The SQL follows a nested subquery pattern where each SELECT level resolves one hop in the FK chain.Standard Pattern (One-to-One)
| Hops | SQL Pattern |
| 1 | SELECT linkStreamId, 'valueStreamId' FROM template WHERE _component_uid = @_component_uid
linkStreamId = the link stream that resolves to the target component.
'valueStreamId' = the value stream on the target whose data is used in the expression. |
| 2 | SELECT linkStreamId2, 'valueStreamId' FROM template2 WHERE _component_uid = (SELECT linkStreamId1 FROM template1 WHERE _component_uid = @_component_uid)
Each nested SELECT follows a link stream. The outermost SELECT specifies the value stream. |
| 3+ | Continue nesting. The innermost SELECT (containing @_component_uid) starts from the current component. |
Fan-In Pattern (One-to-Many)
Fan-in uses the same SQL syntax, but the query naturally returns multiple rows. The key difference is that the FK relationship is reversed — the WHERE clause filters by a FK column instead ofcuid:
| Direction | SQL Pattern |
| Reverse 1-hop | SELECT _component_uid, 'targetStreamId' FROM childTemplate WHERE parentFkStreamId = @_component_uid
This finds all components of childTemplate whose parentFkStreamId points to the current component, then reads targetStreamId on each. |
| Reverse through parent | SELECT _component_uid, 'targetStreamId' FROM childTemplate WHERE parentFkStreamId = (SELECT parentFkStreamId FROM currentTemplate WHERE _component_uid = @_component_uid)
First resolves the current component's parent, then finds all children of that parent. |
SQL pattern requirements: The first column of each SELECT is the link stream ID
(or cuid for reverse lookups) — this is the relationship pointer the engine follows to find the target component.
The second column of the outermost SELECT (as a string literal in single quotes) is the value stream ID —
the stream on the resolved target component whose data will be used as the variable's value in the expression.
JOINs and other SQL constructs are not supported in the resolution SQL — use nested subqueries exclusively.
Fan Aggregator required: If a Resolution SQL returns more than one target and the Fan Aggregator is set to NONE, the engine throws a validation error. You must set Fan Aggregator to SUM, AVG, MIN, MAX, CARDINALITYCOUNT, or NONNULLCARDINALITYCOUNT for queries that can resolve to multiple targets.
Fill Forward with FK Deps
Fill Forward is enabled by default for FK-resolved variables. This is almost always what you want, because FK targets typically have data at different cadences than sibling streams.Example: A meter's
kwh stream reports every 15 minutes. The customer's energy_rate
changes once a quarter. Without Fill Forward, the rate would only contribute a value at the four timestamps
per year when it changes. With Fill Forward, the rate is carried forward to every 15-minute interval.
Fill Forward and Fan-In
For fan-in variables, Fill Forward is applied to each target stream individually before aggregation. This is important when targets report at different cadences:Fill Forward with Rollup Cycles
Fill Forward works with variables that have a rollup cycle and function. The fill-forward seed is the rolled-up value from the prior interval, not the raw last sample. For example, if a variable uses MAX over an hourly cycle, the seed is the MAX from the previous hour.Restrictions
Fill Forward is not available for:- Point stream variables — point streams have no time dimension
Limits and Constraints
The following limits protect derivation performance and prevent misconfigured streams from consuming excessive resources:| Limit | Value | Description |
| FK relationship changes per link stream | 500 | Maximum number of distinct relationship changes in a single link stream's history. Each change creates a temporal segment that loads and derives independently. Link streams are intended for infrequent changes (e.g., a meter reassigned to a different customer a few times per year). If this limit is exceeded, derivation throws an error suggesting a Regular (random) stream be used. |
| Variables per expression | 15 | Maximum number of dependent variables in a single derivation expression.
This includes both static and FK-resolved variables. Configurable via MAX_DERIVED_INTVL_DEPENDENTS. |
| Intervals in memory | 550,000 | Maximum number of intervals that can be held in memory during a single derivation.
For fan-in, this applies to the total across all resolved targets (N targets × intervals per target).
Configurable via MAX_DERIVATION_INTVLS. |
Interval streams as link streams: When an interval stream is used as a link stream, the engine walks backward through the history in chunks of 10,000 intervals to detect relationship changes, rather than loading the entire history. This prevents memory issues but is slower than using a Regular (random) stream, which only stores data points when the value actually changes. The 500-change limit applies to both stream types.
Throttling and scheduling for FK deps: FK-resolved dependencies — especially fan-in aggregations — can be expensive because they load multiple value streams and aggregate them at each time point. If derivation repeatedly takes longer than 30 seconds or throws errors, automatic derivation is throttled (paused) after 3 consecutive issues. Set up a Derivation Schedule on the template to run these streams at fixed intervals instead of on every data arrival. Scheduled streams continue to derive even when throttled. See the Throttling and Scheduling section for details.
Time Filters
Stream time filters are fully respected by FK-resolved dependencies. When a dependent variable has a time filter configured, the filter is applied to the loaded data before the variable enters the expression.For fan-in variables, the time filter applies to each target stream individually. Filtered-out intervals are treated as gaps (null). If Fill Forward is enabled, the fill-forward logic respects the filter — only non-filtered values are carried forward.
Configuration Examples
Each example below shows the variable grid (as configured in the derivation panel), the expression, and the Resolution SQL for each FK variable. The variable grid columns shown are those relevant to FK deps; hidden columns (Reconcile Lock, etc.) are omitted.
Two ways to configure FK deps:
UI: In the stream editor's derivation panel, show the hidden columns (Resolution SQL, Fan Aggregator, Fill Forward)
by right-clicking a column header. Add a variable, enter a name, set the Resolution SQL, and optionally set the Fan Aggregator.
The Component and Stream columns will show "(resolved by SQL)" for FK variables.
DDL: In TEQ DDL, use the VIA clause in DEPENDS ON. The engine generates the
Resolution SQL from the chain syntax. See the TEQ DDL reference.
Example 1: Energy Billing (Interval Stream, Single FK Dep)
Scenario: A utility company has meters and customers. Each meter has acustomerUid link stream
pointing to its assigned customer. The customer has an energy_rate stream (price per kWh) that changes
periodically. A derived stream on each meter calculates the billing amount: kwh * rate.
Templates:
meter— streams:kwh(interval, hourly),customerUid(link to customer),billed(derived)customer— streams:energy_rate(interval or rdm)
| Name | Component | Stream | Cycle | Function | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|---|---|
kwh |
(this component) | kwh | |||||
rate |
(resolved by SQL) | (resolved by SQL) | SELECT customerUid, 'energy_rate' FROM meter WHERE _component_uid = @_component_uid |
NONE | Yes |
kwh DOUBLE INTERVAL BASE_CYCLE 'hour',
customerUid STRING REFERENCES customer,
billed DOUBLE DERIVED AS 'kwh * rate'
DEPENDS ON (kwh FROM kwh, rate VIA customerUid.customer.energy_rate FILL_FORWARD)
)
What happens at derivation:
- The engine reads the
customerUidstream on this meter - If the value changed during the derivation range (meter moved to a different customer), the range is split into segments
- For each segment, the customer's
energy_rateis loaded and fill-forwarded - The expression
kwh * rateis evaluated at each interval using the correct rate
Example 2: Regulatory Compliance Threshold (RDM Stream, Multi-Hop FK)
Scenario: A pharmaceutical company tracks temperature in cold-storage rooms. Each room is assigned to a facility, and each facility is assigned to a regulatory region. The regulatory region has amax_temp_threshold that changes
when regulations are updated. A derived stream on each room calculates whether the room exceeds its regional threshold.
Templates:
cold_room— streams:temperature(rdm, samples at irregular intervals),facilityUid(link),temp_exceedance(derived)facility— streams:regionUid(link)region— streams:max_temp_threshold(rdm)
| Name | Component | Stream | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|
temp |
(this component) | temperature | |||
threshold |
(resolved by SQL) | (resolved by SQL) | SELECT regionUid, 'max_temp_threshold' FROM facility WHERE _component_uid = (SELECT facilityUid FROM cold_room WHERE _component_uid = @_component_uid) |
NONE | Yes |
temperature DOUBLE,
facilityUid STRING REFERENCES facility,
temp_exceedance DOUBLE DERIVED AS 'if(temp > threshold, temp - threshold, 0)'
DEPENDS ON (
temp FROM temperature,
threshold VIA facilityUid.facility.regionUid.region.max_temp_threshold FILL_FORWARD
)
ALLOW_NULLS
)
Why this works with RDM streams: The
temperature stream reports at irregular intervals
(whenever the sensor samples). The max_temp_threshold stream changes only when regulations update
(perhaps once a year). Fill Forward carries the threshold forward to every temperature sample timestamp.
If the room is reassigned to a different facility (or the facility to a different region), the engine
segments the derivation automatically.
Example 3: Customer Total Consumption (Interval Stream, Single Fan-In)
Scenario: A utility customer has multiple meters. The customer needs atotal_kwh derived stream
that sums the kwh values from all connected meters. When meters are added or removed, the total
automatically adjusts.
Templates:
meter— streams:kwh(interval, hourly),customerUid(link to customer)customer— streams:total_kwh(derived, hourly)
| Name | Component | Stream | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|
meter_kwh |
(resolved by SQL) | (resolved by SQL) | SELECT _component_uid, 'kwh' FROM meter WHERE customerUid = @_component_uid |
SUM | Yes |
customerUid
stream points to the current customer (@_component_uid), then read each meter's kwh stream."
If 5 meters are connected, the engine loads all 5 kwh streams, fill-forwards each, sums them
at each hourly interval, and puts the single summed value into the meter_kwh variable.
The expression is just meter_kwh — a pass-through. The fan-in aggregation happens before
the expression is evaluated. You could also combine the fan-in result with other variables:
meter_kwh * rate_adjustment.
Example 4: Portfolio Risk Score (RDM Stream, Multiple Fan-Ins)
Scenario: A financial services company manages portfolios of trading instruments. Each portfolio tracks a risk score derived from the maximum volatility and average return across all instruments in the portfolio. Instruments are assigned to portfolios via a link stream, and assignments can change (instruments rebalanced between portfolios).Templates:
instrument— streams:portfolioUid(link),volatility(rdm),daily_return(rdm)portfolio— streams:risk_score(derived)
| Name | Component | Stream | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|
max_vol |
(resolved by SQL) | (resolved by SQL) | SELECT _component_uid, 'volatility' FROM instrument WHERE portfolioUid = @_component_uid |
MAX | Yes |
avg_ret |
(resolved by SQL) | (resolved by SQL) | SELECT _component_uid, 'daily_return' FROM instrument WHERE portfolioUid = @_component_uid |
AVG | Yes |
- The
max_volvariable resolves to all instruments in this portfolio, loads each instrument'svolatilitystream, and takes the MAX across all instruments at each time point. - The
avg_retvariable resolves to the same instruments but loads theirdaily_returnstreams and computes the AVG. - Both aggregated values enter the expression as single variables. The expression computes the risk ratio.
- If an instrument is moved to a different portfolio (its
portfolioUidchanges), the next resolution cycle detects the change and adjusts which instruments are included.
Example 5: Fleet Efficiency with Supplier Benchmark (Mixed FK Deps)
Scenario: A logistics company manages a fleet of vehicles. Each vehicle tracks fuel consumption and is assigned to a depot. The depot is assigned to a fuel supplier that publishes a benchmark efficiency rate. A derived stream on each vehicle computes its efficiency relative to the supplier's benchmark.Templates:
vehicle— streams:fuel_consumption(interval, daily),distance(interval, daily),depotUid(link),efficiency_vs_benchmark(derived)depot— streams:supplierUid(link)fuel_supplier— streams:benchmark_rate(rdm, changes quarterly)
| Name | Component | Stream | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|
fuel |
(this component) | fuel_consumption | |||
dist |
(this component) | distance | |||
benchmark |
(resolved by SQL) | (resolved by SQL) | SELECT supplierUid, 'benchmark_rate' FROM depot WHERE _component_uid = (SELECT depotUid FROM vehicle WHERE _component_uid = @_component_uid) |
NONE | Yes |
Example 6: Building Zone Average Temperature (Interval Stream, Fan-In with Rollup)
Scenario: A smart building has temperature sensors assigned to zones. Each zone needs an average temperature derived from all sensors in that zone. The sensors report every minute, but the zone average should be computed at a 15-minute cycle.Templates:
temp_sensor— streams:temperature(interval, 1-minute),zoneUid(link)building_zone— streams:avg_temperature(derived, 15-minute base cycle)
| Name | Component | Stream | Cycle | Function | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|---|---|
temps |
(resolved by SQL) | (resolved by SQL) | 15min | AVG | SELECT _component_uid, 'temperature' FROM temp_sensor WHERE zoneUid = @_component_uid |
AVG | Yes |
- Resolution SQL finds all sensors in this zone (e.g., 8 sensors)
- Each sensor's 1-minute
temperaturedata is loaded - The Cycle/Function settings (15min / AVG) roll up each sensor's 1-minute data to 15-minute averages
- The Fan Aggregator (AVG) averages all 8 sensors' rolled-up values at each 15-minute interval
- The result is a single average temperature per 15-minute interval
Two levels of AVG: The Cycle/Function AVG rolls up each sensor from 1-minute to 15-minute. The Fan Aggregator AVG then averages across all sensors. These are independent operations. You could also use Cycle/Function = MAX with Fan Aggregator = AVG to get the average of each sensor's 15-minute peak temperature.
Example 7: Department Salary Budget (Interval Stream, Fan-In + Direct Dep)
Scenario: An HR system tracks employees and departments. Each employee has a monthly salary stream. Each department needs a derived stream showing total payroll as a percentage of the department's annual budget.Templates:
employee— streams:monthly_salary(interval, monthly),departmentUid(link)department— streams:annual_budget(rdm, updated annually),budget_utilization(derived, monthly)
| Name | Component | Stream | Resolution SQL | Fan Agg | Fill Fwd |
|---|---|---|---|---|---|
payroll |
(resolved by SQL) | (resolved by SQL) | SELECT _component_uid, 'monthly_salary' FROM employee WHERE departmentUid = @_component_uid |
SUM | Yes |
budget |
(this component) | annual_budget | Yes |
Derivation Triggers and Performance
Background Derivation Path
Derived streams with FK-resolved dependencies are handled by the background derivation job (not inline with data arrival). The job runs every 1–3 minutes and handles:- FK resolution (~every 10 minutes): Re-resolves each FK dependency's SQL, updates materialized precedent links, and stores the resolved source stream UIDs
- Derivation (on data arrival at resolved source): Once precedent links exist, data arrival at the source stream triggers derivation through the standard dirty-flag mechanism
Relationship Change Propagation
When a relationship changes (e.g., a meter'scustomerUid is updated to point to a different customer):
- The next FK resolution cycle (within ~10 minutes) detects the change
- The old precedent link is removed; a new one is created on the new source stream
- Subsequent data arrivals trigger derivation from the new source
Historical relationship changes: If you backdate a relationship change (append a value to the link stream with a historical timestamp), retroactive re-derivation is not immediate. The next precedent reconciliation cycle (within ~10 minutes) walks the full FK history, migrates stale precedent links, and flags the dependent stream for re-derivation from the earliest affected timestamp. The following derivation cycle then re-derives the historical range automatically. If you need an immediate refresh, use the Re-derive Stream action to trigger it without waiting for the reconciliation cycle.
Performance Characteristics
- FK history loading is fast — FK streams (link streams) typically have very few data points. A meter doesn't change customers often, so loading the full history of a link stream is inexpensive.
- Fan-in scales with target count — Loading N target streams is proportional to N. For large fan-in (hundreds of targets), consider using a Derivation Schedule rather than triggering on every data arrival.
- Temporal segmentation is additive — If an FK relationship has K change points, the engine derives K+1 segments. Each segment is independent, so the cost is roughly K+1 times a single derivation.
Unresolved FK Dependencies
Not every FK relationship is guaranteed to resolve at derivation time. In many real-world scenarios, the link stream may be empty or point to a component that doesn't have the expected value stream:- A pump that hasn't been connected to a tank yet
- An employee who hasn't been assigned a manager
- A meter installed before customer assignment is complete
- A vehicle temporarily unassigned from any route
Behavior
When an FK dependency cannot be resolved (no segments are produced by the Resolution SQL), the derivation engine does not abort. Instead, the unresolved variable is set to null and the expression proceeds with all other variables populated normally.How the null is handled depends on the Allow Nulls setting on the derived stream:
| Allow Nulls | Behavior When FK Dep is Unresolved |
| Enabled (recommended) | The variable is null in the expression.
The expression evaluator produces a gap (null output) for any interval
where the null variable participates in the calculation.
Other variables in the expression are still evaluated normally.
completedDate advances — the derivation is not stuck. |
| Disabled | The null causes the entire expression to produce a gap for those intervals. The result is the same (null output), but the intent is different — Allow Nulls = Disabled indicates that you expect all variables to be present and want explicit gap tracking when they are not. |
Tip: Enable Allow Nulls on derived streams with FK dependencies that may be intermittently unresolved.
This ensures completedDate advances even when a relationship hasn't been established yet.
Once the FK relationship is set (e.g., a pump is connected to a tank), subsequent derivations will resolve
normally and produce values.
Mixed Resolved and Unresolved
An expression can have a mix of FK dependencies where some resolve and others do not. Each dependency is resolved independently:rate and consumption resolve but tankLevel does not
(the pump isn't connected to a tank), the expression evaluates as:
rate, consumption) load data normally.
Only the unresolved variable (tankLevel) is null.
If you need the expression to produce a value even when some FK deps are unresolved, use conditional logic or restructure the expression to isolate the optional dependency into its own derived stream.
Note: When all FK dependencies are unresolved (no static or FK dep resolves), the derivation still
runs. All variables are null, the expression produces null output, and completedDate advances.
This prevents the stream from becoming permanently stuck waiting for a relationship that may not exist yet.
Copy, Folder Copy, and Reconcile
FK-resolved dependencies are portable by design. Because Resolution SQL uses@_component_uid (the current
component's UID), it resolves dynamically at derivation time.
Component Copy
When you copy a component with FK-resolved variables, the copy works correctly with no manual intervention. The Resolution SQL resolves relative to the new component's@_component_uid, so it finds the new
component's own FK relationships.
Folder Copy
FK-resolved variables are unaffected by folder copy. They resolve dynamically and do not store UIDs for external targets.Template Reconcile
Variables with Resolution SQL are automatically safe during reconcile. The template defines the SQL pattern, but each component resolves its own targets based on its own FK stream data. There is nothing component-specific to overwrite.See Expression Derivation and Change for full details.
UI Configuration Reference
The derivation panel's variable grid has several columns relevant to FK deps. Some are hidden by default; right-click a column header to show them.| Column | Default | Description |
| Resolution SQL | Hidden | The TEQ SELECT statement that resolves the target stream(s) at derivation time. When set, the Component and Stream columns show "(resolved by SQL)" and are read-only. Click the cell to open a text editor for the SQL. |
| Fan Aggregator | Hidden | Aggregation function for multi-target (fan-in) resolution.
NONE (default) requires exactly one target.
SUM, AVG, MIN, MAX aggregate across
multiple targets. Only applicable when Resolution SQL is set. |
| Fill Forward | Hidden | Carries the variable's last known value forward to timestamps where other variables have data. Enabled by default for FK-resolved variables. Not applicable to point streams. |
| Cycle | Visible | Optional rollup cycle for the resolved target stream. When set, the target stream's data is rolled up to this cycle before entering the expression. For fan-in, each target stream is rolled up individually before aggregation. |
| Function | Visible | Rollup aggregation method (SUM, AVG, MAX, MIN, LAST, FIRST, TWA). Used when Cycle is set to a non-base cycle. For fan-in, this rollup is applied to each target individually — it is separate from the Fan Aggregator which operates across targets. |
Don't confuse Cycle/Function with Fan Aggregator.
Cycle + Function = vertical aggregation (rolling up one stream's data from a finer cycle to a coarser cycle).
Applied per-stream, before fan-in.
Fan Aggregator = horizontal aggregation (combining N streams' values at the same time point into one).
Applied after rollups, across all resolved target streams.
