ktg-plugin-marketplace/plugins/ms-ai-architect/skills/ms-ai-engineering/references/data-engineering/schema-evolution-management.md
Kjell Tore Guttormsen 6a7632146e feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
Initial addition of ms-ai-architect plugin to the open-source marketplace.
Private content excluded: orchestrator/ (Linear tooling), docs/utredning/
(client investigation), generated test reports and PDF export script.
skill-gen tooling moved from orchestrator/ to scripts/skill-gen/.

Security scan: WARNING (risk 20/100) — no secrets, no injection found.
False positive fixed: added gitleaks:allow to Python variable reference
in output-validation-grounding-verification.md line 109.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:17:17 +02:00

18 KiB

Schema Evolution and Management

Last updated: 2026-02 Status: GA Category: Data Engineering for AI


Introduksjon

Skjemaendringer er uunngaaelige i moderne dataarkitekturer: nye kolonner legges til, datatyper endres, kolonner gis nye navn, og foreldede felt fjernes. For AI-pipelines er dette spesielt utfordrende fordi ML-modeller er trent pa spesifikke feature-skjemaer, og enhver skjemaendring kan bryte trenings- og inferens-pipelines. Delta Lake i Microsoft Fabric og Azure Databricks tilbyr robust stotte for skjemaevolusjon som gjor det mulig a haandtere disse endringene uten nedetid.

Schema enforcement (skjemahindring) sikrer at data som skrives til en tabell matcher forventet skjema, mens schema evolution (skjemaevolusjon) lar tabellskjemaet tilpasse seg nye datastrukturer automatisk. Kombinasjonen av disse to mekanismene gir en kontrollert tilnaerming der daarlig data avvises mens legitime strukturendringer aksepteres.

For norsk offentlig sektor, der datakvalitet og sporbarhet er lovpalagt, er det kritisk a ha en systematisk tilnaerming til skjemahondtering. Delta Lake sin transaksjonslogg gir full audit trail over alle skjemaendringer, noe som stotter krav i Forvaltningsloven og Arkivlova.


Schema Versioning and Compatibility Levels

Skjemaevolusjon i Delta Lake

Delta Lake stotter folgende typer skjemaendringer:

Endringstype Schema Enforcement Schema Evolution Kommentar
Ny kolonne Blokkerer skriving Legger til automatisk Vanligste endring
Kolonnenavn-endring N/A Via column mapping Krever DDL
Slettet kolonne N/A Via column mapping Krever DDL
Type-utvidelse Blokkerer skriving Type widening INT -> BIGINT
Type-endring Blokkerer skriving overwriteSchema Destruktiv

Kompatibilitetsnivaaer

+---------------------------------------------------+
| BACKWARD COMPATIBLE (trygt)                       |
|   - Legge til nye nullable-kolonner               |
|   - Utvide datatyper (INT -> BIGINT -> DOUBLE)   |
|                                                   |
| FORWARD COMPATIBLE (krever koordinering)          |
|   - Gi nytt navn til kolonner                     |
|   - Fjerne kolonner                               |
|                                                   |
| BREAKING CHANGES (krever migrasjon)               |
|   - Endre datatype (STRING -> INT)               |
|   - Endre nullability (nullable -> not null)      |
|   - Omstrukturere nesting                         |
+---------------------------------------------------+

Delta Lake Protocol Versions

Delta Lake bruker protokollversjoner for a kontrollere funksjonskompatibilitet:

Feature minReaderVersion minWriterVersion Beskrivelse
Column Mapping 2 5 Kolonnenavn-endring og sletting
Type Widening 3 7 Automatisk type-utvidelse
Table Features 3 7 Granular feature-kontroll
Liquid Clustering 2 7 Dynamisk clustering
-- Sjekk gjeldende protokollversjoner
DESCRIBE DETAIL lakehouse.default.ml_features;

-- Oppgrader protokoll for a stotte column mapping
ALTER TABLE lakehouse.default.ml_features
SET TBLPROPERTIES (
    'delta.minReaderVersion' = '2',
    'delta.minWriterVersion' = '5',
    'delta.columnMapping.mode' = 'name'
);

Adding Columns with Default Values

Automatisk skjemaevolusjon ved skriving

# Aktiver schema evolution for en skriveoperasjon
df_with_new_column = df.withColumn("weather_score", F.lit(0.0))

# Med mergeSchema: Legger til ny kolonne automatisk
df_with_new_column.write \
    .format("delta") \
    .option("mergeSchema", "true") \
    .mode("append") \
    .saveAsTable("lakehouse.default.ml_features")

