Skip to main content
El Client oficial de C# para conectarse a ClickHouse. El código fuente del Client está disponible en el repositorio de GitHub. Desarrollado originalmente por Oleg V. Kozlyuk. La biblioteca ofrece dos API principales:
  • ClickHouseClient (recomendado): Un Client de alto nivel, seguro para subprocesos y diseñado para utilizarse como instancia única. Ofrece una API asíncrona sencilla para realizar consultas e inserciones masivas. Es la mejor opción para la mayoría de las aplicaciones.
  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): Abstracciones estándar de .NET para bases de datos. Son necesarias para integrar ORM (Dapper, Linq2db) y cuando se requiere compatibilidad con ADO.NET. ClickHouseBulkCopy es una clase auxiliar que permite insertar datos de forma eficiente mediante una conexión ADO.NET. ClickHouseBulkCopy está obsoleta y se eliminará en una versión futura; utilice ClickHouseClient.InsertBinaryAsync en su lugar.
Ambas API comparten el mismo pool de conexiones HTTP y pueden utilizarse juntas en una misma aplicación.

Guía de migración

  1. Actualice el archivo .csproj para usar el nuevo nombre de paquete ClickHouse.Driver y la versión más reciente disponible en NuGet.
  2. Sustituya todas las referencias a ClickHouse.Client por ClickHouse.Driver en su base de código.

Versiones de .NET compatibles

ClickHouse.Driver es compatible con las siguientes versiones de .NET:
  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

Versiones compatibles de ClickHouse

El Client es oficialmente compatible con las tres versiones más recientes, además de las dos últimas versiones LTS.

Instalación

Instale el paquete desde NuGet:
O mediante el Administrador de paquetes NuGet:

Inicio rápido

Configuración

Puede configurar la conexión a ClickHouse de dos maneras:
  • Cadena de conexión: Pares de clave y valor separados por punto y coma que especifican el host, las credenciales de autenticación y otras opciones de conexión.
  • Objeto ClickHouseClientSettings: Un objeto de configuración fuertemente tipado que puede cargarse desde archivos de configuración o definirse en el código.
A continuación se enumeran todos los parámetros de configuración, sus valores predeterminados y sus efectos.

Configuración de la conexión

Formato de datos y serialización

Gestión de sesiones

El indicador UseSession permite mantener la sesión en el servidor, lo que hace posible usar sentencias SET y tablas temporales. Las sesiones se restablecen tras 60 segundos de inactividad (el tiempo de espera predeterminado). La duración de la sesión puede ampliarse ajustando su configuración mediante sentencias de ClickHouse o la configuración del servidor.La clase ClickHouseConnection normalmente permite ejecutar consultas en paralelo desde varios threads. Sin embargo, al habilitar el indicador UseSession, solo podrá haber una consulta activa por conexión en un momento dado (una limitación del lado del servidor).

Seguridad

Configuración del Client HTTP

Registro y depuración

Ajustes personalizados y roles

Si utiliza una cadena de conexión para establecer ajustes personalizados, añada el prefijo set_, por ejemplo, “set_max_threads=4”. Si utiliza un objeto ClickHouseClientSettings, no añada el prefijo set_.Para consultar la lista completa de ajustes disponibles, visite esta página.

Ejemplos de cadenas de conexión

Conexión básica

Con ajustes personalizados de ClickHouse


QueryOptions

QueryOptions permite sobrescribir la configuración del Client para cada consulta. Todas las propiedades son opcionales y, si no se especifican, se mantienen los valores predeterminados del Client. Ejemplo:

InsertOptions

InsertOptions amplía QueryOptions con opciones específicas para las inserciones masivas mediante InsertBinaryAsync. Todas las propiedades de QueryOptions también están disponibles en InsertOptions. Ejemplo:

Omitir la consulta preliminar del esquema

De forma predeterminada, InsertBinaryAsync envía una consulta SELECT ... WHERE 1=0 antes de cada inserción para detectar los tipos de columna. En escenarios de alto caudal, puede evitar este coste adicional de dos maneras: Opción 1: Indicar explícitamente los tipos de columna Si conoce el esquema de la tabla en tiempo de compilación, proporciónelo directamente mediante ColumnTypes. De este modo, no se envía ninguna consulta para obtener el esquema:
Opción 2: Guardar el esquema en caché Si inserta datos varias veces en la misma tabla, establezca UseSchemaCache = true para consultar el esquema una sola vez y reutilizarlo en las inserciones posteriores desde la misma instancia de ClickHouseClient:
  • ColumnTypes tiene prioridad sobre UseSchemaCache. Si se establecen ambos, se utilizan los tipos explícitos.
  • La caché de esquemas no detecta los cambios realizados mediante ALTER TABLE. Si modifica el esquema de la tabla, cree un nuevo ClickHouseClient o no utilice UseSchemaCache para esa tabla.
  • La caché está asociada a la instancia de ClickHouseClient y utiliza como clave el par (base de datos, tabla). Los distintos subconjuntos de columnas de una misma tabla comparten un único esquema almacenado en caché.

ClickHouseClient

ClickHouseClient es la API recomendada para interactuar con ClickHouse. Es seguro para subproceso, está diseñado para utilizarse como singleton y gestiona internamente el pool de conexiones HTTP.

Creación de un Client

Cree un ClickHouseClient mediante una cadena de conexión o un objeto ClickHouseClientSettings. Consulte las opciones disponibles en la sección Configuración. Los datos de conexión de su servicio de ClickHouse Cloud están disponibles en la consola de ClickHouse Cloud. Seleccione un servicio y haga clic en Conectar: Elija C#. Los datos de conexión aparecerán debajo. Si utiliza ClickHouse autogestionado, el administrador de ClickHouse establece los datos de conexión. Para usar una cadena de conexión:
O mediante ClickHouseClientSettings:
En escenarios de inyección de dependencias, use IHttpClientFactory:
ClickHouseClient está diseñado para tener una vida útil prolongada y compartirse en toda la aplicación. Créelo una sola vez (normalmente como un singleton) y reutilícelo para todas las operaciones de la base de datos. El Client gestiona internamente el pool de conexiones HTTP.

Ejecución de consultas

Use ExecuteNonQueryAsync para ejecutar sentencias que no devuelven resultados:
Use ExecuteScalarAsync para obtener un único valor:

Inserción de datos

Inserciones parametrizadas

Inserte datos mediante consultas parametrizadas con ExecuteNonQueryAsync. Los tipos de los parámetros deben indicarse en la consulta SQL con la sintaxis {name:Type}:

Inserciones masivas

Utilice InsertBinaryAsync para insertar grandes cantidades de filas de forma eficiente. Transmite los datos en el formato binario nativo de filas de ClickHouse, permite subir lotes en paralelo y evita los errores “URL too long” que pueden producirse con las consultas parametrizadas.
Para conjuntos de datos grandes, configure la inserción por lotes y el paralelismo mediante InsertOptions:
  • El Client obtiene automáticamente la estructura de la tabla mediante SELECT * FROM <table> WHERE 1=0 antes de insertar datos. Los valores proporcionados deben coincidir con los tipos de las columnas de destino. Para evitar esta consulta, use InsertOptions.ColumnTypes o InsertOptions.UseSchemaCache.
  • Cuando MaxDegreeOfParallelism > 1, los lotes se cargan en paralelo. Las sesiones no son compatibles con la inserción en paralelo; desactive las sesiones o establezca MaxDegreeOfParallelism = 1.
  • Use RowBinaryFormat.RowBinaryWithDefaults en InsertOptions.Format si desea que el servidor aplique los valores DEFAULT a las columnas para las que no se hayan proporcionado valores.

Inserciones de POCO

En lugar de crear arrays object[], puede insertar directamente objetos POCO fuertemente tipados. Registre el tipo una sola vez y después pase IEnumerable<T>:
De forma predeterminada, todas las propiedades públicas que se pueden leer se asignan a columnas mediante una coincidencia estricta de nombres que distingue entre mayúsculas y minúsculas. Puede personalizar esta asignación mediante atributos:
Si todas las propiedades mapeadas especifican un Type explícito, se omite por completo la consulta preliminar del esquema. Si solo algunas propiedades tienen tipos explícitos, el controlador recurre a esa consulta para el conjunto completo de columnas. InsertBinaryAsync<T> admite las mismas opciones de InsertOptions (procesamiento por lotes, paralelismo y almacenamiento en caché del esquema) que la sobrecarga de object[].
A diferencia de la sobrecarga de object[], InsertBinaryAsync<T> no acepta una lista explícita de columnas. Las columnas se determinan a partir de las propiedades mapeadas del tipo registrado. Para controlar qué columnas se insertan, use [ClickHouseNotMapped] para excluir propiedades o [ClickHouseColumn(Name = "...")] para cambiarles el nombre.Si se establece ColumnTypes en InsertOptions, sus valores prevalecen sobre los atributos del POCO.

Evolución del esquema

Las inserciones POCO siguen funcionando sin problemas si se añaden columnas a la tabla de destino después de registrar el tipo. Como el controlador solo inserta las columnas mapeadas por el POCO, el servidor completa automáticamente las columnas nuevas que tengan DEFAULT u otra expresión de valor predeterminado. No es necesario modificar el código ni volver a registrar el tipo.

Posición de la sentencia INSERT

Una inserción binaria coloca la sentencia INSERT INTO ... FORMAT ... en la primera línea del cuerpo de la solicitud, antes de las filas. El cuerpo se comprime de forma predeterminada, por lo que los sistemas de enrutamiento y registro que solo inspeccionan la URL no pueden ver la sentencia. Establezca InsertOptions.QueryPlacement en InsertQueryPlacement.Url para enviar la sentencia como el parámetro de URL query y reservar el cuerpo exclusivamente para las filas:
Úselo cuando un proxy, balanceador de carga o gateway enrute o inspeccione las solicitudes en función del parámetro query, o cuando quiera que la sentencia quede registrada en los logs de acceso y en las herramientas de observabilidad. Esta opción debe habilitarse explícitamente porque la sentencia pasa a contar para el límite de longitud de la URL. El límite efectivo es el menor de los impuestos por el entorno de ejecución de .NET, cualquier intermediario y el servidor. Entre .NET 6 y .NET 9, System.Uri limita la URI completa y codificada de la solicitud a 65,519 caracteres; si se supera ese límite, el controlador genera una InvalidOperationException que indica que debe volver a usar InsertQueryPlacement.Body. El valor predeterminado de http_max_uri_size en ClickHouse es 1 MiB, aunque un intermediario puede imponer un límite menor. En el modo de cuerpo, la sentencia y las filas no están sujetas a ese límite de longitud de la URL; otras opciones de la solicitud pueden seguir apareciendo en ella. Este ajuste es independiente de Compressor: el cuerpo se codifica de la misma manera en ambos modos.

