Skip to main content

ClickHouse data types

This is the type catalogue the dynamic encoder works against. When dfe-loader reflects a table from system.columns, every column comes back as one of these types (possibly wrapped in Nullable or LowCardinality), and the encoder in dfe-clickhouse's dynamic module has to turn a Value into the right bytes for it. Use this page when you are designing a schema.

Nothing here is loader-invented: it is the ClickHouse type system as of the 24.8+ line, annotated with how each type lands on the native wire. For how those bytes are actually shipped (HTTP RowBinary or native blocks), see insert-formats.md. For where the encoder hands its bytes to the client's insert calls, see clickhouse-ext.md.

Sources:


Type status legend​

StatusMeaning
GAGenerally Available
BetaBeta, stable but may change
ExperimentalExperimental, requires setting flag
DeprecatedDeprecated, avoid in new code

Complete type enumeration​

Integer types (GA)​

TypeBytesRangeNative Protocol
Int81-128 to 127Little-endian
Int162-32768 to 32767Little-endian
Int324-2^31 to 2^31-1Little-endian
Int648-2^63 to 2^63-1Little-endian
Int12816-2^127 to 2^127-1Little-endian
Int25632-2^255 to 2^255-1Little-endian
UInt810 to 255Little-endian
UInt1620 to 65535Little-endian
UInt3240 to 2^32-1Little-endian
UInt6480 to 2^64-1Little-endian
UInt128160 to 2^128-1Little-endian
UInt256320 to 2^256-1Little-endian

Floating point types (GA)​

TypeBytesDescriptionNative Protocol
Float324IEEE 754 single precisionIEEE 754 binary
Float648IEEE 754 double precisionIEEE 754 binary
BFloat162Brain floating point (ML)IEEE 754 variant

Decimal types (GA)​

TypePrecisionScaleUnderlying
Decimal(P, S)1-760-PAuto-selected
Decimal32(S)1-90-9Int32
Decimal64(S)10-180-18Int64
Decimal128(S)19-380-38Int128
Decimal256(S)39-760-76Int256

Boolean type (GA)​

TypeUnderlyingValues
BoolUInt80=false, 1=true

String types (GA)​

TypeDescriptionNative Protocol
StringVariable-length, any bytes(len, value) varint + bytes
FixedString(N)Fixed N bytes, zero-paddedN bytes exactly

Date and time types (GA)​

TypeRangeResolutionUnderlying
Date1970-01-01 to 2149-06-061 dayUInt16 (days since epoch)
Date321900-01-01 to 2299-12-311 dayInt32 (days since epoch)
DateTime1970-01-01 to 2106-02-071 secondUInt32 (seconds since epoch)
DateTime(tz)Same + timezone1 secondUInt32 + timezone metadata
DateTime64(P)Wide range10^-P secondsInt64 (scaled)
DateTime64(P, tz)Same + timezone10^-P secondsInt64 + timezone metadata
Time00:00:00 to 23:59:591 secondUInt32
Time64(P)High precision time10^-P secondsInt64

DateTime64 precision:

  • P=0: seconds
  • P=3: milliseconds
  • P=6: microseconds
  • P=9: nanoseconds

UUID type (GA)​

TypeBytesFormat
UUID16RFC 4122, stored as FixedString(16)

Network types (GA)​

TypeBytesFormat
IPv44UInt32, little-endian on the wire
IPv616FixedString(16), network byte order

Enum types (GA)​

TypeUnderlyingMax Values
Enum8('a'=1, 'b'=2, ...)Int8256
Enum16('a'=1, 'b'=2, ...)Int1665536

Container types (GA)​

TypeDescriptionNative Protocol
Array(T)Variable-length arrayOffsets (UInt64[]) + Data (T[])
Tuple(T1, T2, ...)Fixed heterogeneous tupleConcatenated columns
Map(K, V)Key-value pairsOffsets + Keys (K[]) + Values (V[])
Nested(name1 T1, ...)Nested structureFlattened to arrays

Nullable wrapper (GA)​

