Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

Client C# de ClickHouse

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, 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. ClickHouseBulkCopy es una clase auxiliar para insertar datos de forma eficiente mediante una conexión ADO.NET. ClickHouseBulkCopy está obsoleto y se eliminará en una versión futura; en su lugar, use ClickHouseClient.InsertBinaryAsync.

Ambas API comparten el mismo pool de conexiones HTTP y pueden usarse juntas en la misma aplicación.

Guía de migración

  1. Actualiza tu archivo .csproj con el nuevo nombre del paquete ClickHouse.Driver y la versión más reciente en NuGet.
  2. Actualiza en tu código todas las referencias de ClickHouse.Client a ClickHouse.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:

dotnet add package ClickHouse.Driver

O bien, usa el Administrador de paquetes NuGet:

Install-Package ClickHouse.Driver

Inicio rápido

using ClickHouse.Driver;

// Crear un cliente (normalmente como singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Ejecutar una consulta
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);

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.

A continuación se muestra una lista completa de todas las opciones de configuración, sus valores predeterminados y sus efectos.

Configuración de la conexión

Propiedad Tipo Predeterminado Clave de la cadena de conexión Descripción
Host string "localhost" Host Nombre de host o dirección IP del servidor de ClickHouse
Port ushort 8123 (HTTP) / 8443 (HTTPS) Port Número de puerto; los valores predeterminados dependen del protocolo
Username string "default" Username Nombre de usuario para la autenticación
Password string "" Password Contraseña de autenticación
Database string "" Database Base de datos predeterminada; si está vacía, se usa la predeterminada del servidor o del usuario
Protocol string "http" Protocol Protocolo de conexión: "http" o "https"
Path string null Path Ruta de la URL para entornos con proxy inverso (p. ej., /clickhouse)
Timeout TimeSpan 2 minutos Timeout Tiempo de espera de la operación (se almacena como segundos en la cadena de conexión)

Formato de datos y serialización

Propiedad Tipo Predeterminado Clave de la cadena de conexión Descripción
UseCompression bool true Compression Habilita la compresión gzip para la transferencia de datos
UseCustomDecimals bool true UseCustomDecimals Usa ClickHouseDecimal para precisión arbitraria; si es false, usa decimal de .NET (límite de 128 bits)
ReadStringsAsByteArrays bool false ReadStringsAsByteArrays Lee las columnas String y FixedString como byte[] en lugar de string; útil para datos binarios
UseFormDataParameters bool false UseFormDataParameters Envía los parámetros como datos de formulario en lugar de la cadena de consulta de la URL
ReadBufferSize int 8192 (8 KiB) ReadBufferSize Tamaño en bytes del búfer usado para leer las respuestas de consultas HTTP. Increméntalo para reducir las recargas del búfer en conjuntos de resultados grandes. Los valores iguales o superiores a 85.000 bytes se asignan en el Large Object Heap, lo que puede causar fragmentación del LOH y pausas más largas del GC.
ParameterTypeResolver IParameterTypeResolver null Resolver personalizado para la correspondencia de tipos de parámetros con estilo @; consulta Correspondencia personalizada de tipos de parámetros
ParameterFormatter IParameterFormatter null Formateador personalizado para la serialización de valores de parámetros; consulta Formato personalizado de valores de parámetros
ReadValueConverter IReadValueConverter null Transformación personalizada aplicada a los valores devueltos por el lector de datos; consulta Conversión personalizada de valores leídos
JsonReadMode JsonReadMode Binary JsonReadMode Cómo se devuelven los datos JSON: Binary (devuelve JsonObject) o String (devuelve la cadena JSON sin procesar)
JsonWriteMode JsonWriteMode String JsonWriteMode Cómo se envían los datos JSON: String (serializa mediante JsonSerializer, acepta cualquier entrada) o Binary (solo POCO registrados con indicaciones de tipo)

Gestión de sesiones

Propiedad Tipo Predeterminado Clave de la cadena de conexión Descripción
UseSession bool false UseSession Habilita sesiones con estado; serializa las solicitudes
SessionId string null SessionId ID de sesión; genera automáticamente un GUID si es null y UseSession es true

Seguridad

Propiedad Tipo Predeterminado Clave de la cadena de conexión Descripción
SkipServerCertificateValidation bool false Omite la validación del certificado HTTPS; no debe usarse en producción

Configuración del cliente HTTP

Propiedad Tipo Valor predeterminado Clave de la cadena de conexión Descripción
HttpClient HttpClient null Instancia personalizada de HttpClient ya configurada
HttpClientFactory IHttpClientFactory null Fábrica personalizada para crear instancias de HttpClient
HttpClientName string null Nombre para que HttpClientFactory cree un cliente concreto

Registro y depuración

Propiedad Tipo Predeterminado Clave de cadena de conexión Descripción
LoggerFactory ILoggerFactory null Fábrica de registradores para el registro de diagnósticos
EnableDebugMode bool false Activa las trazas de red de .NET (requiere LoggerFactory con el nivel configurado en Trace); impacto significativo en el rendimiento

Ajustes personalizados y roles

Propiedad Tipo Predeterminado Clave de la cadena de conexión Descripción
CustomSettings IDictionary<string, object> Vacío prefijo set_* ajustes del servidor de ClickHouse; consulta la nota a continuación
Roles IReadOnlyList<string> Vacío Roles roles de ClickHouse separados por comas (p. ej., Roles=admin,reader)
ApplicationInfo IReadOnlyDictionary<string, string> Vacío Etiquetas de formato libre añadidas al encabezado HTTP User-Agent para atribuir consultas por aplicación.

Ejemplos de cadenas de conexión

Conexión básica

Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb

Con ajustes personalizados de ClickHouse

Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000

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.

Propiedad Tipo Descripción
QueryId string Identificador de consulta personalizado para el seguimiento en system.query_log o para cancelarla
Database string Anula la base de datos predeterminada para esta consulta
Roles IReadOnlyList<string> Anula los roles del cliente para esta consulta
CustomSettings IDictionary<string, object> Configuración del servidor de ClickHouse para esta consulta (por ejemplo, max_threads)
CustomHeaders IDictionary<string, string> Encabezados HTTP adicionales para esta consulta
UseSession bool? Anula el comportamiento de la sesión para esta consulta
SessionId string ID de sesión para esta consulta (requiere UseSession = true)
BearerToken string Anula el token de autenticación para esta consulta
ParameterTypeResolver IParameterTypeResolver Anula el resolver a nivel de cliente para la correspondencia de tipos de parámetros con estilo @; consulte Correspondencia personalizada de tipos de parámetros
ParameterFormatter IParameterFormatter Anula el formateador a nivel de cliente para la serialización de valores de parámetros con estilo @; consulte Formato personalizado de valores de parámetros
ReadValueConverter IReadValueConverter Anula la transformación a nivel de cliente aplicada a los valores devueltos por el lector de datos; consulte Conversión personalizada de valores leídos
MaxExecutionTime TimeSpan? Timeout de la consulta en el servidor (se pasa como la configuración max_execution_time); el servidor cancela la consulta si se supera
AcceptEncoding string Anulación por consulta del encabezado Accept-Encoding (por ejemplo, "zstd", "zstd, gzip;q=0.5"); también fuerza enable_http_compression=1 en la URL. Consulte Compresión de transporte por consulta.

Ejemplo:

var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

InsertOptions

InsertOptions amplía QueryOptions con opciones específicas para operaciones de inserción masiva mediante InsertBinaryAsync.

Propiedad Type Predeterminado Descripción
BatchSize int 100,000 Número de filas por lote
MaxDegreeOfParallelism int 1 Número de cargas de lotes en paralelo
Format RowBinaryFormat RowBinary Formato binario: RowBinary o RowBinaryWithDefaults
ColumnTypes IReadOnlyDictionary<string, string> null Nombre de columna → cadena de tipo de ClickHouse. Omite la consulta de sondeo del esquema cuando se especifica.
UseSchemaCache bool false Almacena en caché el esquema completo de la tabla para cada par (base de datos, tabla) durante la vida útil del cliente.

Todas las propiedades de QueryOptions también están disponibles en InsertOptions.

Ejemplo:

var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);

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:

var options = new InsertOptions
{
    ColumnTypes = new Dictionary<string, string>
    {
        ["id"] = "UInt64",
        ["name"] = "Nullable(String)",
        ["score"] = "Float32",
    },
};

await client.InsertBinaryAsync("my_table", ["id", "name", "score"], rows, options);

Opción 2: Almacenar en caché el esquema

Cuando realices inserciones repetidas en la misma tabla, establece UseSchemaCache = true para consultar el esquema una sola vez y reutilizarlo en las inserciones posteriores de la misma instancia de ClickHouseClient:

var options = new InsertOptions { UseSchemaCache = true };

// La primera llamada obtiene el esquema del servidor
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// La segunda llamada reutiliza el esquema en caché — sin ida y vuelta adicional
await client.InsertBinaryAsync("my_table", columns, batch2, options);

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 un ClickHouseClient 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:

Botón Connect del servicio de ClickHouse Cloud

Elija C#. Los detalles de la conexión se muestran a continuación.

Detalles de conexión de C# para ClickHouse Cloud

Si utiliza ClickHouse autogestionado, los detalles de la conexión los establece el administrador de ClickHouse.

Usar una cadena de conexión:

using ClickHouse.Driver;

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");

O bien, usando ClickHouseClientSettings:

using ClickHouse.Driver;

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);

Para escenarios con inyección de dependencias, use IHttpClientFactory:

// In your DI configuration
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");

Ejecutar consultas

Use ExecuteNonQueryAsync para las sentencias que no devuelven resultados:

// Crear una tabla
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Eliminar una tabla
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");

Usa ExecuteScalarAsync para obtener un único valor:

var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Número de filas: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Versión del servidor: {version}");

Insertar datos

Inserciones parametrizadas

Inserta datos mediante consultas parametrizadas con ExecuteNonQueryAsync. Los tipos de los parámetros deben especificarse en el SQL usando la sintaxis {name:Type}:

using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("id", 1L);
parameters.AddParameter("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);

Inserciones masivas

Use InsertBinaryAsync 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.

// Preparar datos como IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// Inserción básica
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");

Para grandes volúmenes de datos, configure el procesamiento por lotes y el paralelismo con InsertOptions:

var options = new InsertOptions
{
    BatchSize = 100_000,           // Filas por lote (predeterminado: 100,000)
    MaxDegreeOfParallelism = 4     // Cargas de lotes en paralelo (predeterminado: 1)
};

Inserciones con POCO

En lugar de construir arrays object[], puede insertar directamente objetos POCO fuertemente tipados. Registre el tipo una sola vez y luego pase IEnumerable<T>:

// Defina un POCO que se corresponda con las columnas de su tabla
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Registre el tipo (una vez durante la vida útil del cliente)
client.RegisterBinaryInsertType<SensorReading>();

// Inserte directamente: los nombres de las columnas se obtienen de los nombres de las propiedades
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

long rowsInserted = await client.InsertBinaryAsync("sensors", readings);

De forma predeterminada, todas las propiedades públicas de lectura se asignan a columnas mediante una coincidencia estricta de nombres que distingue entre mayúsculas y minúsculas. Puede personalizar la asignación con atributos:

public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Mapear a una columna con nombre distinto
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Tipo ClickHouse explícito
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Excluir del insert
    public string InternalTag { get; set; }
}
Atributo Propósito
[ClickHouseColumn(Name = "...")] Sobrescribe el nombre de la columna de destino
[ClickHouseColumn(Type = "...")] Declara explícitamente el tipo de ClickHouse
[ClickHouseNotMapped] Excluye la propiedad de la inserción

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[].

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 con DEFAULT (u otras expresiones predeterminadas) la rellena automáticamente el servidor. No se requieren cambios en el código ni volver a registrar nada.


Lectura de datos

Use ExecuteReaderAsync 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.

using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}

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 usa QueryAsync<T>:

// Define a POCO matching your result columns
public class SensorReading
{
    public ulong Id { get; set; }
    public DateTime Timestamp { get; set; }

    [ClickHouseColumn(Name = "sensor_name")]
    public string SensorName { get; set; }
    public double Value { get; set; }

}

// Register the type (once per client lifetime)
client.RegisterPocoType<SensorReading>();

// Stream results as typed objects
await foreach (var reading in client.QueryAsync<SensorReading>(
    "SELECT Id, sensor_name, Value, Timestamp FROM sensors"))
{
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
Registro

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 propiedades required.
Coincidencia de columnas

La coincidencia de columnas es sensible a mayúsculas y minúsculas. Las columnas de resultado que faltan dejan las propiedades con su valor predeterminado; las columnas de resultado adicionales se ignoran.

No hay conversiones automáticas y una incompatibilidad de tipos produce InvalidOperationException.

Materialización de una sola fila

Al iterar manualmente sobre un lector, use ClickHouseDataReader.MapTo<T>() para materializar la fila actual en un POCO registrado sin hacer avanzar el lector:

var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
Diagnóstico de registro

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:

SELECT {value:Array(UInt16)} as a
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})