Lectura de datos

Use ExecuteReaderAsync para ejecutar consultas SELECT. El objeto ClickHouseDataReader que se devuelve permite acceder a las columnas del resultado con el tipo de dato correspondiente mediante métodos como GetInt64(), GetString() y GetFieldValue<T>(). Llame a Read() para pasar a la siguiente fila. Devuelve false cuando no quedan más filas. Acceda a las columnas por su índice (empezando por 0) o por su nombre.

Lectura de objetos POCO

En lugar de leer las columnas por índice o por nombre, puede obtener los resultados de la consulta directamente como instancias de sus propias clases. Registre el tipo una sola vez en el Client y, después, use QueryAsync<T>:
RegisterPocoType<T>() configura las correspondencias de inserción y lectura y valida ambas desde el principio. RegisterBinaryInsertType<T>() no cambia y sigue siendo exclusivo para inserciones por compatibilidad con versiones anteriores. Un tipo registrado debe tener:
  • Un constructor público sin parámetros.
  • Al menos una propiedad pública con un setter público que no sea init. Se admiten las propiedades required.
La correspondencia de columnas distingue entre mayúsculas y minúsculas. Si falta una columna en el resultado, la propiedad correspondiente conserva su valor predeterminado; las columnas adicionales del resultado se ignoran. El controlador no amplía ni reduce los valores. Salvo en las representaciones alternativas que se enumeran a continuación, el tipo de la columna en el framework debe ser asignable al tipo de la propiedad; de lo contrario, se lanza InvalidOperationException. Por tanto, una propiedad de tipo object acepta cualquier columna. QueryAsync<T> lee cada una de estas columnas directamente en una propiedad del tipo correspondiente: Cada fila admite también la variante anulable del tipo de propiedad (long?, DateOnly?, etc.), independientemente de que la columna sea Nullable(...) o no. Se permite registrar una propiedad de tipo valor no anulable para una columna Nullable(T), pero se genera una excepción si se recibe un valor NULL. Las envolturas como LowCardinality(T), SimpleAggregateFunction(f, T) y Object(T) se asignan igual que T. También se admiten las columnas compuestas, que usan el tipo del framework indicado en la referencia de tipos de lectura: Array(T) se asigna a T[], Tuple(...) a System.Tuple<...>, Nested(...) a Tuple<...>[], JSON a JsonObject (o a string con JsonReadMode=String) y Variant/Dynamic a object. Una columna Map(K, V) es un caso especial: una propiedad List<KeyValuePair<K, V>> o KeyValuePair<K, V>[] se lee sin boxing y conserva el orden de transmisión y las claves repetidas, con cualquiera de los valores de MapReadMode. Una propiedad Dictionary<K, V> solo funciona en el modo predeterminado. Los tipos de clave y valor deben coincidir exactamente, por lo que Map(String, Nullable(Int32)) requiere KeyValuePair<string, int?>. Cuando una columna admite más de un tipo de propiedad (por ejemplo, una columna DateTime como DateTime, DateTimeOffset o DateOnly, o una columna String como string o byte[]), el tipo de propiedad declarado determina la representación. Estas representaciones alternativas solo están disponibles al leer objetos POCO: QueryAsync<T> las admite, pero MapTo<T> no. Al recorrer manualmente un lector, use ClickHouseDataReader.MapTo<T>() para materializar la fila actual como un POCO registrado sin avanzar a la siguiente fila:
Use MapTo<T> cuando necesite controlar directamente el bucle de lectura, por ejemplo, para combinar el acceso a columnas sin procesar con la materialización de objetos POCO. Lee la fila a partir de los valores encapsulados del lector, por lo que no admite los tipos de propiedad alternativos mencionados arriba y realiza más asignaciones de memoria que QueryAsync<T>. Es preferible usar QueryAsync<T> si solo necesita las filas; consulte cómo elegir la vía de materialización para ver las cifras. Un conversor de valores de lectura configurado a nivel de Client o por consulta se aplica a ambas rutas y no desactiva la lectura sin boxing. El controlador convierte cada columna con la sobrecarga correspondiente a la forma en que se leyó: la versión tipada ConvertValue<T> para una columna sin boxing y la versión con boxing ConvertValue para una columna compuesta. Implemente ambas sobrecargas de forma coherente; de lo contrario, una misma columna producirá resultados distintos según la ruta. Cuando hay una LoggerFactory configurada, RegisterPocoType<T>() y RegisterBinaryInsertType<T>() generan un log de nivel Debug (categoría ClickHouse.Driver.Client) que detalla qué propiedades se asignaron a cada columna, cuáles se omitieron y por qué. Consulte Registro y diagnóstico.

Parámetros SQL

En ClickHouse, los parámetros de las consultas SQL siguen el formato estándar {parameter_name:DataType}. Ejemplos:
Los parámetros ‘bind’ de SQL se envían como parámetros de consulta en el URI HTTP, por lo que usar demasiados puede provocar una excepción de “URL demasiado larga”. Utilice InsertBinaryAsync para insertar datos en bloque y evitar esta limitación.

Marcadores de posición @name al estilo ADO

El driver también acepta marcadores de posición @name, como los que generan los ORM, por ejemplo, Dapper. Son una comodidad para el cliente: antes de enviar la solicitud, cada marcador se sustituye por {name:ResolvedType}, de modo que el servidor nunca recibe un @. Consulte resolución de tipos para saber cómo se determina el tipo. Utilice la forma explícita {name:Type} siempre que sea posible. Si @name no tiene un parámetro correspondiente, se deja tal cual para que el servidor lo rechace. La correspondencia distingue entre mayúsculas y minúsculas, por lo que @ID no se vincula a un parámetro llamado id.
Para desactivar esta sustitución, establezca el modificador de AppContext ClickHouse.Driver.DisableReplacingParameters antes de usar el driver por primera vez. Solo se desactiva la sustitución en el texto; los parámetros se siguen enviando, por lo que las consultas escritas con la sintaxis nativa {name:Type} siguen funcionando.

Parámetros de identificador

El tipo de parámetro Identifier permite usar de forma segura el nombre de una base de datos, una tabla o una columna como parámetro, en lugar de un literal de cadena entre comillas. Para ello, utilice la sintaxis {name:Identifier} en SQL o establezca ClickHouseDbParameter.ClickHouseType = "Identifier":
El valor se envía tal cual y el servidor lo sustituye como un identificador SQL sin comillas, añadiendo las comillas invertidas y escapando los caracteres según sus propias reglas. Los identificadores que contienen caracteres especiales (incluidas las comillas invertidas) pueden enviarse y recuperarse sin alteraciones.

ID de consulta

A cada consulta se le asigna un query_id único que permite consultar datos en la tabla system.query_log o cancelar consultas de larga duración. Puede especificar un ID de consulta personalizado mediante QueryOptions:
Si especifica un QueryId personalizado, asegúrese de que sea único en cada llamada. Un GUID generado aleatoriamente es una buena opción.

Correspondencia personalizada de tipos de parámetros

Al usar parámetros con la sintaxis @ (por ejemplo, WHERE id = @id), el driver infiere automáticamente el tipo de ClickHouse a partir del tipo del valor de .NET. Por ejemplo, int se corresponde con Int32.
Comportamiento de los parámetros DateTime inferidosEn los parámetros con la sintaxis @ que no tienen una indicación {name:Type} en SQL ni un valor establecido para ClickHouseType, los valores que representan un instante se infieren como DateTime('UTC') en lugar de DateTime sin especificar una zona horaria. Los valores DateTime cuyo Kind es Utc o Local, así como todos los valores DateTimeOffset, se envían como DateTime('UTC'), lo que permite conservar el instante independientemente de la zona horaria del servidor.Las indicaciones explícitas ({name:DateTime}) tienen prioridad sobre la inferencia y son la forma recomendada de crear consultas.
Para sobrescribir estos valores predeterminados, configura ParameterTypeResolver en ClickHouseClientSettings. Esto resulta útil si quieres que todos los parámetros DateTime usen DateTime64(3) para obtener precisión de milisegundos, o que todos los valores decimales usen una escala determinada, sin tener que establecer ClickHouseType en cada parámetro. Uso de DictionaryParameterTypeResolver para correspondencias sencillas de tipos:
IParameterTypeResolver personalizado para casos avanzados: Para resolver el tipo en función del valor o del nombre, implemente directamente la interfaz IParameterTypeResolver. Devuelva null para recurrir a la inferencia predeterminada:
También puede configurar un resolvedor para una consulta concreta mediante QueryOptions.ParameterTypeResolver. Si lo hace, este tiene precedencia sobre el resolvedor configurado a nivel del client. Orden de precedencia para la resolución de tipos: El resolvedor forma parte de una cadena de precedencia. De mayor a menor prioridad:
  1. ClickHouseType establecido explícitamente en el parámetro
  2. Indicación de tipo SQL mediante la sintaxis {name:Type} en la consulta
  3. IParameterTypeResolver (de QueryOptions.ParameterTypeResolver; si no está establecido, se utiliza ClickHouseClientSettings.ParameterTypeResolver)
  4. Inferencia de tipos integrada (TypeConverter.ToClickHouseType)
El resolvedor también funciona con la ruta de ADO.NET ClickHouseConnection: las conexiones creadas a partir del client heredan la configuración.

Formato personalizado de los valores de los parámetros

IParameterFormatter es un punto de extensión que determina cómo se serializan los valores de los parámetros. Úselo cuando el formato integrado (p. ej., la precisión de DateTime, la configuración regional de los decimales, el escape de cadenas o la representación de números) no se ajuste a lo que esperan su esquema o las herramientas posteriores. Establezca ParameterFormatter en ClickHouseClientSettings para usar un formateador en todas las consultas parametrizadas. El formateador recibe el valor, el nombre del tipo de ClickHouse determinado y el nombre del parámetro, y devuelve la representación en cadena que se envía al servidor. Devuelva null para usar el formateador predeterminado. Uso de DictionaryParameterFormatter para aplicar un formato sencillo según el tipo CLR:
IParameterFormatter personalizado para escenarios avanzados:
También puede configurar un formateador para cada consulta mediante QueryOptions.ParameterFormatter. Si se configura, tiene prioridad sobre el formateador del client. Valores compuestos: El formateador se aplica tanto a los parámetros de colección de nivel superior como a cada elemento de los valores compuestos (Array, Tuple, Map, Nullable, LowCardinality, Variant). Por ejemplo, una correspondencia para typeof(int) formatea por separado cada elemento Int32 de un Array(Int32). Uso de comillas simples en contextos compuestos: Cuando los tipos de ClickHouse que representan cadenas (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) forman parte de un literal compuesto, el driver encierra el resultado del formateador entre comillas simples, pero no escapa su contenido. Si la cadena devuelta contiene una comilla simple o una barra invertida sin escapar, el literal compuesto no será válido y el servidor rechazará la consulta. Los parámetros de cadena de nivel superior (que no forman parte de un valor compuesto) se utilizan tal cual, sin encerrarlos entre comillas, por lo que no es necesario escaparlos. Orden de prioridad de los formateadores:
  1. IParameterFormatter (de QueryOptions.ParameterFormatter o, si no está disponible, de ClickHouseClientSettings.ParameterFormatter). Si devuelve un valor distinto de null, se utiliza ese valor.
  2. El formato integrado específico de cada tipo en HttpParameterFormatter.
