Skip to main content
Funções Definidas pelo Usuário (UDF) permitem que os usuários ampliem o comportamento do ClickHouse para além do que é oferecido por mais de mil funções nativas. No ClickHouse Cloud, há várias maneiras de criar e gerenciar funções definidas pelo usuário:
  1. Usando SQL
  2. Usando o console e seu próprio código
  3. Usando a Cloud API (beta)
  4. Usando o Terraform (beta)
As UDFs executáveis estão disponíveis na AWS, no GCP e no Azure. Elas já vêm habilitadas para todas as organizações, sem necessidade de nenhuma ativação. Elas não são cobradas à parte: os processos de UDF são executados dentro dos pods do serviço e consomem a CPU e a memória do próprio serviço.

Funções Definidas pelo Usuário em SQL

As UDFs em SQL podem ser criadas usando a instrução CREATE FUNCTION a partir de uma expressão lambda. Neste exemplo, vamos criar uma função definida pelo usuário executável simples, isBusinessHours. A função verificará se um determinado timestamp está dentro do horário comercial e retornará true se estiver; caso contrário, false.
  1. Faça login no Cloud Console e abra o SQL Console
  2. Escreva a seguinte consulta SQL para criar a função isBusinessHours:
  1. Execute o comando abaixo para testar sua UDF recém-criada:
Você deverá receber o seguinte resultado:
  1. Você pode usar o comando DROP FUNCTION para remover a UDF que acabou de criar:
ImportanteUDFs no ClickHouse Cloud não herdam configurações no nível do usuário. Elas são executadas com as configurações padrão do sistema.
Isso significa:
  • As configurações no nível da sessão (definidas pela instrução SET) não são propagadas para o contexto de execução das UDFs
  • As configurações de perfil do usuário não são herdadas pelas UDFs
  • As configurações no nível da consulta não se aplicam durante a execução das UDFs

Funções Definidas pelo Usuário executáveis

UDFs executáveis rodam seu código em um processo isolado (sandbox) junto a cada réplica do seu serviço. Você pode fazer upload de um script Python, executado no tempo de execução python3.11, ou de um binary pré-compilado, executado no tempo de execução Native. Neste exemplo, vamos criar a mesma função definida pelo usuário executável simples, isBusinessHours, que verifica se um determinado timestamp está dentro do horário comercial. Anteriormente, nós a criamos usando SQL, mas desta vez vamos criá-la em Python e configurá-la pelo console.
1

Crie o arquivo Python

Crie localmente um novo arquivo main.py:
O runtime do Python é python3.11. Se o seu script Python importar pacotes de terceiros, liste-os em um arquivo requirements.txt para que o ClickHouse Cloud os instale para você nas duas arquiteturas de CPU em que o ClickHouse Cloud é executado (x86-64 e ARM64). Como alternativa, você pode empacotar as dependências diretamente no ZIP, mas, nesse caso, precisará incluir pacotes em cache para as duas arquiteturas de CPU; por isso, usar o requirements.txt é mais simples. Por exemplo:
O ClickHouse Cloud espera encontrar main.py no arquivo zip que você enviará pelo console na próxima etapa. Se o arquivo tiver outro nome, ocorrerá um erro.
2

Empacote dependências e arquivos locais

Neste tutorial, que não tem dependências nem arquivos adicionais, o arquivo compactado precisa conter apenas main.py:
Para incluir pacotes de dependências e quaisquer outros arquivos locais (como requirements.txt, arquivos wheel, arquivos de configuração ou arquivos de dados), coloque-os no mesmo diretório do seu main.py e, em seguida, execute zip a partir desse diretório, para que o main.py fique na raiz do arquivo compactado:
Você pode referenciar o diretório base do caminho local empacotado no seu código Python usando os.path.dirname(os.path.abspath(__file__)). Isso retorna o caminho absoluto do diretório onde o seu main.py está localizado dentro do arquivo ZIP, permitindo acessar outros arquivos empacotados:
Isso é útil quando você precisa:
  • Acessar arquivos de configuração incluídos no pacote da sua UDF
  • Carregar pacotes wheel para dependências personalizadas
  • Referenciar scripts adicionais ou arquivos de dados
Links simbólicos não são permitidosO ClickHouse Cloud rejeita arquivos compactados de UDF que contenham links simbólicos. Certifique-se de que seu pacote ZIP contenha apenas arquivos e diretórios comuns — uploads com links simbólicos não passarão na validação.
3

