Skip to main content
Les fonctions définies par l’utilisateur (UDF) permettent d’étendre le comportement de ClickHouse au-delà de ce que permettent les plus de mille fonctions intégrées. Dans ClickHouse Cloud, il existe plusieurs façons de créer et de gérer des fonctions définies par l’utilisateur :
  1. À l’aide de SQL
  2. À l’aide de la console et de votre propre code
  3. À l’aide de la Cloud API (Beta)
  4. À l’aide de Terraform (Beta)
Les UDF exécutables sont disponibles sur AWS, GCP et Azure. Elles sont activées pour toutes les organisations, sans aucune action de votre part. Elles ne sont pas facturées séparément : les processus UDF s’exécutent dans les pods du service et consomment les ressources CPU et mémoire de ce dernier.

Fonctions SQL définies par l’utilisateur

Les UDF SQL peuvent être créées à l’aide de l’instruction CREATE FUNCTION à partir d’une expression lambda. Dans cet exemple, nous allons créer une simple fonction exécutable définie par l’utilisateur, isBusinessHours. La fonction vérifiera si un timestamp donné correspond aux heures ouvrées habituelles et renverra true si c’est le cas, sinon false.
  1. Connectez-vous à Cloud Console et ouvrez la console SQL
  2. Écrivez la SQL query suivante pour créer la fonction isBusinessHours :
  1. Exécutez ce qui suit pour tester l’UDF que vous venez de créer :
Vous devriez obtenir le résultat suivant :
  1. Vous pouvez utiliser la commande DROP FUNCTION pour supprimer l’UDF que vous venez de créer :
ImportantLes UDFs dans ClickHouse Cloud n’héritent pas des paramètres définis au niveau de l’utilisateur. Elles s’exécutent avec les paramètres système par défaut.
Cela signifie :
  • Les paramètres de session (définis via l’instruction SET) ne sont pas transmis au contexte d’exécution des UDFs
  • Les paramètres du profil utilisateur ne sont pas hérités par les UDFs
  • Les paramètres au niveau de la requête ne s’appliquent pas lors de l’exécution des UDFs

Fonctions définies par l’utilisateur exécutables

Les UDF exécutables exécutent votre code dans un processus isolé (sandbox), aux côtés de chaque réplique de votre service. Vous pouvez téléverser un script Python, qui s’exécute sur le runtime python3.11, ou un binaire précompilé, qui s’exécute sur le runtime Native. Dans cet exemple, nous allons créer la même fonction définie par l’utilisateur exécutable simple, isBusinessHours, qui vérifie si un timestamp donné se situe dans les heures ouvrées habituelles. Nous l’avons précédemment créée en SQL, mais cette fois, nous allons la créer en Python et la configurer via la console.
1

Créez le fichier Python

Créez localement un nouveau fichier main.py :
L’environnement d’exécution Python est python3.11. Si votre script Python importe des paquets tiers, indiquez-les dans un fichier requirements.txt : ClickHouse Cloud les installera pour vous, pour les deux architectures du CPU sur lesquelles ClickHouse Cloud fonctionne (x86-64 et ARM64). Vous pouvez aussi intégrer les dépendances directement dans l’archive ZIP, mais vous devez alors y inclure les paquets mis en cache pour les deux architectures du CPU ; il est donc plus simple d’utiliser requirements.txt. Par exemple :
ClickHouse Cloud s’attend à trouver main.py dans le fichier zip que vous téléverserez via la console à l’étape suivante. Si vous nommez le fichier autrement, une erreur se produira.
2

Regrouper les dépendances et les fichiers locaux