El formateador no se invoca para los valores null o DBNull; estos siempre se serializan como el marcador de valor nulo de ClickHouse (\N).

Conversión personalizada de valores al leer

IReadValueConverter permite transformar los valores que devuelve el lector de datos después de la deserialización, sin cambiar su tipo CLR. Entre sus usos habituales están establecer DateTime.Kind = Utc en una columna DateTime sin zona horaria, eliminar espacios o normalizar cadenas y procesar una columna JSON antes de que los datos lleguen al código de la aplicación. Configure ReadValueConverter en ClickHouseClientSettings para aplicar un conversor a todas las lecturas. El conversor se invoca una vez por cada columna de cada fila, tanto en la ruta de valores encapsulados (GetValue) como en la genérica (GetFieldValue<T>). Si no se configura ningún conversor, no se añade ninguna sobrecarga: el lector devuelve los valores directamente. Uso de DictionaryReadValueConverter para conversiones sencillas según el tipo CLR:
Los valores cuyo tipo CLR en tiempo de ejecución no está registrado mediante For<T> pasan sin modificarse. La selección se basa en el tipo CLR exacto, así que registre el tipo que realmente produce el lector (por ejemplo, For<JsonObject> para una columna JSON en JsonReadMode.Binary). IReadValueConverter personalizado para casos avanzados: Si necesita seleccionar el conversor según la cadena de tipo de ClickHouse (por ejemplo, para distinguir DateTime de DateTime('UTC'), que se representan con el mismo tipo CLR), implemente IReadValueConverter directamente:
El conversor debe preservar el tipo CLR en tiempo de ejecución; los metadatos de la columna (GetFieldType, GetSchemaTable) no pasan por él y deben ser coherentes con el valor devuelto. También puede configurar un conversor para cada consulta mediante QueryOptions.ReadValueConverter; si lo hace, tendrá prioridad sobre el conversor configurado a nivel de cliente. Alcance de la conversión: El conversor se invoca una vez por columna y recibe el valor completo de la celda deserializada; no procesa recursivamente los contenedores compuestos. Para una columna Array(Int32), recibe un int[]; para Tuple(Int32, String), un ITuple. Qué sobrecarga se ejecuta: Ambas sobrecargas deben producir resultados coherentes, ya que la sobrecarga que invoca el driver depende de cómo el llamador haya leído la columna:
  • ConvertValue<T> — los métodos de acceso tipados GetByte, GetSByte, GetInt16/32/64, GetUInt16/32/64, GetFloat, GetDouble, GetGuid, GetDateTime, GetIPAddress, GetBigInteger y GetFieldValue<T>, además de todas las columnas sin conversión a objeto en la ruta de lectura de POCO.
  • ConvertValue (con conversión a objeto) — GetValue, GetValues, los indexadores, GetChar, GetTuple y las rutas que fuerzan una conversión de tipo en GetBoolean, GetDecimal y GetString.
IsDBNull no invoca ningún conversor: lee directamente el indicador de valor nulo, por lo que un conversor nunca puede cambiar si un valor se considera nulo. TryGetEnumOrdinal también prescinde del conversor; consulte la lectura del valor ordinal de un enum. El conversor también funciona con la ruta ADO.NET de ClickHouseConnection: las conexiones creadas a partir del cliente heredan la configuración.

Transmisión de datos sin procesar

Use ExecuteRawResultAsync para transmitir directamente los resultados de una consulta en un formato específico, sin pasar por el lector de datos. Esto resulta útil para exportar datos a archivos o enviarlos a otros sistemas:
Formatos habituales: JSONEachRow, CSV, TSV, Parquet, Native. Consulte la documentación de formatos para ver todas las opciones.

Compresión de transporte por consulta

De forma predeterminada, el client negocia zstd, lz4, gzip, deflate cuando Compression=true (el valor predeterminado de la cadena de conexión) y decodifica el flujo por sí mismo, de forma transparente. Para las exportaciones sin procesar (p. ej., Parquet, Arrow, Native), puede ser conveniente negociar otro códec (p. ej., zstd o lz4) para reducir el consumo de ancho de banda a cambio de un mayor uso de CPU, sin modificar la configuración de toda la conexión. QueryOptions.AcceptEncoding y ClickHouseCommand.AcceptEncoding establecen el encabezado HTTP Accept-Encoding para una sola solicitud, sustituyen cualquier valor predeterminado que se hubiera añadido y fuerzan enable_http_compression=1 en la URL (ClickHouse lo requiere para respetar Accept-Encoding).

Configuración de HttpClient

No hay nada que configurar: el HttpClient que crea el driver deja AutomaticDecompression en DecompressionMethods.None, y el propio driver decodifica las respuestas. Así, Content-Encoding no se elimina a sus espaldas y el cuerpo sin procesar le llega exactamente como lo envió el servidor.
Si proporciona su propio HttpClient, deje también desactivado AutomaticDecompression. Esta opción no afecta solo a las respuestas: al enviar la solicitud, el handler añade todos los algoritmos de su máscara que no estén en el Accept-Encoding saliente. Por tanto, un handler con GZip | Deflate convierte un AcceptEncoding = "lz4" explícito en lz4, gzip, deflate y un "identity" explícito en identity, gzip, deflate durante la transmisión. Como ClickHouse interpreta el encabezado según su propio orden fijo de preferencia de códecs (sin tener en cuenta el orden ni los valores q), puede responder con un códec que usted no solicitó. El handler decodifica entonces la respuesta y elimina el encabezado, de modo que ni siquiera podrá darse cuenta de lo ocurrido. Si deja la máscara desactivada, solo se ofrecen los algoritmos que usted eligió.
Si AcceptEncoding solicita un códec que el driver no puede decodificar (snappy), solo es seguro usar ExecuteRawResultAsync. ExecuteReaderAsync, ExecuteScalarAsync y ExecuteNonQueryAsync fallan con una NotSupportedException que indica el códec (antes interpretaban los bytes comprimidos como si tuvieran el formato del resultado y producían datos inservibles).

Cuerpos de error

Cuando el servidor responde con un código 4xx/5xx y enable_http_compression=1 está activado, comprime el cuerpo del error con el mismo códec que habría usado para una respuesta correcta. El driver lo descomprime si se ha utilizado alguno de los códecs que admite (lz4, zstd, gzip, deflate, br/brotli), para que el mensaje de ClickHouseServerException sea legible. Si se ha utilizado otro códec (snappy, …), devuelve un marcador de posición que indica el códec y remite a system.query_log para consultar el texto original del error.

Descompresión de respuestas

Accept-Encoding solo solicita al servidor que comprima la respuesta; después hay que descomprimirla. El driver se encarga de hacerlo según el valor de Content-Encoding de la respuesta. Así, todas las API habituales de lectura (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) funcionan con respuestas comprimidas sin necesidad de configurar nada. Admite lz4, zstd, gzip, deflate y br; snappy no es compatible. De forma predeterminada, el driver anuncia zstd, lz4, gzip, deflate, y ClickHouse responde con zstd. Si prefiere otra opción, establezca Accept-Encoding a nivel de client:
por consulta, que tiene prioridad:
o en la cadena de conexión, si usas un ORM y nunca modificas ClickHouseClientSettings:
Establecerlo también añade enable_http_compression=1 a la URL, requisito para que ClickHouse tenga en cuenta el encabezado, incluso si UseCompression es false: indicar expresamente un códec se interpreta como una solicitud de compresión. Si no se establece ningún valor, UseCompression=false no envía el encabezado Accept-Encoding. Accept-Encoding puede establecerse en cuatro lugares. Prevalece el primero en el que se indique un códec:
  1. QueryOptions.AcceptEncoding (o ClickHouseCommand.AcceptEncoding)
  2. CustomHeaders["Accept-Encoding"] en la consulta
  3. CustomHeaders["Accept-Encoding"] en el client
  4. ClickHouseClientSettings.AcceptEncoding o la palabra clave AcceptEncoding de la cadena de conexión
Si no se indica ningún códec en ninguno de ellos, el driver envía su lista predeterminada. Un valor que no indique ningún códec (nulo, vacío, compuesto solo por espacios en blanco o solo por comas) se considera no establecido y se pasa al siguiente lugar. Para desactivar la compresión, use identity. El servidor, no el client, elige el códec. ClickHouse busca los tokens de Accept-Encoding según su propio orden fijo de preferencia — zstd > br > lz4 > snappy > gzip > deflate — e ignora tanto el orden en que se enumeran como los valores q. Por tanto, el encabezado indica qué códecs se admiten, pero no exige uno en particular: la única forma de influir en la elección es omitir tokens. La lista predeterminada incluye zstd, por lo que una consulta con la configuración predeterminada recibe una respuesta comprimida con zstd; los demás tokens sirven como alternativas. br se puede decodificar, pero no se incluye en el encabezado de forma predeterminada. El tamaño de la carga útil y el uso de CPU del servidor y del client varían según el códec, los datos, la conexión y el valor de http_zlib_compression_level del servidor (valor predeterminado de fábrica: 3). Consulte Ajuste de la compresión.
  • http_zlib_compression_level. Esta configuración se aplica a todos los códecs HTTP y su valor predeterminado es 3. Ajuste este valor según los datos, la velocidad de la conexión y el uso de CPU.
  • Un client limitado por la CPU en una conexión rápida. El driver decodifica el cuerpo de la respuesta en el thread desde el que se realiza la llamada. Por tanto, si la red no es el cuello de botella, la velocidad de decodificación del client podría convertirse en el factor limitante.
