Skip to main content
Le client C# officiel pour se connecter à ClickHouse. Le code source du client est disponible dans le dépôt GitHub. Développé à l’origine par Oleg V. Kozlyuk. La bibliothèque propose deux API principales :
  • ClickHouseClient (recommandé) : un client de haut niveau, thread-safe, conçu pour être utilisé comme singleton. Il fournit une API asynchrone simple pour les requêtes et les insertions en masse. C’est le meilleur choix pour la plupart des applications.
  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand) : les abstractions standard de base de données de .NET. Indispensable pour l’intégration avec les ORM (Dapper, Linq2db) et lorsque vous avez besoin de la compatibilité ADO.NET. ClickHouseBulkCopy est une classe utilitaire qui permet d’insérer efficacement des données à l’aide d’une connexion ADO.NET. ClickHouseBulkCopy est obsolète et sera supprimé dans une prochaine version ; utilisez plutôt ClickHouseClient.InsertBinaryAsync.
Les deux API partagent le même pool de connexions HTTP sous-jacent et peuvent être utilisées ensemble dans la même application.

Guide de migration

  1. Mettez à jour votre fichier .csproj avec le nouveau nom du paquet ClickHouse.Driver et la dernière version disponible sur NuGet.
  2. Remplacez dans votre code toutes les références à ClickHouse.Client par ClickHouse.Driver.

Versions de .NET prises en charge

ClickHouse.Driver prend en charge les versions de .NET suivantes :
  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

Versions de ClickHouse prises en charge

Le client prend officiellement en charge les 3 dernières versions, ainsi que les deux dernières versions LTS.

Installation

Installez le paquet à partir de NuGet :
Ou avec le gestionnaire de packages NuGet :

Démarrage rapide

Configuration

Il existe deux façons de configurer votre connexion à ClickHouse :
  • Chaîne de connexion : paires clé-valeur séparées par des points-virgules, qui indiquent l’hôte, les informations d’authentification et d’autres options de connexion.
  • Objet ClickHouseClientSettings : objet de configuration fortement typé, qui peut être chargé depuis des fichiers de configuration ou défini dans le code.
Vous trouverez ci-dessous la liste complète de tous les paramètres, de leurs valeurs par défaut et de leurs effets.

Paramètres de connexion

Format des données et sérialisation

Gestion des sessions

L’option UseSession active la persistance de la session du serveur, ce qui permet d’utiliser des instructions SET et des tables temporaires. Les sessions sont réinitialisées après 60 secondes d’inactivité (délai d’expiration par défaut). La durée de vie de la session peut être prolongée en définissant des paramètres de session via des instructions ClickHouse ou la configuration du serveur.La classe ClickHouseConnection permet normalement un fonctionnement en parallèle (plusieurs threads peuvent exécuter des requêtes simultanément). Toutefois, l’activation de l’option UseSession limite cela à une seule requête active par connexion à un instant donné (il s’agit d’une limitation côté serveur).

Sécurité

Configuration du client HTTP

Journalisation et débogage

Paramètres personnalisés et rôles

Lorsque vous utilisez une chaîne de connexion pour définir des paramètres personnalisés, utilisez le préfixe set_, par ex. : “set_max_threads=4”. Si vous utilisez un objet ClickHouseClientSettings, n’utilisez pas le préfixe set_.Pour obtenir la liste complète des paramètres disponibles, consultez cette page.

Exemples de chaînes de connexion

Connexion de base

Avec des paramètres ClickHouse personnalisés


QueryOptions

QueryOptions vous permet de surcharger les paramètres définis au niveau du client pour chaque requête. Toutes les propriétés sont facultatives et ne remplacent les valeurs par défaut du client que lorsqu’elles sont spécifiées. Exemple :

InsertOptions

InsertOptions étend QueryOptions avec des paramètres spécifiques aux opérations d’insertion en masse via InsertBinaryAsync. Toutes les propriétés de QueryOptions sont également disponibles dans InsertOptions. Exemple :

Ignorer la requête de sondage du schéma

Par défaut, InsertBinaryAsync envoie une requête SELECT ... WHERE 1=0 avant chaque insertion afin d’identifier les types de colonnes. Pour les scénarios à haut débit, vous pouvez supprimer cette surcharge de deux façons : Option 1 : fournir explicitement les types de colonnes Lorsque vous connaissez le schéma de la table à la compilation, transmettez-le directement via ColumnTypes. Aucune requête de schéma n’est alors envoyée :
Option 2 : Mettre en cache le schéma Lorsque vous effectuez plusieurs insertions dans la même table, définissez UseSchemaCache = true pour n’interroger le schéma qu’une seule fois et le réutiliser lors des insertions suivantes sur la même instance ClickHouseClient :
  • ColumnTypes est prioritaire sur UseSchemaCache. Si les deux sont définis, les types explicites sont utilisés.
  • Le cache de schéma ne détecte pas les modifications apportées via ALTER TABLE. Si vous modifiez le schéma de la table, créez un nouveau ClickHouseClient ou évitez UseSchemaCache pour cette table.
  • Le cache est propre à l’instance ClickHouseClient et indexé par (database, table). Différents sous-ensembles de colonnes d’une même table partagent un schéma mis en cache unique.

ClickHouseClient

ClickHouseClient est l’API recommandée pour interagir avec ClickHouse. Il est thread-safe, conçu pour être utilisé comme singleton et gère en interne le pool de connexions HTTP.

Créer un client

Créez un ClickHouseClient à l’aide d’une chaîne de connexion ou d’un objet ClickHouseClientSettings. Consultez la section Configuration pour connaître les options disponibles. Les informations de votre service ClickHouse Cloud sont disponibles dans la ClickHouse Cloud console. Sélectionnez un service, puis cliquez sur Connect : Choisissez C#. Les détails de connexion s’affichent ci-dessous. Si vous utilisez ClickHouse autogéré, les détails de connexion sont définis par votre administrateur ClickHouse. Avec une chaîne de connexion :
Ou avec ClickHouseClientSettings :
Pour les cas d’injection de dépendances, utilisez IHttpClientFactory :
ClickHouseClient est conçu pour être conservé sur la durée et partagé dans toute votre application. Créez-le une seule fois (généralement sous forme de singleton) et réutilisez-le pour toutes les opérations sur la base de données. Le client gère en interne le pool de connexions HTTP.

Exécution des requêtes

Utilisez ExecuteNonQueryAsync pour les instructions qui ne renvoient pas de résultats :
Utilisez ExecuteScalarAsync pour récupérer une seule valeur :

Insérer des données

Insertions paramétrées

Insérez des données à l’aide de requêtes paramétrées avec ExecuteNonQueryAsync. Les types des paramètres doivent être spécifiés dans le SQL à l’aide de la syntaxe {name:Type} :

Insertions en masse

Utilisez InsertBinaryAsync pour insérer efficacement un grand nombre de lignes. Il transmet les données en flux à l’aide du format binaire natif de lignes de ClickHouse, prend en charge les envois parallèles par lots et évite les erreurs “URL trop longue” qui peuvent survenir avec les requêtes paramétrées.
Pour les jeux de données volumineux, configurez l’envoi par lots et le parallélisme avec InsertOptions :
  • Le client récupère automatiquement la structure de la table via SELECT * FROM <table> WHERE 1=0 avant d’insérer les données. Les valeurs fournies doivent correspondre aux types des colonnes cibles. Pour ignorer cette requête, utilisez InsertOptions.ColumnTypes ou InsertOptions.UseSchemaCache.
  • Lorsque MaxDegreeOfParallelism > 1, les batches sont envoyés en parallèle. Les sessions ne sont pas compatibles avec l’insertion en parallèle ; désactivez-les ou définissez MaxDegreeOfParallelism = 1.
  • Utilisez RowBinaryFormat.RowBinaryWithDefaults dans InsertOptions.Format si vous souhaitez que le serveur applique les valeurs DEFAULT aux colonnes non fournies.

Insertion de POCO

Au lieu de construire des tableaux object[], vous pouvez insérer directement des objets POCO fortement typés. Enregistrez le type une seule fois, puis passez IEnumerable<T> :
Par défaut, toutes les propriétés publiques accessibles en lecture sont associées à des colonnes selon une correspondance stricte des noms, sensible à la casse. Vous pouvez personnaliser ce mappage à l’aide d’attributs :
Lorsque toutes les propriétés mappées spécifient un Type explicite, la requête de sondage du schéma est entièrement omise. Lorsque seules certaines propriétés ont des types explicites, le pilote revient à la requête de sondage du schéma pour l’ensemble des colonnes. InsertBinaryAsync<T> prend en charge les mêmes InsertOptions (batching, parallélisme, mise en cache du schéma) que la surcharge object[].
Contrairement à la surcharge object[], InsertBinaryAsync<T> n’accepte pas de liste explicite de colonnes. Les colonnes sont déterminées par les propriétés mappées du type enregistré. Pour contrôler les colonnes insérées, utilisez [ClickHouseNotMapped] pour exclure des propriétés ou [ClickHouseColumn(Name = "...")] pour les renommer.Si ColumnTypes est défini dans InsertOptions, elles remplaceront les attributs POCO.

Évolution du schéma

Les insertions de POCO fonctionnent de façon transparente lorsque des colonnes sont ajoutées à la table cible après l’enregistrement du type. Comme le pilote n’insère que les colonnes associées au POCO, toute nouvelle colonne avec DEFAULT (ou d’autres expressions par défaut) est automatiquement remplie par le serveur. Aucune modification du code ni aucun nouvel enregistrement ne sont nécessaires.

Placement de la requête d’insertion

Une insertion binaire écrit son instruction INSERT INTO ... FORMAT ... sur la première ligne du corps de la requête, avant les lignes de données. Le corps étant compressé par défaut, les mécanismes de routage et de journalisation qui n’inspectent que l’URL ne voient pas cette instruction. Définissez InsertOptions.QueryPlacement sur InsertQueryPlacement.Url pour envoyer plutôt l’instruction dans le paramètre d’URL query, le corps ne contenant alors que les lignes de données :
Utilisez-la lorsqu’un proxy, un load balancer ou une gateway route ou inspecte le paramètre query, ou lorsque vous souhaitez que l’instruction apparaisse dans les logs d’accès et les outils d’observability. Cette option est facultative, car l’instruction est alors comptabilisée dans la longueur de l’URL. La limite effective est la plus basse de celles imposées par le runtime .NET, par un intermédiaire et par le server. De .NET 6 à .NET 9, System.Uri limite l’URI de requête complet et encodé à 65 519 caractères ; le pilote lève une InvalidOperationException qui vous renvoie vers InsertQueryPlacement.Body lorsque cette limite est dépassée. Le paramètre http_max_uri_size de ClickHouse vaut 1 Mio par défaut, tandis qu’un intermédiaire peut imposer une limite plus basse. En mode corps, l’instruction et les rows ne sont soumis à aucune limite de longueur d’URL ; d’autres options de requête peuvent toujours figurer dans l’URL. Ce paramètre est indépendant de Compressor : le corps est encodé de la même manière dans les deux modes.

