Skip to main content
A continuación se presentan alternativas para modelar JSON en ClickHouse. Se documentan por exhaustividad y eran aplicables antes del desarrollo del tipo JSON, por lo que, en general, no se recomiendan ni resultan adecuadas en la mayoría de los casos de uso.
Aplique un enfoque a nivel de objetoSe pueden aplicar distintas técnicas a diferentes objetos dentro del mismo esquema. Por ejemplo, algunos objetos pueden resolverse mejor con un tipo String y otros con un tipo Map. Tenga en cuenta que, una vez que se usa un tipo String, ya no es necesario tomar más decisiones sobre el esquema. Por el contrario, es posible anidar subobjetos dentro de una clave Map, incluido un String que represente JSON, como se muestra a continuación:

Uso del tipo String

Si los objetos son muy dinámicos, no tienen una estructura predecible y contienen objetos anidados arbitrarios, debe usar el tipo String. Los valores pueden extraerse en tiempo de consulta mediante funciones JSON, como se muestra a continuación. Gestionar datos con el enfoque estructurado descrito anteriormente a menudo no es viable para aquellos usuarios que trabajan con JSON dinámico, ya sea porque está sujeto a cambios o porque su esquema no se comprende bien. Para disponer de la máxima flexibilidad, puede simplemente almacenar el JSON como String y después usar funciones para extraer los campos según sea necesario. Esto representa el extremo opuesto de tratar el JSON como un objeto estructurado. Esta flexibilidad tiene un coste y conlleva desventajas importantes, principalmente un aumento de la complejidad de la sintaxis de las consultas, así como una degradación del rendimiento. Como se señaló antes, para el objeto person original, no podemos garantizar la estructura de la columna tags. Insertamos la fila original (incluyendo company.labels, que por ahora ignoramos), declarando la columna Tags como un String:
Podemos seleccionar la columna tags y ver que el JSON se insertó como una cadena:
Las funciones JSONExtract pueden usarse para extraer valores de este JSON. Considere el ejemplo sencillo que aparece a continuación:
Observe cómo las funciones requieren tanto una referencia a la columna tags de tipo String como una ruta dentro del JSON de la que extraer el valor. Las rutas anidadas requieren anidar funciones; p. ej., JSONExtractUInt(JSONExtractString(tags, 'car'), 'year'), que extrae tags.car.year. La extracción de rutas anidadas puede simplificarse mediante las funciones JSON_QUERY y JSON_VALUE. Considere el caso extremo del conjunto de datos arxiv, donde consideramos todo el cuerpo como un String.
Para insertar datos en este esquema, debemos usar el formato JSONAsString:
Supongamos que queremos contar el número de artículos publicados por año. Compare la siguiente consulta usando solo una cadena con la versión estructurada del esquema:
Tenga en cuenta el uso de una expresión XPath aquí para filtrar el JSON por método; es decir, JSON_VALUE(body, '$.versions[0].created'). Las funciones de String son considerablemente más lentas (> 10x) que las conversiones explícitas de tipos con índices. Las consultas anteriores siempre requieren un escaneo completo de la tabla y procesar cada fila. Aunque estas consultas seguirán siendo rápidas en un conjunto de datos pequeño como este, el rendimiento se degradará en conjuntos de datos más grandes. La flexibilidad de este enfoque tiene un claro coste en términos de rendimiento y sintaxis, y solo debe utilizarse para objetos muy dinámicos en el esquema.

Funciones JSON simples

Los ejemplos anteriores usan la familia de funciones JSON*. Estas utilizan un parser JSON completo basado en simdjson, que realiza un parsing riguroso y distingue entre el mismo campo anidado en distintos niveles. Estas funciones pueden manejar JSON sintácticamente correcto pero mal formateado; por ejemplo, con espacios dobles entre claves. Hay disponible un conjunto de funciones más rápido y estricto. Estas funciones simpleJSON* ofrecen un rendimiento potencialmente superior, principalmente porque asumen de forma estricta la estructura y el formato del JSON. En concreto:
  • Los nombres de los campos deben ser constantes
  • Codificación coherente de los nombres de los campos; por ejemplo, simpleJSONHas('{"abc":"def"}', 'abc') = 1, pero visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0
  • Los nombres de los campos son únicos en todas las estructuras anidadas. No se distingue entre niveles de anidamiento y la coincidencia se realiza de forma indiscriminada. Si hay varios campos coincidentes, se usa la primera aparición.
  • No se permiten caracteres especiales fuera de los literales de cadena. Esto incluye los espacios. Lo siguiente no es válido y no se podrá parsear.
