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. Initialement développé 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 choix le plus adapté à la plupart des applications.
  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand) : les abstractions de base de données standard de .NET. Elles sont indispensables pour l’intégration avec un ORM (Dapper, Linq2db) et lorsque vous avez besoin de la compatibilité ADO.NET. ClickHouseBulkCopy est une classe utilitaire permettant d’insérer efficacement des données via une connexion ADO.NET. ClickHouseBulkCopy est obsolète et sera supprimée dans une version ultérieure ; utilisez plutôt ClickHouseClient.InsertBinaryAsync.
Les deux API partagent le même pool de connexions HTTP sous-jacent et peuvent être utilisées conjointement au sein d’une même application.

Guide de migration

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

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 via le gestionnaire de paquets NuGet (NuGet Package Manager) :

Démarrage rapide

Configuration

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

Paramètres de connexion

Format de données et sérialisation

Gestion des sessions

L’indicateur UseSession permet de conserver la session côté serveur, et ainsi d’utiliser des statements SET et des tables temporaires. Les sessions sont réinitialisées après 60 secondes d’inactivité (délai d’expiration par défaut). Leur durée de vie peut être prolongée en définissant des paramètres de session à l’aide de statements ClickHouse ou dans 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’indicateur UseSession limite l’exécution à une seule requête active par connexion à la fois (cette limite est imposée par le serveur).

Sécurité

Configuration du client HTTP

Journalisation et débogage

Paramètres personnalisés et rôles

Pour définir des paramètres personnalisés dans une chaîne de connexion, utilisez le préfixe set_, par ex. « set_max_threads=4 ». Avec un objet ClickHouseClientSettings, n’utilisez pas le préfixe set_.Pour 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 permet de remplacer, requête par requête, les paramètres définis au niveau du client. Toutes les propriétés sont facultatives et ne remplacent les valeurs par défaut du client que si elles sont spécifiées. Exemple :

InsertOptions

InsertOptions étend QueryOptions avec des paramètres propres aux insertions en masse effectuées via InsertBinaryAsync. Toutes les propriétés de QueryOptions sont également disponibles dans InsertOptions. Exemple :

Éviter la requête de détection du schéma

Par défaut, InsertBinaryAsync envoie une requête SELECT ... WHERE 1=0 avant chaque insertion afin de déterminer les types des colonnes. Pour les scénarios à haut débit, vous pouvez éliminer ce surcoût de deux manières : Option 1 : fournir explicitement les types de colonnes Si vous connaissez le schéma de la table au moment de la compilation, transmettez-le directement via ColumnTypes. Aucune requête de schéma n’est alors envoyée :
Option 2 : mettre le schéma en cache Lorsque vous insérez des données de manière répétée dans la même table, définissez UseSchemaCache = true pour ne récupérer le schéma qu’une seule fois et le réutiliser lors des insertions suivantes sur la même instance de ClickHouseClient :
  • ColumnTypes a priorité sur UseSchemaCache. Si les deux sont définis, ce sont les types explicites qui sont utilisés.
  • Le cache de schéma ne détecte pas les modifications effectuées par ALTER TABLE. Si vous modifiez le schéma de la table, créez un nouveau ClickHouseClient ou n’utilisez pas UseSchemaCache pour cette table.
  • Le cache est propre à chaque instance de ClickHouseClient et indexé par le couple (base de données, table). Des sous-ensembles de colonnes différents d’une même table partagent un seul et même schéma mis en cache.

ClickHouseClient

ClickHouseClient est l’API recommandée pour interagir avec ClickHouse. Conçue pour être utilisée comme singleton, elle peut être employée simultanément par plusieurs threads en toute sécurité et gère en interne un 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 console ClickHouse Cloud. Sélectionnez un service et cliquez sur Connect : Choisissez C#. Les informations de connexion s’affichent en dessous. Si vous utilisez ClickHouse en autogéré, les informations de connexion vous sont fournies par votre administrateur ClickHouse. Avec une chaîne de connexion :
Ou avec ClickHouseClientSettings :
Pour les scénarios d’injection de dépendances, utilisez IHttpClientFactory :
ClickHouseClient est conçu pour être utilisé sur la durée et partagé dans l’ensemble de votre application. Créez une seule instance (généralement un singleton) et réutilisez-la pour toutes les opérations sur la base de données. Le client gère lui-même le pool de connexions HTTP.

Exécution des requêtes

Utilisez ExecuteNonQueryAsync pour les statements qui ne renvoient aucun résultat :
Utilisez ExecuteScalarAsync pour récupérer une valeur unique :

Insertion de données

Insertions paramétrées

Pour insérer des données, utilisez des requêtes paramétrées avec ExecuteNonQueryAsync. Indiquez le type de chaque paramètre dans la requête SQL selon la syntaxe {name:Type} :

Insertions en masse

Utilisez InsertBinaryAsync pour insérer efficacement un grand nombre de lignes. Cette méthode envoie les données en continu au format binaire natif des lignes de ClickHouse, permet de téléverser des lots en parallèle 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 le traitement par lots et le parallélisme à l’aide de InsertOptions :
  • Avant chaque insertion, le client récupère automatiquement la structure de la table à l’aide de SELECT * FROM <table> WHERE 1=0. Les valeurs fournies doivent correspondre aux types des colonnes cibles. Pour éviter cette requête, utilisez InsertOptions.ColumnTypes ou InsertOptions.UseSchemaCache.
  • Lorsque MaxDegreeOfParallelism > 1, les lots sont téléversé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 pour que le serveur applique les valeurs DEFAULT aux colonnes pour lesquelles aucune valeur n’est fournie.

Insertions de POCO