Pour ce tutoriel, qui ne comporte ni dépendances ni fichiers supplémentaires, l’archive n’a besoin de contenir que main.py :
Pour inclure des paquets de dépendances et tout autre fichier local (par exemple requirements.txt, des fichiers wheel, des fichiers de configuration ou des fichiers de données), placez-les dans le même répertoire que votre fichier main.py, puis exécutez zip depuis ce répertoire afin que main.py se trouve à la racine de l’archive :
Vous pouvez faire référence au répertoire de base du path local de l’archive dans votre code Python à l’aide de os.path.dirname(os.path.abspath(__file__)). Cette expression renvoie le path absolu du répertoire contenant votre fichier main.py dans l’archive ZIP, ce qui vous permet d’accéder aux autres fichiers qu’elle contient :
Cela s’avère utile lorsque vous devez :
  • Accéder aux fichiers de configuration fournis avec votre UDF
  • Charger des paquets wheel pour des dépendances personnalisées
  • Référencer des scripts ou des fichiers de données supplémentaires
Les liens symboliques ne sont pas autorisésClickHouse Cloud rejette les archives UDF contenant des liens symboliques. Assurez-vous que votre archive ZIP ne contient que des fichiers et des répertoires ordinaires : tout téléversement comportant des liens symboliques échouera lors de la validation.
3

Créez une UDF depuis la console

  1. Depuis la page d’accueil de la Cloud console, cliquez sur le nom de votre organisation dans le menu en bas à gauche.
  2. Sélectionnez User-defined functions dans le menu.
  3. Sur la page des fonctions définies par l’utilisateur, cliquez sur Set up a UDF. Un panneau de configuration s’ouvre sur la droite de l’écran.
  4. Saisissez un nom de fonction. Pour cet exemple, utilisez isBusinessHours.
  5. Choisissez un type de fonction, Executable pool ou Executable :
    • Executable pool : un pool de processus de longue durée est démarré une seule fois, puis réutilisé d’un bloc et d’une requête à l’autre. Recommandé pour la quasi-totalité des charges de travail.
    • Executable : un nouveau processus est démarré pour chaque bloc de données traité. N’utilisez ce mode que si le processus ne doit conserver aucun état entre les appels.
  6. Sélectionnez un Runtime type. Pour cet exemple, conservez python3.11. Pour téléverser plutôt un binaire précompilé, sélectionnez Native ; consultez Runtime natif.
  7. Pour cet exemple, conservez les paramètres par défaut. Pour la liste complète des paramètres de configuration et de leurs valeurs par défaut dans Cloud, consultez Référence de configuration.
  8. Cliquez sur Browse File pour téléverser le fichier .zip créé au début de ce tutoriel.
  9. Ajoutez un nouvel argument. Pour cet exemple, ajoutez un argument timestamp de type DateTime.
  10. Sélectionnez un type de retour. Pour cet exemple, sélectionnez Bool.
  11. Cliquez sur Create UDF. Une boîte de dialogue affiche l’état d’avancement de la construction.
    • En cas de problème, le statut passe à error.
    • Sinon, le statut passe de building à provisioning. Votre service doit être actif pour que le provisionnement puisse aboutir. S’il est en veille, cliquez sur Wake Up Service dans le panneau UDF details, à côté du nom du service.
    • Une fois l’opération terminée, le statut passe à deployed.
4

Testez votre UDF

  1. revenez à la page d’accueil de la SQL Console en cliquant sur Settings - return to your service view en haut à gauche de la page
  2. cliquez sur SQL Console dans le menu de gauche
  3. saisissez la requête suivante :
Vous devriez obtenir le résultat suivant :
5

Créer une nouvelle version

Pour modifier le code ou les paramètres d’une UDF, créez une nouvelle version. Les versions sont immuables. Le panneau Edit sert uniquement à gérer les services auxquels une UDF est assignée ; téléverser un fichier depuis ce panneau ne remplace pas le code déployé.
  1. Depuis la page d’accueil de la Cloud console, cliquez sur le nom de votre organisation dans le menu en bas à gauche.
  2. Sélectionnez User-defined functions dans le menu.
  3. Cliquez sur les trois points sous Actions pour l’UDF isBusinessHours, puis cliquez sur Create new version
  4. Téléversez une archive zip contenant le code modifié ou modifiez les paramètres, sélectionnez les services qui doivent recevoir la nouvelle version, puis cliquez sur Create new version
