Teradata · tdfs4ds Historical datasets 1 / 14

Historical datasets

build_historical_dataset returns how feature values evolved: one row per entity per period during which its values did not change. Every result on these slides was executed on Vantage 20, on a feature store built from the package's own demo notebooks.

02
03Snapshot or history

A snapshot holds one instant. A history holds every period.

build_dataset answers "what were the values at time t?". build_historical_dataset answers "how did they change?".

The daily category features of the demo store, rolled out over business dates, for Clothing:

ValidStartValidEndsummaxmeanmedian
2023-01-012023-05-114877.36298.05168.18144.81
2023-05-112023-05-124334.25299.90154.79133.42
2023-05-122023-05-134306.97280.23148.52137.25
2023-05-132023-05-146352.94294.98158.82147.62
2023-05-142023-05-155584.32299.44150.93162.76
2023-05-15now4948.17278.53154.63173.42

A snapshot at 2023-05-13 12:00 is the fourth row. The last period is still open in the store; it ends at the window's end, here the moment of the query.

04The call

Same arguments as build_dataset, plus a window

from tdfs4ds import build_historical_dataset
from tdfs4ds.feature_store.feature_query_retrieval import get_feature_versions

tdfs4ds.DATA_DOMAIN = "Customer Transaction Analytics Time Management"
selection = get_feature_versions(
    entity_name='Category',
    features=['sum_Transaction_Amount', 'mean_Transaction_Amount',
              'max_Transaction_Amount', 'median_Transaction_Amount'])

history = build_historical_dataset('Category', selection, 'HIST_CATEGORY')

Output columns, in order

entity
the grain, as in a snapshot (Category)
ValidPeriod
PERIOD(TIMESTAMP(0) WITH TIME ZONE), for CONTAINS, OVERLAPS, P_INTERSECT
ValidStart
included
ValidEnd
excluded
features
one column per selected feature
Still a view

Nothing is materialised. New ingestions show up at once, and a relative window moves by itself.

05The window

Fixed bounds, or relative to now

Half-open [start, end). now is the current valid time when the view is queried, so a window without a fixed end slides on its own.

ArgumentsWindowSlides
none[earliest, now)yes
delay[earliest, now − delay)yes
duration[now − duration, now)yes
duration + delay[now − delay − duration, now − delay)yes
start[start, now)end only
end[earliest, end)no
start + end[start, end)no
start + duration[start, start + duration)no
end + duration[end − duration, end)no

The end always exists: an open period ends at the window's end, never at 9999-12-31.

06Durations

Durations become Teradata date arithmetic

Accepted: '3 months', '90 days', '1 year 2 months', '2 days 6 hours', ISO 8601 'P1Y2M', (3, 'months'), a timedelta.

build_historical_dataset(..., duration='12 months', delay='1 day')

-- end and start, evaluated each time the view is queried
(CAST(CURRENT_TIMESTAMP(0) AS TIMESTAMP(0) WITH TIME ZONE) - INTERVAL '1 00:00:00' DAY TO SECOND)
ADD_MONTHS(<end>, -12)

Months use ADD_MONTHS

It clamps month-ends: 31 May minus 3 months gave 29 Feb (measured). - INTERVAL 'n' MONTH raises an error on such dates.

The rest uses DAY TO SECOND

Literal written without a precision: DAY(4) TO SECOND inside a literal is Error 3706 (measured). 9999 days is accepted.

Measured on real data: duration='40 years', delay='3 hours' ended the window three hours before the query and excluded a run written that morning.

07Join type

NORMALIZE merges equal neighbours. The join type decides which periods exist.

INNER

A period exists only where every feature has a value. An entity's history starts when its last feature appears.

FULL OUTER

A period exists where at least one feature has a value, with NULL elsewhere.

Real case: sum_Transaction_Amount under three processes. Two were rolled out over 2023 (FILTERED, WITH_CLONE), one ran in real time today (HYBRID). Restaurant:

ValidStartValidEndHYBRIDFILTEREDWITH_CLONEFULLINNER
2023-01-012023-05-21NULL1514.001514.00✓–
2023-05-212023-05-22NULL2086.891514.00✓–
…NULL…1514.00✓–
today 08:03:1308:03:461328.461872.261514.00✓✓
08:03:4608:03:471111.841872.261514.00✓✓

All six categories: FULL OUTER 112 rows, INNER 75. The rows both keep are identical.

08The invariant

At any instant, the history equals the snapshot

For every t in the window, the rows whose ValidPeriod contains t are exactly build_dataset(..., as_of=t) with the same join type.

SELECT ... FROM HIST_CUSTOMER WHERE ValidPeriod CONTAINS TIMESTAMP '2023-05-14 00:00:00'
-- compared with
build_dataset(..., as_of='2023-05-14 00:00:00', join_type=...)
InstantJoin typeSnapshot rowsHistory rowsIdentical
2023-05-12INNER / FULL OUTER538538yes / yes
2023-05-14INNER / FULL OUTER884884yes / yes
2023-05-15INNER / FULL OUTER1,0791,079yes / yes

Customer features of the demo store, compared value by value.

09INNER shape

INNER uses Teradata's own sequenced join

SEQUENCED VALIDTIME intersects the valid periods itself. The window is a predicate and a P_INTERSECT, then NORMALIZE, then the final columns.

