준비된 문(Prepared Statement)
핵심 요약 Apache Doris는 MySQL 와이어 프로토콜을 통해 서버 측 Prepared Statement를 지원합니다. JDBC URL에
useServerPrepStmts=true를 설정하면 FE는 세션마다 문을 한 번만 파싱하고 실행 계획을 수립한 뒤,EXECUTE를 실행할 때마다 해당 계획을 재사용합니다. QPS가 높은 포인트 쿼리 트래픽에서는 short-circuit 경로를 통해 처리량이 약 4x로 늘어납니다. 배치 INSERT와 동일한 형태로 반복되는 쿼리도 행마다 파싱과 실행 계획 수립 비용을 부담하지 않게 됩니다.

Apache Doris에서 Prepared Statement를 사용하는 이유는 무엇인가요?
Apache Doris의 Prepared Statement는 세션별로 파싱된 실행 계획을 캐시하고 EXECUTE를 실행할 때마다 재사용합니다. 이를 통해 QPS가 높은 워크로드에서 큰 비중을 차지하는 FE 측 파싱, 분석, 최적화 비용을 없앱니다. 대부분의 분석 엔진에서는 쿼리마다 파싱과 실행 계획 수립 비용이 발생합니다. 이 비용은 일일 보고서 작업에서는 눈에 띄지 않습니다. 하지만 서비스가 같은 쿼리를 초당 수천 번 요청하기 시작하면 핵심 병목이 됩니다. 사용자용 대시보드는 클릭할 때마다 기본 키로 행을 읽습니다. 피처 스토어는 예측할 때마다 엔터티 ID로 벡터를 읽습니다. 대량 데이터 적재 도구는 동일한 INSERT INTO t VALUES (?, ?, ?)를 반복해서 실행합니다.
이 세 경우 모두 SQL 텍스트는 동일하고 매개변수만 바뀝니다. 호출할 때마다 같은 문을 다시 파싱하고 실행 계획을 다시 수립하면 FE CPU를 불필요하게 소모합니다. QPS가 수백 정도만 되어도 BE가 실제 작업을 본격적으로 시작하기 전에 FE가 포화되며, BE 노드를 추가해도 도움이 되지 않습니다. 이럴 때 사용하는 것이 PreparedStatement입니다.
- 높은 QPS에서 병목은 스토리지 엔진이 아니라 Frontend 플래너입니다.
- 반복 쿼리는 호출할 때마다 파싱, 분석, 최적화, 직렬화 비용을 부담합니다.
- 포인트 쿼리 비용을 줄이기 위해 별도의 KV 저장소를 도입하는 것은 부담이 큰 방법입니다.
Apache Doris의 Prepared Statement란 무엇인가요?
Apache Doris의 Prepared Statement는 MySQL 와이어 프로토콜의 COM_STMT_PREPARE / COM_STMT_EXECUTE 명령 쌍을 서버 측에서 구현한 기능입니다. COM_STMT_CLOSE로 캐시 항목을 제거하며, FE가 요청을 처리하고 세션별 실행 계획 캐시를 사용합니다. 클라이언트가 useServerPrepStmts=true로 연결을 열면 JDBC 드라이버는 서로 다른 SQL 텍스트마다 COM_STMT_PREPARE를 한 번 실행합니다. FE는 SQL을 파싱하고 분석한 뒤, 문 ID를 키로 하는 세션별 캐시에 실행 계획을 저장합니다. 이후의 모든 EXECUTE는 바이너리 형식의 매개변수 바이트만 전송합니다. FE는 캐시된 실행 계획에 값을 바인딩하고 실행합니다.
주요 용어
useServerPrepStmts=true: MySQL Connector/J 드라이버가 클라이언트 측 문자열 보간 대신 바이너리COM_STMT_PREPARE/COM_STMT_EXECUTE요청과 응답을 사용하도록 전환하는 JDBC URL 플래그입니다.cachePrepStmts=true: Prepared Statement 핸들을 클라이언트에 캐시하는 별도의 JDBC 플래그입니다. 이 설정이 없으면 호출할 때마다COM_STMT_PREPARE를 다시 실행하므로 속도 향상 효과가 사라집니다.- 세션별 문 캐시: FE의 메모리 내 맵인
ConnectContext.preparedStatementContextMap입니다. 파싱된PrepareCommand,StatementContext, 선택적으로ShortCircuitQueryContext를 보관합니다. 최대 개수는max_prepared_stmt_count로 제한됩니다(기본값 100,000). - 자리 표시자 (
?): 실행 시 바인딩되는 위치 기반 매개변수입니다. 하나의 문에 최대 65,536개까지 포함할 수 있습니다. ShortCircuitQueryContext: 쿼리가 short-circuit 포인트 쿼리 경로의 조건을 충족할 때 준비된 실행 계획과 함께 유지되는 추가 캐시입니다. 직렬화된 디스크립터 테이블, 출력 표현식, BE가 자체적으로 미리 구성한 구조를 조회하는 데 사용하는 UUID를 보관합니다.
Apache Doris의 Prepared Statement는 어떻게 동작하나요?
Apache Doris의 Prepared Statement는 다섯 단계로 동작합니다. FE에서 SQL을 준비하고, 조건을 충족하면 short-circuit 실행 계획을 캐시한 뒤, 바이너리 형식의 매개변수 바이트로 실행합니다. 필요에 따라 감사 로그를 기록하고, 마지막으로 닫아서 캐시 항목을 제거합니다.
- 준비. 클라이언트가
COM_STMT_PREPARE "SELECT ... WHERE k1 = ?"를 전송합니다. FE는 SQL을 파싱하고 Nereids 분석을 수행한 뒤, 연결의 맵에PreparedStatementContext를 저장하고 문 ID와 매개변수 개수를 반환합니다. - short-circuit 경로 캐시(조건 충족 시). 전체 키에 대한 동등 조건으로 단일 Unique Key 테이블을 조회하는 쿼리라면, 재작성 규칙인
LogicalResultSinkToShortCircuitPointQuery가 플래그를 활성화합니다. FE는 디스크립터 테이블과 출력 표현식을 직렬화하여 UUID를 키로 하는ShortCircuitQueryContext에 저장합니다. - 실행. 각
COM_STMT_EXECUTE에는 문 ID, null 비트맵, 타입 코드(최초 실행 시), 매개변수 바이트가 포함됩니다. FE는 이를Literal값으로 디코딩하고 자리 표시자 슬롯에 넣은 뒤 캐시된 실행 계획을 실행합니다. 파싱, 분석, 최적화를 다시 수행하지 않습니다. - 감사 로그 기록(선택 사항).
enable_prepared_stmt_audit_log=true를 설정하면 디버깅을 위해 매개변수 값으로 재구성한 문을fe.audit.log에 기록합니다. QPS가 높은 트래픽에서는 로그가 과도하게 쌓이므로 기본적으로 꺼져 있습니다. - 닫기.
COM_STMT_CLOSE는 세션 캐시에서 해당 항목을 제거합니다. 연결이 닫히면 맵 전체가 삭제됩니다.
빠른 시작
CREATE TABLE tbl_point_query (
k1 INT,
v1 VARCHAR(64)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH(k1) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"store_row_column" = "true"
);
String url = "jdbc:mysql://fe-host:9030/db"
+ "?useServerPrepStmts=true&cachePrepStmts=true"
+ "&prepStmtCacheSize=500&prepStmtCacheSqlLimit=1024";
try (Connection c = DriverManager.getConnection(url, "root", "");
PreparedStatement ps = c.prepareStatement(
"SELECT * FROM tbl_point_query WHERE k1 = ?")) {
ps.setInt(1, 42);
try (ResultSet rs = ps.executeQuery()) { /* ... */ }
}
예상 결과
fe.audit.log에서 두 번째 이후 호출은 원본 SQL 대신 다음과 같이 표시됩니다.
Stmt=EXECUTE(-2147481418)
이는 캐시된 실행 계획을 재사용하고 있다는 신호입니다. 이와 함께 EXPLAIN의 스캔 노드에 SHORT-CIRCUIT가 표시되는지 확인하면 두 계층이 모두 활성화되어 있는지 확인할 수 있습니다.
Apache Doris의 Prepared Statement는 언제 사용해야 하나요?
Apache Doris의 Prepared Statement는 Unique Key 테이블에 대한 높은 QPS의 포인트 쿼리, 반복적인 배치 INSERT, 동일한 문을 초당 수천 번 실행하는 클라이언트 측 워크로드에 적합합니다. 일회성 애드혹 쿼리, 서버 측 기능을 제대로 지원하지 않는 드라이버, short-circuit 경로에서 비결정적 함수를 사용하는 문에는 적합하지 않습니다.
적합한 경우
- 파싱 비용이 큰 비중을 차지하는, Unique Key 테이블에 대한 높은 QPS의 포인트 쿼리.
- Group Commit을 통한 배치 INSERT. 데이터 적재 도구가 동일한
INSERT INTO t VALUES (?, ?, ?)를 초당 수천 번 실행하는 경우입니다. - 피처 조회, 상태 확인, 허용 목록 검사, 온라인 서비스의 읽기 작업처럼 동일한 클라이언트가 상당한 빈도로 반복하는 쿼리.
- Apache Doris와 함께 별도의 KV 저장소를 구축하고 싶지 않은 워크로드.
적합하지 않은 경우
- 일회성 애드혹 쿼리.
PREPARE요청과 응답에 드는 비용이 절약되는 파싱 비용보다 큽니다. - 서버 측 Prepared Statement를 제대로 지원하지 않는 드라이버. 예를 들어 Rust의
sqlx드라이버에서는 Apache Doris 구현과 관련된 문제가 보고되었습니다. 바이너리 프로토콜을 제대로 처리하는 MySQL Connector를 사용하세요. useServerPrepStmts=true를 설정하면서cachePrepStmts=true는 설정하지 않은 연결. 호출할 때마다COM_STMT_PREPARE를 다시 실행하므로 기본 클라이언트 측 모드보다 느립니다.- 비결정적 함수(
NOW(),RAND())를 포함한 문. BE가 캐시된 short-circuit 컨텍스트를 재사용하지 않으므로 파싱 비용만 절약됩니다. - short-circuit의 속도 향상을 기대하는 범위 쿼리와 집계 쿼리. PreparedStatement로 파싱 비용은 절약되지만, BE는 일반적인 분산 실행 계획을 실행합니다. 이러한 워크로드에는 데이터 프루닝을 활용하세요.
성능 및 검증
Doris에 따르면 FE CPU가 병목인 포인트 쿼리 워크로드에서 PreparedStatement를 사용하면 처리량이 4x 이상 개선됩니다. 이는 고동시성 포인트 쿼리 기능 카드에서 short-circuit 관점으로 설명한 4x와 동일한 수치입니다. 두 효과를 합친 개선 폭이므로 각각을 곱해서 계산하면 안 됩니다. 전체 고동시성 구성(Unique Key + Merge-on-Write + 행 저장소 + 행 캐시 + short-circuit + PreparedStatement)은 16코어 / 64GB 머신의 YCSB에서 노드당 30,000+ QPS에 도달하며, 최적화하지 않은 기준 구성보다 평균 지연 시간이 96% 줄어듭니다. 나머지 구성과 분리하여 PreparedStatement만의 기여도를 측정하기는 어렵습니다. FE가 처리량을 제한하는 상황에서는 4x를 계획 수립 시 개선 폭의 하한으로 보세요.
다음 두 가지를 확인하면 해당 경로가 사용 중인지 알 수 있습니다.
- 반복 호출 시
fe.audit.log에 원본 SQL 대신Stmt=EXECUTE(stmtId)가 표시됩니다. - 포인트 쿼리의 경우
EXPLAIN의 스캔 노드에SHORT-CIRCUIT가 표시됩니다.
FE CPU가 여전히 처리량을 제한한다면 Observer를 늘리고 JDBC 부하 분산을 사용하세요: jdbc:mysql:loadbalance://h1,h2,h3/db?useServerPrepStmts=true&cachePrepStmts=true.
더 읽어보기
- 고동시성 포인트 쿼리(전체 가이드)
- 고동시성 포인트 쿼리 기능 카드
- 높은 처리량의 INSERT를 위한 Group Commit
- 포인트 쿼리가 아닌 쿼리를 위한 데이터 프루닝
- Unique Key:
store_row_column과 결합하여 KV 방식의 포인트 조회에 사용하는 테이블 모델입니다.