DataLane
(updated )12 min readSnowflake

Snowflake-Managed Iceberg Tables: Open Storage without Losing Warehouse SQL

When to use Snowflake-managed Iceberg vs native tables: external volumes, catalogs, deletion vectors, and the interoperability jobs Iceberg actually wins.

By Dinesh Chandra

Illustrated overview of Snowflake-Managed Iceberg Tables: Open Storage without Losing Warehouse SQL
Table of contents

Apache Iceberg is the open table format most warehouses and lake engines have agreed to speak. Snowflake-managed Iceberg tables let you keep storage in your cloud account (Parquet + Iceberg metadata) while Snowflake owns compaction, DML, and governance for that table.

If every consumer is Snowflake SQL, you probably want a native table. If Spark jobs and BI both need the same facts, Iceberg is the contract.

flowchart LR
  ev[External volume / your bucket] --> ice[Iceberg table]
  ice --> sf[Snowflake SQL]
  ice --> spark[Spark / Trino]
  sf --> catalog[Horizon / catalog API]

What you are actually buying

A native Snowflake table is micropartitions Snowflake stores and maintains. You never see the files. Cloning, Time Travel, clustering, and Secure Data Sharing are built for that object type.

An Iceberg table is:

  1. Data files (usually Parquet) in your S3 / ADLS / GCS prefix
  2. Metadata and manifests next to those files
  3. A catalog entry that says which snapshot is current
  4. A Snowflake object that runs SQL against that snapshot

Snowflake-managed means Snowflake is the writer of record: it commits snapshots, runs maintenance, and applies grants the way it does for other tables. Other engines read the same files. That is the whole point — and the whole operational surface.

Externally managed Iceberg (Glue, a REST catalog, another engine writing) is a different product. You can register those tables in Snowflake for read. Do not treat “Snowflake can SELECT it” as “Snowflake will compact it, time-travel it, and own the schema.”

Native vs Iceberg (the real split)

Question Native table Snowflake-managed Iceberg
Who stores the files? Snowflake Your bucket
Who writes snapshots? Snowflake Snowflake
Other engines read the files? No (sharing is Snowflake-to-Snowflake) Yes, if they speak that Iceberg version
Time Travel / clone story First-class, well documented Supported, not identical — test it
Best when Snowflake is the only engine A second engine must see the same gold
Ops surface Warehouse + grants Volume + IAM + catalog + warehouse + grants

If the answer to “who else reads this?” is “nobody for a year,” pick native. Iceberg is not a free upgrade. It is a portability tax you pay only when portability is a requirement.

External volume first, table second

You do not create an Iceberg table into a magic Snowflake-managed bucket. You point Snowflake at a cloud location it is allowed to use.

The object is an external volume: IAM role (or equivalent), base URL, and a name you will repeat in every CREATE ICEBERG TABLE.

Sketch — property names move; read the current CREATE EXTERNAL VOLUME docs for your cloud:

create or replace external volume ev_analytics
  storage_locations = (
    (
      name = 'analytics-s3'
      storage_provider = 'S3'
      storage_base_url = 's3://company-lake/iceberg/'
      storage_aws_role_arn = 'arn:aws:iam::123456789012:role/snowflake-iceberg'
    )
  )
  allow_writes = true;

Then prove the volume before you create ten tables:

desc external volume ev_analytics;

If this fails, stop. Table DDL will fail the same way, only later, in a dbt run at 02:00. Typical breaks: wrong bucket prefix, role missing s3:ListBucket on the prefix, KMS key the role cannot use, or allow_writes = false on a table you expected Snowflake to maintain.

Put the volume, the role, and the prefix layout in the same repo as the table DDL. A new environment should not be a console treasure hunt.

A minimal create

create or replace iceberg table analytics.silver.orders_iceberg (
  order_id number,
  customer_id string,
  amount number(12, 2),
  ordered_at timestamp_ntz
)
  catalog = 'SNOWFLAKE'
  external_volume = 'ev_analytics'
  base_location = 'silver/orders/';

catalog = 'SNOWFLAKE' is the managed path: Snowflake writes Iceberg metadata and you query the table like any other.

base_location is relative to the volume. Treat it as an API. Do not reuse a prefix that already has a different table’s metadata. Do not let a Spark job “help” by writing into the same prefix.

Exact properties change with account edition and Iceberg version. Read the current Snowflake Iceberg docs before you copy this into production — especially ICEBERG_VERSION and whether external engines may write.

Prove DML on a throwaway table before you point gold at the volume:

insert into analytics.silver.orders_iceberg
  values (1, 'C01', 19.50, current_timestamp()::timestamp_ntz);

select count(*) from analytics.silver.orders_iceberg;

If this insert fails, the table is not ready for dbt. Fix IAM and the volume first.

flowchart TD
  vol[External volume + IAM] --> create[CREATE ICEBERG TABLE]
  create --> dml[Snowflake INSERT / MERGE]
  dml --> snap[New snapshot in your bucket]
  snap --> sf[Snowflake readers]
  snap --> other[Spark / Trino readers]

