This is an automated email from the ASF dual-hosted git repository.
HappenLee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 6e4d434e41f [fix](fe) Enable profiles for computed DML commands
(#64551)
6e4d434e41f is described below
commit 6e4d434e41f971a8ec25c9d12585cda64185c2a4
Author: HappenLee <[email protected]>
AuthorDate: Thu Jun 25 15:48:42 2026 +0800
[fix](fe) Enable profiles for computed DML commands (#64551)
Problem Summary:
Some DML statements that execute through the compute path were treated
as profile-unsafe statements, so `UPDATE`, `MERGE INTO`, and `DELETE
FROM ... USING` did not produce query/load profiles even when profiling
was enabled.
This PR marks `UpdateCommand`, `MergeIntoCommand`, and
`DeleteFromUsingCommand` as profile-safe. Regular `DeleteFromCommand` is
left unchanged because it should not produce a compute profile.
### Release note
Support profiles for `UPDATE`, `MERGE INTO`, and `DELETE FROM ... USING`
statements.
### Check List (For Author)
- Test: Regression test / Build
- `./build.sh --fe`
- `./run-regression-test.sh --run --conf
/tmp/doris-regression-conf-run-path.groovy -d query_profile -s
dml_profile_safe`
- Behavior changed: Yes
- `UPDATE`, `MERGE INTO`, and `DELETE FROM ... USING` now generate
profiles when profiling is enabled.
- Does this need documentation: No
---
.../plans/commands/DeleteFromUsingCommand.java | 7 +-
.../trees/plans/commands/SupportProfile.java | 47 ++++++
.../trees/plans/commands/UpdateCommand.java | 7 +-
.../plans/commands/merge/MergeIntoCommand.java | 8 +-
.../java/org/apache/doris/qe/StmtExecutor.java | 6 +
...ptive_pipeline_task_serial_read_on_limit.groovy | 6 +-
.../suites/query_profile/dml_profile_safe.groovy | 181 +++++++++++++++++++++
7 files changed, 256 insertions(+), 6 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java
index 2364c690712..6d6b53d2254 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromUsingCommand.java
@@ -33,7 +33,7 @@ import java.util.Optional;
/**
* delete from unique key table.
*/
-public class DeleteFromUsingCommand extends DeleteFromCommand {
+public class DeleteFromUsingCommand extends DeleteFromCommand implements
SupportProfile {
private final Optional<LogicalPlan> cte;
private final boolean hasOrderByLimit;
@@ -75,6 +75,11 @@ public class DeleteFromUsingCommand extends
DeleteFromCommand {
return logicalQuery;
}
+ @Override
+ public List<String> getTargetTableNameParts() {
+ return nameParts;
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitDeleteFromUsingCommand(this, context);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SupportProfile.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SupportProfile.java
new file mode 100644
index 00000000000..cddca92a3bc
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SupportProfile.java
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.commands;
+
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.nereids.util.RelationUtil;
+import org.apache.doris.qe.ConnectContext;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Commands that can generate query profiles.
+ */
+public interface SupportProfile {
+
+ List<String> getTargetTableNameParts();
+
+ /**
+ * Check whether the target table is an OLAP table.
+ */
+ default boolean isTargetTableOlap(ConnectContext ctx) {
+ try {
+ List<String> qualifiedTableName =
RelationUtil.getQualifierName(ctx, getTargetTableNameParts());
+ TableIf table = RelationUtil.getTable(qualifiedTableName,
ctx.getEnv(), Optional.empty());
+ return table instanceof OlapTable;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
index 4e717bbb1d6..736577f8804 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateCommand.java
@@ -76,7 +76,7 @@ import javax.annotation.Nullable;
* =>
* insert into t1 (c1, c3) select t2.c1, t2.c3 * 100 from t1 join t2 inner
join t3 on t2.id = t3.id where t1.id = t2.id
*/
-public class UpdateCommand extends Command implements ForwardWithSync,
Explainable {
+public class UpdateCommand extends Command implements ForwardWithSync,
Explainable, SupportProfile {
private final List<EqualTo> assignments;
private final List<String> nameParts;
private final @Nullable String tableAlias;
@@ -295,6 +295,11 @@ public class UpdateCommand extends Command implements
ForwardWithSync, Explainab
return logicalQuery;
}
+ @Override
+ public List<String> getTargetTableNameParts() {
+ return nameParts;
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitUpdateCommand(this, context);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
index 4a68f739651..e002e596c7d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/merge/MergeIntoCommand.java
@@ -54,6 +54,7 @@ import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.Command;
import org.apache.doris.nereids.trees.plans.commands.ForwardWithSync;
import org.apache.doris.nereids.trees.plans.commands.IcebergMergeCommand;
+import org.apache.doris.nereids.trees.plans.commands.SupportProfile;
import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
import org.apache.doris.nereids.trees.plans.commands.info.DMLCommandType;
import
org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
@@ -84,7 +85,7 @@ import java.util.Optional;
/**
* merge into table
*/
-public class MergeIntoCommand extends Command implements ForwardWithSync,
Explainable {
+public class MergeIntoCommand extends Command implements ForwardWithSync,
Explainable, SupportProfile {
private static final String BRANCH_LABEL =
"__DORIS_MERGE_INTO_BRANCH_LABEL__";
private final List<String> targetNameParts;
@@ -153,6 +154,11 @@ public class MergeIntoCommand extends Command implements
ForwardWithSync, Explai
return RelationUtil.getTable(qualifiedTableName, ctx.getEnv(),
Optional.empty());
}
+ @Override
+ public List<String> getTargetTableNameParts() {
+ return targetNameParts;
+ }
+
private OlapTable getTargetTable(ConnectContext ctx) {
TableIf table = getTargetTableIf(ctx);
if (!(table instanceof OlapTable) || !((OlapTable)
table).getEnableUniqueKeyMergeOnWrite()) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
index 62773925744..8d4f29cda86 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
@@ -102,6 +102,7 @@ import
org.apache.doris.nereids.trees.plans.commands.Forward;
import org.apache.doris.nereids.trees.plans.commands.LoadCommand;
import org.apache.doris.nereids.trees.plans.commands.PrepareCommand;
import org.apache.doris.nereids.trees.plans.commands.Redirect;
+import org.apache.doris.nereids.trees.plans.commands.SupportProfile;
import org.apache.doris.nereids.trees.plans.commands.TransactionCommand;
import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
import
org.apache.doris.nereids.trees.plans.commands.insert.BatchInsertIntoTableCommand;
@@ -1146,6 +1147,11 @@ public class StmtExecutor {
return true;
}
+ // Computed DML profiles are currently only supported for OLAP target
tables.
+ if (plan instanceof SupportProfile && ((SupportProfile)
plan).isTargetTableOlap(context)) {
+ return true;
+ }
+
// Generate profile for:
// 1. CreateTableCommand(mainly for create as select).
// 2. LoadCommand.
diff --git
a/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy
b/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy
index 4a36401b02b..4ccd45af04a 100644
---
a/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy
+++
b/regression-test/suites/query_profile/adaptive_pipeline_task_serial_read_on_limit.groovy
@@ -21,8 +21,8 @@ import groovy.json.StringEscapeUtils
import org.apache.doris.regression.action.ProfileAction
def verifyProfileContent = { suiteContext, stmt, serialReadOnLimit ->
- // Sleep 500ms to wait for the profile collection
- Thread.sleep(500)
+ // Sleep 1000ms to wait for the profile collection
+ Thread.sleep(1000)
// Get profile list by using getProfileList
def profileAction = new ProfileAction(suiteContext)
List profileData = profileAction.getProfileList()
@@ -122,4 +122,4 @@ suite('adaptive_pipeline_task_serial_read_on_limit') {
sql "set enable_adaptive_pipeline_task_serial_read_on_limit=false;"
sql """select "enable_adaptive_pipeline_task_serial_read_on_limit=false",
* from adaptive_pipeline_task_serial_read_on_limit limit 1000000;"""
assertTrue(verifyProfileContent(context, "select
\"enable_adaptive_pipeline_task_serial_read_on_limit=false\", * from
adaptive_pipeline_task_serial_read_on_limit limit 1000000;", false))
-}
\ No newline at end of file
+}
diff --git a/regression-test/suites/query_profile/dml_profile_safe.groovy
b/regression-test/suites/query_profile/dml_profile_safe.groovy
new file mode 100644
index 00000000000..17618df34d1
--- /dev/null
+++ b/regression-test/suites/query_profile/dml_profile_safe.groovy
@@ -0,0 +1,181 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import groovy.json.JsonSlurper
+
+def getProfileList = { masterHTTPAddr ->
+ def dst = 'http://' + masterHTTPAddr
+ def conn = new URL(dst + "/rest/v1/query_profile").openConnection()
+ conn.setRequestMethod("GET")
+ def password = context.config.feHttpPassword == null ? "" :
context.config.feHttpPassword
+ def encoding = Base64.getEncoder()
+ .encodeToString((context.config.feHttpUser + ":" +
password).getBytes("UTF-8"))
+ conn.setRequestProperty("Authorization", "Basic ${encoding}")
+ return conn.getInputStream().getText()
+}
+
+def getMasterHttpAddress = { allFrontends ->
+ for (def frontend : allFrontends) {
+ if (frontend[8] == "true") {
+ return frontend[1] + ":" + frontend[3]
+ }
+ }
+ throw new IllegalStateException("master frontend not found")
+}
+
+def hasProfile = { masterHTTPAddr, taskType, sqlToken ->
+ def wholeString = getProfileList(masterHTTPAddr)
+ def profileListData = new JsonSlurper().parseText(wholeString).data.rows
+ for (def profileList : profileListData) {
+ def profileTaskType = profileList["Task Type"].toString()
+ def stmt = profileList["Sql Statement"].toString().toLowerCase()
+ if (profileTaskType == taskType &&
stmt.contains(sqlToken.toLowerCase())) {
+ return true
+ }
+ }
+ return false
+}
+
+def waitForProfile = { masterHTTPAddr, taskType, sqlToken ->
+ for (int i = 0; i < 20; i++) {
+ if (hasProfile(masterHTTPAddr, taskType, sqlToken)) {
+ return true
+ }
+ Thread.sleep(500)
+ }
+ return false
+}
+
+suite("dml_profile_safe") {
+ sql """set enable_nereids_planner = true;"""
+ sql """set enable_fallback_to_original_planner = false;"""
+
+ sql """drop table if exists dml_profile_update_target;"""
+ sql """drop table if exists dml_profile_merge_target;"""
+ sql """drop table if exists dml_profile_merge_source;"""
+ sql """drop table if exists dml_profile_delete_using_target;"""
+ sql """drop table if exists dml_profile_delete_using_source;"""
+ sql """drop table if exists dml_profile_delete_command_target;"""
+
+ sql """
+ create table dml_profile_update_target (
+ k int,
+ v int
+ )
+ unique key(k)
+ distributed by hash(k) buckets 1
+ properties(
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ );
+ """
+
+ sql """
+ create table dml_profile_merge_target (
+ k int,
+ v int
+ )
+ unique key(k)
+ distributed by hash(k) buckets 1
+ properties(
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ );
+ """
+
+ sql """
+ create table dml_profile_merge_source (
+ k int,
+ v int
+ )
+ duplicate key(k)
+ distributed by hash(k) buckets 1
+ properties("replication_num" = "1");
+ """
+
+ sql """
+ create table dml_profile_delete_using_target (
+ k int,
+ v int
+ )
+ unique key(k)
+ distributed by hash(k) buckets 1
+ properties(
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ );
+ """
+
+ sql """
+ create table dml_profile_delete_using_source (
+ k int
+ )
+ duplicate key(k)
+ distributed by hash(k) buckets 1
+ properties("replication_num" = "1");
+ """
+
+ sql """
+ create table dml_profile_delete_command_target (
+ k int,
+ v int
+ )
+ duplicate key(k)
+ distributed by hash(k) buckets 1
+ properties("replication_num" = "1");
+ """
+
+ sql """insert into dml_profile_update_target values (1, 10), (2, 20);"""
+ sql """insert into dml_profile_merge_target values (1, 10);"""
+ sql """insert into dml_profile_merge_source values (1, 11), (2, 22);"""
+ sql """insert into dml_profile_delete_using_target values (1, 10), (2,
20);"""
+ sql """insert into dml_profile_delete_using_source values (1);"""
+ sql """insert into dml_profile_delete_command_target values (1, 10), (2,
20);"""
+ sql """sync;"""
+
+ def masterHTTPAddr = getMasterHttpAddress(sql """show frontends;""")
+
+ sql """clean all profile;"""
+ sql """set enable_profile = true;"""
+ sql """set profile_level = 2;"""
+
+ sql """update dml_profile_update_target set v = v + 1 where k = 1;"""
+ assertTrue(waitForProfile(masterHTTPAddr, "LOAD",
+ "update dml_profile_update_target set v = v + 1 where k = 1"))
+
+ sql """
+ merge into dml_profile_merge_target t
+ using dml_profile_merge_source s
+ on t.k = s.k
+ when matched then update set v = s.v
+ when not matched then insert values(s.k, s.v);
+ """
+ assertTrue(waitForProfile(masterHTTPAddr, "LOAD", "merge into
dml_profile_merge_target"))
+
+ sql """
+ delete from dml_profile_delete_using_target t
+ using dml_profile_delete_using_source s
+ where t.k = s.k;
+ """
+ assertTrue(waitForProfile(masterHTTPAddr, "LOAD", "delete from
dml_profile_delete_using_target"))
+
+ sql """clean all profile;"""
+ sql """delete from dml_profile_delete_command_target where k = 1;"""
+ Thread.sleep(1000)
+ assertFalse(hasProfile(masterHTTPAddr, "LOAD", "delete from
dml_profile_delete_command_target"))
+ assertFalse(hasProfile(masterHTTPAddr, "QUERY", "delete from
dml_profile_delete_command_target"))
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]