Lecture des données

Utilisez ExecuteReaderAsync pour exécuter des requêtes SELECT. Le ClickHouseDataReader renvoyé fournit un accès typé aux colonnes de résultat via des méthodes comme GetInt64(), GetString() et GetFieldValue<T>(). Appelez Read() pour passer à la ligne suivante. Cette méthode renvoie false lorsqu’il n’y a plus de lignes. Accédez aux colonnes par index (à partir de 0) ou par nom de colonne.

Lecture de POCO

Au lieu de lire les colonnes par index ou par nom, vous pouvez acheminer directement le résultat de la requête vers vos propres classes. Enregistrez le type une fois auprès du client, puis utilisez QueryAsync<T> :
RegisterPocoType<T>() configure à la fois les mappages d’insert et de lecture, et valide les deux en amont. RegisterBinaryInsertType<T>() reste inchangé et demeure réservé à l’insert pour des raisons de compatibilité descendante. Un type enregistré doit avoir :
  • Un constructeur public sans paramètre.
  • Au moins une propriété publique avec un setter public non-init. Les propriétés required sont prises en charge.
La correspondance des colonnes est sensible à la casse. Les colonnes de résultat manquantes laissent les propriétés à leur valeur par défaut ; les colonnes de résultat supplémentaires sont ignorées. Le pilote n’élargit ni ne restreint les valeurs. En dehors des représentations alternatives listées ci-dessous, le type framework de la colonne doit être assignable au type de la propriété, et une incompatibilité lève InvalidOperationException. Une propriété object accepte donc n’importe quelle colonne. QueryAsync<T> lit chacune de ces colonnes directement dans une propriété correspondante : Chaque ligne accepte également la forme nullable de son type de propriété (long?, DateOnly?, etc.), que la colonne soit Nullable(...) ou non. Une propriété de type valeur non nullable sur une colonne Nullable(T) est acceptée à l’enregistrement, mais lève une exception à l’arrivée d’un NULL. Les wrappers tels que LowCardinality(T), SimpleAggregateFunction(f, T) et Object(T) se mappent exactement comme T. Les colonnes composites sont également prises en charge et adoptent le type du framework indiqué dans la référence des types en lecture : Array(T) vers T[], Tuple(...) vers System.Tuple<...>, Nested(...) vers Tuple<...>[], JSON vers JsonObject (ou string avec JsonReadMode=String), et Variant/Dynamic vers object. Une colonne Map(K, V) constitue un cas particulier : une propriété List<KeyValuePair<K, V>> ou KeyValuePair<K, V>[] est lue sans boxing et conserve l’ordre de transmission ainsi que les éventuelles clés répétées, quel que soit le MapReadMode. Une propriété Dictionary<K, V> ne fonctionne qu’en mode par défaut. Les types de clé et de valeur doivent correspondre exactement : ainsi, Map(String, Nullable(Int32)) requiert KeyValuePair<string, int?>. Lorsqu’une colonne propose plusieurs types de propriété (une colonne DateTime en tant que DateTime, DateTimeOffset ou DateOnly, une colonne String en tant que string ou byte[]), le type de propriété déclaré détermine la représentation. Ces représentations alternatives appartiennent au chemin POCO : QueryAsync<T> en dispose, mais pas MapTo<T>. Lorsque vous parcourez manuellement un lecteur, utilisez ClickHouseDataReader.MapTo<T>() pour matérialiser la ligne en cours dans un POCO enregistré sans faire avancer le lecteur :
Utilisez MapTo<T> lorsque vous devez piloter vous-même la boucle du lecteur — par exemple pour combiner un accès brut aux colonnes avec la matérialisation en POCO. Cette méthode lit la ligne via les valeurs boxées du lecteur : elle n’offre donc pas les types de propriétés alternatifs présentés plus haut, et elle alloue davantage que QueryAsync<T>. Préférez QueryAsync<T> lorsque vous n’avez besoin que des lignes ; voir choisir le chemin de matérialisation pour les chiffres. Un convertisseur de valeurs en lecture défini au niveau du client ou par requête s’applique aux deux chemins et ne désactive pas la lecture sans boxing. Le pilote convertit chaque colonne via la surcharge correspondant à la manière dont il l’a lue : le ConvertValue<T> typé pour une colonne sans boxing, et le ConvertValue boxé pour une colonne composite. Implémentez ces deux surcharges de façon cohérente, sans quoi une même colonne donnera des résultats différents selon le chemin emprunté. Si un LoggerFactory est configuré, RegisterPocoType<T>() et RegisterBinaryInsertType<T>() génèrent un journal de niveau Débogage (catégorie ClickHouse.Driver.Client) indiquant quelles propriétés ont été mappées à quelles colonnes, ainsi que celles qui ont été ignorées et pour quelle raison. Voir Journalisation et diagnostics.

Paramètres SQL

Dans ClickHouse, le format standard des paramètres dans les requêtes SQL est {parameter_name:DataType}. Exemples :
Les paramètres SQL ‘bind’ sont transmis sous forme de paramètres de requête dans l’URI HTTP. En utiliser un trop grand nombre peut donc entraîner une exception “URL too long”. Utilisez InsertBinaryAsync pour l’insertion en masse de données afin d’éviter cette limitation.

Placeholders @name de style ADO

Le driver accepte également les placeholders @name, tels qu’en émettent les ORM comme Dapper. Il s’agit d’une commodité côté client : avant l’envoi de la requête, chacun d’eux est réécrit en {name:ResolvedType}, si bien que le serveur ne voit jamais de @. Consultez la résolution de type pour savoir comment le type est choisi. Privilégiez la forme explicite {name:Type} lorsque c’est possible. Un @name sans paramètre correspondant est laissé tel quel, à charge pour le serveur de le rejeter. La correspondance est case-sensitive : @ID ne lie donc pas un paramètre nommé id.
Pour désactiver la rewrite, activez le commutateur AppContext ClickHouse.Driver.DisableReplacingParameters avant la première utilisation du driver. Seule la réécriture du texte est désactivée ; les paramètres sont toujours envoyés, de sorte que les requêtes écrites avec la syntaxe native {name:Type} continuent de fonctionner.

Paramètres Identifier

Le type de paramètre Identifier vous permet de lier en toute sécurité un nom de base de données, de table ou de colonne, au lieu d’un littéral de chaîne entre guillemets. Utilisez-le avec la syntaxe {name:Identifier} en SQL, ou en définissant ClickHouseDbParameter.ClickHouseType = "Identifier" :
La valeur est envoyée telle quelle, et le serveur la substitue comme un identifiant SQL nu, en appliquant lui-même la mise entre accents graves et l’échappement. Les identifiants contenant des caractères spéciaux (y compris des accents graves) sont ainsi préservés sans risque à l’aller-retour.

ID de requête

Chaque requête se voit attribuer un query_id unique, qui peut être utilisé pour extraire des données de la table system.query_log ou annuler des requêtes de longue durée. Vous pouvez spécifier un ID de requête personnalisé via QueryOptions:
Si vous définissez un QueryId personnalisé, assurez-vous qu’il soit unique à chaque appel. Un GUID aléatoire est un bon choix.

Correspondance personnalisée des types de paramètres

Lorsque vous utilisez des paramètres de style @ (par exemple, WHERE id = @id), le driver déduit automatiquement le type ClickHouse à partir du type de valeur .NET. Par exemple, int correspond à Int32.
Comportement des paramètres DateTime déduitsPour les paramètres de style @ sans indication {name:Type} dans le SQL et sans ClickHouseType défini, les valeurs représentant un instant sont déduites comme DateTime('UTC') plutôt que comme un simple DateTime. Les valeurs DateTime dont Kind vaut Utc ou Local, ainsi que toutes les valeurs DateTimeOffset, sont envoyées comme DateTime('UTC'), ce qui préserve l’instant quel que soit le fuseau horaire du serveur.Les indications explicites ({name:DateTime}) prévalent sur l’inférence et constituent la manière recommandée de construire les requêtes.
Pour remplacer ces valeurs par défaut, définissez ParameterTypeResolver dans ClickHouseClientSettings. C’est utile si vous voulez que tous les paramètres DateTime utilisent DateTime64(3) pour une précision à la milliseconde, ou que toutes les valeurs décimales utilisent une échelle spécifique, sans avoir à définir ClickHouseType sur chaque paramètre individuellement. Utilisation de DictionaryParameterTypeResolver pour des correspondances de types simples :
IParameterTypeResolver personnalisé pour les cas d’usage avancés : Pour une résolution basée sur le nom ou tenant compte de la valeur, implémentez directement l’interface IParameterTypeResolver. Renvoyez null pour utiliser l’inférence par défaut :
Vous pouvez également définir un résolveur pour une seule requête via QueryOptions.ParameterTypeResolver. Lorsqu’il est défini, il prévaut sur le résolveur défini au niveau du client. Ordre de priorité pour la résolution des types : Le résolveur s’inscrit lui aussi dans une chaîne de priorité. De la priorité la plus élevée à la plus faible :
  1. ClickHouseType explicite défini sur le paramètre
  2. annotation de type SQL issue de la syntaxe {name:Type} dans la requête
  3. IParameterTypeResolver (via QueryOptions.ParameterTypeResolver, avec repli sur ClickHouseClientSettings.ParameterTypeResolver)
  4. Inférence de type intégrée (TypeConverter.ToClickHouseType)
Le résolveur fonctionne également avec le chemin ClickHouseConnection d’ADO.NET : les paramètres de configuration sont hérités par les connexions créées à partir du client.

Formatage personnalisé des valeurs de paramètre

