Подготовленные операторы
Кратко Apache Doris поддерживает подготовленные операторы на стороне сервера по сетевому протоколу MySQL. Если в URL JDBC задать
useServerPrepStmts=true, FE разбирает оператор и строит его план один раз за сеанс, а затем повторно использует этот план при каждомEXECUTE. При большом QPS точечных запросов путь short-circuit повышает пропускную способность примерно в 4 раза; при пакетных INSERT и запросах с повторяющейся структурой больше не приходится выполнять разбор и построение плана для каждой строки.

Зачем использовать подготовленные операторы в Apache Doris?
Подготовленные операторы Apache Doris кешируют план после разбора в рамках сеанса и повторно используют его при каждом EXECUTE. Это устраняет затраты на разбор, анализ и оптимизацию на стороне FE, которые преобладают в нагрузках с большим QPS. Большинство аналитических движков выполняют разбор и построение плана для каждого запроса. В ежедневном задании по формированию отчёта эти затраты незаметны. Но они становятся определяющими, когда сервис начинает выполнять один и тот же запрос тысячи раз в секунду. Пользовательская панель при каждом нажатии считывает строку по первичному ключу. Хранилище признаков при каждом предсказании считывает вектор по идентификатору сущности. Загрузчик массовых данных снова и снова выполняет один и тот же INSERT INTO t VALUES (?, ?, ?).
Во всех трёх случаях текст SQL одинаковый; меняются только параметры. Повторный разбор и построение плана этого оператора при каждом вызове зря расходуют CPU FE. Уже при нескольких сотнях QPS FE достигает предела загрузки, прежде чем BE начинает выполнять существенную работу, и добавление узлов BE не помогает. В такой ситуации помогает PreparedStatement.
- При большом QPS узким местом становится планировщик Frontend, а не движок хранения.
- Повторяющиеся запросы при каждом вызове требуют затрат на разбор, анализ, оптимизацию и сериализацию.
- Отдельное KV-хранилище требует слишком много усилий ради снижения стоимости точечных запросов.
Что представляет собой подготовленный оператор Apache Doris?
Подготовленный оператор Apache Doris представляет собой серверную реализацию пары команд сетевого протокола MySQL COM_STMT_PREPARE / COM_STMT_EXECUTE (с COM_STMT_CLOSE для удаления из кеша). Эти команды обрабатывает FE, используя кеш планов для каждого сеанса. Когда клиент открывает соединение с useServerPrepStmts=true, драйвер JDBC отправляет COM_STMT_PREPARE один раз для каждого уникального текста SQL. FE разбирает и анализирует оператор, а затем сохраняет план в кеше сеанса, используя идентификатор оператора в качестве ключа. При каждом последующем EXECUTE передаются только байты параметров в бинарном формате. FE подставляет значения в кешированный план и выполняет его.
Основные термины
useServerPrepStmts=true: флаг URL JDBC, который переключает драйвер MySQL Connector/J с подстановки значений в строку на стороне клиента на обмен по бинарному протоколу с помощьюCOM_STMT_PREPARE/COM_STMT_EXECUTE.cachePrepStmts=true: отдельный флаг JDBC, который кеширует дескриптор подготовленного оператора на клиенте. Без него каждый вызов повторно отправляетCOM_STMT_PREPARE, сводя ускорение на нет.- Кеш операторов сеанса: ассоциативный массив в памяти FE (
ConnectContext.preparedStatementContextMap), содержащий разобранныйPrepareCommand,StatementContextи необязательныйShortCircuitQueryContext. Его размер ограничен параметромmax_prepared_stmt_count(по умолчанию 100,000). - Заполнитель (
?): позиционный параметр, значение которого задаётся при выполнении. Один оператор может содержать до 65,536 таких параметров. ShortCircuitQueryContext: дополнительный кеш, который существует наряду с подготовленным планом, если запрос подходит для выполнения по пути short-circuit для точечных запросов. Он содержит сериализованную таблицу дескрипторов, выходные выражения и UUID, по которому BE находит собственные заранее построенные структуры.
Как работает подготовленный оператор Apache Doris?
Работа подготовленного оператора Apache Doris состоит из пяти этапов: подготовка SQL на FE, кеширование плана short-circuit при соблюдении условий, выполнение с передачей параметров в бинарном формате, необязательное журналирование аудита и закрытие с удалением из кеша.
- Подготовка. Клиент отправляет
COM_STMT_PREPARE "SELECT ... WHERE k1 = ?". FE разбирает SQL, выполняет анализ Nereids, сохраняетPreparedStatementContextв ассоциативном массиве соединения и возвращает идентификатор оператора и количество параметров. - Кеширование пути short-circuit (при соблюдении условий). Когда запрос обращается к одной таблице Unique Key и содержит предикаты равенства по всем столбцам ключа, правило преобразования
LogicalResultSinkToShortCircuitPointQueryустанавливает флаг, а FE сериализует таблицу дескрипторов и выходные выражения вShortCircuitQueryContextс UUID в качестве ключа. - Выполнение. Каждый
COM_STMT_EXECUTEсодержит идентификатор оператора, битовую карту 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 для второго и последующих вызовов отображается:
Stmt=EXECUTE(-2147481418)
вместо исходного текста SQL. Это признак повторного использования кешированного плана. Дополнительно выполните EXPLAIN: в узле сканирования должно отображаться SHORT-CIRCUIT, что подтверждает работу обоих уровней.
Когда следует использовать подготовленные операторы Apache Doris?
Подготовленные операторы Apache Doris подходят для точечных запросов с большим QPS к таблицам Unique Key, повторяющихся пакетных INSERT и любых клиентских нагрузок, в которых один и тот же оператор выполняется тысячи раз в секунду. Они не подходят для разовых произвольных запросов, драйверов без корректной поддержки подготовки на стороне сервера и операторов с недетерминированными функциями на пути short-circuit.
Подходящие сценарии
- Точечные запросы с большим QPS к таблицам Unique Key, в которых преобладают затраты на разбор.
- Пакетные INSERT с Group Commit, когда загрузчик выполняет один и тот же
INSERT INTO t VALUES (?, ?, ?)тысячи раз в секунду. - Любой запрос, который один клиент повторяет достаточно часто: получение признаков, проверка состояния, проверка по спискам разрешённых значений, чтение данных онлайн-сервисами.
- Нагрузки, для которых вы предпочли бы обойтись без отдельного KV-хранилища рядом с Apache Doris.
Неподходящие сценарии
- Разовые произвольные запросы. Обмен сообщениями
PREPAREобходится дороже, чем сэкономленный разбор. - Драйверы без корректной поддержки подготовленных операторов на стороне сервера. Например, для драйвера Rust
sqlxсообщалось о проблемах с реализацией Apache Doris. Используйте MySQL Connector, который корректно работает с бинарным протоколом. - Соединения, в которых задано
useServerPrepStmts=trueбезcachePrepStmts=true. Каждый вызов повторно отправляетCOM_STMT_PREPARE, что медленнее стандартного режима подготовки на стороне клиента. - Операторы с недетерминированными функциями (
NOW(),RAND()). BE отказывается повторно использовать кешированный контекст short-circuit, поэтому экономятся только затраты на разбор. - Диапазонные запросы и запросы с агрегацией, если вы рассчитываете на ускорение short-circuit. PreparedStatement по-прежнему экономит затраты на разбор, но BE выполняет обычный распределённый план. Для таких нагрузок используйте отсечение данных.
Производительность и проверка
Doris сообщает об увеличении пропускной способности в 4 раза и более благодаря PreparedStatement в нагрузках с точечными запросами, где узким местом является CPU FE. Это тот же прирост в 4 раза, который описан с точки зрения short-circuit в карточке функции «Точечные запросы с высокой конкурентностью»: единый совокупный эффект, а не два независимых ускорения, которые можно перемножить. Полный набор оптимизаций для высокой конкурентности (Unique Key + Merge-on-Write + row store + row cache + short-circuit + PreparedStatement) достигает 30,000+ QPS на узел в YCSB на машине с 16 ядрами и 64GB памяти, а средняя задержка снижается на 96% по сравнению с исходной конфигурацией без оптимизации. Вклад PreparedStatement трудно отделить от остальных оптимизаций; когда ограничением служит FE, ориентируйтесь на рост в 4 раза как на нижнюю границу при планировании.
Две проверки позволяют убедиться, что этот путь выполнения используется:
- В
fe.audit.logдля повторных вызовов отображаетсяStmt=EXECUTE(stmtId)вместо исходного текста SQL. - Для точечных запросов
EXPLAINпоказываетSHORT-CIRCUITв узле сканирования.
Если ограничением по-прежнему остаётся CPU FE, увеличьте число Observers и используйте балансировку нагрузки JDBC: jdbc:mysql:loadbalance://h1,h2,h3/db?useServerPrepStmts=true&cachePrepStmts=true.
Дополнительные материалы
- Точечные запросы с высокой конкурентностью (полное руководство)
- Карточка функции «Точечные запросы с высокой конкурентностью»
- Group Commit для INSERT с высокой пропускной способностью
- Отсечение данных для неточечных запросов
- Unique Key: модель таблицы, которая в сочетании с
store_row_columnподходит для точечного поиска в стиле KV-хранилищ.