Skill 54 · Ingesting Into Data Lake
Subchapter 54.21
references/schema-evolution.mdMarkdown9 KBView on GitHub
This document describes expected approaches for handling schema evolution and nested JSON/struct data during imports.
Schema evolution occurs when source data has columns that don’t exist in the target table. This is common when:
| Change Type | Example | Handling |
|---|---|---|
| New columns | Source has phone_number, table doesn’t | ALTER TABLE ADD COLUMNS |
| Missing columns | Table has country, source doesn’t | Use NULL or default value |
| Type changes | Source price is STRING, was INT | Type conflict resolution (see type-transformations.md) |
| Column rename | Source has customer_name, table has name | Manual mapping or user decision |
# Get current table schema from Glue Catalog
import boto3
glue = boto3.client('glue')
response = glue.get_table(
DatabaseName='my_database',
Name='my_table'
)
existing_columns = {col['Name']: col['Type'] for col in response['Table']['StorageDescriptor']['Columns']}
# Compare with source schema
source_columns = {'customer_id': 'int', 'name': 'string', 'email': 'string', 'phone': 'string'} # Inferred
new_columns = set(source_columns.keys()) - set(existing_columns.keys())
missing_columns = set(existing_columns.keys()) - set(source_columns.keys())Expected output to user:
Schema Comparison:
Existing table columns: customer_id, name, email
Source data columns: customer_id, name, email, phone
New columns in source (will be added): phone
Missing columns in source (will be NULL): None
Schema evolution will automatically add new columns to the table.With AWS CLI:
aws athena start-query-execution \
--query-string "ALTER TABLE \"catalog\".\"namespace\".\"table\" ADD COLUMNS (phone STRING)" \
--query-execution-context Database=namespace \
--result-configuration OutputLocation=s3://bucket/results/ \
--region us-east-1If source is missing columns that exist in the target table, two approaches:
Option 1: Use NULL for missing columns (recommended) — New rows will have NULL in these columns. Existing rows keep their values.
Option 2: Fail the import — Ensures data completeness. Requires source to have all columns.
When source data has nested structures:
{
"order_id": 12345,
"customer": {
"customer_id": 789,
"name": "John Doe",
"email": "john@example.com"
},
"items": [
{"product_id": 456, "quantity": 2, "price": 29.99}
]
}PySpark - Flatten Struct:
from pyspark.sql.functions import col
flattened_df = source_df.select(
col("order_id"),
col("customer.customer_id").alias("customer_id"),
col("customer.name").alias("customer_name"),
col("customer.email").alias("customer_email"),
col("order_date"),
col("total")
)PySpark - Explode Array:
from pyspark.sql.functions import explode, col
# One row per item
exploded_df = source_df.select(
col("order_id"),
col("customer.customer_id").alias("customer_id"),
explode(col("items")).alias("item")
).select(
"order_id",
"customer_id",
col("item.product_id"),
col("item.quantity"),
col("item.price")
)Athena SQL - Flatten with UNNEST:
-- Create external table with nested types
CREATE EXTERNAL TABLE orders_nested (
order_id BIGINT,
customer STRUCT<customer_id: BIGINT, name: STRING, email: STRING>,
items ARRAY<STRUCT<product_id: BIGINT, quantity: INT, price: DECIMAL(10,2)>>,
order_date DATE,
total DECIMAL(10,2)
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://bucket/orders/';
-- Flatten and insert
INSERT INTO "catalog"."namespace"."orders_flat"
SELECT
order_id,
customer.customer_id,
customer.name AS customer_name,
customer.email AS customer_email,
item.product_id,
item.quantity,
item.price,
order_date
FROM orders_nested
CROSS JOIN UNNEST(items) AS t(item);S3 Tables DDL with Nested Types:
CREATE TABLE "catalog"."namespace"."orders_nested" (
order_id BIGINT,
customer STRUCT<
customer_id: BIGINT,
name: STRING,
email: STRING
>,
items ARRAY<STRUCT<
product_id: BIGINT,
quantity: INT,
price: DECIMAL(10,2)
>>,
order_date DATE,
total DECIMAL(10,2)
)
USING ICEBERGQuerying Nested Data:
-- Access struct fields
SELECT
order_id,
customer.name,
customer.email,
order_date
FROM "catalog"."namespace"."orders_nested"
WHERE customer.customer_id = 789;
-- Explode array in queries
SELECT
order_id,
item.product_id,
item.quantity,
item.price
FROM "catalog"."namespace"."orders_nested"
CROSS JOIN UNNEST(items) AS t(item);PySpark - Write with Nested Types:
# Preserve nested structure
source_df.writeTo(args['target_table']).append()
# No flattening needed - PySpark DataFrame schema maps directly to IcebergImplementation examples for each array handling approach:
Store as ARRAY<STRUCT<...>> in S3 Table. Query with UNNEST when needed. Preserves one-to-many relationships efficiently.
Each array element becomes its own row. Simple flat table structure. May create many duplicate rows if arrays are large.
Store items in separate table (e.g., order_items). Link via foreign key. Normalized database design.
Before (existing table):
CREATE TABLE customers (
customer_id INT,
name STRING,
email STRING
)New Source Data adds columns: phone STRING, address STRING
After Evolution:
ALTER TABLE customers ADD COLUMNS (
phone STRING,
address STRING
);Result:
customer_id=1, name="Alice", email="alice@example.com", phone=NULL, address=NULLcustomer_id=2, name="Bob", email="bob@example.com", phone="555-1234", address="123 Main St"Source JSON:
{
"user_id": 100,
"profile": {
"age": 30,
"city": "Seattle"
},
"purchases": [
{"item": "book", "amount": 20},
{"item": "laptop", "amount": 1200}
]
}Flattened Table:
user_id | age | city | item | amount
--------|-----|---------|--------|-------
100 | 30 | Seattle | book | 20
100 | 30 | Seattle | laptop | 1200PySpark Code:
from pyspark.sql.functions import explode, col
df = spark.read.json("s3://bucket/data.json")
flattened = df.select(
col("user_id"),
col("profile.age"),
col("profile.city"),
explode(col("purchases")).alias("purchase")
).select(
"user_id",
"age",
"city",
col("purchase.item"),
col("purchase.amount")
)Same Source, but preserved as nested:
Table Schema:
CREATE TABLE user_purchases (
user_id BIGINT,
profile STRUCT<age: INT, city: STRING>,
purchases ARRAY<STRUCT<item: STRING, amount: DECIMAL(10,2)>>
)Query Example:
-- Get users from Seattle who bought laptops
SELECT
user_id,
profile.age,
purchase.item,
purchase.amount
FROM user_purchases
CROSS JOIN UNNEST(purchases) AS t(purchase)
WHERE profile.city = 'Seattle'
AND purchase.item = 'laptop';Detection:
Automatic Handling:
Execution:
Detection:
User Choice:
Implementation:
Query Examples:
customer.name)Recreating entire table when only ALTER TABLE ADD COLUMNS is needed Silently using NULL for missing columns without informing user Not asking user how to handle nested structures (flatten vs preserve) Incomplete flattening code (missing some nested fields) Incorrect DDL for nested types (wrong syntax) Not validating that ALTER TABLE succeeded Exploding arrays without explaining it creates multiple rows Not providing query examples for nested data access