Parámetros de Identifier

El tipo de parámetro Identifier 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":

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

var reader = await client.ExecuteReaderAsync("SELECT {col:Identifier} FROM t", parameters);

El valor se envía literalmente, y el servidor lo sustituye como un identificador SQL sin comillas, aplicando su propio entrecomillado con comillas invertidas y su propio escape. Los identificadores que contienen caracteres especiales (incluidas las comillas invertidas) se conservan de forma segura al hacer un recorrido de ida y vuelta.


ID de consulta

A cada consulta se le asigna un query_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:

var options = new QueryOptions
{
    QueryId = $"report-{Guid.NewGuid()}"
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

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:

using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterTypeResolver = new DictionaryParameterTypeResolver(new Dictionary<Type, string>
    {
        [typeof(DateTime)] = "DateTime64(3)",
        [typeof(decimal)] = "Decimal64(4)",
    }),
};
using var client = new ClickHouseClient(settings);

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("dt", DateTime.UtcNow);     // Mapped to DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Mapped to Decimal64(4)

await client.ExecuteReaderAsync("SELECT @dt, @amount", parameters);

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:

public class SmartDecimalResolver : IParameterTypeResolver
{
    public string ResolveType(Type clrType, object value, string parameterName)
    {
        if (clrType != typeof(decimal))
            return null; // Fall through to default

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}

También puede configurar un resolver para una sola consulta mediante 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:

  1. ClickHouseType explícito establecido en el parámetro
  2. Indicación de tipo SQL de la sintaxis {name:Type} en la consulta
  3. IParameterTypeResolver (de QueryOptions.ParameterTypeResolver, con fallback a ClickHouseClientSettings.ParameterTypeResolver)
  4. Inferencia de tipos integrada (TypeConverter.ToClickHouseType)

El resolver también funciona con la vía 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:

using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterFormatter = new DictionaryParameterFormatter(new Dictionary<Type, Func<object, string>>
    {
        [typeof(DateTime)] = v => ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.ffffff",
            System.Globalization.CultureInfo.InvariantCulture),
        [typeof(decimal)] = v => ((decimal)v).ToString("F4",
            System.Globalization.CultureInfo.InvariantCulture),
    }),
};
using var client = new ClickHouseClient(settings);

IParameterFormatter personalizado para casos avanzados:

public class FixedDecimalFormatter : IParameterFormatter
{
    public string Format(object value, string typeName, string parameterName)
    {
        if (value is decimal d)
            return d.ToString("F4", System.Globalization.CultureInfo.InvariantCulture);
        return null; // Fall through for anything else
    }
}

También puede establecer un formateador por consulta mediante 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:

  1. IParameterFormatter (de QueryOptions.ParameterFormatter, con respaldo en ClickHouseClientSettings.ParameterFormatter). Si devuelve un valor no nulo, se usa ese valor.
  2. Formato integrado específico del tipo en HttpParameterFormatter.

No se consulta al formateador para valores 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:

using ClickHouse.Driver.ADO.Readers;

var converter = new DictionaryReadValueConverter()
    .For<DateTime>(dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))
    .For<string>(s => s.Trim());

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ReadValueConverter = converter,
};
using var client = new ClickHouseClient(settings);

Los valores cuyo tipo CLR en tiempo de ejecución no esté registrado con 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:

public class UtcKindForNoTzDateTimeConverter : IReadValueConverter
{
    public object ConvertValue(object value, string columnName, string clickhouseType)
    {
        if (value is DateTime dt && clickhouseType == "DateTime")
            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }

    public T ConvertValue<T>(T value, string columnName, string clickhouseType)
    {
        if (typeof(T) == typeof(DateTime) && value is DateTime dt && clickhouseType == "DateTime")
            return (T)(object)DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }
}

El convertidor debe preservar el tipo CLR en tiempo de ejecución; los metadatos de columna (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.

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

Use ExecuteRawResultAsync 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:

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();

Formatos comunes: 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 negocia gzip, deflate cuando Compression=true (el valor predeterminado de la cadena de conexión), y el cliente HTTP descomprime el flujo 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).

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT Parquet",
    options: new QueryOptions { AcceptEncoding = "zstd" });

// Decode yourself or write to a file
await using var body = await result.ReadAsStreamAsync();

Configuración de HttpClient

El HttpClient predeterminado que crea el driver tiene AutomaticDecompression = GZip | Deflate, lo que descomprime automáticamente esos algoritmos y elimina Content-Encoding de la respuesta. Esto es lo habitual para las consultas normales, pero no si quieres encargarte tú mismo de los datos comprimidos en bruto.

En ese caso, pasa un HttpClient o HttpClientFactory a ClickHouseClient con AutomaticDecompression = DecompressionMethods.None

Cuerpos de error

Cuando el servidor responde con un 4xx/5xx y se ha establecido enable_http_compression=1, comprime el cuerpo del error con el mismo códec que habría usado para una respuesta satisfactoria. El driver los descomprime en el caso de los códecs que incluye la BCL (gzip, deflate, br/brotli), de modo que el mensaje que aparece en ClickHouseServerException sea legible. Para los códecs que no puede decodificar (zstd, lz4, …), el driver devuelve un mensaje provisional que indica el códec y remite a system.query_log para ver el texto original del error.


Inserción desde flujo sin procesar

Use InsertRawStreamAsync para insertar datos directamente desde flujos de archivo o de memoria en formatos como CSV, JSON, Parquet o cualquier formato compatible con ClickHouse.

Insertar desde un archivo CSV:

await using var fileStream = File.OpenRead("data.csv");

using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: fileStream,
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);

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 mediante ClickHouseConnection, 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 un ClickHouseDataSource 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.

using ClickHouse.Driver.ADO;

// Crear DataSource una vez (registrar como singleton en DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Crear conexiones ligeras según sea necesario
await using var connection = await dataSource.OpenConnectionAsync();

// Usar la conexión
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();

Para la inyección de dependencias:

// En Startup.cs o Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// En el servicio
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Usa la conexión...
    }
}

Uso de ClickHouseCommand

Cree comandos a partir de una conexión para ejecutar SQL:

await using var connection = await dataSource.OpenConnectionAsync();

// Crear comando con SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Ejecutar y leer resultados
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}

Métodos del comando:

  • ExecuteNonQueryAsync() - Para INSERT, UPDATE, DELETE y sentencias DDL
  • ExecuteScalarAsync() - Devuelve 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 la consulta:

await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // Acceso por índice de columna
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Acceso por nombre de columna
    var email = reader.GetString("email");

    // Acceso genérico
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Comprobar si es NULL
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}

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 ClickHouseClient o ClickHouseConnection.

Patrones recomendados:

Escenario Enfoque recomendado
Uso general Use un ClickHouseClient singleton
ADO.NET / ORMs Use ClickHouseDataSource (crea conexiones que comparten el mismo pool)
Entornos de DI Registre ClickHouseClient o ClickHouseDataSource como singleton con IHttpClientFactory

Gestión de DateTime

  1. Usa UTC siempre que sea posible. Almacena las marcas de tiempo como columnas DateTime('UTC') y usa DateTimeKind.Utc en tu código. Esto elimina la ambigüedad de la zona horaria.

  2. Usa DateTimeOffset para gestionar explícitamente la zona horaria. Siempre representa un instante específico e incluye la información de desplazamiento.

  3. Especifica la zona horaria en las indicaciones de tipo de SQL. Al usar parámetros con valores DateTime Unspecified destinados a columnas que no son UTC, incluye la zona horaria en el SQL:

    var parameters = new ClickHouseParameterCollection();
    parameters.AddParameter("dt", myDateTime);
    
    await client.ExecuteNonQueryAsync(
        "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
        parameters
    );

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 mediante CustomSettings o la cadena de conexión:

// Usando CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recomendado: esperar confirmación de flush

// O mediante connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"

Dos modos (controlados por wait_for_async_insert):

Modo Comportamiento Caso de uso
wait_for_async_insert=1 La inserción devuelve después de que los datos se escriben en disco. Los errores se devuelven al cliente. Recomendado para la mayoría de las cargas de trabajo
wait_for_async_insert=0 La inserción devuelve inmediatamente cuando los datos se almacenan en el búfer. No se garantiza que los datos se persistan. Solo cuando la pérdida de datos sea aceptable

Configuraciones clave:

Configuración Descripción
async_insert_max_data_size Vacía el búfer cuando alcanza este tamaño (bytes)
async_insert_busy_timeout_ms Vacía el búfer cuando se alcanza este tiempo de espera (milisegundos)
async_insert_max_query_number Vacía el búfer cuando se acumula esta cantidad de consultas

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)

Cuando las sesiones están habilitadas, las solicitudes se serializan para evitar el uso concurrente de la misma sesión. Esto añade sobrecarga a las cargas de trabajo que no requieren estado de sesión.

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Opcional -- se generará automáticamente si no se especifica
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);

Uso de ADO.NET (para compatibilidad con ORM):

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();

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

Tipo de ClickHouse Tipo de .NET
Int8 sbyte
UInt8 byte
Int16 short
UInt16 ushort
Int32 int
UInt32 uint
Int64 long
UInt64 ulong
Int128 BigInteger
UInt128 BigInteger
Int256 BigInteger
UInt256 BigInteger

Tipos de coma flotante

Tipo de ClickHouse Tipo de .NET
Float32 float
Float64 double
BFloat16 float

Tipos decimales

Tipo de ClickHouse Tipo de .NET
Decimal(P, S) decimal / ClickHouseDecimal
Decimal32(S) decimal / ClickHouseDecimal
Decimal64(S) decimal / ClickHouseDecimal
Decimal128(S) decimal / ClickHouseDecimal
Decimal256(S) decimal / ClickHouseDecimal

Tipo booleano

Tipo de ClickHouse Tipo de .NET
Bool bool

Tipos String

Tipo de ClickHouse Tipo de .NET
String string
FixedString(N) string

Tipos de fecha y hora

ClickHouse Type .NET Type
Date DateTime
Date32 DateTime
DateTime DateTime
DateTime32 DateTime
DateTime64 DateTime
Time TimeSpan
Time64 TimeSpan

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:

Definición de columna DateTime.Kind devuelto Notas
DateTime('UTC') Utc Zona horaria UTC explícita
DateTime('Europe/Amsterdam') Unspecified Se aplica el desplazamiento
DateTime Unspecified Se conserva la hora tal cual

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:

var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)