Crie uma UDF pelo console

  1. Na página inicial do Cloud console, clique no nome da sua organização no menu inferior esquerdo.
  2. Selecione User-defined functions no menu.
  3. Na página de funções definidas pelo usuário, clique em Set up a UDF. Um painel de configuração será aberto no lado direito da tela.
  4. Insira um nome para a função. Neste exemplo, use isBusinessHours.
  5. Selecione um tipo de função, Executable pool ou Executable:
    • Executable pool: um pool de processos de longa duração é iniciado uma única vez e reutilizado entre blocos e consultas. Recomendado para praticamente qualquer carga de trabalho.
    • Executable: um novo processo é iniciado para cada bloco de dados processado. Use esta opção somente quando o processo não puder manter estado entre chamadas.
  6. Selecione um Runtime type. Neste exemplo, mantenha python3.11. Para fazer upload de um binary pré-compilado, selecione Native; consulte Tempo de execução nativo.
  7. Neste exemplo, use as configurações padrão. Para ver a lista completa de parâmetros de configuração e seus valores padrão no Cloud, consulte Referência de configuração.
  8. Clique em Browse File para fazer upload do arquivo .zip criado no início deste tutorial.
  9. Adicione um novo argumento. Neste exemplo, adicione um argumento timestamp do tipo DateTime.
  10. Selecione um tipo de retorno. Neste exemplo, selecione Bool.
  11. Clique em Create UDF. Uma caixa de diálogo exibirá o status atual da compilação.
    • Se houver algum problema, o status muda para error.
    • Caso contrário, o status passa de building para provisioning. Seu serviço precisa estar ativo para que o provisionamento seja concluído. Se o serviço estiver ocioso, clique em Wake Up Service no painel UDF details, ao lado do nome do serviço.
    • Após a conclusão, o status muda para deployed.
4

Teste sua UDF

  1. volte à página inicial do SQL Console clicando em Settings - return to your service view no canto superior esquerdo da página
  2. clique em SQL Console no menu lateral esquerdo
  3. escreva a seguinte consulta:
Você deverá ver o seguinte resultado:
5

Criar uma nova versão

Para alterar o código ou as configurações de uma UDF, crie uma nova versão. As versões são imutáveis. O painel Edit apenas define a quais serviços uma UDF está atribuída; fazer upload de um arquivo nele não substitui o código implantado.
  1. Na página inicial do Cloud console, clique no nome da sua organização no menu inferior esquerdo.
  2. Selecione User-defined functions no menu.
  3. Selecione os três pontos em Actions da UDF isBusinessHours e clique em Create new version
  4. Faça upload de um zip com o código modificado ou altere as configurações, selecione os serviços que devem receber a nova versão e clique em Create new version
Uma nova versão não herda as associações de serviço da versão anterior. O painel de nova versão tem um seletor de serviços, o que permite testar uma versão em um único serviço e avançar ou reverter a versão serviço a serviço. Cada serviço mantém apenas uma versão de uma função por vez.Para UDFs executable_pool, os processos de longa duração do pool continuam executando a versão anterior até que o pool seja recarregado. Use a ação Reload UDF ao lado do serviço no painel da UDF. Ela executa SYSTEM RELOAD FUNCTION ON CLUSTER default isBusinessHours nesse serviço.Você adicionou sua primeira função definida pelo usuário pelo console, confirmou que ela funciona e viu como criar uma nova versão dela, se necessário.

Tempo de execução nativo (binários compilados)

