저장 프로시저와 준비된 문
저장 프로시저와 준비된 문 (Stored Procedures and Prepared Statements)
전통적인 관계형 데이터베이스에서 오셨다면 ClickHouse에서 저장 프로시저(stored procedure)와 준비된 문(prepared statement)을 찾고 계실 수 있어요. 이 가이드는 ClickHouse가 이 개념들을 어떻게 다루는지 설명하고 권장 대안을 제공할게요.
출처: 문서
본문
ClickHouse에서 저장 프로시저의 대안
ClickHouse는 제어 흐름 로직(IF/ELSE, 루프 등)이 있는 전통적인 저장 프로시저를 지원하지 않아요. 이것은 분석 데이터베이스로서의 ClickHouse 아키텍처에 기반한 의도적인 설계 결정이에요. 루프는 분석 데이터베이스에서 권장되지 않는데, 왜냐하면 O(n) 단순 쿼리를 처리하는 것이 더 적은 복잡한 쿼리를 처리하는 것보다 보통 느리기 때문이에요. ClickHouse는 다음에 최적화되어 있어요.
- 분석 워크로드 — 대규모 데이터셋에 대한 복잡한 집계
- 배치 처리 — 대량 데이터 볼륨을 효율적으로 처리
- 선언적 쿼리 — 데이터를 어떻게 처리할지가 아니라 무엇을 가져올지 설명하는 SQL 쿼리
절차적 로직이 있는 저장 프로시저는 이런 최적화와 상충돼요. 대신 ClickHouse는 그 강점에 맞는 대안을 제공해요.
사용자 정의 함수 (UDFs)
사용자 정의 함수(UDF)는 제어 흐름 없이 재사용 가능한 로직을 캡슐화할 수 있게 해줘요. ClickHouse는 두 가지 유형을 지원해요.
Lambda 기반 UDF
SQL 표현식과 lambda 문법으로 함수를 만들 수 있어요.
예시를 위한 샘플 데이터
-- Create the products table
CREATE TABLE products (
product_id UInt32,
product_name String,
price Decimal(10, 2)
)
ENGINE = MergeTree()
ORDER BY product_id;
-- Insert sample data
INSERT INTO products (product_id, product_name, price) VALUES
(1, 'Laptop', 899.99),
(2, 'Wireless Mouse', 24.99),
(3, 'USB-C Cable', 12.50),
(4, 'Monitor', 299.00),
(5, 'Keyboard', 79.99),
(6, 'Webcam', 54.95),
(7, 'Desk Lamp', 34.99),
(8, 'External Hard Drive', 119.99),
(9, 'Headphones', 149.00),
(10, 'Phone Stand', 15.99);
-- Simple calculation function
CREATE FUNCTION calculate_tax AS (price, rate) -> price * rate;
SELECT
product_name,
price,
calculate_tax(price, 0.08) AS tax
FROM products;
-- Conditional logic using if()
CREATE FUNCTION price_tier AS (price) ->
if(price < 100, 'Budget',
if(price < 500, 'Mid-range', 'Premium'));
SELECT
product_name,
price,
price_tier(price) AS tier
FROM products;
-- String manipulation
CREATE FUNCTION format_phone AS (phone) ->
concat('(', substring(phone, 1, 3), ') ',
substring(phone, 4, 3), '-',
substring(phone, 7, 4));
SELECT format_phone('5551234567');
-- Result: (555) 123-4567
제한 사항:
- 루프나 복잡한 제어 흐름 없음
- 데이터 수정(
INSERT/UPDATE/DELETE) 불가 - 재귀 함수 허용 안 됨
완전한 문법은 CREATE FUNCTION를 참고하세요.
실행형 UDF (Executable UDFs)
더 복잡한 로직을 위해서는 외부 프로그램을 호출하는 실행형 UDF를 사용해요.
<!-- /etc/clickhouse-server/sentiment_analysis_function.xml -->
<functions>
<function>
<type>executable</type>
<name>sentiment_score</name>
<return_type>Float32</return_type>
<argument>
<type>String</type>
</argument>
<format>TabSeparated</format>
<command>python3 /opt/scripts/sentiment.py</command>
</function>
</functions>
-- Use the executable UDF
SELECT
review_text,
sentiment_score(review_text) AS score
FROM customer_reviews;
실행형 UDF는 어떤 언어(Python, Node.js, Go 등)로든 임의의 로직을 구현할 수 있어요. 자세한 내용은 Executable UDFs를 참고하세요.
파라미터화된 뷰 (Parameterized views)
파라미터화된 뷰는 데이터셋을 반환하는 함수처럼 동작해요. 동적 필터링이 있는 재사용 가능한 쿼리에 이상적이에요.
예시를 위한 샘플 데이터
-- Create the sales table
CREATE TABLE sales (
date Date,
product_id UInt32,
product_name String,
category String,
quantity UInt32,
revenue Decimal(10, 2),
sales_amount Decimal(10, 2)
)
ENGINE = MergeTree()
ORDER BY (date, product_id);
-- Insert sample data
INSERT INTO sales VALUES
('2024-01-05', 12345, 'Laptop Pro', 'Electronics', 2, 1799.98, 1799.98),
('2024-01-06', 12345, 'Laptop Pro', 'Electronics', 1, 899.99, 899.99),
('2024-01-10', 12346, 'Wireless Mouse', 'Electronics', 5, 124.95, 124.95),
('2024-01-15', 12347, 'USB-C Cable', 'Accessories', 10, 125.00, 125.00),
('2024-01-20', 12345, 'Laptop Pro', 'Electronics', 3, 2699.97, 2699.97),
('2024-01-25', 12348, 'Monitor 4K', 'Electronics', 2, 598.00, 598.00),
('2024-02-01', 12345, 'Laptop Pro', 'Electronics', 1, 899.99, 899.99),
('2024-02-05', 12349, 'Keyboard Mechanical', 'Accessories', 4, 319.96, 319.96),
('2024-02-10', 12346, 'Wireless Mouse', 'Electronics', 8, 199.92, 199.92),
('2024-02-15', 12350, 'Webcam HD', 'Electronics', 3, 164.85, 164.85);
-- Create a parameterized view
CREATE VIEW sales_by_date AS
SELECT
date,
product_id,
sum(quantity) AS total_quantity,
sum(revenue) AS total_revenue
FROM sales
WHERE date BETWEEN {start_date:Date} AND {end_date:Date}
GROUP BY date, product_id;
-- Query the view with parameters
SELECT *
FROM sales_by_date(start_date='2024-01-01', end_date='2024-01-31')
WHERE product_id = 12345;
일반적인 사용 사례
-
동적 날짜 범위 필터링
-
사용자별 데이터 슬라이싱
-
보고서 템플릿
-
-- More complex parameterized view CREATE VIEW top_products_by_category AS SELECT category, product_name, revenue, rank FROM ( SELECT category, product_name, revenue, rank() OVER (PARTITION BY category ORDER BY revenue DESC) AS rank FROM ( SELECT category, product_name, sum(sales_amount) AS revenue FROM sales WHERE category = {category:String} AND date >= {min_date:Date} GROUP BY category, product_name ) ) WHERE rank <= {top_n:UInt32};
-- Use it SELECT * FROM top_products_by_category( category='Electronics', min_date='2024-01-01', top_n=10 );
더 많은 정보는 Parameterized Views 섹션을 참고하세요.
매터리얼라이즈드 뷰
매터리얼라이즈드 뷰는 전통적으로 저장 프로시저에서 수행되던 비싼 집계를 사전 계산하는 데 이상적이에요. 전통적인 데이터베이스에서 오셨다면, 매터리얼라이즈드 뷰를 소스 테이블에 삽입될 때 데이터를 자동으로 변환·집계하는 INSERT 트리거로 생각하세요.
-- Source table
CREATE TABLE page_views (
user_id UInt64,
page String,
timestamp DateTime,
session_id String
)
ENGINE = MergeTree()
ORDER BY (user_id, timestamp);
-- Materialized view that maintains aggregated statistics
CREATE MATERIALIZED VIEW daily_user_stats
ENGINE = SummingMergeTree()
ORDER BY (date, user_id)
AS SELECT
toDate(timestamp) AS date,
user_id,
count() AS page_views,
uniq(session_id) AS sessions,
uniq(page) AS unique_pages
FROM page_views
GROUP BY date, user_id;
-- Insert sample data into source table
INSERT INTO page_views VALUES
(101, '/home', '2024-01-15 10:00:00', 'session_a1'),
(101, '/products', '2024-01-15 10:05:00', 'session_a1'),
(101, '/checkout', '2024-01-15 10:10:00', 'session_a1'),
(102, '/home', '2024-01-15 11:00:00', 'session_b1'),
(102, '/about', '2024-01-15 11:05:00', 'session_b1'),
(101, '/home', '2024-01-16 09:00:00', 'session_a2'),
(101, '/products', '2024-01-16 09:15:00', 'session_a2'),
(103, '/home', '2024-01-16 14:00:00', 'session_c1'),
(103, '/products', '2024-01-16 14:05:00', 'session_c1'),
(103, '/products', '2024-01-16 14:10:00', 'session_c1'),
(102, '/home', '2024-01-17 10:30:00', 'session_b2'),
(102, '/contact', '2024-01-17 10:35:00', 'session_b2');
-- Query pre-aggregated data
SELECT
user_id,
sum(page_views) AS total_views,
sum(sessions) AS total_sessions
FROM daily_user_stats
WHERE date BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY user_id;
Refreshable 매터리얼라이즈드 뷰
스케줄링된 배치 처리(야간 저장 프로시저 같은)를 위해:
-- Automatically refresh every day at 2 AM
CREATE MATERIALIZED VIEW monthly_sales_report
REFRESH EVERY 1 DAY OFFSET 2 HOUR
AS SELECT
toStartOfMonth(order_date) AS month,
region,
product_category,
count() AS order_count,
sum(amount) AS total_revenue,
avg(amount) AS avg_order_value
FROM orders
WHERE order_date >= today() - INTERVAL 13 MONTH
GROUP BY month, region, product_category;
-- Query always has fresh data
SELECT * FROM monthly_sales_report
WHERE month = toStartOfMonth(today());
고급 패턴은 Cascading Materialized Views를 참고하세요.
외부 오케스트레이션
복잡한 비즈니스 로직, ETL 워크플로, 또는 다단계 프로세스의 경우 언어 클라이언트를 사용해 ClickHouse 밖에서 로직을 구현하는 것이 항상 가능해요.
애플리케이션 코드 사용
여기에 MySQL 저장 프로시저가 ClickHouse와 함께 애플리케이션 코드로 어떻게 변환되는지 보여주는 나란한 비교가 있어요.
-
MySQL 저장 프로시저
-
ClickHouse 애플리케이션 코드
DELIMITER $$
CREATE PROCEDURE process_order( IN p_order_id INT, IN p_customer_id INT, IN p_order_total DECIMAL(10,2), OUT p_status VARCHAR(50), OUT p_loyalty_points INT ) BEGIN DECLARE v_customer_tier VARCHAR(20); DECLARE v_previous_orders INT; DECLARE v_discount DECIMAL(10,2);
-- Start transaction START TRANSACTION; -- Get customer information SELECT tier, total_orders INTO v_customer_tier, v_previous_orders FROM customers WHERE customer_id = p_customer_id; -- Calculate discount based on tier IF v_customer_tier = 'gold' THEN SET v_discount = p_order_total * 0.15; ELSEIF v_customer_tier = 'silver' THEN SET v_discount = p_order_total * 0.10; ELSE SET v_discount = 0; END IF; -- Insert order record INSERT INTO orders (order_id, customer_id, order_total, discount, final_amount) VALUES (p_order_id, p_customer_id, p_order_total, v_discount, p_order_total - v_discount); -- Update customer statistics UPDATE customers SET total_orders = total_orders + 1, lifetime_value = lifetime_value + (p_order_total - v_discount), last_order_date = NOW() WHERE customer_id = p_customer_id; -- Calculate loyalty points (1 point per dollar) SET p_loyalty_points = FLOOR(p_order_total - v_discount); -- Insert loyalty points transaction INSERT INTO loyalty_points (customer_id, points, transaction_date, description) VALUES (p_customer_id, p_loyalty_points, NOW(), CONCAT('Order #', p_order_id)); -- Check if customer should be upgraded IF v_previous_orders + 1 >= 10 AND v_customer_tier = 'bronze' THEN UPDATE customers SET tier = 'silver' WHERE customer_id = p_customer_id; SET p_status = 'ORDER_COMPLETE_TIER_UPGRADED_SILVER'; ELSEIF v_previous_orders + 1 >= 50 AND v_customer_tier = 'silver' THEN UPDATE customers SET tier = 'gold' WHERE customer_id = p_customer_id; SET p_status = 'ORDER_COMPLETE_TIER_UPGRADED_GOLD'; ELSE SET p_status = 'ORDER_COMPLETE'; END IF; COMMIT;END$$
DELIMITER ;
-- Call the stored procedure CALL process_order(12345, 5678, 250.00, @status, @points); SELECT @status, @points;
쿼리 파라미터 아래 예시는 ClickHouse에서 쿼리 파라미터를 사용해요. 아직 ClickHouse 쿼리 파라미터에 익숙하지 않다면 “ClickHouse에서 준비된 문의 대안”으로 건너뛰세요.
# Python example using clickhouse-connect
import clickhouse_connect
from datetime import datetime
from decimal import Decimal
client = clickhouse_connect.get_client(host='localhost')
def process_order(order_id: int, customer_id: int, order_total: Decimal) -> tuple[str, int]:
"""
Processes an order with business logic that would be in a stored procedure.
Returns: (status_message, loyalty_points)
Note: ClickHouse is optimized for analytics, not OLTP transactions.
For transactional workloads, use an OLTP database (PostgreSQL, MySQL)
and sync analytics data to ClickHouse for reporting.
"""
# Step 1: Get customer information
result = client.query(
"""
SELECT tier, total_orders
FROM customers
WHERE customer_id = {cid: UInt32}
""",
parameters={'cid': customer_id}
)
if not result.result_rows:
raise ValueError(f"Customer {customer_id} not found")
customer_tier, previous_orders = result.result_rows[0]
# Step 2: Calculate discount based on tier (business logic in Python)
discount_rates = {'gold': 0.15, 'silver': 0.10, 'bronze': 0.0}
discount = order_total * Decimal(str(discount_rates.get(customer_tier, 0.0)))
final_amount = order_total - discount
# Step 3: Insert order record
client.command(
"""
INSERT INTO orders (order_id, customer_id, order_total, discount,
final_amount, order_date)
VALUES ({oid: UInt32}, {cid: UInt32}, {total: Decimal64(2)},
{disc: Decimal64(2)}, {final: Decimal64(2)}, now())
""",
parameters={
'oid': order_id,
'cid': customer_id,
'total': float(order_total),
'disc': float(discount),
'final': float(final_amount)
}
)
# Step 4: Calculate new customer statistics
new_order_count = previous_orders + 1
# For analytics databases, prefer INSERT over UPDATE
# This uses a ReplacingMergeTree pattern
client.command(
"""
INSERT INTO customers (customer_id, tier, total_orders, last_order_date,
update_time)
SELECT
customer_id,
tier,
{new_count: UInt32} AS total_orders,
now() AS last_order_date,
now() AS update_time
FROM customers
WHERE customer_id = {cid: UInt32}
""",
parameters={'cid': customer_id, 'new_count': new_order_count}
)
# Step 5: Calculate and record loyalty points
loyalty_points = int(final_amount)
client.command(
"""
INSERT INTO loyalty_points (customer_id, points, transaction_date, description)
VALUES ({cid: UInt32}, {pts: Int32}, now(),
{desc: String})
""",
parameters={
'cid': customer_id,
'pts': loyalty_points,
'desc': f'Order #{order_id}'
}
)
# Step 6: Check for tier upgrade (business logic in Python)
status = 'ORDER_COMPLETE'
if new_order_count >= 10 and customer_tier == 'bronze':
# Upgrade to silver
client.command(
"""
INSERT INTO customers (customer_id, tier, total_orders, last_order_date,
update_time)
SELECT
customer_id, 'silver' AS tier, total_orders, last_order_date,
now() AS update_time
FROM customers
WHERE customer_id = {cid: UInt32}
""",
parameters={'cid': customer_id}
)
status = 'ORDER_COMPLETE_TIER_UPGRADED_SILVER'
elif new_order_count >= 50 and customer_tier == 'silver':
# Upgrade to gold
client.command(
"""
INSERT INTO customers (customer_id, tier, total_orders, last_order_date,
update_time)
SELECT
customer_id, 'gold' AS tier, total_orders, last_order_date,
now() AS update_time
FROM customers
WHERE customer_id = {cid: UInt32}
""",
parameters={'cid': customer_id}
)
status = 'ORDER_COMPLETE_TIER_UPGRADED_GOLD'
return status, loyalty_points
# Use the function
status, points = process_order(
order_id=12345,
customer_id=5678,
order_total=Decimal('250.00')
)
print(f"Status: {status}, Loyalty Points: {points}")
주요 차이점
- 제어 흐름 — MySQL 저장 프로시저는
IF/ELSE,WHILE루프를 사용해요. ClickHouse에서는 이 로직을 애플리케이션 코드(Python, Java 등)로 구현해요. - 트랜잭션 — MySQL은 ACID 트랜잭션을 위해
BEGIN/COMMIT/ROLLBACK을 지원해요. ClickHouse는 append-only 워크로드에 최적화된 분석 데이터베이스이지, 트랜잭션 업데이트를 위한 것이 아니에요. - 업데이트 — MySQL은
UPDATE문을 사용해요. ClickHouse는 가변 데이터를 위해 ReplacingMergeTree나 CollapsingMergeTree와 함께INSERT를 선호해요. - 변수와 상태 — MySQL 저장 프로시저는 변수(
DECLARE v_discount)를 선언할 수 있어요. ClickHouse에서는 애플리케이션 코드에서 상태를 관리해요. - 오류 처리 — MySQL은
SIGNAL과 예외 핸들러를 지원해요. 애플리케이션 코드에서는 언어 고유의 오류 처리(try/catch)를 사용해요.
각 접근을 언제 사용할까:
- OLTP 워크로드(주문, 결제, 사용자 계정) → 애플리케이션 트랜잭션과 저장 프로시저에는 MySQL 또는 PostgreSQL을 사용하세요. ClickHouse Managed Postgres가 ClickHouse Cloud에서 제공돼요.
- 분석 워크로드(보고, 집계, 시계열) → 애플리케이션 오케스트레이션과 함께 ClickHouse를 사용하세요.
- 하이브리드 아키텍처 → 애플리케이션 트랜잭션은 Postgres에 두고, 분석을 위해 커밋된 변경 사항을 ClickHouse로 복제하세요. Postgres와 ClickHouse가 함께 동작하는 방법과 통합 퀵스타트를 참고하세요.
워크플로 오케스트레이션 도구 사용
- Apache Airflow — ClickHouse 쿼리의 복잡한 DAG를 스케줄링·모니터링
- dbt — SQL 기반 워크플로로 데이터 변환
- Prefect/Dagster — 현대 Python 기반 오케스트레이션
- 커스텀 스케줄러 — Cron 작업, Kubernetes CronJobs 등
외부 오케스트레이션의 장점:
- 완전한 프로그래밍 언어 능력
- 더 나은 오류 처리와 재시도 로직
- 외부 시스템(API, 다른 데이터베이스)과의 통합
- 버전 관리와 테스팅
- 모니터링과 알림
- 더 유연한 스케줄링
ClickHouse에서 준비된 문의 대안
ClickHouse에는 RDBMS 의미의 전통적인 "준비된 문(prepared statement)"이 없지만, 같은 목적을 제공하는 쿼리 파라미터(query parameters) 를 제공해요. SQL 삽입을 방지하는 안전하고 파라미터화된 쿼리예요.
문법
쿼리 파라미터를 정의하는 두 가지 방법이 있어요.
방법 1: SET 사용
예시 테이블과 데이터
-- Create the user_events table (ClickHouse syntax)
CREATE TABLE user_events (
event_id UInt32,
user_id UInt64,
event_name String,
event_date Date,
event_timestamp DateTime
) ENGINE = MergeTree()
ORDER BY (user_id, event_date);
-- Insert sample data for multiple users and events
INSERT INTO user_events (event_id, user_id, event_name, event_date, event_timestamp) VALUES
(1, 12345, 'page_view', '2024-01-05', '2024-01-05 10:30:00'),
(2, 12345, 'page_view', '2024-01-05', '2024-01-05 10:35:00'),
(3, 12345, 'add_to_cart', '2024-01-05', '2024-01-05 10:40:00'),
(4, 12345, 'page_view', '2024-01-10', '2024-01-10 14:20:00'),
(5, 12345, 'add_to_cart', '2024-01-10', '2024-01-10 14:25:00'),
(6, 12345, 'purchase', '2024-01-10', '2024-01-10 14:30:00'),
(7, 12345, 'page_view', '2024-01-15', '2024-01-15 09:15:00'),
(8, 12345, 'page_view', '2024-01-15', '2024-01-15 09:20:00'),
(9, 12345, 'page_view', '2024-01-20', '2024-01-20 16:45:00'),
(10, 12345, 'add_to_cart', '2024-01-20', '2024-01-20 16:50:00'),
(11, 12345, 'purchase', '2024-01-25', '2024-01-25 11:10:00'),
(12, 12345, 'page_view', '2024-01-28', '2024-01-28 13:30:00'),
(13, 67890, 'page_view', '2024-01-05', '2024-01-05 11:00:00'),
(14, 67890, 'add_to_cart', '2024-01-05', '2024-01-05 11:05:00'),
(15, 67890, 'purchase', '2024-01-05', '2024-01-05 11:10:00'),
(16, 12345, 'page_view', '2024-02-01', '2024-02-01 10:00:00'),
(17, 12345, 'add_to_cart', '2024-02-01', '2024-02-01 10:05:00');
SET param_user_id = 12345;
SET param_start_date = '2024-01-01';
SET param_end_date = '2024-01-31';
SELECT
event_name,
count() AS event_count
FROM user_events
WHERE user_id = {user_id: UInt64}
AND event_date BETWEEN {start_date: Date} AND {end_date: Date}
GROUP BY event_name;
방법 2: CLI 파라미터 사용
clickhouse-client \
--param_user_id=12345 \
--param_start_date='2024-01-01' \
--param_end_date='2024-01-31' \
--query="SELECT count() FROM user_events
WHERE user_id = {user_id: UInt64}
AND event_date BETWEEN {start_date: Date} AND {end_date: Date}"
파라미터 문법
파라미터는 {parameter_name: DataType}로 참조돼요.
parameter_name— 파라미터 이름 (param_접두사는 제외)DataType— 파라미터를 변환할 ClickHouse 데이터 타입
데이터 타입 예시
예시를 위한 테이블과 샘플 데이터
-- 1. Create a table for string and number tests
CREATE TABLE IF NOT EXISTS users (
name String,
age UInt8,
salary Float64
) ENGINE = Memory;
INSERT INTO users VALUES
('John Doe', 25, 75000.50),
('Jane Smith', 30, 85000.75),
('Peter Jones', 20, 50000.00);
-- 2. Create a table for date and timestamp tests
CREATE TABLE IF NOT EXISTS events (
event_date Date,
event_timestamp DateTime
) ENGINE = Memory;
INSERT INTO events VALUES
('2024-01-15', '2024-01-15 14:30:00'),
('2024-01-15', '2024-01-15 15:00:00'),
('2024-01-16', '2024-01-16 10:00:00');
-- 3. Create a table for array tests
CREATE TABLE IF NOT EXISTS products (
id UInt32,
name String
) ENGINE = Memory;
INSERT INTO products VALUES (1, 'Laptop'), (2, 'Monitor'), (3, 'Mouse'), (4, 'Keyboard');
-- 4. Create a table for Map (struct-like) tests
CREATE TABLE IF NOT EXISTS accounts (
user_id UInt32,
status String,
type String
) ENGINE = Memory;
INSERT INTO accounts VALUES
(101, 'active', 'premium'),
(102, 'inactive', 'basic'),
(103, 'active', 'basic');
-- 5. Create a table for Identifier tests
CREATE TABLE IF NOT EXISTS sales_2024 (
value UInt32
) ENGINE = Memory;
INSERT INTO sales_2024 VALUES (100), (200), (300);
-
문자열과 숫자
-
날짜와 시간
-
배열
-
맵
-
식별자
SET param_name = 'John Doe'; SET param_age = 25; SET param_salary = 75000.50;
SELECT name, age, salary FROM users WHERE name = {name: String} AND age >= {age: UInt8} AND salary <= {salary: Float64};
SET param_date = '2024-01-15'; SET param_timestamp = '2024-01-15 14:30:00';
SELECT * FROM events WHERE event_date = {date: Date} OR event_timestamp > {timestamp: DateTime};
SET param_ids = [1, 2, 3, 4, 5];
SELECT * FROM products WHERE id IN {ids: Array(UInt32)};
SET param_filters = {'target_status': 'active'};
SELECT user_id, status, type FROM accounts WHERE status = arrayElement( mapValues({filters: Map(String, String)}), indexOf(mapKeys({filters: Map(String, String)}), 'target_status') );
SET param_table = 'sales_2024';
SELECT count() FROM {table: Identifier};
언어 클라이언트에서의 쿼리 파라미터 사용은 관심 있는 특정 언어 클라이언트 문서를 참고하세요.
쿼리 파라미터의 제한 사항
쿼리 파라미터는 일반적인 텍스트 치환이 아니에요. 특정한 제한이 있어요.
- 주로 SELECT 문을 위해 설계됨 — 가장 좋은 지원은 SELECT 쿼리에 있어요.
- 식별자나 리터럴로 동작 — 임의의 SQL 조각을 치환할 수 없어요.
- DDL 지원이 제한적 —
CREATE TABLE에서는 지원되지만ALTER TABLE에서는 지원되지 않아요.
동작하는 것 (WORKS):
-- ✓ Values in WHERE clause
SELECT * FROM users WHERE id = {user_id: UInt64};
-- ✓ Table/database names
SELECT * FROM {db: Identifier}.{table: Identifier};
-- ✓ Values in IN clause
SELECT * FROM products WHERE id IN {ids: Array(UInt32)};
-- ✓ CREATE TABLE
CREATE TABLE {table_name: Identifier} (id UInt64, name String) ENGINE = MergeTree() ORDER BY id;
동작하지 않는 것 (DOESN'T work):
-- ✗ Column names in SELECT (use Identifier carefully)
SELECT {column: Identifier} FROM users; -- Limited support
-- ✗ Arbitrary SQL fragments
SELECT * FROM users {where_clause: String}; -- NOT SUPPORTED
-- ✗ ALTER TABLE statements
ALTER TABLE {table: Identifier} ADD COLUMN new_col String; -- NOT SUPPORTED
-- ✗ Multiple statements
{statements: String}; -- NOT SUPPORTED
보안 모범 사례
사용자 입력에는 항상 쿼리 파라미터를 사용하세요:
# ✓ SAFE - Uses parameters
user_input = request.get('user_id')
result = client.query(
"SELECT * FROM orders WHERE user_id = {uid: UInt64}",
parameters={'uid': user_input}
)
# ✗ DANGEROUS - SQL injection risk!
user_input = request.get('user_id')
result = client.query(f"SELECT * FROM orders WHERE user_id = {user_input}")
입력 타입 검증:
def get_user_orders(user_id: int, start_date: str):
# Validate types before querying
if not isinstance(user_id, int) or user_id <= 0:
raise ValueError("Invalid user_id")
# Parameters enforce type safety
return client.query(
"""
SELECT * FROM orders
WHERE user_id = {uid: UInt64}
AND order_date >= {start: Date}
""",
parameters={'uid': user_id, 'start': start_date}
)
MySQL 프로토콜 준비된 문
ClickHouse의 MySQL 인터페이스는 준비된 문(COM_STMT_PREPARE, COM_STMT_EXECUTE, COM_STMT_CLOSE)에 대한 최소한의 지원을 포함해요. 주로 Tableau Online 같은 도구와의 연결을 가능하게 하기 위한 것으로, 쿼리를 준비된 문으로 감싸요. 주요 제한 사항:
- 파라미터 바인딩이 지원되지 않음 — 바인딩된 파라미터와 함께
?플레이스홀더를 사용할 수 없어요. - 쿼리는
PREPARE동안 저장되지만 파싱되지는 않아요. - 구현은 최소 수준이며 특정 BI 도구 호환성을 위해 설계됐어요.
동작하지 않는 것의 예시:
-- This MySQL-style prepared statement with parameters does NOT work in ClickHouse
PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?';
EXECUTE stmt USING @user_id; -- Parameter binding not supported
대신 ClickHouse 네이티브 쿼리 파라미터를 사용하세요. 모든 ClickHouse 인터페이스에서 완전한 파라미터 바인딩 지원, 타입 안전성, SQL 삽입 방지를 제공해요.
-- ClickHouse native query parameters (recommended)
SET param_user_id = 12345;
SELECT * FROM users WHERE id = {user_id: UInt64};
더 자세한 내용은 MySQL Interface 문서와 MySQL 지원에 관한 블로그 글을 참고하세요.
요약
ClickHouse에서 저장 프로시저의 대안
| 전통적인 저장 프로시저 패턴 | ClickHouse 대안 |
|---|---|
| 단순 계산과 변환 | 사용자 정의 함수 (UDFs) |
| 재사용 가능한 파라미터화된 쿼리 | 파라미터화된 뷰 |
| 사전 계산된 집계 | 매터리얼라이즈드 뷰 |
| 스케줄링된 배치 처리 | Refreshable Materialized Views |
| 복잡한 다단계 ETL | 체인 매터리얼라이즈드 뷰 또는 외부 오케스트레이션 (Python, Airflow, dbt) |
| 제어 흐름이 있는 비즈니스 로직 | 애플리케이션 코드 |
쿼리 파라미터 사용
쿼리 파라미터는 다음에 사용될 수 있어요.
- SQL 삽입 방지
- 타입 안전성이 있는 파라미터화된 쿼리
- 애플리케이션의 동적 필터링
- 재사용 가능한 쿼리 템플릿
관련 문서
CREATE FUNCTION— 사용자 정의 함수CREATE VIEW— 파라미터화·매터리얼라이즈드 포함 뷰- SQL Syntax - Query Parameters — 완전한 파라미터 문법
- Cascading Materialized Views — 고급 매터리얼라이즈드 뷰 패턴
- Executable UDFs — 외부 함수 실행