TypeDescriptionNative Protocol
Nullable(T)T or NULLNulls (UInt8[]) + Values (T[])

LowCardinality wrapper (GA)​

TypeDescriptionNative Protocol
LowCardinality(T)Dictionary encodingIndex + Keys columns

Supported inner types: String, FixedString, Date, DateTime, numbers

Geo types (GA)​

TypeUnderlyingDescription
PointTuple(Float64, Float64)(x, y) coordinate: longitude, latitude
RingArray(Point)Closed polygon ring
LineStringArray(Point)Open line
PolygonArray(Ring)Polygon with holes
MultiLineStringArray(LineString)Multiple lines
MultiPolygonArray(Polygon)Multiple polygons

Semi-structured types​

DFE's floor for these, and for the text index, is ClickHouse 26.2+.

Variant type​

Variant(T1, T2, T3, ...)

Description: Discriminated union of types. Each value is exactly one of the specified types.

Example:

Variant(String, UInt64, Array(String))

Native protocol:

  • Discriminator column (UInt8) indicating which type
  • Separate column for each type variant
  • Only the active variant has data for each row

Dynamic type​

Dynamic
Dynamic(max_types=N)

Description: Can store ANY type without pre-specification. Like Variant but types discovered at runtime.

Features:

  • No need to declare types upfront
  • max_types limits separate storage columns (default 32)
  • Types beyond limit stored as String

Native protocol:

  • Type descriptor column
  • Multiple data columns
  • Overflow column for excess types

JSON type​

JSON
JSON(max_dynamic_paths=N, max_dynamic_types=M)

Description: Native columnar JSON storage. NOT the old Object('json') type.

Features:

  • Paths flattened to subcolumns
  • Dynamic types per path via Variant
  • Typed paths extracted for efficient queries
  • Subpath access: json_column.path.to.field

Parameters:

  • max_dynamic_paths: Limit dynamic path columns (default 1024)
  • max_dynamic_types: Limit types per path (default 32)

Native protocol:

  • Serialised as dynamic column structure
  • Each path becomes separate column
  • Uses Dynamic type internally for varying types

Example:

CREATE TABLE events (
data JSON
) ENGINE = MergeTree ORDER BY tuple();

INSERT INTO events VALUES ('{"user": "alice", "score": 42, "tags": ["a", "b"]}');

SELECT data.user, data.score FROM events;

Aggregate function types (GA)​

TypeDescription
AggregateFunction(name, T1, ...)Intermediate state of aggregate
SimpleAggregateFunction(name, T)Simplified aggregate state

Common aggregates:

  • AggregateFunction(sum, UInt64)
  • AggregateFunction(avg, Float64)
  • AggregateFunction(uniq, String)
  • AggregateFunction(quantile(0.5), Float64)
  • SimpleAggregateFunction(sum, UInt64)
  • SimpleAggregateFunction(max, DateTime)

Deprecated/legacy types​

TypeStatusReplacement
Object('json')DeprecatedUse JSON type

How dfe-loader writes these types​

dfe-loader inserts through the official clickhouse crate from crates.io. The crate keeps insertion typed -- compile-time T: Row schemas -- which is the right upstream default. The loader's rows are not typed at compile time: their columns come from system.columns at runtime, so the dynamic encoding layer comes from dfe-clickhouse's dynamic module, on top of the crate's public surface. See clickhouse-ext.md for where the encoder calls into that surface.

The loader writes in one of two formats, dispatched by clickhouse.insert_format:

FormatPathPurposeDefault
RowBinarydfe_clickhouse::dynamic::DynamicInsert over ClientSchema-reflected typed binary encodingYes
JSONEachRowClient::insert_formatted_with (FORMAT JSONEachRow)Fallback, server-side type coercionNo

DynamicInsert reflects the target schema from system.columns, encodes a Map<String, Value> against that schema column-by-column, and sets input_format_binary_read_json_as_string=1 for JSON-typed columns. The loader never calls clickhouse::Row -- everything is dynamic. DDL and schema queries go through the same client via ClickHouseQueryClient. The full transport x format matrix is in insert-formats.md.

