Storage & Data Lake
Learn how each platform manages cloud object storage. Compare AWS S3 with Lake Formation, Snowflake's proprietary micro-partitions, and Databricks' Delta Lake transaction log.
Storage & Data Lake
Bottom Line: S3 is the universal substrate that all three platforms can use, but each platform adds a different reliability and management layer on top. Snowflake abstracts it away entirely; Databricks makes it first-class with Delta Lake; AWS gives you the raw controls and the governance tools to manage it yourself.
3.1 The Object Storage Foundation
All three platforms ultimately store data in cloud object storage (S3, ADLS Gen2, or GCS). The differentiation is the management and reliability layer built on top.
What each platform does with Object Storage:
AWS ββββββββββββββββββββββββββββββββββββββββββββββββ
(You manage) S3 β [YOU define: folders, formats, partitions, compaction]
Governance via: Lake Formation + Glue Catalog
Snowflake ββββββββββββββββββββββββββββββββββββββββββββββββ
(They manage) S3 β [Snowflake manages: FDN format, micro-partitions, clustering]
Your data is ON S3, but you can't directly read it (proprietary format)
Databricks ββββββββββββββββββββββββββββββββββββββββββββββββ
(Shared) S3 β Delta Log (JSON) + Parquet files
YOUR S3, YOUR format, YOU can read it with any Spark engine
3.2 Amazon S3 as a Data Lake (AWS)
Core S3 Concepts for Data Platforms
S3 is not "just storage" β it has evolved into a sophisticated foundation for data lakes:
s3://my-data-lake/
βββ raw/ β Bronze zone (untouched source data)
β βββ crm/salesforce/year=2024/month=01/day=01/
β β βββ contacts_20240101.json.gz
β βββ ecommerce/orders/year=2024/month=01/
β βββ orders_part_00001.parquet
βββ curated/ β Silver zone (cleaned, typed, joined)
β βββ customer_360/
β βββ year=2024/month=01/
β βββ *.snappy.parquet β Hive-style partitioning
βββ aggregated/ β Gold zone (business-level aggregates)
βββ regional_revenue/
βββ *.parquet
S3 Storage Classes and Cost Optimization
| Storage Class | Use Case | Retrieval Time | Relative Cost |
|---|---|---|---|
| S3 Standard | Hot data, active queries | Immediate | $$$ |
| S3 Intelligent-Tiering | Unknown access patterns | Immediate (Standard tier) | $$ + monitoring fee |
| S3 Standard-IA | Infrequent access (>1 month) | Immediate | $$ |
| S3 Glacier Instant Retrieval | Archive, quarterly access | Immediate | $ |
| S3 Glacier Deep Archive | Compliance cold storage | 12 hrs | Β’ |
π° Cost Gotcha: Athena and Glue jobs are billed on data scanned, not stored. A poorly partitioned table in S3 Standard can cost 10x more to query than a well-partitioned table in S3 IA, even if the storage cost is similar.
AWS Glue Data Catalog
The Glue Catalog is the central metadata registry for AWS data lake assets. It is Hive Metastore-compatible and used by Athena, EMR, Redshift Spectrum, and Glue ETL jobs.
# Create a table in Glue Catalog using Boto3
import boto3
glue = boto3.client('glue')
glue.create_table(
DatabaseName='ecommerce',
TableInput={
'Name': 'orders',
'StorageDescriptor': {
'Columns': [
{'Name': 'order_id', 'Type': 'string'},
{'Name': 'customer_id', 'Type': 'string'},
{'Name': 'revenue', 'Type': 'double'},
{'Name': 'order_date', 'Type': 'date'}
],
'Location': 's3://my-data-lake/curated/orders/',
'InputFormat': 'org.apache.hadoop.mapred.TextInputFormat',
'OutputFormat': 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat',
'SerdeInfo': {
'SerializationLibrary': 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe'
}
},
'PartitionKeys': [
{'Name': 'year', 'Type': 'string'},
{'Name': 'month', 'Type': 'string'}
],
'TableType': 'EXTERNAL_TABLE'
}
)AWS Lake Formation: Adding Governance to S3
Lake Formation sits above S3 and Glue Catalog to provide fine-grained access control. Without Lake Formation, S3 access is all-or-nothing via IAM bucket policies.
WITHOUT Lake Formation:
IAM Policy: s3:GetObject on s3://data-lake/*
β User can read ALL files in the bucket (no column/row-level control)
WITH Lake Formation:
LF Permission: SELECT on database=ecommerce, table=orders, columns=[order_id, revenue]
β User can only SELECT two specific columns, even if they have S3 access
LF Row Filter: revenue > 0 AND region = 'APAC'
β User only sees rows matching the filter
Lake Formation Table Formats:
Lake Formation supports governed tables with ACID properties:
-- Create a Lake Formation Governed Table (ACID on S3)
CREATE TABLE ecommerce.orders
LOCATION 's3://my-data-lake/governed/orders/'
TBLPROPERTIES (
'table_type' = 'GOVERNED'
);
-- ACID inserts via Athena (Lake Formation handles the transaction log)
INSERT INTO ecommerce.orders VALUES
('ord-001', 'cust-001', 299.99, DATE '2024-01-15');β οΈ Limitation: Lake Formation Governed Tables use AWS's own transaction log format, which is NOT compatible with Delta Lake or Apache Iceberg. Prefer Apache Iceberg on S3 if you need open-format ACID tables on AWS.
Apache Iceberg on AWS (The Open Format Recommendation)
For organisations wanting open-format ACID transactions on AWS (comparable to Delta Lake), Apache Iceberg via Athena or EMR is the recommended path:
-- Create an Iceberg table in Athena
CREATE TABLE ecommerce.orders (
order_id VARCHAR,
customer_id VARCHAR,
revenue DOUBLE,
order_date DATE
)
PARTITIONED BY (year(order_date))
LOCATION 's3://my-data-lake/iceberg/orders/'
TBLPROPERTIES (
'table_type' = 'ICEBERG',
'write_compression' = 'snappy',
'optimize_rewrite_delete_file_threshold' = '10'
);
-- MERGE (upsert) β fully ACID
MERGE INTO ecommerce.orders t
USING (SELECT * FROM staging.orders_updates) s
ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET revenue = s.revenue
WHEN NOT MATCHED THEN INSERT *;3.3 Snowflake Storage
How Snowflake Manages Storage
Snowflake abstracts storage completely. The "database" you interact with via SQL is backed by Snowflake's internal File/Data Node (FDN) format, stored in Snowflake's own S3/GCS/Azure Blob tenant. Key properties:
- Immutable micro-partitions: 50β500MB compressed blocks, automatically created on INSERT.
- Automatic clustering: Snowflake tracks the range of values in each column per micro-partition, enabling automatic partition pruning without explicit indexes.
- Columnar compression: Each column within a micro-partition is compressed independently using the optimal algorithm (LZO, Zstd, etc.).
-- You interact with Snowflake storage purely via SQL
-- All physical layout decisions are automatic
CREATE TABLE orders (
order_id VARCHAR,
customer_id VARCHAR,
revenue NUMBER(10,2),
order_date DATE,
region VARCHAR
)
-- No SORTKEY, DISTKEY, PARTITION BY required (unlike Redshift)
-- Snowflake handles physical layout automatically
;
-- Optional: Add a clustering key for very large tables (>1TB)
-- where access patterns are highly selective on a specific column
ALTER TABLE orders CLUSTER BY (order_date);
-- Snowflake will automatically re-cluster in the background (costs credits π°)Snowflake External Tables and Stages
Snowflake can also query data directly from S3/GCS/ADLS without loading it β useful for landing zones:
-- Step 1: Create a Storage Integration (one-time, admin task)
CREATE STORAGE INTEGRATION s3_data_lake_int
TYPE = EXTERNAL_STAGE
STORAGE_PROVIDER = 'S3'
ENABLED = TRUE
STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789:role/snowflake-s3-role'
STORAGE_ALLOWED_LOCATIONS = ('s3://my-data-lake/');
-- Step 2: Create a Named Stage
CREATE STAGE raw_orders_stage
STORAGE_INTEGRATION = s3_data_lake_int
URL = 's3://my-data-lake/raw/orders/'
FILE_FORMAT = (TYPE = 'PARQUET');
-- Step 3: Create an External Table (queries S3 directly, no data copy)
CREATE EXTERNAL TABLE ext_orders (
order_id VARCHAR AS (VALUE:order_id::VARCHAR),
customer_id VARCHAR AS (VALUE:customer_id::VARCHAR),
revenue FLOAT AS (VALUE:revenue::FLOAT),
order_date DATE AS (VALUE:order_date::DATE)
)
PARTITION BY (order_date)
LOCATION = @raw_orders_stage
AUTO_REFRESH = TRUE -- Snowflake monitors S3 event notifications
FILE_FORMAT = (TYPE = 'PARQUET');Snowflake Time Travel and Fail-Safe
| Feature | Time Travel | Fail-Safe |
|---|---|---|
| Purpose | User-accessible history (queries, clone, undrop) | Disaster recovery (Snowflake-managed) |
| Duration | 0β90 days (Standard=1, Enterprise=90) | Fixed 7 days (after Time Travel period) |
| Accessible by | You (via AT/BEFORE clause) | Snowflake Support only |
| Cost | β Included in storage cost | β Included in storage cost |
-- Query data as it was 2 days ago
SELECT * FROM orders AT (OFFSET => -172800); -- seconds
-- Query at a specific timestamp
SELECT * FROM orders AT (TIMESTAMP => '2024-03-15 09:00:00'::TIMESTAMP_LTZ);
-- Restore a dropped table
UNDROP TABLE orders;
-- Zero-Copy Clone (reference original micro-partitions β no data copy)
CREATE TABLE orders_dev CLONE orders; -- Instant, storage cost = 0 until writes diverge3.4 Delta Lake on Databricks
The Delta Lake Transaction Log in Detail
Delta Lake's durability and reliability come from the _delta_log/ directory. Understanding this is essential for operational work:
s3://my-bucket/delta-table/
βββ _delta_log/
β βββ 00000000000000000000.json {"commitInfo":..., "protocol":..., "metaData":...}
β βββ 00000000000000000001.json {"add": {"path": "part-001.parquet", ...}}
β βββ 00000000000000000002.json {"remove": {"path": "part-001.parquet",...},
β β "add": {"path": "part-003.parquet",...}}
β βββ 00000000000000000010.checkpoint.parquet β Snapshot at commit 10
βββ part-00001-uuid.snappy.parquet β Still physically exists (soft-deleted)
βββ part-00002-uuid.snappy.parquet
βββ part-00003-uuid.snappy.parquet β Newest data file
This log enables:
- Snapshot isolation: Readers see a consistent version; writers don't block readers.
- Time Travel: Read old snapshots by replaying the log to a target version.
- Audit History: Every change is recorded with timestamp, user, and operation.
# Read a specific Delta table version (Time Travel)
df = spark.read.format("delta") \
.option("versionAsOf", 5) \
.load("s3://my-bucket/delta-table/")
# Or by timestamp
df = spark.read.format("delta") \
.option("timestampAsOf", "2024-03-15") \
.load("s3://my-bucket/delta-table/")
# SQL equivalents
spark.sql("SELECT * FROM orders VERSION AS OF 5")
spark.sql("SELECT * FROM orders TIMESTAMP AS OF '2024-03-15'")Delta Lake: Key Operations
from delta.tables import DeltaTable
from pyspark.sql import functions as F
# --- MERGE (Upsert) ---
deltaTable = DeltaTable.forPath(spark, "s3://my-bucket/delta/orders/")
updates_df = spark.read.parquet("s3://my-bucket/raw/order_updates/")
deltaTable.alias("t").merge(
updates_df.alias("s"),
"t.order_id = s.order_id"
).whenMatchedUpdate(set={
"revenue": "s.revenue",
"status": "s.status",
"updated_at": F.current_timestamp()
}).whenNotMatchedInsert(values={
"order_id": "s.order_id",
"revenue": "s.revenue",
"status": "s.status",
"created_at": F.current_timestamp(),
"updated_at": F.current_timestamp()
}).execute()
# --- OPTIMIZE (Compaction + Z-Ordering) ---
# Compact small files AND co-locate related data for faster filtering
spark.sql("""
OPTIMIZE delta.`s3://my-bucket/delta/orders/`
ZORDER BY (customer_id, order_date)
""")
# ZORDER co-locates customer_id + order_date values in the same files
# Dramatically improves queries that filter on both columns
# --- VACUUM (Remove old files no longer referenced) ---
spark.sql("""
VACUUM delta.`s3://my-bucket/delta/orders/`
RETAIN 168 HOURS -- 7 days minimum recommended
""")
# WARNING: After VACUUM, Time Travel to pre-VACUUM versions is no longer possibleDatabricks Auto Optimize Features
Databricks adds managed optimisation on top of open-source Delta Lake:
-- Enable Auto Optimize at table creation
CREATE TABLE orders (
order_id STRING,
customer_id STRING,
revenue DOUBLE,
order_date DATE
)
USING DELTA
TBLPROPERTIES (
'delta.autoOptimize.optimizeWrite' = 'true', -- Compacts during writes
'delta.autoOptimize.autoCompact' = 'true', -- Async compaction post-write
'delta.dataSkippingNumIndexedCols' = '4' -- Stats collected on first 4 cols
);3.5 Medallion Architecture: Implementation per Platform
The Medallion (Bronze/Silver/Gold) pattern is a universal data lake design pattern. Here's how it maps to each platform:
| Layer | Purpose | AWS Implementation | Snowflake Implementation | Databricks Implementation |
|---|---|---|---|---|
| Bronze | Raw, immutable ingestion | S3 prefix /raw/, Glue Catalog, no transformations | Raw staging tables or External Tables on S3 | Delta tables with PERMISSIVE mode, schema-on-read |
| Silver | Cleaned, conformed, joined | S3 /curated/, Glue ETL or EMR Spark jobs, Parquet | Snowflake tables after Snowpipe/COPY INTO | Delta tables after DLT (Delta Live Tables) validation |
| Gold | Business-level aggregates | S3 /aggregated/, served by Athena or Redshift Spectrum | Snowflake views + aggregation tables | Delta tables, served by SQL Warehouses or Databricks dashboards |
Databricks DLT (Delta Live Tables) β Declarative Medallion:
import dlt
from pyspark.sql import functions as F
# Bronze β raw ingestion
@dlt.table(comment="Raw orders from source systems")
def bronze_orders():
return (
spark.readStream.format("cloudFiles")
.option("cloudFiles.format", "json")
.load("s3://my-bucket/landing/orders/")
)
# Silver β validated and cleaned
@dlt.table(comment="Validated orders with quality checks")
@dlt.expect_or_drop("valid_revenue", "revenue > 0")
@dlt.expect_or_drop("valid_order_id", "order_id IS NOT NULL")
def silver_orders():
return (
dlt.read_stream("bronze_orders")
.withColumn("order_date", F.to_date("order_date_str", "yyyy-MM-dd"))
.withColumn("revenue", F.col("revenue").cast("double"))
.drop("order_date_str")
)
# Gold β business aggregate
@dlt.table(comment="Daily revenue by region")
def gold_daily_revenue():
return (
dlt.read("silver_orders")
.groupBy("region", "order_date")
.agg(F.sum("revenue").alias("total_revenue"),
F.count("order_id").alias("order_count"))
)
3.6 Storage Feature Comparison Matrix
| Feature | AWS S3 + Lake Formation | Snowflake | Databricks Delta Lake |
|---|---|---|---|
| ACID Transactions | β οΈ Via Iceberg or Governed Tables | β Native | β Native |
| Time Travel | β οΈ Via S3 Versioning (costly) or Iceberg | β 0β90 days | β 30 days (default) |
| Zero-Copy Clone | β Not native | β‘ Instant clone | β οΈ Shallow clone (metadata only) |
| Auto Compaction | β Manual Glue/EMR job | β Automatic | β Auto Optimize (Databricks) |
| Schema Evolution | β οΈ Manual Glue update | β Automatic with policy | β
mergeSchema option |
| Row-Level Security | β LF Row Filters | β Row Access Policies | β Unity Catalog row filters |
| Column Masking | β LF Column Masking | β Dynamic Data Masking | β Unity Catalog column masks |
| Open Format | β Parquet/ORC/Iceberg | β Proprietary (internal) | β Parquet + Delta spec |
| Storage Cost Model | Pay per GB + requests | Pay per TB compressed | Pay per GB (your own S3 account) |
| Multi-Cloud Storage | β AWS only | β S3/GCS/ADLS | β S3/GCS/ADLS |