SELECT z.Category, z.TDFS_VP AS ValidPeriod, BEGIN(z.TDFS_VP) AS ValidStart, ...
FROM ( SELECT NORMALIZE ON MEETS OR OVERLAPS ...
       FROM ( SELECT x.*, x.TDFS_VT P_INTERSECT PERIOD(<start>, <end>) AS TDFS_VP
              FROM ( SEQUENCED VALIDTIME
                     SELECT B1.Category, B1.FEATURE_VALUE AS sum_Transaction_Amount, ...
                     FROM FS_T_... B1 INNER JOIN FS_T_... B2 ON B1.Category = B2.Category ...
                     WHERE (B1.FEATURE_ID = 18 AND B1.FEATURE_VERSION = '...') ...
                   ) x (..., TDFS_VT)
              WHERE x.TDFS_VT OVERLAPS PERIOD(<start>, <end>) ) y ) z

What EXPLAIN shows (customer history, 4 features)

  • 9 steps; each storage table read once, from a single partition
  • rowkey-based merge joins, built locally, no redistribution
  • the sliding window folded into the join step as constants: TIMESTAMP '2026-10-06 07:50:41+00:00'
10FULL OUTER shape

FULL OUTER cuts each timeline at every change

A sequenced outer join is refused: Error 9347, in a derived table and at the top level.

clip each feature to the window→ change points→ elementary intervals→ LEFT JOIN each feature→ NORMALIZE

Change points first read each feature twice (starts, ends). One read now emits both bounds through a two-row expansion. Same rows, measured on the real category history:

Change pointsStorage readsRedistributionsRows
starts and ends read separately81036
one read, two-row expansion (shipped)3236

FULL OUTER stays about twice as slow as INNER: each table is still read twice.

11Normalization

Why NORMALIZE, not TD_NORMALIZE_OVERLAP_MEET

TD_NORMALIZE_OVERLAP_MEET was tested first. Fed by a derived table, the input became a second FROM item and was product-joined with the output:

SELECT n.* FROM (SELECT ... 3 rows ...) d,
     TABLE (TD_SYSFNLIB.TD_NORMALIZE_OVERLAP_MEET(NEW VARIANT_TYPE(d.e, d.v), d.p)
            RETURNS (e INTEGER, v INTEGER, p PERIOD(...), nrm_count INTEGER)
            HASH BY e LOCAL ORDER BY e, v, p) AS n
-- 3 rows in, 6 rows out: every result row duplicated

The function

  • correct only behind WITH
  • RETURNS must match input types exactly (Error 9134)
  • an extra redistribution and sort

SELECT NORMALIZE ON MEETS OR OVERLAPS

  • same rows on the same input
  • a plain SELECT, valid in a view
  • one aggregation step, no type list
12Ingestion behaviour

Three behaviours of the store that a history makes visible

1

Unchanged values do not split periods

A process re-run on unchanged data, in real time or at an explicit valid time, left the stored periods intact. On the demo store NORMALIZE had nothing to merge. It keeps "consecutive rows differ" true for data written some other way.

2

"Unchanged" means bit-identical

A re-run summed floats in another order: 16038.699999999999 became 16038.7, and the store opened a new period. The history shows two rows that print alike. Round such features in the process view.

3

Real-time runs stamp each feature separately

Without FEATURE_STORE_TIME, each feature is merged by its own statement: one run changed sum at 08:03:46 and max at 08:03:47. The history shows that second as a period. TimeManager and roll_out runs share one valid start.

13Catalog

Registered, rebuilt and migrated like any dataset

The catalog records TIME_MODE='HISTORY' and the window. The view's own DDL is read back, so nothing is re-resolved against the feature catalog.

# a snapshot rebuilt as a history
tdfs4ds.rebuild_dataset('DATASET_CUSTOMER', target_view_name='DATASET_CUSTOMER_HIST', history=True)
# a history's window moved
tdfs4ds.rebuild_dataset('HIST_CATEGORY', duration='6 months', replace=True)
# back to a snapshot
tdfs4ds.rebuild_dataset('HIST_CATEGORY', target_view_name='CATEGORY_NOW', current=True)
restore_dataset_catalog migrate_objects lineage recorded no grouped strategy yet one DATA_DOMAIN per dataset
14Verification

How it was verified

The demo notebooks 00 and 02–18 were run against a fresh demo_user on Vantage 20, producing three data domains with rolled-out, filtered and real-time history. Every view was compared with a reference computed in Python from the raw storage rows, value by value.

CaseJoinRowsStepsSingle-partition readsElapsed (warm)Reference
Customer, 4 featuresINNER1,079940.13–0.17 sidentical
Customer, 4 featuresFULL OUTER1,0792280.22–0.25 sidentical
Category, 4 featuresINNER39840.07 sidentical
Category, 4 featuresFULL OUTER392050.09–0.11 sidentical
Category, 2 × 3 versionsINNER75960.07 sidentical
Category, 2 × 3 versionsFULL OUTER1122490.14–0.28 sidentical

Four more windows (fixed, start + duration, end − duration, sliding with delay) × both join types: 8 of 8 identical. Offline: 114 tests for the window, both shapes and the DDL round trip.

Small data: read the plan columns, not the times, as the scaling indicator.