-
ClickHouseClient(recomendado): un cliente de alto nivel, seguro para subprocesos, diseñado para usarse como singleton. Ofrece una API asíncrona sencilla para 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 base de datos de .NET. Son necesarias para la integración con ORM (Dapper, Linq2db) y cuando necesita compatibilidad con ADO.NET.ClickHouseBulkCopyes una clase auxiliar para insertar datos de forma eficiente mediante una conexión ADO.NET.ClickHouseBulkCopyestá obsoleto y se eliminará en una versión futura; en su lugar, useClickHouseClient.InsertBinaryAsync.
Guía de migración
- Actualiza tu archivo
.csprojcon el nuevo nombre del paqueteClickHouse.Drivery la versión más reciente en NuGet. - Actualiza en tu código todas las referencias de
ClickHouse.ClientaClickHouse.Driver.
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 de ClickHouse compatibles
El Client admite oficialmente las 3 versiones más recientes, además de las 2 últimas versiones LTS.Instalación
Instale el paquete desde NuGet:Inicio rápido
Configuración
Hay dos formas de configurar su conexión a ClickHouse:- Cadena de conexión: pares clave/valor separados por punto y coma que especifican el host, las credenciales de autenticación y otras opciones de conexión.
- Objeto
ClickHouseClientSettings: objeto de configuración fuertemente tipado que puede cargarse desde archivos de configuración o establecerse en el código.
Configuración de la conexión
Formato de datos y serialización
Gestión de sesiones
El indicador
UseSession habilita la persistencia de la sesión del servidor, lo que permite usar sentencias SET y tablas temporales. Las sesiones se reinician tras 60 segundos de inactividad (timeout predeterminado). La duración de la sesión puede ampliarse configurando ajustes de sesión mediante sentencias de ClickHouse o la configuración del servidor.La clase ClickHouseConnection normalmente permite operaciones en paralelo (varios hilos pueden ejecutar consultas de forma concurrente). Sin embargo, habilitar el indicador UseSession lo limita a una sola consulta activa por conexión en un momento dado (esta es una limitación del lado del servidor).Seguridad
Configuración del cliente HTTP
Registro y depuración
Ajustes personalizados y roles
Al usar una cadena de conexión para establecer ajustes personalizados, usa el prefijo
set_, p. ej., “set_max_threads=4”. Al usar un objeto ClickHouseClientSettings, no uses el prefijo set_.Para ver la lista completa de ajustes disponibles, consulta aquí.Ejemplos de cadenas de conexión
Conexión básica
Con ajustes personalizados de ClickHouse
QueryOptions
QueryOptions permite anular la configuración del cliente para una consulta concreta. Todas las propiedades son opcionales y solo anulan los valores predeterminados del cliente cuando se especifican.
Ejemplo:
InsertOptions
InsertOptions amplía QueryOptions con opciones específicas para operaciones de inserción masiva mediante InsertBinaryAsync.
Todas las propiedades de
QueryOptions también están disponibles en InsertOptions.
Ejemplo:
Omitir la consulta de sondeo 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. Para escenarios de alto rendimiento, puedes eliminar esta sobrecarga con dos opciones:
Opción 1: Proporcionar los tipos de columna explícitamente
Cuando conoces el esquema de la tabla en tiempo de compilación, pásalo directamente mediante ColumnTypes. No se envía ninguna consulta de esquema en absoluto:
UseSchemaCache = true para consultar el esquema una sola vez y reutilizarlo en las inserciones posteriores de la misma instancia de ClickHouseClient:
ColumnTypestiene prioridad sobreUseSchemaCache. Si se configuran ambos, se usan los tipos explícitos.- La caché de esquema no detecta cambios realizados con
ALTER TABLE. Si modifica el esquema de la tabla, cree un nuevoClickHouseCliento eviteUseSchemaCachepara esa tabla. - La caché se limita a la instancia de
ClickHouseClienty se indexa por (database, table). Los distintos subconjuntos de columnas de una misma tabla comparten un único esquema en caché.
ClickHouseClient
ClickHouseClient es la API recomendada para interactuar con ClickHouse. Es seguro para subprocesos, está diseñado para usarse como singleton y gestiona internamente el pool de conexiones HTTP.
Crear un Client
Cree unClickHouseClient con una cadena de conexión o un objeto ClickHouseClientSettings. Consulte la sección Configuración para ver las opciones disponibles.
Los detalles de su servicio de ClickHouse Cloud están disponibles en la consola de ClickHouse Cloud.
Seleccione un servicio y haga clic en Connect:
Elija C#. Los detalles de la conexión se muestran a continuación.
Si utiliza ClickHouse autogestionado, los detalles de la conexión los establece el administrador de ClickHouse.
Usar una cadena de conexión:
ClickHouseClientSettings:
IHttpClientFactory:
ClickHouseClient está diseñado para ser de larga duración y para compartirse en toda la aplicación. Créelo una sola vez (normalmente como un singleton) y reutilícelo para todas las operaciones de base de datos. El Client administra internamente el pool de conexiones HTTP.Ejecutar consultas
UseExecuteNonQueryAsync para las sentencias que no devuelven resultados:
ExecuteScalarAsync para obtener un único valor:
Insertar datos
Inserciones parametrizadas
Inserta datos mediante consultas parametrizadas conExecuteNonQueryAsync. Los tipos de los parámetros deben especificarse en el SQL usando la sintaxis {name:Type}:
Inserciones masivas
UseInsertBinaryAsync para insertar un gran número de filas de forma eficiente. Transmite los datos mediante el formato binario nativo de filas de ClickHouse, admite envíos por lotes en paralelo y evita los errores de “URL too long” que pueden producirse con consultas parametrizadas.
InsertOptions:
- El cliente obtiene automáticamente la estructura de la tabla mediante
SELECT * FROM <table> WHERE 1=0antes de insertar. Los valores proporcionados deben coincidir con los tipos de las columnas de destino. Para omitir esta consulta, useInsertOptions.ColumnTypesoInsertOptions.UseSchemaCache. - Cuando
MaxDegreeOfParallelism > 1, los batches se cargan en paralelo. Las sesiones no son compatibles con la inserción en paralelo; desactive las sesiones o establezcaMaxDegreeOfParallelism = 1. - Use
RowBinaryFormat.RowBinaryWithDefaultsenInsertOptions.Formatsi desea que el servidor aplique valores DEFAULT a las columnas no proporcionadas.
Inserciones con POCO
En lugar de construir arraysobject[], puede insertar directamente objetos POCO fuertemente tipados. Registre el tipo una sola vez y luego pase IEnumerable<T>:
Cuando todas las propiedades mapeadas especifican un
Type explícito, la consulta de sondeo del esquema se omite por completo. Cuando solo algunas propiedades tienen tipos explícitos, el driver recurre a la consulta de sondeo del esquema para el conjunto completo de columnas.
InsertBinaryAsync<T> admite las mismas InsertOptions (agrupación en lotes, paralelismo, almacenamiento en caché del esquema) que la sobrecarga object[].
A diferencia de la sobrecarga
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, sobrescribirá los atributos del POCO.Evolución del esquema
Las inserciones de POCO funcionan sin problemas cuando se añaden columnas a la tabla de destino después de registrar el tipo. Como el driver solo inserta las columnas asignadas por el POCO, cualquier columna nueva conDEFAULT (u otras expresiones predeterminadas) la rellena automáticamente el servidor. No se requieren cambios en el código ni volver a registrar nada.
Posición de la consulta de inserción
Un insert binario escribe su sentenciaINSERT INTO ... FORMAT ... como primera línea del request body, antes de las filas. El body se comprime de forma predeterminada, por lo que el enrutamiento y el logging que solo inspeccionan la URL no ven la sentencia. Establezca InsertOptions.QueryPlacement en InsertQueryPlacement.Url para enviar la sentencia como el URL parameter query, dejando el body solo para las filas:
query, o cuando desee que la sentencia aparezca en los logs de acceso y en las herramientas de observabilidad. Es opcional porque, en ese caso, la sentencia cuenta para la longitud de la URL. El límite efectivo es el más bajo de los impuestos por el runtime de .NET, un intermediario y el server. Desde .NET 6 hasta .NET 9, System.Uri limita el URI de solicitud completo codificado a 65.519 caracteres; el driver lanza una InvalidOperationException que lo remite de nuevo a InsertQueryPlacement.Body cuando se supera este límite. En ClickHouse, http_max_uri_size es de 1 MiB de forma predeterminada, aunque un intermediario puede imponer un límite inferior. En el modo body, la sentencia y las filas no están sujetos a ese límite de longitud de URL; otras opciones de la solicitud sí pueden aparecer en la URL.
Esta configuración es independiente de Compressor: el body se codifica de la misma manera en ambos modos.
Lectura de datos
UseExecuteReaderAsync para ejecutar consultas SELECT. El ClickHouseDataReader devuelto proporciona acceso tipado a las columnas del resultado mediante métodos como GetInt64(), GetString() y GetFieldValue<T>().
Llame a Read() para avanzar a la fila siguiente. Devuelve false cuando ya no quedan más filas. Acceda a las columnas por índice (empezando en 0) o por nombre de columna.
Lectura de POCO
En lugar de leer columnas por índice o por nombre, puedes enviar los resultados de la consulta directamente a tus propias clases. Registra el tipo una sola vez con el Client y luego usaQueryAsync<T>:
RegisterPocoType<T>() configura tanto los mapeos de inserción como los de lectura y valida ambos desde el principio. RegisterBinaryInsertType<T>() no cambia y sigue siendo solo para inserción 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 propiedadesrequired.
InvalidOperationException. Por lo tanto, una propiedad object acepta cualquier columna.
QueryAsync<T> lee cada una de estas columnas directamente en una propiedad coincidente:
Cada fila admite también la forma anulable de su tipo de propiedad (
long?, DateOnly?, etc.),
sea o no la columna Nullable(...). Una propiedad de tipo de valor no anulable sobre una columna
Nullable(T) se acepta en el registro, pero lanza una excepción cuando llega un NULL.
Las envolturas como LowCardinality(T), SimpleAggregateFunction(f, T) y Object(T) se corresponden exactamente con T.
También se admiten las columnas compuestas, que adoptan el tipo del framework indicado en la
referencia de tipos de lectura: Array(T) a T[], Tuple(...)
a System.Tuple<...>, Nested(...) a Tuple<...>[], JSON a JsonObject (o 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 por la ruta sin boxing y conserva el orden del flujo binario y las claves repetidas, en cualquiera de los
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 ofrece más de un tipo de propiedad (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
pertenecen a la ruta POCO, por lo que QueryAsync<T> dispone de ellas y MapTo<T> no.
Al iterar manualmente sobre un lector, use ClickHouseDataReader.MapTo<T>() para materializar la fila actual en un POCO registrado sin hacer avanzar el lector:
MapTo<T> cuando necesite controlar usted mismo el bucle del lector; por ejemplo, para combinar el acceso directo a columnas
con la materialización de POCO. Lee la fila a través de los valores encajonados (boxed) del lector, por lo que no ofrece
los tipos de propiedad alternativos indicados arriba y realiza más asignaciones de memoria que QueryAsync<T>. Es preferible usar
QueryAsync<T> cuando solo necesite las filas; consulte
elegir la ruta de materialización para ver las cifras.
Un convertidor de valores de lectura a nivel de client o por consulta se aplica a ambas rutas y
no desactiva la lectura sin boxing. El driver convierte cada columna mediante la sobrecarga que corresponde a la forma en que
se leyó la columna: el ConvertValue<T> tipado para una columna
sin boxing, y el ConvertValue con boxing para una columna compuesta. Implemente ambas sobrecargas
de forma coherente; de lo contrario, una misma columna dará resultados distintos según la ruta.
Cuando se configura un LoggerFactory, RegisterPocoType<T>() y RegisterBinaryInsertType<T>() emiten un registro de nivel Debug (categoría ClickHouse.Driver.Client) en el que se indica qué propiedades se asignaron a qué columnas y cuáles se omitieron, además del motivo. Consulte Registro y diagnóstico.
Parámetros SQL
En ClickHouse, el formato estándar de los parámetros en las consultas SQL es{parameter_name:DataType}.
Ejemplos:
Los parámetros SQL ‘bind’ se pasan como parámetros de consulta en la URI HTTP, por lo que usar demasiados puede provocar una excepción de “URL demasiado larga”. Use
InsertBinaryAsync para la inserción masiva de datos y así evitar esta limitación.Marcadores de posición @name de estilo ADO
El driver también acepta marcadores de posición @name, como los que emiten ORM del tipo Dapper. Son una
comodidad del lado del cliente: antes de enviarse la petición, cada uno se reescribe como
{name:ResolvedType}, de modo que el servidor nunca llega a ver una @. Consulta
la resolución de tipos para saber cómo se elige el tipo. Usa la forma explícita
{name:Type} siempre que puedas.
Un @name sin ningún parámetro coincidente se deja intacto para que el servidor lo rechace. La coincidencia es
sensible a mayúsculas y minúsculas, por lo que @ID no vincula un parámetro llamado id.
Para desactivar la reescritura, activa el interruptor de AppContext
ClickHouse.Driver.DisableReplacingParameters
antes del primer uso del driver. Solo se detiene la reescritura del texto; los parámetros se siguen enviando, por lo que
las consultas escritas con la sintaxis nativa {name:Type} siguen funcionando.Parámetros de Identifier
El tipo de parámetroIdentifier le permite especificar de forma segura el nombre de una base de datos, tabla o columna en lugar de un literal de cadena entre comillas. Úselo mediante la sintaxis {name:Identifier} en SQL, o estableciendo ClickHouseDbParameter.ClickHouseType = "Identifier":
ID de consulta
A cada consulta se le asigna unquery_id único que puede usarse para obtener datos de la tabla system.query_log o cancelar consultas de larga duración. Puedes especificar un ID de consulta personalizado mediante QueryOptions:
Correspondencia personalizada de tipos de parámetros
Al usar parámetros con el estilo@ (por ejemplo, WHERE id = @id), el driver infiere automáticamente el tipo de ClickHouse a partir del tipo de valor de .NET. Por ejemplo, int se corresponde con Int32.
Para anular estos valores predeterminados, configure ParameterTypeResolver en ClickHouseClientSettings. Esto resulta útil cuando desea que todos los parámetros DateTime usen DateTime64(3) con precisión de milisegundos, o que todos los decimales usen una escala específica, sin tener que establecer ClickHouseType en cada parámetro individual.
Uso de DictionaryParameterTypeResolver para correspondencias de tipos simples:
IParameterTypeResolver personalizado para casos avanzados:
Para una resolución basada en el valor o en el nombre, implemente directamente la interfaz IParameterTypeResolver. Devuelva null para que se aplique la inferencia predeterminada:
QueryOptions.ParameterTypeResolver. Cuando se establece, tiene prioridad sobre el resolver a nivel de cliente.
Precedencia de la resolución de tipos:
El resolver es un paso dentro de una cadena de precedencia. De mayor a menor prioridad:
ClickHouseTypeexplícito establecido en el parámetro- Indicación de tipo SQL de la sintaxis
{name:Type}en la consulta IParameterTypeResolver(deQueryOptions.ParameterTypeResolver, con fallback aClickHouseClientSettings.ParameterTypeResolver)- Inferencia de tipos integrada (
TypeConverter.ToClickHouseType)
ClickHouseConnection de ADO.NET: las conexiones creadas desde el cliente heredan la configuración.
Formato personalizado de valores de parámetros
IParameterFormatter es un hook que determina cómo se serializan los valores de los parámetros. Úselo cuando el formato integrado (por ejemplo, la precision de DateTime, la configuración regional de los decimales, el escape de cadenas o la representación de números) no coincida con lo que espera su schema o sus herramientas de destino.
Configure ParameterFormatter en ClickHouseClientSettings para instalar un formateador para todas las consultas parametrizadas. El formateador recibe el valor, el type name de ClickHouse resuelto y el nombre del parámetro, y devuelve la string representation que se envía al server. Devuelva null para que se use el formateador predeterminado.
Uso de DictionaryParameterFormatter para un formato sencillo por tipo de CLR:
IParameterFormatter personalizado para casos avanzados:
QueryOptions.ParameterFormatter. Cuando se establece, tiene prioridad sobre el formateador a nivel de client.
Valores compuestos:
El formateador se ejecuta tanto para los parámetros de collection de nivel superior como para cada elemento dentro de valores compuestos (Array, Tuple, Map, Nullable, LowCardinality, Variant). Por ejemplo, una correspondencia de typeof(int) formatea individualmente cada elemento Int32 de un Array(Int32).
Comillas simples en contextos compuestos:
Para los tipos de ClickHouse similares a cadenas (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) incrustados dentro de un literal compuesto, el driver encierra la salida del formateador entre comillas simples, pero no escapa su contenido. Si la cadena devuelta contiene una comilla simple o una barra invertida sin escape, el literal compuesto quedará mal formado y el server rechazará la consulta.
Los parámetros de cadena de nivel superior (no incrustados en un compuesto) se usan textualmente, sin comillas adicionales, por lo que no es necesario aplicar escaping en ese caso.
Prioridad del formateador:
IParameterFormatter(deQueryOptions.ParameterFormatter, con respaldo enClickHouseClientSettings.ParameterFormatter). Si devuelve un valor no nulo, se usa ese valor.- Formato integrado específico del tipo en
HttpParameterFormatter.
null o DBNull; esos siempre se serializan como el centinela nulo de ClickHouse (\N).
Conversión personalizada de valores leídos
IReadValueConverter permite transformar los valores devueltos por el lector de datos después de la deserialización, sin cambiar su tipo CLR. Usos habituales: establecer DateTime.Kind = Utc en una columna DateTime que no tiene zona horaria, recortar o normalizar cadenas, o posprocesar una columna JSON antes de que llegue al código de la aplicación.
Configure ReadValueConverter en ClickHouseClientSettings para instalar un convertidor para todas las operaciones de lectura. El convertidor se invoca una vez por columna y por fila, tanto en la variante boxed (GetValue) como en la genérica (GetFieldValue<T>). Si no se configura ningún convertidor, no hay sobrecarga: el lector devuelve los valores directamente.
Uso de DictionaryReadValueConverter para una conversión sencilla por tipo CLR:
For<T> se devuelven sin cambios. La resolución se basa en el tipo CLR exacto, así que registre el tipo real que produce el lector (p. ej., For<JsonObject> para una columna JSON en JsonReadMode.Binary).
IReadValueConverter personalizado para escenarios avanzados:
Si necesita resolver según la cadena de tipo del lado de ClickHouse (por ejemplo, para distinguir DateTime de DateTime('UTC') — ambos aparecen como el mismo tipo CLR), implemente IReadValueConverter directamente:
GetFieldType, GetSchemaTable) no se redirigen a través de él y deben seguir siendo coherentes con lo que se devuelve.
También puedes establecer un convertidor por consulta mediante QueryOptions.ReadValueConverter; cuando se establece, tiene prioridad sobre el convertidor a nivel de Client.
Límite de despacho:
El convertidor se invoca una vez por columna con el valor completo de la celda deserializado; no desciende recursivamente a contenedores compuestos. Para una columna Array(Int32), el valor que se pasa es un int[]; para Tuple(Int32, String), es un ITuple.
Qué sobrecarga se ejecuta:
Ambas sobrecargas deben ser coherentes entre sí, porque la que invoca el driver depende de cómo el llamador haya leído la
columna:
ConvertValue<T>: los accessors tipadosGetByte,GetSByte,GetInt16/32/64,GetUInt16/32/64,GetFloat,GetDouble,GetGuid,GetDateTime,GetIPAddress,GetBigIntegeryGetFieldValue<T>, además de todas las columnas sin boxing de la ruta de lectura POCO.ConvertValue(boxed):GetValue,GetValues, los indexadores,GetChar,GetTupley las rutas de coerción enGetBoolean,GetDecimalyGetString.
IsDBNull no ejecuta ningún convertidor: lee el indicador de nulo directamente, por lo que un convertidor nunca puede
cambiar si un valor cuenta como nulo. TryGetEnumOrdinal también lo omite; consulta
lectura del ordinal de un enum.
El convertidor funciona con la ruta de ADO.NET ClickHouseConnection: la configuración se hereda en las conexiones creadas desde el Client.
Transmisión sin procesar
UseExecuteRawResultAsync para transmitir directamente los resultados de una consulta en un formato específico, omitiendo el lector de datos. Esto resulta útil para exportar datos a archivos o enviarlos a otros sistemas:
JSONEachRow, CSV, TSV, Parquet, Native. Consulta la documentación sobre formatos para conocer todas las opciones.
Compresión de transporte por consulta
De forma predeterminada, el client negociazstd, lz4, gzip, deflate cuando Compression=true (el valor predeterminado de la cadena de conexión) y descodifica el flujo por sí mismo, de forma transparente.
Para exportaciones sin procesar (p. ej., Parquet, Arrow, Native), es posible que quiera negociar un códec distinto (p. ej., zstd o lz4) para intercambiar CPU por ancho de banda sin cambiar la configuración de toda la conexión. QueryOptions.AcceptEncoding y ClickHouseCommand.AcceptEncoding establecen la cabecera HTTP Accept-Encoding para una sola solicitud, reemplazan cualquier valor predeterminado asociado y fuerzan enable_http_compression=1 en la URL (que es lo que ClickHouse requiere antes de respetar Accept-Encoding).
Configuración de HttpClient
No hay nada que configurar: elHttpClient que construye el driver deja AutomaticDecompression en DecompressionMethods.None y es el propio driver quien decodifica las respuestas, de modo que Content-Encoding nunca se elimina a tus espaldas y el cuerpo sin procesar te llega exactamente como lo envió el servidor.
Cuerpos de error
Cuando el servidor responde con un 4xx/5xx y se ha establecidoenable_http_compression=1, comprime el cuerpo del error con el mismo códec que habría usado para una respuesta satisfactoria. El driver los decodifica en el caso de todos los códecs que admite (lz4, zstd, gzip, deflate, br/brotli), de modo que el mensaje en ClickHouseServerException sea legible. Para cualquier otro caso (snappy, …) devuelve un mensaje provisional que indica el códec y remite a system.query_log para ver el texto original del error.
Descompresión de la respuesta
Accept-Encoding solo le pide al servidor que comprima la respuesta; alguien tiene que decodificarla. De eso se encarga el propio driver, a partir del Content-Encoding de la respuesta, de modo que todas las API de lectura habituales (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) funcionan sobre una respuesta comprimida sin necesidad de configurar nada. Decodifica 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. Para elegir otra opción, define Accept-Encoding tú mismo, a nivel de client:
ClickHouseClientSettings:
enable_http_compression=1 en la URL, algo que ClickHouse exige para tener en cuenta el header siquiera —incluso cuando UseCompression es false, ya que nombrar un códec de forma explícita se interpreta como una solicitud de compresión—. Si no se define ningún valor, UseCompression=false no envía ningún Accept-Encoding.
Accept-Encoding puede definirse en cuatro lugares. Gana el primero de ellos que nombre un códec:
QueryOptions.AcceptEncoding(oClickHouseCommand.AcceptEncoding)CustomHeaders["Accept-Encoding"]en la consultaCustomHeaders["Accept-Encoding"]en el clientClickHouseClientSettings.AcceptEncoding, o el keywordAcceptEncodingde la cadena de connection
identity.
Es el servidor, no el client, quien elige el códec. ClickHouse analiza Accept-Encoding en busca de tokens siguiendo su propio orden de preferencia fijo —zstd > br > lz4 > snappy > gzip > deflate— e ignora tanto el orden en que los enumere como cualquier valor q. Por tanto, el header es un anuncio de capacidades más que una exigencia, y la única forma de dirigir la elección es omitir determinados tokens. La lista predeterminada incluye zstd, de modo que una consulta predeterminada se responde con zstd; los tokens restantes actúan como fallback. br puede decodificarse, pero no se anuncia de forma predeterminada.
Cómo se comparan los códecs en cuanto a tamaño del payload, CPU del servidor y CPU del client depende de sus datos, de su enlace y del valor de http_zlib_compression_level del servidor (valor predeterminado de fábrica: 3) — consulte Ajuste de la compresión.
http_zlib_compression_level. Ese SETTING se aplica a todos los códecs HTTP y su valor predeterminado es 3. Conviene ajustarlo en función de sus datos, la velocidad del enlace y el uso de CPU.- Un client limitado por CPU en un enlace rápido. El driver decodifica el cuerpo de la respuesta en el hilo que realiza la llamada, por lo que, cuando la red no es el cuello de botella, la velocidad de decodificación del lado del client puede convertirse en el factor limitante.
Content-Encoding así lo indique, con independencia de lo que se haya solicitado: si está ausente o es identity, pasa sin modificarse; si se trata de un códec compatible, se decodifica; y en cualquier otro caso se lanza un error que lo nombra. No existe riesgo de doble decodificación: si el AutomaticDecompression de un handler proporcionado por el llamador ya ha decodificado un cuerpo, también elimina el Content-Encoding, de modo que el driver ve plaintext y lo deja intacto.
Los resultados sin procesar no anuncian ningún códec. ExecuteRawResultAsync (y los métodos públicos PostStreamAsync / InsertRawStreamAsync) entregan su cuerpo tal cual, de modo que, salvo que se indique explícitamente un códec, no solicitan ninguno: nada en el driver decodifica un cuerpo así, por lo que ofrecer un códec ahí convertiría silenciosamente una exportación en un archivo comprimido. Por tanto, la regla es sencilla e independiente de cómo esté configurado el HttpClient: un cuerpo literal llega exactamente como lo envió el servidor, y el servidor envía plaintext salvo que se haya solicitado un códec. Solicitarlo (a nivel de client o por consulta) es la manera de exportar bytes comprimidos de forma intencionada.
Un AcceptEncoding explícito (en cualquiera de los dos niveles) sigue aplicándose a las peticiones sin procesar, y ClickHouseRawResult.ReadDecompressedStreamAsync() decodifica el resultado cuando así se desee; ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync y CopyToAsync siempre devuelven los bytes exactamente como llegaron.
leaveOpen, de modo que liberarlo deja intacta la respuesta; cuando no está comprimida, obtienes el propio stream de contenido HTTP, así que liberarlo cierra el cuerpo. En cualquier caso, ClickHouseRawResult es el propietario de la respuesta: no llames a sus otros miembros de lectura después de haber liberado el stream. Liberar el ClickHouseRawResult siempre es obligatorio y, por sí solo, suficiente: libera tanto la respuesta como cualquier decodificador insertado aquí (los decodificadores retienen búferes agrupados). Por lo tanto, el await using anterior es opcional y puede conservarse sin riesgo. Las llamadas secuenciales repetidas devuelven el mismo stream; el tipo no es seguro para usarse de forma concurrente.
Consulta Select_007_ResponseCompression.cs para ver un ejemplo ejecutable.
Compresión de inserciones (solicitudes)
Zstd es el códec predeterminado para las inserciones:InsertOptions.Compressor toma inicialmente el valor ZstdCompressor.Default,
es decir, zstd de nivel 3. Asígnele otro compresor para cambiar el códec, o null para enviar el
cuerpo sin comprimir.
Default y con un constructor que recibe un nivel
y el tamaño del write buffer:
Comparta las instancias de compresor. Cada
Default es una única instancia compartida, y los cuatro compresores
pueden usarse de forma segura desde varios hilos a la vez, que es justo lo que ocurre cuando
InsertOptions.MaxDegreeOfParallelism es mayor que 1, ya que cada insert usa un compresor por
batch. Ninguno de ellos implementa IDisposable. Cree su propia instancia una sola vez y reutilícela, del
mismo modo en que se usa Default.IClickHouseCompressor es público y una implementación solo debe proporcionar dos miembros:
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 decodificar los cuerpos de respuesta además de comprimir las solicitudes, y lanza
InvalidDataException desde el stream que devuelve cuando un cuerpo esté corrupto o tenga un formato incorrecto.
InsertOptions.Compressor solo rige los insert binarios. Los demás cuerpos de solicitud del driver se comprimen según reglas distintas y ninguno pasa por él:
- Toda solicitud de texto SQL (
ExecuteReaderAsync,ExecuteScalarAsync,ExecuteNonQueryAsync,QueryAsync<T>,ExecuteRawResultAsync, la capa ADO.NET) envía su statement conContent-Encoding: gzipsiempre queUseCompressionseatrue, es decir, de forma predeterminada. El códec no es configurable:AcceptEncodingsolo afecta a la respuesta, así que la elección se reduce a gzip o nada.Compression=falseenvía el statement sin comprimir. Los statements son pequeños, por lo que rara vez merece la pena preocuparse por esto, pero conviene saberlo cuando estés inspeccionando solicitudes en un proxy o en una captura de packets. - Un cuerpo multiparte —una consulta cuyos parámetros se envían como form data (
UseFormDataParameters=true)— siempre se envía sin comprimir, diga lo que digaUseCompression. - Una carga sin procesar (
InsertRawStreamAsync,PostStreamAsync) usa su propio indicador por llamada y no consulta niUseCompressionniInsertOptions.Compressor: gzip cuando el indicador está activado y sin comprimir en caso contrario. Ten en cuenta que el parámetrouseCompressiondeInsertRawStreamAsyncestruede forma predeterminada, por lo que una carga sin procesar se comprime con gzip a menos que pasesfalse, incluso conCompression=falseen el client.
Ajustar la compresión
La compresión sacrifica CPU a cambio de bytes. Que compense o no depende casi por completo de la velocidad de su enlace en relación con la rapidez con la que se ejecuta el códec. No existe una SETTING que sirva para todos los casos.La única cifra que lo decide
Comprimir merece la pena siempre que el códec sea más rápido que la red. Ese umbral es más bajo de lo que la mayoría espera en la ruta de lectura, porque ClickHouse comprime las respuestas HTTP en un solo hilo dentro del búfer de salida. Medido en un servicio de ClickHouse Cloud de 16 vCPU (hits, RowBinary, nivel 3), el servidor genera salida comprimida a aproximadamente 100-200 MB/s.
Así pues, para un resultado grande, y suponiendo que 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 cifra, mientras que todo lo que atraviesa internet pública, una VPN o el límite de una región normalmente queda por debajo.
La ruta de inserción tolera la compresión hasta velocidades de enlace más altas, porque tu cliente comprime en un núcleo propio y suele ser más rápido que la compresión de respuestas del servidor.
Guía aproximada por implementación
Hay tres aspectos que esta tabla no refleja:
- Coste de salida: si te facturan la transferencia de datos, los bytes tienen un precio más allá de la latencia, lo que inclina la balanza hacia una mayor compresión al margen de la velocidad del enlace.
- Resultados pequeños: todo lo anterior se refiere a payloads grandes. En respuestas pequeñas el códec apenas importa y lo que domina es el overhead por petición.
- Las inserciones en paralelo elevan los umbrales de inserción. Todas las cifras de throughput anteriores corresponden a un único hilo.
InsertOptions.MaxDegreeOfParallelismes1de forma predeterminada, pero al aumentarlo los batches se comprimen de forma concurrente, con lo que la tasa de codificación agregada del cliente escala aproximadamente con los núcleos que le asignes. Así, en un enlace rápido, comprimir una inserción en paralelo puede seguir compensando muy por encima de la velocidad a la que deja de compensar en una de un solo hilo. Trata las filas de inserción de la tabla como un mínimo y, si ya agrupas en paralelo, vuelve a hacer pruebas antes de concluir que tu enlace es demasiado rápido para la compresión.
Elegir un códec
Niveles
La compresión de las respuestas se controla mediante un único SETTING de servidor,http_zlib_compression_level, que se aplica a todos los códecs HTTP, no solo a zlib. Su valor predeterminado es 3.
No lo modifique salvo que tenga mediciones que lo justifiquen. Por encima del valor predeterminado apenas reduce el tamaño a costa de mucha CPU (para zstd, pasar de 3 a 6 duplica aproximadamente la CPU del servidor a cambio de un ~14% menos de bytes), y br se vuelve patológico. Por debajo, en el nivel 1, el panorama sí cambia de verdad: lz4 resulta mucho más barato y zstd pierde su ventaja de CPU frente a él. Configúrelo por consulta si lo necesita:
Medir su propio punto de cruce
La forma más rápida de optimizar la elección del códec y del nivel de compresión es cronometrar la misma consulta con varios códecs y comparar los resultados.ProfileEvents de system.query_log: establece
QueryOptions.QueryId para poder localizar la fila:
LIMIT n sin ORDER BY devuelve filas distintas
en cada ejecución, por lo que cada repetición comprime datos diferentes y los ratios se vuelven ruido. Compare
siempre contra un conjunto de resultados fijo.
Inserción con stream sin procesar
UtiliceInsertRawStreamAsync para insertar datos directamente desde archivos o streams en memoria en formatos como CSV, JSON, Parquet o cualquier formato compatible con ClickHouse.
Insertar desde un archivo CSV:
Consulta la documentación de configuración de formats para conocer las opciones que controlan el comportamiento de la ingestión de datos.
Más ejemplos
Para ver más ejemplos prácticos de uso, consulta el directorio de ejemplos en el repositorio de GitHub.ADO.NET
La biblioteca ofrece compatibilidad completa con ADO.NET medianteClickHouseConnection, ClickHouseCommand y ClickHouseDataReader. Esta API es necesaria para la integración con ORM (Dapper, Linq2db) y cuando necesita las abstracciones estándar de bases de datos de .NET.
Gestión del ciclo de vida con ClickHouseDataSource
Cree siempre conexiones desde unClickHouseDataSource para garantizar una gestión correcta del ciclo de vida y del pool de conexiones. El DataSource administra internamente un único ClickHouseClient, y todas las conexiones comparten su pool de conexiones HTTP.
Uso de ClickHouseCommand
Cree comandos a partir de una conexión para ejecutar SQL:ExecuteNonQueryAsync()- Para INSERT, UPDATE, DELETE y sentencias DDLExecuteScalarAsync()- Devuelve la primera columna de la primera filaExecuteReaderAsync()- Devuelve unClickHouseDataReaderpara recorrer los resultados
Uso de ClickHouseDataReader
ClickHouseDataReader proporciona acceso tipado a los resultados de la consulta:
Lectura del ordinal de un enum
Una columnaEnum8 o Enum16 se materializa como su etiqueta: GetFieldType devuelve string, y tanto
GetString como GetValue y GetFieldValue<string> proporcionan la etiqueta. Los accesores numéricos
lanzan InvalidCastException sobre una columna de enum, porque el valor almacenado es una cadena.
Utilice TryGetEnumOrdinal para obtener el número que hay detrás de la etiqueta:
true y establece value para una columna Enum8/Enum16, y para una columna
Nullable(Enum...) cuya celda no sea NULL. Devuelve false, con value establecido en 0, para una celda NULL o para
cualquier columna que no sea un enum. El ordinal es el
valor con signo recibido por el wire, por lo que puede ser negativo, y un ordinal Enum16 puede ocupar más de un
byte.
Buenas prácticas
Tiempo de vida de las conexiones y pool de conexiones
ClickHouse.Driver usa System.Net.Http.HttpClient internamente. HttpClient tiene un pool de conexiones por endpoint. Como consecuencia:
- Las sesiones de la base de datos se multiplexan a través de conexiones HTTP administradas por el pool de conexiones.
- El pool recicla automáticamente las conexiones HTTP.
- Las conexiones pueden permanecer activas después de desechar los objetos
ClickHouseClientoClickHouseConnection.
Gestión de DateTime
-
Usa UTC siempre que sea posible. Almacena las marcas de tiempo como columnas
DateTime('UTC')y usaDateTimeKind.Utcen tu código. Esto elimina la ambigüedad de la zona horaria. -
Usa
DateTimeOffsetpara gestionar explícitamente la zona horaria. Siempre representa un instante específico e incluye la información de desplazamiento. -
Especifica la zona horaria en las indicaciones de tipo de SQL. Al usar parámetros con valores
DateTimeUnspecifieddestinados a columnas que no son UTC, incluye la zona horaria en el SQL:
Inserciones asíncronas
Las inserciones asíncronas trasladan la responsabilidad de agrupar en lotes del cliente al servidor. En lugar de requerir el agrupamiento en lotes del lado del cliente, el servidor guarda en un búfer los datos entrantes y los vuelca al almacenamiento en función de umbrales configurables. Esto resulta útil en escenarios de alta concurrencia, como las cargas de trabajo de observabilidad, donde muchos agentes envían payloads pequeños. Habilite las inserciones asíncronas medianteCustomSettings o la cadena de conexión:
wait_for_async_insert):
Configuraciones clave:
Sesiones
Habilita las sesiones solo cuando necesites funcionalidades con estado en el servidor, por ejemplo:- Tablas temporales (
CREATE TEMPORARY TABLE) - Mantener el contexto de la consulta entre varias sentencias
- Ajustes a nivel de sesión (
SET max_threads = 4)
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 enteros
Tipos de coma flotante
Tipos decimales
La conversión de tipos decimales se controla con la configuración UseCustomDecimals.
Tipo booleano
Tipos String
De forma predeterminada, las columnas
String y FixedString(N) se devuelven como string. Establezca ReadStringsAsByteArrays=true en la cadena de conexión para leerlas como byte[] en su lugar. Esto es útil cuando se almacenan datos binarios que podrían no ser UTF-8 válidos.La configuración también se aplica a las cadenas anidadas dentro de otros tipos, por lo que Array(String) se lee como byte[][]
y Map(String, String) como Dictionary<byte[], byte[]>, incluidas las claves. La única excepción es una
columna JSON, cuyas hojas de tipo cadena siempre son texto; consulte JSON type.Tipos de fecha y hora
ClickHouse almacena internamente los valores
DateTime y DateTime64 como marcas de tiempo Unix (segundos o fracciones de segundo desde la época Unix). Aunque el almacenamiento siempre está en UTC, las columnas pueden tener una zona horaria asociada que afecta a cómo se muestran e interpretan los valores.
Al leer valores DateTime, la propiedad DateTime.Kind se establece en función de la zona horaria de la columna:
Para las columnas que no son UTC, el
DateTime devuelto representa la hora local en esa zona horaria. Usa ClickHouseDataReader.GetDateTimeOffset() para obtener un DateTimeOffset con el desplazamiento correcto para esa zona horaria:
DateTime en lugar de DateTime('Europe/Amsterdam')), el driver devuelve un DateTime con Kind=Unspecified. Esto conserva la hora local exactamente tal como está almacenada, sin hacer suposiciones sobre la zona horaria.
Si necesita un comportamiento con reconocimiento de zona horaria para columnas sin una zona horaria explícita, haga una de estas dos cosas:
- Use zonas horarias explícitas en las definiciones de sus columnas:
DateTime('UTC')oDateTime('Europe/Amsterdam') - Aplique usted mismo la zona horaria después de leer el valor.
Tipo JSON
El tipo de retorno de las columnas JSON está determinado por la configuración
JsonReadMode:
-
Binary(predeterminado): DevuelveSystem.Text.Json.Nodes.JsonObject. Proporciona acceso estructurado a los datos JSON, pero los tipos especializados de ClickHouse (como direcciones IP, UUIDs y decimales grandes) se convierten a su representación en cadena dentro de la estructura JSON. -
String: Devuelve el JSON sin procesar comostring. Conserva la representación exacta del JSON de ClickHouse, lo que resulta útil cuando necesitas pasar el JSON sin analizarlo o cuando quieres encargarte tú mismo de la deserialización.
None es un tercer modo. Se lee exactamente igual que Binary, pero no envía ninguna configuración de servidor con la
consulta: úsalo en una conexión que no tenga permitido establecerla.
Un path declarado en el tipo de la columna es un path tipado; cualquier otro path del documento es un path dinámico. Ambos se comportan de forma distinta cuando el valor es NULL.
Un path tipado siempre aparece en el JsonObject. Si se declara como Nullable(T) o Dynamic, se devuelve como un NULL de JSON tanto cuando el valor almacenado es NULL como cuando el documento no contiene ese path: ambos casos son indistinguibles:
JSON(x String)
devuelve {"x":""} y JSON(x Int64) devuelve {"x":0}.
Un path dinámico cuyo valor es null se elimina por completo del objeto, por lo que ContainsKey devuelve
false para él. Leer {"x":null} desde una columna JSON simple devuelve {}.
Los paths tipados anidados construyen sus ancestros, de modo que JSON(a.b Nullable(Int64)) produce {"a":{"b":null}}
incluso para un documento vacío.
Esto es lo que renderiza el propio server, por lo que los modos
Binary y String ahora coinciden. Antes de la versión 1.4.0, un
path tipado que contuviera null se eliminaba del JsonObject, lo que hacía que {"x":null} se leyera como
{}; y, en el caso de un path anidado como JSON(a.b Nullable(Int64)), desaparecía todo el subárbol a.JSON siempre se devuelven como texto, sea cual sea el valor de
ReadStringsAsByteArrays: JsonValue no dispone de una forma de array de bytes, por lo que un byte[] se
representaría como base64. Esto vale para String, FixedString y para los tipos envueltos en
LowCardinality, Nullable o SimpleAggregateFunction, así como para las cadenas dentro de Array y Map,
incluidas las claves del map.
Un array de bytes cuyo tipo el lector de JSON no puede determinar sí se representa como base64: un
path tipado como
Variant o Dynamic contiene un valor cuyo tipo solo se conoce fila a fila, de modo que una cadena
bajo Variant(Array(UInt8), String) se devuelve codificada en base64. Esto ocurre igual con ambas configuraciones.Un tipo de clave de map JSON que no sea exactamente String —por ejemplo, Map(LowCardinality(String), String)— lanza NotSupportedException.JSON(a Int64, a.b Int64). Ambas rutas están presentes en cada fila, por lo que el servidor
representa la fila con una clave duplicada: {"a":0,"a":{"b":7}}. Un JsonObject no puede contener dos valores
para una misma clave, de modo que JsonReadMode.Binary lanza una SerializationException que indica ambas rutas. Lo
mismo ocurre cuando el valor es un Map, como en JSON(a Map(String, Int64)) leído de una fila que
también tiene un a.b dinámico.
Esto solo se aplica cuando ambos lados contienen un valor en esa fila. Un lado que no contiene nada —un valor nulo,
un objeto vacío o un subárbol cuyos valores son todos nulos— cede ante el lado que sí tiene los datos,
sea cual sea de las dos rutas la que el servidor envíe primero. Por lo tanto, una superposición declarada con tipos Nullable rellena un solo lado por fila y se lee sin
error: JSON(a Nullable(Int64), a.b Nullable(Int64)) devuelve {"a":5} y {"a":{"b":7}}, tal como
se espera.
Lea una columna de este tipo con JsonReadMode.String para obtener el texto JSON del servidor sin cambios, incluida la clave
duplicada.
Establezca AllowDuplicateJsonKeys para seguir leyendo la columna como un JsonObject en lugar de lanzar una excepción. En ese caso, el
driver conserva el último de los dos valores que lleva la fila y descarta el otro, por lo que el
resultado es con pérdida: JSON(a Int64, a.b Int64) que contiene {"a.b":7} se lee como {"a":0}. Una ruta que
contiene un valor y cuyo padre contiene un valor escalar o un array sigue lanzando una excepción, porque un subárbol no puede
ubicarse bajo ninguno de los dos.
Map type
Un
Map(K, V) de ClickHouse es físicamente un Array(Tuple(K, V)) y puede contener varias entradas con la misma clave. Un Dictionary no puede, por lo que en el modo predeterminado una clave repetida conserva únicamente su último valor y los pares anteriores se descartan. El ajuste MapReadMode determina la representación:
-
Dictionary(predeterminado): devuelveDictionary<K, V>. -
KeyValuePairs: devuelveList<KeyValuePair<K, V>>en el orden en que el server envió los pares, con lo que se conservan todos, incluidas las entradas que repiten una clave.
Map, por lo que también se aplica a GetFieldValue<T>, a los tipos de schema que informa el driver y a la correspondencia de propiedades POCO. Se aplica en cualquier lugar donde aparezca un map dentro del árbol de tipos de una columna, incluidos Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) y Dynamic.
Ambas representaciones se aceptan en la ruta de escritura en cualquiera de los dos modes; consulte escritura de maps.
Otros tipos
Los tipos Dynamic y Variant se convertirán al tipo correspondiente según el tipo subyacente real de cada fila.
Tipos de geometría
El tipo Geometry es un tipo Variant que puede contener cualquiera de los tipos de geometría. Se convertirá al tipo correspondiente.
Correspondencia de tipos: escritura en ClickHouse
Al insertar datos, el driver convierte los tipos de .NET en sus correspondientes tipos de ClickHouse. Las tablas siguientes muestran qué tipos de .NET se admiten para cada tipo de columna de ClickHouse.Tipos enteros
Tipos de coma flotante
Tipo booleano
Tipos de cadena
Tipos de fecha y hora
Valores fuera de rangoEn la ruta de escritura binaria, los valores
Date, Date32, DateTime y DateTime32 fuera de su rango admitido lanzan ArgumentOutOfRangeException en el momento de Write, indicando el tipo de columna y el rango admitido. Anteriormente, los valores fuera de rango podían truncarse silenciosamente a través de un entero de 32 bits y ser reinterpretados por el servidor, lo que producía timestamps reales pero incorrectos.DateTime.Kind al escribir valores:
Los valores
DateTimeOffset siempre conservan el instante exacto.
Ejemplo: DateTime UTC (se conserva el instante)
DateTimeKind.Utc o DateTimeOffset para todas las operaciones con DateTime. Esto garantiza que su código funcione de forma coherente independientemente de la zona horaria del servidor, la zona horaria del cliente o la zona horaria de la columna.
Parámetros HTTP vs Bulk Copy
Hay una diferencia importante entre la vinculación de parámetros HTTP y Bulk Copy al escribir valores DateTimeUnspecified:
Bulk Copy conoce la zona horaria de la columna de destino e interpreta correctamente los valores Unspecified en esa zona horaria.
HTTP Parameters no conocen automáticamente la zona horaria de la columna. Debe especificarla en la indicación de tipo de SQL:
Tipos Decimal
Tipo JSON
El comportamiento al escribir JSON está controlado por el ajuste
JsonWriteMode:
-
String(predeterminado): Aceptastring,JsonObject,JsonNodeo cualquier objeto. Todas las entradas se serializan medianteSystem.Text.Json.JsonSerializery se envían como cadenas JSON para que el servidor las procese. Este es el modo más flexible y funciona sin registrar tipos. -
Binary: Solo acepta tipos POCO registrados. Los datos se convierten en el cliente al formato JSON binario de ClickHouse, con compatibilidad completa con indicaciones de tipo. Requiere llamar aconnection.RegisterJsonSerializationType<T>()antes de usarlo. Escribir valoresstringoJsonNodeen este modo lanzaArgumentException.
JSON(id UInt64, price Decimal128(2))), el driver usa estas indicaciones para serializar los valores respetando plenamente sus tipos. Esto preserva 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 formas, según JsonWriteMode:
Modo String (predeterminado): los POCO se serializan mediante System.Text.Json.JsonSerializer. No es necesario registrar tipos. Es el enfoque más sencillo y funciona con objetos anónimos.
Modo binario: los POCO se serializan usando el formato JSON binario del driver, con compatibilidad completa con indicaciones de tipo. Los tipos deben registrarse con connection.RegisterJsonSerializationType<T>() antes de usarlos. Este modo admite asignaciones de rutas personalizadas mediante atributos:
-
[ClickHouseJsonPath("path")]: Asigna una propiedad a una ruta JSON personalizada. Es útil para estructuras anidadas o cuando el nombre de la propiedad difiere de la clave JSON deseada. Solo funciona en modo binario. -
[ClickHouseJsonIgnore]: Excluye una propiedad de la serialización. Solo funciona en modo binario.
UserId solo coincidirá con una indicación definida como UserId, no como userid. Esto sigue el comportamiento de ClickHouse, que permite que rutas como userName y UserName coexistan como campos independientes.
Limitaciones (solo en modo Binary):
- Los tipos POCO deben registrarse en la conexión con
connection.RegisterJsonSerializationType<T>()antes de serializarse. Si se intenta serializar un tipo no registrado, se lanzaClickHouseJsonSerializationException. - Las propiedades de diccionario y array/lista requieren indicaciones de tipo en la definición de la columna para serializarse correctamente. Sin esas indicaciones, use el modo String.
- Los valores NULL en las propiedades POCO solo se escriben cuando la ruta tiene una indicación de tipo
Nullable(T)en la definición de la columna. ClickHouse no permite tiposNullabledentro de rutas JSON dinámicas, por lo que las propiedades con valor NULL sin indicación se omiten. - Los atributos
ClickHouseJsonPathyClickHouseJsonIgnorese ignoran en modo String (solo funcionan en modo Binary).
Otros tipos
Tipos de geometría
No admitido para escritura
Manejo de tipos anidados
Los tipos anidados de ClickHouse (Nested(...)) se pueden leer y escribir usando la semántica de arrays.
Registro y diagnósticos
El cliente .NET de ClickHouse se integra con las abstracciones deMicrosoft.Extensions.Logging para ofrecer un registro ligero y opcional. Cuando está habilitado, el driver emite mensajes estructurados sobre eventos del ciclo de vida de la conexión, la ejecución de comandos, las operaciones de transporte y las operaciones de inserción masiva. El registro es totalmente opcional: las aplicaciones que no configuran un logger siguen ejecutándose sin sobrecarga adicional.
Primeros pasos
Uso de appsettings.json
Puede configurar los niveles de registro mediante la configuración estándar de .NET:Uso de la configuración en memoria
También puede configurar en el código la verbosidad del registro por categoría:Categorías y emisores
El driver usa categorías específicas para que puedas ajustar con precisión los niveles de registro de cada componente:Ejemplo: Cómo diagnosticar problemas de conexión
- Selección de la fábrica de clientes HTTP (pool predeterminado frente a conexión única)
- Configuración del controlador HTTP (
SocketsHttpHandleroHttpClientHandler) - Configuración del pool de conexiones (
MaxConnectionsPerServer,PooledConnectionLifetime, etc.) - Configuración de timeout (
ConnectTimeout,Expect100ContinueTimeout, etc.) - Configuración de SSL/TLS
- Eventos de apertura/cierre de conexiones
- 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 driver incluye un asistente que habilita el tracing de bajo nivel de los componentes internos de red de .NET. Para habilitarlo, debe pasar una LoggerFactory con el nivel establecido en Trace y establecer EnableDebugMode en true (o habilitarlo manualmente mediante la claseClickHouse.Driver.Diagnostic.TraceHelper). Los eventos se registrarán en la categoría ClickHouse.Driver.NetTrace. Advertencia: esto generará logs extremadamente verbosos y afectará al rendimiento. No se recomienda habilitar el modo de depuración en producción.
OpenTelemetry
El driver ofrece compatibilidad integrada con el tracing distribuido de OpenTelemetry mediante la API de .NETSystem.Diagnostics.Activity. Cuando está habilitado, el driver emite spans para las operaciones de base de datos que pueden exportarse a backends de observabilidad como Jaeger o al propio ClickHouse (mediante el OpenTelemetry Collector).
Habilitar el tracing
En las aplicaciones ASP.NET Core, agregue elActivitySource del driver de ClickHouse a su configuración de OpenTelemetry:
Atributos del span
Cada span incluye atributos de base de datos estándar de OpenTelemetry, además de estadísticas de consulta específicas de ClickHouse que pueden usarse para depuración.Opciones de configuración
Controle el comportamiento del tracing conClickHouseDiagnosticsOptions:
Configuración de TLS
Al conectarse a ClickHouse a través de HTTPS, puede configurar el comportamiento de TLS/SSL de varias formas.Validación personalizada de certificados
Para entornos de producción que requieran una lógica personalizada de validación de certificados, proporcione su propioHttpClient con un controlador ServerCertificateCustomValidationCallback configurado:
Consideraciones importantes al proporcionar un
HttpClient personalizado- Descompresión automática: deja
AutomaticDecompressiondesactivado. El driver decodifica por sí mismo las respuestas comprimidas, por lo que no es necesario — y habilitarlo juega en tu contra en el lado de la solicitud: al enviar, el handler también añade todos los algoritmos de su máscara alAccept-Encodingsaliente, ampliando lo que el driver hubiera anunciado, de modo que ClickHouse puede responder con un codec que no solicitaste. Consulta Descompresión de respuestas. - Tiempo de espera de inactividad: Configura
PooledConnectionIdleTimeoutcon un valor inferior alkeep_alive_timeoutdel servidor (10 segundos en ClickHouse Cloud) para evitar errores de conexión causados por conexiones semiabiertas.
Ajuste del rendimiento
En esta sección se describe cómo usar el client para obtener un rendimiento óptimo, así como las distintas opciones que puede ajustar para adaptar el rendimiento del client a su caso de uso concreto.De un vistazo
| Si necesitas | Haz esto | |---|---|---| | Leer filas en POCOs | UsaQueryAsync<T>, no MapTo<T> |
| Realizar inserciones grandes | Aumenta InsertOptions.BatchSize |
| Ejecutar una aplicación de consola o worker con gran volumen de inserciones | Activa el GC de servidor |
| Leer resultados grandes a través de la red | Mantén activada la compresión de respuestas (opción predeterminada) |
| Insertar a través de un enlace rápido | Prueba InsertOptions.Compressor = null |
| Insertar muchas veces en la misma tabla | Usa UseSchemaCache o ColumnTypes |
| Leer resultados muy grandes | Aumenta ReadBufferSize |
Lectura: elegir la ruta de materialization
Hay tres formas de obtener una fila de un resultado, y no todas cuestan lo mismo. Algunas de las rutas aplican boxing a los resultados, lo que aumenta las allocations y reduce el rendimiento.
Para una lectura de 1.000.000 de filas de 105 columnas del dataset hits:
Los ORM toman la ruta rápida cuando usan accesores tipados. linq2db registra
GetInt64,
GetDouble y GetDateTime para cada columna, por lo que la lectura se realiza 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 mucha frecuencia
y lee mediante GetValue, utilice QueryAsync<T> para esa consulta en concreto.Inserción: tamaño de lote y paralelismo
El tamaño de lote es el factor que más influye en el throughput de inserción.InsertOptions.BatchSize tiene un valor predeterminado de
100.000 filas.
Use lotes grandes. En una inserción de 1.000.000 de filas, aumentar de 10.000 a 100.000 filas por
lote dio como resultado:
Si no puede controlar el tamaño de lote (por ejemplo, cuando muchos productores pequeños envían filas de forma independiente), use inserciones async y deje que el server se encargue del batching.
Cargas en paralelo.
InsertOptions.MaxDegreeOfParallelism tiene el valor predeterminado 1. Auméntelo para enviar
varios lotes a la vez. Resulta especialmente útil cuando la compresión está activada, ya que así cada lote se comprime
en su propio thread. Las sessions no funcionan con inserciones en paralelo: desactive las sessions o mantenga
MaxDegreeOfParallelism = 1.
Elimine el schema probe. Cada llamada a InsertBinaryAsync envía primero una consulta SELECT ... WHERE 1=0
para averiguar los column types. Consulte Skipping the schema probe query para eliminar ese
viaje de ida y vuelta mediante ColumnTypes o UseSchemaCache.
La ruta de inserción sin boxing se aplica al format predeterminado
RowBinary. RowBinaryWithDefaults debe
examinar cada value para encontrar el marker DBDefault, por lo que mantiene la ruta más lenta.Compresión: las dos direcciones no coinciden
La compresión intercambia CPU por bytes. Que ese intercambio resulte conveniente depende de la dirección de la transferencia, del ancho de banda de tu conexión con el ClickHouse server, de cómo interactúan tus datos con el algoritmo de compresión elegido y de si debes pagar por cada byte transferido. Lecturas: mantén la compresión activada, salvo que tu server se ejecute en la misma máquina. Es el comportamiento predeterminado. En comparación con no usar compresión,zstd en el nivel 1
arrojó:
Inserciones: mide antes de comprimir. Puede que el ahorro no baste para justificar su activación. Ten en cuenta también que la descompresión añadirá carga adicional al servidor; esa carga es moderada con Zstd y LZ4, pero puede ser alta con otros algoritmos (por ejemplo, Brotli).
Para desactivar la compresión en las inserciones:
Búferes
ReadBufferSize establece el tamaño del búfer que lee las respuestas HTTP. Su valor predeterminado es 64 KiB.
El driver toma prestado este búfer de un grupo compartido y lo devuelve al liberar el lector, por lo que
no implica una reserva de memoria en cada consulta. Auméntelo para reducir la cantidad de rellenados del búfer en
resultados grandes. El driver mantiene un búfer por cada lector abierto simultáneamente, de modo que el uso de memoria
crece con el tamaño del búfer y con la cantidad de lectores concurrentes.
Runtime y GC
Active el GC de servidor en aplicaciones con muchas inserciones. Con el mismo código y el mismo número de bytes asignados, el GC de workstation resultó hasta un 97 % más lento en las inserciones que el GC de servidor.El Server GC es una configuración orientada al throughput, no a la latencia. En esas mismas mediciones, el Server GC pasó en pausa menos de la mitad del tiempo total, pero sus pausas individuales fueron más largas (percentil 95 de 114,6 ms frente a 61,9 ms). Si tu service es sensible a la latencia de cola, mide ambos modes antes de decidir.
Latencia: reutilizar conexiones
Establecer una nueva conexión TCP y realizar el handshake TLS lleva una cantidad de tiempo considerable. Reutilizar las conexiones reducirá significativamente la latencia de sus consultas.- No cree un client para cada solicitud. Cada nuevo client con su propio
HttpClientcrea un nuevo grupo de conexiones y vuelve a pagar el costo del handshake. Use un únicoClickHouseClientdurante toda la vida de la aplicación. Es seguro para subprocesos y está diseñado para un uso singleton. - Para ADO.NET y los ORM, use
ClickHouseDataSource, de modo que todas las conexiones compartan un mismo grupo.
Mídalo usted mismo
En muchos casos, el rendimiento dependerá de la estructura de sus datos, de la velocidad de su conexión con el servidor, de si prefiere sacrificar CPU del cliente a cambio de CPU del servidor (o a la inversa), de las limitaciones de su hardware, etc. Por ello, se recomienda medir el rendimiento usted mismo en función de sus datos y su entorno. Para conocer la parte del trabajo que corresponde al servidor, establezcaQueryOptions.QueryId y consulte los contadores:
Compatibilidad con los ORM
Los ORM requieren la API de ADO.NET (ClickHouseConnection). Para gestionar correctamente el ciclo de vida de la conexión, cree las conexiones desde un ClickHouseDataSource:
Dapper
ClickHouse.Driver funciona con Dapper. El driver 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.
Usa ClickHouseDataSource para gestionar correctamente el ciclo de vida de la conexión:
Estilos para pasar parámetros
Se admiten todos los estilos estándar de parámetros de Dapper: Objetos anónimos:DynamicParameters (de un diccionario o de un objeto anónimo):
Consultas con POCOs
Dapper asigna columnas a propiedades por nombre (sin distinguir entre mayúsculas y minúsculas):Sintaxis de parámetros nativa de ClickHouse
Cuando necesites un control explícito de los tipos, usa directamente en el SQL la sintaxis{param:Type} de ClickHouse con un Dictionary<string, object> para los valores de los parámetros. No combines la sintaxis @param con la sintaxis {param:Type} para el mismo parámetro.
WHERE IN
La expansión nativa de IN de Dapper funciona:WHERE id IN (@Ids1, @Ids2, @Ids3), y el driver convierte cada parámetro expandido.
La función has() de ClickHouse con un parámetro Array también funciona:
Manejadores de tipos personalizados
Algunos tipos de ClickHouse, p. ej.,ITuple, BigInteger y ClickHouseDecimal, requieren registrar manejadores al inicio:
Dapper.Contrib
GetAll<T>() y Get<T>(id) funcionan. Insert<T>() no: genera sintaxis de SQL Server (SCOPE_IDENTITY, []). En su lugar, se recomienda usar el método nativo InsertBinaryAsync de ClickHouseClient.
Limitaciones
Linq2db
Este driver es compatible con linq2db, un ORM ligero y un proveedor de LINQ para .NET. Consulta el sitio web del proyecto para obtener documentación detallada. Ejemplo de uso: Crea unaDataConnection con el proveedor de ClickHouse:
BulkCopyAsync para realizar inserciones masivas de forma eficiente.
Entity Framework Core
El proveedor oficial de Entity Framework Core para ClickHouse. Permite asignar clases de C# a tablas de ClickHouse, realizar consultas con LINQ e insertar datos medianteSaveChanges, todo ello con los patrones habituales de EF Core.
- NuGet:
ClickHouse.EntityFrameworkCore - Código fuente: GitHub
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 del motor de tabla específica de ClickHouse. UPDATE / DELETE no son compatibles.Instalación
Inicio rápido
Define la entidad yDbContext, y luego haz consultas con LINQ:
Tipos compatibles
Usa
ClickHouseDecimal (de ClickHouse.Driver.Numerics) en lugar de decimal cuando necesites toda la precisión de las columnas Decimal128/Decimal256: decimal de .NET está limitado a 28–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, Max — incluido HAVING (.Where() después de .GroupBy()), varias agregaciones en una sola proyección y OrderBy sobre resultados agregados.
JOINs: Join (INNER) y patrones GroupJoin/SelectMany (LEFT y CROSS). LEFT JOIN devuelve null real para las filas sin coincidencia (consulta la semántica de null de 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 insertadas en línea: los joins y Contains con colecciones en memoria (int[], List<T>, etc.) se traducen en una serie de UNION.
Métodos de cadena: 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 sus equivalentes en ClickHouse — funciones aritméticas, logarítmicas, trigonométricas y auxiliares.
El proveedor inserta automáticamente set_join_use_nulls=1 en cada connection path para ajustarse a las expectativas de Entity Framework sobre el comportamiento de JOIN.
Si su servidor ClickHouse o profile impide cambiar esta configuración (por ejemplo, un profile readonly=1), desactívelo con:
0 / "" en lugar de == null.
Inserción de datos
SaveChanges usa la API nativa InsertBinaryAsync del driver: codificación RowBinary con un cuerpo de solicitud comprimido, mucho más eficiente que SQL con parámetros:
Added a Unchanged tras guardar, igual que con cualquier otro proveedor de EF Core.
El tamaño del lote es configurable (valor predeterminado: 1000):
Inserción masiva
Para cargas de alto rendimiento, useBulkInsertAsync en lugar de SaveChanges. Es un método de extensión de DbContext que omite por completo el seguimiento de cambios, la resolución de identidad y la administración del estado de EF Core; llama directamente al método InsertBinaryAsync del driver con codificación RowBinary y un cuerpo de solicitud comprimido.
Esto lo hace adecuado para cargar grandes volúmenes de datos cuando no necesita el seguimiento de entidades después de la inserción:
IEnumerable<T> — recorre las entidades en streaming sin cargarlas todas en memoria. El valor devuelto es el número de filas insertadas. Las entidades no se adjuntan al DbContext después de la inserción, por lo que no hay transición de estado Added → Unchanged.
Enumeraciones
Las columnasEnum8/Enum16 de ClickHouse se pueden asignar a propiedades string o a tipos enum de C#. Al usar enumeraciones de C#, el proveedor convierte automáticamente entre la enumeración y su representación textual:
Conversiones de tipos personalizadas
El sistemaValueConverter de EF Core te permite mapear tipos personalizados a tipos que el proveedor ya admite. El proveedor nunca ve tu tipo personalizado: EF Core realiza la conversión en ese punto.
Conversión por propiedad:
Anotaciones de tipo de columna
Para tipos escalares comostring, int, DateTime, etc., el proveedor infiere automáticamente el tipo de ClickHouse. Para los tipos parametrizados y los envoltorios, debe especificar explícitamente el tipo de ClickHouse.
Uso de anotaciones de datos (atributos):
OnModelCreating:
Array(Nullable(Int32)) y LowCardinality(Nullable(String)) — el proveedor elimina automáticamente Nullable y LowCardinality en cada nivel de anidamiento.
Columnas Variant y Dynamic
Las columnasVariant(T1, T2, ...) y Dynamic de ClickHouse se corresponden con object en .NET. Como object es demasiado genérico para la inferencia automática de tipos, debe declarar explícitamente el tipo de almacenamiento mediante .HasColumnType():
string, ulong, ulong[]).
Columnas JSON
El proveedor admite el tipo de columnaJson de ClickHouse, que se corresponde con System.Text.Json.Nodes.JsonNode (principal) o string (mediante ValueConverter automático):
SaveChanges como con BulkInsertAsync:
string con un tipo de columna Json; el proveedor aplica automáticamente un ValueConverter:
- Sin traducción de rutas JSON —
entity.Data["name"]en LINQ no se corresponde con la sintaxis SQLdata.namede ClickHouse. Filtre por columnas no JSON e inspeccione el JSON en memoria. - Semántica de NULL — El tipo JSON de ClickHouse devuelve
{}(objeto vacío) para los valores NULL en lugar de SQL NULL. - Precisión de enteros — El JSON de ClickHouse almacena todos los enteros como
Int64. Al leerlo medianteJsonNode, useGetValue<long>()en lugar deGetValue<int>().
Motores de tablas
Configure los motores de tablas de ClickHouse y las cláusulas específicas de cada motor mediante la API fluidaToTable(name, t => ...). Si no se configura ningún motor, el proveedor usa MergeTree de forma predeterminada, con ORDER BY derivado de la clave primaria de la entidad.
Cláusulas del motor:
WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Todas se aplican al generador de motores devuelto por HasXxxEngine().
Características a nivel de columna: HasCodec, HasTtl, HasComment, HasDefault — todas forman parte de las migraciones.
Índices de omisión de datos — mediante HasIndex(...).HasSkippingIndexType(...):
Migraciones
Flujo de trabajo estándar para las migraciones de EF Core:Limitaciones de las migraciones
Además de las migraciones, el proveedor tampoco admite aún:
UPDATE/DELETE- Transacciones:
BeginTransactiones una operación sin efecto. ClickHouse no admite transacciones ACID. - Traducción de consultas con rutas JSON:
entity.Data["key"]en LINQ no se traduce a la sintaxis SQLdata.keyde ClickHouse. Filtre por columnas que no sean JSON e inspeccione el JSON en memoria.
Limitaciones
Tuplas con más de 8 elementos y una tupla anidada en la última posición
Los tiposValueTuple de C# con más de 7 elementos usan un esquema de anidamiento generado por el compilador: el 8.º argumento genérico (TRest) es, a su vez, un ValueTuple que contiene los elementos restantes. Por ejemplo, (int, int, int, int, int, int, int, string, string) se compila como ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
Esto crea una ambigüedad cuando la columna de ClickHouse es una tupla de 8 elementos cuyo último elemento es, a su vez, una tupla; por ejemplo, Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). El driver no puede distinguir entre:
- Una tupla plana de 9 elementos (anidamiento TRest generado por el compilador)
- Una tupla de 8 elementos cuyo último elemento es un
Tuple(String, String)anidado
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
El driver trata el 8.º argumento como TRest (es decir, lo aplana), lo que significa que el caso de 8 elementos con una tupla anidada se serializará incorrectamente.
Esto afecta tanto a System.Tuple como a ValueTuple, ya que ambos usan anidamiento TRest para >7 elementos. Las tuplas de 7 elementos o menos, o aquellas cuyo último elemento no es a su vez una tupla, no se ven afectadas.
Solución alternativa: Envuelva la tupla interna en una capa adicional para que el driver pueda distinguirla del anidamiento TRest:
Columnas de AggregateFunction
Las columnas de tipoAggregateFunction(...) no se pueden consultar ni insertar directamente.
Para insertar: