사용자 정의 함수
사용자 정의 함수 (User Defined Functions, UDF)
ClickHouse는 여러 종류의 사용자 정의 함수(UDF)를 지원해요. 실행형(Executable), SQL, WebAssembly, 드라이버 기반 실행형 등 선택지가 있으며, 상황에 맞게 고를 수 있어요.
출처: 문서
본문
ClickHouse는 여러 종류의 사용자 정의 함수(UDF)를 지원합니다:
- 실행형 UDF는 외부 프로그램이나 스크립트(Python, Bash 등)를 시작하고 STDIN / STDOUT으로 데이터 블록을 스트리밍합니다. ClickHouse를 다시 컴파일하지 않고 기존 코드나 도구를 통합할 때 사용합니다. 인프로세스 옵션에 비해 호출당 오버헤드가 높아, 더 무거운 로직이나 다른 런타임이 필요한 경우에 적합합니다.
- SQL UDF는
CREATE FUNCTION으로 순수 SQL로 정의합니다. 쿼리 플랜에 인라인/확장되므로(프로세스 경계 없음) 가볍고, 표현식 로직 재사용이나 복잡한 계산 컬럼 단순화에 이상적입니다. - 실험적 WebAssembly UDF는 서버 프로세스 내부의 샌드박스에서 WebAssembly로 컴파일된 코드를 실행합니다. 외부 실행 파일보다 호출당 오버헤드가 낮고 네이티브 확장보다 격리가 좋아, WASM을 대상으로 할 수 있는 언어(예: C/C++/Rust)로 작성한 사용자 지정 알고리즘에 적합합니다.
- 실험적 드라이버 기반 실행형 UDF는 운영자가 제공한 "드라이버"가
CREATE FUNCTION ... ENGINE = DriverName(...) AS '...'에 제공된 코드 조각을 함수 생성 시점에 실행형 UDF로 바꿉니다(예: 컴파일). 실행형 UDF를 기반으로 하며 서버 측 드라이버 구성이 필요합니다.
실행형 사용자 정의 함수
ClickHouse Cloud에서는 실행형 UDF가 공개 베타이며 Cloud 콘솔 UI로 생성합니다. Cloud 전용 워크플로는 Cloud의 사용자 정의 함수를 참고하세요.
ClickHouse는 데이터를 처리하기 위해 외부 실행 프로그램이나 스크립트를 호출할 수 있습니다.
실행형 사용자 정의 함수의 구성은 하나 이상의 xml 파일에 있을 수 있습니다. 구성 경로는 user_defined_executable_functions_config 파라미터에 지정됩니다.
함수 구성은 다음 설정을 포함합니다:
| 파라미터 | 설명 | 필수 | 기본값 |
|---|---|---|---|
name |
함수 이름 | 예 | - |
command |
실행할 스크립트 이름, 또는 execute_direct가 false면 명령 |
예 | - |
argument |
type과 선택적 name을 가진 인자 설명. 각 인자는 별도 설정에 설명됩니다. Native나 JSONEachRow 같은 사용자 정의 함수 포맷의 직렬화에 인자 이름이 포함된다면 이름 지정이 필요합니다 |
예 | c + 인자_번호 |
format |
인자가 명령에 전달되는 포맷. 명령 출력도 같은 포맷이어야 합니다 | 예 | - |
return_type |
반환 값의 타입 | 예 | - |
return_name |
반환 값의 이름. Native나 JSONEachRow 같은 사용자 정의 함수 포맷의 직렬화에 반환 이름이 포함된다면 지정 필요 |
선택 | result |
type |
실행형 타입. executable이면 단일 명령이 시작되고, executable_pool이면 명령 풀이 생성됩니다 |
예 | - |
max_command_execution_time |
데이터 블록 처리를 위한 최대 실행 시간(초). executable_pool 명령에만 유효 |
선택 | 10 |
command_termination_timeout |
파이프가 닫힌 후 명령이 끝나야 하는 시간(초). 이 시간 후 명령을 실행하는 프로세스에 SIGTERM이 전송됩니다 |
선택 | 10 |
command_read_timeout |
명령 stdout에서 데이터를 읽는 타임아웃(밀리초) | 선택 | 10000 |
command_write_timeout |
명령 stdin에 데이터를 쓰는 타임아웃(밀리초) | 선택 | 10000 |
pool_size |
명령 풀 크기 | 선택 | 16 |
send_chunk_header |
처리할 데이터 청크를 보내기 전에 행 수를 보낼지 제어 | 선택 | false |
execute_direct |
execute_direct = 1이면 command가 user_scripts_path로 지정된 user_scripts 폴더 안에서 검색됩니다. 추가 스크립트 인자는 공백 구분자로 지정할 수 있습니다. 예: script_name arg1 arg2. execute_direct = 0이면 command가 bin/sh -c의 인자로 전달됩니다 |
선택 | 1 |
lifetime |
함수의 재로드 간격(초). 0이면 함수가 재로드되지 않습니다 |
선택 | 0 |
deterministic |
함수가 결정적인지(같은 입력에 같은 결과) | 선택 | false |
stderr_reaction |
명령의 stderr 출력 처리 방법. 값: none(무시), log(모든 stderr 즉시 기록), log_first(종료 후 처음 4 KiB 기록), log_last(종료 후 마지막 4 KiB 기록), throw(stderr 출력이 있으면 즉시 예외 발생). log_first 또는 log_last를 0이 아닌 종료 코드와 함께 쓸 때 stderr 내용이 예외 메시지에 포함됩니다 |
선택 | log_last |
check_exit_code |
true면 ClickHouse가 명령의 종료 코드를 확인합니다. 0이 아닌 종료 코드는 예외를 일으킵니다 | 선택 | true |
명령은 STDIN에서 인자를 읽고 결과를 STDOUT으로 출력해야 합니다. 명령은 인자를 반복적으로 처리해야 합니다. 즉 인자 청크를 처리한 후 다음 청크를 기다려야 합니다.
예제
인라인 스크립트의 UDF
execute_direct를 0으로 지정해 test_function_sum을 XML 또는 YAML 구성으로 수동 생성합니다.
- XML
- YAML
파일 test_function.xml(기본 경로 설정에서 /etc/clickhouse-server/test_function.xml).
/etc/clickhouse-server/test_function.xml
<functions>
<function>
<type>executable</type>
<name>test_function_sum</name>
<return_type>UInt64</return_type>
<argument>
<type>UInt64</type>
<name>lhs</name>
</argument>
<argument>
<type>UInt64</type>
<name>rhs</name>
</argument>
<format>TabSeparated</format>
<command>cd /; clickhouse-local --input-format TabSeparated --output-format TabSeparated --structure 'x UInt64, y UInt64' --query "SELECT x + y FROM table"</command>
<execute_direct>0</execute_direct>
<deterministic>true</deterministic>
</function>
</functions>
파일 test_function.yaml(기본 경로 설정에서 /etc/clickhouse-server/test_function.yaml).
/etc/clickhouse-server/test_function.yaml
functions:
type: executable
name: test_function_sum
return_type: UInt64
argument:
- type: UInt64
name: lhs
- type: UInt64
name: rhs
format: TabSeparated
command: 'cd /; clickhouse-local --input-format TabSeparated --output-format TabSeparated --structure ''x UInt64, y UInt64'' --query "SELECT x + y FROM table"'
execute_direct: 0
deterministic: true
Query
SELECT test_function_sum(2, 2);
Result
┌─test_function_sum(2, 2)─┐
│ 4 │
└─────────────────────────┘
Python 스크립트의 UDF
이 예제에서는 STDIN에서 값을 읽어 문자열로 반환하는 UDF를 만듭니다.
test_function을 XML 또는 YAML 구성으로 생성합니다.
- XML
- YAML
파일 test_function.xml(기본 경로 설정에서 /etc/clickhouse-server/test_function.xml).
/etc/clickhouse-server/test_function.xml
<functions>
<function>
<type>executable</type>
<name>test_function_python</name>
<return_type>String</return_type>
<argument>
<type>UInt64</type>
<name>value</name>
</argument>
<format>TabSeparated</format>
<command>test_function.py</command>
</function>
</functions>
파일 test_function.yaml(기본 경로 설정에서 /etc/clickhouse-server/test_function.yaml).
/etc/clickhouse-server/test_function.yaml
functions:
type: executable
name: test_function_python
return_type: String
argument:
- type: UInt64
name: value
format: TabSeparated
command: test_function.py
user_scripts 폴더(/var/lib/clickhouse/user_scripts/test_function.py) 안에 스크립트 파일 test_function.py를 만듭니다.
#!/usr/bin/python3
import sys
if __name__ == '__main__':
for line in sys.stdin:
print("Value " + line, end='')
sys.stdout.flush()
Query
SELECT test_function_python(toUInt64(2));
Result
┌─test_function_python(2)─┐
│ Value 2 │
└─────────────────────────┘
STDIN에서 두 값을 읽고 그 합을 JSON 객체로 반환
이름 있는 인자와 JSONEachRow 포맷으로 test_function_sum_json을 XML 또는 YAML 구성으로 생성합니다.
- XML
- YAML
파일 test_function.xml.
/etc/clickhouse-server/test_function.xml
<functions>
<function>
<type>executable</type>
<name>test_function_sum_json</name>
<return_type>UInt64</return_type>
<return_name>result_name</return_name>
<argument>
<type>UInt64</type>
<name>argument_1</name>
</argument>
<argument>
<type>UInt64</type>
<name>argument_2</name>
</argument>
<format>JSONEachRow</format>
<command>test_function_sum_json.py</command>
</function>
</functions>
/etc/clickhouse-server/test_function.yaml
functions:
type: executable
name: test_function_sum_json
return_type: UInt64
return_name: result_name
argument:
- type: UInt64
name: argument_1
- type: UInt64
name: argument_2
format: JSONEachRow
command: test_function_sum_json.py
user_scripts 폴더 안에 스크립트 파일 test_function_sum_json.py를 만듭니다.
#!/usr/bin/python3
import sys
import json
if __name__ == '__main__':
for line in sys.stdin:
value = json.loads(line)
first_arg = int(value['argument_1'])
second_arg = int(value['argument_2'])
result = {'result_name': first_arg + second_arg}
print(json.dumps(result), end='\n')
sys.stdout.flush()
Query
SELECT test_function_sum_json(2, 2);
Result
┌─test_function_sum_json(2, 2)─┐
│ 4 │
└──────────────────────────────┘
command 설정에서 파라미터 사용
실행형 사용자 정의 함수는 command 설정에 구성된 상수 파라미터를 받을 수 있습니다(executable 타입의 사용자 정의 함수에서만 동작). 셸 인자 확장 취약점이 없도록 execute_direct 옵션도 필요합니다.
- XML
- YAML
파일 test_function_parameter_python.xml.
/etc/clickhouse-server/test_function_parameter_python.xml
<functions>
<function>
<type>executable</type>
<execute_direct>true</execute_direct>
<name>test_function_parameter_python</name>
<return_type>String</return_type>
<argument>
<type>UInt64</type>
</argument>
<format>TabSeparated</format>
<command>test_function_parameter_python.py {test_parameter:UInt64}</command>
</function>
</functions>
/etc/clickhouse-server/test_function_parameter_python.yaml
functions:
type: executable
execute_direct: true
name: test_function_parameter_python
return_type: String
argument:
- type: UInt64
format: TabSeparated
command: test_function_parameter_python.py {test_parameter:UInt64}
user_scripts 폴더 안에 스크립트 파일 test_function_parameter_python.py를 만듭니다.
#!/usr/bin/python3
import sys
if __name__ == "__main__":
for line in sys.stdin:
print("Parameter " + str(sys.argv[1]) + " value " + str(line), end="")
sys.stdout.flush()
Query
SELECT test_function_parameter_python(1)(2);
Result
┌─test_function_parameter_python(1)(2)─┐
│ Parameter 1 value 2 │
└──────────────────────────────────────┘
셸 스크립트의 UDF
이 예제에서는 각 값을 2로 곱하는 셸 스크립트를 만듭니다.
- XML
- YAML
파일 test_function_shell.xml.
/etc/clickhouse-server/test_function_shell.xml
<functions>
<function>
<type>executable</type>
<name>test_shell</name>
<return_type>String</return_type>
<argument>
<type>UInt8</type>
<name>value</name>
</argument>
<format>TabSeparated</format>
<command>test_shell.sh</command>
</function>
</functions>
/etc/clickhouse-server/test_function_shell.yaml
functions:
type: executable
name: test_shell
return_type: String
argument:
- type: UInt8
name: value
format: TabSeparated
command: test_shell.sh
user_scripts 폴더 안에 스크립트 파일 test_shell.sh를 만듭니다.
/var/lib/clickhouse/user_scripts/test_shell.sh
#!/bin/bash
while read read_data;
do printf "$(expr $read_data \* 2)\n";
done
Query
SELECT test_shell(number) FROM numbers(10);
Result
┌─test_shell(number)─┐
1. │ 0 │
2. │ 2 │
3. │ 4 │
4. │ 6 │
5. │ 8 │
6. │ 10 │
7. │ 12 │
8. │ 14 │
9. │ 16 │
10. │ 18 │
└────────────────────┘
오류 처리
일부 함수는 데이터가 잘못되면 예외를 던질 수 있습니다. 이 경우 쿼리가 취소되고 클라이언트에 오류 텍스트가 반환됩니다. 분산 처리는 서버 하나에서 예외가 발생하면 다른 서버들도 쿼리를 중단하려 시도합니다.
인자 표현식 평가
거의 모든 프로그래밍 언어에서 특정 연산자에 대해 인자 중 하나가 평가되지 않을 수 있습니다. 보통 &&, ||, ?: 연산자가 그렇습니다. ClickHouse에서는 함수(연산자)의 인자가 항상 평가됩니다. 각 행을 따로 계산하는 대신 컬럼 전체 부분을 한 번에 평가하기 때문입니다.
분산 쿼리 처리를 위한 함수 수행
분산 쿼리 처리에서는 가능한 한 많은 쿼리 처리 단계가 원격 서버에서 수행되고, 나머지 단계(중간 결과 병합과 그 이후)는 요청 서버에서 수행됩니다. 이는 함수가 서로 다른 서버에서 수행될 수 있음을 뜻합니다. 예를 들어 쿼리 SELECT f(sum(g(x))) FROM distributed_table GROUP BY h(y),에서:
distributed_table에 샤드가 둘 이상이면 함수g와h가 원격 서버에서, 함수f는 요청 서버에서 수행됩니다.distributed_table에 샤드가 하나뿐이면f,g,h함수가 모두 이 샤드의 서버에서 수행됩니다.
함수의 결과는 보통 어떤 서버에서 수행되는지에 의존하지 않습니다. 그러나 때로는 중요합니다. 예를 들어 딕셔너리를 다루는 함수는 실행 중인 서버에 존재하는 딕셔너리를 사용합니다. 또 다른 예로 hostName 함수는 SELECT 쿼리에서 서버별 GROUP BY를 위해 실행 중인 서버의 이름을 반환합니다. 쿼리의 함수가 요청 서버에서 수행되는데 원격 서버에서 수행해야 한다면, 함수를 any 집계 함수로 감싸거나 GROUP BY의 키에 추가하면 됩니다.
SQL 사용자 정의 함수
람다 표현식의 사용자 지정 함수는 CREATE FUNCTION 문으로 만들 수 있습니다. 이러한 함수를 삭제하려면 DROP FUNCTION 문을 사용합니다.
WebAssembly 사용자 정의 함수
WebAssembly 사용자 정의 함수(WASM UDF)는 ClickHouse 서버 프로세스 내부에서 WebAssembly로 컴파일된 사용자 지정 코드를 실행할 수 있게 해줍니다.
빠른 시작
ClickHouse 구성에서 실험적 WebAssembly 지원을 활성화합니다:
<clickhouse>
<allow_experimental_webassembly_udf>true</allow_experimental_webassembly_udf>
</clickhouse>
컴파일된 WASM 모듈을 시스템 테이블에 삽입합니다:
INSERT INTO system.webassembly_modules (name, code)
SELECT 'my_module', base64Decode('AGFzbQEAAAA...');
WASM 모듈을 사용해 함수를 만듭니다:
CREATE FUNCTION my_function
LANGUAGE WASM
ABI ROW_DIRECT
FROM 'my_module'
ARGUMENTS (x UInt32, y UInt32)
RETURNS UInt32;
쿼리에서 함수 사용:
SELECT my_function(10, 20);
자세한 내용
자세한 내용은 WebAssembly 사용자 정의 함수 문서를 참고하세요.
드라이버 기반 실행형 사용자 정의 함수
이 실험적 기능은 향후 릴리스에서 하위 호환되지 않는 방식으로 바뀔 수 있습니다. allow_experimental_executable_udf_drivers 서버 설정으로 활성화합니다.
드라이버는 운영자가 제공하는 어댑터로, 사용자 코드 조각을 실행 가능한 실행형 UDF로 바꿉니다. ENGINE = DriverName(...)으로 함수를 만들면 ClickHouse가 드라이버의 create_command를 실행해 함수 시그니처와 코드 본문을 전달합니다. 드라이버는 본문을 컴파일하거나 처리한 뒤 실행형 UDF 구성을 출력하며, ClickHouse가 이를 저장·로드합니다.
이를 통해 관리자는 임의 언어(예: 샌드박스 컨테이너 안에서 컴파일된 C)로 함수를 정의하는 안전하고 제한된 방법을 서버 구성 파일이나 파일시스템 접근을 주지 않고 사용자에게 제공할 수 있습니다. 사용 가능한 드라이버 집합은 전적으로 운영자가 제어합니다.
드라이버 활성화
드라이버 기반 실행형 UDF는 기본적으로 비활성화입니다. 활성화하려면:
-
서버 구성에 실험적 게이트 설정:
<clickhouse> <allow_experimental_executable_udf_drivers>true</allow_experimental_executable_udf_drivers> </clickhouse> -
user_defined_executable_function_drivers_config를 하나 이상의 드라이버 구성 파일(glob 지원)로 지정하고, 선택적으로 생성된 실행형 UDF 구성이 저장되는 디렉터리dynamic_user_defined_executable_functions_path를 설정합니다:<clickhouse> <user_defined_executable_function_drivers_config>user_defined_executable_function_drivers_config.d/*_driver.xml</user_defined_executable_function_drivers_config> <dynamic_user_defined_executable_functions_path>/var/lib/clickhouse/dynamic_user_defined_executable_functions/</dynamic_user_defined_executable_functions_path> </clickhouse>
드라이버 레지스트리는 서버 시작 시 로드되고 SYSTEM RELOAD CONFIG로 갱신되므로, 드라이버를 서버 재시작 없이 추가·변경·제거할 수 있습니다.
드라이버 구성
드라이버는 최상위 <driver> 요소가 있는 XML(또는 YAML) 파일로 설명됩니다. 지원 필드는 다음과 같습니다:
| 필드 | 설명 | 필수 |
|---|---|---|
name |
CREATE FUNCTION ... ENGINE = <name>(...)에서 사용하는 드라이버 이름. |
예 |
create_command |
코드 조각에서 UDF를 만들기 위해 호출되는 프로그램 경로. 상대 경로는 드라이버 구성 파일 기준으로 해석됩니다. | 예 |
drop_command |
이 드라이버 기반 함수가 삭제될 때 호출되는 프로그램 경로. | 아니오 |
engine_arguments |
ENGINE = DriverName(...) 안에서 허용되는 인자를 선언합니다. 각 자식 요소는 인자 이름이며, <required>true</required> 자식이 필수임을 표시합니다. |
아니오 |
env |
드라이버 명령을 호출할 때 내보내는 환경 변수. | 아니오 |
드라이버 구성 예제:
<clickhouse>
<driver>
<name>DockerC</name>
<create_command>../user_defined_executable_function_drivers/docker_c_create.sh</create_command>
<drop_command>../user_defined_executable_function_drivers/docker_c_drop.sh</drop_command>
<engine_arguments>
<opt_level><required>false</required></opt_level>
</engine_arguments>
<env>
<CLICKHOUSE_C_DRIVER_MEMORY>256m</CLICKHOUSE_C_DRIVER_MEMORY>
<CLICKHOUSE_C_DRIVER_CPUS>1.0</CLICKHOUSE_C_DRIVER_CPUS>
</env>
</driver>
</clickhouse>
드라이버 호출 계약
CREATE FUNCTION가 실행되면 create_command가 구성된 env 변수와 다음 인자를 가지고 호출됩니다:
--name <function_name>--return <return_type>(RETURNS절이 있으면)--args <signature>(ARGUMENTS절이 있으면), 시그니처는 선언된 인자 목록(예:x UInt8, y DateTime)ENGINE = DriverName(key = value)에서 제공된 모든 선언 엔진 인자에 대한--<key> <value>
사용자 코드 본문(AS 뒤 텍스트)은 명령의 표준 입력으로 전송됩니다. 명령은 실행형 UDF의 구성을 표준 출력으로 출력해야 합니다. 포맷은 자동 감지됩니다. <로 시작하는 출력은 XML, 그 외는 YAML로 처리됩니다. 생성된 구성의 함수 이름은 생성 중인 이름과 일치해야 합니다. create_command가 0이 아닌 상태로 종료하면 문은 종료 코드와 드라이버의 표준 오류를 포함한 예외로 실패합니다.
drop_command가 있으면 함수가 삭제될 때 같은 방식으로 호출됩니다(stdin에 코드 본문 없이).
드라이버로 함수 만들기
CREATE [OR REPLACE] FUNCTION [IF NOT EXISTS] name [ON CLUSTER cluster]
ARGUMENTS (a UInt8, b String) RETURNS UInt64
ENGINE = DriverName(key1 = 'value1', key2 = 42)
AS '...code body...'
ClickHouse가 드라이버의 create_command를 실행하고, 생성된 구성을 dynamic_user_defined_executable_functions_path에 기록하며, 기존 실행형 UDF 로더가 이를 가져옵니다. 그런 다음 함수를 다른 함수처럼 호출할 수 있습니다.
드라이버로 함수 삭제
DROP FUNCTION [IF EXISTS] name [ON CLUSTER cluster]
DROP FUNCTION은 드라이버의 drop_command(있으면)를 호출하고, 생성된 동적 구성과 함수별 작업 디렉터리를 제거하며, 실행형 UDF 로더를 재로드하고 지속된 쿼리를 제거합니다.
지속성과 재시작
원본 쿼리는 사용자 정의 SQL 객체 디렉터리의 ATTACH FUNCTION ... 문으로 지속되므로, 서버 재시작 후에도 함수가 유지됩니다. 시작 시 dynamic_user_defined_executable_functions_path의 생성된 구성은 드라이버를 다시 실행하지 않고 직접 로드됩니다. 지속된 ATTACH FUNCTION에 일치하는 생성 구성이 없으면(예: 동적 디렉터리 분실) 드라이버를 다시 실행해 이를 재생성합니다.
제한 사항
- 이 기능은 실험적이며
allow_experimental_executable_udf_drivers뒤에 있습니다. - 드라이버 기반 함수는 복제된 사용자 정의 함수 스토리지(
ON CLUSTER및<user_defined_zookeeper_path>)를 지원하지 않습니다. 원본 쿼리만 복제되고 생성 산출물은 복제되지 않기 때문입니다. - 백업된 드라이버 기반 함수의
RESTORE는 쿼리를 지속하지만 드라이버를 다시 실행하지 않습니다. 생성 구성은 나중에 재시작 복구로 구체화됩니다.
예제 C 드라이버
소스 트리에는 programs/server/user_defined_executable_function_drivers_config.d/ 아래에 C 함수 본문을 컴파일·실행하는 개념 증명 드라이버가 포함되어 있습니다. 이것들은 예제이며 패키지로 설치되지 않습니다:
DockerC- 샌드박스 Docker 컨테이너(--network=none --read-only --cap-drop=ALL --security-opt=no-new-privileges, 메모리/CPU/PID 제한 포함) 안에서 코드를 컴파일·실행하며executable_poolUDF를 생성합니다.GVisorC- 컴파일된 바이너리를 gVisorrunsc런타임 아래에서 실행하는 변형.UnsafeC- 샌드박스 없이 호스트에서 직접 코드를 컴파일·실행합니다. 이름이 나타내듯 격리를 제공하지 않으며 신뢰할 수 있는 환경과 테스트 전용입니다.
이 예제 드라이버는 출발점으로 의도된 것입니다. 신뢰할 수 없는 사용자에게 노출하기 전에 환경에 맞게 샌드박싱을 검토하고 강화하세요.