Legge til kolonner via DDL

-- Legg til ny kolonne med kommentar
ALTER TABLE lakehouse.default.ml_features
ADD COLUMN weather_score DOUBLE
COMMENT 'Vaerscore 0-1 for prediksjonskvalitet';

-- Legg til flere kolonner samtidig
ALTER TABLE lakehouse.default.ml_features
ADD COLUMNS (
    model_version STRING COMMENT 'Versjon av ML-modellen',
    confidence_score DOUBLE COMMENT 'Konfidensintervall 0-1',
    processing_timestamp TIMESTAMP COMMENT 'Tidspunkt for prosessering'
);

-- Legg til kolonne med generert verdi
ALTER TABLE lakehouse.default.ml_features
ADD COLUMN year_month STRING
GENERATED ALWAYS AS (DATE_FORMAT(created_date, 'yyyy-MM'));

Backfill av nye kolonner

from delta.tables import DeltaTable

def backfill_column(table_name, column_name, default_value=None, compute_func=None):
    """
    Fyll ny kolonne med verdier for eksisterende rader.

    Args:
        table_name: Tabellnavn
        column_name: Kolonnenavn
        default_value: Statisk standardverdi
        compute_func: Funksjon for a beregne verdi basert pa andre kolonner
    """
    delta_table = DeltaTable.forName(spark, table_name)

    if default_value is not None:
        delta_table.update(
            condition=F.col(column_name).isNull(),
            set={column_name: F.lit(default_value)}
        )
    elif compute_func is not None:
        delta_table.update(
            condition=F.col(column_name).isNull(),
            set={column_name: compute_func}
        )

# Eksempler
# Statisk standardverdi
backfill_column("lakehouse.default.ml_features", "weather_score", default_value=0.5)

# Beregnet verdi basert pa andre kolonner
backfill_column(
    "lakehouse.default.ml_features",
    "confidence_score",
    compute_func=F.when(F.col("prediction_count") > 100, 0.9).otherwise(0.5)
)

Type Promotions and Narrowing

Stottede type-utvidelser (Type Widening)

Delta Lake stotter folgende trygge type-utvidelser:

Fra Til Automatisk Kommentar
BYTE SHORT Ja Uten datatap
SHORT INT Ja Uten datatap
INT LONG Ja Uten datatap
LONG DECIMAL Betinget Desimalbredde ma vaere tilstrekkelig
FLOAT DOUBLE Ja Uten datatap
DATE TIMESTAMP Ja Legger til tid 00:00:00
DECIMAL(p,s) DECIMAL(p',s') Ja Hvis p'>=p og s'>=s
-- Aktiver type widening pa tabellen
ALTER TABLE lakehouse.default.ml_features
SET TBLPROPERTIES ('delta.enableTypeWidening' = 'true');

-- Na kan du skrive data med bredere typer
-- F.eks. INT-kolonner aksepterer LONG-verdier automatisk

Type-utvidelse med Schema Evolution

# Automatisk type-utvidelse under merge
spark.conf.set("spark.databricks.delta.schema.autoMerge.enabled", "true")

# Na vil en DataFrame med LONG-verdi for en INT-kolonne
# automatisk utvide kolonnetypen
df_new = spark.createDataFrame([
    (1, 3000000000, "test")  # 3 milliarder overskrider INT
], ["id", "large_count", "name"])

# Merge med schema evolution og type widening
delta_table = DeltaTable.forName(spark, "lakehouse.default.counts")
delta_table.alias("target").merge(
    df_new.alias("source"),
    "target.id = source.id"
).whenMatchedUpdateAll() \
 .whenNotMatchedInsertAll() \
 .execute()

Type-innsnevring (farlig)

Type-innsnevring (f.eks. LONG -> INT) kan fore til datatap og krever full overskriving:

# ADVARSEL: Dette overskriver hele tabellskjemaet
df_narrowed = spark.table("lakehouse.default.legacy_table") \
    .withColumn("count_col", F.col("count_col").cast("int"))

df_narrowed.write \
    .format("delta") \
    .option("overwriteSchema", "true") \
    .mode("overwrite") \
    .saveAsTable("lakehouse.default.legacy_table")

Deprecated Column Handling

Column Mapping for sikker kolonnefjerning

-- Aktiver column mapping (kreves for rename/drop)
ALTER TABLE lakehouse.default.ml_features
SET TBLPROPERTIES (
    'delta.columnMapping.mode' = 'name'
);

