Variant data type

Updated at:

Hologres V5.0 and later can read Variant data from data lake tables through external tables, but Hologres internal tables do not currently support the Variant data type. Variant suits semi-structured data whose field structure changes often: it preserves the native type of each value and speeds up access to frequently queried subfields through Parquet shredding. This topic describes where Variant applies, which operators it supports, how typed casts work, and how queries are optimized.

What is Variant

Events, logs, and device telemetry rarely have a fixed shape: fields come and go, the same field may hold different types, and the schema keeps evolving. Storing such data as JSON text makes ingestion easy, but every query must parse the whole document to read a single field. Flattening every field into structured columns makes queries fast, but each new field requires another schema change and sparse fields leave large numbers of nulls behind.

Variant keeps the schema flexible while making semi-structured data in lake tables cheaper to store and faster to query. It uses the Apache Parquet Variant encoding to store values in a compact binary format that preserves native types such as dates, timestamps, timestamps with time zone, and exact numerics. A single Variant column can hold objects, arrays, or scalars with different structures, and adding an inner field does not change the table schema.

Hologres reads Variant columns from data lake tables, accesses nested fields with operators, and converts fields to native Hologres types with typed casts. For frequently used fields that are stored as shredded columns, queries also benefit from Parquet column pruning and row group-level data skipping, which cut the amount of data read.

Variant compared with JSON and JSONB

JSON, JSONB, and Variant all represent semi-structured data, but they differ in where the data lives, how it is encoded, and how queries are optimized. For details about JSON and JSONB, see JSON and JSONB.

Item

JSON and JSONB

Variant

Where it applies

Internal tables.

External tables such as DLF Paimon tables.

Storage format

Text for JSON, binary for JSONB.

Parquet Variant encoding, with shredding support.

Write support

INSERT and COPY are supported.

Not supported. Data is written to Paimon by engines such as Spark and Flink.

Index support

GIN indexes for JSONB.

No indexes. Queries are accelerated by shredding and row group data skipping.

Operators

->, ->>, #>, #>>, ?, @>, and <@.

->, ->>, #>, and ?, plus typed casts.

Type conversion

Extract TEXT with ->>, then cast, for example with ::int.

Cast directly, for example (v->'age')::int8.

Column pruning

Columnar JSONB in Hologres V1.3 and later provides limited column pruning.

Native column pruning on shredded subcolumns.

Limits

  • Hologres V5.0 and later can read Variant columns only from data lake tables accessed through external tables, such as DLF Paimon tables.

  • Hologres internal tables do not currently support the Variant data type. You cannot create a Variant column in an internal table. To store semi-structured data in a Hologres internal table, use the JSON or JSONB data type. For more information, see JSON and JSONB.

  • Hologres can only read Variant columns, not write to them. Variant data in data lake tables is written to Paimon by engines such as Spark and Flink.

  • The #>> operator, which returns the value at a path as TEXT, is not supported. To get the text value at a nested path, use the #> operator with a ::text cast.

  • Typed casts support five target types: ::int8 (BIGINT), ::float8 (DOUBLE PRECISION), ::bool (BOOLEAN), ::text (TEXT), and ::timestamptz (TIMESTAMP WITH TIME ZONE). Other casts, such as ::int4 (INTEGER), are not supported. To get an INTEGER value, cast to ::int8 first and then cast the result.

  • Variant columns do not support GIN indexes.

Variant operators

Common operators

Variant supports the following operators. Their semantics match the JSON and JSONB operators.

Operator

Right operand type

Description

Example

Result

->

int

Returns the array element at the specified index. Indexes start at 0.

SELECT (v->0)::text FROM t WHERE id=5;

10

->

text

Returns the object field with the specified key. The result is still a Variant value.

SELECT v->'age' FROM t WHERE id=0;

21

->>

text

Returns the object field as TEXT, without the enclosing quotation marks.

SELECT v->>'city' FROM t WHERE id=0;

Beijing

#>

text[]

Returns the Variant value at the specified path.

