Columnar Data, from the inside out Iceberg overview →

Iceberg, Explained

metadata/

JSON 00004-5a81aac7-9cbf-4921-88f9-91bfe8643e88.metadata.json 3772 B

Avro snap-6837741135159966129-0-3208d62c-7a08-4314-baaf-827fa54bcc40.avro 1884 B

Avro 3208d62c-7a08-4314-baaf-827fa54bcc40-m0.avro 4665 B

Avro 3208d62c-7a08-4314-baaf-827fa54bcc40-m1.avro 4665 B

Avro 3ac6b924-605e-42e5-96bc-68baa90fc30d-m0.avro 4886 B

Avro 3208d62c-7a08-4314-baaf-827fa54bcc40-m2.avro 4666 B

File 1 · 1814 bytesFile 2 · 1703 bytesFile 3 · 1839 bytesFile 4 · 1839 bytesFile 5 · 1814 bytes

Start with the artefact

A warehouse holds more than a table.

5 Parquet files remain in this history. Follow the latest metadata pointer to find which ones belong to its table.

What Iceberg is

Iceberg is an open table format: metadata that turns a collection of Parquet files into a table with versions. Parquet describes the contents of one file. Iceberg decides which files belong to a snapshot, including which replaced files an older reader still needs. A catalog points through metadata JSON, a manifest list and manifests to the data. Engines such as Spark, Trino and Flink read and write tables through those references.

ParquetColumns stored as bytesIceberg · Delta LakeFiles selected by a versionArrowTyped arrays in memoryDuckDB · DataFusionOperators turn values into answers
Table metadata selects files. Readers decode stored columns into arrays. Engines compute on those arrays.
About this fixture

Written by PyIceberg 0.10.0 / PyArrow 25.0.0; checked with DuckDB 1.4.4 + iceberg extension. JSON, Avro and Parquet originals are hash-checked and decoded in this browser. Native delete and lifecycle experiments retain their own version pins. Concurrency controls model commit decisions over recorded states.

Copy-on-write table

Data snapshot

Verified fixture preview

Every arrow has evidence.

The catalog identifies a metadata file. That JSON records snapshots; each snapshot names a manifest list. The Avro list names manifests, and those manifests describe the data files.

Catalog pointerSelected metadata location
↓ metadata location ↓ snapshots[]
Initial write1295454848827360255operation: append
↓ manifest-list ↓ manifest_path
↓ data_file.file_path

0 original artifact bytes fetched in this session (cached per file). Loading a snapshot reads its complete tiny table to verify the demonstration; the pruning model below describes potential savings separately.

The table is a selection.

These are all 5 Parquet files retained in the bundled history. Only the files referenced by the selected snapshot contribute rows. The directory listing below is a comparison; it is never used to decide membership.

12 table rows come from 2 of 5 data files, containing 3,678 stored bytes.

Snapshot 012 visible rowsFile 1 · not selected · 5 physical rows12456File 2 · not selected · 1 physical rows13File 3 · selected · 6 physical rows123456File 4 · selected · 6 physical rows789101112File 5 · not selected · 5 physical rows1415161718
ID 3: read from File 3. The other file bytes still exist; only solid reference paths contribute rows.
Read the selected table · 12 rows
IDStationObserved at (no time zone)temperature_c
1North2026-01-01 00:00:005
2South2026-01-01 03:00:006
3North2026-01-01 06:00:007
4South2026-01-01 09:00:008
5North2026-01-01 12:00:009
6South2026-01-01 15:00:0010
7North2026-01-02 00:00:0011
8South2026-01-02 03:00:0012
9North2026-01-02 06:00:0013
10South2026-01-02 09:00:0014
11North2026-01-02 12:00:0015
12South2026-01-02 15:00:0016

The source CSV has 24 synthetic observations; this story uses the first 18. Timestamps are Iceberg timestamps without a time zone. ISO serialization provides a consistent display for those synthetic timestamps.

Query the logical result.

These Parquet exports contain complete logical results from the recorded native reads, after file selection and applicable deletes. The workbench runs SQL on the selected export. The export already contains the rows selected by the native table reader.

Exports preserve the recorded schema names and values. Iceberg’s renamed export uses the current metadata after the rename; historical exports retain their original names. Native delete cases include the dependencies needed by their original readers even when your SQL projects only a value column.