IParameterFormatter est un hook qui détermine comment les valeurs de paramètre sont sérialisées. Utilisez-le lorsque le formatage intégré (par ex. la précision de DateTime, la gestion des décimaux selon les paramètres régionaux, l’échappement des chaînes et la représentation des nombres) ne correspond pas à ce qu’attendent votre schéma ou vos outils en aval. Définissez ParameterFormatter dans ClickHouseClientSettings pour installer un formateur pour toutes les requêtes paramétrées. Le formateur reçoit la valeur, le nom de type ClickHouse résolu et le nom du paramètre, puis renvoie la représentation sous forme de chaîne envoyée au serveur. Renvoyez null pour utiliser le formateur par défaut. Utilisation de DictionaryParameterFormatter pour un formatage simple par type CLR :
IParameterFormatter personnalisé pour les cas d’usage avancés :
Vous pouvez également définir un formateur par requête via QueryOptions.ParameterFormatter. Lorsqu’il est défini, il prévaut sur le formateur défini au niveau du client. Valeurs composites : Le formateur s’applique à la fois aux paramètres de collection de premier niveau et à chaque élément à l’intérieur des valeurs composites (Array, Tuple, Map, Nullable, LowCardinality, Variant). Par exemple, un mappage typeof(int) formate individuellement chaque élément Int32 d’un Array(Int32). Encadrement par apostrophes simples dans les contextes composites : Pour les types ClickHouse de type chaîne (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) intégrés dans un littéral composite, le driver entoure la sortie du formateur d’apostrophes simples, mais n’en échappe pas le contenu. Si la chaîne renvoyée contient une apostrophe simple ou un antislash non échappé, le littéral composite sera mal formé et le server rejettera la query. Les paramètres de chaîne de premier niveau (non intégrés dans un composite) sont utilisés tels quels, sans encadrement ; aucun échappement n’est donc nécessaire dans ce cas. Priorité du formateur :
  1. IParameterFormatter (issu de QueryOptions.ParameterFormatter, ou à défaut de ClickHouseClientSettings.ParameterFormatter). S’il renvoie une valeur non nulle, c’est cette valeur qui est utilisée.
  2. Le formatage intégré, spécifique au type, dans HttpParameterFormatter.
Le formateur n’est pas consulté pour les valeurs null ou DBNull, qui sont toujours sérialisées comme la valeur sentinelle null de ClickHouse (\N).

Conversion personnalisée des valeurs lues

IReadValueConverter vous permet de transformer les valeurs renvoyées par le lecteur de données après la désérialisation, sans modifier leur type CLR. Cas d’usage typiques : définir DateTime.Kind = Utc sur une colonne DateTime sans fuseau horaire, tronquer ou normaliser des chaînes, ou post-traiter une colonne JSON avant qu’elle n’arrive au code d’application. Définissez ReadValueConverter dans ClickHouseClientSettings pour installer un convertisseur pour toutes les opérations de lecture. Le convertisseur est appelé une fois par colonne et par ligne, à la fois via le chemin encapsulé (GetValue) et le chemin générique (GetFieldValue<T>). Lorsqu’aucun convertisseur n’est défini, il n’y a aucun overhead : le lecteur renvoie directement les valeurs. Utilisation de DictionaryReadValueConverter pour une conversion simple par type CLR :
Les valeurs dont le type CLR à l’exécution n’est pas enregistré avec For<T> sont transmises inchangées. La sélection s’effectue sur le type CLR exact ; enregistrez donc le type réel renvoyé par le lecteur (par ex. For<JsonObject> pour une colonne JSON en JsonReadMode.Binary). IReadValueConverter personnalisé pour les scénarios avancés : Si vous devez effectuer la sélection en fonction de la chaîne de type côté ClickHouse (par exemple, pour distinguer DateTime de DateTime('UTC') — tous deux apparaissent comme le même type CLR), implémentez directement IReadValueConverter :
Le convertisseur doit préserver le type CLR à l’exécution ; les métadonnées de colonne (GetFieldType, GetSchemaTable) ne transitent pas par lui et doivent rester cohérentes avec ce qui est renvoyé. Vous pouvez également définir un convertisseur par requête via QueryOptions.ReadValueConverter ; lorsqu’il est défini, il prévaut sur le convertisseur au niveau du client. Limite de dispatch : Le convertisseur est invoqué une fois par colonne avec la valeur complète de la cellule désérialisée ; il ne parcourt pas récursivement les conteneurs composés. Pour une colonne Array(Int32), la valeur transmise est un int[] ; pour Tuple(Int32, String), c’est un ITuple. Quelle surcharge est exécutée : Les deux surcharges doivent être cohérentes, car celle que le driver appelle dépend de la façon dont l’appelant a lu la colonne :
  • ConvertValue<T> — les accesseurs typés GetByte, GetSByte, GetInt16/32/64, GetUInt16/32/64, GetFloat, GetDouble, GetGuid, GetDateTime, GetIPAddress, GetBigInteger et GetFieldValue<T>, ainsi que chaque colonne sans encapsulation sur le chemin de lecture POCO.
  • ConvertValue (encapsulé) — GetValue, GetValues, les indexeurs, GetChar, GetTuple, et les chemins de coercition dans GetBoolean, GetDecimal et GetString.
IsDBNull n’exécute aucun convertisseur : il lit directement le drapeau null, de sorte qu’un convertisseur ne peut jamais modifier le fait qu’une valeur soit considérée comme null. TryGetEnumOrdinal le contourne également — voir lecture de l’ordinal d’un enum. Le convertisseur fonctionne avec le chemin ADO.NET ClickHouseConnection — les paramètres sont hérités par les connexions créées à partir du client.

Flux brut

Utilisez ExecuteRawResultAsync pour transmettre directement le résultat de la requête dans un format spécifique, sans passer par le lecteur de données. Cela est utile pour exporter des données vers des fichiers ou les acheminer vers d’autres systèmes :
Formats courants : JSONEachRow, CSV, TSV, Parquet, Native. Consultez la documentation des formats pour voir toutes les options.

Compression du transport par requête

Par défaut, le client négocie zstd, lz4, gzip, deflate lorsque Compression=true (valeur par défaut de la chaîne de connexion) et décode lui-même le flux, de manière transparente. Pour les exports bruts (par ex. Parquet, Arrow, Native), vous pouvez souhaiter négocier un codec différent (par ex. zstd ou lz4) afin de troquer du CPU contre du débit sans modifier le paramètre défini pour l’ensemble de la connexion. QueryOptions.AcceptEncoding et ClickHouseCommand.AcceptEncoding définissent l’en-tête HTTP Accept-Encoding pour une seule requête, en remplaçant toute valeur par défaut appliquée jusque-là, et forcent enable_http_compression=1 dans l’URL (condition exigée par ClickHouse pour prendre en compte Accept-Encoding).

Configuration de HttpClient

Rien à configurer : le HttpClient construit par le driver laisse AutomaticDecompression à DecompressionMethods.None et décode lui-même les réponses ; ainsi, Content-Encoding n’est jamais retiré à votre insu et le corps brut vous parvient exactement tel que le server l’a envoyé.
Si vous fournissez votre propre HttpClient, laissez également AutomaticDecompression désactivé. Ce n’est pas uniquement un paramètre côté réponse : au moment de l’envoi, le handler ajoute à l’en-tête Accept-Encoding sortant tous les algorithmes de son masque qui y sont absents. Un handler configuré avec GZip | Deflate transforme donc un AcceptEncoding = "lz4" explicite en lz4, gzip, deflate, et un "identity" explicite en identity, gzip, deflate dans le format binaire — et comme ClickHouse résout l’en-tête selon son propre ordre de préférence de codecs (en ignorant l’ordre et les valeurs q), il peut répondre avec un codec que vous n’avez jamais demandé, que le handler décode puis retire, si bien que vous ne pouvez même pas vous en apercevoir. En laissant le masque désactivé, l’offre reste exactement celle que vous avez choisie.
Si AcceptEncoding demande un codec que le driver ne sait pas décoder (snappy), seul ExecuteRawResultAsync est sûr. ExecuteReaderAsync, ExecuteScalarAsync et ExecuteNonQueryAsync échouent avec une NotSupportedException indiquant le codec en cause (auparavant, ils interprétaient les compressed bytes comme s’il s’agissait du format de résultat et produisaient des données inexploitables).

Corps des erreurs

Lorsque le serveur renvoie une réponse 4xx/5xx et que enable_http_compression=1 est défini, il compresse le corps de l’erreur avec le même codec que celui qu’il aurait utilisé pour une réponse réussie. Le driver décode ces corps pour tous les codecs qu’il prend en charge (lz4, zstd, gzip, deflate, br/brotli), afin que le message affiché dans ClickHouseServerException soit lisible. Pour tout autre codec (snappy, …), il renvoie un message de substitution qui indique le codec et pointe vers system.query_log pour le texte d’erreur d’origine.

Décompression des réponses

Accept-Encoding demande uniquement au serveur de compresser la réponse — encore faut-il que quelque chose la décode. Le driver s’en charge lui-même, à partir du Content-Encoding de la réponse, si bien que toutes les API de lecture ordinaires (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) fonctionnent sur une réponse compressée sans aucune configuration. Il décode lz4, zstd, gzip, deflate et br ; snappy n’est pas pris en charge. Par défaut, le driver annonce zstd, lz4, gzip, deflate, et ClickHouse répond en zstd. Pour un autre choix, définissez vous-même Accept-Encoding — pour l’ensemble du client :
par requête, qui a la préséance :
ou dans la connection string, pour les utilisateurs d’ORM qui ne manipulent jamais ClickHouseClientSettings :
Le définir force également enable_http_compression=1 dans l’URL, ce que ClickHouse exige avant même de prendre l’en-tête en compte — y compris lorsque UseCompression vaut false, puisque nommer explicitement un codec revient à en demander un. Sans valeur définie, UseCompression=false n’envoie aucun Accept-Encoding. Accept-Encoding peut être défini à quatre endroits. Le premier d’entre eux qui nomme un codec l’emporte :
  1. QueryOptions.AcceptEncoding (ou ClickHouseCommand.AcceptEncoding)
  2. CustomHeaders["Accept-Encoding"] sur la requête
  3. CustomHeaders["Accept-Encoding"] sur le client
  4. ClickHouseClientSettings.AcceptEncoding, ou le keyword de chaîne de connection AcceptEncoding
Si aucun ne le fait, le driver envoie sa liste par défaut. Une valeur qui ne nomme aucun codec (null, vide, espaces, ou uniquement des virgules) est considérée comme non définie et laisse la main à l’endroit suivant. Pour désactiver la compression, utilisez identity. C’est le server, et non le client, qui choisit le codec. ClickHouse parcourt Accept-Encoding à la recherche de tokens selon son propre ordre de préférence figé — zstd > br > lz4 > snappy > gzip > deflate — et ignore aussi bien l’ordre dans lequel vous les listez que les q-values. L’en-tête constitue donc une announcement de capability plutôt qu’une exigence, et le seul moyen d’orienter le choix consiste à omettre certains tokens. La liste par défaut inclut zstd : une requête par défaut reçoit donc une réponse en zstd, les tokens restants servant de fallback. br est décodable, mais n’est pas annoncé par défaut. La comparaison des codecs en matière de taille de payload, de CPU côté server et de CPU côté client dépend de vos données, de votre lien réseau et de la valeur de http_zlib_compression_level du server (valeur par défaut livrée : 3) — voir Réglage de la compression.
  • http_zlib_compression_level. Ce SETTING s’applique à tous les codecs HTTP, et sa default value est 3. Cette valeur doit être ajustée en fonction de vos données, de la vitesse du lien réseau et de l’utilisation CPU.
  • Un client CPU-bound sur un lien rapide. Le driver décode le response corps sur le thread appelant : lorsque le réseau n’est pas le goulot d’étranglement, la vitesse de décodage client-side peut devenir le facteur limitant.