O tempo de execução Native permite fazer upload de um binary pré-compilado em vez de um script Python. Defina Runtime type como Native ao criar a UDF (runtime: native na API). Nada é instalado no sandbox para você; tudo de que o binary precisa deve ser incluído na compilação. Atualmente compatíveis: Rust, Go, C++ e JavaScript compilado em um binary com o Bun. O Bun foi testado nas versões 1.4.0 e 1.4.2; outras versões podem não funcionar. O arquivo compactado deve conter um executável por arquitetura de CPU, cada um em sua própria pasta:
As duas arquiteturas são necessárias porque os serviços do ClickHouse Cloud são executados em ambas. O executável deve se chamar exatamente main e é executado sem argumentos; para passar um modo ou subcomando, use um argumento de função comum. Recomendamos fortemente o uso de binários vinculados estaticamente: eles não dependem das bibliotecas de sistema do host, cujas chamadas de sistema podem variar entre versões do servidor, e a política de chamadas de sistema do sandbox é ajustada para eles. Binários vinculados dinamicamente só funcionam se forem vinculados exclusivamente à glibc e tiverem sido compilados com a glibc 2.35 ou anterior. JavaScript compilado com Bun é sempre vinculado dinamicamente.
Resolva os arquivos de dados em relação ao executávelOs arquivos na pasta da arquitetura são implantados ao lado do binário, mas o processo não é iniciado nesse diretório. Resolva-os em relação ao caminho do próprio executável, nunca em relação ao diretório de trabalho: em Go, use filepath.Dir(os.Args[0]) ou os.Executable(); em Rust, use std::env::current_exe(). Se um arquivo necessário estiver ausente, escreva uma mensagem no stderr e encerre com código diferente de zero, em vez de recorrer silenciosamente a um comportamento alternativo.
UDFs nativas não podem ter acesso à rede habilitado; o acesso de saída à rede está disponível apenas para o tempo de execução Python. Consulte Acesso à rede. Compile o binário para as duas arquiteturas. Em Rust, adicione os dois targets uma única vez e, em seguida, compile cada um:
Em Go:
Copie cada binário gerado para amd64/main e arm64/main antes de compactar as pastas em zip. O wire protocol é idêntico ao do runtime Python: o processo lê os argumentos de stdin e grava os resultados em stdout no formato configurado. Consulte Formatos e cabeçalhos de fragmento. O repositório llm-token-udf contém UDFs nativas em Rust e Go, com scripts de compilação.

Referência de configuração

As configurações a seguir estão disponíveis no console e na API. Os valores padrão são os do ClickHouse Cloud e, em alguns casos, diferem dos padrões da versão de código aberto. As seguintes configurações de código aberto são gerenciadas pelo ClickHouse Cloud e não podem ser alteradas por UDF: command_termination_timeout, lifetime, stderr_reaction, check_exit_code e execute_direct. O padrão de código aberto para pool_size é 16, enquanto o padrão do Cloud é 3. A lista completa das configurações de código aberto está em Funções Definidas pelo Usuário executáveis. Os nomes de funções devem corresponder a ^[A-Za-z][A-Za-z0-9_]*$, são únicos dentro da organização e não podem reutilizar o nome de uma função nativa do ClickHouse; a API rejeita nomes reservados. Todo argumento deve ter um nome que siga o mesmo padrão; a API e o Terraform exigem isso em todos os formatos. Arquivos compactados que contenham links simbólicos são rejeitados.

Formatos e cabeçalhos de fragmento

TabSeparated é o formato padrão e o mais simples. Ele exige o escape de tabulações, quebras de linha e barras invertidas em ambos os lados. RowBinary é seguro para dados binários e dispensa a formatação e o parsing de texto no lado do ClickHouse; prefira-o para strings grandes. Com Send chunk header ativado, o ClickHouse grava o número de linhas como texto decimal seguido de \n antes de cada fragmento e, em seguida, as linhas no formato configurado. O processo deve ler exatamente essa quantidade de linhas, gravar a mesma quantidade de resultados e executar o flush uma única vez. Sem essa opção, um processo que lê um formato orientado a linhas, como TabSeparated, RowBinary ou JSONEachRow, e mantém sua saída em buffer não tem como saber quando um bloco termina, e a consulta fica travada até atingir o tempo limite de leitura. Habilite essa opção para qualquer UDF desse tipo que não execute o flush após cada linha. O formato Native já delimita os blocos, portanto uma UDF Native pode ler um bloco, gravar um bloco e executar o flush sem o cabeçalho.

Acesso à rede

O acesso à rede está disponível apenas para o tempo de execução Python; UDFs nativas não podem ter acesso à rede habilitado. As UDFs Python criadas no console já são compiladas com o acesso à rede habilitado, e não há nenhuma configuração no console para alterar isso. Na API, sandboxType permite escolher entre netenable, que permite conexões de saída, e basic, que não as permite; o Terraform provider oferece a mesma opção como sandbox_type. O acesso à rede permite conexões de saída para endpoints na internet pública. Conexões com redes privadas, endereços link-local e endereços internos são bloqueadas. O sandbox não possui identidade na nuvem: não há instance role, endpoint de metadados nem variáveis de ambiente herdadas, de modo que uma UDF tem acesso apenas às credenciais que você fornecer. O acesso à rede é aplicado no momento em que uma versão da UDF é compilada; portanto, se uma versão existente foi criada sem ele, será necessário criar uma nova versão. Ainda não há um repositório de secrets para UDFs. Inclua um arquivo de configuração no pacote zip e leia-o usando um caminho relativo a __file__; nunca coloque chaves em SQL.