Choose from 14 verified snapshot exports
  • Query Initial write → 12 logical rows · 2,191 export bytes
    Version, schema and evidence

    Version / snapshot: 1295454848827360255
    Published metadata: warehouse/weather/metadata/00001-2b5db7e2-8443-4c54-8ea0-7fccc861418f.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 62c5be0af71d79e3ea931abc8f2a810837321dd8a46daa8ce19fd8ab3f1fc7f5
    Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591

  • Query Append six → 18 logical rows · 2,288 export bytes
    Version, schema and evidence

    Version / snapshot: 142073692612853544
    Published metadata: warehouse/weather/metadata/00002-8aec4d25-67cb-4464-9fe9-844ca8255b17.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 92666a4c7c7738d5bd54af02f6200da232e46650146b6488587d9da8c8e3c262
    Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591

  • Query Remove observation 3 → 17 logical rows · 2,278 export bytes
    Version, schema and evidence

    Version / snapshot: 6837741135159966129
    Published metadata: warehouse/weather/metadata/00003-35b3c799-6435-4079-bc89-c4fd3e21b174.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 7dc459ecc729b43c1687c687e6f6deac01f5b26545e9fa717e0dfcfd966bff76
    Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591

  • Query Remove observation 3 · renamed schema → 17 logical rows · 2,286 export bytes
    Version, schema and evidence

    Version / snapshot: 6837741135159966129
    Published metadata: warehouse/weather/metadata/00004-5a81aac7-9cbf-4921-88f9-91bfe8643e88.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · air_temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 25df145c385b87be65f4f3654745829d7db99abc2a27251ad63a3cb129ddcc79
    Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591

  • Query position · Eighteen original observations → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 4629318251272514798
    Published metadata: tables/position/metadata/v2.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 09671b8223e3ebe9a8efa080dd9d760a284868e923d27dee3836357d1ae7f10d
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query position · Delete A at position 2 → 17 logical rows · 2,278 export bytes
    Version, schema and evidence

    Version / snapshot: 1551675423107445728
    Published metadata: tables/position/metadata/v3.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 286a694491ff17688c32643b7f54e95138d1d62f7d55f94f84dc2b0b1a80e8c9
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query position · Append a new observation 3 → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 4243774134377942860
    Published metadata: tables/position/metadata/v4.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 9b0fea4ffb44e87b373ba023f3c77c2b8ffeba913d498bbc6dfc36ea91d5ea3d
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query equality · Eighteen original observations → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 5833448318538228973
    Published metadata: tables/equality/metadata/v2.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: e8521a3d92a7a6a90ad4cbd5054af028373cb2b42e39ee2a4072dc36da00c598
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query equality · Delete key 3 and insert its replacement together → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 7654658913322873365
    Published metadata: tables/equality/metadata/v3.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: e8db8268a64200225023c197c29b43ead39738e68fcfe0580ea698699d59d9bb
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query position-same · Eighteen original observations → 18 logical rows · 2,288 export bytes
    Version, schema and evidence

    Version / snapshot: 966253210586484786
    Published metadata: tables/position-same/metadata/v2.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: c7edc5705e1c5586227e15fd24e356688ecbaaf58a1897812d105262e5b7591f
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query position-same · Insert D and delete its position 0 together → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 3304934347579782469
    Published metadata: tables/position-same/metadata/v3.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 543dd84fde919f36a1a104d051042d081e813b39b59880750a0846aaa43b61b3
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query partition · Eighteen original observations → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 3019292698062751979
    Published metadata: tables/partition/metadata/v2.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 6790685eb834844d4ecf320d11191eec1572bf11b5f1d7aabe24b8cc8c3ad668
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query partition · Delete key 3 scoped to South → 18 logical rows · 2,289 export bytes
    Version, schema and evidence

    Version / snapshot: 4855542995429446986
    Published metadata: tables/partition/metadata/v3.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 5451f9ea337edee1d3715082109d233e0b5f3cab2a829521163675711f6fcd37
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

  • Query partition · Delete key 3 scoped to North → 17 logical rows · 2,278 export bytes
    Version, schema and evidence

    Version / snapshot: 6414387081102762729
    Published metadata: tables/partition/metadata/v4.metadata.json

    observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double

    Original native proof · Download derived Parquet result

    Export SHA-256: 62bc853ba798602f5399c67879e67d289a0347325519f66c2bfea0eb4e9dae39
    Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93

