Jaco van der Laan

Implementing Supertypes & Subtypes in a Modern Data Platform

14/09/2025  ·  Data Modeling

article base/from-logical-to-physical-data-models content/code lineage metadata
‹ ›

Why modeling patterns still matter for performance and clarity.

Summary

In Part 1, we saw why consolidating supertypes and subtypes in the PDM makes sense: fewer tables, clearer lineage, and historization at the right granularity. In this part, we’ll get practical and see how to load IDS subtypes into one supertype table — using metadata-driven patterns.

Why Metadata-Driven Loading?

Hard-coding pipelines for every subtype doesn’t scale. Business models evolve, new systems get integrated, and suddenly you’re maintaining dozens of nearly identical loaders.

The better way:

  • Export metadata from your modeling tool (Erwin, PowerDesigner, …).
  • Use it to drive the loading logic automatically.
  • Discriminator values (PERSON, ORG, …) can come either from the IDS table itself or from metadata.

This keeps pipelines lean, consistent, and adaptable.

Case 1 — Discriminator Comes from IDS

Sometimes the source table already tells you what subtype a record belongs to.

INSERT INTO party_core (
  party_sk, party_bk, party_type, name_full, country_code,
  person_birthdate, org_legal_name, valid_from_ts, valid_to_ts, is_current
)
SELECT
  NEXTVAL('seq_party_sk') AS party_sk,
  s.party_bk,
  s.party_type,  -- comes directly from IDS column
  s.full_name    AS name_full,
  s.country      AS country_code,
  s.birth_dt     AS person_birthdate,
  NULL           AS org_legal_name,
  s.valid_from,
  s.valid_to,
  s.is_current
FROM ids_party_person s
WHERE s.party_type = 'PERSON';

Case 2 — Discriminator from Metadata

Other times, the IDS table doesn’t carry a subtype marker. In that case, your interface model injects the value.

INSERT INTO party_core (
  party_sk, party_bk, party_type, name_full, country_code,
  person_birthdate, org_legal_name, valid_from_ts, valid_to_ts, is_current
)
SELECT
  NEXTVAL('seq_party_sk'),
  s.party_bk,
  'PERSON'                AS party_type,   -- injected by metadata
  s.full_name             AS name_full,
  s.country               AS country_code,
  s.birth_dt              AS person_birthdate,
  NULL                    AS org_legal_name,
  s.valid_from,
  s.valid_to,
  s.is_current
FROM ids_party_person s;

For the ORGANIZATION feed, you’d repeat with 'ORG' and its specific attributes (like org_legal_name).

UNION ALL Loader

In some platforms (Snowflake, dbt, Spark SQL), you may prefer a single INSERT ... SELECT that combines all subtype feeds with UNION ALL:

INSERT INTO party_core (party_sk, party_bk, party_type, name_full, country_code,
                        person_birthdate, org_legal_name, valid_from_ts, valid_to_ts, is_current)
SELECT
  NEXTVAL('seq_party_sk'), party_bk, 'PERSON', name_full, country_code,
  person_birthdate, NULL, valid_from_ts, valid_to_ts, is_current
FROM ids_party_person
UNION ALL
SELECT
  NEXTVAL('seq_party_sk'), party_bk, 'ORG', name_full, country_code,
  NULL, org_legal_name, valid_from_ts, valid_to_ts, is_current
FROM ids_party_org;

Metadata-Driven Code Generation

The real power comes from letting metadata generate the loaders for you.

Here’s a minimal Python example that builds INSERT ... SELECT statements from a JSON export of your model:

subtype_mappings = [
    {
        "source_table": "ids_party_person",
        "discriminator_value": "PERSON",
        "column_map": {
            "name_full": "full_name",
            "country_code": "country",
            "person_birthdate": "birth_dt",
            "valid_from_ts": "valid_from",
            "valid_to_ts": "valid_to",
            "is_current": "is_current"
        }
    },
    {
        "source_table": "ids_party_org",
        "discriminator_value": "ORG",
        "column_map": {
            "name_full": "display_name",
            "country_code": "country",
            "org_legal_name": "legal_name",
            "valid_from_ts": "valid_from",
            "valid_to_ts": "valid_to",
            "is_current": "is_current"
        }
    }
]

TARGET_COLS = [
    "party_sk", "party_bk", "party_type", "name_full", "country_code",
    "person_birthdate", "org_legal_name", "valid_from_ts", "valid_to_ts", "is_current"
]

def build_insert(mapping):
    selects = [
        "NEXTVAL('seq_party_sk') AS party_sk",
        "party_bk",
        f"'{mapping['discriminator_value']}' AS party_type"
    ]
    for col in TARGET_COLS[3:]:
        src = mapping["column_map"].get(col)
        selects.append(f"{src} AS {col}" if src else f"NULL AS {col}")
    select_sql = ",\n  ".join(selects)
    return f"""INSERT INTO party_core ({", ".join(TARGET_COLS)})
SELECT
  {select_sql}
FROM {mapping['source_table']};"""

for m in subtype_mappings:
    print(build_insert(m))

This script generates the exact SQL inserts needed for each subtype feed. Add a new subtype? Just extend the metadata — no pipeline code changes.

Historization

Once subtype rows land in party_core, apply your standard SCD2 historization (via MERGE or a dedicated historization layer). The important part is:

  • Each record already carries its discriminator.
  • Timelines stay consistent across subtypes.

Wrapping Up

With this approach you:

  • Unify subtypes into one clean supertype table.
  • Load automatically using metadata from your model.
  • Keep pipelines future-proof — new subtypes are just metadata entries, not new pipeline code.

Combined with the concepts from Part 1, this gives you both the why and the how of building a business-shaped integration layer.