Solicite un códec diferente por consulta o para todo el client si se da cualquiera de estas situaciones:
Como la decisión se basa en la respuesta, el cuerpo se decodifica cuando Content-Encoding así lo indica, con independencia de lo solicitado: si el encabezado no está presente o su valor es identity, el cuerpo se deja intacto; si indica un códec compatible, se decodifica; y cualquier otro valor provoca un error que lo menciona. No hay riesgo de decodificarlo dos veces: si AutomaticDecompression de un manejador proporcionado por el llamador ya ha decodificado el cuerpo, también elimina Content-Encoding, de modo que el driver recibe el texto sin cifrar y lo deja intacto. Los resultados sin procesar no anuncian ningún códec. ExecuteRawResultAsync (y los métodos públicos PostStreamAsync / InsertRawStreamAsync) entregan el cuerpo tal como se recibió. Por eso, si no se especifica un códec, no solicitan ninguno: el driver no decodifica ese cuerpo, de modo que anunciar un códec en este caso podría convertir inadvertidamente una exportación en un archivo comprimido. La regla es sencilla y no depende de cómo esté configurado HttpClient: el cuerpo llega exactamente como lo envió el servidor, y el servidor envía datos sin cifrar a menos que se solicite un códec. Para exportar bytes comprimidos de forma deliberada, hay que solicitar un códec (para todo el client o para una consulta concreta). Un valor explícito de AcceptEncoding (en cualquiera de los dos niveles) también se aplica a las solicitudes sin procesar. Si se desea decodificar el resultado, puede usarse ClickHouseRawResult.ReadDecompressedStreamAsync(); ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync y CopyToAsync siempre devuelven los bytes exactamente como llegaron.
Lea el flujo devuelto hasta el final antes de que salga de su ámbito, como se muestra arriba. Si la respuesta está comprimida, se obtiene un decodificador creado con leaveOpen, por lo que desecharlo no afecta a la respuesta; si no está comprimida, se obtiene el propio flujo de contenido HTTP, por lo que desecharlo cierra el cuerpo. En ambos casos, ClickHouseRawResult es responsable de la respuesta: no llame a sus otros miembros de lectura después de desechar el flujo. Siempre debe desechar ClickHouseRawResult, y eso basta para liberar tanto la respuesta como cualquier decodificador creado aquí (los decodificadores mantienen búferes reutilizables). Por tanto, el await using anterior es opcional, aunque puede conservarse sin problemas. Las llamadas secuenciales repetidas devuelven el mismo flujo; no es seguro usar este tipo de forma concurrente. Consulte Select_007_ResponseCompression.cs para ver un ejemplo ejecutable.

Compresión de las solicitudes de inserción

Zstd es el códec predeterminado para las inserciones: InsertOptions.Compressor se inicializa con ZstdCompressor.Default, que corresponde a zstd con nivel de compresión 3. Asígnale otro compresor para cambiar el códec, o null para enviar el cuerpo sin comprimir.
El driver incluye cuatro códecs. Cada uno dispone de una instancia Default y de un constructor que acepta un nivel y el tamaño del búfer de escritura:
Comparta las instancias de los compresores. Cada Default es una instancia compartida, y los cuatro compresores pueden usarse de forma segura desde varios threads a la vez. Esto ocurre cuando InsertOptions.MaxDegreeOfParallelism es mayor que 1, ya que cada operación de inserción usa un compresor por lote. Ninguno implementa IDisposable. Cree una instancia propia y reutilícela, tal como se hace con Default.
IClickHouseCompressor es público y basta con que una implementación proporcione dos miembros:
El servidor debe aceptar el Content-Encoding que indiques. Los demás miembros — Decompress, MethodByte, MaxEncodedLength, Encode y Decode — tienen implementaciones predeterminadas que lanzan NotSupportedException, así que sobrescribe solo los que necesite tu códec. Implementa Decompress para descomprimir también los cuerpos de las respuestas, además de comprimir las solicitudes, y haz que el flujo que devuelve lance InvalidDataException cuando un cuerpo esté dañado o tenga un formato incorrecto. InsertOptions.Compressor solo se aplica a las inserciones binarias. Los demás cuerpos de las solicitudes del driver se comprimen según otras reglas y ninguno pasa por él:
  • Cada solicitud que contiene texto SQL (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, ExecuteRawResultAsync, la capa ADO.NET) envía su sentencia con Content-Encoding: gzip siempre que UseCompression sea true, es decir, de forma predeterminada. El códec no se puede configurar: AcceptEncoding solo afecta a la respuesta, así que las opciones son gzip o ninguna compresión. Compression=false envía la sentencia sin comprimir. Las sentencias son pequeñas, por lo que rara vez es necesario prestar atención a esto, pero conviene saberlo al inspeccionar las solicitudes en un proxy o una captura de paquetes.
  • Un cuerpo multipart — una consulta cuyos parámetros se envían como datos de formulario (UseFormDataParameters=true) — siempre se envía sin comprimir, independientemente del valor de UseCompression.
  • Una carga sin procesar (InsertRawStreamAsync, PostStreamAsync) tiene su propio indicador para cada llamada y no utiliza ni UseCompression ni InsertOptions.Compressor: usa gzip cuando el indicador está activado y se envía sin comprimir en caso contrario. Ten en cuenta que el parámetro useCompression de InsertRawStreamAsync tiene true como valor predeterminado, por lo que una carga sin procesar se comprime con gzip a menos que pases false, incluso si el cliente tiene Compression=false.

Ajuste de la compresión

La compresión reduce el volumen de datos a costa de un mayor uso de CPU. Que esto compense depende casi por completo de la velocidad de la conexión en comparación con la velocidad del códec. No hay ningún ajuste que sea adecuado para todos los casos.

La cifra decisiva

La compresión merece la pena siempre que el códec sea más rápido que la red. En la ruta de lectura, ese umbral es más bajo de lo que suele pensarse, porque ClickHouse comprime las respuestas HTTP en un único thread en el búfer de salida. En las mediciones realizadas con un servicio de ClickHouse Cloud de 16 vCPU (hits, RowBinary, nivel 3), el servidor genera datos comprimidos a unos 100-200 MB/s. Por tanto, con un resultado grande y si se procesa una sola consulta a la vez, la compresión deja de compensar en torno a los 100 MB/s. Un único flujo HTTPS dentro de una misma región de la nube suele superar esa velocidad, mientras que una conexión que atraviese la Internet pública, una VPN o el límite entre regiones suele quedar por debajo. La compresión sigue siendo útil en la ruta de inserción a velocidades de enlace más altas, porque el client comprime en un núcleo propio y suele hacerlo más rápido que el servidor al comprimir las respuestas.

Guía orientativa según el entorno de despliegue

Hay tres aspectos que esta tabla no refleja:
  • Coste del tráfico de salida: si pagas por la transferencia de datos, los bytes tienen un coste más allá de la latencia, lo que inclina la balanza hacia una mayor compresión, independientemente de la velocidad de la conexión.
  • Resultados pequeños: todo lo anterior se aplica a cargas útiles grandes. En las respuestas pequeñas, el códec apenas influye y predomina la sobrecarga de cada solicitud.
  • Las inserciones en paralelo elevan los umbrales de inserción. Todas las cifras de caudal anteriores corresponden a un único thread. El valor predeterminado de InsertOptions.MaxDegreeOfParallelism es 1, pero, al aumentarlo, los lotes se comprimen de forma concurrente. Por tanto, la tasa de codificación total del client crece aproximadamente en proporción al número de núcleos que se le asignan. Así, en una conexión rápida, puede seguir mereciendo la pena comprimir una inserción en paralelo incluso a velocidades a las que ya no compensaría comprimirla con un solo thread. Toma las filas sobre inserciones de la tabla como un límite inferior y, si ya procesas lotes en paralelo, repite las pruebas antes de concluir que tu conexión es demasiado rápida para que la compresión resulte útil.
La ruta de lectura solo se paraleliza cuando se ejecutan varias consultas.

Elegir un códec

Niveles

La compresión de las respuestas se controla mediante una única configuración del servidor, http_zlib_compression_level, que se aplica a todos los códecs HTTP, no solo a zlib. Su valor predeterminado es 3. No la cambie a menos que las mediciones justifiquen hacerlo. Por encima del valor predeterminado, la reducción de tamaño es mínima en relación con el aumento del uso de CPU (con zstd, pasar de 3 a 6 aproximadamente duplica el uso de CPU del servidor y reduce los bytes en torno a un 14 %), mientras que el rendimiento de br se degrada drásticamente. En el nivel 1, en cambio, la situación cambia de forma notable: lz4 consume mucha menos CPU y zstd pierde su ventaja frente a él en ese aspecto. Si lo necesita, ajuste el nivel por consulta:

Determinar su propio punto de equilibrio

La forma más rápida de elegir el códec y el nivel de compresión óptimos es cronometrar la misma consulta con varios códecs y comparar los tiempos.
Para ver los mismos datos desde el lado del servidor, consulte ProfileEvents en system.query_log — establezca QueryOptions.QueryId para poder localizar la fila:
Una advertencia si realiza este benchmark por su cuenta: un simple LIMIT n sin ORDER BY devuelve filas distintas en cada ejecución, por lo que cada repetición comprime datos diferentes y las proporciones dejan de ser significativas. Haga la comparación con un conjunto de resultados fijo.

Inserción desde un flujo sin procesar

Use InsertRawStreamAsync para insertar datos directamente desde flujos de archivos o de memoria en formatos como CSV, JSON y Parquet, o en cualquier otro formato compatible con ClickHouse. Inserción desde un archivo CSV:
El driver asume la responsabilidad de gestionar el flujo. InsertRawStreamAsync y PostStreamAsync llaman a Dispose en el flujo que reciben cuando termina la solicitud, tanto si se completa correctamente como si falla. No llame a Dispose usted mismo ni reutilice el flujo después; por eso el ejemplo anterior no incluye FileStream en un bloque using.Si añade su propio bloque using, este se ejecutará después de que el driver ya haya llamado a Dispose en el flujo. En un FileStream o MemoryStream, esa segunda llamada es inofensiva, pero en un flujo cuyo método Dispose devuelve un búfer a un grupo o decrementa un contador de referencias, el recurso se libera dos veces.La responsabilidad de gestionar el flujo solo se transfiere cuando se aceptan los argumentos: si la llamada lanza ArgumentException o ArgumentNullException porque falta una tabla, un flujo o un formato, usted sigue siendo responsable del flujo.
Consulte la documentación de configuración de formatos para conocer las opciones que permiten controlar el comportamiento de la ingestión de datos.

Más ejemplos

Para ver más ejemplos prácticos de uso, consulte el directorio de ejemplos del repositorio de GitHub.