Export manifest · Pinned export recipe. Source proofs are checked by the existing native Spark/DuckDB verifiers; PyArrow and DuckDB check exported records and narrow projections. Reproduction instructions and scope.

A manifest is a file inventory with meaning.

Each manifest entry carries a status: 0 existing, 1 added, 2 deleted. The reader keeps 0 and 1 and skips 2. In the final snapshot, File 3 has status 2. Its replacement preserves the surviving observations. A later snapshot can reuse a manifest, so “added” describes when a file entered that manifest.

FileEntry statusPartition dayRowsTemperature boundsEvidence
File 3Added (1)2026-01-0165–10 °C
File 4Added (1)2026-01-02611–16 °C

Partition values come from the manifest’s partition struct. Bounds are typed values keyed by field ID: field 4 is the temperature, stored here as little-endian doubles.

A manifest entry is not automatically a live file.

Return to the original weather table and the snapshot selected at the top. A manifest entry records a file and its status: existing (0), added (1), or deleted (2). Existing and added entries can supply this snapshot’s rows; deleted entries describe removal and must not be counted as active data.

Added · 6 rows · live in the selected snapshot
Added · 6 rows · live in the selected snapshot

Counting live entries gives 12 rows. The verified snapshot has 12 rows.

Choose the delete snapshot to expose the difference. Click an entry to inspect its original manifest in the existing reference workbench. Read status within the selected snapshot’s manifests. Those references identify the inventory to inspect.

Sequence numbers have another job: they track ordering for changes and, in formats that support them, delete applicability. They are distinct from snapshot IDs, file paths, partition spec IDs and schema field IDs. This fixture uses copy-on-write, so no equality or position delete file is being silently applied.

New files join. Old files keep their identity.

The first write contains 12 rows across two days. Appending six observations adds 2 files and reuses all 2 original files. Observation 13 arrives late for January 1; the other five observations introduce January 3.

The new snapshot names its own manifest list. A reused manifest keeps its references to the same data files. Uploading new files alone would not change the table.

Remove one row. Rewrite its file.

Observation 3 shares a file with five other rows. Removing it writes a replacement containing those five survivors. The snapshot operation is overwrite, and the current table now has 17 rows.

Removed from current membership

File 3 · 6 rows

IDs 1, 2, 3, 4, 5, 6

Open historical file →
Replacement

File 1 · 5 rows

IDs 1, 2, 4, 5, 6

Open replacement file →

The original six-row file still exists for time travel. A deleted manifest entry drops a file reference. A delete file instead records which rows to remove while retaining their data file. Iceberg v2 also supports position and equality delete files; the delete-files chapter follows their sequence and applicability rules through actual files.

Change the snapshot, change the table.

The selector above drives the file graph, inventory, rows and pruning together. Snapshot identifiers are exact 64-bit values; they are never rounded to JavaScript numbers.

An old snapshot remains readable while its required files are retained. Snapshot expiration and physical file cleanup are separate maintenance operations; this fixture keeps the complete history.

A field’s name can change. Its identity stays.

After the deletion, the writer renames temperature_c to air_temperature_c. Both names identify field 4. The data files are unchanged, and the current snapshot ID is unchanged.

Field ID 4temperature_c

Checking the box selects the latest metadata context, which can still reference every retained snapshot. The row header changes; its values still come from the original physical column. Historical reads may use the snapshot’s original schema.

Separate metadata timeline

  1. snapshot 1295454848827360255

  2. snapshot 142073692612853544

  3. snapshot 6837741135159966129

  4. snapshot 6837741135159966129

Partition evolution is another metadata capability. The mixed-specification lesson in the pruning chapter uses a second real table to demonstrate it; this original fixture has one day partition specification.

Reusing an old name must not resurrect an old column.

Use the alternate layout’s weather observations to see how a rename preserves values. Rename temperature_c to air_temperature_c, move it to the first display position, and add a new nullable temperature_c field. No data file changes. The renamed field keeps ID 4, matching the main story; the newly added field receives ID 5.

Source row

Original physical columns

ID 1 · observation_id1
ID 2 · stationNorth
ID 3 · observed_at2026-01-01T00:00:00
ID 4 · temperature_c5

Current schema order