Une nouvelle version n’hérite pas des services rattachés à la version précédente. Le panneau de création de version comporte un sélecteur de services : vous pouvez ainsi tester une version sur un seul service, puis la déployer ou revenir en arrière service par service. Un service ne peut héberger qu’une seule version d’une fonction à la fois.Pour les UDF executable_pool, les processus persistants du pool continuent d’exécuter la version précédente tant que le pool n’a pas été rechargé. Utilisez l’action Reload UDF à côté du service dans le panneau de l’UDF. Elle exécute SYSTEM RELOAD FUNCTION ON CLUSTER default isBusinessHours sur ce service.Vous avez ajouté votre première fonction définie par l’utilisateur via la console, vérifié qu’elle s’exécute correctement et découvert comment en créer une nouvelle version si nécessaire.

Runtime Native (binaires compilés)

Le runtime Native vous permet de téléverser un binaire précompilé au lieu d’un script Python. Définissez Type de runtime sur Native lors de la création de l’UDF (runtime: native dans l’API). Rien n’est installé pour vous dans le sandbox : tout ce dont le binaire a besoin doit être intégré lors de la compilation. Actuellement pris en charge : Rust, Go, C++ et JavaScript compilé en binaire avec Bun. Bun a été testé avec les versions 1.4.0 et 1.4.2 ; le fonctionnement avec d’autres versions n’est pas garanti. L’archive doit contenir un exécutable par architecture du CPU, chacun dans son propre dossier :
Les deux architectures sont obligatoires, car les services ClickHouse Cloud s’exécutent sur l’une comme sur l’autre. L’exécutable doit être nommé exactement main et est lancé sans argument ; transmettez plutôt un mode ou une sous-commande sous la forme d’un argument de fonction ordinaire. Les binaires liés statiquement sont vivement recommandés : ils ne dépendent pas des bibliothèques système de l’hôte, dont les appels système peuvent varier d’une version de serveur à l’autre, et la politique d’appels système du sandbox est optimisée pour eux. Les binaires liés dynamiquement ne fonctionnent que s’ils sont liés exclusivement à glibc et ont été compilés avec glibc 2.35 ou une version antérieure. Le JavaScript compilé avec Bun est toujours lié dynamiquement.
Résoudre les fichiers de données par rapport à l’exécutableLes fichiers du dossier d’architecture sont déployés à côté du binaire, mais le processus ne démarre pas dans ce répertoire. Résolvez-les par rapport au path de l’exécutable lui-même, et jamais par rapport au répertoire de travail : en Go, utilisez filepath.Dir(os.Args[0]) ou os.Executable() ; en Rust, utilisez std::env::current_exe(). Si un fichier obligatoire est manquant, écrivez un message sur stderr et quittez avec un code de sortie non nul plutôt que de basculer silencieusement vers un comportement de repli.
Les UDF natives ne peuvent pas disposer d’un accès réseau ; l’accès réseau sortant est réservé au runtime Python. Consultez Accès réseau. Compilez le binaire pour les deux architectures. En Rust, ajoutez une fois pour toutes les deux cibles, puis compilez pour chacune d’elles :
En Go :
Copiez chaque binaire produit dans amd64/main et arm64/main avant de compresser les dossiers au format zip. Le wire protocol est identique à celui du runtime Python : le processus lit les arguments depuis stdin et écrit les résultats sur stdout dans le format configuré. Consultez Formats et en-têtes de fragments. Le dépôt llm-token-udf contient des UDF natives en Rust et en Go, accompagnées de scripts de construction.

Référence de configuration