En cambio, lo siguiente se parseará correctamente:
La consulta anterior usa simpleJSONExtractString para extraer la clave created, aprovechando que para la fecha de publicación solo nos interesa el primer valor. En este caso, las limitaciones de las funciones simpleJSON* son aceptables a cambio de la mejora del rendimiento.

Uso del tipo Map

Si el objeto se usa para almacenar claves arbitrarias, en su mayoría de un mismo tipo, considere usar el tipo Map. Idealmente, el número de claves únicas no debería superar unos pocos cientos. El tipo Map también puede considerarse para objetos con subobjetos, siempre que estos tengan tipos uniformes. En general, recomendamos usar el tipo Map para labels y tags; por ejemplo, labels de pods de Kubernetes en datos de logs. Aunque los Map ofrecen una forma sencilla de representar estructuras anidadas, tienen algunas limitaciones importantes:
  • Todos los campos deben ser del mismo tipo.
  • El acceso a subcolumnas requiere una sintaxis especial para mapas, ya que los campos no existen como columnas. El objeto completo es una columna.
  • Acceder a una subcolumna carga el valor completo del Map, es decir, todos los elementos del mismo nivel y sus respectivos valores. En mapas más grandes, esto puede tener un impacto significativo en el rendimiento.
Claves StringAl modelar objetos como Map, se usa una clave String para almacenar el nombre de la clave JSON. Por lo tanto, el mapa siempre será Map(String, T), donde T depende de los datos.

Valores primitivos

La aplicación más sencilla de un Map se da cuando el objeto contiene valores del mismo tipo primitivo. En la mayoría de los casos, esto implica usar el tipo String para el valor T. Considera nuestro JSON anterior de una persona, donde se determinó que el objeto company.labels era dinámico. Es importante destacar que solo esperamos que se agreguen a este objeto pares clave-valor de tipo String. Por lo tanto, podemos declararlo como Map(String, String):
Podemos insertar nuestro objeto JSON original completo:
Para consultar estos campos dentro del objeto de solicitud, es necesario usar la sintaxis de map, por ejemplo:
Hay disponible un conjunto completo de funciones de Map para consultar este tipo, descritas aquí. Si sus datos no son de un tipo consistente, existen funciones para realizar la coerción de tipos necesaria.

Valores de objetos

El tipo Map también puede considerarse para objetos que tienen subobjetos, siempre que estos últimos mantengan consistencia en sus tipos. Supóngase que la clave tags de nuestro objeto persons requiere una estructura consistente, en la que el subobjeto de cada tag tenga las columnas name y time. Un ejemplo simplificado de un documento JSON de este tipo podría verse así:
Esto se puede representar con un Map(String, Tuple(name String, time DateTime)), como se muestra a continuación:
El uso de Map en este caso suele ser poco frecuente y sugiere que los datos deberían remodelarse para que los nombres de clave dinámicos no tengan subobjetos. Por ejemplo, lo anterior podría remodelarse de la siguiente manera, lo que permite usar Array(Tuple(key String, name String, time DateTime)).

Uso del tipo Nested

El tipo Nested puede usarse para modelar objetos estáticos que rara vez cambian, como alternativa a Tuple y Array(Tuple). En general, recomendamos evitar el uso de este tipo para JSON, ya que su comportamiento suele ser confuso. La principal ventaja de Nested es que las subcolumnas pueden utilizarse en las claves de ordenación. A continuación, mostramos un ejemplo de cómo usar el tipo Nested para modelar un objeto estático. Considere la siguiente entrada de registro sencilla en JSON:
Podemos declarar la clave request como Nested. Al igual que con Tuple, es necesario especificar las subcolumnas.