Para las columnas sin una zona horaria explícita (es decir, 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:

  1. Use zonas horarias explícitas en las definiciones de sus columnas: DateTime('UTC') o DateTime('Europe/Amsterdam')
  2. Aplique usted mismo la zona horaria después de leer el valor.

Tipo JSON

Tipo de ClickHouse Tipo de .NET Notas
Json JsonObject Predeterminado (JsonReadMode=Binary)
Json string Cuando JsonReadMode=String

El tipo de retorno de las columnas JSON está determinado por la configuración JsonReadMode:

  • Binary (predeterminado): Devuelve System.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 como string. 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.

// Configurar el modo string mediante configuración
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// O mediante cadena de conexión
// "Host=localhost;JsonReadMode=String"

Otros tipos

Tipo de ClickHouse Tipo de .NET
UUID Guid
IPv4 IPAddress
IPv6 IPAddress
Nothing DBNull
Dynamic Vea la nota
Array(T) T[] (el Array(Array(T)) anidado se lee como un T[][] escalonado; use reader.GetFieldValue<T[,]>(ordinal) para materializar datos rectangulares como una matriz CLR multidimensional)
Tuple(T1, T2, …) Tuple<T1, T2, ...> / LargeTuple
Map(K, V) Dictionary<K, V>
Nullable(T) T?
Enum8 string
Enum16 string
LowCardinality(T) Igual que T
SimpleAggregateFunction Igual que el tipo subyacente
Nested(…) Tuple[]
Variant(T1, T2, …) Vea la nota
QBit(T, dimension) T[]

Tipos de geometría

Tipo de ClickHouse Tipo de .NET
Point Tuple<double, double>
Ring Tuple<double, double>[]
LineString Tuple<double, double>[]
Polygon Ring[]
MultiLineString LineString[]
MultiPolygon Polygon[]
Geometry Consulte la nota

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

Tipo de ClickHouse Tipos .NET aceptados Notas
Int8 sbyte, cualquier tipo compatible con Convert.ToSByte()
UInt8 byte, cualquier tipo compatible con Convert.ToByte()
Int16 short, cualquier tipo compatible con Convert.ToInt16()
UInt16 ushort, cualquier tipo compatible con Convert.ToUInt16()
Int32 int, cualquier tipo compatible con Convert.ToInt32()
UInt32 uint, cualquier tipo compatible con Convert.ToUInt32()
Int64 long, cualquier tipo compatible con Convert.ToInt64()
UInt64 ulong, cualquier tipo compatible con Convert.ToUInt64()
Int128 BigInteger, decimal, double, float, int, uint, long, ulong, cualquier tipo compatible con Convert.ToInt64()
UInt128 BigInteger, decimal, double, float, int, uint, long, ulong, cualquier tipo compatible con Convert.ToInt64()
Int256 BigInteger, decimal, double, float, int, uint, long, ulong, cualquier tipo compatible con Convert.ToInt64()
UInt256 BigInteger, decimal, double, float, int, uint, long, ulong, cualquier tipo compatible con Convert.ToInt64()

Tipos de coma flotante

Tipo de ClickHouse Tipos de .NET aceptados Notas
Float32 float, cualquier tipo compatible con Convert.ToSingle()
Float64 double, cualquier tipo compatible con Convert.ToDouble()
BFloat16 float, cualquier tipo compatible con Convert.ToSingle() Se trunca al formato brain float de 16 bits

Tipo booleano

Tipo de ClickHouse Tipos de .NET aceptados Notas
Bool bool

Tipos de cadena

Tipo de ClickHouse Tipos de .NET aceptados Notas
String string, byte[], ReadOnlyMemory<byte>, Stream Los tipos binarios se escriben directamente; los streams pueden ser posicionables o no
FixedString(N) string, byte[], ReadOnlyMemory<byte>, Stream String se codifica en UTF-8 y se rellena; los tipos binarios deben tener exactamente N bytes

Tipos de fecha y hora

Tipo de ClickHouse Tipos de .NET aceptados Notas
Date DateTime, DateTimeOffset, DateOnly, tipos de NodaTime Convertido a días Unix como UInt16; rango admitido [1970-01-01, 2149-06-06]
Date32 DateTime, DateTimeOffset, DateOnly, tipos de NodaTime Convertido a días Unix como Int32; rango admitido [1900-01-01, 2299-12-31]
DateTime DateTime, DateTimeOffset, DateOnly, tipos de NodaTime Consulta más abajo para obtener detalles; rango admitido [1970-01-01, 2106-02-07 06:28:15] UTC
DateTime32 DateTime, DateTimeOffset, DateOnly, tipos de NodaTime Igual que DateTime
DateTime64 DateTime, DateTimeOffset, DateOnly, tipos de NodaTime Precisión basada en el parámetro Scale
Time TimeSpan, int Limitado a ±999:59:59; int se trata como segundos
Time64 TimeSpan, decimal, double, float, int, long, string La cadena se interpreta como [-]HHH:MM:SS[.fraction]; limitado a ±999:59:59.999999999

El driver respeta DateTime.Kind al escribir valores:

DateTime.Kind Parámetros HTTP Copia masiva
Utc Se conserva el instante Se conserva el instante
Local Se conserva el instante Se conserva el instante
Unspecified Se trata como hora local en la zona horaria del tipo de parámetro (UTC de forma predeterminada) Se trata como hora local en la zona horaria de la columna

Los valores DateTimeOffset siempre conservan el instante exacto.

Ejemplo: DateTime UTC (se conserva el instante)

var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC

Ejemplo: DateTime sin especificar (hora local)

var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30

Recomendación: para obtener el comportamiento más simple y predecible, use 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 DateTime Unspecified:

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:

// CORRECTO: Zona horaria en la indicación de tipo SQL - el tipo se extrae automáticamente
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECTO: Sin indicación de zona horaria, se interpreta como UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// ¡El valor de cadena "2024-01-15 14:30:00" se interpreta como UTC, no como hora de Ámsterdam!
DateTime.Kind Columna de destino Parámetro HTTP (con indicación de zona horaria) Parámetro HTTP (sin indicación de zona horaria) Copia masiva
Utc UTC Se conserva el instante Se conserva el instante Se conserva el instante
Utc Europe/Amsterdam Se conserva el instante Se conserva el instante Se conserva el instante
Local Cualquiera Se conserva el instante Se conserva el instante Se conserva el instante
Unspecified UTC Se trata como UTC Se trata como UTC Se trata como UTC
Unspecified Europe/Amsterdam Se trata como hora de Ámsterdam Se trata como UTC Se trata como hora de Ámsterdam

Tipos Decimal

Tipo de ClickHouse Tipos de .NET aceptados Notas
Decimal(P,S) decimal, ClickHouseDecimal, cualquier tipo compatible con Convert.ToDecimal() Lanza OverflowException si excede la precisión
Decimal32 decimal, ClickHouseDecimal, cualquier tipo compatible con Convert.ToDecimal() Precisión máxima: 9
Decimal64 decimal, ClickHouseDecimal, cualquier tipo compatible con Convert.ToDecimal() Precisión máxima: 18
Decimal128 decimal, ClickHouseDecimal, cualquier tipo compatible con Convert.ToDecimal() Precisión máxima: 38
Decimal256 decimal, ClickHouseDecimal, cualquier tipo compatible con Convert.ToDecimal() Precisión máxima: 76

Tipo JSON

Tipo de ClickHouse Tipos de .NET aceptados Notas
Json string, JsonObject, JsonNode, cualquier objeto El comportamiento depende del ajuste JsonWriteMode

El comportamiento al escribir JSON está controlado por el ajuste JsonWriteMode:

Tipo de entrada JsonWriteMode.String (predeterminado) JsonWriteMode.Binary
string Se pasa directamente Lanza ArgumentException
JsonObject Se serializa con ToJsonString() Lanza ArgumentException
JsonNode Se serializa con ToJsonString() Lanza ArgumentException
POCO registrado Se serializa con JsonSerializer.Serialize() Codificación binaria con indicaciones de tipo; se admiten atributos de ruta personalizados
POCO no registrado / objeto anónimo Se serializa con JsonSerializer.Serialize() Lanza ClickHouseJsonSerializationException
  • 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 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 a connection.RegisterJsonSerializationType<T>() antes de usarlo. Escribir valores string o JsonNode en este modo lanza ArgumentException.

// El modo String predeterminado funciona con cualquier entrada
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// El modo Binary requiere habilitación explícita y registro de tipos
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
Columnas JSON tipadas

Cuando una columna JSON tiene indicaciones de tipo (p. ej., 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.

Serialización de POCO

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.

CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
using ClickHouse.Driver.Json;

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // No se serializa
}

// Para el modo Binary: registre el tipo y habilítelo
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insertar un POCO: se serializa a JSON con una estructura anidada mediante atributos de ruta personalizados
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// JSON resultante: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}

La coincidencia entre el nombre de la propiedad y las indicaciones de tipo de la columna es sensible a mayúsculas y minúsculas. Una propiedad 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 lanza ClickHouseJsonSerializationException.
  • 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 tipos Nullable dentro de rutas JSON dinámicas, por lo que las propiedades con valor NULL sin indicación se omiten.
  • Los atributos ClickHouseJsonPath y ClickHouseJsonIgnore se ignoran en modo String (solo funcionan en modo Binary).

Otros tipos

Tipo de ClickHouse Tipos de .NET aceptados Notas
UUID Guid, string La cadena se interpreta como Guid
IPv4 IPAddress, string Debe ser IPv4; la cadena se analiza con IPAddress.Parse()
IPv6 IPAddress, string Debe ser IPv6; la cadena se analiza con IPAddress.Parse()
Nothing Any No escribe nada (no-op)
Dynamic No admitido (lanza NotImplementedException)
Array(T) IList, null null escribe un array vacío. Para los tipos anidados (Array(Array(T)) y más profundos), se aceptan tanto las formas irregulares (T[][], List<List<T>>) como los arrays CLR multidimensionales rectangulares (T[,], T[,,], …); el número de dimensiones de CLR debe coincidir con la profundidad de anidamiento de ClickHouse.
Tuple(T1, T2, …) ITuple, IList El número de elementos debe coincidir con la aridad de la tupla. Consulta la advertencia sobre ValueTuple para más de 7 elementos.
Map(K, V) IDictionary
Nullable(T) null, DBNull, o tipos aceptados por T Escribe un byte indicador de null antes del valor
Enum8 string, sbyte, tipos numéricos La cadena se busca en el diccionario del enum
Enum16 string, short, tipos numéricos La cadena se busca en el diccionario del enum
LowCardinality(T) Tipos aceptados por T Se delega en el tipo subyacente
SimpleAggregateFunction Tipos aceptados por el tipo subyacente Se delega en el tipo subyacente
Nested(…) IList de tuplas El número de elementos debe coincidir con el número de campos
Variant(T1, T2, …) Valor que coincida con uno de T1, T2, … Lanza ArgumentException si no coincide ningún tipo
QBit(T, dim) IList Se delega en Array; la dimensión es solo metadatos

Tipos de geometría

Tipo de ClickHouse Tipos de .NET aceptados Notas
Point System.Drawing.Point, ITuple, IList (2 elementos)
Ring IList de Point
LineString IList de Point
Polygon IList de Ring
MultiLineString IList de LineString
MultiPolygon IList de Polygon
Geometry Cualquier tipo de geometría anterior Variante de todos los tipos de geometría

No admitido para escritura

Tipo de ClickHouse Notas
Dynamic Lanza NotImplementedException
AggregateFunction Lanza AggregateFunctionException

Manejo de tipos anidados

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

CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

await client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);