SELECT (v#>'{nested,k}')::text FROM t WHERE id=4;

[1, 2, 3]

?

text

Checks whether the specified key exists in the Variant object.

SELECT v?'age' FROM t WHERE id=0;

t

Typed casts

Use the ::type syntax to cast a subfield directly to a native type, without first converting it to TEXT. The following target types are supported.

Cast

Target type

Description

Example

Result

::text

TEXT

Converts the Variant value to text. Objects and arrays are returned as JSON text, strings keep their enclosing quotation marks, and numbers and booleans are returned as their text representation.

SELECT (v->'city')::text FROM t WHERE id=0;

"Beijing"

::int8

BIGINT

Converts a Variant number to a 64-bit integer.

SELECT (v->'age')::int8 FROM t WHERE id=0;

21

::float8

DOUBLE PRECISION

Converts a Variant number to a double-precision floating-point number.

SELECT (v->'score')::float8 FROM t WHERE id=0;

3.14

::bool

BOOLEAN

Converts a Variant boolean to BOOLEAN.

SELECT (v->'active')::bool FROM t WHERE id=0;

t

::timestamptz

TIMESTAMP WITH TIME ZONE

Converts a Variant timestamp with time zone to TIMESTAMP WITH TIME ZONE. The displayed value depends on the time zone of the current session.

SELECT (v->'event_time')::timestamptz FROM t WHERE id=0;

2026-09-02 10:00:00+08

Shredded storage and query optimization

The main advantage of Variant is shredded storage. When Paimon writes a Variant column, it splits frequently used fields into separate physical Parquet columns (typed_value), so the query engine can read the relevant subcolumn without parsing the entire Variant document.

Column pruning

When a query touches only a few fields of a Variant object, the engine reads just the shredded physical subcolumns and skips the rest. For example, in a Variant column with 20 fields, a query on v->'age' reads only the typed_value column of age, which costs about as much I/O as reading a single plain INT column.

Row group data skipping

Shredded subcolumns automatically carry Parquet row group statistics (min, max, and null_count). Before scanning, the query engine uses these statistics to skip row groups that cannot match the filter, which greatly reduces the amount of data read.

The following point query runs against a table with 500,000 rows and skips most of the data based on the row group statistics of a shredded subcolumn.

SELECT id, (v->'seq')::int8 AS seq
FROM ext_dlf.variant_acc.v_skip
WHERE (v->'seq')::int8 = 250000;

Run EXPLAIN ANALYZE to verify that data skipping takes effect.

->  Seq Scan on v_skip
      Filter: (((v -> 'seq'::text))::bigint = '250000'::bigint)
      RowGroupFilter: (((v -> 'seq'::text))::bigint = '250000'::bigint)
      [scan_rows=3868]

In this plan, RowGroupFilter shows that the filter is pushed down to row group pruning: only 3,868 of the 500,000 rows are actually scanned.

Examples

Read a Variant column

The following example reads a Variant column from a Paimon table through a DLF external database. For the full syntax, see CREATE EXTERNAL DATABASE. For the end-to-end procedure of accessing Paimon data through DLF, see Access Paimon data with DLF Catalog.

-- 1. Create an external database if you do not have one.
CREATE EXTERNAL DATABASE ext_dlf WITH
  catalog_type 'paimon'
  metastore_type 'dlf-rest'
  dlf_catalog '<YOUR_CATALOG_NAME>'
  comment 'DLF Paimon catalog';

-- 2. Read the Variant column.
SELECT id, v::text
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
ORDER BY id
LIMIT 10;

A sample result is as follows.

 id |                    v
----+-----------------------------------------
  0 | {"age": 21, "city": "Beijing"}
  1 | {"age": 27}
  2 | {"city": "Beijing", "other": "xxx"}
  3 | {"other": "yyy"}
  4 | {"age": 28, "nested": {"k": [1, 2, 3]}}
(5 rows)

Project multiple fields

You can extract several fields from a Variant object in one query and cast each field independently.

SELECT
  id,
  v::text AS raw,
  (v->'age')::int8 AS age,
  (v->'city')::text AS city
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
ORDER BY id;

A sample result is as follows.

 id |                   raw                   | age  |   city
----+-----------------------------------------+------+-----------
  0 | {"age": 21, "city": "Beijing"}          |   21 | "Beijing"
  1 | {"age": 27}                             |   27 |
  2 | {"city": "Beijing", "other": "xxx"}     |      | "Beijing"
  3 | {"other": "yyy"}                        |      |
  4 | {"age": 28, "nested": {"k": [1, 2, 3]}} |   28 |
(5 rows)
Note

If a row does not contain the field that you access, the corresponding column returns SQL NULL.

Access a nested path

Use the #> operator to access a nested object.

SELECT (v#>'{nested,k}')::text AS nested_array
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id = 4;

The result is as follows.

 nested_array
--------------
 [1, 2, 3]
(1 row)

Access array elements

The following example accesses each element of a row whose Variant value is [10, 20, 30].

SELECT
  (v->0)::text AS idx0,
  (v->1)::text AS idx1,
  (v->2)::text AS idx2
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id = 5;

The result is as follows.

 idx0 | idx1 | idx2
------+------+------
 10   | 20   | 30
(1 row)

Check whether a key exists

Use the ? operator to check whether a key exists.

SELECT id, v?'age' AS has_age
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id IN (0, 3)
ORDER BY id;

The result is as follows.

 id | has_age
----+---------
  0 | t
  3 | f
(2 rows)

Sort and aggregate by a Variant field

After a typed cast, a Variant subfield can be used directly in ORDER BY and GROUP BY clauses.

-- Sort by the age field.
SELECT id
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
ORDER BY (v->'age')::int8 NULLS LAST
LIMIT 5;

-- Aggregate by the age field.
SELECT (v->'age')::int8 AS age, COUNT(*)
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
GROUP BY 1
ORDER BY 1 NULLS LAST;

Access a field that does not exist

The root value of a Variant column can be any JSON value, not only an object. For a non-object root value such as an array, string, number, boolean, or null, accessing a key with -> returns SQL NULL. In the following example, the Variant values of rows 5 to 9 are [10,20,30], "scalar string", 12345, true, and null.

SELECT
  id,
  v::text,
  (v->'nokey')::int8 AS n_int,
  (v->'nokey')::text AS n_text
FROM ext_dlf.<YOUR_SCHEMA>.<YOUR_TABLE>
WHERE id >= 5
ORDER BY id;

The result is as follows.

 id |        v        | n_int | n_text
----+-----------------+-------+--------
  5 | [10, 20, 30]    |       |
  6 | "scalar string" |       |
  7 | 12345           |       |
  8 | true            |       |
  9 | null            |       |
(5 rows)
Note
  • Accessing any key of a non-object Variant value returns SQL NULL.

  • Query results display Variant values as JSON text.