Demandez un codec différent par requête, ou pour l’ensemble du client, dès que l’un de ces cas s’applique :
Comme la décision est prise à partir de la réponse, un corps est décodé dès lors que son Content-Encoding l’indique, quelle que soit la requête initiale : en l’absence d’en-tête ou avec identity, il passe tel quel ; un codec pris en charge est décodé ; toute autre valeur lève une erreur la mentionnant. Il n’y a aucun risque de double décodage — si l’AutomaticDecompression d’un handler fourni par l’appelant a déjà décodé un corps, il supprime également Content-Encoding, si bien que le driver voit du plaintext et n’y touche pas. Les résultats bruts n’annoncent aucun codec. ExecuteRawResultAsync (ainsi que les méthodes publiques PostStreamAsync / InsertRawStreamAsync) vous transmettent leur corps tel quel : à moins que vous ne spécifiiez vous-même un codec, elles n’en demandent aucun — rien dans le driver ne décode un tel corps, si bien que proposer un codec à cet endroit transformerait silencieusement un export en fichier compressé. La règle est donc simple, et indépendante de la configuration du HttpClient : un corps transmis tel quel arrive exactement comme le server l’a envoyé, et le server envoie du plaintext sauf si vous avez demandé un codec. En demander un (au niveau du client ou par requête) est le moyen d’exporter délibérément des compressed bytes. Un AcceptEncoding explicite (à l’un ou l’autre niveau) s’applique malgré tout aux requêtes brutes, et ClickHouseRawResult.ReadDecompressedStreamAsync() décode le résultat lorsque vous le souhaitez ; ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync et CopyToAsync renvoient toujours les octets exactement tels qu’ils sont arrivés.
Lisez le flux retourné jusqu’à la fin avant qu’il ne sorte de sa portée, comme ci-dessus. Lorsque la réponse est compressée, vous obtenez un decoder créé avec leaveOpen : sa libération laisse donc la réponse intacte ; lorsqu’elle n’est pas compressée, vous obtenez le flux de contenu HTTP lui-même, et sa libération met fin au corps. Dans les deux cas, c’est le ClickHouseRawResult qui est propriétaire de la réponse — n’appelez pas ses autres membres de lecture une fois le flux libéré. Libérer le ClickHouseRawResult est toujours nécessaire et suffit à lui seul : cela libère à la fois la réponse et tout decoder inséré ici (les decoders retiennent des buffers issus du pool). Le await using ci-dessus est donc facultatif, et peut être conservé sans risque. Des appels séquentiels répétés renvoient le même flux ; le type ne peut pas être utilisé de manière concurrente. Consultez Select_007_ResponseCompression.cs pour un exemple exécutable.

Compression des insertions (requêtes)

Zstd est le codec par défaut pour les insertions : InsertOptions.Compressor vaut initialement ZstdCompressor.Default, c’est-à-dire zstd au niveau 3. Affectez-lui un autre compresseur pour changer de codec, ou null pour envoyer le corps non compressé.
Quatre codecs sont fournis avec le driver. Chacun dispose d’une instance Default ainsi que d’un constructeur qui prend en paramètres un niveau et la taille du write buffer :
Partagez les instances de compresseur. Chaque Default est une instance partagée unique, et les quatre compresseurs peuvent être utilisés sans risque depuis plusieurs threads à la fois — ce qui est le cas lorsque InsertOptions.MaxDegreeOfParallelism est supérieur à 1, puisqu’un insert utilise un compresseur par batch. Aucun d’eux n’implémente IDisposable. Créez votre propre instance une seule fois et réutilisez-la, de la même manière que Default est utilisé.
IClickHouseCompressor est public, et une implémentation ne doit fournir que deux membres :
Le serveur doit accepter le Content-Encoding que vous indiquez. Les autres membres — Decompress, MethodByte, MaxEncodedLength, Encode et Decode — disposent d’implémentations par défaut qui lèvent NotSupportedException : ne surchargez donc que celles dont votre codec a besoin. Implémentez Decompress pour décoder les corps de réponse en plus de compresser les requêtes, et levez InvalidDataException depuis le flux qu’il renvoie lorsqu’un corps est corrompu ou dans un format incorrect. InsertOptions.Compressor ne régit que les insertions binaires. Les autres corps de requête du driver sont compressés selon d’autres règles, et aucun d’eux n’y fait appel :
  • Toute requête en texte SQL (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, ExecuteRawResultAsync, la couche ADO.NET) envoie son statement avec Content-Encoding: gzip dès lors que UseCompression vaut true — c’est-à-dire par défaut. Le codec n’est pas configurable : AcceptEncoding ne pilote que la réponse, le choix se limite donc à gzip ou rien. Avec Compression=false, le statement part en clair. Les statements étant de petite taille, il est rarement utile de s’en préoccuper — mais mieux vaut le savoir lorsque vous observez les requêtes dans un proxy ou une capture de packets.
  • Un corps multipart — une requête dont les parameters sont envoyés sous forme de form data (UseFormDataParameters=true) — est toujours envoyé uncompressed, quelle que soit la valeur de UseCompression.
  • Un téléversement raw (InsertRawStreamAsync, PostStreamAsync) s’appuie sur son propre flag, propre à chaque appel, et ne consulte ni UseCompression ni InsertOptions.Compressor : gzip lorsque le flag est positionné, uncompressed sinon. Notez que le parameter useCompression de InsertRawStreamAsync vaut true par défaut : un téléversement raw est donc gzippé sauf si vous passez false — même avec Compression=false sur le client.

Ajuster la compression

La compression troque du CPU contre des octets. Le gain réel dépend presque entièrement de la rapidité de votre lien réseau par rapport à la vitesse d’exécution du codec. Aucun réglage ne convient à tout le monde.

Le chiffre décisif

La compression en vaut la peine tant que le codec est plus rapide que le réseau. Ce seuil est plus bas qu’on ne l’imagine généralement sur le chemin de lecture, car ClickHouse compresse les réponses HTTP en mono-thread dans le buffer de sortie. Mesuré sur un service ClickHouse Cloud à 16 vCPU (hits, RowBinary, niveau 3), le serveur produit une sortie compressée à environ 100-200 Mo/s. Ainsi, pour un résultat volumineux, et en supposant qu’une seule requête soit traitée à la fois, la compression cesse d’être rentable aux alentours de 100 Mo/s. Un flux HTTPS unique au sein d’une même région cloud dépasse couramment ce débit, tandis que tout trafic traversant l’Internet public, un VPN ou une frontière de région se situe généralement en dessous. Le chemin d’insertion tolère la compression jusqu’à des débits de liaison plus élevés, car votre client compresse sur un cœur qui lui est propre et se révèle généralement plus rapide que la compression des réponses côté serveur.

Guide approximatif par déploiement

Trois éléments que ce tableau ne prend pas en compte :
  • Le coût de l’egress : si le transfert de données vous est facturé, les octets ont un prix qui dépasse la seule question de la latence, ce qui incite à compresser davantage, quelle que soit la vitesse du lien.
  • Les petits résultats : tout ce qui précède concerne des payloads volumineux. Pour de petites réponses, le codec n’a quasiment aucune importance et c’est l’overhead par requête qui domine.
  • Les insertions parallèles relèvent les seuils d’insertion. Chaque valeur de débit ci-dessus vaut pour un seul thread. InsertOptions.MaxDegreeOfParallelism vaut 1 par défaut, mais l’augmenter permet de compresser les batches en parallèle : le taux d’encodage agrégé du client évolue donc à peu près proportionnellement au nombre de cœurs que vous lui allouez. Ainsi, sur un lien rapide, il reste intéressant de compresser une insertion parallèle bien au-delà de la vitesse à laquelle cela cesse d’être rentable pour une insertion mono-thread. Considérez les lignes d’insertion du tableau comme un plancher et, si vous regroupez déjà en batches parallèles, refaites vos tests avant de conclure que votre lien est trop rapide pour la compression.
Le chemin de lecture ne se parallélise qu’entre plusieurs requêtes.

Choisir un codec

Levels

La compression des réponses est régie par un unique server setting, http_zlib_compression_level, qui s’applique à tous les codecs HTTP, et pas seulement à zlib. Sa valeur par défaut est 3. N’y touchez pas sans avoir mesuré une raison de le faire. Au-dessus de la valeur par défaut, le gain de taille est minime au regard du coût CPU (pour zstd, passer de 3 à 6 double environ la charge CPU du server pour ~14 % d’octets en moins), et br devient pathologique. En dessous, au niveau 1, la donne change vraiment : lz4 devient bien moins coûteux et zstd perd son avantage CPU sur lui. Définissez-le par requête si nécessaire :

Mesurer votre propre point de bascule

Le moyen le plus rapide d’optimiser votre choix de codec et de niveau de compression consiste à chronométrer la même requête avec plusieurs codecs, puis à comparer les résultats.
Pour obtenir la vision côté serveur du même phénomène, relisez ProfileEvents dans system.query_log — définissez QueryOptions.QueryId afin de pouvoir retrouver la ligne :
Un piège si vous réalisez ce benchmark vous-même : un simple LIMIT n sans ORDER BY renvoie des rows différentes à chaque exécution, si bien que chaque répétition compresse des données différentes et que les ratios ne sont plus que du bruit. Comparez plutôt sur un result set fixe.

Insertion via raw stream

Utilisez InsertRawStreamAsync pour insérer des données directement depuis un fichier ou un flux en mémoire, dans des formats tels que CSV, JSON, Parquet ou tout autre format ClickHouse pris en charge. Insertion depuis un fichier CSV :
Le driver prend possession du stream. InsertRawStreamAsync et PostStreamAsync libèrent le stream que vous leur transmettez dès la fin de la requête, qu’elle ait réussi ou échoué. Ne le libérez pas vous-même et ne le réutilisez pas par la suite — d’où le fait que l’exemple ci-dessus n’encapsule pas le FileStream dans un using.Un using de votre côté s’exécuterait alors que le driver a déjà libéré le stream. Pour un FileStream ou un MemoryStream, ce second appel est sans conséquence, mais pour un stream dont le Dispose restitue un buffer à un pool ou décrémente un compteur de références, la ressource est libérée deux fois.La possession n’est transférée qu’une fois les arguments acceptés : si l’appel lève une exception ArgumentException ou ArgumentNullException parce qu’il manque la table, le stream ou le format, le stream vous appartient toujours.
Consultez la documentation des paramètres de format pour connaître les options permettant de contrôler le comportement de l’ingestion de données.

Autres exemples

Pour d’autres exemples pratiques d’utilisation, consultez le répertoire examples du dépôt GitHub.

ADO.NET

La bibliothèque offre une prise en charge complète d’ADO.NET via ClickHouseConnection, ClickHouseCommand et ClickHouseDataReader. Cette API est nécessaire pour l’intégration avec les ORM (Dapper, Linq2db) et lorsque vous avez besoin des abstractions .NET standard pour les bases de données.

