Executable 및 ExecutablePool 테이블 엔진

Executable 및 ExecutablePool 테이블 엔진

Executable과 ExecutablePool 테이블 엔진은 정의한 스크립트(stdout에 행을 써서)로부터 행이 생성되는 테이블을 정의할 수 있게 해줘요. 실행 스크립트는 user_scripts 디렉토리에 저장되며 어떤 소스에서든 데이터를 읽을 수 있답니다.

출처: 문서

본문

Executable과 ExecutablePool 테이블 엔진을 사용하면 정의한 스크립트(stdout에 행을 작성하여)에서 행이 생성되는 테이블을 정의할 수 있어요. 실행 가능한 스크립트는 user_scripts 디렉토리에 저장되며 어떤 소스에서든 데이터를 읽을 수 있습니다.

  • Executable 테이블: 스크립트가 모든 쿼리에서 실행됩니다
  • ExecutablePool 테이블: 영구 프로세스 풀(pool)을 유지하며, 읽기에 대해 풀에서 프로세스를 가져옵니다

선택적으로 하나 이상의 입력 쿼리를 포함할 수 있으며, 그 결과가 스크립트가 읽도록 stdin으로 스트리밍됩니다.

Executable 테이블 생성 (Creating an Executable table)

Executable 테이블 엔진은 두 개의 매개변수가 필요합니다: 스크립트 이름과 들어오는 데이터의 형식. 선택적으로 하나 이상의 입력 쿼리를 전달할 수 있어요:

Executable(script_name, format, [input_query...])

Executable 테이블과 관련된 설정은 다음과 같습니다:

  • send_chunk_header - 설명: 청크를 처리하기 전에 각 청크의 행 수를 보냅니다. 이 설정은 일부 리소스를 사전 할당하도록 스크립트를 더 효율적으로 작성하는 데 도움이 될 수 있어요 - 기본값: false
  • command_termination_timeout - 설명: 명령 종료 타임아웃(초) - 기본값: 10
  • command_read_timeout - 설명: 명령 stdout에서 데이터를 읽는 타임아웃(밀리초) - 기본값: 10000
  • command_write_timeout - 설명: 명령 stdin에 데이터를 쓰는 타임아웃(밀리초) - 기본값: 10000

예시를 살펴볼게요. 다음 Python 스크립트는 my_script.py라는 이름으로 user_scripts 폴더에 저장되어 있습니다. 숫자 i를 읽고 i개의 무작위 문자열을 출력하며, 각 문자열 앞에는 탭으로 구분된 숫자가 옵니다:

#!/usr/bin/python3

import sys
import string
import random

def main():

    # Read input value
    for number in sys.stdin:
        i = int(number)

        # Generate some random rows
        for id in range(0, i):
            letters = string.ascii_letters
            random_string = ''.join(random.choices(letters, k=10))
            print(str(id) + '\t' + random_string + '\n', end='')

        # Flush results to stdout
        sys.stdout.flush()

if __name__ == "__main__":
    main()

다음 my_executable_table은 my_script.py의 출력으로 만들어졌습니다. my_executable_table에서 SELECT를 실행할 때마다 10개의 무작위 문자열을 생성합니다:

CREATE TABLE my_executable_table (
   x UInt32,
   y String
)
ENGINE = Executable('my_script.py', TabSeparated, (SELECT 10))

테이블 생성은 즉시 반환되며 스크립트를 호출하지 않아요. my_executable_table을 쿼리하면 스크립트가 호출됩니다:

SELECT * FROM my_executable_table
┌─x─┬─y──────────┐
│ 0 │ BsnKBsNGNH │
│ 1 │ mgHfBCUrWM │
│ 2 │ iDQAVhlygr │
│ 3 │ uNGwDuXyCk │
│ 4 │ GcFdQWvoLB │
│ 5 │ UkciuuOTVO │
│ 6 │ HoKeCdHkbs │
│ 7 │ xRvySxqAcR │
│ 8 │ LKbXPHpyDI │
│ 9 │ zxogHTzEVV │
└───┴────────────┘

쿼리 결과를 스크립트에 전달하기 (Passing query results to a script)

Hacker News 웹사이트의 사용자들은 댓글을 남깁니다. Python에는 댓글이 긍정적인지, 부정적인지, 중립적인지 판단하는 SentimentIntensityAnalyzer를 가진 자연어 처리 툴킷(nltk)이 포함되어 있어요. 댓글이 매우 부정적임을 뜻하는 -1과 매우 긍정적임을 뜻하는 1 사이의 값을 할당하기도 합니다. nltk를 사용하여 Hacker News 댓글의 감정을 계산하는 Executable 테이블을 만들어 볼게요.

이 예시는 여기에 설명된 hackernews 테이블을 사용합니다. hackernews 테이블에는 UInt64 타입의 id 컬럼과 comment라는 이름의 String 컬럼이 포함됩니다. 먼저 Executable 테이블을 정의해 볼게요:

CREATE TABLE sentiment (
id UInt64,
sentiment Float32
)
ENGINE = Executable(
    'sentiment.py',
    TabSeparated,
    (SELECT id, comment FROM hackernews WHERE id > 0 AND comment != '' LIMIT 20)
);

sentiment 테이블에 대한 몇 가지 설명:

  • sentiment.py 파일은 user_scripts 폴더(user_scripts_path 설정의 기본 폴더)에 저장됩니다
  • TabSeparated 형식은 Python 스크립트가 탭으로 구분된 값을 포함하는 원시 데이터 행을 생성해야 함을 의미합니다
  • 쿼리는 hackernews에서 두 개의 컬럼을 선택합니다. Python 스크립트는 들어오는 행에서 해당 컬럼 값을 파싱해야 합니다

sentiment.py의 정의는 다음과 같습니다:

#!/usr/local/bin/python3.9

import sys
import nltk
from nltk.sentiment import SentimentIntensityAnalyzer

def main():
    sentiment_analyzer = SentimentIntensityAnalyzer()

    while True:
        try:
            row = sys.stdin.readline()
            if row == '':
                break

            split_line = row.split("\t")

            id = str(split_line[0])
            comment = split_line[1]

            score = sentiment_analyzer.polarity_scores(comment)['compound']
            print(id + '\t' + str(score) + '\n', end='')
            sys.stdout.flush()
        except BaseException as x:
            break

if __name__ == "__main__":
    main()

Python 스크립트에 대한 몇 가지 설명:

  • 이 작업이 동작하려면 nltk.downloader.download('vader_lexicon')를 실행해야 합니다. 이것을 스크립트에 넣을 수도 있지만, 그러면 sentiment 테이블에 쿼리를 실행할 때마다 다운로드하게 되어 비효율적입니다
  • row의 각 값은 SELECT id, comment FROM hackernews WHERE id > 0 AND comment != '' LIMIT 20 결과 집합의 행이 됩니다
  • 들어오는 행은 탭으로 구분되므로 Python split 함수를 사용하여 idcomment를 파싱합니다
  • polarity_scores의 결과는 몇 가지 값을 가진 JSON 객체입니다. 우리는 이 JSON 객체의 compound 값만 가져오기로 결정했습니다
  • ClickHouse의 sentiment 테이블이 TabSeparated 형식을 사용하고 두 개의 컬럼을 포함한다는 점을 기억할게요. 따라서 print 함수는 탭으로 컬럼을 구분합니다

sentiment 테이블에서 행을 선택하는 쿼리를 작성할 때마다 SELECT id, comment FROM hackernews WHERE id > 0 AND comment != '' LIMIT 20 쿼리가 실행되고 그 결과가 sentiment.py에 전달됩니다. 테스트해 볼게요:

SELECT *
FROM sentiment

응답은 다음과 같아요:

┌───────id─┬─sentiment─┐
│  7398199 │    0.4404 │
│ 21640317 │    0.1779 │
│ 21462000 │         0 │
│ 25168863 │         0 │
│ 25168978 │   -0.1531 │
│ 25169359 │         0 │
│ 25169394 │   -0.9231 │
│ 25169766 │    0.4137 │
│ 25172570 │    0.7469 │
│ 25173687 │    0.6249 │
│ 28291534 │         0 │
│ 28291669 │   -0.4767 │
│ 28291731 │         0 │
│ 28291949 │   -0.4767 │
│ 28292004 │    0.3612 │
│ 28292050 │    -0.296 │
│ 28292322 │         0 │
│ 28295172 │    0.7717 │
│ 28295288 │    0.4404 │
│ 21465723 │   -0.6956 │
└──────────┴───────────┘

ExecutablePool 테이블 생성 (Creating an ExecutablePool table)

ExecutablePool의 구문은 Executable과 유사하지만, ExecutablePool 테이블에만 고유한 몇 가지 관련 설정이 있습니다:

  • pool_size - 설명: 프로세스 풀 크기. 크기가 0이면 크기 제한이 없음 - 기본값: 16
  • max_command_execution_time - 설명: 최대 명령 실행 시간(초) - 기본값: 10

위의 sentiment 테이블을 Executable 대신 ExecutablePool을 사용하도록 쉽게 변환할 수 있어요:

CREATE TABLE sentiment_pooled (
id UInt64,
sentiment Float32
)
ENGINE = ExecutablePool(
    'sentiment.py',
    TabSeparated,
    (SELECT id, comment FROM hackernews WHERE id > 0 AND comment != '' LIMIT 20000)
)
SETTINGS
    pool_size = 4;

ClickHouse는 클라이언트가 sentiment_pooled 테이블을 쿼리할 때 온디맨드로 4개의 프로세스를 유지합니다.

더 알아보기 (Learn more)