Limite de memória

Limite de memória define a quantidade máxima de memória, em MiB, disponível para cada processo do sandbox. Se não for definido, aplica-se o padrão do sandbox, de 4 GiB. O limite se aplica ao espaço de endereçamento virtual, e não à memória residente; por isso, runtimes que reservam grandes intervalos de endereços precisam de uma folga bem acima do uso real. O Go, por exemplo, reserva bem mais de 1 GiB de espaço de endereçamento na inicialização. Quando um processo excede o limite, a alocação falha dentro do próprio processo: uma UDF em Python é encerrada com um traceback MemoryError, e a consulta falha exibindo essa saída stderr na mensagem de erro.

Funções determinísticas e o cache de consultas

Por padrão, uma UDF é tratada como não determinística. Uma consulta que chama uma UDF com use_query_cache = 1 falha com QUERY_CACHE_USED_WITH_NONDETERMINISTIC_FUNCTIONS, a menos que query_cache_nondeterministic_function_handling seja alterada. Habilite Deterministic somente se a função sempre retornar o mesmo resultado para os mesmos argumentos. Dessa forma, o cache de consultas poderá armazenar e fornecer os resultados das consultas que a chamam. Outras funções não determinísticas na mesma consulta, como now, today ou rand, continuam impedindo o uso do cache com o tratamento padrão throw; query_cache_nondeterministic_function_handling = 'save' armazena essas consultas em cache mesmo assim, e 'ignore' as executa sem usar o cache.

Reinicializações do serviço

A primeira associação reinicia o serviçoAssociar a primeira UDF a um serviço adiciona um contêiner helper aos pods do serviço e aciona uma reinicialização gradual. A associação de UDFs adicionais não causa reinicialização. Remover a última UDF de um serviço aciona uma nova reinicialização gradual. Planeje a primeira associação levando isso em conta.

Monitoramento

O ClickHouse 26.6 e versões posteriores registram o uso de recursos de UDFs executáveis em oito contadores, todos com o prefixo ExecutableUserDefinedFunction. Por consulta, eles aparecem como entradas na coluna do tipo map ProfileEvents de system.query_log. No nível do servidor, eles aparecem como linhas de system.events, cumulativas desde a inicialização do servidor, e como colunas ProfileEvent_<name> de system.metric_log, amostradas ao longo do tempo. Os sufixos dos contadores são: system.asynchronous_metrics contém ExecutableUserDefinedFunctionProcesses e ExecutableUserDefinedFunctionMemoryResidentBytes. Esta última é a soma de VmRSS dos processos de UDF ativos e representa um limite superior, já que as páginas compartilhadas são contadas uma vez para cada processo.
system.query_log.memory_usage considera apenas o processo do servidor ClickHouse e não inclui os processos de UDF.

Orientações de desempenho

Use executable_pool. Com executable, um novo processo é iniciado para cada bloco de dados, de modo que o custo de inicialização se repete a cada bloco. Cada invocação tem um custo fixo. Se ExecutableUserDefinedFunctionInvocations estiver muito alto em relação ao número de linhas processadas, aumente preferred_block_size_bytes para essa consulta, para que cada bloco contenha mais linhas. Uma única consulta usa no máximo max_threads processos do pool simultaneamente; portanto, tamanhos de pool acima de max_threads não trazem benefício para essa consulta. Comece com um valor pequeno e aumente o tamanho do pool quando ExecutableUserDefinedFunctionPoolWaitMicroseconds deixar de ser desprezível. Cada linha atravessa uma fronteira de processo. UDFs servem para lógica que o SQL não consegue expressar, e não para operações triviais por linha que uma função nativa ou uma UDF SQL possa executar.

Solução de problemas