Gestion du cycle de vie avec ClickHouseDataSource

Créez toujours des connexions à partir d’un ClickHouseDataSource afin de garantir une gestion correcte du cycle de vie ainsi qu’un pool de connexions. Le ClickHouseDataSource gère en interne une unique instance de ClickHouseClient, et toutes les connexions partagent son pool de connexions HTTP.
Avec l’injection de dépendances :
Ne créez pas ClickHouseConnection directement dans le code de production. Chaque instanciation directe crée un nouveau client HTTP et un nouveau pool de connexions, ce qui peut entraîner un épuisement des sockets en cas de charge :
Utilisez toujours ClickHouseDataSource à la place, ou partagez une unique instance de ClickHouseClient.

Utilisation de ClickHouseCommand

Créez des commandes à partir d’une connexion pour exécuter des requêtes SQL :
Méthodes de commande :
  • ExecuteNonQueryAsync() - Pour les instructions INSERT, UPDATE, DELETE et DDL
  • ExecuteScalarAsync() - Renvoie la première colonne de la première ligne
  • ExecuteReaderAsync() - Renvoie un ClickHouseDataReader permettant de parcourir les résultats

Utilisation de ClickHouseDataReader

Le ClickHouseDataReader permet un accès typé au résultat de la requête :

Lecture de l’ordinal d’un enum

Une colonne Enum8 ou Enum16 se matérialise sous la forme de son label : GetFieldType renvoie string, et GetString, GetValue et GetFieldValue<string> retournent tous le label. Les accesseurs numériques lèvent une InvalidCastException sur une colonne enum, car la valeur stockée est une chaîne de caractères. Utilisez TryGetEnumOrdinal pour obtenir le nombre associé au label :
Elle renvoie true et définit value pour une colonne Enum8/Enum16, ainsi que pour une colonne Nullable(Enum...) dont la cellule n’est pas NULL. Elle renvoie false, avec value défini à 0, pour une cellule NULL ou pour toute colonne qui n’est pas un enum. L’ordinal correspond à la valeur signée transmise sur le wire : il peut donc être négatif, et un ordinal Enum16 peut dépasser la taille d’un octet.

Bonnes pratiques

Durée de vie des connexions et pool de connexions

ClickHouse.Driver utilise System.Net.Http.HttpClient en interne. HttpClient dispose d’un pool de connexions par endpoint. Par conséquent :
  • Les sessions de base de données sont multiplexées via des connexions HTTP gérées par le pool de connexions.
  • Les connexions HTTP sont automatiquement recyclées par le pool.
  • Les connexions peuvent rester actives après la libération des objets ClickHouseClient ou ClickHouseConnection.
Approches recommandées :
Si vous utilisez un HttpClient ou un HttpClientFactory personnalisé, assurez-vous que PooledConnectionIdleTimeout est défini sur une valeur inférieure au keep_alive_timeout du serveur, afin d’éviter les erreurs dues à des connexions semi-fermées. La valeur par défaut de keep_alive_timeout pour les déploiements Cloud est de 10 secondes.
Évitez de créer plusieurs instances de ClickHouseClient ou des instances autonomes de ClickHouseConnection sans HttpClient partagé. Chaque instance crée son propre pool de connexions.

Gestion des valeurs DateTime

  1. Utilisez UTC chaque fois que possible. Stockez les horodatages dans des colonnes DateTime('UTC') et utilisez DateTimeKind.Utc dans votre code. Cela élimine toute ambiguïté liée au fuseau horaire.
  2. Utilisez DateTimeOffset pour gérer explicitement le fuseau horaire. Il représente toujours un instant précis et inclut les informations de décalage.
  3. Spécifiez le fuseau horaire dans les annotations de type SQL. Lorsque vous utilisez des paramètres avec des valeurs DateTime Unspecified pour des colonnes non UTC, incluez le fuseau horaire dans le SQL :

Insertions asynchrones

Les insertions asynchrones transfèrent la responsabilité du batching du client vers le serveur. Au lieu d’exiger un batching côté client, le serveur met les données entrantes en mémoire tampon, puis les écrit dans le stockage en fonction de seuils configurables. Cela est utile dans les scénarios à forte concurrence, comme les workloads d’observability où de nombreux agents envoient de petites charges utiles. Activez les insertions asynchrones via CustomSettings ou la chaîne de connexion :
Deux modes (contrôlés par wait_for_async_insert) :
Avec wait_for_async_insert=0, les erreurs n’apparaissent qu’au moment de l’écriture sur disque et ne peuvent pas être rattachées à l’insertion d’origine. Le client ne fournit pas non plus de mécanisme de régulation, ce qui risque de surcharger le serveur.
Paramètres clés :

Sessions

N’activez les sessions que si vous avez besoin de fonctionnalités côté serveur avec état, par exemple :
  • Tables temporaires (CREATE TEMPORARY TABLE)
  • Conservation du contexte de requête d’une instruction à l’autre
  • Paramètres de session (SET max_threads = 4)
Lorsque les sessions sont activées, les requêtes sont sérialisées afin d’éviter l’utilisation concurrente d’une même session. Cela ajoute un surcoût pour les workload qui ne nécessitent pas d’état de session.
Utilisation d’ADO.NET (pour assurer la compatibilité avec les ORM) :

Types de données pris en charge

ClickHouse.Driver prend en charge tous les types de données de ClickHouse. Les tableaux ci-dessous présentent les correspondances entre les types ClickHouse et les types .NET natifs lors de la lecture des données depuis la base de données.

Correspondance de types : lecture à partir de ClickHouse

Types d’entiers


Types à virgule flottante


Types décimaux

La conversion du type Decimal est gérée par le paramètre UseCustomDecimals.

Type booléen


Types de chaînes

Par défaut, les colonnes String et FixedString(N) sont toutes deux renvoyées sous forme de string. Définissez ReadStringsAsByteArrays=true dans votre chaîne de connexion pour les lire sous forme de byte[]. Cela est utile lorsque vous stockez des données binaires qui peuvent ne pas être en UTF-8 valide.Ce paramètre s’applique aussi aux chaînes imbriquées dans d’autres types : ainsi, Array(String) est lu comme byte[][] et Map(String, String) comme Dictionary<byte[], byte[]> — clés comprises. La seule exception est une colonne JSON, dont les feuilles de type chaîne sont toujours du texte ; voir Type JSON.

Types de date et d’heure

ClickHouse stocke les valeurs DateTime et DateTime64 en interne sous forme de timestamps Unix (secondes ou fractions de seconde écoulées depuis l’époque). Bien que le stockage soit toujours en UTC, les colonnes peuvent avoir un fuseau horaire associé, qui influe sur la manière dont les valeurs sont affichées et interprétées. Lors de la lecture de valeurs DateTime, la propriété DateTime.Kind est définie en fonction du fuseau horaire de la colonne : Pour les colonnes non UTC, le DateTime renvoyé représente l’heure d’horloge dans ce fuseau horaire. Utilisez ClickHouseDataReader.GetDateTimeOffset() pour obtenir un DateTimeOffset avec le décalage correct pour ce fuseau horaire :
Pour les colonnes sans fuseau horaire explicite (c.-à-d. DateTime au lieu de DateTime('Europe/Amsterdam')), le pilote renvoie un DateTime avec Kind=Unspecified. Cela préserve exactement l’heure telle qu’elle est stockée, sans supposer de fuseau horaire. Si vous avez besoin d’un comportement tenant compte du fuseau horaire pour des colonnes sans fuseau horaire explicite, vous pouvez :
  1. Utiliser des fuseaux horaires explicites dans vos définitions de colonnes : DateTime('UTC') ou DateTime('Europe/Amsterdam')
  2. Appliquer vous-même le fuseau horaire après la lecture.

Type JSON

Le type de retour des colonnes JSON dépend du paramètre JsonReadMode :
  • Binary (par défaut) : renvoie System.Text.Json.Nodes.JsonObject. Fournit un accès structuré aux données JSON, mais les types ClickHouse spécialisés (comme les adresses IP, les UUID ou les grands nombres décimaux) sont convertis en représentations sous forme de chaîne dans la structure JSON.
  • String : renvoie le JSON brut sous forme de string. Préserve la représentation JSON exacte de ClickHouse, ce qui est utile lorsque vous devez transmettre le JSON sans l’analyser, ou lorsque vous souhaitez gérer vous-même la désérialisation.
None est un troisième mode. Il effectue la lecture exactement comme Binary, mais n’envoie aucun paramètre serveur avec la requête — utilisez-le sur une connexion qui n’est pas autorisée à en définir un. Un chemin déclaré dans le type de la colonne est un typed path ; tout autre chemin du document est un dynamic path. Les deux diffèrent lorsqu’une valeur est nulle. Un typed path apparaît toujours dans le JsonObject. Déclaré Nullable(T) ou Dynamic, il est renvoyé sous la forme d’un null JSON aussi bien lorsque la valeur stockée est nulle que lorsque le document ne contient pas ce chemin — les deux cas sont impossibles à distinguer :
Déclaré avec un type non nullable, un chemin absent prend la valeur par défaut du type — JSON(x String) donne {"x":""} et JSON(x Int64) donne {"x":0}. Un chemin dynamique dont la valeur est null est entièrement supprimé de l’objet, si bien que ContainsKey renvoie false pour celui-ci. La lecture de {"x":null} depuis une colonne JSON simple donne {}. Les chemins typés imbriqués créent leurs parents : JSON(a.b Nullable(Int64)) produit donc {"a":{"b":null}} même pour un document vide.
C’est ce que le server restitue lui-même, si bien que les modes Binary et String concordent désormais. Avant la version 1.4.0, un chemin typé contenant null était supprimé du JsonObject, si bien que {"x":null} était relu comme {} — et, pour un chemin imbriqué tel que JSON(a.b Nullable(Int64)), tout le sous-arbre a disparaissait.
Les feuilles de type chaîne au sein d’une colonne JSON sont toujours renvoyées sous forme de texte, quelle que soit la valeur de ReadStringsAsByteArrays — JsonValue n’a pas de forme tableau d’octets, un byte[] serait donc rendu en base64. Cela vaut pour String, FixedString, ainsi que pour ces types encapsulés dans LowCardinality, Nullable ou SimpleAggregateFunction, et pour les chaînes contenues dans Array et Map, clés de map incluses.
Un tableau d’octets dont le lecteur JSON ne peut pas déterminer le type est bel et bien rendu en base64 : un chemin typé Variant ou Dynamic contient une valeur dont le type n’est connu que ligne par ligne ; ainsi, une chaîne sous Variant(Array(UInt8), String) est renvoyée encodée en base64. Le comportement est identique avec les deux réglages.Un type de clé de map JSON autre que String exactement — Map(LowCardinality(String), String), par exemple — lève une NotSupportedException.
ClickHouse accepte une colonne qui déclare un chemin à la fois comme valeur et comme parent d’un autre chemin, par exemple JSON(a Int64, a.b Int64). Les deux chemins sont présents dans chaque ligne, si bien que le serveur restitue la ligne avec une clé dupliquée : {"a":0,"a":{"b":7}}. Un JsonObject ne peut pas contenir deux valeurs pour une même clé ; JsonReadMode.Binary lève donc une SerializationException désignant les deux chemins. Il en va de même lorsque la valeur est un Map, comme dans JSON(a Map(String, Int64)) lu depuis une ligne qui possède également un a.b dynamique. Cela ne s’applique que si les deux côtés portent une valeur dans cette ligne. Un côté qui ne contient rien — un null, un objet vide ou un sous-arbre dont toutes les valeurs sont nulles — cède la place à celui qui porte la donnée, quel que soit celui des deux chemins que le serveur envoie en premier. Un chevauchement déclaré avec des types Nullable ne renseigne donc qu’un seul côté par ligne et se lit sans erreur : JSON(a Nullable(Int64), a.b Nullable(Int64)) donne bien {"a":5} et {"a":{"b":7}}, comme attendu. Lisez une telle colonne avec JsonReadMode.String pour obtenir le texte JSON du serveur inchangé, clé dupliquée comprise. Définissez AllowDuplicateJsonKeys pour continuer à lire la colonne en tant que JsonObject au lieu de lever une exception. Le pilote conserve alors celle des deux valeurs qui apparaît en dernier dans la ligne et abandonne l’autre : le résultat est donc incomplet : JSON(a Int64, a.b Int64) contenant {"a.b":7} se lit comme {"a":0}. Un chemin qui porte une valeur et dont le parent porte un scalar ou un Array lève malgré tout une exception, car un sous-arbre ne peut être placé sous ni l’un ni l’autre.