Registro y diagnósticos

El cliente .NET de ClickHouse se integra con las abstracciones de Microsoft.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

using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Information);
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

Uso de appsettings.json

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

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

Uso de la configuración en memoria

También puede configurar en el código la verbosidad del registro por categoría:

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

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:

Categoría Origen Aspectos destacados
ClickHouse.Driver.Connection ClickHouseConnection Ciclo de vida de la conexión, selección de la factoría de clientes HTTP, apertura y cierre de conexiones, gestión de sesiones.
ClickHouse.Driver.Command ClickHouseCommand Inicio y finalización de la ejecución de consultas, tiempos, ID de consulta, estadísticas del servidor y detalles de errores.
ClickHouse.Driver.Transport ClickHouseConnection Solicitudes HTTP streaming de bajo nivel, indicadores de compresión, códigos de estado de la respuesta y errores de transporte.
ClickHouse.Driver.Client ClickHouseClient Inserción binaria, consultas y otras operaciones
ClickHouse.Driver.NetTrace TraceHelper Trazado de red, solo cuando el modo de depuración está habilitado

Ejemplo: Cómo diagnosticar problemas de conexión

{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}

Esto registrará:

  • Selección de la fábrica de clientes HTTP (pool predeterminado frente a conexión única)
  • Configuración del controlador HTTP (SocketsHttpHandler o HttpClientHandler)
  • 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 clase ClickHouse.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.

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Debe estar en el nivel Trace para ver los eventos de red
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Habilita el tracing de red de bajo nivel
};

OpenTelemetry

El driver ofrece compatibilidad integrada con el tracing distribuido de OpenTelemetry mediante la API de .NET System.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 el ActivitySource del driver de ClickHouse a su configuración de OpenTelemetry:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Suscribirse a los spans del driver de ClickHouse
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // O bien AddJaegerExporter(), etc.

Para aplicaciones de consola, pruebas o configuración manual:

using OpenTelemetry;
using OpenTelemetry.Trace;

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)
    .AddConsoleExporter()
    .Build();

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.

Atributo Descripción
db.system Siempre "clickhouse"
db.name Nombre de la base de datos
db.user Nombre de usuario
db.statement Consulta SQL (si está habilitada)
db.clickhouse.read_rows Filas leídas por la consulta
db.clickhouse.read_bytes Bytes leídos por la consulta
db.clickhouse.written_rows Filas escritas por la consulta
db.clickhouse.written_bytes Bytes escritos por la consulta
db.clickhouse.elapsed_ns Tiempo de ejecución del lado del servidor en nanosegundos

Opciones de configuración

Controle el comportamiento del tracing con ClickHouseDiagnosticsOptions:

using ClickHouse.Driver.Diagnostic;

// Incluir sentencias SQL en spans (valor predeterminado: false por seguridad)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncar sentencias SQL largas (valor predeterminado: 1000 caracteres)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;

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 propio HttpClient con un controlador ServerCertificateCustomValidationCallback configurado:

using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // Obligatorio cuando la compresión está activada (valor predeterminado)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Ejemplo: Aceptar la huella digital de un certificado concreto
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Ejemplo: Aceptar certificados de un emisor concreto
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Valor predeterminado: usar la validación estándar
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);

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:

// Registrar DataSource como singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Crear conexiones para usarlas con el ORM
await using var connection = await dataSource.OpenConnectionAsync();
// Pasar la conexión al ORM...

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:

var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Registrar como singleton en la DI

using var connection = dataSource.CreateConnection();

Estilos para pasar parámetros

Se admiten todos los estilos estándar de parámetros de Dapper:

Objetos anónimos:

await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)",
    new { Id = 1, Name = "alice", Balance = 3.14 });

Clases POCO:

class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

var param = new InsertParams { Id = 42, Name = "bob", Balance = 99.9 };
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)", param);

Diccionario:

var parameters = new Dictionary<string, object> { { "Id", 2 } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", parameters);

DynamicParameters (de un diccionario o de un objeto anónimo):

var dynParams = new DynamicParameters(new { Id = 1 });
// o bien: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", dynParams);

Consultas con POCOs

Dapper asigna columnas a propiedades por nombre (sin distinguir entre mayúsculas y minúsculas):

class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// De una tabla
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// De un literal
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();

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.

var parameters = new Dictionary<string, object> { { "value", 42 } };
var result = await connection.QueryAsync<int>("SELECT {value:Int32}", parameters);

WHERE IN

La expansión nativa de IN de Dapper funciona:

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id IN @Ids ORDER BY id",
    new { Ids = new[] { 1, 3, 5 } });

Dapper reescribe esto como 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:

var parameters = new Dictionary<string, object> { { "ids", new[] { 1, 3, 5 } } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE has({ids:Array(Int32)}, id) ORDER BY id",
    parameters);

Manejadores de tipos personalizados

Algunos tipos de ClickHouse, p. ej., ITuple, BigInteger y ClickHouseDecimal, requieren registrar manejadores al inicio:

// ClickHouseDecimal (para columnas Decimal64/128/256)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (para columnas Int128/Int256/UInt128/UInt256)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (para columnas IPv4/IPv6)
SqlMapper.AddTypeHandler(new IpAddressHandler());

Consulte el ejemplo de Dapper como ejemplo de implementación de un manejador de tipos.

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.

[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

var all = await connection.GetAllAsync<UserRecord>();
var one = await connection.GetAsync<UserRecord>(1);

Los nombres de las propiedades deben coincidir exactamente con los nombres de columna de ClickHouse (la coincidencia es sensible a mayúsculas y minúsculas).

Limitaciones

Qué Estado Detalles
Tuple como resultado Funciona Requiere registrar SqlMapper.TypeHandler<ITuple>
Tuple como parámetro No compatible Dapper no puede serializar ITuple/Tuple<> como valor de DbParameter
Tipos anidados como parámetro No compatible Mismo motivo: Dapper rechaza los tipos complejos como valores de parámetro
Tipos Geo como parámetro No compatible Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon
Dapper.Contrib.Insert<T>() No compatible Genera sintaxis específica de SQL Server
Tipo Nothing No compatible No tiene una representación significativa en .NET

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 una DataConnection con el proveedor de ClickHouse:

using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);

Los mapeos de tablas pueden definirse mediante atributos o la API fluida. Si los nombres de la clase y de las propiedades coinciden exactamente con los nombres de la tabla y de las columnas, no se necesita ninguna configuración:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Consultas:

await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();

Copia masiva:

Utilice BulkCopyAsync para realizar inserciones masivas de forma eficiente.

await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);

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 mediante SaveChanges, todo ello con los patrones habituales de EF Core.

Instalación

dotnet add package ClickHouse.EntityFrameworkCore

Requiere .NET 10.0 y EF Core 10.

Inicio rápido

Define la entidad y DbContext, y luego haz consultas con LINQ:

using Microsoft.EntityFrameworkCore;

public class PageView
{
    public long Id { get; set; }
    public string Path { get; set; }
    public DateOnly Date { get; set; }
    public string UserAgent { get; set; }
}

public class AnalyticsContext : DbContext
{
    public DbSet<PageView> PageViews { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseClickHouse("Host=localhost;Database=analytics");
}

// Consulta
await using var ctx = new AnalyticsContext();

var topPages = await ctx.PageViews
    .Where(v => v.Date >= new DateOnly(2024, 1, 1))
    .GroupBy(v => v.Path)
    .Select(g => new { Path = g.Key, Views = g.Count() })
    .OrderByDescending(x => x.Views)
    .Take(10)
    .ToListAsync();

Tipos compatibles

Categoría Tipos de ClickHouse Tipos de CLR
Enteros Int8Int64, UInt8UInt64 sbyte, short, int, long, byte, ushort, uint, ulong
Enteros grandes Int128, Int256, UInt128, UInt256 BigInteger
Punto flotante Float32, Float64, BFloat16 float, double
Decimales Decimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S) decimal o ClickHouseDecimal
Bool Bool bool
Cadenas String, FixedString(N) string
Enumeraciones Enum8(...), Enum16(...) string o enum de C#
Fecha/hora Date, Date32, DateTime, DateTime64(P, 'TZ') DateOnly, DateTime
Hora Time, Time64(N) TimeSpan
UUID UUID Guid
Red IPv4, IPv6 IPAddress
Arrays Array(T) T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
Mapas Map(K, V) Dictionary<K,V>
Tuples Tuple(T1, ...) Tuple<...> o ValueTuple<...>
Variant Variant(T1, T2, ...) object
Dinámico Dynamic object
JSON Json JsonNode o string
Geográficos Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon, Geometry Tuple<double,double> y arrays de estos; object para Geometry
Envoltorios Nullable(T), LowCardinality(T) Se desempaquetan automáticamente

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.

Semántica de NULL en LEFT JOIN

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:

optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());

Con la exclusión activada, LEFT JOIN devuelve los valores predeterminados de las columnas de ClickHouse y la detección de navegación de EF basada en valores nulos deja de funcionar como se espera. Use comparaciones explícitas con 0 / "" en lugar de == null.

Inserción de datos

SaveChanges usa la API nativa InsertBinaryAsync del driver: codificación RowBinary con compresión GZip, mucho más eficiente que SQL con parámetros:

await using var ctx = new AnalyticsContext();

ctx.PageViews.Add(new PageView
{
    Id = 1,
    Path = "/home",
    Date = new DateOnly(2024, 6, 15),
    UserAgent = "Mozilla/5.0"
});

await ctx.SaveChangesAsync();

Las entidades pasan de Added a Unchanged tras guardar, igual que con cualquier otro proveedor de EF Core.

El tamaño del lote es configurable (valor predeterminado: 1000):

optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));

Inserción masiva

Para cargas de alto rendimiento, use BulkInsertAsync 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 compresión GZip.

Esto lo hace adecuado para cargar grandes volúmenes de datos cuando no necesita el seguimiento de entidades después de la inserción:

var events = Enumerable.Range(0, 100_000)
    .Select(i => new PageView
    {
        Id = i,
        Path = $"/page/{i}",
        Date = DateOnly.FromDateTime(DateTime.Today)
    });

long rowsInserted = await ctx.BulkInsertAsync(events);

La entrada puede ser cualquier 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 AddedUnchanged.

Enumeraciones

Las columnas Enum8/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:

public enum Status { Active, Inactive, Pending }

public class User
{
    public long Id { get; set; }
    public Status Status { get; set; }
}

// Consulta con valores de enum
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();

Conversiones de tipos personalizadas

