# 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 | ```sql -- 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 ```python # 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 ```sql -- 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 ```python 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 | ```sql -- 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 ```python # 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: ```python # 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 ```sql -- 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: ```python # 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 ```python # 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 ```python 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 ```python # 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 ```python 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 - [Schema evolution in Azure Databricks](https://learn.microsoft.com/en-us/azure/databricks/data-engineering/schema-evolution) -- Komplett guide til skjemaevolusjon - [What is Delta Lake?](https://learn.microsoft.com/en-us/azure/synapse-analytics/spark/apache-spark-what-is-delta-lake) -- Delta Lake features inkludert schema enforcement og evolution - [Delta Lake feature compatibility](https://learn.microsoft.com/en-us/azure/databricks/delta/feature-compatibility) -- Protokollversjoner og table features - [Schema enforcement](https://learn.microsoft.com/en-us/azure/databricks/tables/schema-enforcement) -- Skjemahondtering pa skrivetidspunktet - [Column mapping](https://learn.microsoft.com/en-us/azure/databricks/delta/column-mapping) -- Rename og drop av kolonner - [Type widening](https://learn.microsoft.com/en-us/azure/databricks/delta/type-widening) -- Automatisk type-utvidelse - [Update Delta Lake table schema](https://learn.microsoft.com/en-us/azure/databricks/delta/update-schema) -- DDL og mergeSchema --- ## 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.