One writer. Many readers. The snapshot is the contract.

Catalogs: managed vs registered

Three shapes show up in real accounts:

Snowflake-managed. CREATE ICEBERG TABLE … CATALOG = 'SNOWFLAKE'. Snowflake is the writer. This is the shape this post is about.

Catalog-linked / externally managed. You attach a Glue, REST, or Open Catalog integration and read (sometimes write, when the docs and region say so) tables another engine already owns. Useful for “Spark wrote bronze, analysts want SQL.” Dangerous if two writers commit.

Conversion / dual metadata. Some platforms emit Iceberg metadata beside another format (UniForm-style). That can let Snowflake read a table Databricks still writes. The rule does not change: one writer of record, written down.

If you cannot draw “who commits the next snapshot” on a whiteboard, you do not have a lake contract. You have a future incident.

What Snowflake maintains (and what you still own)

On a managed Iceberg table, Snowflake is responsible for:

  • Committing DML as Iceberg snapshots
  • Table maintenance it documents for that version (compaction / file sizing — names and commands change; check current docs)
  • Role-based access on the Snowflake object

You still own:

  • The bucket, encryption keys, lifecycle rules, and requester-pays surprises
  • IAM that must outlive the intern who clicked “allow”
  • Compatibility: Spark/Trino/Databricks versions vs Iceberg v2/v3
  • The decision that nothing else writes those prefixes

Lifecycle policies that expire objects under an Iceberg table are a classic self-own. The catalog will still point at files you deleted. Expire old snapshots through the table format, not with a blanket S3 rule on *.parquet.

Iceberg v3 highlights (2026)

Snowflake’s 2026 Iceberg v3 GA added deletion vectors (cheaper DML), row lineage for CDC-shaped reads, VARIANT-like payloads, and richer geo/timestamp types. Two practical warnings:

  1. Upgrades are one-way. A v3 table is not readable by a v2-only engine. Align Spark/Trino versions before you flip the table.
  2. External writes to Snowflake-managed v3 tables may still be limited. Design “Snowflake writes, others read” unless the docs say otherwise for your region.

Deletion vectors are why MERGE on Iceberg stopped feeling like “rewrite the world.” They are also why a reader that does not understand v3 deletion vectors will return wrong rows or refuse the table. Version alignment is a release checklist item, not a footnote.

Row lineage helps “what changed since snapshot N” without inventing your own CDC from whole-table diffs. Use it when a downstream engine can consume it. Do not assume every BI tool will.

Reads from a second engine

The happy path:

Snowflake MERGE/INSERT
  → new Iceberg snapshot in your bucket
  → Spark/Trino refresh catalog / read latest snapshot
  → same grain, same files

What breaks that path:

  • Cached metadata in Spark (you “wrote it,” they still see yesterday)
  • A reader pinned to an old snapshot “for stability”
  • Schema evolution Snowflake accepted that the Spark job’s struct cannot parse
  • Case-sensitive identifiers and timestamp types that are not the same type on both sides

Give the Spark job an explicit catalog refresh and a logged snapshot id in the run output. “It looked empty” is usually a stale catalog, not a missing write.

A minimal Spark read (names vary by catalog):

# Reader side — Snowflake already committed the snapshot
df = (
    spark.read
    .format("iceberg")
    .load("analytics.silver.orders_iceberg")
)
df.createOrReplaceTempView("orders")

If that load string is a guess, you do not have a contract. Put the catalog namespace and table ident in the same config both jobs import.

Time Travel, clones, and shares

Native Time Travel is a well-worn incident tool. Iceberg snapshots are also point-in-time, but the SQL, retention knobs, and clone behavior are not a copy-paste of native Time Travel.

Do this once per environment, on a throwaway table:

-- After a known write
select count(*) from analytics.silver.orders_iceberg;

insert into analytics.silver.orders_iceberg
  values (0, 'probe', 0, current_timestamp()::timestamp_ntz);

-- Confirm you can see "before" with the Iceberg/Snowflake syntax
-- your account actually documents (AT / snapshot / timestamp).

If the restore story is “we will figure it out during the incident,” you do not have a restore story.

Secure Data Sharing of Iceberg vs native is another place people assume. Sharing a native table to a customer’s Snowflake account is the product Snowflake optimized for years. Sharing files in your bucket is a different permission model (their IAM, your bucket, or a Snowflake listing that still has to resolve storage). Design the share before you promise a customer “same as our other shares.”

Cost and ops

  • Storage is in your bucket (S3/ADLS/GCS rates) plus Snowflake compute for queries and maintenance.
  • You still need warehouse sizing — Iceberg does not make a Large warehouse free. See the cost guide.
  • Small files still hurt. Managed maintenance helps; a thousand single-row commits an hour will still show up as list-and-open overhead on the reader side.
  • Cross-cloud is not free. Snowflake in AWS reading a GCS volume is a conversation with networking and egress, not a checkbox.

