- ClickHouse introduce un nuevo tipo JSON que coloca los valores de cada ruta JSON en un almacenamiento verdaderamente columnar para evitar el cuello de botella de guardar documentos JSON como cadenas y parsearlos cada vez
- El núcleo de la implementación son los tipos Variant y Dynamic, y aunque en una misma ruta JSON entren tipos distintos como enteros, cadenas o arreglos, no los fuerza a unificarse en un tipo común mínimo
max_dynamic_paths tiene un valor predeterminado de 1024 y max_dynamic_types de 32, lo que limita la cantidad de subcolumnas y archivos por tipo para controlar el aumento de descriptores de archivo y del costo de los merges
- Con type hints,
SKIP y SKIP REGEXP se puede ajustar cómo se almacena cada ruta, y los valores pueden leerse con sintaxis de subcolumnas como C.a.b
- El nuevo tipo busca reemplazar al ya deprecated
Object('json'), y en la hoja de ruta aún quedan mejoras como usar rutas de claves JSON en la clave primaria o en data-skipping indexes
El reto de adaptar JSON a un almacenamiento columnar
- JSON se usa como formato común para manejar datos semiestructurados y no estructurados en logs, observabilidad, streaming de datos en tiempo real, almacenamiento de apps móviles y pipelines de machine learning
- ClickHouse es una base de datos verdaderamente orientada a columnas que guarda una tabla como un conjunto de archivos de datos por columna en disco para realizar compresión, filtrado vectorizado y agregaciones
- Para lograr ese mismo rendimiento con JSON, en vez de guardar el documento en una columna de texto y parsearlo después, hay que almacenar los valores de cada ruta JSON única como si fueran columnas
Cuatro restricciones que aborda el nuevo tipo JSON
-
Almacenamiento columnar por ruta
- Los valores de cada ruta JSON también deben poder comprimirse y filtrarse/agregarse de forma vectorizada, igual que una columna común como una numérica
-
Tipos que cambian dinámicamente
- En una misma ruta JSON
a pueden entrar tipos distintos como entero, flotante o arreglo
- ClickHouse no puede saber esto de antemano y los tipos incluso pueden no ser compatibles entre sí, así que unirlos en un tipo común mínimo puede causar pérdida de información
-
Evitar la explosión de archivos de columnas
- Si se crea un nuevo archivo de columna por cada ruta JSON nueva, la cantidad de archivos en disco se dispara en datos con muchas claves únicas
- Los file descriptors consumen memoria, y si hay demasiados archivos que procesar también se ve afectado el rendimiento de los merges
-
Almacenamiento denso para claves dispersas
- Cuando hay muchas claves JSON únicas pero dispersas, no se deben guardar repetidamente
NULL o valores por defecto en cada fila que no tenga valor
- Solo los valores reales deben guardarse en almacenamiento denso para poder escalar incluso en análisis a nivel de PB
Tipo Variant: la base para no forzar la unificación de tipos
- El tipo de datos Variant es una funcionalidad independiente que puede usarse aparte de JSON, y permite guardar y leer valores de distintos tipos de datos dentro de una sola columna de tabla
- Las columnas tradicionales de ClickHouse tienen un tipo fijo, y los valores insertados deben coincidir con ese tipo o convertirse implícitamente
- Una columna
Nullable usa un archivo de máscara NULL además del archivo de valores
Array guarda el tamaño del arreglo en un archivo separado y con eso calcula los offsets
- Una columna Variant guarda los valores del mismo tipo concreto en subcolumnas separadas por tipo
- Ejemplo: todos los valores
Int64 se guardan en C.Int64.bin, y todos los String en C.String.bin
- El tipo que usa cada fila se rastrea con una columna discriminator de
UInt8
- El valor del discriminator es el índice dentro de una lista ordenada de nombres de tipo
- El discriminator
255 está reservado para NULL
- Por este diseño, Variant puede tener como máximo 255 tipos concretos
- Los archivos de datos por tipo usan una estructura de almacenamiento denso que solo contiene filas con valor
- Los archivos por tipo no guardan valores
NULL
- Para encontrar la posición de una fila dentro del archivo de su tipo real desde la fila del discriminator, se usa en memoria una columna de offsets
UInt64
- Ese offset no se guarda en disco y puede generarse al vuelo a partir del archivo de la columna discriminator
- Variant admite anidamiento arbitrario
- El orden de tipos en
Variant(T1, T2) y Variant(T2, T1) tiene el mismo significado
- También se puede anidar un Variant dentro de otro Variant
- Los valores de un tipo anidado específico se leen agregando el nombre del tipo como subcolumna
Tipo Dynamic: guardar sin conocer de antemano la lista de tipos
- El tipo Dynamic es una funcionalidad independiente implementada sobre Variant, y puede usarse también fuera del contexto de JSON
- Dynamic agrega dos capacidades sobre Variant
- Permite guardar valores de tipos arbitrarios dentro de una sola columna sin declarar antes la lista de tipos
- Permite limitar cuántos tipos se guardan en archivos de datos de columna separados
- El almacenamiento interno es igual al de Variant, pero se añade el archivo
C.dynamic_structure.bin
- Ese archivo contiene la lista de tipos guardados como subcolumnas y estadísticas del tamaño de los archivos de datos por tipo
- Esa metadata se usa para leer subcolumnas y para los merges de data parts
Dynamic(max_types=N) limita la cantidad de tipos que se guardan en archivos separados
0 <= N < 255
- El valor predeterminado es 32
- Cuando se alcanza el límite, los valores de los demás tipos se guardan en un solo archivo de columna como
C.SharedVariant.bin
- El tipo de ese archivo es
String
- Cada fila contiene un valor de cadena con la estructura
<binary_encoded_data_type><binary_value>
- Esto permite guardar y volver a leer valores de varios tipos dentro de un único archivo de columna
- Dynamic también permite leer valores de un tipo específico usando nombres de tipo como subcolumnas, igual que Variant
Declaración del tipo JSON y estructura de almacenamiento
- El nuevo tipo JSON permite guardar objetos JSON de estructura arbitraria y leer cada valor JSON como una subcolumna basada en rutas
- La declaración del tipo puede incluir parámetros opcionales y hints
<column_name> JSON(
max_dynamic_paths=N,
max_dynamic_types=M,
some.path TypeName,
SKIP path.to.skip,
SKIP REGEXP 'paths_regexp')
max_dynamic_paths
- El valor predeterminado es 1024
- Define cuántas rutas de claves JSON se guardan como subcolumnas separadas
- Las rutas que exceden ese límite se guardan juntas en una sola subcolumna con una estructura especial
max_dynamic_types
- El valor predeterminado es 32
- Su rango de valores va de
0 a 254
- Define cuántos tipos de datos en una columna de ruta JSON se guardan en archivos de datos separados
- Los tipos nuevos que exceden el límite se guardan juntos en un solo archivo especial de datos de columna
some.path TypeName
- Es un type hint para una ruta JSON específica
- Esa ruta siempre se guarda como subcolumna del tipo indicado, lo que ofrece una garantía de rendimiento
SKIP path.to.skip
- Omite una ruta JSON específica durante el parseo
- Esa ruta no se guarda en la columna JSON
- Si la ruta indicada es un objeto JSON anidado, se omite todo el objeto anidado
SKIP REGEXP 'path_regexp'
- Omite durante el parseo las rutas que coincidan con la expresión regular
- Las rutas coincidentes no se guardan en la columna JSON
Cómo se leen las rutas JSON como columnas
- El valor de cada ruta leaf única de una columna JSON se guarda en disco de una de estas dos maneras
- Las rutas con type hint se guardan como archivos de datos de columnas normales
- Las rutas cuyo tipo puede cambiar dinámicamente se guardan como subcolumnas Dynamic
- El tipo JSON usa un archivo especial llamado
object_structure
- Contiene metadata sobre rutas dinámicas
- Contiene estadísticas de valores no nulos para cada ruta dinámica
- Se usa para leer subcolumnas y para los merges de data parts
- La explosión de archivos de columnas se controla con un límite de dos niveles
max_dynamic_types limita cuántos tipos se guardan en archivos separados dentro de una misma ruta de clave JSON
max_dynamic_paths limita cuántas rutas de clave JSON se guardan como subcolumnas separadas
- Las rutas JSON dinámicas adicionales que exceden el límite de
max_dynamic_paths se guardan como shared data
- Algunos archivos de ejemplo son
C.object_shared_data.size0.bin, C.object_shared_data.paths.bin y C.object_shared_data.values.bin
object_shared_data.values es de tipo String
- Cada entrada tiene la estructura
<binary_encoded_data_type><binary_value>
- Incluso para shared data se guardan estadísticas adicionales en
object_structure.bin
- Actualmente se guardan estadísticas de valores no nulos para las primeras 10000 rutas entre las que estén almacenadas en la columna shared data
Sintaxis de rutas JSON y objetos anidados
- El tipo JSON permite leer el valor leaf de cada ruta usando subcolumnas basadas en el nombre de la ruta
- Los valores de rutas sin type hint siempre tienen tipo Dynamic
- Ejemplo: el tipo de
C.a.d es Dynamic
- Las subcolumnas de subtipos dentro de Dynamic se leen con una sintaxis JSON especial
- Los objetos JSON anidados pueden leerse como subcolumnas de tipo JSON usando la sintaxis
JSON_column.^some.path
- Por motivos de rendimiento, la sintaxis con punto actualmente no lee objetos anidados
- La estructura de almacenamiento actual es eficiente para leer valores literales por ruta
- Leer subobjetos completos por ruta puede requerir leer más datos y volverse más lento
- Para devolver objetos se necesita la sintaxis
.^
- ClickHouse planea unificar ambas sintaxis
.
Serialización compacta del discriminator
- Muchas rutas JSON dinámicas pueden tener casi siempre el mismo tipo de valor
- Si hay muchas rutas JSON únicas pero dispersas, el archivo discriminator de cada ruta contendrá sobre todo
255, es decir, valores NULL
- Aunque esos archivos se comprimen bien, si todos los valores de todas las filas son iguales sigue habiendo mucha redundancia
- ClickHouse implementó un formato compacto para la serialización del discriminator
- En vez de escribir normalmente todos los valores
UInt8 del discriminator, si todos los discriminator de un granule objetivo son iguales solo se serializan 3 valores
- Un indicador de formato compact granule
- Un indicador de cantidad de valores de ese granule
- El valor del discriminator
- Esta optimización se controla con la configuración de MergeTree
use_compact_variant_discriminators_serialization
- Está activada por defecto
- Con la granularidad de índice habitual, puede haber casos donde se guarden solo 3 valores en lugar de 8192 valores
Estado de la versión y próximos pasos
- El nuevo tipo JSON fue diseñado para reemplazar el tipo Object('json'), que ya está deprecated
- La implementación se ofrece como funcionalidad experimental con fines de prueba en el lanzamiento ClickHouse 24.08
- La hoja de ruta de JSON incluye mejoras para usar rutas de claves JSON dentro de la clave primaria de la tabla o en data-skipping indexes
- Componentes como Variant y Dynamic también sirven de base para soportar otros tipos semiestructurados además de JSON, como XML y YAML
- Si un usuario de ClickHouse Cloud quiere probar el nuevo tipo de datos JSON, debe contactar al equipo de soporte de ClickHouse para solicitar acceso a la private preview
Aún no hay comentarios.