Skip to main content

Iceberg v3 in Gnok: Deletion Vectors, VARIANT, Spatial Types, and Row Lineage

· 8 min read
Gnok Team

Apache Iceberg format version 3 brings four major capabilities: deletion vectors for more efficient row-level mutations, the VARIANT type for semi-structured data, GEOMETRY/GEOGRAPHY types for spatial analytics, and row lineage for stable row identity across compaction. Gnok now supports all of them.

This post explains what each feature does, when to use it, and how to get started.

Format Versions at a Glance​

Iceberg defines three format versions, each building on the previous:

VersionKey Additions
v1Immutable data files, schema evolution, partition specs, sort orders, column statistics
v2Position and equality deletes, sequence numbers, partition evolution, snapshot references
v3Deletion vectors, VARIANT type, GEOMETRY/GEOGRAPHY, row lineage, default column values

Gnok defaults to v2 when creating tables. To use v3 features, specify the format version explicitly:

CREATE TABLE events (
id BIGINT,
payload VARIANT,
location GEOMETRY
) WITH ('format-version' = '3');

You can also upgrade an existing table:

ALTER TABLE events SET TBLPROPERTIES ('format-version' = '3');
caution

Format version upgrades are one-way -- downgrading is not supported. Make sure all consumers of the table support v3 before upgrading.

Deletion Vectors​

The Problem with Position Deletes​

In v2, every UPDATE or DELETE writes a position delete file -- a Parquet file containing (file_path, pos) pairs that mark which rows are deleted. This works, but delete files accumulate over time. A table with frequent updates can end up with hundreds of delete files, each of which must be read and merged during every scan.

Data file (1M rows)
├── Position delete file #1 (50 rows)
├── Position delete file #2 (30 rows)
├── Position delete file #3 (120 rows)
└── ... (N more delete files)

Each scan must load all N delete files, build an index, and filter every row against it. At scale this becomes the dominant cost of reads.

How Deletion Vectors Work​

Deletion vectors replace individual delete files with a single, compact Roaring bitmap stored in a Puffin file. Instead of N separate Parquet files with (file_path, pos) pairs, each data file has at most one deletion vector containing the set of deleted row positions.

Data file (1M rows)
└── Deletion vector (Roaring bitmap, ~2KB) → {pos: 12, 47, 193, ...}

The binary format is compact:

Combined Length (4 bytes)
Magic: 0xD1D33964 (4 bytes)
64-bit Roaring Bitmap (variable)
CRC-32C checksum (4 bytes)

Roaring bitmaps are extremely space-efficient for sparse sets (a few thousand deletes in a million-row file) and support O(1) membership tests. The bitmap is stored in a Puffin blob with LZ4 or Zstd compression.

Current Support​

Gnok fully supports reading deletion vectors. During scan planning, DVs are matched to data files by referenced_data_file path, preloaded, and merged with any existing position deletes. The write path currently produces v2-style position delete files; DV write support is planned.

OperationStatus
Read DVs during scanSupported
Merge DVs with position deletesSupported
Write DVs on UPDATE/DELETEPlanned

What This Means for You​

If you produce data with a writer that creates deletion vectors (e.g., Spark 4.0), Gnok can read those tables efficiently today. For Gnok-originated writes, position deletes are used until DV write support ships. Run OPTIMIZE PURGE DELETES periodically to consolidate accumulated delete files.

VARIANT Type​

Semi-Structured Data, Natively​

JSON columns stored as VARCHAR work for simple cases, but they have drawbacks: no type safety, no columnar pruning, and every access requires parsing the entire string. The VARIANT type solves this by storing semi-structured data in Iceberg's native binary variant format with optional shredding -- extracting frequently accessed fields into separate columns for efficient columnar reads.

Creating and Querying VARIANT Data​

-- Create a table with a VARIANT column
CREATE TABLE events (
event_id BIGINT,
event_type VARCHAR,
payload VARIANT
) WITH ('format-version' = '3');

-- Insert JSON data as VARIANT
INSERT INTO events VALUES
(1, 'click', PARSE_JSON('{"page": "/home", "duration_ms": 1200, "user": "alice"}')),
(2, 'purchase', PARSE_JSON('{"item": "widget", "price": 29.99, "quantity": 3}')),
(3, 'click', PARSE_JSON('{"page": "/products", "duration_ms": 800}'));

Accessing Nested Values​

Use VARIANT_GET to extract typed values at a dot-path:

SELECT
event_id,
event_type,
VARIANT_GET(payload, 'page', 'VARCHAR') AS page,
VARIANT_GET(payload, 'duration_ms', 'INT') AS duration_ms,
VARIANT_GET(payload, 'price', 'DOUBLE') AS price
FROM events;

Result:

event_id | event_type | page      | duration_ms | price
---------+------------+-----------+-------------+------
1 | click | /home | 1200 | NULL
2 | purchase | NULL | NULL | 29.99
3 | click | /products | 800 | NULL

Function Reference​

FunctionDescription
PARSE_JSON(string)Parse JSON into VARIANT
TO_VARIANT(expr)Convert a scalar to VARIANT
VARIANT_GET(v, path, type)Extract a typed value at a dot-path
VARIANT_EXTRACT_STRING(v, path)Extract any value as a string
VARIANT_TYPE(v)Return the runtime type name
VARIANT_AS_BOOL(v)Cast to BOOLEAN
VARIANT_AS_INT(v)Cast to BIGINT
VARIANT_AS_DOUBLE(v)Cast to DOUBLE
VARIANT_AS_STRING(v)Cast to VARCHAR
IS_VARIANT_NULL(v)True if the VARIANT value is null