-- Gi nytt navn til en kolonne
ALTER TABLE lakehouse.default.ml_features
RENAME COLUMN old_feature_name TO new_feature_name;

-- Slett en kolonne (logisk, ingen data-omskriving)
ALTER TABLE lakehouse.default.ml_features
DROP COLUMN deprecated_feature;

-- Slett flere kolonner
ALTER TABLE lakehouse.default.ml_features
DROP COLUMNS (temp_col1, temp_col2, debug_flag);

Soft Deprecation-moenster

For gradvis utfasing av kolonner i AI-pipelines:

# Trinn 1: Merk kolonne som deprecated via kommentar
spark.sql("""
    ALTER TABLE lakehouse.default.ml_features
    ALTER COLUMN old_score COMMENT 'DEPRECATED: Bruk new_score i stedet. Fjernes 2026-06-01.'
""")

# Trinn 2: Legg til ny kolonne med forbedret logikk
spark.sql("""
    ALTER TABLE lakehouse.default.ml_features
    ADD COLUMN new_score DOUBLE COMMENT 'Erstatter old_score med forbedret beregning'
""")

# Trinn 3: Backfill ny kolonne
delta_table = DeltaTable.forName(spark, "lakehouse.default.ml_features")
delta_table.update(
    set={"new_score": F.col("old_score") * 1.1}  # Eksempel: justert beregning
)

# Trinn 4: Oppdater downstream-pipelines til a bruke new_score
# (Gjoeres over tid, ikke alt pa en gang)

# Trinn 5: Etter overgangsperiode - fjern gammel kolonne
# spark.sql("ALTER TABLE lakehouse.default.ml_features DROP COLUMN old_score")

Kolonneregistrering for ML Feature Store

# Hold styr pa hvilke kolonner som er aktive, deprecated, eller fjernet
feature_registry = {
    "ml_features": {
        "active": [
            {"name": "traffic_volume", "type": "DOUBLE", "since": "2025-01"},
            {"name": "weather_score", "type": "DOUBLE", "since": "2025-06"},
            {"name": "road_condition_index", "type": "DOUBLE", "since": "2025-03"},
            {"name": "new_score", "type": "DOUBLE", "since": "2026-01"}
        ],
        "deprecated": [
            {"name": "old_score", "type": "DOUBLE", "since": "2025-01",
             "deprecated_date": "2026-01", "removal_date": "2026-06",
             "replacement": "new_score"}
        ],
        "removed": [
            {"name": "temp_debug_col", "type": "STRING",
             "removed_date": "2025-12", "reason": "Debug-kolonne, ikke lenger noedvendig"}
        ]
    }
}

Schema Registration and Validation

Skjemavalidering i pipelines

from pyspark.sql.types import StructType, StructField, StringType, DoubleType, TimestampType, LongType

def validate_schema(df, expected_schema: StructType, strict: bool = False):
    """
    Valider at en DataFrame matcher forventet skjema.

    Args:
        df: DataFrame a validere
        expected_schema: Forventet StructType
        strict: Hvis True, avvis ekstra kolonner. Hvis False, tillat ekstra.
    """
    actual_fields = {f.name: f for f in df.schema.fields}
    expected_fields = {f.name: f for f in expected_schema.fields}

    errors = []

    # Sjekk at alle forventede kolonner finnes
    for name, expected_field in expected_fields.items():
        if name not in actual_fields:
            errors.append(f"Mangler kolonne: {name} ({expected_field.dataType})")
        else:
            actual_field = actual_fields[name]
            # Sjekk datatype
            if actual_field.dataType != expected_field.dataType:
                errors.append(
                    f"Type-mismatch for '{name}': "
                    f"forventet {expected_field.dataType}, fikk {actual_field.dataType}"
                )
            # Sjekk nullability
            if not expected_field.nullable and actual_field.nullable:
                errors.append(
                    f"Nullability-mismatch for '{name}': "
                    f"forventet NOT NULL, fikk NULLABLE"
                )

    # Sjekk for uventede kolonner
    if strict:
        extra_cols = set(actual_fields.keys()) - set(expected_fields.keys())
        if extra_cols:
            errors.append(f"Uventede kolonner: {extra_cols}")

    return {
        "valid": len(errors) == 0,
        "errors": errors,
        "actual_columns": len(actual_fields),
        "expected_columns": len(expected_fields)
    }

# Definer forventet skjema for ML features
expected_feature_schema = StructType([
    StructField("entity_id", StringType(), nullable=False),
    StructField("feature_timestamp", TimestampType(), nullable=False),
    StructField("traffic_volume", DoubleType(), nullable=True),
    StructField("weather_score", DoubleType(), nullable=True),
    StructField("road_condition_index", DoubleType(), nullable=True),
    StructField("prediction_target", DoubleType(), nullable=False)
])