ADO.NET

La biblioteca ofrece compatibilidad completa con ADO.NET a través de ClickHouseConnection, ClickHouseCommand y ClickHouseDataReader. Esta API es necesaria para integrar ORM (Dapper, Linq2db) y para utilizar las abstracciones estándar de .NET para bases de datos.

Gestión de la duración de las conexiones con ClickHouseDataSource

Cree siempre las conexiones a partir de un ClickHouseDataSource para garantizar una gestión adecuada de su duración y la agrupación de conexiones. DataSource gestiona internamente un único ClickHouseClient, y todas las conexiones comparten su pool de conexiones HTTP.
Para la inyección de dependencias:
No cree instancias de ClickHouseConnection directamente en el código de producción. Cada vez que lo hace, se crean un nuevo cliente HTTP y un nuevo pool de conexiones, lo que puede agotar los sockets cuando la carga es elevada:
En su lugar, utilice siempre ClickHouseDataSource o comparta una única instancia de ClickHouseClient.

Uso de ClickHouseCommand

Cree comandos desde una conexión para ejecutar consultas SQL:
Métodos de comando:
  • ExecuteNonQueryAsync() - Para sentencias INSERT, UPDATE, DELETE y DDL
  • ExecuteScalarAsync() - Devuelve el valor de la primera columna de la primera fila
  • ExecuteReaderAsync() - Devuelve un ClickHouseDataReader para recorrer los resultados

Uso de ClickHouseDataReader

ClickHouseDataReader proporciona acceso tipado a los resultados de las consultas:

Lectura del valor ordinal de un enum

Una columna Enum8 o Enum16 se materializa como una etiqueta: GetFieldType devuelve string, y GetString, GetValue y GetFieldValue<string> devuelven esa etiqueta. Los accesores numéricos lanzan InvalidCastException al usarse con una columna de tipo enum, ya que el valor almacenado es una cadena. Use TryGetEnumOrdinal para obtener el número asociado a la etiqueta:
Devuelve true y asigna un valor a value si la columna es de tipo Enum8/Enum16 o si es una columna Nullable(Enum...) cuya celda no es NULL. Devuelve false y asigna 0 a value si la celda es NULL o si la columna no es de tipo enum. El ordinal es el valor con signo recibido en formato wire, por lo que puede ser negativo; además, el ordinal de un Enum16 puede ocupar más de un byte.

Buenas prácticas

Duración y reutilización de las conexiones

ClickHouse.Driver utiliza System.Net.Http.HttpClient internamente. HttpClient mantiene un pool de conexiones por punto de conexión. Por tanto:
  • Las sesiones de la base de datos se multiplexan a través de conexiones HTTP gestionadas por el pool de conexiones.
  • El pool reutiliza automáticamente las conexiones HTTP.
  • Las conexiones pueden permanecer abiertas después de liberar los objetos ClickHouseClient o ClickHouseConnection.
Prácticas recomendadas:
Si utiliza un HttpClient o HttpClientFactory personalizado, asegúrese de que PooledConnectionIdleTimeout tenga un valor inferior al de keep_alive_timeout del servidor para evitar errores causados por conexiones semicerradas. En las implementaciones de Cloud, el valor predeterminado de keep_alive_timeout es de 10 segundos.
Evite crear varias instancias de ClickHouseClient o instancias independientes de ClickHouseConnection sin un HttpClient compartido. Cada instancia crea su propio pool de conexiones.

Gestión de DateTime

  1. Use UTC siempre que sea posible. Almacene las marcas de tiempo en columnas DateTime('UTC') y use DateTimeKind.Utc en su código. Así evitará ambigüedades con las zonas horarias.
  2. Use DateTimeOffset para gestionar explícitamente la zona horaria. Siempre representa un instante concreto e incluye el desfase respecto de UTC.
  3. Especifique la zona horaria en las indicaciones de tipo SQL. Si usa parámetros con valores DateTime Unspecified para columnas que no están en UTC, incluya la zona horaria en la sentencia SQL:

Inserciones asíncronas

Las inserciones asíncronas trasladan del cliente al servidor la responsabilidad de agrupar los datos en lotes. En lugar de agruparlos en el cliente, el servidor almacena temporalmente los datos entrantes y los escribe en el almacenamiento cuando se alcanzan los umbrales configurados. Esto resulta útil en escenarios de alta concurrencia, como las cargas de trabajo de observabilidad, en las que muchos agentes envían cargas útiles pequeñas. Habilite las inserciones asíncronas mediante CustomSettings o la cadena de conexión:
Dos modos (controlados por wait_for_async_insert):
Con wait_for_async_insert=0, los errores solo se detectan durante el vaciado y no pueden atribuirse a la inserción original. Además, el client no aplica contrapresión, por lo que el servidor podría sobrecargarse.
Parámetros de configuración principales:

Sesiones

Habilite las sesiones solo cuando necesite funciones del lado del servidor que mantengan el estado, por ejemplo:
  • Tablas temporales (CREATE TEMPORARY TABLE)
  • Mantener el contexto de las consultas entre varias sentencias
  • Configuración a nivel de sesión (SET max_threads = 4)
Cuando se habilitan las sesiones, las solicitudes se procesan de forma secuencial para evitar que se use una misma sesión de forma simultánea. Esto añade una sobrecarga a los workloads que no necesitan mantener el estado de la sesión.
Uso de ADO.NET (para garantizar la compatibilidad con ORM):

Tipos de datos compatibles

ClickHouse.Driver admite todos los tipos de datos de ClickHouse. Las tablas siguientes muestran la correspondencia entre los tipos de ClickHouse y los tipos nativos de .NET al leer datos de la base de datos.

Correspondencia de tipos: lectura desde ClickHouse

Tipos de enteros


Tipos de coma flotante


Tipos decimales

La conversión de tipos decimales se controla mediante la configuración UseCustomDecimals.

Tipo booleano


Tipos String

De forma predeterminada, las columnas String y FixedString(N) se devuelven como string. Para leerlas como byte[], establezca ReadStringsAsByteArrays=true en la cadena de conexión. Esto resulta útil si almacena datos binarios que podrían no tener una codificación UTF-8 válida.La configuración también afecta a las cadenas anidadas en otros tipos: Array(String) se lee como byte[][] y Map(String, String) como Dictionary<byte[], byte[]>, incluidas las claves. La única excepción son las columnas JSON, cuyos valores de cadena siempre se tratan como texto; consulte Tipo JSON.

Tipos de fecha y hora

ClickHouse almacena internamente los valores DateTime y DateTime64 como marcas de tiempo Unix (segundos o fracciones de segundo transcurridos desde la época Unix). Aunque el almacenamiento siempre se realiza en UTC, las columnas pueden tener una zona horaria asociada que determina cómo se muestran e interpretan los valores. Al leer valores DateTime, la propiedad DateTime.Kind se establece según la zona horaria de la columna: En las columnas con una zona horaria distinta de UTC, el valor DateTime devuelto representa la hora local de esa zona horaria. Use ClickHouseDataReader.GetDateTimeOffset() para obtener un DateTimeOffset con el desplazamiento horario correspondiente:
Para las columnas sin una zona horaria explícita (es decir, DateTime en lugar de DateTime('Europe/Amsterdam')), el controlador devuelve un DateTime con Kind=Unspecified. Así se conserva la hora tal como se almacenó, sin suponer ninguna zona horaria. Si necesita que se tenga en cuenta la zona horaria en columnas que no la especifican, puede:
  1. Especificar una zona horaria en la definición de cada columna: DateTime('UTC') o DateTime('Europe/Amsterdam')
  2. Aplicar la zona horaria después de leer los datos.

Tipo JSON

El tipo de retorno de las columnas JSON depende de la configuración JsonReadMode:
  • Binary (predeterminado): Devuelve System.Text.Json.Nodes.JsonObject. Permite acceder a los datos JSON de forma estructurada, pero convierte los tipos especializados de ClickHouse (como las direcciones IP, los UUIDs y los valores decimales grandes) a sus representaciones en cadena dentro de la estructura JSON.
  • String: Devuelve el JSON sin procesar como string. Conserva la representación JSON exacta que proporciona ClickHouse, lo que resulta útil si necesita pasar el JSON a otro sistema sin analizarlo o encargarse de la deserialización.
None es un tercer modo. Lee los datos exactamente igual que Binary, pero no envía ninguna configuración del servidor junto con la consulta; úselo en una conexión que no permita establecerla. Una ruta declarada en el tipo de columna es una ruta tipada; cualquier otra ruta del documento es una ruta dinámica. La diferencia entre ambas se manifiesta cuando un valor es nulo. Una ruta tipada siempre aparece en el JsonObject. Si se declara como Nullable(T) o Dynamic, se devuelve como un valor nulo de JSON tanto si el valor almacenado es nulo como si el documento no contiene esa ruta; ambos casos son indistinguibles:
Si se declara con un tipo que no admite valores nulos, una ruta ausente adopta el valor predeterminado de ese tipo: JSON(x String) da {"x":""} y JSON(x Int64) da {"x":0}. Una ruta dinámica cuyo valor es nulo se elimina por completo del objeto, por lo que ContainsKey devuelve false para esa ruta. Al leer {"x":null} de una columna JSON sin más, se obtiene {}. Las rutas tipadas anidadas crean sus rutas superiores, por lo que JSON(a.b Nullable(Int64)) produce {"a":{"b":null}} incluso si el documento está vacío.
Esta es la representación que genera el propio servidor, por lo que los modos Binary y String ahora coinciden. Antes de la versión 1.4.0, una ruta tipada con un valor nulo se eliminaba del JsonObject, de modo que {"x":null} se leía como {}; en una ruta anidada como JSON(a.b Nullable(Int64)), desaparecía todo el subárbol a.
Las cadenas en las hojas de una columna JSON siempre se devuelven como texto, sea cual sea el valor de ReadStringsAsByteArrays: JsonValue no tiene una representación como array de bytes, por lo que un byte[] se representaría en base64. Esto se aplica a String, FixedString y a los tipos encapsulados en LowCardinality, Nullable o SimpleAggregateFunction, así como a las cadenas dentro de Array y Map, incluidas las claves de los mapas.
Un array de bytes cuyo tipo no puede determinar el lector JSON sí se representa en base64: una ruta tipada como Variant o Dynamic contiene un valor cuyo tipo solo se conoce para cada fila, por lo que una cadena en Variant(Array(UInt8), String) se devuelve codificada en base64. Esto ocurre con ambas configuraciones.Si el tipo de una clave de un mapa JSON no es exactamente String —por ejemplo, Map(LowCardinality(String), String)—, se lanza NotSupportedException.
ClickHouse acepta una columna que declara una ruta tanto como valor como ruta padre de otra ruta, por ejemplo, JSON(a Int64, a.b Int64). Ambas rutas están presentes en cada fila, por lo que el servidor genera una fila con una clave duplicada: {"a":0,"a":{"b":7}}. Un JsonObject no puede contener dos valores para una misma clave, así que JsonReadMode.Binary lanza una SerializationException que identifica ambas rutas. Lo mismo ocurre si el valor es un Map, como en JSON(a Map(String, Int64)), al leer una fila que también contiene una ruta dinámica a.b. Esto solo ocurre cuando ambas rutas contienen un valor en la misma fila. Si una de ellas no contiene nada —un valor nulo, un objeto vacío o un subárbol cuyos valores son todos nulos—, prevalece la que contiene datos, sin importar cuál envíe primero el servidor. Por tanto, cuando las rutas superpuestas se declaran con tipos Nullable, solo una aporta un valor en cada fila y la columna se lee sin errores: JSON(a Nullable(Int64), a.b Nullable(Int64)) produce {"a":5} y {"a":{"b":7}}, como se esperaba. Lea la columna con JsonReadMode.String para obtener el texto JSON enviado por el servidor sin cambios, incluida la clave duplicada. Configure AllowDuplicateJsonKeys para seguir leyendo la columna como un JsonObject sin que se lance una excepción. El controlador conserva entonces el último de los dos valores que aparecen en la fila y descarta el otro, por lo que se pierde información: JSON(a Int64, a.b Int64) con {"a.b":7} se lee como {"a":0}. Si una ruta contiene un valor y su ruta padre contiene un escalar o un array, se sigue lanzando una excepción, porque no se puede insertar un subárbol en ninguno de ellos.