When to Use VARIANT vs JSON​

ScenarioUse
Schema is unknown or evolving rapidlyVARIANT
You need columnar pruning on nested fieldsVARIANT (with shredding)
Existing tables with JSON string columnsKeep as-is; use json_extract()
Small payloads, infrequent accessEither works

GEOMETRY and GEOGRAPHY Types​

Spatial Analytics in SQL​

v3 adds native spatial types following PostGIS naming conventions. GEOMETRY operates in Cartesian coordinates (planar); GEOGRAPHY uses geodesic calculations (great-circle distances on the WGS84 ellipsoid).

Example: Store Locator​

-- Create a table with spatial columns
CREATE TABLE stores (
store_id BIGINT,
name VARCHAR,
location GEOMETRY
) WITH ('format-version' = '3');

-- Insert store locations
INSERT INTO stores VALUES
(1, 'Downtown', ST_POINT(-73.9857, 40.7484)),
(2, 'Midtown', ST_POINT(-73.9787, 40.7614)),
(3, 'Brooklyn', ST_POINT(-73.9442, 40.6782));

-- Find stores within 2km of a location
SELECT
name,
ST_DISTANCE(location, ST_POINT(-73.98, 40.75)) AS distance_m
FROM stores
WHERE ST_DWITHIN(location, ST_POINT(-73.98, 40.75), 2000)
ORDER BY distance_m;

Spatial Pruning​

Gnok applies spatial pruning at the scan level using bounding box statistics stored in file metadata. Queries with ST_WITHIN, ST_CONTAINS, ST_INTERSECTS, or ST_DWITHIN predicates can skip entire data files whose bounding boxes don't overlap the query region.

CRS Support​

CRSEPSG CodeUse Case
WGS844326GPS coordinates (lat/lon)
Web Mercator3857Web maps (Leaflet, Mapbox)
UTM zones326xx/327xxRegional metric projections

Transform between projections with ST_TRANSFORM:

SELECT ST_TRANSFORM(location, 3857) AS web_mercator_point
FROM stores;

Full Function List​

ST_POINT, ST_DISTANCE, ST_WITHIN, ST_CONTAINS, ST_INTERSECTS, ST_AREA, ST_LENGTH, ST_ASTEXT, ST_GEOMFROMTEXT, ST_GEOGFROMTEXT, ST_X, ST_Y, ST_SRID, ST_TRANSFORM, ST_BUFFER, ST_DWITHIN, ST_ENVELOPE

Row Lineage​

Stable Row Identity​

Row lineage provides globally unique, stable row identifiers that survive compaction, schema evolution, and table maintenance. Each row gets a _row_id that doesn't change when the underlying data files are rewritten.

How It Works​

  1. Each table tracks a monotonically increasing next_row_id counter in its metadata
  2. When data files are committed, each file is assigned a first_row_id from this counter
  3. A row's _row_id is computed as first_row_id + position_within_file
  4. The counter advances by the total number of rows written

Querying Row Lineage​

Two metadata columns are available on v3 tables:

SELECT
_row_id,
_last_updated_sequence_number,
order_id,
amount
FROM orders
LIMIT 5;
_row_id | _last_updated_sequence_number | order_id | amount
--------+-------------------------------+----------+-------
0 | 1 | 1001 | 99.99
1 | 1 | 1002 | 49.50
2 | 1 | 1003 | 199.00
3 | 2 | 1004 | 75.25
4 | 2 | 1005 | 320.00
ColumnDescription
_row_idGlobally unique, stable row identifier
_last_updated_sequence_numberThe commit sequence number that last modified this row

These are metadata columns -- they're excluded from SELECT * but can be requested explicitly.

Use Cases​

  • CDC pipelines: Track which rows changed between commits using _last_updated_sequence_number
  • Deduplication: Use _row_id as a stable key for exactly-once processing
  • Audit trails: Record _row_id in downstream systems for lineage tracking
  • Incremental ETL: Process only rows where _last_updated_sequence_number > last_processed_sequence

Default Column Values​

v3 adds support for default values that apply both to new rows (when the column is omitted on INSERT) and to existing rows (when reading data written before the column was added):

-- Add a column with defaults
ALTER TABLE orders ADD COLUMN priority INT DEFAULT 0;
ALTER TABLE orders ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
Default TypeWhen Applied
write_defaultNew rows where the column value is omitted
initial_defaultExisting rows written before the column was added

This makes schema evolution seamless -- old data files don't need to be rewritten when a new column is added.

Migration Guide​

Upgrading to v3​

  1. Assess compatibility: Ensure all consumers (Spark, Trino, Flink, etc.) support Iceberg v3
  2. Upgrade the table:
    ALTER TABLE my_table SET TBLPROPERTIES ('format-version' = '3');
  3. Start using v3 features: Add VARIANT/GEOMETRY columns, query _row_id, etc.

Creating New v3 Tables​

CREATE TABLE sensor_data (
sensor_id BIGINT NOT NULL,
reading DOUBLE,
metadata VARIANT,
location GEOMETRY,
ts TIMESTAMP
)
PARTITIONED BY (day(ts))
WITH ('format-version' = '3');

What Doesn't Change​

  • All v1 and v2 features continue to work unchanged
  • Existing queries don't need modification
  • Position deletes are still written (until DV write support ships)
  • Schema evolution, partition pruning, and time travel work identically

Further Reading​