# Valider incoming data
result = validate_schema(incoming_df, expected_feature_schema, strict=False)
if not result["valid"]:
    raise ValueError(f"Skjemavalidering feilet: {result['errors']}")

Schema evolution i Structured Streaming

# Auto Loader med skjemaevolusjon
df_stream = spark.readStream \
    .format("cloudFiles") \
    .option("cloudFiles.format", "json") \
    .option("cloudFiles.schemaLocation", "/checkpoints/schema/") \
    .option("cloudFiles.schemaEvolutionMode", "addNewColumns") \
    .option("cloudFiles.schemaHints", "event_id STRING, timestamp TIMESTAMP") \
    .load("/landing/events/")

# Skriv med schema evolution aktivert
df_stream.writeStream \
    .format("delta") \
    .option("checkpointLocation", "/checkpoints/events/") \
    .option("mergeSchema", "true") \
    .outputMode("append") \
    .toTable("lakehouse.default.events")

Schema evolution per komponent-oversikt

Komponent Nye kolonner Rename Drop Type-utvidelse
Auto Loader Ja (restart) Ja (restart) Ja (soft delete) Nei
Delta Connector Ja (mergeSchema) Ja (column mapping) Ja (column mapping) Ja (type widening)
Streaming Tables Ja (auto) Ja (auto) Ja (soft delete) Ja (type widening)
Materialized Views Full recompute Full recompute Full recompute Full recompute
Delta Tables Ja (auto/DDL) Ja (DDL) Ja (DDL) Ja (auto/DDL)

Skjemamigrasjon for ML-modeller

class SchemaVersionManager:
    """
    Holder styr pa skjemaversjoner og sikrer at ML-modeller
    bruker kompatible skjemaer.
    """

    def __init__(self, registry_table="lakehouse.default.schema_registry"):
        self.registry_table = registry_table

    def register_schema(self, table_name: str, version: str, schema: StructType):
        """Registrer en ny skjemaversjon."""
        schema_json = schema.json()
        spark.sql(f"""
            INSERT INTO {self.registry_table}
            VALUES ('{table_name}', '{version}', '{schema_json}',
                    current_timestamp(), true)
        """)

    def get_schema(self, table_name: str, version: str = None) -> StructType:
        """Hent skjema for en spesifikk versjon (eller siste)."""
        if version:
            row = spark.sql(f"""
                SELECT schema_json FROM {self.registry_table}
                WHERE table_name = '{table_name}' AND version = '{version}'
            """).first()
        else:
            row = spark.sql(f"""
                SELECT schema_json FROM {self.registry_table}
                WHERE table_name = '{table_name}' AND is_current = true
            """).first()

        return StructType.fromJson(json.loads(row.schema_json))

    def check_compatibility(self, table_name: str, new_schema: StructType) -> dict:
        """Sjekk om nytt skjema er bakoverkompatibelt."""
        current = self.get_schema(table_name)
        current_fields = {f.name: f for f in current.fields}
        new_fields = {f.name: f for f in new_schema.fields}

        added = set(new_fields.keys()) - set(current_fields.keys())
        removed = set(current_fields.keys()) - set(new_fields.keys())

        is_backward_compatible = len(removed) == 0

        return {
            "backward_compatible": is_backward_compatible,
            "added_columns": list(added),
            "removed_columns": list(removed),
            "recommendation": "SAFE" if is_backward_compatible else "BREAKING - koordiner med downstream"
        }

Referanser


For Cosmo

  • Bruk denne referansen naar kunder haandterer skjemaendringer i Delta Lake-tabeller, eller naar de trenger strategier for skjemaversjonering i ML-pipelines.
  • Schema enforcement + evolution er komplementaere: Enforcement hindrer daarlig data, evolution lar skjemaet vokse. Aktiver begge for AI-datatabeller.
  • Column mapping er pabudt for rename/drop-operasjoner. Aktiver det tidlig pa tabeller som vil utvikle seg over tid.
  • Type widening er trygt for analytics: INT -> BIGINT og FLOAT -> DOUBLE er trygge operasjoner. Type-innsnevring bor aldri gjores automatisk.
  • For norsk offentlig sektor: Fremhev at Delta Lake sin transaksjonslogg gir full sporbarhet over alle skjemaendringer, noe som stotter Arkivlovas krav til dokumentasjon av dataendringer.