Tipo Map

Un Map(K, V) de ClickHouse se almacena físicamente como un Array(Tuple(K, V)) y puede contener varias entradas con la misma clave. Un Dictionary no admite claves duplicadas; por eso, en el modo predeterminado, solo se conserva el último valor de cada clave repetida y se descartan los pares anteriores. La opción MapReadMode determina la representación:
  • Dictionary (predeterminado): Devuelve Dictionary<K, V>.
  • KeyValuePairs: Devuelve List<KeyValuePair<K, V>> en el orden en que el servidor envió los pares, de modo que se conservan todos, incluidos los que tienen claves repetidas.
El modo determina qué tipo utiliza el framework para una columna Map, por lo que también afecta a GetFieldValue<T>, a los tipos del esquema que devuelve el controlador y a la correspondencia con las propiedades de POCO. Se aplica en cualquier punto del árbol de tipos de una columna donde aparezca un mapa, incluidos Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) y Dynamic. Ambas representaciones se aceptan al escribir, independientemente del modo; consulta escritura de mapas.

Otros tipos

En cada fila, los tipos Dynamic y Variant se convierten al tipo correspondiente al valor subyacente real.

Tipos de geometría

Geometry es un tipo Variant que puede contener cualquiera de los tipos de geometría. Cada valor se convierte al tipo correspondiente.

Correspondencia de tipos: escritura en ClickHouse

Al insertar datos, el controlador convierte los tipos de .NET en sus tipos correspondientes de ClickHouse. Las siguientes tablas muestran qué tipos de .NET se aceptan para cada tipo de columna de ClickHouse.

Tipos enteros


Tipos de coma flotante


Tipo booleano


Tipos String


Tipos de fecha y hora

Valores fuera de rangoEn la ruta de escritura binaria, los valores de Date, Date32, DateTime y DateTime32 que quedan fuera del rango admitido generan una excepción ArgumentOutOfRangeException al ejecutar Write, que indica el tipo de columna y el rango admitido. Antes, los valores fuera de rango podían truncarse sin aviso al convertirse en enteros de 32 bits y luego ser reinterpretados por el servidor, lo que producía marcas de tiempo válidas, pero incorrectas.
El controlador tiene en cuenta DateTime.Kind al escribir valores: Los valores de DateTimeOffset siempre conservan el instante exacto. Ejemplo: DateTime en UTC (se conserva el instante)
Ejemplo: DateTime sin especificar (hora local)
Recomendación: para obtener el comportamiento más sencillo y predecible, utilice DateTimeKind.Utc o DateTimeOffset en todas las operaciones con DateTime. Así, su código funcionará de forma coherente independientemente de la zona horaria del servidor, del cliente o de la columna.

Parámetros HTTP frente a copia masiva

Existe una diferencia importante entre la vinculación de parámetros HTTP y la copia masiva al escribir valores DateTime Unspecified: La copia masiva conoce la zona horaria de la columna de destino e interpreta correctamente los valores Unspecified según esa zona horaria. Los parámetros HTTP no detectan automáticamente la zona horaria de la columna. Debe especificarla en la indicación de tipo SQL:

Tipos decimales


Tipo JSON

La configuración JsonWriteMode determina cómo se escribe JSON:
  • String (predeterminado): Acepta string, JsonObject, JsonNode o cualquier objeto. Todas las entradas se serializan mediante System.Text.Json.JsonSerializer y se envían como cadenas JSON para que el servidor las analice. Es el modo más flexible y no requiere registrar los tipos.
  • Binary: Solo acepta tipos POCO registrados. Los datos se convierten al formato JSON binario de ClickHouse en el cliente, con compatibilidad total con indicaciones de tipo. Antes de usar este modo, es necesario llamar a connection.RegisterJsonSerializationType<T>(). Si se escriben valores string o JsonNode en este modo, se lanza ArgumentException.
Cuando una columna JSON tiene indicaciones de tipo (p. ej., JSON(id UInt64, price Decimal128(2))), el controlador las utiliza para serializar los valores sin alterar sus tipos. Así se conserva la precisión de tipos como UInt64, Decimal, UUID y DateTime64, que de otro modo la perderían al serializarse como JSON genérico. Los POCO se pueden escribir en columnas JSON de dos maneras, según el valor de JsonWriteMode: Modo de cadena (predeterminado): Los POCO se serializan con System.Text.Json.JsonSerializer. No es necesario registrar los tipos. Es la opción más sencilla y funciona con objetos anónimos. Modo binario: Los POCO se serializan con el formato JSON binario del controlador, que admite indicaciones de tipo completas. Los tipos deben registrarse con connection.RegisterJsonSerializationType<T>() antes de usarlos. Este modo permite asignar rutas JSON personalizadas mediante atributos:
  • [ClickHouseJsonPath("path")]: Asigna una propiedad a una ruta JSON personalizada. Resulta útil para estructuras anidadas o cuando el nombre de la propiedad difiere de la clave JSON deseada. Solo funciona en el modo binario.
  • [ClickHouseJsonIgnore]: Excluye una propiedad de la serialización. Solo funciona en el modo binario.
La comparación de los nombres de las propiedades con las indicaciones de tipo de la columna distingue entre mayúsculas y minúsculas. Una propiedad UserId solo coincidirá con una indicación definida como UserId, no como userid. Esto es coherente con el comportamiento de ClickHouse, que permite que rutas como userName y UserName coexistan como campos distintos. Limitaciones (solo en Binary mode):
  • Los tipos POCO deben registrarse en la conexión mediante connection.RegisterJsonSerializationType<T>() antes de serializarlos. Si se intenta serializar un tipo no registrado, se produce una excepción ClickHouseJsonSerializationException.
  • Las propiedades de diccionario y array/lista necesitan indicaciones de tipo en la definición de la columna para serializarse correctamente. Si no se proporcionan, utilice String mode.
  • Los valores nulos de las propiedades POCO solo se escriben si la ruta tiene una indicación de tipo Nullable(T) en la definición de la columna. ClickHouse no permite tipos Nullable en rutas JSON dinámicas, por lo que se omiten las propiedades nulas sin indicación de tipo.
  • Los atributos ClickHouseJsonPath y ClickHouseJsonIgnore se ignoran en String mode (solo funcionan en Binary mode).

Otros tipos


Tipos de geometría


No se admiten para escritura


Manejo de tipos anidados

Los tipos anidados de ClickHouse (Nested(...)) se pueden leer y escribir mediante la semántica de los arrays.

Registro y diagnóstico

El client .NET de ClickHouse se integra con las abstracciones de Microsoft.Extensions.Logging para ofrecer un registro ligero que se habilita solo si se solicita. Cuando está habilitado, el controlador emite mensajes estructurados sobre el ciclo de vida de la conexión, la ejecución de comandos, las operaciones de transporte y las inserciones masivas. El registro es totalmente opcional: las aplicaciones que no configuran un logger siguen funcionando sin sobrecarga adicional.

Inicio rápido

Uso de appsettings.json

Puede configurar los niveles de registro con la configuración estándar de .NET:

Uso de la configuración en memoria

También puede configurar en el código el nivel de detalle de los logs por categoría:

Categorías y emisores

El controlador utiliza categorías específicas que permiten ajustar el nivel de log de cada componente:

Ejemplo: Diagnóstico de problemas de conexión