ID 4 · air_temperature_c5
ID 1 · observation_id1
ID 2 · stationNorth
ID 3 · observed_at2026-01-01T00:00:00
ID 5 · temperature_cNULL: no field 5 in old file

Tracking ID 3. It is absent from the final seventeen-row table; the schema demonstration currently shows ID 1.

Renaming and reordering preserve values. The newly added nullable field reads as null in historical files.

Names are for people and queries; field IDs preserve identity across compatible evolution. Dropping a field and later adding the same name must allocate a new identity. Defaults and required-field rules complicate other cases; this native fixture specifically adds an optional field with no non-null default.

Inspect the actual evolved schema
{
  "type": "struct",
  "fields": [
    {
      "id": 4,
      "name": "air_temperature_c",
      "type": "double",
      "required": false
    },
    {
      "id": 1,
      "name": "observation_id",
      "type": "long",
      "required": false
    },
    {
      "id": 2,
      "name": "station",
      "type": "string",
      "required": false
    },
    {
      "id": 3,
      "name": "observed_at",
      "type": "timestamp",
      "required": false
    },
    {
      "id": 5,
      "name": "temperature_c",
      "type": "double",
      "required": false
    }
  ],
  "schema-id": 3,
  "identifier-field-ids": []
}

Rule out files before opening them.

First, the manifest list’s day bounds can rule out manifests. Inside those retained manifests, the day partition and upper temperature bound can prove a data file has no matches. A surviving file remains a candidate until its rows are evaluated. Missing bounds keep the file.

Observed day

1 / 1 candidate manifests after the day filter.

The filter retains 2 of 2 files. Decoded rows yield 12 matches; excluded files contain 0 bytes.

File 3Candidate2026-01-01 · 5–10 °C
File 4Candidate2026-01-02 · 11–16 °C
Snapshot observationsID 1ID 2ID 3ID 4ID 5ID 6ID 7ID 8ID 9ID 10ID 11ID 12Candidate file rowsID 1 · candidateID 2 · candidateID 3 · candidateID 4 · candidateID 5 · candidateID 6 · candidateID 7 · candidateID 8 · candidateID 9 · candidateID 10 · candidateID 11 · candidateID 12 · candidatePredicate resultID 1 · 5 °CID 2 · 6 °CID 3 · 7 °CID 4 · 8 °CID 5 · 9 °CID 6 · 10 °CID 7 · 11 °CID 8 · 12 °CID 9 · 13 °CID 10 · 14 °CID 11 · 15 °CID 12 · 16 °C
File-level pruning and row-level filtering preserve different sets of observations. Lines follow the same identity between columns.

Matching observation IDs: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.

The first pass checks manifest-list partition summaries. The second checks each file’s partition and temperature bounds. Decoded rows decide which candidates actually match.

Change physical organization, preserve the answer

The same seventeen rows, two partition specifications.

Keep the final weather records and their field IDs. This native experiment changes when the writer switches from day to month; every source value remains the same.

Partition evolution changes new writes, not old files.

Keep the same seventeen observations from the delete snapshot. Change only how their files are partitioned: write the eleven surviving observations from the first two days under the day specification, then write the six appended observations, IDs 13–18, under a month specification. ID 13 is a late observation from January 1; IDs 14–18 belong to January 3. This is a separate native layout of the same logical answer, with observation 3 still absent. Both specifications remain in metadata. Each manifest identifies the specification needed to interpret its files.

Timestamp predicate

observed_at ≥ 2026-01-02T12:00:00 AND observed_at < 2026-01-03T00:00:00

Spec 0: day(observed_at)

File A · pruned

Partition value 20454
Physical IDs: 1, 2, 4, 5, 6

Original timestamps

2026-01-01T00:00:00

2026-01-01T03:00:00

2026-01-01T09:00:00

2026-01-01T12:00:00

2026-01-01T15:00:00

File B · candidate

Partition value 20455
Physical IDs: 7, 8, 9, 10, 11, 12

Original timestamps

2026-01-02T00:00:00

2026-01-02T03:00:00

2026-01-02T06:00:00

2026-01-02T09:00:00

2026-01-02T12:00:00

2026-01-02T15:00:00

Spec 1: month(observed_at)

File C · candidate

Partition value 672
Physical IDs: 13, 14, 15, 16, 17, 18