Quando uma UDF falha, a consulta também falha com o código de erro 754 (UDF_EXECUTION_FAILED). O stderr do processo filho é incluído na mensagem; portanto, escreva os diagnósticos nele e encerre com um código de saída diferente de zero:
Casos comuns:
  • A consulta trava e depois falha por tempo limite esgotado: o processo armazenou a saída em buffer em um formato orientado a linhas sem Send chunk header. Habilite essa opção e faça flush uma vez por fragmento, ou uma vez por bloco ao usar o formato Native.
  • Código de retorno 1 com um traceback de MemoryError (Python): o limite de memória foi excedido. Aumente o Memory limit ou reduza o consumo de memória por processo.
  • Falhas intermitentes com o tipo executable sob carga elevada: mude para executable_pool.
  • O status da compilação é erro: main.py (Python) ou amd64/main e arm64/main (Native) não estão na raiz do zip, ou o arquivo contém links simbólicos.
  • Não é possível criar a função: o nome conflita com o de uma função interna.

Gerencie UDFs com a Cloud API

As operações de ciclo de vida do console também podem ser acessadas programaticamente por meio da ClickHouse Cloud API. Os endpoints de UDF permitem automatizar o upload de arquivos de origem, a criação de funções e versões, a anexação delas a serviços e a desanexação, além da exclusão de versões e funções. A API não tem uma operação de recarregamento: depois de anexar uma nova versão de uma UDF executable_pool a um serviço, execute SYSTEM RELOAD FUNCTION ON CLUSTER default <name> via SQL nesse serviço para que os processos do pool passem a usar a nova versão.
Estes endpoints estão em beta, e o contrato da API pode mudar.
O fluxo de trabalho típico para criar e implantar uma UDF pela API é:
  1. Crie uma URL de upload para obter uma URL pré-assinada para upload de application/zip e, em seguida, faça upload do seu arquivo ZIP. Cada ID de upload pode ser usado em apenas uma tentativa de criação ou versionamento; solicite uma nova URL de upload ao tentar novamente.
  2. Crie a UDF a partir do arquivo enviado, especificando o nome da função, o runtime, os argumentos e o tipo de retorno.
  3. Anexe a UDF a um serviço. Quando a versão é omitida, a versão pronta mais recente é anexada. O serviço deve estar em execução; serviços inativos podem ser ativados antes.
Os campos da requisição que determinam os recursos descritos acima são: O conjunto completo de endpoints: Consulte a referência da API de UDF para ver os schemas de solicitação e resposta.

Gerencie UDFs com o Terraform

O provider Terraform oficial do ClickHouse inclui dois recursos para gerenciar UDFs como infraestrutura como código:
  • clickhouse_udf gerencia a própria função. Ele aceita um arquivo ZIP com o código-fonte da função e publica uma nova versão sempre que o hash do arquivo é alterado, aguardando a conclusão da compilação.
  • clickhouse_udf_attachment anexa uma versão de UDF a um serviço. Um serviço comporta no máximo uma versão de uma função por vez. Você pode fixar um número de versão específico ou fazer referência a clickhouse_udf.<name>.version para atualizar automaticamente os serviços para a versão mais recente.
Esses recursos estão disponíveis na versão 3.24.0 e posteriores do provider. Eles estão em beta, e seu comportamento pode mudar em futuras versões do provider.
Por exemplo, para implantar com o Terraform a UDF isBusinessHours do exemplo anterior:
Além dos atributos obrigatórios mostrados acima, o recurso clickhouse_udf aceita format, pool_size, max_command_execution_time, command_read_timeout, command_write_timeout, send_chunk_header, sandbox_type, sandbox_version, return_name e fail_on_build_error; consulte a documentação do recurso para ver o schema completo. Para testar uma nova versão em um serviço de desenvolvimento antes que ela chegue à produção, configure uma anexação para acompanhar a versão mais recente e fixe a outra em uma variável. A anexação de desenvolvimento acompanha automaticamente cada nova compilação, enquanto a anexação de produção só é atualizada quando você altera var.prod_udf_version:
O provider ainda não expõe deterministic nem o limite de memória. Ambos são definidos quando uma versão é criada, portanto as versões publicadas pelo provider não podem tê-los. Para usá-los, crie essas versões pelo console ou pela API.
A anexação só é concluída para versões prontas e pode levar vários minutos; serviços inativos são ativados automaticamente. O provider também não recarrega os pools: para UDFs executable_pool, execute SYSTEM RELOAD FUNCTION ON CLUSTER default <name> no serviço sempre que uma anexação mudar de versão. A exclusão de um recurso clickhouse_udf remove todas as versões da função e a desanexa de todos os serviços.