Map type

Un Map(K, V) ClickHouse est physiquement un Array(Tuple(K, V)) et peut contenir plusieurs entrées associées à la même clé. Ce n’est pas le cas d’un Dictionary : en mode par défaut, une clé répétée ne conserve donc que sa dernière valeur et les paires précédentes sont abandonnées. Le paramètre MapReadMode détermine la représentation utilisée :
  • Dictionary (par défaut) : renvoie Dictionary<K, V>.
  • KeyValuePairs : renvoie List<KeyValuePair<K, V>> dans l’ordre où le serveur a envoyé les paires, si bien que chaque paire est préservée, y compris les entrées qui répètent une clé.
Le mode détermine le type framework d’une colonne Map ; il s’applique donc également à GetFieldValue<T>, aux types de schema signalés par le pilote et au mapping des propriétés POCO. Il s’applique partout où une map apparaît dans l’arborescence de types d’une colonne — y compris Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) et Dynamic. Les deux représentations sont acceptées à l’écriture, quel que soit le mode — voir écriture des maps.

Autres types

Les types Dynamic et Variant sont convertis dans le type correspondant au type sous-jacent réel de chaque ligne.

Types de géométrie

Le type Geometry est un type Variant qui peut contenir n’importe quel type de géométrie. Il sera converti en type correspondant.

Correspondance des types : écriture dans ClickHouse

Lors de l’insertion de données, le pilote convertit les types .NET vers leurs types ClickHouse correspondants. Les tableaux ci-dessous indiquent quels types .NET sont pris en charge pour chaque type de colonne ClickHouse.

Types entiers


Types en virgule flottante


Type Boolean


Types de chaînes


Types de date et d’heure

Valeurs hors plageLors d’une écriture binaire, les valeurs Date, Date32, DateTime et DateTime32 en dehors de leur plage prise en charge lèvent une ArgumentOutOfRangeException au moment de Write, en indiquant le type de colonne et la plage prise en charge. Auparavant, les valeurs hors plage pouvaient être tronquées silencieusement via un entier 32 bits puis réinterprétées par le serveur, produisant des timestamps réels mais erronés.
Le pilote respecte DateTime.Kind lors de l’écriture des valeurs : Les valeurs DateTimeOffset préservent toujours l’instant exact. Exemple : DateTime UTC (instant préservé)
Exemple : DateTime non spécifié (heure d’horloge)
Recommandation : pour un comportement aussi simple et prévisible que possible, utilisez DateTimeKind.Utc ou DateTimeOffset pour toutes les opérations sur les types DateTime. Cela garantit que votre code fonctionne de manière cohérente, quel que soit le fuseau horaire du serveur, du client ou de la colonne.

Paramètres HTTP vs copie en masse

Il existe une différence importante entre la liaison de paramètres HTTP et la copie en masse lors de l’écriture de valeurs DateTime Unspecified : Copie en masse connaît le fuseau horaire de la colonne cible et interprète correctement les valeurs Unspecified dans ce fuseau horaire. Paramètres HTTP ne connaissent pas automatiquement le fuseau horaire de la colonne. Vous devez le spécifier dans l’indication de type SQL :

Types décimaux


Type JSON

Le comportement lors de l’écriture de JSON est contrôlé par le paramètre JsonWriteMode :
  • String (par défaut) : Accepte string, JsonObject, JsonNode ou tout objet. Toutes les entrées sont sérialisées via System.Text.Json.JsonSerializer et envoyées sous forme de chaînes JSON pour un traitement côté serveur. C’est le mode le plus flexible et il fonctionne sans enregistrement préalable de type.
  • Binary : Accepte uniquement les types POCO enregistrés. Les données sont converties côté client au format JSON binaire de ClickHouse avec une prise en charge complète des indications de type. Nécessite d’appeler connection.RegisterJsonSerializationType<T>() avant utilisation. L’écriture de valeurs string ou JsonNode dans ce mode lève ArgumentException.
Lorsqu’une colonne JSON inclut des indications de type (par ex. JSON(id UInt64, price Decimal128(2))), le driver utilise ces indications pour sérialiser les valeurs en respectant pleinement les types. Cela préserve la précision de types comme UInt64, Decimal, UUID et DateTime64, qui perdraient sinon en précision s’ils étaient sérialisés sous forme de JSON générique. Les POCO peuvent être écrits dans des colonnes JSON de deux manières, selon le JsonWriteMode : Mode String (par défaut) : les POCO sont sérialisés via System.Text.Json.JsonSerializer. Aucun enregistrement de type n’est nécessaire. C’est l’approche la plus simple, et elle fonctionne avec les objets anonymes. Mode binaire : les POCO sont sérialisés à l’aide du format JSON binaire du driver, avec prise en charge complète des indications de type. Les types doivent être enregistrés avec connection.RegisterJsonSerializationType<T>() avant utilisation. Ce mode prend en charge des mappages de chemins personnalisés via des attributs :
  • [ClickHouseJsonPath("path")] : associe une propriété à un chemin JSON personnalisé. Utile pour les structures imbriquées ou lorsque le nom de la propriété diffère de la clé JSON souhaitée. Fonctionne uniquement en mode binaire.
  • [ClickHouseJsonIgnore] : exclut une propriété de la sérialisation. Fonctionne uniquement en mode binaire.
La correspondance entre les noms de propriété et les indications de type de colonne est sensible à la casse. Une propriété UserId ne correspondra qu’à une indication définie comme UserId, et non userid. Cela correspond au comportement de ClickHouse, qui permet à des chemins comme userName et UserName de coexister en tant que champs distincts. Limitations (mode binaire uniquement) :
  • Les types POCO doivent être enregistrés sur la connexion avec connection.RegisterJsonSerializationType<T>() avant la sérialisation. Toute tentative de sérialiser un type non enregistré lève une ClickHouseJsonSerializationException.
  • Les propriétés de type Dictionary et array/list nécessitent des indications de type dans la définition de la colonne pour être sérialisées correctement. Sans ces indications, utilisez plutôt String mode.
  • Les valeurs nulles des propriétés POCO ne sont écrites que lorsque le chemin possède une indication de type Nullable(T) dans la définition de la colonne. ClickHouse n’autorise pas les types Nullable dans les chemins JSON dynamiques ; les propriétés nulles sans indication sont donc ignorées.
  • Les attributs ClickHouseJsonPath et ClickHouseJsonIgnore sont ignorés en String mode (ils ne fonctionnent qu’en mode binaire).

Autres types


Types de géométrie


Non pris en charge en écriture


Gestion du type Nested

Les types Nested de ClickHouse (Nested(...)) peuvent être lus et écrits avec la sémantique des tableaux.

Journalisation et diagnostics

Le client .NET ClickHouse s’intègre aux abstractions Microsoft.Extensions.Logging afin d’offrir une journalisation légère, activée sur demande. Lorsqu’elle est activée, le driver émet des messages structurés pour les événements du cycle de vie de la connexion, l’exécution des commandes, les opérations de transport et les opérations d’insertion en masse. La journalisation est entièrement facultative : les applications qui ne configurent pas de logger continuent de s’exécuter sans surcharge supplémentaire.

Démarrage rapide

Utilisation du fichier appsettings.json

Vous pouvez configurer les niveaux de journalisation à l’aide de la configuration .NET standard :

Utilisation d’une configuration en mémoire

Vous pouvez également configurer le niveau de verbosité de la journalisation par catégorie dans le code :

Catégories et émetteurs

Le driver utilise des catégories dédiées afin de vous permettre d’ajuster finement les niveaux de journalisation par composant :

Exemple : diagnostic des problèmes de connexion