Se registrará en el log:
  • La selección de la fábrica de clientes HTTP (pool de conexiones predeterminado o conexión única)
  • La configuración del controlador HTTP (SocketsHttpHandler o HttpClientHandler)
  • La configuración del pool de conexiones (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
  • La configuración de los tiempos de espera (ConnectTimeout, Expect100ContinueTimeout, etc.)
  • La configuración de SSL/TLS
  • Los eventos de apertura y cierre de conexiones
  • El seguimiento del ID de sesión

Modo de depuración: tracing de red y diagnóstico

Para ayudar a diagnosticar problemas de red, la biblioteca del controlador incluye un proceso auxiliar que permite realizar tracing de bajo nivel de los componentes internos de red de .NET. Para activarlo, debe pasar una LoggerFactory con el nivel configurado como Trace y establecer EnableDebugMode en true (o activarlo manualmente mediante la clase ClickHouse.Driver.Diagnostic.TraceHelper). Los eventos se registrarán en la categoría ClickHouse.Driver.NetTrace. Advertencia: esto generará logs extremadamente detallados y afectará al rendimiento. No se recomienda activar el modo de depuración en producción.

OpenTelemetry

El controlador incluye compatibilidad con el trazado distribuido de OpenTelemetry a través de la API de .NET System.Diagnostics.Activity. Cuando se habilita, el controlador genera spans para las operaciones de base de datos que pueden exportarse a sistemas de observabilidad como Jaeger o el propio ClickHouse (mediante el OpenTelemetry Collector).

Habilitar el trazado

En las aplicaciones ASP.NET Core, agregue el ActivitySource del controlador de ClickHouse a la configuración de OpenTelemetry:
Para aplicaciones de consola, pruebas o configuración manual:

Atributos de los spans

Cada span incluye atributos estándar de OpenTelemetry para bases de datos, además de estadísticas de las consultas específicas de ClickHouse que facilitan la depuración.

Opciones de configuración

Controle el comportamiento del trazado mediante ClickHouseDiagnosticsOptions:
Activar IncludeSqlInActivityTags puede exponer datos sensibles en las trazas. Use esta opción con precaución en entornos de producción.

Configuración de TLS

Al conectarse a ClickHouse por HTTPS, puede configurar TLS/SSL de varias formas.

Validación personalizada de certificados

En entornos de producción que requieran una lógica de validación de certificados personalizada, proporcione su propia instancia de HttpClient con un controlador ServerCertificateCustomValidationCallback configurado:
Consideraciones importantes al proporcionar un HttpClient personalizado
  • Descompresión automática: mantenga AutomaticDecompression desactivado. El controlador descomprime las respuestas por su cuenta, por lo que no es necesario activarlo. Además, activarlo puede causar problemas al enviar solicitudes: en el momento del envío, el handler también añade todos los algoritmos de su máscara al encabezado Accept-Encoding saliente. Esto amplía los algoritmos anunciados por el controlador y permite que ClickHouse responda con un códec que no se había solicitado. Consulte Descompresión de respuestas.
  • Tiempo de espera de inactividad: configure PooledConnectionIdleTimeout con un valor inferior al keep_alive_timeout del servidor (10 segundos para ClickHouse Cloud) para evitar errores causados por conexiones semiabiertas.

Optimización del rendimiento

En esta sección se explica cómo usar el client para obtener un rendimiento óptimo y qué opciones puede ajustar para adaptarlo a las necesidades de su caso de uso.

De un vistazo

| Si | Haga esto | |---|---|---| | Lee filas en objetos POCO | Use QueryAsync<T>, no MapTo<T> | | Realiza inserciones masivas | Aumente InsertOptions.BatchSize | | Ejecuta una aplicación de consola o un proceso de trabajo que realiza muchas inserciones | Active Server GC | | Lee resultados voluminosos a través de la red | Mantenga activada la compresión de las respuestas (activada de forma predeterminada) | | Inserta datos a través de una conexión rápida | Pruebe InsertOptions.Compressor = null | | Inserta datos repetidamente en la misma tabla | Use UseSchemaCache o ColumnTypes | | Lee resultados muy voluminosos | Aumente ReadBufferSize |

Lectura: elija la ruta de materialización

Hay tres formas de obtener una fila de un resultado, y no todas tienen el mismo costo. Algunas encapsulan los valores como objetos, lo que aumenta las asignaciones de memoria y reduce el rendimiento. Para una lectura de 1,000,000 filas y 105 columnas del conjunto de datos hits:
Los ORM usan la ruta rápida cuando emplean métodos de acceso tipados. linq2db registra GetInt64, GetDouble y GetDateTime para cada columna, lo que le permite leer sin boxing. El código que lee mediante GetValue (incluido un resultado dynamic de Dapper) aplica boxing a cada valor. Si una consulta de un ORM se ejecuta con frecuencia y lee mediante GetValue, use QueryAsync<T> para esa consulta.

Inserción: tamaño de lote y paralelismo

El tamaño de lote es el factor que más influye en el caudal de inserción. InsertOptions.BatchSize tiene un valor predeterminado de 100 000 filas. Utilice lotes grandes. En una inserción de 1 000 000 de filas, aumentar el tamaño de cada lote de 10 000 a 100 000 filas dio estos resultados: Si no puede controlar el tamaño de lote (por ejemplo, porque muchos productores envían pequeñas cantidades de filas por separado), utilice la inserción asíncrona y deje que el servidor agrupe las filas en lotes. Envíos paralelos. InsertOptions.MaxDegreeOfParallelism tiene un valor predeterminado de 1. Auméntelo para enviar varios lotes a la vez. Esto resulta especialmente útil si la compresión está activada, ya que cada lote se comprime en su propio thread. Las sesiones no son compatibles con las inserciones paralelas: desactívelas o mantenga MaxDegreeOfParallelism = 1. Elimine la consulta de comprobación del esquema. Cada llamada a InsertBinaryAsync envía primero una consulta SELECT ... WHERE 1=0 para determinar los tipos de columna. Consulte Cómo omitir la consulta de comprobación del esquema para evitar ese viaje de ida y vuelta mediante ColumnTypes o UseSchemaCache.
La ruta de inserción sin boxing se aplica al formato RowBinary predeterminado. RowBinaryWithDefaults debe examinar cada valor para encontrar el marcador DBDefault, por lo que sigue utilizando la ruta más lenta.

Compresión: los resultados varían según la dirección

La compresión reduce la cantidad de bytes transferidos a cambio de consumir CPU. Que compense depende de la dirección de la transferencia, del ancho de banda de la conexión con el servidor de ClickHouse, de la eficacia del algoritmo de compresión elegido con sus datos y de si paga por cada byte transferido. Lecturas: mantenga activada la compresión, salvo que el servidor se ejecute en la misma máquina. Este es el comportamiento predeterminado. En comparación con la transferencia sin compresión, zstd en el nivel 1 dio estos resultados: Inserciones: mida el efecto antes de activar la compresión. El ahorro quizá no sea suficiente para justificarla. Tenga en cuenta también que la descompresión aumenta la carga del servidor; ese aumento es moderado con Zstd y LZ4, pero puede ser considerable con otros algoritmos (por ejemplo, Brotli). Para desactivar la compresión de inserciones:
Para elegir un códec, ajustar los niveles de compresión y determinar a partir de qué punto le conviene comprimir, consulte Optimización de la compresión.

Búferes

ReadBufferSize establece el tamaño del búfer utilizado para leer las respuestas HTTP. Su valor predeterminado es 64 KiB. El controlador obtiene este búfer de un grupo compartido y lo devuelve cuando libera el lector, por lo que no necesita asignar uno por cada consulta. Aumente su tamaño para reducir la frecuencia con la que debe rellenarse el búfer al procesar resultados grandes. El controlador mantiene un búfer por cada lector abierto simultáneamente, por lo que el consumo de memoria aumenta tanto con el tamaño del búfer como con el número de lectores concurrentes.
Libere siempre los recursos de los lectores. Al hacerlo, el lector devuelve su búfer al grupo y libera su conexión HTTP. Si se abandona un lector sin liberar sus recursos, el búfer no vuelve al grupo y la conexión HTTP puede quedar inutilizable; la recolección de basura no sustituye la liberación explícita de recursos.

Entorno de ejecución y recolección de basura

Active Server GC en aplicaciones que realizan muchas inserciones. Con el mismo código y la misma cantidad de bytes asignados, las inserciones con Workstation GC fueron hasta un 97 % más lentas que con Server GC.
Los proyectos de ASP.NET Core ya tienen esta opción configurada. Las aplicaciones de consola, los servicios de trabajo y la mayoría de las imágenes de contenedor no la tienen. La causa es el tamaño del presupuesto de la generación 0. Workstation GC utiliza un presupuesto pequeño, por lo que los búferes de corta duración que crea una inserción sobreviven a la generación 0 y pasan a la generación 1. Esto aumenta la promoción y el trabajo de la generación 2. En una prueba de inserción, se registraron 4,000 recolecciones de la generación 2 por cada 1,000 operaciones con Server GC, frente a 73,000 con Workstation GC.
Server GC es una configuración orientada al caudal, no a la latencia. En las mismas mediciones, Server GC acumuló menos de la mitad del tiempo total de pausa, pero cada pausa fue más larga (percentil 95: 114.6 ms frente a 61.9 ms). Si su servicio es sensible a la latencia de las solicitudes más lentas, mida ambos modos antes de elegir uno.

Latencia: reutilizar conexiones

Establecer una nueva conexión TCP y completar el handshake TLS lleva bastante tiempo. Reutilizar las conexiones reduce considerablemente la latencia de las consultas.
  • No cree un cliente por cada solicitud. Cada cliente nuevo con su propio HttpClient crea un nuevo grupo de conexiones y requiere otro handshake. Utilice un único ClickHouseClient durante toda la vida útil de la aplicación. Es seguro para subprocesos y está diseñado para usarse como singleton.
  • Para ADO.NET y los ORM, utilice ClickHouseDataSource para que todas las conexiones compartan el mismo grupo.
Para conocer todos los patrones, consulte Duración y agrupación de conexiones.

Mida el rendimiento usted mismo

En muchos casos, el rendimiento dependerá de la estructura de sus datos, de la velocidad de la conexión con el servidor, de si prefiere aumentar el uso de CPU del client para reducir el del servidor (o viceversa), de las limitaciones de su hardware, etc. Por eso, le recomendamos que mida el rendimiento con sus propios datos y en su entorno. Para conocer la parte del trabajo que realiza el servidor, establezca QueryOptions.QueryId y consulte los contadores:

Compatibilidad con ORM

Los ORM requieren la API ADO.NET (ClickHouseConnection). Para gestionar correctamente la duración de las conexiones, créelas a partir de un ClickHouseDataSource:

Dapper

ClickHouse.Driver funciona con Dapper. El controlador convierte automáticamente la sintaxis @parameter de Dapper a la sintaxis nativa {parameter:Type} de ClickHouse e infiere los tipos a partir de los valores de .NET. Utilice ClickHouseDataSource para gestionar correctamente la duración de la conexión:

Formas de pasar parámetros

Dapper admite todas las formas estándar de pasar parámetros: Objetos anónimos:
Clases POCO:
Diccionario:
DynamicParameters (a partir de un diccionario o un objeto anónimo):

Consultas que devuelven objetos POCO

Dapper relaciona las columnas con las propiedades por nombre (sin distinguir entre mayúsculas y minúsculas):

Sintaxis de parámetros nativa de ClickHouse

Si necesita controlar explícitamente el tipo, use la sintaxis {param:Type} de ClickHouse directamente en SQL y proporcione los valores de los parámetros mediante un Dictionary<string, object>. No combine las sintaxis @param y {param:Type} para un mismo parámetro.

WHERE IN

La expansión nativa de IN en Dapper funciona:
Dapper reescribe la consulta como WHERE id IN (@Ids1, @Ids2, @Ids3), y el controlador convierte cada parámetro expandido. La función has() de ClickHouse también funciona con un parámetro de tipo Array:

Handlers de tipos personalizados

Algunos tipos de ClickHouse, como ITuple, BigInteger y ClickHouseDecimal, requieren registrar handlers al inicio:
Consulte el ejemplo de Dapper para ver cómo se implementa un controlador de tipos personalizado.

Dapper.Contrib

GetAll<T>() y Get<T>(id) funcionan. Insert<T>(), en cambio, no: genera sintaxis propia de SQL Server (SCOPE_IDENTITY, []). Se recomienda utilizar el método nativo InsertBinaryAsync de ClickHouseClient en su lugar.
Los nombres de las propiedades deben coincidir exactamente con los nombres de las columnas de ClickHouse (se distinguen mayúsculas y minúsculas).

Limitaciones

Linq2db

Este controlador es compatible con linq2db, un ORM ligero y un proveedor de LINQ para .NET. Encontrará documentación detallada en el sitio web del proyecto. Ejemplo de uso: Cree una DataConnection con el proveedor de ClickHouse:
Las correspondencias de tablas se pueden definir mediante atributos o mediante una configuración fluida. Si los nombres de tus clases y propiedades coinciden exactamente con los nombres de las tablas y columnas, no necesitas ninguna configuración adicional:
Consultas:
Copia masiva: Utilice BulkCopyAsync para insertar grandes volúmenes de datos de forma eficiente.

Entity Framework Core

El proveedor oficial de Entity Framework Core para ClickHouse. Asocie clases de C# a tablas de ClickHouse, haga consultas con LINQ e inserte datos mediante SaveChanges, todo ello con los patrones habituales de EF Core.
Este proveedor está en desarrollo activo. La versión actual admite consultas LINQ (incluidos JOIN, subconsultas y operaciones de conjuntos), INSERT mediante SaveChanges / BulkInsertAsync, migraciones con DDL completo (CREATE / ALTER / DROP) y la configuración de motores de tabla específicos de ClickHouse. UPDATE / DELETE no son compatibles.

Instalación

Requiere .NET 10.0 y EF Core 10.

Inicio rápido

Defina la entidad y el DbContext y, después, haga consultas con LINQ:

Tipos compatibles

Use ClickHouseDecimal (de ClickHouse.Driver.Numerics) en lugar de decimal si necesita toda la precisión de las columnas Decimal128/Decimal256: el tipo decimal de .NET admite como máximo entre 28 y 29 dígitos significativos.

Operaciones LINQ compatibles

Consultas: Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking GROUP BY y agregaciones: GroupBy con Count, LongCount, Sum, Average, Min y Max; también admite HAVING (.Where() después de .GroupBy()), varias agregaciones en una misma proyección y OrderBy aplicado a resultados de agregaciones. JOIN: Join (INNER) y patrones GroupJoin/SelectMany (LEFT y CROSS). LEFT JOIN devuelve null para las filas sin coincidencia (consulte Semántica de los valores nulos en LEFT JOIN más abajo). Subconsultas: Contains / IN correlacionados, Any / EXISTS, All y subconsultas escalares en proyecciones. Operaciones de conjuntos: Concat (→ UNION ALL), Union (→ UNION DISTINCT), Intersect, Except. Colecciones locales en línea: las uniones y Contains con colecciones en memoria (int[], List<T>, etc.) se traducen en una serie de operaciones UNION. Métodos de cadenas: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat (y el operador +). Funciones matemáticas: los métodos estándar de Math y MathF se traducen a las funciones equivalentes de ClickHouse: aritméticas, logarítmicas, trigonométricas y auxiliares. El provider añade automáticamente set_join_use_nulls=1 a todas las rutas de conexión para que el comportamiento de JOIN coincida con el que espera Entity Framework. Si el servidor de ClickHouse o el perfil impiden cambiar esta configuración (por ejemplo, un perfil con readonly=1), desactive esta opción con:
Cuando se activa la opción de exclusión, LEFT JOIN devuelve los valores predeterminados de las columnas de ClickHouse, por lo que la detección de propiedades de navegación basada en valores nulos de EF deja de funcionar como se espera. Compare explícitamente con 0 / "" en lugar de usar == null.

Inserción de datos

SaveChanges utiliza la API nativa InsertBinaryAsync del controlador, con codificación RowBinary y un cuerpo de solicitud comprimido, lo que resulta mucho más eficiente que el SQL parametrizado:
Las entidades pasan de Added a Unchanged después de guardarse, como ocurre con cualquier otro provider de EF Core. El tamaño del lote es configurable (valor predeterminado: 1000):

Inserción masiva

Para cargas de datos de alto caudal, use BulkInsertAsync en lugar de SaveChanges. Este método de extensión de DbContext prescinde por completo del seguimiento de cambios, la resolución de identidades y la gestión de estados de EF Core: llama directamente a InsertBinaryAsync del controlador con codificación RowBinary y un cuerpo de solicitud comprimido. Por eso, resulta adecuado para cargar grandes conjuntos de datos si no necesita hacer un seguimiento de las entidades después de insertarlas:
La entrada puede ser cualquier IEnumerable<T>: las entidades se procesan una a una sin cargarlas todas en memoria. El valor devuelto indica cuántas filas se han insertado. Tras la inserción, las entidades no quedan asociadas al DbContext, por lo que no pasan del estado Added al estado Unchanged.

Enumeraciones

Las columnas Enum8/Enum16 de ClickHouse se pueden mapear a propiedades de tipo string o a tipos enum de C#. Al usar enumeraciones de C#, el provider convierte automáticamente entre la enumeración y su representación en cadena:

Conversiones de tipos personalizados

El sistema ValueConverter de EF Core permite convertir tipos personalizados en tipos que el provider ya admite. El provider nunca llega a trabajar con el tipo personalizado: EF Core lo convierte al pasar de una capa a otra. Conversión por propiedad:
Class de conversor reutilizable:

Anotaciones del tipo de columna

Para los tipos escalares como string, int, DateTime, etc., el provider infiere automáticamente el tipo de ClickHouse. Para los tipos parametrizados y las envolturas, debe indicar el tipo de ClickHouse de forma explícita. Uso de anotaciones de datos (atributos):
Uso de la API fluida en OnModelCreating:
Se admiten envolturas anidadas como Array(Nullable(Int32)) y LowCardinality(Nullable(String)): el provider elimina automáticamente las envolturas Nullable y LowCardinality en todos los niveles de anidamiento.

Columnas Variant y Dynamic

Las columnas Variant(T1, T2, ...) y Dynamic de ClickHouse se representan como object en .NET. Como object es demasiado genérico para inferir el tipo automáticamente, debe especificar el tipo de almacenamiento mediante .HasColumnType():
Al leer el valor, este se deserializa automáticamente al tipo de .NET correspondiente al discriminador almacenado (p. ej., string, ulong, ulong[]).

Columnas JSON

El provider admite el tipo de columna Json de ClickHouse y lo asigna a System.Text.Json.Nodes.JsonNode (opción principal) o a string (mediante un ValueConverter automático):
La lectura y escritura de JSON son compatibles tanto con SaveChanges como con BulkInsertAsync:
Si prefiere cadenas JSON sin procesar, configure la propiedad como string con un tipo de columna Json; el provider aplicará automáticamente un ValueConverter:
  • No se traducen las rutas JSON — entity.Data["name"] en LINQ no se convierte a la sintaxis SQL data.name de ClickHouse. Filtre por columnas que no sean JSON y examine el contenido JSON en memoria.
  • Semántica de NULL — El tipo JSON de ClickHouse devuelve {} (un objeto vacío) para los valores NULL, en lugar de SQL NULL.
  • Precisión de los enteros — ClickHouse almacena todos los enteros en JSON como Int64. Al leerlos mediante JsonNode, use GetValue<long>() en lugar de GetValue<int>().

Motores de tabla

Configure los motores de tabla de ClickHouse y las cláusulas específicas de cada motor mediante la API fluida ToTable(name, t => ...). Si no se configura ningún motor, el provider usa MergeTree de forma predeterminada y determina ORDER BY a partir de la clave primaria de la entidad.
Familias de motores compatibles: Cláusulas del motor: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Todas se añaden al constructor de motores que devuelve HasXxxEngine(). Funciones a nivel de columna: HasCodec, HasTtl, HasComment, HasDefault — todas se tienen en cuenta en las migraciones. Índices de omisión de datos — mediante HasIndex(...).HasSkippingIndexType(...):
Los índices estándar (que no son de omisión de datos) se ignoran sin previo aviso porque no tienen un equivalente en ClickHouse. Los índices únicos provocan una excepción porque ClickHouse no garantiza la unicidad.

Migraciones

Flujo de trabajo estándar para las migraciones de EF Core:
Operaciones compatibles:

Limitaciones de las migraciones

Al margen de las migraciones, el provider tampoco admite todavía:
  • UPDATE / DELETE
  • Transacciones: BeginTransaction no tiene efecto. ClickHouse no admite transacciones ACID.
  • Traducción de consultas con rutas JSON: entity.Data["key"] en LINQ no se traduce a la sintaxis SQL data.key de ClickHouse. Filtre por columnas que no sean JSON y examine el JSON en memoria.

Limitaciones

Tuplas de 8 o más elementos con una tupla anidada en la última posición

Los tipos ValueTuple de C# con más de 7 elementos utilizan un esquema de anidamiento generado por el compilador: el octavo argumento genérico (TRest) es un ValueTuple que contiene los elementos restantes. Por ejemplo, (int, int, int, int, int, int, int, string, string) se compila en ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. Esto genera una ambigüedad cuando una columna de ClickHouse contiene una tupla de 8 elementos y el último es, a su vez, una tupla; por ejemplo, Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). El controlador no puede distinguir entre:
  • Una tupla plana de 9 elementos (con el anidamiento de TRest generado por el compilador)
  • Una tupla de 8 elementos cuyo último elemento es una Tuple(String, String) anidada
Ambas producen el mismo tipo .NET: ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. El controlador interpreta el octavo argumento como TRest (es decir, lo aplana), por lo que serializa incorrectamente la tupla de 8 elementos con una tupla anidada. Esto afecta tanto a System.Tuple como a ValueTuple, ya que ambos utilizan el anidamiento de TRest cuando tienen más de 7 elementos. Las tuplas de 7 elementos o menos y aquellas cuyo último elemento no es una tupla no se ven afectadas. Solución alternativa: Envuelva la tupla interna en un nivel adicional de anidamiento para que el controlador pueda distinguirla del anidamiento de TRest:

Columnas de tipo AggregateFunction

Las columnas de tipo AggregateFunction(...) no se pueden consultar ni insertar directamente. Para insertar datos:
Para seleccionar:

Última modificación el 14 de octubre de 2026