Type support comes from the loader's own encoder, not the client crate. A column type the encoder does not cover (a Variant column, for one) fails the write with DynamicError::UnsupportedType, and the rows are held rather than dead-lettered.


Native protocol serialisation notes​

Numeric types​

All numeric types use little-endian byte order, matching x86/x64 memory layout for zero-copy operations.

String/FixedString​

  • String: Varint length prefix + raw bytes
  • FixedString(N): Exactly N bytes, zero-padded

Arrays​

Offsets: [UInt64 offsets array]
Data: [T elements array]

Offset[i] = end index of array[i] in data

Nullable​

Nulls: [UInt8 mask, 1=null]
Values: [T values, null positions have default]

LowCardinality​

Index type + Keys + Dictionary

DateTime with timezone​

Timezone stored in column metadata, not per-value. All values in a column share the same timezone.

JSON columns on the wire​

A JSON-typed column (including _json) is written as a length-prefixed string. On the RowBinary path the loader sets input_format_binary_read_json_as_string=1 on the insert so the server reads that string back as a JSON value rather than a literal String. The setting is applied automatically whenever the resolved schema contains a JSON column -- no configuration needed. See insert-formats.md.


Encoder implementation status​

RowBinary (default): encoded by dfe_clickhouse::dynamic::DynamicInsert from the reflected schema. It covers the integer, float, decimal, bool, string, date and time, UUID, IP, enum, Array, Map, JSON and geo types, inside Nullable and LowCardinality. Decimal(P, S) resolves to the concrete Decimal32/64/128/256 at encode time based on precision. A value bound for a type the encoder has no arm for (Tuple, Variant, Dynamic, Nested, ...) is refused with UnsupportedType, which holds the batch for retry rather than writing wrong bytes.

JSONEachRow (fallback): the server parses each type itself. Before it does, the loader shapes the values it would refuse by the same rules as RowBinary: a JSON column's non-object, every geo_point form (sent as [lon, lat]), and a single value bound for an Array. Set insert_format = "json_each_row" to opt in (HTTP transport only -- see the config guard in insert-formats.md).

Geo points​

A Point is written as two little-endian Float64, x then y, where x is the longitude and y the latitude. The value can arrive in any form Elasticsearch takes for a geo_point:

FormExampleCoordinate order
Object{"lat": 41.12, "lon": -71.34}named
GeoJSON{"type": "Point", "coordinates": [-71.34, 41.12]}lon, lat
String"41.12,-71.34"lat, lon
Array[-71.34, 41.12]lon, lat
WKT"POINT (-71.34 41.12)"lon, lat

A third coordinate is read and ignored. A geohash, a latitude outside -90 to 90, a longitude outside -180 to 180, or a value no form reads is an encoding error for that row alone: salvage isolates it and it goes to the DLQ with the reason. An absent Point is (0, 0), the type's own default. ClickHouse refuses Nullable(Point), so there is no null to write instead.

Ring, LineString, Polygon, MultiLineString and MultiPolygon are arrays of points on the wire, written from nested arrays whose innermost elements take any form above. An absent one is the empty array.

A single value bound for an Array​

Elasticsearch lets any field carry one value or many, so a scalar bound for an Array(T) column is written as a one-element array, and T's own rules then decide whether it is valid. "related_ip": "52.108.0.3" lands in an Array(IPv6) column as ['::ffff:52.108.0.3']. An object is wrapped only where the element is read from one (JSON, Map, Point). Bound for an array of scalars, it stays an encoding error. A null or absent array is the empty array. For an Array(Point) column, a bare [lon, lat] is one point, as Elasticsearch reads it.

The rationale for RowBinary as the default: schema-reflected RowBinary skips the server-side JSON parse, cuts ClickHouse CPU, and gives full type support without requiring compile-time schemas. Records stay a Map<String, Value> from intake to insert -- encoding happens against system.columns at runtime.