Les paramètres suivants sont exposés dans la console et dans l’API. Les valeurs par défaut indiquées sont celles de ClickHouse Cloud et diffèrent par endroits de celles de la version open source. Les paramètres open source suivants sont gérés par ClickHouse Cloud et ne peuvent pas être modifiés UDF par UDF : command_termination_timeout, lifetime, stderr_reaction, check_exit_code et execute_direct. La valeur par défaut open source de pool_size est 16, contre 3 dans Cloud. La liste complète des paramètres open source figure dans Fonctions définies par l’utilisateur exécutables. Les noms de fonction doivent respecter le motif ^[A-Za-z][A-Za-z0-9_]*$, sont uniques au sein de l’organisation et ne peuvent pas reprendre le nom d’une fonction intégrée de ClickHouse ; l’API rejette les noms réservés. Chaque argument doit porter un nom respectant le même motif ; l’API et Terraform l’exigent quel que soit le format. Les archives contenant des liens symboliques sont rejetées.

Formats et en-têtes de fragment

TabSeparated est le format par défaut et le plus simple. Il impose l’échappement des tabulations, des sauts de ligne et des antislashs des deux côtés. RowBinary prend en charge les données binaires sans risque et évite le formatage et l’analyse de texte côté ClickHouse ; privilégiez-le pour les chaînes de caractères volumineuses. Lorsque Send chunk header est activé, ClickHouse écrit avant chaque fragment le nombre de lignes sous forme de texte décimal suivi de \n, puis les lignes dans le format configuré. Le processus doit lire exactement ce nombre de lignes, écrire autant de résultats, puis effectuer un seul vidage. Sans cette option, un processus qui lit un format orienté lignes tel que TabSeparated, RowBinary ou JSONEachRow et qui met sa sortie en tampon n’a aucun moyen de savoir où se termine un bloc, et la requête reste bloquée jusqu’à l’expiration du délai de lecture. Activez-la pour toute UDF de ce type qui n’effectue pas de vidage après chaque ligne. Le format Native intègre déjà les limites de bloc : une UDF Native peut donc lire un bloc, écrire un bloc et effectuer un vidage sans recourir à l’en-tête.

Accès réseau

L’accès réseau n’est disponible que pour le runtime Python ; il ne peut pas être activé pour les UDF natives. Les UDF Python créées dans la console sont générées avec l’accès réseau activé, et aucun paramètre de la console ne permet de le modifier. Dans l’API, sandboxType permet de choisir entre netenable, qui autorise les connexions sortantes, et basic, qui les interdit ; le Terraform provider propose le même choix via sandbox_type. L’accès réseau autorise les connexions sortantes vers des endpoints accessibles sur l’Internet public. Les connexions vers des réseaux privés, des adresses link-local et des adresses internes sont bloquées. Le sandbox ne dispose d’aucune identity cloud : ni rôle d’instance, ni endpoint de métadonnées, ni variables d’environnement héritées. Une UDF ne dispose donc que des identifiants que vous lui fournissez. L’accès réseau est appliqué lors de la génération d’une version d’UDF : pour une version existante créée sans cet accès, il faut donc générer une nouvelle version. Il n’existe pas encore de magasin de secrets pour les UDF. Incluez un fichier de configuration dans l’archive zip et lisez-le via un chemin relatif à __file__ ; ne placez jamais de clés dans le code SQL.

Limite de mémoire

Limite de mémoire définit la quantité maximale de mémoire, en MiB, dont dispose chaque processus du sandbox. Si elle n’est pas définie, la valeur par défaut du sandbox, soit 4 GiB, s’applique. La limite porte sur l’espace d’adressage virtuel, et non sur la mémoire résidente : les environnements d’exécution qui réservent de vastes plages d’adresses ont donc besoin d’une marge nettement supérieure à leur utilisation réelle. Go, par exemple, réserve bien plus de 1 GiB d’espace d’adressage au démarrage. Lorsqu’un processus dépasse la limite, l’allocation échoue au sein même du processus : une UDF Python se termine avec une trace d’appels MemoryError, et la requête échoue en incluant cette sortie stderr dans le message d’erreur.

Fonctions déterministes et cache de requêtes

