- Usando SQL
- Usando o console e seu próprio código
- Usando a Cloud API (beta)
- Usando o Terraform (beta)
Funções Definidas pelo Usuário em SQL
As UDFs em SQL podem ser criadas usando a instruçãoCREATE 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.
- Faça login no Cloud Console e abra o SQL Console
- Escreva a seguinte consulta SQL para criar a função
isBusinessHours:
- Execute o comando abaixo para testar sua UDF recém-criada:
- Você pode usar o comando
DROP FUNCTIONpara remover a UDF que acabou de criar:
- 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çãopython3.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 O runtime do Python é
main.py: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 Para incluir pacotes de dependências e quaisquer outros arquivos locais (como Você pode referenciar o diretório base do caminho local empacotado no seu código Python usando Isso é útil quando você precisa:
main.py: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: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:- 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
3
Crie uma UDF pelo console
- Na página inicial do Cloud console, clique no nome da sua organização no menu inferior esquerdo.
- Selecione User-defined functions no menu.
- 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.
- Insira um nome para a função. Neste exemplo, use
isBusinessHours. - 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.
- 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. - 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.
- Clique em Browse File para fazer upload do arquivo
.zipcriado no início deste tutorial. - Adicione um novo argumento. Neste exemplo, adicione um argumento
timestampdo tipoDateTime. - Selecione um tipo de retorno. Neste exemplo, selecione
Bool. - 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
- volte à página inicial do SQL Console clicando em Settings - return to your service view no canto superior esquerdo da página
- clique em SQL Console no menu lateral esquerdo
- escreva a seguinte consulta:
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.
- Na página inicial do Cloud console, clique no nome da sua organização no menu inferior esquerdo.
- Selecione User-defined functions no menu.
- Selecione os três pontos em Actions da UDF
isBusinessHourse clique em Create new version - 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
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:
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.
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:
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 tracebackMemoryError, 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 comuse_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
Monitoramento
O ClickHouse 26.6 e versões posteriores registram o uso de recursos de UDFs executáveis em oito contadores, todos com o prefixoExecutableUserDefinedFunction. 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
Useexecutable_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 erro754 (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:
- 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
executablesob carga elevada: mude paraexecutable_pool. - O status da compilação é erro:
main.py(Python) ouamd64/mainearm64/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 UDFexecutable_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.
- Crie uma URL de upload para obter uma URL pré-assinada para upload de
application/zipe, 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. - Crie a UDF a partir do arquivo enviado, especificando o nome da função, o runtime, os argumentos e o tipo de retorno.
- 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.
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_udfgerencia 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_attachmentanexa 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 aclickhouse_udf.<name>.versionpara 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.
isBusinessHours do exemplo anterior:
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.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.