Plutôt que de créer des tableaux object[], vous pouvez insérer directement des objets POCO fortement typés. Enregistrez le type une fois, puis transmettez un IEnumerable<T> :
Par défaut, toutes les propriétés publiques accessibles en lecture sont mises en correspondance avec les colonnes selon une correspondance stricte des noms, sensible à la casse. Vous pouvez personnaliser cette mise en correspondance à l’aide d’attributs :
Lorsque toutes les propriétés mappées définissent explicitement un Type, la requête de détection du schéma est entièrement évitée. Si seules certaines propriétés définissent explicitement un type, le driver revient à cette requête pour l’ensemble des colonnes. InsertBinaryAsync<T> prend en charge les mêmes InsertOptions (traitement par lots, 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 choisir les colonnes à insérer, utilisez [ClickHouseNotMapped] pour exclure des propriétés ou [ClickHouseColumn(Name = "...")] pour les renommer.Si ColumnTypes est défini dans InsertOptions, ses valeurs prévalent sur les attributs POCO.

Évolution du schéma

Les insertions POCO continuent de fonctionner sans problème lorsque des colonnes sont ajoutées à la table cible après l’enregistrement du type. Comme le driver n’insère que les colonnes auxquelles les propriétés du POCO sont associées, le serveur renseigne automatiquement toute nouvelle colonne dotée d’une clause DEFAULT (ou d’une autre expression par défaut). Il n’est pas nécessaire de modifier le code ni de réenregistrer le type.

Emplacement de la requête d’insertion

Lors d’une insertion binaire, le statement INSERT INTO ... FORMAT ... est placé sur la première ligne du corps de la requête, avant les lignes de données. Comme le corps est compressé par défaut, les mécanismes de routage et de journalisation qui n’inspectent que l’URL ne voient pas ce statement. Définissez InsertOptions.QueryPlacement sur InsertQueryPlacement.Url pour envoyer plutôt le statement dans le paramètre d’URL query, de sorte que le corps ne contienne que les lignes de données :
Utilisez cette option lorsqu’un proxy, un répartiteur de charge ou une passerelle achemine ou inspecte les requêtes en fonction du paramètre query, ou si vous souhaitez que le statement apparaisse dans les logs d’accès et les outils d’observability. Elle doit être activée explicitement, car le statement entre alors dans le calcul de la longueur de l’URL. La limite applicable est la plus basse de celles imposées par le runtime .NET, un intermédiaire ou le serveur. De .NET 6 à .NET 9, System.Uri limite l’URI complète de la requête encodée à 65 519 caractères ; si cette limite est dépassée, le driver lève une InvalidOperationException qui vous invite à revenir à InsertQueryPlacement.Body. Dans ClickHouse, la valeur par défaut de http_max_uri_size est de 1 Mio, mais un intermédiaire peut imposer une limite inférieure. En mode corps, le statement et les lignes ne sont pas soumis à cette limite de longueur d’URL ; d’autres options de requête peuvent néanmoins 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é permet d’accéder aux colonnes du résultat de façon typée, à l’aide de 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 leur position (à partir de 0) ou par leur nom.

Lecture des données dans des POCO

Plutôt que de lire les colonnes par index ou par nom, vous pouvez recevoir les résultats de la requête directement dans vos propres classes, au fil de l’eau. Enregistrez le type une seule fois auprès du client, puis utilisez QueryAsync<T> :
RegisterPocoType<T>() configure les correspondances pour l’insertion et la lecture, puis les valide toutes deux dès l’enregistrement. RegisterBinaryInsertType<T>() reste inchangé et ne concerne que l’insertion, afin de préserver la compatibilité descendante. Un type enregistré doit comporter :
  • Un constructeur public sans paramètre.
  • Au moins une propriété publique dotée d’un accesseur en écriture public qui n’utilise pas init. Les propriétés required sont prises en charge.
La correspondance des colonnes est sensible à la casse. Les colonnes manquantes dans le résultat laissent les propriétés à leur valeur par défaut ; les colonnes supplémentaires sont ignorées. Le driver ne convertit pas les valeurs vers un type plus large ou plus restreint. En dehors des autres représentations répertoriées ci-dessous, le type de la colonne dans le framework doit pouvoir être affecté au type de la propriété ; sinon, une incompatibilité de types lève InvalidOperationException. Une propriété de type object accepte donc n’importe quelle colonne. QueryAsync<T> lit directement chacune de ces colonnes dans une propriété du type correspondant : Chaque ligne accepte aussi la forme nullable du type de propriété (long?, DateOnly?, etc.), que la colonne soit de type Nullable(...) ou non. Une propriété de type valeur non nullable associée à une colonne Nullable(T) est acceptée à l’enregistrement, mais provoque une exception si une valeur NULL est lue. Les types enveloppants comme LowCardinality(T), SimpleAggregateFunction(f, T) et Object(T) se comportent exactement comme T. Les colonnes composites sont également prises en charge et utilisent le type du framework indiqué dans la référence des types de lecture : Array(T) devient T[], Tuple(...) devient System.Tuple<...>, Nested(...) devient Tuple<...>[], JSON devient JsonObject (ou string avec JsonReadMode=String), et Variant/Dynamic deviennent object. Une colonne Map(K, V) est un cas particulier : une propriété List<KeyValuePair<K, V>> ou KeyValuePair<K, V>[] est lue sans mise en boîte et conserve l’ordre de transmission ainsi que les 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 des clés et des valeurs doivent correspondre exactement : ainsi, Map(String, Nullable(Int32)) nécessite KeyValuePair<string, int?>. Lorsqu’une colonne admet plusieurs types de propriétés (une colonne DateTime sous forme de DateTime, DateTimeOffset ou DateOnly, une colonne String sous forme de string ou byte[]), le type de propriété déclaré détermine la représentation. Ces autres représentations sont propres à la lecture des POCO : QueryAsync<T> les prend donc en charge, mais pas MapTo<T>. Lorsque vous parcourez un lecteur manuellement, utilisez ClickHouseDataReader.MapTo<T>() pour matérialiser la ligne en cours sous forme de POCO enregistré, sans passer à la ligne suivante :
Utilisez MapTo<T> lorsque vous devez gérer vous-même la boucle de lecture, par exemple pour combiner l’accès direct aux colonnes avec la materialization des POCO. Cette méthode lit la ligne à partir des valeurs encapsulées du lecteur : elle ne prend donc pas en charge les autres types de propriétés mentionnés ci-dessus et génère davantage d’allocations que QueryAsync<T>. Préférez QueryAsync<T> si vous n’avez besoin que des lignes ; consultez choisir la méthode de materialization pour connaître les chiffres. Un convertisseur de valeurs à la lecture configuré au niveau du client ou pour une requête donnée s’applique aux deux chemins et ne désactive pas la lecture sans mise en boîte. Le driver convertit chaque colonne au moyen de la surcharge correspondant à son mode de lecture : la surcharge typée ConvertValue<T> pour une colonne lue sans mise en boîte, et la surcharge ConvertValue avec mise en boîte pour une colonne composite. Implémentez les deux surcharges de manière cohérente, faute de quoi une même colonne produira des résultats différents selon le chemin emprunté. Lorsqu’une LoggerFactory est configurée, RegisterPocoType<T>() et RegisterBinaryInsertType<T>() émettent un message de journalisation de niveau Debug (catégorie ClickHouse.Driver.Client) précisant quelles propriétés sont associées à quelles colonnes, et lesquelles ont été ignorées, avec les raisons de leur exclusion. Consultez Journalisation et diagnostics.

Paramètres SQL

Dans ClickHouse, les paramètres des requêtes SQL s’écrivent selon le format standard {parameter_name:DataType}. Exemples :
Les paramètres SQL ‘bind’ sont transmis sous forme de paramètres de requête dans l’URI HTTP. S’ils sont trop nombreux, une exception “URL trop longue” peut se produire. Pour éviter cette limitation lors d’insertions massives de données, utilisez InsertBinaryAsync.

Caractères de substitution @name de style ADO

Le driver accepte également les caractères de substitution @name utilisés par des ORM comme Dapper. Ils simplifient l’utilisation côté client : avant l’envoi de la requête, chacun est réécrit sous la forme {name:ResolvedType}. Le serveur ne voit donc jamais de @. Consultez la résolution des types pour savoir comment le type est déterminé. Privilégiez la forme explicite {name:Type} lorsque c’est possible. Un @name sans paramètre correspondant est laissé tel quel et sera rejeté par le serveur. La correspondance est sensible à la casse : @ID ne correspond donc pas au paramètre nommé id.
Pour désactiver cette réécriture, 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. Les requêtes utilisant la syntaxe native {name:Type} continuent donc de fonctionner.

Paramètres d’identifiant

Le type de paramètre Identifier permet de passer en toute sécurité un nom de base de données, de table ou de colonne, plutôt qu’une chaîne littérale entre guillemets. Utilisez la syntaxe {name:Identifier} en SQL ou définissez ClickHouseDbParameter.ClickHouseType = "Identifier" :
La valeur est envoyée telle quelle, puis le serveur la substitue en tant qu’identifiant SQL sans délimiteurs, en se chargeant de l’encadrer d’accents graves et d’échapper les caractères nécessaires. Les identifiants contenant des caractères spéciaux (y compris des accents graves) sont préservés lors d’un aller-retour.

Identifiant de requête

Chaque requête reçoit un query_id unique, qui permet de récupérer des données dans la table system.query_log ou d’annuler les requêtes de longue durée. Vous pouvez définir un identifiant de requête personnalisé à l’aide de QueryOptions :
Si vous définissez un QueryId personnalisé, veillez à utiliser une valeur unique pour 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 la forme @ (par exemple, WHERE id = @id), le driver déduit automatiquement le type ClickHouse du type de la valeur .NET. Par exemple, le type int correspond à Int32.
Comportement des paramètres DateTime dont le type est déduitPour les paramètres de la forme @ sans indication {name:Type} dans la requête SQL ni ClickHouseType défini, les valeurs représentant un instant sont associées à DateTime('UTC') plutôt qu’à un simple DateTime. Les valeurs DateTime dont la propriété Kind vaut Utc ou Local, ainsi que toutes les valeurs DateTimeOffset, sont envoyées en tant que DateTime('UTC'), ce qui préserve l’instant quel que soit le fuseau horaire du serveur.Les indications explicites ({name:DateTime}) priment sur la déduction automatique et constituent la méthode recommandée pour construire des requêtes.
Pour modifier ce comportement par défaut, définissez ParameterTypeResolver sur ClickHouseClientSettings. Vous pouvez ainsi utiliser DateTime64(3) pour tous les paramètres DateTime afin d’obtenir une précision à la milliseconde, ou appliquer un scale donné à tous les nombres décimaux, sans définir ClickHouseType pour chaque paramètre. Utilisation de DictionaryParameterTypeResolver pour des correspondances de types simples :
Implémentation personnalisée de IParameterTypeResolver pour les cas d’usage avancés : Pour déterminer le type d’un paramètre en fonction de sa valeur ou de son nom, 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 requête donnée via QueryOptions.ParameterTypeResolver. Dans ce cas, il prend le pas sur le résolveur configuré au niveau du client. Ordre de priorité pour la résolution des types : Le résolveur intervient dans une chaîne de priorité. Par ordre décroissant :
  1. ClickHouseType défini explicitement sur le paramètre
  2. Annotation de type SQL indiquée par la syntaxe {name:Type} dans la requête
  3. IParameterTypeResolver (celui de QueryOptions.ParameterTypeResolver ou, à défaut, celui de ClickHouseClientSettings.ParameterTypeResolver)
  4. Inférence de type intégrée (TypeConverter.ToClickHouseType)
Le résolveur fonctionne également avec ClickHouseConnection via ADO.NET : les connexions créées à partir du client héritent de ces paramètres.

Formatage personnalisé des valeurs de paramètres

IParameterFormatter est un point d’extension qui détermine comment les valeurs des paramètres sont sérialisées. Utilisez-le lorsque le formatage intégré (par exemple, la précision de DateTime, les conventions locales pour les décimaux, l’échappement des chaînes ou la représentation des nombres) ne correspond pas à ce qu’attendent votre schéma ou les outils en aval. Définissez ParameterFormatter dans ClickHouseClientSettings pour appliquer un formateur à toutes les requêtes paramétrées. Ce formateur reçoit la valeur, le nom du 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 laisser le formateur par défaut prendre le relais. Utilisation de DictionaryParameterFormatter pour un formatage simple selon le type CLR :
IParameterFormatter personnalisé pour les cas d’usage avancés :
Vous pouvez également définir un formateur pour chaque requête via QueryOptions.ParameterFormatter. Dans ce cas, il prévaut sur celui configuré au niveau du client. Valeurs composites : Le formateur s’applique aussi bien aux paramètres de collection de premier niveau qu’à chaque élément des valeurs composites (Array, Tuple, Map, Nullable, LowCardinality, Variant). Par exemple, une correspondance pour typeof(int) formate séparément chaque élément Int32 d’un Array(Int32). Encadrement par des apostrophes dans les valeurs composites : Pour les types ClickHouse assimilables à des chaînes (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) présents dans un littéral composite, le driver entoure la sortie du formateur d’apostrophes, sans toutefois échapper son contenu. Si la chaîne renvoyée contient une apostrophe ou un antislash non échappé, le littéral composite sera mal formé et le serveur rejettera la requête. Les paramètres de chaîne de premier niveau (non intégrés dans une valeur composite) sont utilisés tels quels, sans être entourés d’apostrophes ; il n’est donc pas nécessaire de les échapper. Ordre de priorité des formateurs :
  1. IParameterFormatter (défini via QueryOptions.ParameterFormatter, ou à défaut via ClickHouseClientSettings.ParameterFormatter). S’il renvoie une valeur autre que null, celle-ci est utilisée.
  2. Le formatage intégré propre à chaque type dans HttpParameterFormatter.
Le formateur n’est pas appelé pour les valeurs null ou DBNull : elles sont toujours sérialisées sous la forme du marqueur de valeur nulle de ClickHouse (\N).

Conversion personnalisée des valeurs lues

IReadValueConverter permet de transformer les valeurs renvoyées par le lecteur de données après désérialisation, sans modifier leur type CLR. Parmi les cas d’usage courants : définir DateTime.Kind = Utc pour une colonne DateTime sans fuseau horaire, supprimer les espaces en début et en fin de chaîne ou normaliser les chaînes de caractères, ou encore traiter une colonne JSON avant que ses valeurs ne parviennent au code de l’application. Définissez ReadValueConverter dans ClickHouseClientSettings pour appliquer un convertisseur à toutes les lectures. Celui-ci est appelé une fois par colonne et par ligne, aussi bien par la voie des valeurs encapsulées (GetValue) que par la voie générique (GetFieldValue<T>). Si aucun convertisseur n’est défini, il n’y a aucun surcoût : 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’a pas été enregistré avec For<T> restent inchangées. La sélection repose sur une correspondance exacte du type CLR : enregistrez donc le type effectivement produit par le lecteur (par exemple, For<JsonObject> pour une colonne JSON en mode JsonReadMode.Binary). Convertisseur IReadValueConverter personnalisé pour les cas avancés : Si la sélection doit reposer sur la chaîne représentant le type côté ClickHouse (par exemple, pour distinguer DateTime de DateTime('UTC'), qui sont tous deux représentés par 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 la colonne (GetFieldType, GetSchemaTable) ne passent pas par lui et doivent rester cohérentes avec la valeur renvoyée. Vous pouvez également définir un convertisseur pour chaque requête via QueryOptions.ReadValueConverter ; s’il est défini, il prévaut sur le convertisseur configuré au niveau du client. Périmètre d’application du convertisseur : Le convertisseur est appelé une fois par colonne, avec la valeur complète de la cellule désérialisée ; il ne parcourt pas récursivement les conteneurs composites. Pour une colonne Array(Int32), la valeur transmise est un int[] ; pour Tuple(Int32, String), c’est un ITuple. Surcharge appelée : Les deux surcharges doivent produire des résultats cohérents, car celle qu’appelle le driver dépend de la façon dont le code 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 lue sans mise en boîte par le chemin de lecture des POCO.
  • ConvertValue (avec mise en boîte) — GetValue, GetValues, les indexeurs, GetChar, GetTuple et les chemins de conversion de GetBoolean, GetDecimal et GetString.
IsDBNull n’appelle aucun convertisseur : il lit directement l’indicateur de valeur nulle. Un convertisseur ne peut donc jamais modifier le statut nul d’une valeur. TryGetEnumOrdinal ne passe pas non plus par le convertisseur ; voir la lecture de la valeur ordinale d’une énumération. Le convertisseur fonctionne également avec ClickHouseConnection d’ADO.NET : les connexions créées à partir du client héritent de ces paramètres.

Diffusion en flux brut

Utilisez ExecuteRawResultAsync pour diffuser directement les résultats de la requête au format souhaité, sans passer par le lecteur de données. Cette méthode est utile pour exporter des données dans des fichiers ou les transmettre à d’autres systèmes :
Formats courants : JSONEachRow, CSV, TSV, Parquet, Native. Consultez la documentation sur les formats pour connaître toutes les options.

Compression des données transportées par requête

Par défaut, le client négocie zstd, lz4, gzip, deflate lorsque Compression=true (valeur par défaut dans la chaîne de connexion) et décompresse lui-même le flux, de manière transparente. Pour les exportations de données brutes (par exemple, Parquet, Arrow, Native), vous pouvez négocier un autre codec (par exemple, zstd ou lz4) afin de réduire le débit nécessaire au prix d’une utilisation accrue du CPU, sans modifier le paramètre de compression de la connexion. QueryOptions.AcceptEncoding et ClickHouseCommand.AcceptEncoding définissent l’en-tête HTTP Accept-Encoding pour une seule requête, à la place de toute valeur par défaut, et ajoutent enable_http_compression=1 à l’URL (ce que ClickHouse exige pour prendre en compte Accept-Encoding).

Configuration de HttpClient

Rien à configurer : le HttpClient créé par le driver conserve AutomaticDecompression à DecompressionMethods.None, et le driver décode lui-même les réponses. Ainsi, l’en-tête Content-Encoding n’est jamais supprimé à votre insu et vous recevez le corps brut exactement tel que le serveur l’a envoyé.
Si vous fournissez votre propre HttpClient, laissez aussi AutomaticDecompression désactivé. Ce paramètre n’agit pas seulement sur les réponses : au moment de l’envoi, le handler ajoute tous les algorithmes de son masque absents de l’en-tête Accept-Encoding sortant. 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 les données transmises. Comme ClickHouse interprète l’en-tête selon un ordre de préférence des codecs qui lui est propre et qui est fixe (sans tenir compte de l’ordre ni des valeurs q), il peut répondre avec un codec que vous n’avez jamais demandé. Le handler décode alors la réponse et supprime l’en-tête, si bien que vous ne pouvez même pas vous en apercevoir. En laissant le masque désactivé, vous vous assurez que seuls les codecs choisis sont proposés.
Si AcceptEncoding demande un codec que le driver ne peut pas décoder (snappy), seul ExecuteRawResultAsync peut être utilisé sans risque. ExecuteReaderAsync, ExecuteScalarAsync et ExecuteNonQueryAsync échouent en levant une NotSupportedException qui indique le codec concerné (auparavant, ils interprétaient les octets compressés comme le format du résultat et produisaient des données inutilisables).

Corps des erreurs

Lorsque le serveur renvoie un code 4xx/5xx alors que enable_http_compression=1 est défini, il compresse le corps de l’erreur avec le codec qu’il aurait utilisé pour une réponse réussie. Le driver décode ce corps pour chacun des codecs qu’il prend en charge (lz4, zstd, gzip, deflate, br/brotli), afin que le message de ClickHouseServerException soit lisible. Pour les autres codecs (snappy, …), il renvoie un caractère de substitution qui indique le nom du codec et renvoie à system.query_log pour retrouver le texte d’origine de l’erreur.

Décompression des réponses

Accept-Encoding demande seulement au serveur de compresser la réponse : encore faut-il la décompresser. Le driver s’en charge lui-même en se fondant sur l’en-tête Content-Encoding de la réponse. Toutes les API de lecture habituelles (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db) fonctionnent donc avec les réponses compressées sans configuration supplémentaire. Il décompresse 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 utiliser un autre format, définissez vous-même Accept-Encoding pour l’ensemble du client :
par requête, qui prévaut :
ou dans la chaîne de connexion, pour les utilisateurs d’ORM qui ne modifient jamais ClickHouseClientSettings :
Le définir ajoute également enable_http_compression=1 à l’URL, condition exigée par ClickHouse pour prendre en compte l’en-tête, même lorsque UseCompression vaut false : nommer explicitement un codec revient à en demander un. Si aucune valeur n’est définie, UseCompression=false n’envoie pas d’en-tête Accept-Encoding. Accept-Encoding peut être défini à quatre endroits. Le premier à indiquer un codec prévaut :
  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 mot-clé AcceptEncoding de la chaîne de connexion
Si aucun n’indique de codec, le driver envoie sa liste par défaut. Une valeur qui n’indique aucun codec (null, vide, composée uniquement d’espaces ou de virgules) est considérée comme non définie : c’est alors l’emplacement suivant qui est pris en compte. Pour désactiver la compression, utilisez identity. C’est le serveur, et non le client, qui choisit le codec. ClickHouse examine Accept-Encoding selon son propre ordre de préférence, qui est fixe — zstd > br > lz4 > snappy > gzip > deflate — sans tenir compte de l’ordre dans lequel vous indiquez les codecs ni des valeurs q. L’en-tête indique donc les codecs acceptés sans imposer de choix : pour orienter celui-ci, il faut omettre certains codecs. La liste par défaut contient zstd : une requête utilisant cette liste reçoit donc une réponse compressée avec zstd, les autres codecs servant de solutions de repli. br peut être décodé, mais ne figure pas dans la liste par défaut. La taille des charges utiles, l’utilisation du CPU du serveur et celle du client varient selon le codec, vos données, votre connexion et le paramètre http_zlib_compression_level du serveur (valeur par défaut à la livraison : 3) — voir Optimiser la compression.
  • http_zlib_compression_level. Ce paramètre s’applique à tous les codecs HTTP et sa valeur par défaut est 3. Ajustez-le en fonction de vos données, du débit de votre connexion et de l’utilisation du CPU.
  • Un client limité par le CPU sur une connexion rapide. Le driver décode le corps de la réponse sur le thread appelant. Lorsque le réseau n’est pas le goulot d’étranglement, la vitesse de décodage côté client peut donc devenir le facteur limitant.
Si l’un de ces cas s’applique, demandez un autre codec pour chaque requête ou pour l’ensemble du client :
Comme la décision repose sur la réponse, le corps est décodé lorsque son en-tête Content-Encoding l’indique, quelle que soit la requête initiale : si cet en-tête est absent ou vaut identity, le corps reste inchangé ; s’il indique un codec pris en charge, le corps est décodé ; toute autre valeur déclenche une erreur qui la mentionne. Il n’y a aucun risque de double décodage : si la propriété AutomaticDecompression d’un handler fourni par l’appelant a déjà décodé le corps, elle supprime également l’en-tête Content-Encoding. Le driver reçoit donc des données en clair et les laisse telles quelles. Les résultats bruts n’annoncent aucun codec. ExecuteRawResultAsync (ainsi que les méthodes publiques PostStreamAsync / InsertRawStreamAsync) vous transmettent le corps de la réponse tel quel. Si vous ne spécifiez pas vous-même de codec, ces méthodes n’en demandent donc aucun : le driver ne décode pas ce corps, et annoncer un codec dans ce cas transformerait silencieusement une exportation en fichier compressé. La règle est donc simple, quelle que soit la configuration de HttpClient : le corps de la réponse arrive exactement tel que le serveur l’a envoyé, et le serveur envoie des données en clair sauf si vous avez demandé un codec. Demander un codec (pour l’ensemble du client ou pour une requête donnée) permet d’exporter délibérément des octets compressés. Un AcceptEncoding explicite (à l’un ou l’autre niveau) s’applique aussi aux requêtes brutes, et ClickHouseRawResult.ReadDecompressedStreamAsync() décode le résultat si vous le souhaitez ; ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync et CopyToAsync renvoient toujours les octets exactement tels qu’ils ont été reçus.
Lisez le flux renvoyé jusqu’au bout avant qu’il ne sorte de portée, comme ci-dessus. Si la réponse est compressée, vous obtenez un décodeur créé avec leaveOpen : le libérer n’affecte donc pas la réponse. Si elle n’est pas compressée, vous obtenez directement le flux du contenu HTTP : le libérer ferme donc le corps de la réponse. Dans les deux cas, ClickHouseRawResult est propriétaire de la réponse : n’appelez plus ses autres méthodes de lecture après avoir libéré le flux. Vous devez toujours libérer ClickHouseRawResult, et cela suffit : cette opération libère la réponse ainsi que tout décodeur créé ici (les décodeurs conservent des tampons provenant d’un pool). Le await using ci-dessus est donc facultatif, mais vous pouvez le conserver sans risque. Plusieurs appels successifs renvoient le même flux ; ce type ne peut pas être utilisé simultanément par plusieurs opérations. Consultez Select_007_ResponseCompression.cs pour un exemple exécutable.

Compression des requêtes d’insertion

Zstd est le codec utilisé par défaut pour les insertions : InsertOptions.Compressor est initialisé à ZstdCompressor.Default, qui correspond à zstd au niveau 3. Pour changer de codec, définissez un autre compresseur ; pour envoyer le corps sans compression, définissez null.
Le driver est fourni avec quatre codecs. Chacun dispose d’une instance Default et d’un constructeur qui accepte un niveau et une taille de tampon d’écriture :
Partagez les instances de compresseur. Chaque Default est une instance partagée, et les quatre compresseurs peuvent être utilisés simultanément par plusieurs threads sans risque — ce qui se produit lorsque InsertOptions.MaxDegreeOfParallelism est supérieur à 1, car une même insertion utilise un compresseur par lot. Aucun d’entre eux n’implémente IDisposable. Créez votre propre instance une seule fois, puis réutilisez-la comme vous le feriez avec Default.
IClickHouseCompressor est public et toute implémentation ne doit fournir que deux membres :
Le serveur doit accepter la valeur de 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 redéfinissez donc que ceux dont votre codec a besoin. Implémentez Decompress pour décoder les corps des réponses, en complément de la compression des requêtes. Si un corps est corrompu ou dans un format incorrect, le flux renvoyé doit lever InvalidDataException. InsertOptions.Compressor ne s’applique qu’aux insertions binaires. Les autres corps de requête du driver suivent d’autres règles de compression et ne passent pas par ce paramètre :
  • Toute requête contenant du texte SQL (ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, ExecuteRawResultAsync, la couche ADO.NET) envoie son statement avec Content-Encoding: gzip lorsque UseCompression vaut true, ce qui est le cas par défaut. Le codec n’est pas configurable : AcceptEncoding ne concerne que la réponse. Pour la requête, c’est donc gzip ou aucune compression. Avec Compression=false, le statement est envoyé sans compression. Les statements sont petits : il est rarement nécessaire de s’en préoccuper, mais cette distinction est utile lorsque vous examinez les requêtes dans un proxy ou une capture de paquets.
  • Un corps multipart — une requête dont les paramètres sont envoyés sous forme de données de formulaire (UseFormDataParameters=true) — est toujours envoyé sans compression, quelle que soit la valeur de UseCompression.
  • Un téléversement brut (InsertRawStreamAsync, PostStreamAsync) dispose de son propre indicateur, défini à chaque appel, et ne tient compte ni de UseCompression ni de InsertOptions.Compressor : il utilise gzip si cet indicateur est activé, et aucune compression sinon. Notez que le paramètre useCompression d’InsertRawStreamAsync vaut true par défaut : un téléversement brut est donc compressé avec gzip, sauf si vous passez false, même lorsque Compression=false est défini sur le client.

Optimiser la compression

La compression réduit le volume de données transférées au prix d’une consommation accrue de CPU. Son intérêt dépend presque entièrement du débit de votre connexion par rapport à la vitesse d’exécution du codec. Aucun paramètre ne convient à tous les cas.

Le chiffre qui fait toute la différence

La compression est avantageuse tant que le codec est plus rapide que le réseau. Ce seuil est plus bas qu’on ne le pense généralement lors de la lecture, car ClickHouse compresse les réponses HTTP sur un seul thread dans le tampon de sortie. Sur un service ClickHouse Cloud à 16 vCPU, les mesures (hits, RowBinary, niveau 3) montrent que le serveur produit des données compressées à un débit d’environ 100 à 200 Mo/s. Ainsi, pour un résultat volumineux, si une seule requête est traitée à la fois, la compression cesse d’être avantageuse aux alentours de 100 Mo/s. Un seul flux HTTPS au sein d’une même région cloud dépasse généralement ce débit, tandis qu’un flux traversant l’Internet public, un VPN ou une frontière entre régions reste généralement en dessous. Lors des insertions, la compression reste avantageuse jusqu’à des débits réseau plus élevés, car le client compresse les données sur un cœur qui lui est dédié et le fait généralement plus vite que le serveur ne compresse les réponses.

Repères selon le type de déploiement

Ce tableau ne tient pas compte de trois facteurs :
  • Coût du trafic sortant : si le transfert de données vous est facturé, chaque octet a un coût, indépendamment de la latence, ce qui incite à choisir une compression plus forte, quelle que soit la vitesse de la connexion.
  • Petits résultats : les indications ci-dessus concernent les charges utiles volumineuses. Pour les petites réponses, le choix du codec a peu d’importance : c’est le coût de traitement de chaque requête qui prédomine.
  • Les insertions parallèles repoussent les seuils de débit indiqués pour les insertions. Chaque valeur de débit ci-dessus correspond à un seul thread. InsertOptions.MaxDegreeOfParallelism vaut 1 par défaut, mais augmenter cette valeur permet de compresser plusieurs lots en parallèle. Le débit d’encodage global du client augmente donc à peu près proportionnellement au nombre de cœurs qui lui sont alloués. Ainsi, même sur une connexion rapide, il peut rester intéressant de compresser une insertion parallèle à des débits bien supérieurs à ceux pour lesquels la compression cesse d’être avantageuse avec un seul thread. Considérez les débits des lignes du tableau consacrées aux insertions comme des planchers et, si vous traitez déjà les lots en parallèle, refaites des tests avant de conclure que votre connexion est trop rapide pour que la compression soit utile.
Les lectures ne peuvent être parallélisées qu’en exécutant plusieurs requêtes.

Choisir un codec

Niveaux

La compression des réponses est contrôlée par un seul paramètre de serveur, http_zlib_compression_level, qui s’applique à tous les codecs HTTP, et pas seulement à zlib. Sa valeur par défaut est 3. Ne le modifiez que si vos mesures le justifient. Au-delà de la valeur par défaut, le gain en volume est faible au regard du coût en CPU (pour zstd, passer de 3 à 6 double environ l’utilisation du CPU du serveur pour seulement 14 % d’octets en moins), et le coût de br devient démesuré. À l’inverse, au niveau 1, la situation change nettement : lz4 consomme beaucoup moins de CPU et zstd perd son avantage sur ce point. Si nécessaire, définissez ce paramètre pour chaque requête :

Mesurer votre propre seuil de bascule

Pour choisir le codec et le niveau de compression les mieux adaptés, le plus simple est de mesurer le temps d’exécution d’une même requête avec plusieurs codecs, puis de comparer les résultats.
Pour obtenir la vue correspondante côté serveur, lisez ProfileEvents dans system.query_log — définissez QueryOptions.QueryId pour pouvoir retrouver la ligne :
Attention si vous effectuez vous-même ce test de performance : un simple LIMIT n sans ORDER BY renvoie des lignes différentes à chaque exécution. Chaque essai compresse donc des données différentes, et les ratios ne sont plus comparables. Faites la comparaison sur un ensemble de résultats fixe.

Insertion à partir d’un flux brut

Utilisez InsertRawStreamAsync pour insérer des données directement à partir de flux provenant de fichiers ou de la mémoire, dans des formats tels que CSV, JSON, Parquet ou tout autre format pris en charge par ClickHouse. Insertion à partir d’un fichier CSV :
Le driver prend possession du flux. InsertRawStreamAsync et PostStreamAsync libèrent le flux que vous leur transmettez dès que la requête se termine, qu’elle réussisse ou non. Ne le libérez pas vous-même et ne le réutilisez pas ensuite : c’est pourquoi, dans l’exemple ci-dessus, le FileStream n’est pas placé dans un bloc using.Si vous ajoutez un bloc using, il s’exécute alors que le driver a déjà libéré le flux. Pour un FileStream ou un MemoryStream, ce second appel est sans conséquence. En revanche, si la méthode Dispose du flux restitue un tampon à un pool ou décrémente un compteur de références, la ressource est libérée deux fois.Le driver ne prend possession du flux qu’une fois les arguments acceptés : si l’appel lève ArgumentException ou ArgumentNullException parce qu’il manque une table, un flux ou un format, le flux reste sous votre responsabilité.
Consultez la documentation des paramètres de format pour connaître les options permettant de contrôler l’ingestion des données.

Autres exemples

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

ADO.NET

La bibliothèque prend entièrement en charge ADO.NET via ClickHouseConnection, ClickHouseCommand et ClickHouseDataReader. Cette API est indispensable pour intégrer des ORM (Dapper, Linq2db) ou utiliser les abstractions standard de .NET pour les bases de données.

Gestion du cycle de vie avec ClickHouseDataSource

Créez toujours les connexions à partir d’un ClickHouseDataSource pour assurer une gestion correcte de leur cycle de vie et bénéficier du pool de connexions. Le ClickHouseDataSource gère en interne un seul ClickHouseClient, dont toutes les connexions partagent le pool de connexions HTTP.
Pour l’injection de dépendances :
Ne créez pas directement de ClickHouseConnection dans le code de production. Chaque instanciation crée un nouveau client HTTP et un nouveau pool de connexions, ce qui risque d’épuiser les sockets sous une forte charge :
Utilisez plutôt systématiquement ClickHouseDataSource ou partagez une même 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 statements de type INSERT, UPDATE, DELETE ou 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 d’accéder aux résultats des requêtes avec des types de données définis :

Lecture de la valeur ordinale d’une énumération

Une colonne Enum8 ou Enum16 se présente sous la forme de son libellé : GetFieldType renvoie string, et GetString, GetValue et GetFieldValue<string> renvoient tous le libellé. Les accesseurs numériques lèvent une InvalidCastException pour une colonne d’énumération, car la valeur stockée est une chaîne. Utilisez TryGetEnumOrdinal pour obtenir la valeur numérique associée au libellé :
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 et fixe value à 0 pour une cellule NULL ou pour toute colonne qui n’est pas de type enum. La valeur ordinale est la valeur signée issue du format wire : elle peut donc être négative et, dans le cas d’un Enum16, dépasser la capacité 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 pour chaque endpoint. Il en résulte que :
  • Les sessions de base de données sont multiplexées sur les connexions HTTP gérées par le pool.
  • Le pool renouvelle automatiquement les connexions HTTP.
  • Les connexions peuvent rester actives après la libération des objets ClickHouseClient ou ClickHouseConnection.
Pratiques recommandées :
Si vous utilisez un HttpClient ou un HttpClientFactory personnalisé, veillez à définir PooledConnectionIdleTimeout sur une valeur inférieure au keep_alive_timeout du serveur pour éviter les erreurs liées aux connexions semi-fermées. Dans les déploiements Cloud, la valeur par défaut de keep_alive_timeout 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 timestamps dans des colonnes DateTime('UTC') et utilisez DateTimeKind.Utc dans votre code. Vous éviterez ainsi toute ambiguïté liée aux fuseaux horaires.
  2. Utilisez DateTimeOffset pour gérer explicitement les fuseaux horaires. Ce type représente toujours un instant précis et contient le décalage par rapport à UTC.
  3. Précisez le fuseau horaire dans les annotations de type SQL. Si vous utilisez des paramètres contenant des valeurs DateTime Unspecified pour des colonnes qui ne sont pas en UTC, indiquez le fuseau horaire dans la requête SQL :

Insertions asynchrones

Les insertions asynchrones confient au serveur, plutôt qu’au client, le regroupement des données en lots. Le serveur met les données entrantes en tampon, puis les écrit dans le stockage lorsque les seuils configurés sont atteints, sans nécessiter de regroupement en lots côté client. Cette approche est particulièrement utile lorsque de nombreux agents envoient de petites charges utiles simultanément, comme pour les charges de travail d’observability. Activez les insertions asynchrones à l’aide de CustomSettings ou de la chaîne de connexion :
Deux modes (contrôlés par wait_for_async_insert) :
Avec wait_for_async_insert=0, les erreurs ne sont signalées qu’au moment du vidage du tampon et ne peuvent pas être rattachées à l’insertion d’origine. Le client n’exerce pas non plus de régulation du flux, ce qui peut entraîner une surcharge du serveur.
Principaux paramètres :

Sessions

N’activez les sessions que si vous avez besoin de fonctionnalités côté serveur nécessitant la conservation d’un état, par exemple :
  • Les tables temporaires (CREATE TEMPORARY TABLE)
  • La conservation du contexte de requête d’un statement à l’autre
  • Les paramètres propres à la session (SET max_threads = 4)
Lorsque les sessions sont activées, les requêtes sont sérialisées pour empêcher l’utilisation simultanée d’une même session. Cela ajoute un surcoût aux charges de travail qui n’ont pas besoin de conserver l’état de la session.
Utilisation d’ADO.NET (pour la compatibilité avec les ORM) :

Types de données pris en charge

ClickHouse.Driver prend en charge tous les types de données ClickHouse. Les tableaux ci-dessous indiquent les correspondances entre les types ClickHouse et les types .NET natifs utilisés pour lire les données de la base de données.

Correspondance des types : lecture depuis ClickHouse

Types entiers


Types à virgule flottante


Types décimaux

La conversion des types décimaux dépend du paramètre UseCustomDecimals.

Type booléen


Types de chaînes de caractères

Par défaut, les colonnes String et FixedString(N) sont renvoyées sous forme de string. Pour les lire sous forme de byte[], définissez ReadStringsAsByteArrays=true dans votre chaîne de connexion. C’est utile si vous stockez des données binaires qui ne sont pas forcément encodées en UTF-8 valide.Ce paramètre s’applique aussi aux chaînes de caractères imbriquées dans d’autres types : Array(String) est ainsi lu sous forme de byte[][] et Map(String, String) sous forme de Dictionary<byte[], byte[]>, y compris pour les clés. Seules les colonnes JSON font exception : leurs valeurs de type chaîne, même imbriquées, sont toujours renvoyées sous forme de texte ; voir Type JSON.

Types de date et d’heure

ClickHouse stocke les valeurs DateTime et DateTime64 en interne sous forme d’horodatages Unix (en secondes ou en fractions de seconde écoulées depuis l’époque Unix). Le stockage se fait toujours en UTC, mais un fuseau horaire peut être associé aux colonnes et influer sur l’affichage et l’interprétation des valeurs. Lors de la lecture des valeurs DateTime, la propriété DateTime.Kind est définie selon le fuseau horaire de la colonne : Pour les colonnes dont le fuseau horaire n’est pas UTC, la valeur DateTime renvoyée représente l’heure locale dans ce fuseau horaire. Utilisez ClickHouseDataReader.GetDateTimeOffset() pour obtenir une valeur DateTimeOffset avec le décalage horaire correspondant :
Pour les colonnes sans fuseau horaire explicite (c’est-à-dire DateTime plutôt que DateTime('Europe/Amsterdam')), le driver renvoie une valeur DateTime avec Kind=Unspecified. L’heure est ainsi conservée exactement telle qu’elle est stockée, sans présumer du fuseau horaire. Si vous souhaitez tenir compte du fuseau horaire pour les colonnes qui n’en définissent pas explicitement, vous pouvez :
  1. Définir un fuseau horaire explicite pour vos colonnes : DateTime('UTC') ou DateTime('Europe/Amsterdam')
  2. Appliquer vous-même le fuseau horaire après la lecture.

Type JSON

Le type renvoyé pour les colonnes JSON dépend du paramètre JsonReadMode :
  • Binary (par défaut) : renvoie System.Text.Json.Nodes.JsonObject. Ce mode permet d’accéder aux données JSON de manière structurée, mais convertit les types ClickHouse spécialisés (comme les adresses IP, les UUID et les nombres décimaux de grande taille) en chaînes de caractères au sein de la structure JSON.
  • String : renvoie le JSON brut sous forme de string. Ce mode conserve la représentation JSON exacte fournie par ClickHouse. Il est utile si vous devez transmettre le JSON sans l’analyser ou gérer vous-même la désérialisation.
None est un troisième mode. Il lit les données de la même manière que Binary, mais n’envoie aucun paramètre serveur avec la requête : utilisez-le sur une connexion qui n’autorise pas la définition d’un tel paramètre. Un chemin déclaré dans le type de la colonne est un chemin typé ; tout autre chemin du document est un chemin dynamique. La différence apparaît lorsque la valeur est nulle. Un chemin typé figure toujours dans le JsonObject. S’il est déclaré comme Nullable(T) ou Dynamic, il est renvoyé sous forme de valeur nulle JSON, que la valeur stockée soit nulle ou que le document ne contienne pas ce chemin : ces deux cas sont indiscernables :
Lorsqu’un chemin absent est déclaré avec un type non nullable, il prend la valeur par défaut de ce type : JSON(x String) donne {"x":""} et JSON(x Int64) donne {"x":0}. Un chemin dynamique dont la valeur est nulle est entièrement omis de l’objet ; ContainsKey renvoie donc false pour ce chemin. La lecture de {"x":null} dans une colonne JSON sans type de chemin déclaré donne {}. Les chemins typés imbriqués créent leurs éléments parents : JSON(a.b Nullable(Int64)) produit {"a":{"b":null}} même pour un document vide.
C’est ainsi que le serveur lui-même représente ces valeurs : les modes Binary et String donnent désormais le même résultat. Avant la version 1.4.0, un chemin typé contenant une valeur nulle était omis du JsonObject ; {"x":null} était donc relu comme {} — et, pour un chemin imbriqué tel que JSON(a.b Nullable(Int64)), tout le sous-arbre a disparaissait.
Les chaînes de caractères situées aux feuilles d’une colonne JSON sont toujours renvoyées sous forme de texte, quelle que soit la valeur de ReadStringsAsByteArrays : JsonValue ne prend pas en charge les tableaux d’octets ; un byte[] serait donc représenté en base64. Cela vaut pour String et FixedString, y compris lorsqu’ils sont encapsulés dans LowCardinality, Nullable ou SimpleAggregateFunction, ainsi que pour les chaînes de caractères dans Array et Map, y compris les clés des maps.
Un tableau d’octets dont le lecteur JSON ne peut pas connaître le type est néanmoins représenté en base64 : un chemin typé Variant ou Dynamic contient une valeur dont le type n’est connu qu’au niveau de chaque ligne. Une chaîne dans Variant(Array(UInt8), String) est donc renvoyée encodée en base64. Ce comportement est identique pour les deux valeurs du paramètre.Si le type d’une clé de map JSON n’est pas exactement String — par exemple, Map(LowCardinality(String), String) — une NotSupportedException est levée.
ClickHouse accepte une colonne dans laquelle un chemin est déclaré à 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 : le serveur produit donc une ligne avec une clé en double : {"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 qui indique les deux chemins. Il en va de même lorsque la valeur est une Map, comme dans JSON(a Map(String, Int64)), si la ligne contient aussi un chemin dynamique a.b. Cela ne s’applique que si les deux chemins contiennent une valeur dans la ligne. Si l’un ne contient rien — une valeur nulle, un objet vide ou un sous-arbre dont toutes les valeurs sont nulles —, il cède la place à celui qui contient des données, quel que soit l’ordre dans lequel le serveur envoie les deux chemins. Avec des types Nullable, un seul des deux chemins est donc renseigné par ligne, et la lecture ne génère pas d’erreur : JSON(a Nullable(Int64), a.b Nullable(Int64)) donne {"a":5} et {"a":{"b":7}}, comme prévu. Lisez une telle colonne avec JsonReadMode.String pour obtenir le texte JSON tel que le serveur l’envoie, sans modification, y compris la clé en double. Définissez AllowDuplicateJsonKeys pour continuer à lire la colonne comme un JsonObject sans lever d’exception. Le driver conserve alors celle des deux valeurs qui apparaît en dernier dans la ligne et ignore l’autre : la lecture entraîne donc une perte de données. Ainsi, JSON(a Int64, a.b Int64) contenant {"a.b":7} est lu comme {"a":0}. Si un chemin contient une valeur et que son parent contient un scalaire ou un tableau, une exception est tout de même levée, car un sous-arbre ne peut être placé sous aucun des deux.

Type Map

Dans ClickHouse, un Map(K, V) est stocké sous la forme d’un Array(Tuple(K, V)) et peut contenir plusieurs entrées ayant la même clé. Ce n’est pas le cas d’un Dictionary : en mode par défaut, seule la dernière valeur associée à une clé répétée est conservée, et les paires précédentes sont supprimées. Le paramètre MapReadMode détermine la représentation utilisée :
  • Dictionary (par défaut) : renvoie un Dictionary<K, V>.
  • KeyValuePairs : renvoie un List<KeyValuePair<K, V>> dans l’ordre où le serveur a envoyé les paires. Toutes sont ainsi conservées, y compris celles dont la clé est répétée.
Le mode détermine le type utilisé par le framework pour une colonne Map ; il s’applique donc aussi à GetFieldValue<T>, aux types de schéma renvoyés par le driver et au mappage des propriétés POCO. Il s’applique dès qu’un type Map apparaît dans l’arborescence des types d’une colonne, notamment dans Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)) et Dynamic. Les deux représentations sont acceptées à l’écriture, quel que soit le mode ; voir l’écriture des Map.

Autres types

Pour chaque ligne, les valeurs des types Dynamic et Variant sont converties dans le type correspondant à leur type sous-jacent réel.

Types géométriques

Le type Geometry est un type Variant qui peut contenir n’importe quel type géométrique. Sa valeur est convertie dans le type correspondant.

Correspondance des types : écriture dans ClickHouse

Lors de l’insertion de données, le driver convertit les types .NET en types ClickHouse correspondants. Les tableaux ci-dessous indiquent les types .NET acceptés pour chaque type de colonne ClickHouse.

Types entiers


Types à virgule flottante


Type booléen


Types de chaînes de caractères


Types de date et d’heure

Valeurs hors plageLors de l’écriture en binaire, les valeurs Date, Date32, DateTime et DateTime32 hors de leur plage prise en charge déclenchent une ArgumentOutOfRangeException au moment de l’appel à Write. L’exception indique le type de la colonne et la plage prise en charge. Auparavant, ces valeurs pouvaient être tronquées sans avertissement lors de leur conversion en entier 32 bits, puis réinterprétées par le serveur, ce qui produisait des timestamps valides, mais erronés.
Le driver tient compte de DateTime.Kind lors de l’écriture des valeurs : Les valeurs DateTimeOffset conservent toujours l’instant exact. Exemple : DateTime UTC (instant préservé)
Exemple : DateTime non spécifié (heure locale)
Recommandation : pour un comportement aussi simple et prévisible que possible, utilisez DateTimeKind.Utc ou DateTimeOffset pour toutes les opérations sur les valeurs DateTime. Votre code se comportera ainsi de la même manière, quel que soit le fuseau horaire du serveur, du client ou de la colonne.

Paramètres HTTP et copie en masse : quelle différence ?

La liaison de paramètres HTTP et la copie en masse ne traitent pas de la même façon les valeurs DateTime Unspecified lors de l’écriture : La copie en masse connaît le fuseau horaire de la colonne cible et interprète correctement les valeurs Unspecified dans ce fuseau horaire. Les paramètres HTTP ne connaissent pas automatiquement le fuseau horaire de la colonne. Vous devez donc l’indiquer dans l’annotation de type SQL :

Types décimaux


Type JSON

Le paramètre JsonWriteMode détermine le comportement lors de l’écriture de données JSON :
  • String (par défaut) : accepte string, JsonObject, JsonNode ou tout autre objet. Toutes les entrées sont sérialisées avec System.Text.Json.JsonSerializer, puis envoyées sous forme de chaînes JSON pour analyse côté serveur. C’est le mode le plus souple et il ne nécessite pas d’enregistrer les types.
  • 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. Il faut appeler connection.RegisterJsonSerializationType<T>() avant de l’utiliser. Dans ce mode, l’écriture de valeurs string ou JsonNode lève ArgumentException.
Lorsqu’une colonne JSON comporte des indications de type (par exemple, JSON(id UInt64, price Decimal128(2))), le driver s’en sert pour sérialiser les valeurs en respectant exactement leurs types. La précision des types tels que UInt64, Decimal, UUID et DateTime64 est ainsi préservée, alors qu’elle serait perdue lors d’une sérialisation en JSON générique. Les POCO peuvent être écrits dans des colonnes JSON de deux manières, selon la valeur de JsonWriteMode : Mode String (par défaut) : les POCO sont sérialisés avec System.Text.Json.JsonSerializer. Il n’est pas nécessaire d’enregistrer les types. C’est la méthode la plus simple, qui fonctionne aussi avec les objets anonymes. Mode Binary : les POCO sont sérialisés au format JSON binaire du driver, avec une prise en charge complète des indications de type. Les types doivent être enregistrés avec connection.RegisterJsonSerializationType<T>() avant d’être utilisés. Ce mode permet de personnaliser la correspondance entre les propriétés et les chemins JSON à l’aide d’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 Binary.
  • [ClickHouseJsonIgnore] : exclut une propriété de la sérialisation. Fonctionne uniquement en mode Binary.
La correspondance entre les noms de propriétés et les indications de type des colonnes est sensible à la casse. Ainsi, la propriété UserId ne correspond qu’à une indication définie comme UserId, et non comme userid. Cela reflète le comportement de ClickHouse, qui permet à des chemins tels que userName et UserName de coexister en tant que champs distincts. Limitations (mode Binary uniquement) :
  • Les types POCO doivent être enregistrés sur la connexion à l’aide de connection.RegisterJsonSerializationType<T>() avant la sérialisation. La sérialisation d’un type non enregistré lève une exception ClickHouseJsonSerializationException.
  • Pour être correctement sérialisées, les propriétés de type dictionnaire ou tableau/liste nécessitent des indications de type dans la définition de la colonne. À défaut, utilisez le mode String.
  • Les valeurs nulles des propriétés POCO ne sont écrites que si 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 de type sont donc ignorées.
  • Les attributs ClickHouseJsonPath et ClickHouseJsonIgnore sont ignorés en mode String (ils ne fonctionnent qu’en mode Binary).

Autres types


Types géométriques


Non pris en charge en écriture


Gestion des types imbriqués

Les types imbriqués de ClickHouse (Nested(...)) se lisent et s’écrivent comme des tableaux.

Journalisation et diagnostic

Le client .NET de ClickHouse s’appuie sur les abstractions de Microsoft.Extensions.Logging pour proposer une journalisation légère, activée uniquement si nécessaire. Lorsqu’elle est activée, le driver émet des messages structurés sur les événements liés au cycle de vie des connexions, l’exécution des commandes, les opérations de transport et les insertions en masse. La journalisation est entièrement facultative : les applications qui ne configurent pas de logger continuent de fonctionner sans surcoût.

Démarrage rapide

Utilisation d’appsettings.json

Vous pouvez définir les niveaux de journalisation avec la configuration standard de .NET :

Utiliser une configuration en mémoire

Vous pouvez également définir dans le code le niveau de détail de la journalisation pour chaque catégorie :

Catégories et émetteurs

Le driver utilise des catégories distinctes pour vous permettre de régler le niveau de journalisation de chaque composant :

Exemple : diagnostiquer les problèmes de connexion

Les informations suivantes seront consignées dans le journal :
  • Le choix de la fabrique de clients HTTP (pool par défaut ou connexion unique)
  • La configuration du handler HTTP (SocketsHttpHandler ou HttpClientHandler)
  • Les paramètres du pool de connexions (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
  • Les paramètres de délai d’expiration (ConnectTimeout, Expect100ContinueTimeout, etc.)
  • La configuration SSL/TLS
  • Les événements d’ouverture et de fermeture des connexions
  • Le suivi de l’ID de session

Mode de débogage : traçage réseau et diagnostics

Pour faciliter le diagnostic des problèmes réseau, la bibliothèque du driver comprend un utilitaire permettant de tracer à bas niveau le fonctionnement interne du réseau .NET. Pour activer ce traçage, vous devez fournir une LoggerFactory avec le niveau défini sur Trace et définir EnableDebugMode sur true (ou l’activer manuellement à l’aide de la classe ClickHouse.Driver.Diagnostic.TraceHelper). Les événements seront consignés dans la catégorie ClickHouse.Driver.NetTrace. Avertissement : cette opération générera des logs extrêmement détaillés et nuira aux performances. Il est déconseillé d’activer le mode de débogage en production.

OpenTelemetry

Le driver prend en charge nativement le traçage distribué OpenTelemetry au moyen de l’API .NET System.Diagnostics.Activity. Une fois activé, il émet des spans pour les opérations de base de données. Ces spans peuvent être exportés vers des plateformes d’observability comme Jaeger ou ClickHouse lui-même (via l’OpenTelemetry Collector).

Activer le traçage

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

Attributs des spans

Chaque span comporte les attributs OpenTelemetry standard relatifs aux bases de données, ainsi que des statistiques sur les requêtes propres à ClickHouse, utiles pour le débogage.

Options de configuration

Configurez le comportement du traçage à l’aide de ClickHouseDiagnosticsOptions :
L’activation de IncludeSqlInActivityTags risque d’exposer des données sensibles dans vos traces. Utilisez cette option avec précaution en production.

Configuration TLS

Pour vous connecter à ClickHouse en HTTPS, vous pouvez configurer TLS/SSL de plusieurs manières.

Validation personnalisée des certificats

Dans les environnements de production qui nécessitent une logique de validation des certificats personnalisée, fournissez votre propre HttpClient avec un handler ServerCertificateCustomValidationCallback configuré :
Précautions à prendre si vous fournissez un HttpClient personnalisé
  • Décompression automatique : laissez AutomaticDecompression désactivé. Le driver décompresse lui-même les réponses : cette option est donc inutile. L’activer peut même poser problème côté requête : lors de l’envoi, le handler ajoute également tous les algorithmes de son masque à l’en-tête Accept-Encoding sortant, en plus de ceux annoncés par le driver. ClickHouse peut alors répondre avec un codec que vous n’avez pas demandé. Consultez 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.

Optimisation des performances

Cette section explique comment tirer le meilleur parti du client et quelles options ajuster pour optimiser ses performances en fonction de votre cas d’utilisation.

En bref

| Si vous | Faites ceci | |---|---|---| | Chargez des lignes dans des objets POCO | Utilisez QueryAsync<T> plutôt que MapTo<T> | | Effectuez des insertions volumineuses | Augmentez InsertOptions.BatchSize | | Exécutez une application console ou un worker qui effectue de nombreuses insertions | Activez le Server GC | | Lisez des résultats volumineux via le réseau | Gardez la compression des réponses activée (c’est le cas par défaut) | | Effectuez des insertions via une connexion rapide | Essayez InsertOptions.Compressor = null | | Effectuez de nombreuses insertions dans la même table | Utilisez UseSchemaCache ou ColumnTypes | | Lisez des résultats très volumineux | Augmentez ReadBufferSize |

Lecture : choisir le mode de matérialisation

Il existe trois façons de lire une ligne dans un résultat, et leur coût varie. Certaines impliquent une mise en boîte des valeurs, ce qui augmente les allocations mémoire et réduit les performances. Pour la lecture de 1 000 000 de lignes comportant 105 colonnes du jeu de données hits :
Les ORM empruntent le chemin d’exécution optimisé lorsqu’ils utilisent des accesseurs typés. linq2db enregistre GetInt64, GetDouble et GetDateTime pour chaque colonne, ce qui permet de lire les valeurs sans mise en boîte. Le code qui passe par GetValue (y compris pour un résultat dynamic de Dapper) met chaque valeur en boîte. Si une requête ORM est exécutée fréquemment et passe par GetValue, utilisez QueryAsync<T> pour cette requête uniquement.

Insertion : taille des lots et parallélisme

La taille des lots est le facteur qui influe le plus sur le débit des insertions. Par défaut, InsertOptions.BatchSize est fixé à 100 000 lignes. Utilisez de grands lots. Pour une insertion de 1 000 000 de lignes, le passage de 10 000 à 100 000 lignes par lot a donné les résultats suivants : Si vous ne pouvez pas maîtriser la taille des lots (par exemple, lorsque de nombreux petits producteurs envoient des lignes séparément), utilisez les insertions asynchrones et laissez le serveur regrouper les lignes en lots. Téléversements parallèles. Par défaut, InsertOptions.MaxDegreeOfParallelism est fixé à 1. Augmentez cette valeur pour envoyer plusieurs lots simultanément. Le gain est particulièrement important lorsque la compression est activée, car chaque lot est alors compressé dans son propre thread. Les sessions sont incompatibles avec les insertions parallèles : désactivez-les ou conservez MaxDegreeOfParallelism = 1. Supprimez la requête d’inspection du schéma. Chaque appel à InsertBinaryAsync envoie d’abord une requête SELECT ... WHERE 1=0 pour déterminer les types des colonnes. Consultez Éviter la requête d’inspection du schéma pour éviter cet aller-retour grâce à ColumnTypes ou à UseSchemaCache.
Le chemin d’insertion sans mise en boîte s’applique au format RowBinary par défaut. RowBinaryWithDefaults doit examiner chaque valeur pour repérer le marqueur DBDefault ; il emprunte donc le chemin plus lent.

Compression : les résultats diffèrent selon le sens du transfert

La compression réduit le volume de données transférées au prix d’une consommation accrue de CPU. Son intérêt dépend du sens du transfert, du débit de votre connexion au serveur ClickHouse, de la façon dont vos données se prêtent à l’algorithme de compression choisi et du coût éventuel de chaque octet transféré. Lectures : laissez la compression activée, sauf si le serveur s’exécute sur la même machine. Elle est activée par défaut. Par rapport à un transfert sans compression, zstd au niveau 1 a donné les résultats suivants : Insertions : mesurez les performances avant d’activer la compression. Les gains peuvent être insuffisants pour la justifier. N’oubliez pas non plus que la décompression accroît la charge du serveur : cette charge est modérée avec Zstd et LZ4, mais peut être élevée avec d’autres algorithmes, comme Brotli. Pour désactiver la compression des insertions :
Pour choisir un codec, régler les niveaux de compression et trouver votre propre seuil de bascule, consultez Optimiser la compression.

Tampons

ReadBufferSize définit la taille du tampon utilisé pour lire les réponses HTTP. Par défaut, cette taille est de 64 Kio. Le driver emprunte ce tampon à un pool partagé et le restitue lorsqu’il libère le lecteur : il n’effectue donc pas d’allocation à chaque requête. Augmentez cette taille pour limiter les remplissages du tampon lorsque les résultats sont volumineux. Le driver conserve un tampon par lecteur ouvert simultanément : la consommation de mémoire augmente donc avec la taille du tampon et le nombre de lecteurs ouverts en parallèle.
Libérez toujours les lecteurs. Lorsqu’un lecteur est libéré, il restitue son tampon au pool et ferme sa connexion HTTP. Si vous abandonnez un lecteur sans le libérer, son tampon n’est pas restitué au pool et sa connexion HTTP peut rester indisponible ; la récupération automatique de la mémoire ne remplace pas une libération explicite.

Environnement d’exécution et ramasse-miettes

Activez Server GC pour les applications qui effectuent de nombreuses insertions. À code identique et à nombre d’octets alloués égal, les insertions étaient jusqu’à 97 % plus lentes avec Workstation GC qu’avec Server GC.
Les projets ASP.NET Core activent 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 différence tient à la taille du budget de génération 0. Le Workstation GC utilise un budget réduit : les tampons temporaires créés lors d’une insertion ne sont donc pas libérés en génération 0. Ils passent en génération 1, ce qui multiplie les promotions et accroît considérablement le travail en génération 2. Dans un cas d’insertion, on comptait 4 000 collectes de génération 2 pour 1 000 opérations avec le Server GC, contre 73 000 avec le Workstation GC.
Le Server GC est un paramètre qui privilégie le débit, pas la latence. Lors de ces mêmes mesures, le temps total passé en pause était plus de deux fois moindre avec le Server GC, mais chaque pause était plus longue (114,6 ms au 95e percentile, contre 61,9 ms). Si votre service est sensible à la latence des requêtes les plus lentes, mesurez les deux modes avant de faire votre choix.

Latence : réutiliser les connexions

L’établissement d’une nouvelle connexion TCP et la négociation TLS prennent beaucoup de temps. Réutiliser les connexions réduit considérablement la latence de vos requêtes.
  • Ne créez pas de client pour chaque requête. Chaque nouveau client disposant de son propre HttpClient crée un nouveau pool de connexions et doit répéter la négociation TLS. Utilisez un seul ClickHouseClient pendant toute la durée de vie de l’application. Il peut être utilisé simultanément par plusieurs threads et est conçu pour être utilisé comme singleton.
  • Pour ADO.NET et les ORM, utilisez ClickHouseDataSource afin que toutes les connexions partagent le même pool.
Pour découvrir toutes les approches possibles, consultez Durée de vie des connexions et pool de connexions.

Mesurez les performances par vous-même

Dans bien des cas, les performances dépendent de la structure de vos données, du débit de votre connexion au serveur, de la façon dont vous répartissez la charge CPU entre le client et le serveur, des limites de votre matériel, etc. Il est donc recommandé de mesurer les performances avec vos propres données, dans votre environnement. Pour connaître la part du travail effectuée par le serveur, définissez QueryOptions.QueryId et récupérez les compteurs :

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 driver convertit automatiquement la syntaxe @parameter de Dapper en syntaxe native {parameter:Type} de ClickHouse et déduit les types des valeurs .NET. Utilisez ClickHouseDataSource pour gérer correctement le cycle de vie des connexions :

Modes de passage des paramètres

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

Requêtes avec des POCO

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

Syntaxe des paramètres native de ClickHouse

Si vous devez définir explicitement le type d’un paramètre, utilisez directement la syntaxe {param:Type} de ClickHouse dans le code SQL, avec un Dictionary<string, object> contenant les valeurs des paramètres. N’utilisez pas à la fois les syntaxes @param et {param:Type} pour un même paramètre.

WHERE IN

L’expansion native de la clause IN par Dapper fonctionne :
Dapper transforme cette expression en WHERE id IN (@Ids1, @Ids2, @Ids3), puis le driver convertit chaque paramètre issu de cette expansion. La fonction has() de ClickHouse fonctionne aussi avec un paramètre Array :

Handlers de type personnalisés

Certains types ClickHouse, comme ITuple, BigInteger et ClickHouseDecimal, nécessitent l’enregistrement de handlers au démarrage :
Consultez l’exemple Dapper pour découvrir une implémentation de type handler.

Dapper.Contrib

GetAll<T>() et Get<T>(id) fonctionnent. En revanche, Insert<T>() ne fonctionne pas : cette méthode génère une syntaxe propre à SQL Server (SCOPE_IDENTITY, []). Utilisez plutôt la méthode native InsertBinaryAsync de ClickHouseClient.
Les noms des propriétés doivent correspondre exactement aux noms des colonnes ClickHouse (la casse doit être respectée).

Limitations

Linq2db

Ce driver est compatible avec linq2db, un ORM léger et un fournisseur LINQ pour .NET. Pour plus de détails, consultez la documentation sur le site du projet. Exemple d’utilisation : Créez une DataConnection à l’aide du fournisseur ClickHouse :
Les correspondances entre classes et tables peuvent être définies à l’aide d’attributs ou d’une configuration fluide. Si les noms de vos classes et de leurs propriétés correspondent exactement aux noms des tables et des colonnes, aucune configuration n’est nécessaire :
Requêtes :
Copie en masse : Utilisez BulkCopyAsync pour effectuer des insertions en masse efficacement.

Entity Framework Core

Le fournisseur officiel Entity Framework Core pour ClickHouse. Associez des classes C# à des tables ClickHouse, interrogez-les avec LINQ et insérez des données via SaveChanges — le tout selon les pratiques habituelles d’EF Core.
Ce fournisseur est activement développé. La version actuelle prend en charge les requêtes LINQ (y compris les jointures, les sous-requêtes et les opérations ensemblistes), les insertions avec INSERT via SaveChanges / BulkInsertAsync, les migrations avec une prise en charge complète du DDL (CREATE / ALTER / DROP) et la configuration des table engines spécifiques à 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 votre DbContext, puis effectuez une requête avec LINQ :

Types pris en charge

Utilisez ClickHouseDecimal (de ClickHouse.Driver.Numerics) plutôt que decimal si vous avez besoin de la pleine précision des colonnes Decimal128/Decimal256 : le type decimal de .NET est limité à 28 ou 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égations : GroupBy avec Count, LongCount, Sum, Average, Min, Max — y compris HAVING (.Where() après .GroupBy()), plusieurs agrégations dans une même projection et OrderBy sur les résultats agrégés. Jointures : Join (INNER), combinaisons GroupJoin/SelectMany (LEFT et CROSS). Une jointure LEFT JOIN renvoie 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 sur les ensembles : Concat (→ UNION ALL), Union (→ UNION DISTINCT), Intersect, Except. Collections locales intégrées : les jointures et les appels à Contains portant sur des collections en mémoire (int[], List<T>, etc.) sont traduits en une série d’opérations UNION. Méthodes sur les chaînes de caractères : 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 fonctions ClickHouse équivalentes — fonctions arithmétiques, logarithmiques, trigonométriques et utilitaires. Le fournisseur ajoute automatiquement set_join_use_nulls=1 à chaque chemin de connexion afin que le comportement des jointures corresponde aux attentes d’Entity Framework. Si votre serveur ClickHouse ou votre profil interdit la modification de ce paramètre (par exemple, avec un profil readonly=1), désactivez cet ajout avec :
Lorsque l’option de désactivation est activée, LEFT JOIN renvoie les valeurs par défaut des colonnes ClickHouse, et la détection des propriétés de navigation par EF, qui repose sur les valeurs nulles, ne fonctionne plus comme prévu. Effectuez des comparaisons explicites avec 0 / "" plutôt qu’avec == null.

Insertion de données

SaveChanges utilise l’API native InsertBinaryAsync du driver : un encodage RowBinary avec un corps de requête compressé, bien plus efficace que des requêtes SQL paramétrées :
Les entités passent de l’état Added à l’état Unchanged après l’enregistrement, comme avec n’importe quel autre fournisseur EF Core. La taille des lots est configurable (1 000 par défaut) :

Insertion en masse

Pour les insertions à haut débit, utilisez BulkInsertAsync plutôt que SaveChanges. Cette méthode d’extension de DbContext évite complètement le suivi des modifications, la résolution des identités et la gestion des états d’EF Core : elle appelle directement la méthode InsertBinaryAsync du driver, avec un encodage RowBinary et un corps de requête compressé. Elle convient donc au chargement de grands jeux de données lorsque vous n’avez pas besoin de suivre les entités après leur insertion :
L’entrée peut être n’importe quel IEnumerable<T> : les entités sont parcourues au fil de l’eau, sans être toutes chargées en mémoire. La valeur renvoyée correspond au nombre de lignes insérées. Après l’insertion, les entités ne sont pas rattachées au DbContext : leur état ne passe donc pas de Added à Unchanged.

Énumérations

Les colonnes ClickHouse Enum8/Enum16 peuvent être associées à des propriétés de type string ou à des types enum C#. Avec les énumérations C#, le fournisseur assure automatiquement la conversion entre l’énumération et sa représentation sous forme de chaîne :

Conversions de types personnalisés

Le système ValueConverter d’EF Core permet de convertir vos types personnalisés en types déjà pris en charge par le fournisseur. Celui-ci n’a jamais accès à votre type personnalisé : EF Core effectue la conversion avant de lui transmettre la valeur. 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 indiquer explicitement le type ClickHouse. À l’aide d’annotations de données (attributs) :
Utilisation de l’API fluide dans OnModelCreating :
Les wrappers imbriqués tels que Array(Nullable(Int32)) et LowCardinality(Nullable(String)) sont pris en charge — le fournisseur retire automatiquement les couches Nullable et LowCardinality à chaque niveau d’imbrication.

Colonnes Variant et Dynamic

Les colonnes ClickHouse Variant(T1, T2, ...) et Dynamic sont représentées par le type object en .NET. Comme object est trop générique pour que le type soit déduit automatiquement, vous devez déclarer explicitement le type de stockage à l’aide de .HasColumnType() :
À la lecture, la valeur est automatiquement désérialisée en un objet du type .NET correspondant au discriminateur stocké (par exemple, string, ulong, ulong[]).

Colonnes JSON

Le fournisseur prend en charge le type de colonne Json de ClickHouse, avec une correspondance vers System.Text.Json.Nodes.JsonNode (type principal) ou string (au moyen d’un ValueConverter automatique) :
La lecture et l’écriture de données JSON sont prises en charge aussi bien par SaveChanges que par BulkInsertAsync :
Si vous préférez les chaînes JSON brutes, faites correspondre la propriété au type string avec un type de colonne Json : le fournisseur applique automatiquement un ValueConverter :
  • Pas de traduction des chemins JSON — L’expression entity.Data["name"] dans LINQ n’est pas convertie en syntaxe SQL data.name de ClickHouse. Appliquez les filtres aux colonnes non JSON et examinez les données JSON en mémoire.
  • Sémantique des valeurs NULL — Le type JSON de ClickHouse renvoie {} (un objet vide) pour les valeurs NULL, et non un NULL SQL.
  • Précision des entiers — Le type JSON de ClickHouse stocke tous les entiers sous forme de Int64. Lors de la lecture avec JsonNode, utilisez GetValue<long>() plutôt que GetValue<int>().

Moteurs de table

Configurez les moteurs des tables ClickHouse et les clauses propres à chaque moteur au moyen de l’API fluide ToTable(name, t => ...). Si aucun moteur n’est configuré, le fournisseur utilise MergeTree par défaut, avec une clause ORDER BY définie à partir de la clé primaire de l’entité.
Familles de moteurs prises en charge : Clauses du moteur : WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. Elles s’ajoutent toutes au constructeur de moteur renvoyé par HasXxxEngine(). Fonctionnalités au niveau des colonnes : HasCodec, HasTtl, HasComment, HasDefault — elles sont toutes prises en compte dans les migrations. Index de saut de données — avec HasIndex(...).HasSkippingIndexType(...) :
Les index standard (qui ne permettent pas de sauter des blocs de données) sont ignorés sans avertissement, car ClickHouse ne dispose d’aucun équivalent. Les index uniques lèvent une exception, car ClickHouse n’impose pas l’unicité.

Migrations

Flux de travail standard des migrations avec EF Core :
Opérations prises en charge :

Limites des migrations

Outre ces limites de migration, le fournisseur ne prend pas encore en charge :
  • UPDATE / DELETE
  • Transactions : BeginTransaction est une opération sans effet. Les transactions ACID ne sont pas prises en charge dans ClickHouse.
  • Traduction des requêtes sur les chemins JSON : l’expression LINQ entity.Data["key"] n’est pas traduite en syntaxe SQL data.key de ClickHouse. Filtrez sur des colonnes non JSON et examinez les données JSON en mémoire.

Limitations

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

En C#, les types ValueTuple de plus de 7 éléments utilisent une imbrication générée 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>>. Une ambiguïté apparaît lorsque la colonne ClickHouse contient 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 driver ne peut pas distinguer :
  • Un tuple plat de 9 éléments (avec une imbrication de TRest générée par le compilateur)
  • Un tuple de 8 éléments dont le dernier est un Tuple(String, String) imbriqué
Les deux correspondent au même type .NET : ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>. Le driver interprète le 8e argument comme TRest et l’aplatit. Par conséquent, un tuple de 8 éléments dont le dernier est un tuple imbriqué sera sérialisé de manière incorrecte. Cela concerne aussi bien System.Tuple que ValueTuple, car tous deux utilisent l’imbrication de TRest au-delà de 7 éléments. Les tuples de 7 éléments ou moins, ainsi que ceux dont le dernier élément n’est pas lui-même un tuple, ne sont pas concernés. Solution de contournement : Placez le tuple interne dans un niveau d’imbrication supplémentaire pour que le driver puisse le distinguer de l’imbrication de TRest :

Colonnes de type AggregateFunction

Les colonnes de type AggregateFunction(...) ne peuvent pas être interrogées directement ni recevoir de données par insertion directe. Pour insérer des données :
Pour sélectionner :

Dernière modification le 14 octobre 2026