flatten_nested

La configuración flatten_nested controla el comportamiento del tipo Nested.

flatten_nested=1

Un valor de 1 (el valor predeterminado) no admite un nivel arbitrario de anidamiento. Con este valor, la forma más sencilla de entender una estructura de datos anidada es como varias columnas Array de la misma longitud. En la práctica, los campos method, path y version son columnas Array(Type) independientes, con una restricción fundamental: la longitud de los campos method, path y version debe ser la misma. Esto se ilustra con SHOW CREATE TABLE:
A continuación, insertamos datos en esta tabla:
Algunos puntos importantes que conviene tener en cuenta aquí:
  • Debemos usar la configuración input_format_import_nested_json para insertar el JSON como una estructura anidada. Sin esto, tendríamos que aplanar el JSON, es decir:
  • Los campos anidados method, path y version deben pasarse como arrays de JSON, es decir:
Las columnas pueden consultarse mediante notación de puntos:
Tenga en cuenta que el uso de Array para las subcolumnas significa que puede aprovecharse potencialmente todo el abanico de funciones de arrays, incluida la cláusula ARRAY JOIN, lo cual resulta útil si sus columnas tienen varios valores.

flatten_nested=0

Esto permite un nivel arbitrario de anidamiento y significa que las columnas anidadas se mantienen como un único array de Tuples; en la práctica, pasan a ser lo mismo que Array(Tuple). Esta es la forma preferida y, a menudo, la más sencilla de usar JSON con Nested. Como mostramos a continuación, solo requiere que todos los objetos sean una lista. A continuación, volvemos a crear nuestra tabla y volvemos a insertar una fila:
Algunos puntos importantes que conviene tener en cuenta aquí:
  • input_format_import_nested_json no es necesario para insertar datos.
  • El tipo Nested se conserva en SHOW CREATE TABLE. En la práctica, esta columna es en realidad un Array(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String))))
  • Como resultado, debemos insertar request como un array, es decir:
Las columnas pueden volver a consultarse mediante notación de punto:

Ejemplo

Hay un ejemplo más amplio de los datos anteriores disponible en un bucket público de S3 en: s3://datasets-documentation/http/.
Dadas las restricciones y el formato de entrada de JSON, insertamos este conjunto de datos de ejemplo con la siguiente consulta. Aquí, configuramos flatten_nested=0. La siguiente sentencia inserta 10 millones de filas, por lo que puede tardar unos minutos en ejecutarse. Aplique un LIMIT si es necesario:
Para consultar estos datos, debemos acceder a los campos de la solicitud como arrays. A continuación, resumimos los errores y los métodos HTTP durante un intervalo de tiempo fijo.

Uso de arrays por pares

Los arrays por pares ofrecen un equilibrio entre la flexibilidad de representar JSON como Strings y el rendimiento de un enfoque más estructurado. El esquema es flexible, ya que se puede añadir potencialmente cualquier campo nuevo en la raíz. Sin embargo, esto requiere una sintaxis de consulta considerablemente más compleja y no es compatible con estructuras anidadas. Como ejemplo, considere la siguiente tabla:
Para insertar datos en esta tabla, debemos estructurar el JSON como una lista de claves y valores. La siguiente consulta ilustra cómo usar JSONExtractKeysAndValues para lograrlo:
Observa cómo la columna request sigue siendo una estructura anidada representada como una cadena. Podemos añadir nuevas claves en la raíz. El propio JSON también puede tener diferencias arbitrarias. Para insertar en nuestra tabla local, ejecuta lo siguiente:
Consultar esta estructura requiere usar la función indexOf para identificar el índice de la clave requerida (que debe ser coherente con el orden de los valores). Esto permite acceder a la columna de Array values, es decir, values[indexOf(keys, 'status')]. Seguimos necesitando un método de análisis de JSON para la columna request; en este caso, simpleJSONExtractString.
Última modificación el 3 de julio de 2026