Original timestamps

2026-01-01T21:00:00

2026-01-03T03:00:00

2026-01-03T06:00:00

2026-01-03T09:00:00

2026-01-03T12:00:00

2026-01-03T15:00:00

Day partition values count days since 1970-01-01; month values count months since January 1970. The integer 672 means January 2026 under a month transform. Interpreting it as a day would route the file to a different date. The spec ID supplies the missing meaning.

Source observationID 1 · 01-01T00:00ID 2 · 01-01T03:00ID 4 · 01-01T09:00ID 5 · 01-01T12:00ID 6 · 01-01T15:00ID 7 · 01-02T00:00ID 8 · 01-02T03:00ID 9 · 01-02T06:00ID 10 · 01-02T09:00ID 11 · 01-02T12:00ID 12 · 01-02T15:00ID 13 · 01-01T21:00ID 14 · 01-03T03:00ID 15 · 01-03T06:00ID 16 · 01-03T09:00ID 17 · 01-03T12:00ID 18 · 01-03T15:00Planned fileID 7 → file BID 8 → file BID 9 → file BID 10 → file BID 11 → file BID 12 → file BID 13 → file CID 14 → file CID 15 → file CID 16 → file CID 17 → file CID 18 → file CResidual passedID 11 survivesID 12 survives
A coarse partition selects a file; its residual still selects individual observations. Lines follow the same identity between columns.

The residual is the part still owed by the reader.

File B
GreaterThanOrEqual(term=Reference(name='observed_at'), literal=literal('2026-01-02T12:00:00'))

Read row values to evaluate the remaining timestamp condition.

File C
And(left=GreaterThanOrEqual(term=Reference(name='observed_at'), literal=literal('2026-01-02T12:00:00')), right=LessThan(term=Reference(name='observed_at'), literal=literal('2026-01-03T00:00:00')))

Read row values to evaluate the remaining timestamp condition.

Native result IDs: 11, 12. Planned files: B, C.

Predict the change from a full day to noon onward

File B contains January 2 observations 7–12, from midnight to 15:00. A full-day predicate needs no timestamp residual there. Starting at noon keeps the file but only IDs 11 and 12 pass. File C contains late January 1 observation 13 and January 3 observations 14–18. It has no January 2 rows at all, yet its month partition and broad timestamp interval retain it. The residual rejects every row for a January 2 query.

Hidden partitioning lets queries use the source timestamp. Planning translates that condition through each file’s partition transform. Previous files retain their own partition specification when the current spec changes. PyIceberg’s actual task residuals are shown above; DuckDB independently verifies each final result.

Keep the data file. Change which rows can be read.

Two kinds of delete file, two different identities.

The copy-on-write example replaced a data file. These four independent Iceberg v2 tables start again with the same eighteen weather observations. They commit actual Parquet delete files through Apache Iceberg’s Java API. Spark and DuckDB read every resulting snapshot. Each branch has its own sequence numbers; these are alternate histories, not continuations of the seventeen-row table above.

A position delete names a data-file path and a zero-based row position. An equality delete names values of fields identified by equality_ids. Both remain separate from the data bytes. An entry with status DELETED removes a file from membership; a live entry with content 1 or 2 adds a file that removes rows.

Delete experiment

Snapshot 1551675423107445728 · sequence 2 · 17 visible / 18 physical rows

Live manifest entries for this snapshot
FileContentPartitionRaw data sequenceInherited from manifestEffective data / file sequenceEquality IDs
A0 · dataunpartitionednull11 / 1
B0 · dataunpartitionednull11 / 1
P1 · position deleteunpartitionednull22 / 2

In these added entries, a null sequence inherits the manifest’s sequence. The data sequence controls delete applicability. The file sequence records when the file was added. They happen to match here; a rewrite can preserve data age while changing file age.

Follow observation 3 through the read

Observation to trace

For each physical occurrence, first check partition scope and sequence, then the delete’s row identity. Removal requires every applicability check to pass, including the key match. These branches each use a single partition specification; the partition experiment uses identity(station).

A : position 2ID 3 · 7°CData sequence 1PMaskedBytes retained

P: partition passes; sequence 1 ≤ 2 passes; path + position matches. Remove this occurrence.

A contains observation 3 at ordinal 2, data sequence 1. Its stored values remain readable in the physical file, but do not contribute a table row.

