In Iceberg v2, how do equality delete files differ from position delete files at read time?
answer
- one says where, the other says what
- the writer's shortcut is the reader's bill
- offsets can be pruned; values cannot
- streaming writers cannot afford lookups
- sequence numbers decide which files are covered
basics
~20 sA position delete names a data file and the row offsets to drop, so a reader skips exactly those rows. An equality delete names column values, so the reader must evaluate them against every candidate data file in the partition — far more expensive.
solid answer
~50 sBoth are merge-on-read delete encodings in Iceberg format v2, but they carry different information. A **position delete** file stores `file_path` and `pos` pairs: it says "in this file, drop rows 17, 42 and 981". The reader loads only the delete files whose path bounds cover the data file it is scanning and filters by position — cheap and precise. An **equality delete** stores the values of one or more equality field IDs, meaning "any row whose id equals 42 is gone". The writer never had to find where those rows live, which is why Flink CDC pipelines emit them at streaming speed, but the reader must apply the predicate against every data file in the partition with an older sequence number, effectively an anti-join per scan. Equality deletes are cheap to write and expensive to read; position deletes are the reverse. Compaction converts either into clean data files.
code
text · 7 linesposition delete file (columns: file_path, pos)
s3://wh/db/events/data/00000-12-abc.parquet, 17
s3://wh/db/events/data/00000-12-abc.parquet, 42
equality delete file (equality field ids: [1] = user_id)
user_id = 42
user_id = 77go deeper
Recall that a merge-on-read table records removed rows in separate small files rather than rewriting data, and that there is more than one way to describe which rows went away.
Explain both encodings concretely — file path plus row offset versus column values — and why the second is cheaper to write and dearer to read.
Tie the delete kind to the writer that produced it, explain applicability by sequence number, and prescribe the compaction cadence that keeps a CDC-fed table's read latency stable.
Own the ingest-versus-query tradeoff: whether streaming upserts justify the read-side anti-join across the platform, what compaction budget that commits you to, and when a batch merge path is the cheaper answer.
## Two ways to say a row is gone Iceberg format version 2 introduced delete files so that an update or delete need not rewrite whole data files. There are two kinds, and they encode the removal at different levels of precision. **Position deletes** record a pair per removed row: the path of the data file and the ordinal position of the row inside it. Reading them is nearly free. Position delete files carry lower and upper bounds on `file_path` in the manifest, so a scan of one data file loads only delete files whose range covers that path, then drops rows by offset as it streams. The cost to the writer is that it had to know *where* each row lived — meaning it had to locate the rows first, typically with a join against the table. **Equality deletes** record values instead: one or more columns (identified by equality field IDs) and the values whose rows are deleted, for example `id = 42`. The writer never has to find the rows, which is the whole point — a change-data-capture stream can emit a delete for a primary key it has never read. The reader pays instead. It cannot know which files might contain matching rows, so it must apply the equality predicate against every data file in the partition eligible for that delete, an anti-join executed on every scan. ## Which deletes apply to which data files Iceberg resolves applicability by sequence number within a partition. Each commit gets a sequence number; a delete file applies only to data files committed earlier. Precisely, a position delete applies to data files whose sequence number is less than or equal to its own, and an equality delete to data files with a strictly smaller sequence number. This is what lets newly written rows survive an older delete of the same key — an insert after a delete is not silently removed. ## Who writes which Spark's merge-on-read `DELETE`, `UPDATE` and `MERGE` write position deletes, because Spark already resolves the matching rows as part of planning the statement. Flink's upsert/CDC sink writes equality deletes, because a streaming job applying a change log cannot afford a lookup per record. So the delete types you see are a fingerprint of the writers on your table, and an interviewer asking this is usually probing whether you have operated a streaming ingest path. ## Why equality deletes hurt On a table absorbing continuous CDC, equality delete files accumulate per partition and every one of them widens the anti-join each reader must perform. Query latency degrades non-linearly with the number of un-compacted equality deletes, and unlike position deletes there is no cheap path-based pruning to fall back on. The remedy is compaction: `rewrite_data_files` reads data files together with the delete files that apply and writes clean replacements, dropping the deletes entirely. Its `delete-file-threshold` option makes files carrying many deletes eligible even when their size is already fine. For position deletes specifically there is also `rewrite_position_delete_files`, which compacts the delete files without rewriting the data — a cheaper intermediate step with no equality-delete equivalent. ## What changed in format v3 Format version 3 replaces position delete files with **deletion vectors**: a compact bitmap of deleted row positions for one data file, stored in a Puffin file and referenced from the manifest. A reader gets at most one vector per data file instead of a scattered set of small position delete files, so applying deletes becomes a bitmap lookup rather than a merge over many files. Equality deletes remain the streaming-writer mechanism. Note that this is Iceberg's own mechanism; other table formats implement the same idea differently, so keep the vocabulary attached to the format you mean. ## Diagnosing what you have The `files` metadata table exposes a content column distinguishing data files, position deletes and equality deletes: ```sql SELECT content, count(*) AS files FROM prod.db.events.files GROUP BY content; ``` A rising equality-delete count is a signal that the streaming writer is outrunning compaction, and the fix is a compaction cadence matched to the ingest rate — not a query-side tuning knob.
- Why does a Flink CDC sink write equality deletes rather than position deletes?A change log arrives as keys and values, not file offsets. Producing a position delete would require locating the row's current file and position — a lookup or join per record, which a streaming sink cannot sustain. Emitting `key = value` lets the writer commit at streaming latency and pushes the resolution work to readers, which is a deliberate trade the ingest path makes on the query path's behalf.
- How do you stop equality deletes from degrading query latency?Compact on a cadence matched to the ingest rate. `rewrite_data_files` reads each data file with the delete files that apply to it and writes clean replacements, after which the delete files are no longer referenced; `delete-file-threshold` targets files carrying many deletes regardless of size. Then expire snapshots so the replaced files and delete files are actually released from storage.
saying these in an interview costs you the question
- Treats the two delete kinds as interchangeable encodings
- Thinks equality deletes are cheaper for readers too
- Assumes a delete file applies to every data file in the table
- Mixes up this mechanism with another format's deletion vectors
- Expects delete files to vanish without compaction