Les éléments suivants seront consignés :
  • Sélection de la fabrique de clients HTTP (pool par défaut ou connexion unique)
  • Configuration du gestionnaire HTTP (SocketsHttpHandler ou HttpClientHandler)
  • Paramètres du pool de connexions (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
  • Paramètres de délai d’expiration (ConnectTimeout, Expect100ContinueTimeout, etc.)
  • Configuration SSL/TLS
  • Événements d’ouverture/fermeture des connexions
  • Suivi des ID de session

Mode Débogage : tracing réseau et diagnostics

Pour faciliter le diagnostic des problèmes réseau, la bibliothèque du driver inclut un utilitaire qui active le tracing de bas niveau des mécanismes réseau internes de .NET. Pour l’activer, vous devez fournir une instance de LoggerFactory avec le niveau défini sur Trace, et définir EnableDebugMode sur true (ou l’activer manuellement via la classe ClickHouse.Driver.Diagnostic.TraceHelper). Les événements seront consignés dans la catégorie ClickHouse.Driver.NetTrace. Avertissement : cela générera des logs extrêmement verbeux et aura un impact sur les performances. Il n’est pas recommandé d’activer le mode Débogage en production.

OpenTelemetry

Le driver intègre une prise en charge native du tracing distribué avec OpenTelemetry via l’API .NET System.Diagnostics.Activity. Lorsqu’il est activé, le driver émet des spans pour les opérations sur la base de données, qui peuvent être exportés vers des backends d’observabilité comme Jaeger ou ClickHouse lui-même (via l’OpenTelemetry Collector).

Activer le tracing

Dans les applications ASP.NET Core, ajoutez l’ActivitySource du driver ClickHouse à votre configuration OpenTelemetry :
Pour les applications en ligne de commande, les tests ou la configuration manuelle :

Attributs des spans

Chaque span inclut les attributs de base de données standard d’OpenTelemetry, ainsi que des statistiques de requête propres à ClickHouse utiles pour le débogage.

Options de configuration

Contrôlez le comportement du tracing à l’aide de ClickHouseDiagnosticsOptions :
L’activation de IncludeSqlInActivityTags peut exposer des données sensibles dans vos traces. À utiliser avec prudence dans les environnements de production.

Configuration TLS

Lorsque vous vous connectez à ClickHouse via HTTPS, vous pouvez configurer le comportement de TLS/SSL de plusieurs manières.

Validation personnalisée des certificats

Pour les environnements de production nécessitant une logique de validation des certificats personnalisée, fournissez votre propre HttpClient avec un gestionnaire ServerCertificateCustomValidationCallback configuré :
Points importants à prendre en compte lors de la fourniture d’un HttpClient personnalisé
  • Décompression automatique : laissez AutomaticDecompression désactivé. Le driver décode lui-même les réponses compressées, ce n’est donc pas nécessaire — et l’activer joue en votre défaveur côté requête : au moment de l’envoi, le handler ajoute aussi chaque algorithme de son masque à l’en-tête Accept-Encoding sortant, élargissant ce que le driver avait annoncé, si bien que ClickHouse peut répondre avec un codec que vous n’aviez pas demandé. Voir Décompression des réponses.
  • Délai d’inactivité : définissez PooledConnectionIdleTimeout sur une valeur inférieure au keep_alive_timeout du serveur (10 secondes pour ClickHouse Cloud) afin d’éviter les erreurs de connexion dues à des connexions semi-ouvertes.

Performance tuning

Cette section décrit comment utiliser le client pour obtenir des performances optimales, ainsi que les différentes options que vous pouvez ajuster pour adapter les performances du client à votre cas d’usage.

En un coup d’œil

| Si vous | Faites ceci | |---|---|---| | Lisez des rows dans des POCO | Utilisez QueryAsync<T>, et non MapTo<T> | | Effectuez des insertions volumineuses | Augmentez InsertOptions.BatchSize | | Exécutez une application console ou worker à forte charge d’insertion | Activez le Server GC | | Lisez de gros résultats via le réseau | Laissez la compression des réponses activée (comportement par défaut) | | Insérez via une liaison rapide | Essayez InsertOptions.Compressor = null | | Insérez de nombreuses fois dans la même table | Utilisez UseSchemaCache ou ColumnTypes | | Lisez de très gros résultats | Augmentez ReadBufferSize |

Lecture : choisir le chemin de matérialisation

Il existe trois façons d’extraire une row d’un résultat, et leur coût n’est pas le même. Certains chemins encapsulent les résultats (boxing), ce qui augmente les allocations et dégrade les performances. Pour une lecture de 1 000 000 de rows sur 105 colonnes du dataset hits :
Les ORM bénéficient du chemin rapide lorsqu’ils utilisent des accesseurs typés. linq2db enregistre GetInt64, GetDouble et GetDateTime pour chaque colonne, et lit donc sans boxing. Le code qui lit via GetValue (y compris un résultat dynamic renvoyé par Dapper) applique un boxing à chaque valeur. Si une requête ORM est très sollicitée et lit via GetValue, utilisez QueryAsync<T> pour cette requête précise.

Insertion : taille de batch et parallélisme

La taille de batch est le principal levier sur le throughput d’insertion. InsertOptions.BatchSize vaut par défaut 100 000 rows. Utilisez de grands batches. Pour une insertion de 1 000 000 rows, passer de 10 000 à 100 000 rows par batch a donné : Si vous ne maîtrisez pas la taille de batch (par exemple lorsque de nombreux petits producteurs envoient des rows indépendamment), utilisez les async inserts et laissez le server se charger du batching. Téléversements parallèles. InsertOptions.MaxDegreeOfParallelism vaut 1 par défaut. Augmentez cette valeur pour envoyer plusieurs batches simultanément. Le gain est surtout net lorsque la compression est activée, car chaque batch est alors compressé sur son propre thread. Les sessions sont incompatibles avec les inserts parallèles : désactivez les sessions ou conservez MaxDegreeOfParallelism = 1. Supprimez la schema probe. Chaque appel à InsertBinaryAsync envoie d’abord une requête SELECT ... WHERE 1=0 afin de déterminer les types de colonne. Voir Ignorer la schema probe requête pour éliminer cet aller-retour à l’aide de ColumnTypes ou UseSchemaCache.
Le chemin d’insertion sans boxing s’applique au format RowBinary par défaut. RowBinaryWithDefaults doit examiner chaque value pour repérer le marker DBDefault : il conserve donc le chemin le plus lent.

Compression : les deux directions ne s’accordent pas

La compression échange du CPU contre des octets. L’intérêt de cet échange dépend du sens du transfert, du débit de votre connexion au serveur ClickHouse, de la manière dont vos données réagissent à l’algorithme de compression choisi, et du fait que vous payiez ou non chaque octet transféré. Lectures : laissez la compression activée, sauf si votre serveur s’exécute sur la même machine. C’est le comportement par défaut. Par rapport à l’absence de compression, zstd au niveau 1 a donné : Insertions : mesurez avant de compresser. Les gains ne justifient pas toujours son activation. Gardez également à l’esprit que la décompression ajoute de la charge sur le serveur ; cette charge reste modeste pour Zstd et LZ4, mais peut être élevée pour d’autres algorithmes (par exemple Brotli). Pour désactiver la compression des insertions :
Pour le choix du codec, les niveaux de compression et la manière de déterminer votre propre point de bascule, consultez Réglage de la compression.

Buffers

ReadBufferSize définit la taille du buffer qui lit les réponses HTTP. Sa valeur par défaut est de 64 Kio. Le pilote emprunte ce buffer à un pool partagé et le restitue lorsqu’il libère le reader : il ne s’agit donc pas d’une allocation pour chaque requête. Augmentez cette valeur pour réduire le nombre de remplissages du buffer sur des résultats volumineux. Le pilote conserve un buffer par reader ouvert simultanément : la consommation mémoire augmente donc avec la buffer size et avec le nombre de readers concurrents.
Libérez toujours les readers. Un reader restitue son buffer au pool et libère sa connexion HTTP lorsque vous le libérez. Abandonner un reader sans le libérer ne restitue pas le buffer au pool et peut rendre la connexion HTTP indisponible ; le garbage collection ordinaire ne remplace pas une libération explicite.

Runtime et GC

Activez le Server GC pour les applications réalisant de nombreuses insertions. À code identique et à nombre d’octets alloués identique, le Workstation GC s’est révélé jusqu’à 97 % plus lent que le Server GC sur les insertions.
Les projets ASP.NET Core définissent déjà ce paramètre. Ce n’est pas le cas des applications console, des services worker ni de la plupart des images de conteneur. La cause tient à la taille du budget de la génération 0. Le GC Workstation utilise un budget réduit, si bien que les buffers à courte durée de vie créés par une insertion ne meurent pas en génération 0 : ils passent en génération 1, ce qui augmente la promotion et génère beaucoup plus de travail en génération 2. Dans un cas d’insertion, les collectes de génération 2 pour 1 000 opérations s’élevaient à 4 000 avec le GC Server et à 73 000 avec le GC Workstation.
Le GC Server est un réglage de throughput, pas de latency. Dans ces mêmes mesures, le GC Server a passé moins de la moitié du temps total en pause, mais ses pauses individuelles étaient plus longues (95ᵉ percentile à 114,6 ms contre 61,9 ms). Si votre service est sensible à la tail latency, mesurez les deux modes avant de choisir.

Latence : réutiliser les connexions

L’établissement d’une nouvelle connexion TCP et la réalisation du handshake TLS prennent un temps considérable. Réutiliser les connexions réduit nettement la latence de vos requêtes.
  • Ne créez pas un client pour chaque requête. Chaque nouveau client, avec son propre HttpClient, crée un nouveau pool de connexions et repaie le coût du handshake. Utilisez un seul ClickHouseClient pendant toute la durée de vie de l’application : il est thread-safe et conçu pour un usage en singleton.
  • Pour ADO.NET et les ORM, utilisez ClickHouseDataSource, afin que toutes les connexions partagent un même pool.
Pour l’ensemble des patterns, consultez Durée de vie des connexions et pool de connexions.

Mesurez par vous-même

Dans bien des cas, les performances dépendront de la shape de vos données, de la vitesse de votre liaison au server, du fait que vous souhaitiez ou non troquer du CPU client contre du CPU server (ou l’inverse), des limitations de votre matériel, etc. Il est donc recommandé de mesurer vous-même les performances en fonction de vos données et de votre environnement. Pour connaître la part de travail effectuée par le server, définissez QueryOptions.QueryId et relisez les counters :

Prise en charge des ORM

Les ORM nécessitent l’API ADO.NET (ClickHouseConnection). Pour gérer correctement le cycle de vie des connexions, créez-les à partir d’un ClickHouseDataSource :

Dapper

ClickHouse.Driver est compatible avec Dapper. Le pilote convertit automatiquement la syntaxe @parameter de Dapper en syntaxe native {parameter:Type} de ClickHouse, en déduisant les types à partir des valeurs .NET. Utilisez ClickHouseDataSource pour gérer correctement le cycle de vie de la connexion :

Modes de passage des paramètres

Tous les modes standard de passage des paramètres de Dapper sont pris en charge : Objets anonymes :
Classes POCO :
Dictionnaire :
DynamicParameters (à partir d’un dictionnaire ou d’un objet anonyme) :

Requêtes vers des POCO

Dapper associe les colonnes aux propriétés par leur nom (sans tenir compte de la casse) :

Syntaxe native des paramètres ClickHouse

Lorsque vous avez besoin d’un contrôle explicite des types, utilisez directement dans le SQL la syntaxe {param:Type} de ClickHouse avec un Dictionary<string, object> pour les valeurs de paramètre. N’utilisez pas à la fois la syntaxe @param et la syntaxe {param:Type} pour un même paramètre.

WHERE IN

L’expansion native de IN dans Dapper fonctionne :
Dapper réécrit cela en WHERE id IN (@Ids1, @Ids2, @Ids3), et le driver convertit chaque paramètre étendu. La fonction has() de ClickHouse avec un paramètre Array fonctionne également :

Gestionnaires de types personnalisés

Certains types ClickHouse, par exemple ITuple, BigInteger et ClickHouseDecimal, nécessitent l’enregistrement de gestionnaires au démarrage :
Voir l’exemple Dapper pour un exemple d’implémentation d’un type handler.

Dapper.Contrib

GetAll<T>() et Get<T>(id) fonctionnent. En revanche, Insert<T>() ne fonctionne pas : il génère une syntaxe SQL Server (SCOPE_IDENTITY, []). Il est recommandé d’utiliser à la place la méthode native InsertBinaryAsync de ClickHouseClient.
Les noms des propriétés doivent correspondre exactement aux noms de colonnes de ClickHouse (respect de la casse).

Limitations

Linq2db

Ce pilote est compatible avec linq2db, un ORM léger et un fournisseur LINQ pour .NET. Consultez le site du projet pour une documentation détaillée. Exemple d’utilisation : Créez une DataConnection à l’aide du fournisseur ClickHouse :
Les mappages de tables peuvent être définis à l’aide d’attributs ou de l’API fluide. Si les noms de votre classe et de vos propriétés correspondent exactement aux noms de la table et des colonnes, aucune configuration n’est nécessaire :
Requêtes :
Bulk Copy : Utilisez BulkCopyAsync pour effectuer efficacement des insertions en bloc.

Entity Framework Core

Le fournisseur officiel Entity Framework Core pour ClickHouse. Associez des classes C# à des tables ClickHouse, effectuez des requêtes avec LINQ et insérez des données via SaveChanges — le tout avec les conventions EF Core habituelles.
Ce fournisseur est activement développé. La version actuelle prend en charge les requêtes LINQ (y compris les JOIN, les sous-requêtes et les opérations ensemblistes), INSERT via SaveChanges / BulkInsertAsync, les migrations avec DDL complet (CREATE / ALTER / DROP), ainsi que la configuration du moteur de table spécifique à ClickHouse. UPDATE / DELETE ne sont pas pris en charge.

Installation

Nécessite .NET 10.0 et EF Core 10.

Démarrage rapide

Définissez votre entité et le DbContext, puis effectuez des requêtes avec LINQ :

Types pris en charge

Utilisez ClickHouseDecimal (de ClickHouse.Driver.Numerics) au lieu de decimal lorsque vous avez besoin de toute la précision des colonnes Decimal128/Decimal256 — decimal en .NET est limité à 28–29 chiffres significatifs.

Opérations LINQ prises en charge

Requêtes : Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking GROUP BY et agrégats : GroupBy avec Count, LongCount, Sum, Average, Min, Max — y compris HAVING (.Where() après .GroupBy()), plusieurs agrégats dans une même projection et OrderBy sur les résultats agrégés. JOINs : Join (INNER), schémas GroupJoin/SelectMany (LEFT et CROSS). LEFT JOIN renvoie de vraies valeurs null pour les lignes sans correspondance (voir la sémantique des valeurs nulles de LEFT JOIN ci-dessous). Sous-requêtes : Contains / IN corrélés, Any / EXISTS, All, et sous-requêtes scalaires dans les projections. Opérations ensemblistes : Concat (→ UNION ALL), Union (→ UNION DISTINCT), Intersect, Except. Collections locales en mémoire : les joins et Contains sur des collections en mémoire (int[], List<T>, etc.) sont traduits en une série de UNION. Méthodes de chaîne : Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat (et l’opérateur +). Fonctions mathématiques : les méthodes standard de Math et MathF sont traduites en leurs équivalents ClickHouse — fonctions arithmétiques, logarithmiques, trigonométriques et utilitaires. Le fournisseur injecte automatiquement set_join_use_nulls=1 dans chaque chemin de connexion afin de correspondre aux attentes d’Entity Framework concernant le comportement des JOIN. Si votre serveur ClickHouse ou votre profil interdit la modification de ce paramètre (par exemple, un profil readonly=1), désactivez ce comportement avec :
Lorsque l’opt-out est activé, LEFT JOIN renvoie les valeurs par défaut des colonnes ClickHouse, et la détection par EF des propriétés de navigation basée sur les valeurs nulles ne fonctionne plus comme prévu. Utilisez des comparaisons explicites avec 0 / "" plutôt que == null.

Insertion de données

SaveChanges utilise l’API native InsertBinaryAsync du pilote — l’encodage RowBinary avec un corps de requête compressé est bien plus efficace que le SQL paramétré :
Les entités passent de Added à Unchanged après la sauvegarde, comme avec tout autre fournisseur EF Core. La taille du lot est configurable (1000 par défaut) :

Insertion en masse

Pour les chargements à haut débit, utilisez BulkInsertAsync au lieu de SaveChanges. Il s’agit d’une méthode d’extension sur DbContext qui contourne entièrement le suivi des modifications d’EF Core, la résolution des identités et la gestion d’état — elle appelle directement InsertBinaryAsync du pilote avec l’encodage RowBinary et un corps de requête compressé. Cette méthode convient donc au chargement de grands ensembles de données lorsque vous n’avez pas besoin du suivi des entités après l’insertion :
L’entrée peut être n’importe quel IEnumerable<T> — les entités sont traitées en flux, sans être toutes chargées en mémoire. La valeur de retour est le nombre de lignes insérées. Les entités ne sont pas rattachées au DbContext après l’insertion, il n’y a donc pas de transition d’état Added → Unchanged.

Énumérations

Les colonnes ClickHouse Enum8/Enum16 peuvent être mappées sur des propriétés string ou sur des types C# enum. Lorsqu’on utilise des énumérations C#, le fournisseur convertit automatiquement l’énumération depuis et vers sa représentation sous forme de chaîne :

Conversions de type personnalisées

Le système ValueConverter d’EF Core vous permet d’associer des types personnalisés à des types déjà pris en charge par le fournisseur. Le fournisseur ne voit jamais votre type personnalisé — EF Core effectue la conversion à la limite entre les deux. Conversion par propriété :
Classe de convertisseur réutilisable :

Annotations de type de colonne

Pour les types scalaires comme string, int, DateTime, etc., le fournisseur détermine automatiquement le type ClickHouse. Pour les types paramétrés et les wrappers, vous devez spécifier explicitement le type ClickHouse. Utilisation des annotations de données (attributs) :
Utilisation de l’API fluide dans OnModelCreating :
Les wrappers imbriqués comme Array(Nullable(Int32)) et LowCardinality(Nullable(String)) sont pris en charge — le fournisseur retire automatiquement les wrappers Nullable et LowCardinality à chaque niveau d’imbrication.

Colonnes Variant et Dynamic

Dans .NET, les colonnes ClickHouse Variant(T1, T2, ...) et Dynamic sont mappées à object. Comme object est trop générique pour une inférence de type automatique, vous devez déclarer explicitement le type de stockage via .HasColumnType() :
Lors de la lecture, la valeur est automatiquement désérialisée dans le type .NET correspondant au discriminateur stocké (par ex. string, ulong, ulong[]).

Colonnes JSON

Le fournisseur prend en charge le type de colonne Json de ClickHouse, qu’il associe à System.Text.Json.Nodes.JsonNode (par défaut) ou à string (via un ValueConverter automatique) :
La lecture et l’écriture du JSON s’effectuent via SaveChanges et BulkInsertAsync :
Si vous préférez des chaînes JSON brutes, mappez la propriété en string avec un type de colonne Json — le fournisseur applique automatiquement un ValueConverter :
  • Pas de traduction des chemins JSON — entity.Data["name"] dans LINQ n’est pas converti en syntaxe SQL ClickHouse data.name. Filtrez sur des colonnes non JSON et examinez le JSON en mémoire.
  • Sémantique de NULL — le type JSON de ClickHouse renvoie {} (objet vide) pour les valeurs NULL au lieu de SQL NULL.
  • Précision des entiers — le JSON de ClickHouse stocke tous les entiers en Int64. Lors de la lecture via JsonNode, utilisez GetValue<long>() plutôt que GetValue<int>().

Moteurs de table

Configurez les moteurs de table ClickHouse et les clauses propres à chaque moteur à l’aide de l’API fluide ToTable(name, t => ...). Si aucun moteur n’est configuré, le fournisseur utilise par défaut MergeTree, avec ORDER BY déduit de la clé primaire de l’entité.
Familles de moteurs prises en charge : Clauses du moteur : WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Elles s’appliquent toutes au builder de moteur renvoyé par HasXxxEngine(). Fonctionnalités au niveau des colonnes : HasCodec, HasTtl, HasComment, HasDefault — toutes sont prises en compte dans les migrations. Index de saut de données — via HasIndex(...).HasSkippingIndexType(...):
Les index standard (non-skipping) sont ignorés sans avertissement, car ClickHouse n’a pas d’équivalent. Les index uniques provoquent une exception, car ClickHouse ne garantit pas l’unicité.

Migrations

Processus standard des migrations EF Core :
Opérations prises en charge :

Limitations des migrations

Au-delà des migrations, le fournisseur ne prend pas encore en charge :
  • UPDATE / DELETE
  • Transactions : BeginTransaction est sans effet. Les transactions ACID ne sont pas prises en charge dans ClickHouse.
  • Traduction des requêtes avec chemin JSON : entity.Data["key"] dans LINQ ne se traduit pas en syntaxe SQL ClickHouse data.key. Filtrez sur des colonnes non JSON et inspectez le JSON en mémoire.

Limites

Tuples de 8 éléments ou plus avec un tuple imbriqué en dernière position

Les types C# ValueTuple de plus de 7 éléments utilisent un schéma d’imbrication généré par le compilateur : le 8e argument générique (TRest) est lui-même un ValueTuple qui contient les éléments restants. Par exemple, (int, int, int, int, int, int, int, string, string) est compilé en ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. Cela crée une ambiguïté lorsque la colonne ClickHouse est un tuple de 8 éléments dont le dernier est lui-même un tuple — par exemple, Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). Le pilote ne peut pas faire la distinction entre :
  • Un tuple plat de 9 éléments (imbrication TRest générée par le compilateur)
  • Un tuple de 8 éléments dont le dernier élément est un Tuple(String, String) imbriqué
Les deux produisent le même type .NET : ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. Le pilote traite le 8e argument comme TRest (c’est-à-dire qu’il l’aplatit), ce qui signifie que le cas du tuple de 8 éléments avec tuple imbriqué sera sérialisé de manière incorrecte. Cela affecte à la fois System.Tuple et ValueTuple, car tous deux utilisent l’imbrication TRest au-delà de 7 éléments. Les tuples de 7 éléments ou moins, ou les tuples dont le dernier élément n’est pas lui-même un tuple, ne sont pas affectés. Contournement : encapsulez le tuple interne dans une couche supplémentaire afin que le pilote puisse le distinguer de l’imbrication TRest :

Colonnes de type AggregateFunction

Les colonnes de type AggregateFunction(...) ne peuvent pas être interrogées ni faire l’objet d’une insertion directe. Pour insérer :
Pour sélectionner :

Dernière modification le 26 septembre 2026