Position: data sequence ≤ delete sequence

It can remove a row from a file added in the same commit. In the third branch, D and its position delete both have sequence 2, so D:0 is masked. The original ID 3 in A survives: the path is different.

Equality: data sequence < delete sequence

It applies to older data. In the second branch, E at sequence 2 removes ID 3 from A at sequence 1. D at sequence 2 contains the same ID, now 30°C, and survives. An update can therefore delete the old key and add its replacement in one commit.

The fourth branch shows why partition scope comes first. Key 3 belongs to North. The South-scoped equality delete leaves it visible: partition scope fails despite the matching sequence and key. Adding the North delete removes it. An equality delete written with an unpartitioned specification has global scope; the next native experiment executes that cross-spec case.

Trace the reference into the representation

Open the delete, down to its value bytes.

Use the currently selected branch and snapshot. Read the original metadata JSON, follow its manifest list, open the delete manifest, then decode the referenced Parquet file. Every read is checked against its captured SHA-256. The full local paths inside the original v2 files identify the captured table; the download links below serve those exact bytes.

Choose an artifact to inspect.

Position deletes use reserved field IDs 2147483546 (file_path) and 2147483545 (pos). Our equality delete stores field ID 1, observation_id. A renamed column keeps its ID; a newly added column with the old name gets a different ID. Matching only names would apply the wrong key.

The byte view is deliberately specific: these one-row delete files use uncompressed Parquet V1 PLAIN encoding. The reader skips the optional column’s definition-level stream before showing the integer. Other encodings require their own decoding steps.

Continue into delete boundaries and Puffin vectors →

The pointer update is the commit.

Conceptual simulation: two writers start from the same metadata. Their files can coexist in storage; only a successful catalog commit publishes a new table state.

Step 1 / 5

Both writers read metadata M0.

Catalog pointerM0

A catalog provides atomic publication and conflict detection. Before retrying, the writer validates its operation against the newly committed state.

Refreshing the pointer is not enough.

Use the real delete operation as writer A’s change: replace File 3 with File 1. Writer B either rewrites that same old file or appends an independent file D. D and the competing writers are hypothetical; the file replacement comes from the verified history.

Writer B's operation

Published catalog root

M0

Reader R pinned M0 before either writer committed. R stays on M0’s snapshot and reads File 3 through the end of its scan. The replacement belongs to the newly published snapshot.

B's validation assumptions

File 3 must still be live because B is replacing the rows it read from that file.

Expected catalog root: M0. Staged files are not published by their existence.

Both writers prepare against M0.

Blindly installing B’s original M2 would lose A’s committed change. A successful retry rebuilds metadata from the refreshed state and checks the operation’s assumptions. A passing validation is still separate from winning the next atomic publication attempt.

The model follows publication and file-rewrite validation. The native schedules below show the corresponding decisions in executed transactions. Iceberg’s concurrency and retry model.

Removing a snapshot reference does not make every old file garbage.

Snapshots share unchanged data files. In this reachability model, retain or expire historical snapshots and optionally pin the first snapshot with a branch. The current snapshot always remains. A file stays reachable if any retained reference needs it.

File 1Reachable from snapshot 2
File 2Reachable from snapshot 1, snapshot 2
File 3Reachable from snapshot 0, snapshot 1
File 4Reachable from snapshot 0, snapshot 1, snapshot 2
File 5Reachable from snapshot 1, snapshot 2

5 of 5 data files remain reachable.

Expiring the first snapshot alone may free nothing: a later retained snapshot can still refer to the same file. A branch or tag can keep older history reachable. Cleanup also accounts for file age, active operations and retained catalog references. These controls explore which references keep each file reachable.

The catalog’s atomic metadata-pointer update is the publication boundary. Data and metadata files can be staged beforehand, but readers resolve one committed root and follow its references. A competing writer validates against the current root, then atomically publishes its new metadata pointer.

Download the mixed-spec and field-identity evidence · Iceberg specification

Run the writers, inspect the outcome

A stale snapshot can be safe for one operation and unsafe for another.

These are actual Apache Iceberg Java operations over local Hadoop tables. Two independent handles read the same eighteen-row snapshot before either commits. Writer B prepares its operation; A commits first; then B attempts to commit. Snapshot lifetimes overlap in a controlled schedule. The local Hadoop tables publish metadata through the filesystem.

