Introduced in: v26.8.0
Aggregates JSON values by merging them with last-write-wins semantics, implementing the core merge behavior of RFC 7396 JSON Merge Patch at the path level.
The aggregate function stores state as triplets (key, value, sorting_key) where each key (JSON path)
only keeps the latest effective record according to the sorting_key. Object writes are flattened into
descendant paths for paths that the JSON type stores as dynamic scalar leaves. Ancestor non-object
writes shadow conflicting descendants.
Every explicitly typed path (declared with JSON(a Map(...)), JSON(a Tuple(...)),
JSON(a Variant(...)), JSON(a Dynamic), etc.) is treated as an atomic value: the whole path value
is replaced by the newer patch rather than deep-merged. Only untyped (dynamic) scalar paths follow
RFC 7396 deep-merge semantics.
The sort_key determines which value wins for each JSON path. The row with the largest sort_key is
retained. If two conflicting patches have equal sort keys, the result is order-dependent: the patch
processed later wins the tie. Users should not rely on ORDER BY to break ties deterministically.
LIMITATIONS (inherited from ColumnObject):
-
Null deletion: a patch
{"key": null}does not remove the key.ColumnObjectdrops null-valued members on insertion, so the function cannot distinguish “key absent” from “key is null”. -
Empty-object replacement: a patch
{"a": {}}cannot displace an older scalar or array at patha.ColumnObjectsilently drops paths whose value is an empty object{}, so the newer patch contributes nothing and the old value survives. -
Non-Nullable typed-path absence: when a
JSONcolumn declares a typed path with a non-nullable type (e.g.,JSON(a UInt32)), a row that omitsais stored with the type default value (e.g.,0). The aggregate cannot tell “absent” from “explicitly written as the default”, so a newer patch that omitsasilently erases an older non-zero value. To avoid this, declare typed paths asNullable(e.g.,JSON(a Nullable(UInt32))). A null in a nullable typed path is treated as “path absent” and is correctly skipped. -
All typed paths are atomic: every typed path (
Map(K,V),JSON,Dynamic,Tuple(…),Variant(…),Array(…), or any other declared type) is stored as a single value. The aggregate replaces the entire value atomically rather than deep-merging its contents. Only dynamic (untyped, scalar) paths are deep-merged path-by-path. -
Dot-in-key ambiguity: the
JSONtype represents{"a":{"b":1}}and{"a.b":1}with the same internal patha.b. A single row can therefore expose bothaanda.bas independent peers. When a newer patch writes onlya, the ancestor/descendant conflict rule erasesa.b; when it writes onlya.b, the same rule erasesa. To avoid this, setjson_type_escape_dots_in_keys = 1. With this setting, literal dots in JSON keys are percent-encoded (e.g.a.bbecomesa%2Eb), making them distinct from nested paths and eliminating the false conflict.
Syntax
mergedJSONPatch(json, sort_key)Arguments
json— JSON column to aggregate.JSONsort_key— Comparable column that determines which write wins for each path. The row with the largest sort_key value is retained.
Returned value
Returns a single JSON object that is the result of merging all input JSON objects. JSON
Examples
Basic usage with sort key
SELECT mergedJSONPatch(json, sort_key) FROM
(
SELECT '{"a":1}'::JSON AS json, 1 AS sort_key
UNION ALL
SELECT '{"b":2}'::JSON, 2
UNION ALL
SELECT '{"a":3, "c":4}'::JSON, 3
);┌─mergedJSONPatch(json, sort_key)─┐
│ {"a":3,"b":2,"c":4} │
└─────────────────────────────────┘