Par défaut, une UDF est considérée comme non déterministe. Une requête qui en appelle une avec use_query_cache = 1 échoue avec l’erreur QUERY_CACHE_USED_WITH_NONDETERMINISTIC_FUNCTIONS, sauf si le paramètre query_cache_nondeterministic_function_handling est modifié. N’activez déterministe que si la fonction renvoie toujours le même résultat pour les mêmes arguments. Le cache de requêtes peut alors stocker et servir les résultats des requêtes qui l’appellent. Les autres fonctions non déterministes présentes dans la même requête, telles que now, today ou rand, continuent d’empêcher la mise en cache avec le comportement par défaut throw ; avec query_cache_nondeterministic_function_handling = 'save', ces requêtes sont tout de même mises en cache, tandis qu’avec 'ignore', elles sont exécutées sans mise en cache.

Redémarrages du service

La première association redémarre le serviceRattacher la première UDF à un service ajoute un conteneur auxiliaire aux pods du service et déclenche un redémarrage progressif. L’association d’UDF supplémentaires, en revanche, n’en déclenche pas. Supprimer la dernière UDF d’un service déclenche un nouveau redémarrage progressif. Planifiez donc la première association en conséquence.

Supervision

À partir de ClickHouse 26.6, l’utilisation des ressources par les UDF exécutables est enregistrée dans huit compteurs, tous préfixés par ExecutableUserDefinedFunction. Par requête, ils apparaissent sous forme d’entrées dans la colonne de type map ProfileEvents de system.query_log. À l’échelle du serveur, ils apparaissent sous forme de lignes dans system.events, avec des valeurs cumulées depuis le démarrage du serveur, ainsi que sous forme de colonnes ProfileEvent_<name> dans system.metric_log, avec des valeurs échantillonnées au fil du temps. Les suffixes des compteurs sont les suivants : system.asynchronous_metrics contient ExecutableUserDefinedFunctionProcesses et ExecutableUserDefinedFunctionMemoryResidentBytes. Cette dernière correspond à la somme des valeurs VmRSS de tous les processus UDF actifs et constitue une borne supérieure, car les pages partagées sont comptabilisées une fois pour chaque processus.
system.query_log.memory_usage ne prend en compte que le processus du serveur ClickHouse et n’inclut pas les processus des UDF.

Recommandations de performance

Utilisez executable_pool. Avec executable, un nouveau processus est lancé pour chaque bloc de données : le coût de démarrage est donc payé à chaque bloc. Chaque invocation a un coût fixe. Si ExecutableUserDefinedFunctionInvocations est très élevé par rapport au nombre de lignes traitées, augmentez preferred_block_size_bytes pour cette requête afin que chaque bloc contienne davantage de lignes. Une même requête utilise au maximum max_threads processus du pool simultanément ; une taille de pool supérieure à max_threads n’apporte donc aucun gain à cette requête. Commencez avec une taille réduite et augmentez-la lorsque ExecutableUserDefinedFunctionPoolWaitMicroseconds devient non négligeable. Chaque ligne franchit une frontière de processus. Les UDF sont destinées à la logique que SQL ne peut pas exprimer, et non aux traitements triviaux par ligne qu’une fonction intégrée ou une UDF SQL peut prendre en charge.

Dépannage

Une UDF qui échoue fait échouer la requête avec le code d’erreur 754 (UDF_EXECUTION_FAILED). La sortie stderr du processus enfant étant incluse dans le message, écrivez-y vos diagnostics et terminez avec un code de sortie non nul :
Cas courants :
  • La requête se bloque, puis échoue sur un timeout : le processus a mis sa sortie en tampon dans un format orienté lignes sans Send chunk header. Activez cette option et effectuez un vidage à chaque fragment, ou à chaque bloc si vous utilisez le format Native.
  • Code de retour 1 avec une trace d’appels MemoryError (Python) : la limite de mémoire a été dépassée. Augmentez la limite de mémoire ou réduisez la mémoire utilisée par processus.
  • Échecs intermittents avec le type executable sous forte charge : passez à executable_pool.
  • Le statut de construction indique une erreur : main.py (Python) ou amd64/main et arm64/main (Native) sont absents de la racine du zip, ou l’archive contient des liens symboliques.
  • Impossible de créer la fonction : son nom entre en conflit avec celui d’une fonction intégrée.