Native Iceberg writer schedule

1. Both writers read the same snapshot18 rowsRead by A and B 3706799091859689728
2. Writer A commits17 rowsNative snapshot 6423870635082758303
3. B rejects; A remains committed17 rowsNo new snapshot 6423870635082758303
4. B recomputes from current rows16 rowsNative snapshot 5809130760053370453

Both writers read the same snapshot. The prepared file is private until its metadata commit succeeds.

Native scan tasks: data files and attached deletes
Data fileSpecPhysical recordsDeletes the reader must apply
A.parquet018None

18 native result rows · IDs 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18

Snapshot 3706799091859689728 · sequence 1 · Original metadata JSON

Inspect the native manifest entries and complete result

B originally read snapshot 3706799091859689728. Its staged file contains IDs 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18. It still contains ID 3, which A removed. Publishing that stale replacement would resurrect 3. Recomputing from A’s seventeen rows and removing 4 produces sixteen rows.

The rewrite declares the snapshot from which it read and validates data/delete conflicts. Compatible appends can rebase metadata on the newer state. The recorded schedules expose both rejected changes and successful rebasing against a newer snapshot. The abstract control above remains useful for exploring another publication race.

Observe actual removal in an isolated table

Expiration and orphan cleanup remove different things.

Main replaces A after deleting ID 3, but an audit branch still points to the original snapshot. A separate Parquet file was written and never committed. Predict whether expiring history should remove that unpublished file.

Cleanup prediction

Original snapshot

Retained references

main
3456057876572685767

Retained snapshots

3456057876572685767

Parquet files physically present

A.parquet

Native scan tasks: data files and attached deletes
Data fileSpecPhysical recordsDeletes the reader must apply
A.parquet018None

18 native result rows · IDs 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18

Snapshot 3456057876572685767 · sequence 1 · Original metadata JSON

Inspect the native manifest entries and complete result

The generator ran these destructive maintenance operations only on fresh temporary tables and checked existence before and after. The download preserves copies of artifacts removed during cleanup, so you can inspect what the operation deleted. In a real table, choose retention long enough for active readers and in-progress writers before running cleanup.

Reproduce every native boundary

Download lifecycle tables, source and pinned native recipe. The verifier checks original artifact hashes and regenerates every schedule and maintenance operation in a new temporary directory, comparing complete rows, rejection types and scan tasks. It requires Java 17 and uv. The v2 deletion-file archive above separately reopens relocated copies of its original tables.

The reproduction bundle preserves the local publication schedule and the original vector bytes.

Iceberg selects it. Parquet stores it.

Open an exact bundled file in the existing explorer. The return link preserves this snapshot selection, including when you inspect a historical file.

FileMembershipOpen
File 1 · 1,814 bytes

warehouse/weather/data/observed_day=2026-01-01/00000-0-3208d62c-7a08-4314-baaf-827fa54bcc40.parquet

Not in this snapshotExplore File 1 →
File 2 · 1,703 bytes

warehouse/weather/data/observed_day=2026-01-01/00000-0-3ac6b924-605e-42e5-96bc-68baa90fc30d.parquet

Not in this snapshotExplore File 2 →
File 3 · 1,839 bytes

warehouse/weather/data/observed_day=2026-01-01/00000-0-9030630c-1236-4919-943f-8538d66de3bf.parquet

AddedExplore File 3 →
File 4 · 1,839 bytes

warehouse/weather/data/observed_day=2026-01-02/00000-1-9030630c-1236-4919-943f-8538d66de3bf.parquet

AddedExplore File 4 →
File 5 · 1,814 bytes

warehouse/weather/data/observed_day=2026-01-03/00000-1-3ac6b924-605e-42e5-96bc-68baa90fc30d.parquet

Not in this snapshotExplore File 5 →

Take the evidence with you.

Written with PyIceberg 0.10.0 / PyArrow 25.0.0. Every snapshot’s exact rows and values were independently checked with DuckDB 1.4.4 + iceberg extension (extension 1095c1fa). The downloadable bundle contains the original artifacts, source CSV and a hash index.

Read the Apache Iceberg specification, schema evolution guide and reliability model.

Your turn.

Choose a metadata JSON from your own table. Follow its snapshot and manifest references, then compare the files it selects.