Many customers need an ACID transaction (atomic, consistent, isolated, durable) data lake that can log change data capture (CDC) from operational data sources. There is also demand for merging real-time data into batch data. Delta Lake framework provides these two capabilities. In this post, we discuss how to handle UPSERTs (updates and inserts) of the operational data using natively integrated Delta Lake with AWS Glue, and query the Delta Lake using Amazon Athena.
We examine a hypothetical insurance organization that issues commercial policies to small- and medium-scale businesses. The insurance prices vary based on several criteria, such as where the business is located, business type, earthquake or flood coverage, and so on. This organization is planning to build a data analytical platform, and the insurance policy data is one of the inputs to this platform. Because the business is growing, hundreds and thousands of new insurance policies are being enrolled and renewed every month. Therefore, all this operational data needs to be sent to Delta Lake in near-real time so that the organization can perform various analytics, and build machine learning (ML) models to serve their customers in a more efficient and cost-effective way.
The data can originate from any source, but typically customers want to bring operational data to data lakes to perform data analytics. One of the solutions is to bring the relational data by using AWS Database Migration Service (AWS DMS). AWS DMS tasks can be configured to copy the full load as well as ongoing changes (CDC). The full load and CDC load can be brought into the raw and curated (Delta Lake) storage layers in the data lake. To keep it simple, in this post we opt out of the data sources and ingestion layer; the assumption is that the data is already copied to the raw bucket in the form of CSV files. An AWS Glue ETL job does the necessary transformation and copies the data to the Delta Lake layer. The Delta Lake layer ensures ACID compliance of the source data.
The following diagram illustrates the solution architecture.

The use case we use in this post is about a commercial insurance company. We use a simple dataset that contains the following columns:
The dataset contains a sample of 25 insurance policies. In the case of a production dataset, it may contain millions of records.
In the following sections, we walk through the steps to perform the Delta Lake UPSERT operations. We use the AWS Management Console to perform all the steps. However, you can also automate these steps using tools like AWS CloudFormation, the AWS Cloud Development Kit (AWS CDK), Terraforms, and so on.
This post is focused towards architects, engineers, developers, and data scientists who build, design, and build analytical solutions on AWS. We expect a basic understanding of the console, AWS Glue, Amazon Simple Storage Service (Amazon S3), and Athena. Additionally, the persona is able to create AWS Identity and Access Management (IAM) policies and roles, create and run AWS Glue jobs and crawlers, and is able work with the Athena query editor.
Use Athena query engine version 3 to query delta lake tables, later in the section “Query the full load using Athena”.

To set up your S3 bucket, complete the following steps:
delta-lake-cdc-blog-<some random number>).full-load.csv to your local machine.$bucket\_name/fullload.
In this section, we create an IAM policy for the S3 bucket access and a role for AWS Glue jobs to run, and also use the same role for querying the Delta Lake using Athena.
{bucket\_name} you created in the earlier step.delta-lake-cdc-blog-policy and select Create policy.delta-lake-cdc-blog-policyAWSGlueServiceRoleCloudWatchFullAccessdelta-lake-cdc-blog-role).
In this section, we set up two AWS Glue jobs: one for full load and one for the CDC load. Let’s start with the full load job.