A useful habit: one worksheet that lists Iceberg tables, volumes, and base locations. When a Spark job 404s, you want the prefix in git, not in a screenshot from last quarter.

show iceberg tables in schema analytics.silver;

select
  table_catalog,
  table_schema,
  table_name
from information_schema.tables
where table_type ilike '%iceberg%'
   or table_name ilike '%iceberg%';  -- confirm the real filter in your account

Inventory Iceberg objects the same way you inventory warehouses.

dbt and Iceberg

Put volume name, catalog, and base_location in dbt configs, not in tribal knowledge:

# models/silver/orders_iceberg.yml  (shape, not a promise of every key)
models:
  - name: orders_iceberg
    config:
      materialized: incremental
      incremental_strategy: merge
      unique_key: order_id
      catalog: snowflake
      external_volume: ev_analytics
      base_location: silver/orders/

Exact dbt-snowflake Iceberg keys depend on adapter version. If the adapter cannot set them, emit a pre-hook / run-operation that CREATE ICEBERG TABLE IF NOT EXISTS with the contract, then insert. The anti-pattern is “analyst created it in the UI, dbt only inserts.”

vs Delta on Databricks

Delta is the default on Databricks; Iceberg is the default shared format across Snowflake, Athena, Spark, and Flink. If the lake is Databricks-only, Delta is simpler. If Snowflake and Spark share gold facts, Iceberg (or a catalog-linked database) is the interoperability play. We expand that in the Delta vs Iceberg comparison.

UniForm and catalog links blur the screenshot. They do not blur the writer rule. If Databricks writes Delta and Snowflake reads Iceberg metadata generated from that table, Databricks is still the writer. If Snowflake-managed Iceberg is the gold table, Spark is a reader.

Two writers on one prefix is not “flexibility.” It is split-brain.

Pitfalls

  • Second engine that cannot read v3. Upgrade readers first, or stay on the version both sides support.
  • Lifecycle rules on the data prefix. You will delete live snapshots. Restrict expiry to a trash prefix, not the table root.
  • Assuming Time Travel equals native AT. Test undrop, clone, and point-in-time SELECT on Iceberg in staging.
  • Spark “repair” jobs writing the same base_location. One writer.
  • External volume in account A, table cloned to account B, IAM still pointing at A’s role. Clones copy metadata, not your AWS relationship.
  • VARIANT / geo / timestamp types that Snowflake accepts and Trino rejects. Pick a lowest-common-denominator schema for shared gold.
  • Clustering / search optimization habits from native tables copied blindly. Confirm the feature exists for Iceberg in your edition before you budget it.

Decision rules

Use Snowflake-managed Iceberg when:

  • A non-Snowflake engine must read the same gold files this quarter (or you have a dated project, not a vibe, that it will)
  • You want Snowflake SQL and governance on data you are required to keep in your bucket (residency, another platform already billed there)

Use a native Snowflake table when:

  • Consumers are Snowflake (BI, dbt, sharing to other Snowflake accounts)
  • You want the simplest Time Travel / clone / clustering story
  • Nobody can name the second engine

Use an externally managed Iceberg table in Snowflake when:

  • Spark or Databricks already owns writes
  • You need analyst SQL without moving the files
  • You can live with Snowflake as a reader

Never:

  • Two independent writers
  • Iceberg “for the resume” on a 20 GB dimension only Snowflake reads
  • A bucket layout you cannot recreate from git

What this means for data engineers

Pick Iceberg when you have a second engine today or a credible six-month plan for one. Otherwise native tables plus Secure Data Sharing are less operational surface. Put the external volume, catalog, and table DDL in the same repo as dbt so a new environment is not a click-ops mystery.

The format is the contract. The warehouse is the engine. Do not pay for both without a consumer that needs the files.

FAQ

If only Snowflake reads the table, should I use Iceberg? Usually no. Native tables have a simpler Time Travel, clone, and clustering story. Iceberg is a portability tax.

Can Spark write into a Snowflake-managed prefix? Treat that as forbidden unless current docs for your region say otherwise. Two writers on one base_location is split-brain.

Does Iceberg Time Travel match native AT? It is point-in-time, not a copy-paste of native syntax or retention. Test clone and AT / snapshot SELECT on a throwaway table.

Who pays for storage? Your bucket (cloud rates) plus Snowflake compute for queries and maintenance. Egress appears if the warehouse and the bucket are in different clouds.

Will a blanket S3 lifecycle rule save money? It can delete live snapshot files. Expire old snapshots through the table format, not with *.parquet expiry on the table root.

Share this post:X / TwitterLinkedIn

Enjoyed this post?

Get the next one in your inbox — one email a week, no spam.

Newsletter signup is not live yet. Use the contact form if you want to be notified.

More on Snowflake

↑↓ navigate openesc close