El sistema ValueConverter 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:

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public long Id { get; set; }
    public Money Price { get; set; }
}

// En el método OnModelCreating:
modelBuilder.Entity<Order>()
    .Property(o => o.Price)
    .HasConversion(
        m => $"{m.Amount}|{m.Currency}",
        s => new Money
        {
            Amount = decimal.Parse(s.Split('|')[0]),
            Currency = s.Split('|')[1]
        })
    .HasColumnType("String");

Clase de convertidor reutilizable:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter() : base(
        m => $"{m.Amount}|{m.Currency}",
        s => Parse(s)) { }

    private static Money Parse(string s)
    {
        var parts = s.Split('|');
        return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
    }
}

// Aplicar a una propiedad individual:
.HasConversion<MoneyConverter>()

// O aplicarlo a todas las propiedades de un tipo mediante convenciones:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}

Anotaciones de tipo de columna

Para tipos escalares como string, 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):

using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("sensor_readings")]
public class SensorReading
{
    public long Id { get; set; }

    [Column(TypeName = "Array(String)")]
    public string[] Tags { get; set; }

    [Column(TypeName = "Map(String, String)")]
    public Dictionary<string, string> Metadata { get; set; }

    [Column(TypeName = "Nullable(Float64)")]
    public double? Value { get; set; }

    [Column(TypeName = "Decimal128(18)")]
    public decimal HighPrecision { get; set; }
}

Uso de la API fluida en OnModelCreating:

modelBuilder.Entity<SensorReading>(e =>
{
    e.ToTable("sensor_readings");
    e.Property(x => x.Tags).HasColumnType("Array(String)");
    e.Property(x => x.Metadata).HasColumnType("Map(String, String)");
    e.Property(x => x.Value).HasColumnType("Nullable(Float64)");
    e.Property(x => x.Category).HasColumnType("LowCardinality(String)");
    e.Property(x => x.HighPrecision).HasColumnType("Decimal128(18)");
});

Se admiten envoltorios anidados como 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 columnas Variant(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():

public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// En OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// o bien:
entity.Property(e => e.Payload).HasColumnType("Dynamic");

Al leer, el valor se deserializa automáticamente al tipo .NET correspondiente según el discriminador almacenado (p. ej., string, ulong, ulong[]).

Columnas JSON

El proveedor admite el tipo de columna Json de ClickHouse, que se corresponde con System.Text.Json.Nodes.JsonNode (principal) o string (mediante ValueConverter automático):

using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// En OnModelCreating:
entity.Property(e => e.Data).HasColumnType("Json");

La lectura y escritura de JSON funcionan tanto con SaveChanges como con BulkInsertAsync:

ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"

Si prefiere cadenas JSON sin procesar, asigne a la propiedad el tipo string con un tipo de columna Json; el proveedor aplica automáticamente un ValueConverter:

public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // cadena JSON sin procesar
}

entity.Property(e => e.Data).HasColumnType("Json");

Motores de tablas

Configure los motores de tablas 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 proveedor usa MergeTree de forma predeterminada, con ORDER BY derivado de la clave primaria de la entidad.

modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});

Familias de motores compatibles:

Engine Método fluido Notas
MergeTree HasMergeTreeEngine() Predeterminado si no se configura ninguno
ReplacingMergeTree HasReplacingMergeTreeEngine("Version", "IsDeleted") o HasReplacingMergeTreeEngine<T>(e => e.Version) Las columnas Version / IsDeleted son opcionales
SummingMergeTree HasSummingMergeTreeEngine(…) o HasSummingMergeTreeEngine<T>(e => new { … }) Columnas a sumar opcionales
AggregatingMergeTree HasAggregatingMergeTreeEngine()
CollapsingMergeTree HasCollapsingMergeTreeEngine("Sign") o HasCollapsingMergeTreeEngine<T>(e => e.Sign) La columna Sign debe ser Int8
VersionedCollapsingMergeTree HasVersionedCollapsingMergeTreeEngine("Sign", "Version") o <T>(e => e.Sign, e => e.Version)
GraphiteMergeTree HasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, Memory HasLogEngine(), HasTinyLogEngine(), HasStripeLogEngine(), HasMemoryEngine() Sin ORDER BY / PARTITION BY

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(...):

modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// Índice con parámetros (p. ej., bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);

Los índices estándar (sin omitir datos) se ignoran sin avisar, ya que ClickHouse no tiene ningún equivalente. Los índices únicos provocan un error, ya que ClickHouse no garantiza la unicidad.

Migraciones

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

dotnet ef migrations add InitialCreate
dotnet ef database update

Operaciones admitidas:

Operación Genera
CREATE TABLE Incluye la cláusula ENGINE, ORDER BY, PARTITION BY, SETTINGS, códecs/TTL/comentarios/valores predeterminados de columnas
ALTER TABLE ADD COLUMN
ALTER TABLE DROP COLUMN
ALTER TABLE MODIFY COLUMN Gestiona el cambio de tipo, además de la adición/eliminación de anotaciones (CODEC, TTL, COMMENT, DEFAULT)
ALTER TABLE RENAME COLUMN
RENAME TABLE
ALTER TABLE ADD INDEX / DROP INDEX Solo índices de omisión de datos
CREATE DATABASE / DROP DATABASE Mediante EnsureCreated / EnsureDeleted y migraciones

Limitaciones de las migraciones

Funcionalidad Motivo
Claves foráneas ClickHouse no hace cumplir las claves foráneas. Las migraciones rechazan AddForeignKey; el validador del modelo emite una advertencia al compilar el modelo.
Restricciones de unicidad / índices únicos ClickHouse no hace cumplir la unicidad. Los índices únicos generan un error durante la migración.
Valores generados por el servidor (incremento automático / IDENTITY) ClickHouse no tiene un equivalente.
columnas Nested(…) Aún no son compatibles como tipo CLR asignado.
Entidades owned como JSON (.ToJson()) La correspondencia estructural de JSON para entidades owned aún no está implementada. Use JsonNode / string en una columna Json en su lugar (consulte columnas JSON).

Además de las migraciones, el proveedor tampoco admite aún:

  • UPDATE / DELETE
  • Transacciones: BeginTransaction es 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 SQL data.key de 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 tipos ValueTuple 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

Ambas producen el mismo tipo de .NET: 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:

// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))

Columnas de AggregateFunction

Las columnas de tipo AggregateFunction(...) no se pueden consultar ni insertar directamente.

Para insertar:

INSERT INTO t VALUES (uniqState(1));

Para consultar:

SELECT uniqMerge(c) FROM t;

Navigation