Full-Load-Job).delta-lake-cdc-blog-role that you created earlier.--s3\_bucket with the bucket name you created earlier as the value.--datalake-formats and give the value delta
Now let’s create the CDC load job.
CDC-Load-Job.import sysfrom awsglue.utils import getResolvedOptionsfrom awsglue.context import GlueContextfrom pyspark.sql.session import SparkSessionfrom pyspark.sql.functions import colfrom pyspark.sql.functions import expr## For Delta lakefrom delta.tables import DeltaTable## @params: [JOB\_NAME]args = getResolvedOptions(sys.argv, ['JOB\_NAME','s3\_bucket'])# Initialize Spark Session with Delta Lakespark = SparkSession \.builder \.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \.config("spark.sql.catalog.spark\_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \.getOrCreate()# Read the CDC loadcdc\_df = spark.read.csv("s3://"+ args['s3\_bucket']+"/cdcload")cdc\_df.show(5,True)# now read the full load (latest data) as delta tabledelta\_df = DeltaTable.forPath(spark, "s3://"+ args['s3\_bucket']+"/delta/insurance/")delta\_df.toDF().show(5,True)# UPSERT process if matches on the condition the update else insert# if there is no keyword then create a data set with Insert, Update and Delete flag and do it separately.# for delete it has to run in loop with delete condition, this script do not handle deletes. final\_df = delta\_df.alias("prev\_df").merge( \source = cdc\_df.alias("append\_df"), \#matching on primarykeycondition = expr("prev\_df.policy\_id = append\_df.\_c1"))\.whenMatchedUpdate(set= { "prev\_df.expiry\_date" : col("append\_df.\_c2"), "prev\_df.location\_name" : col("append\_df.\_c3"), "prev\_df.state\_code" : col("append\_df.\_c4"), "prev\_df.region\_name" : col("append\_df.\_c5"), "prev\_df.insured\_value" : col("append\_df.\_c6"), "prev\_df.business\_type" : col("append\_df.\_c7"), "prev\_df.earthquake\_coverage" : col("append\_df.\_c8"), "prev\_df.flood\_coverage" : col("append\_df.\_c9")} )\.whenNotMatchedInsert(values =#inserting a new row to Delta table{ "prev\_df.policy\_id" : col("append\_df.\_c1"), "prev\_df.expiry\_date" : col("append\_df.\_c2"), "prev\_df.location\_name" : col("append\_df.\_c3"), "prev\_df.state\_code" : col("append\_df.\_c4"), "prev\_df.region\_name" : col("append\_df.\_c5"), "prev\_df.insured\_value" : col("append\_df.\_c6"), "prev\_df.business\_type" : col("append\_df.\_c7"), "prev\_df.earthquake\_coverage" : col("append\_df.\_c8"), "prev\_df.flood\_coverage" : col("append\_df.\_c9")})\.execute()
On the AWS Glue console, open full-load-job and choose Run. The job takes about 2 minutes to complete, and the job run status changes to Succeeded. Go to $bucket\_name and open the delta folder, which contains the insurance folder. You can note the Delta Lake files in it. 
In this step, we create an AWS Glue crawler with Delta Lake as the data source type. After successfully running the crawler, we inspect the data using Athena.
delta-lake-crawler) and choose Next.s3://delta-lake-cdc-blog-123456789/delta/insurance) and enter the Delta Lake table path location.default target database, and provide delta\_ for the table name prefix. If no default database exist, you may create one.delta\_insurance table is available under Databases/Tables.You can observe nine columns and their data types. 
In the earlier step, we created the delta\_insurance table by running a crawler against the Delta Lake location. In this section, we query the delta\_insurance table using Athena. Note that if you’re using Athena for the first time, set the query output folder to store the Athena query results (for example, s3://<your-s3-bucket>/query-output/).
SELECT * FROM delta\_insurance;. This query returns a total of 25 rows, the same as what was in the full load data feed.The following screenshot shows the Athena query result.

In this section, we update three insurance policies and insert two new policies.
cdc-load.csv:The first column in the CDC feed describes the UPSERT operations. U is for updating an existing record, and I is for inserting a new record.
$bucket\_name/cdcload/ folder.CDC-Load-Job. This job takes care of updating the Delta Lake accordingly.The change details are as follows:
As shown in the following screenshot, the changes in the CDC data feed are reflected in the Athena query results.

In this solution, we used all managed services, and there is no cost if AWS Glue jobs aren’t running. However, if you want to clean up the tasks, you can delete the two AWS Glue jobs, AWS Glue table, and S3 bucket.
Organizations are continuously looking at high performance, cost-effective, and scalable analytical solutions to extract the value of their operational data sources in near-real time. The analytical platform should be ready to receive changes in the operational data as soon as they occur. Typical data lake solutions face challenges to handle the changes in source data; the Delta Lake framework can close this gap. This post demonstrated how to build data lakes for UPSERT operations using AWS Glue and native Delta Lake tables, and how to query AWS Glue tables from Athena. You can implement your large scale UPSERT data operations using AWS Glue, Delta Lake and perform analytics using Amazon Athena.
Praveen Allam is a Solutions Architect at AWS. He helps customers design scalable, better cost-perfromant enterprise-grade applications using the AWS Cloud. He builds solutions to help organizations make data-driven decisions.
Vivek Singh is Senior Solutions Architect with the AWS Data Lab team. He helps customers unblock their data journey on the AWS ecosystem. His interest areas are data pipeline automation, data quality and data governance, data lakes, and lake house architectures.