Variant data type
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 |
|
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 |
|
|
|
Type conversion |
Extract TEXT with |
Cast directly, for example |
|
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::textcast. -
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::int8first 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. |
|
|
|
|
text |
Returns the object field with the specified key. The result is still a Variant value. |
|
|
|
|
text |
Returns the object field as TEXT, without the enclosing quotation marks. |
|
|
|
|
text[] |
Returns the Variant value at the specified path. |
|
|
|
|
text |
Checks whether the specified key exists in the Variant object. |
|
|
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 |
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. |
|
|
|
|
BIGINT |
Converts a Variant number to a 64-bit integer. |
|
|
|
|
DOUBLE PRECISION |
Converts a Variant number to a double-precision floating-point number. |
|
|
|
|
BOOLEAN |
Converts a Variant boolean to BOOLEAN. |
|
|
|
|
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. |
|
|
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)
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)
-
Accessing any key of a non-object Variant value returns SQL NULL.
-
Query results display Variant values as JSON text.