Gérer les UDF avec l’API Cloud

Les opérations de cycle de vie de la console sont également accessibles par programmation via l’API ClickHouse Cloud. Les endpoints UDF vous permettent d’automatiser le téléversement d’archives sources, la création de fonctions et de versions, leur association à des services et leur détachement, ainsi que la suppression de versions et de fonctions. L’API ne propose aucune opération de rechargement : après avoir associé une nouvelle version d’une UDF executable_pool à un service, exécutez SYSTEM RELOAD FUNCTION ON CLUSTER default <name> en SQL sur ce service afin que les processus du pool prennent en compte la nouvelle version.
Ces endpoints sont en bêta et le contrat de l’API est susceptible d’évoluer.
Le workflow type pour créer et déployer une UDF via l’API est le suivant :
  1. Créez une URL de téléversement afin d’obtenir une URL présignée pour téléverser un fichier application/zip, puis téléversez-y votre archive ZIP. Chaque ID de téléversement ne peut être utilisé que pour une seule tentative de création ou de création de version ; demandez une nouvelle URL de téléversement lors d’une nouvelle tentative.
  2. Créez l’UDF à partir de l’archive téléversée, en spécifiant le nom de la fonction, le runtime, les arguments et le type de retour.
  3. Associez l’UDF à un service. Si la version est omise, la dernière version prête est associée. Le service doit être en cours d’exécution ; les services inactifs peuvent d’abord être réactivés.
Les champs de requête permettant de sélectionner les fonctionnalités décrites ci-dessus sont les suivants : L’ensemble des endpoints : Consultez la référence de l’API UDF pour les schémas de requête et de réponse.

Gérer les UDF avec Terraform

Le provider Terraform ClickHouse officiel inclut deux ressources pour gérer les UDF en tant qu’infrastructure as code :
  • clickhouse_udf gère la fonction elle-même. Cette ressource accepte une archive ZIP contenant le code source de la fonction et publie une nouvelle version chaque fois que le hash de l’archive change, en attendant la fin de la construction.
  • clickhouse_udf_attachment associe une version d’UDF à un service. Un service ne peut utiliser qu’une seule version d’une fonction à la fois. Vous pouvez épingler un numéro de version fixe ou référencer clickhouse_udf.<name>.version pour mettre automatiquement à jour les services vers la dernière version.
Ces ressources sont disponibles à partir de la version 3.24.0 du provider. Elles sont au statut bêta et leur comportement peut changer dans les prochaines versions du provider.
Par exemple, pour déployer avec Terraform l’UDF isBusinessHours de l’exemple précédent :
Outre les attributs obligatoires présentés ci-dessus, la ressource clickhouse_udf accepte format, pool_size, max_command_execution_time, command_read_timeout, command_write_timeout, send_chunk_header, sandbox_type, sandbox_version, return_name et fail_on_build_error ; consultez la documentation de la ressource pour obtenir le schéma complet. Pour tester une nouvelle version sur un service de développement avant son passage en production, faites en sorte qu’une association suive la dernière version et épinglez l’autre à une variable. L’association de développement suit automatiquement chaque nouvelle construction, tandis que l’association de production n’évolue que lorsque vous modifiez var.prod_udf_version :
Le provider n’expose pas encore deterministic ni la limite de mémoire. Ces deux paramètres sont définis lors de la création d’une version ; les versions publiées par le provider ne peuvent donc pas en bénéficier. Pour les utiliser, créez plutôt ces versions via la console ou l’API.
L’opération d’association ne réussit que pour les versions prêtes et peut prendre plusieurs minutes ; les services inactifs sont automatiquement réactivés. Le provider ne recharge pas non plus les pools : pour les UDF executable_pool, exécutez SYSTEM RELOAD FUNCTION ON CLUSTER default <name> sur le service après tout changement de version d’une association. La suppression d’une ressource clickhouse_udf supprime toutes les versions de la FUNCTION et la détache de tous les services.