This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new e7fd2894912 [Fix] Support unordered query results in COPY TO TsFile 
(#18928)
e7fd2894912 is described below

commit e7fd289491233a66ab5895c4866f92f7842f8801
Author: shuwenwei <[email protected]>
AuthorDate: Tue Oct 20 19:30:55 2026 +0800

    [Fix] Support unordered query results in COPY TO TsFile (#18928)
---
 .../it/insertquery/IoTDBInsertQueryIT.java         |   4 +-
 .../it/query/recent/IoTDBComplexQueryIT.java       |   9 +
 .../query/recent/copyto/IoTDBCopyToTsFileIT.java   | 185 ++++++++++++++
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |  11 +
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |  11 +
 .../process/copyto/TableCopyToOperator.java        |   7 +-
 .../process/copyto/tsfile/CopyToTsFileOptions.java |   7 +
 .../planner/plan/node/PlanGraphJsonPrinter.java    |  11 +
 .../plan/planner/plan/node/PlanGraphPrinter.java   |  36 +++
 .../relational/analyzer/StatementAnalyzer.java     |   8 +-
 .../relational/planner/TableLogicalPlanner.java    |  59 ++++-
 .../TableModelTypeProviderExtractor.java           |   3 +-
 .../plan/relational/planner/node/CopyToNode.java   |  13 +-
 .../optimizations/LogicalOptimizeFactory.java      |   2 +
 .../RemoveRedundantSortForCopyTo.java              |  88 +++++++
 .../optimizations/TransformSortToStreamSort.java   |   4 +
 .../optimizations/UnaliasSymbolReferences.java     |   3 +-
 .../plan/relational/sql/parser/AstBuilder.java     |   4 +
 .../OrderingPropertyPropagationTest.java           | 281 +++++++++++++++++++++
 .../db/relational/grammar/sql/RelationalSql.g4     |   4 +-
 20 files changed, 728 insertions(+), 22 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/insertquery/IoTDBInsertQueryIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/insertquery/IoTDBInsertQueryIT.java
index 75a7725c414..d6aa44a18f1 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/insertquery/IoTDBInsertQueryIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/insertquery/IoTDBInsertQueryIT.java
@@ -461,7 +461,7 @@ public class IoTDBInsertQueryIT {
           e.getMessage(),
           e.getMessage()
               .contains(
-                  "700: line 1:9: mismatched input 'INSERT'. Expecting: '(', 
'ANALYZE', 'EXECUTE', <query>"));
+                  "700: line 1:9: mismatched input 'INSERT'. Expecting: '(', 
'ANALYZE', 'COPY', 'EXECUTE', <query>"));
     }
 
     try {
@@ -473,7 +473,7 @@ public class IoTDBInsertQueryIT {
           e.getMessage(),
           e.getMessage()
               .contains(
-                  "700: line 1:17: mismatched input 'INSERT'. Expecting: '(', 
'EXECUTE', 'VERBOSE', <query>"));
+                  "700: line 1:17: mismatched input 'INSERT'. Expecting: '(', 
'COPY', 'EXECUTE', 'VERBOSE', <query>"));
     }
   }
 
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
index 55b42142249..e3ca746ecb7 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBComplexQueryIT.java
@@ -82,6 +82,15 @@ public class IoTDBComplexQueryIT {
         DATABASE_NAME);
   }
 
+  @Test
+  public void testTableLessQueryWithOrderBy() {
+    tableResultSetEqualTest(
+        "SELECT CAST(1 AS TIMESTAMP) AS time, 1 AS v ORDER BY v, time",
+        new String[] {"time", "v"},
+        new String[] {"1970-01-01T00:00:00.001Z,1,"},
+        DATABASE_NAME);
+  }
+
   @Test
   public void testTableLessQuery() {
     String[] expectedHeader;
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
index 388d833f700..f0093a16bbc 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
@@ -32,6 +32,9 @@ import org.apache.tsfile.file.metadata.IDeviceID;
 import org.apache.tsfile.file.metadata.StringArrayDeviceID;
 import org.apache.tsfile.file.metadata.TimeseriesMetadata;
 import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.read.query.dataset.ResultSet;
+import org.apache.tsfile.read.v4.ITsFileReader;
+import org.apache.tsfile.read.v4.TsFileReaderBuilder;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Assert;
@@ -45,6 +48,10 @@ import org.slf4j.LoggerFactory;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -76,6 +83,7 @@ public class IoTDBCopyToTsFileIT {
 
   @BeforeClass
   public static void setUp() throws Exception {
+    
EnvFactory.getEnv().getConfig().getCommonConfig().setMaxTsBlockLineNumber(2);
     EnvFactory.getEnv().initClusterEnvironment();
     prepareTableData(createSqls);
   }
@@ -97,6 +105,66 @@ public class IoTDBCopyToTsFileIT {
     EnvFactory.getEnv().cleanClusterEnvironment();
   }
 
+  @Test
+  public void testExplainCopyTable() throws Exception {
+    assertExplainCopyToContainsNodes("explain", "table1");
+  }
+
+  @Test
+  public void testExplainCopyQueryJson() throws Exception {
+    assertExplainCopyToContainsNodes(
+        "explain (format json)", "(select * from table1 order by time desc 
limit 3)");
+  }
+
+  @Test
+  public void testExplainAnalyzeCopyTable() throws Exception {
+    assertExplainCopyToContainsNodes("explain analyze", "table1(time, tag1, 
tag2, s1, s2)");
+  }
+
+  @Test
+  public void testExplainAnalyzeCopyQueryJson() throws Exception {
+    assertExplainCopyToContainsNodes(
+        "explain analyze verbose (format json)",
+        "(select * from table1 order by time desc limit 3)");
+  }
+
+  private void assertExplainCopyToContainsNodes(String explainPrefix, String 
source)
+      throws Exception {
+    File targetDirectory = 
Files.createTempDirectory("iotdb-explain-copy-to").toFile();
+    File targetFile = new File(targetDirectory, "result.tsfile");
+    String targetPath = targetFile.getAbsolutePath().replace("\\", 
"\\\\").replace("'", "''");
+    String exportDirectoryPath =
+        targetDirectory.getAbsolutePath().replace("\\", "\\\\").replace("'", 
"''");
+
+    try (ITableSession session =
+        EnvFactory.getEnv().getTableSessionConnectionWithDB(DATABASE_NAME)) {
+      session.executeNonQueryStatement(
+          "set configuration \"copy_to_allowed_export_dirs\"='" + 
exportDirectoryPath + "'");
+      try (SessionDataSet dataSet =
+          session.executeQueryStatement(
+              explainPrefix
+                  + " copy "
+                  + source
+                  + " to '"
+                  + targetPath
+                  + "' (memory_threshold 1000000)")) {
+        SessionDataSet.DataIterator iterator = dataSet.iterator();
+        StringBuilder output = new StringBuilder();
+        while (iterator.next()) {
+          output.append(iterator.getString(1)).append('\n');
+        }
+        String result = output.toString();
+        Assert.assertTrue(result, result.contains("TableScan"));
+        Assert.assertTrue(result, result.contains("CopyTo"));
+      } finally {
+        session.executeNonQueryStatement("set configuration 
\"copy_to_allowed_export_dirs\"=''");
+      }
+    } finally {
+      Files.deleteIfExists(targetFile.toPath());
+      Files.deleteIfExists(targetDirectory.toPath());
+    }
+  }
+
   @Test
   public void testCopyTable()
       throws IoTDBConnectionException, StatementExecutionException, 
IOException {
@@ -743,6 +811,123 @@ public class IoTDBCopyToTsFileIT {
     }
   }
 
+  @Test
+  public void testCopyWithDescendingTime() throws Exception {
+    assertCopyToRows(
+        "COPY (SELECT time, tag1, tag2, s1, s2 FROM table1 ORDER BY time DESC, 
tag1)"
+            + " TO 'copy_descending_time.tsfile' WITH (MEMORY_THRESHOLD 
1000000)",
+        List.of("tag1", "tag2"),
+        "1,t1_1,t2,1,1",
+        "2,t1_1,t2,2,2",
+        "3,t1_1,t2,3,3",
+        "1,t1_2,t2,1,1",
+        "2,t1_2,t2,2,2",
+        "3,t1_2,t2,3,3");
+  }
+
+  @Test
+  public void testCopyWithUnorderedTimeByField() throws Exception {
+    assertCopyToRows(
+        "COPY (SELECT time, tag1, tag2, s1, s2 FROM table1 ORDER BY s1 % 2, 
time DESC, tag1)"
+            + " TO 'copy_field_order.tsfile' WITH (MEMORY_THRESHOLD 1000000)",
+        List.of("tag1", "tag2"),
+        "1,t1_1,t2,1,1",
+        "2,t1_1,t2,2,2",
+        "3,t1_1,t2,3,3",
+        "1,t1_2,t2,1,1",
+        "2,t1_2,t2,2,2",
+        "3,t1_2,t2,3,3");
+  }
+
+  @Test
+  public void testCopyWithDescendingTimeAndLimitOffset() throws Exception {
+    assertCopyToRows(
+        "COPY (SELECT time, tag1, tag2, s1, s2 FROM table1"
+            + " ORDER BY time DESC, tag1 OFFSET 1 LIMIT 3) TO 
'copy_descending_limit.tsfile'"
+            + " WITH (MEMORY_THRESHOLD 1000000)",
+        List.of("tag1", "tag2"),
+        "2,t1_1,t2,2,2",
+        "2,t1_2,t2,2,2",
+        "3,t1_2,t2,3,3");
+  }
+
+  @Test
+  public void testCopyWithDescendingAliasedTimeAndReorderedTags() throws 
Exception {
+    assertCopyToRows(
+        "COPY (SELECT time AS t, tag1 AS renamed_tag1, tag2 AS renamed_tag2, 
s1, s2"
+            + " FROM table1 ORDER BY t DESC, renamed_tag1)"
+            + " TO 'copy_descending_alias.tsfile'"
+            + " WITH (TIME t, TAGS(renamed_tag2, renamed_tag1), 
MEMORY_THRESHOLD 1000000)",
+        List.of("renamed_tag2", "renamed_tag1"),
+        "1,t2,t1_1,1,1",
+        "2,t2,t1_1,2,2",
+        "3,t2,t1_1,3,3",
+        "1,t2,t1_2,1,1",
+        "2,t2,t1_2,2,2",
+        "3,t2,t1_2,3,3");
+  }
+
+  @Test
+  public void testCopyWithDescendingTimeWithoutTags() throws Exception {
+    assertCopyToRows(
+        "COPY (SELECT time, s1, s2 FROM table1 WHERE tag1 = 't1_1' ORDER BY 
time DESC)"
+            + " TO 'copy_descending_no_tags.tsfile' WITH (MEMORY_THRESHOLD 
1000000)",
+        Collections.emptyList(),
+        "1,1,1",
+        "2,2,2",
+        "3,3,3");
+  }
+
+  private void assertCopyToRows(String sql, List<String> tagColumns, String... 
expectedRows)
+      throws Exception {
+    long deviceCount;
+    try (ITableSession session =
+            EnvFactory.getEnv().getTableSessionConnectionWithDB(DATABASE_NAME);
+        SessionDataSet dataSet = session.executeQueryStatement(sql)) {
+      SessionDataSet.DataIterator iterator = dataSet.iterator();
+      Assert.assertTrue(iterator.next());
+      targetFilePath = iterator.getString(1);
+      Assert.assertTrue(new File(targetFilePath).isFile());
+      Assert.assertEquals(expectedRows.length, iterator.getLong(2));
+      deviceCount = iterator.getLong(3);
+      Assert.assertTrue(iterator.getLong(4) > 0);
+      Assert.assertEquals("table1", iterator.getString(5));
+      Assert.assertEquals(tagColumns.toString(), iterator.getString(7));
+      Assert.assertFalse(iterator.next());
+    }
+
+    List<String> columns = new ArrayList<>(tagColumns);
+    columns.add("s1");
+    columns.add("s2");
+    List<String> actualRows = new ArrayList<>();
+    Map<List<String>, Long> lastTimeByDevice = new HashMap<>();
+    try (ITsFileReader reader = new TsFileReaderBuilder().file(new 
File(targetFilePath)).build();
+        ResultSet result = reader.query("table1", columns, Long.MIN_VALUE, 
Long.MAX_VALUE)) {
+      while (result.next()) {
+        long time = result.getLong(1);
+        StringBuilder row = new StringBuilder(Long.toString(time));
+        List<String> tags = new ArrayList<>();
+        for (int i = 0; i < tagColumns.size(); i++) {
+          String tag = result.isNull(i + 2) ? null : result.getString(i + 2);
+          tags.add(tag);
+          row.append(',').append(tag);
+        }
+        Long lastTime = lastTimeByDevice.put(tags, time);
+        if (lastTime != null) {
+          Assert.assertTrue(time > lastTime);
+        }
+        row.append(',').append(result.getInt(tagColumns.size() + 2));
+        row.append(',').append(result.getInt(tagColumns.size() + 3));
+        actualRows.add(row.toString());
+      }
+    }
+    Assert.assertEquals(deviceCount, lastTimeByDevice.size());
+    List<String> expected = new ArrayList<>(Arrays.asList(expectedRows));
+    Collections.sort(expected);
+    Collections.sort(actualRows);
+    Assert.assertEquals(expected, actualRows);
+  }
+
   @Test
   public void testFindTimeColumn()
       throws IoTDBConnectionException, StatementExecutionException, 
IOException {
diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index d6fcfc64eb1..db0fa44dbc5 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -297,6 +297,9 @@ public final class DataNodeQueryMessages {
           + " (configure copy_to_allowed_export_dirs to permit it): ";
   public static final String DATA_TYPE_OF_TARGET_TIME_COLUMN_IS_NOT =
       "Data type of target time column is not TIMESTAMP";
+  public static final String
+      
EXCEPTION_SPECIFIED_TIME_COLUMN_ARG_DOES_NOT_EXIST_IN_THE_QUERY_RESULT_34791B26 
=
+          "Specified TIME column %s does not exist in the query result";
   public static final String DUPLICATE_COLUMN_NAMES_IN_QUERY_DATASET =
       "Duplicate column names in query dataset.";
   public static final String SOME_SPECIFIED_TAG_COLUMNS_ARE_NOT_EXIST_IN =
@@ -4054,6 +4057,14 @@ public final class DataNodeQueryMessages {
   public static final String 
EXCEPTION_LOAD_CONSENSUS_INVALID_PIECE_REF_F3498507 =
       "Invalid LOAD consensus piece ref: path %s, offset %d, size %d";
 
+  public static final String MESSAGE_COPYTO_ARG_38343199 = "CopyTo-%s";
+  public static final String MESSAGE_TARGETFILE_ARG_EC484B24 = "TargetFile: 
%s";
+  public static final String MESSAGE_FORMAT_ARG_CE5DC4EE = "Format: %s";
+  public static final String MESSAGE_TARGETTABLE_ARG_00BE4714 = "TargetTable: 
%s";
+  public static final String MESSAGE_TIMECOLUMN_ARG_54F6A2A6 = "TimeColumn: 
%s";
+  public static final String MESSAGE_TAGCOLUMNS_ARG_6520A0C2 = "TagColumns: 
%s";
+  public static final String MESSAGE_MEMORYTHRESHOLD_ARG_3676AB46 = 
"MemoryThreshold: %s";
+
   public static final String 
EXCEPTION_CHUNKDATA_CONTAINS_NO_PHYSICAL_CHUNK_607BCFC8 =
       "ChunkData contains no physical chunk";
   public static final String EXCEPTION_DEVICE_CANNOT_BE_NULL_F1EB20B6 = 
"device cannot be null";
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index e06c0ded22c..f500838c1ca 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -285,6 +285,9 @@ public final class DataNodeQueryMessages {
       "COPY TO 目标路径不在允许的导出目录内(可通过 copy_to_allowed_export_dirs 配置):";
   public static final String DATA_TYPE_OF_TARGET_TIME_COLUMN_IS_NOT =
       "目标时间列的数据类型不是 TIMESTAMP";
+  public static final String
+      
EXCEPTION_SPECIFIED_TIME_COLUMN_ARG_DOES_NOT_EXIST_IN_THE_QUERY_RESULT_34791B26 
=
+          "指定的 TIME 列 %s 在查询结果中不存在";
   public static final String DUPLICATE_COLUMN_NAMES_IN_QUERY_DATASET =
       "查询数据集中存在重复的列名。";
   public static final String SOME_SPECIFIED_TAG_COLUMNS_ARE_NOT_EXIST_IN =
@@ -4810,6 +4813,14 @@ public final class DataNodeQueryMessages {
   public static final String 
EXCEPTION_LOAD_CONSENSUS_INVALID_PIECE_REF_F3498507 =
       "无效的 LOAD 共识分片引用:路径 %s,偏移 %d,大小 %d";
 
+  public static final String MESSAGE_COPYTO_ARG_38343199 = "CopyTo-%s";
+  public static final String MESSAGE_TARGETFILE_ARG_EC484B24 = "目标文件:%s";
+  public static final String MESSAGE_FORMAT_ARG_CE5DC4EE = "格式:%s";
+  public static final String MESSAGE_TARGETTABLE_ARG_00BE4714 = "目标表:%s";
+  public static final String MESSAGE_TIMECOLUMN_ARG_54F6A2A6 = "时间列:%s";
+  public static final String MESSAGE_TAGCOLUMNS_ARG_6520A0C2 = "TAG 列:%s";
+  public static final String MESSAGE_MEMORYTHRESHOLD_ARG_3676AB46 = "内存阈值:%s";
+
   public static final String 
EXCEPTION_CHUNKDATA_CONTAINS_NO_PHYSICAL_CHUNK_607BCFC8 =
       "ChunkData 中不包含任何物理 chunk";
   public static final String EXCEPTION_DEVICE_CANNOT_BE_NULL_F1EB20B6 = 
"device 不能为 null";
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/TableCopyToOperator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/TableCopyToOperator.java
index f43fbd2b730..14ead195e8d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/TableCopyToOperator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/TableCopyToOperator.java
@@ -83,7 +83,12 @@ public class TableCopyToOperator implements ProcessOperator {
 
   @Override
   public TsBlock next() throws Exception {
-    IFormatCopyToWriter formatWriter = getWriter();
+    IFormatCopyToWriter formatWriter;
+    try {
+      formatWriter = getWriter();
+    } catch (IOException e) {
+      throw newCopyToWriteError(e);
+    }
     if (!childOperator.hasNext()) {
       try {
         formatWriter.seal();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
index 22b2e25a6df..ffdba2995b1 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
@@ -201,6 +201,13 @@ public class CopyToTsFileOptions implements CopyToOptions {
     if (columns.size() != columnHeaders.size()) {
       throw new 
SemanticException(DataNodeQueryMessages.DUPLICATE_COLUMN_NAMES_IN_QUERY_DATASET);
     }
+    if (!generateNewTimeColumn && !columns.contains(targetTimeColumn)) {
+      throw new SemanticException(
+          String.format(
+              DataNodeQueryMessages
+                  
.EXCEPTION_SPECIFIED_TIME_COLUMN_ARG_DOES_NOT_EXIST_IN_THE_QUERY_RESULT_34791B26,
+              targetTimeColumn));
+    }
     if (foundTagColumns != targetTagColumns.size()) {
       throw new SemanticException(
           DataNodeQueryMessages.SOME_SPECIFIED_TAG_COLUMNS_ARE_NOT_EXIST_IN);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java
index ecd9d2a58a5..a5f6e061453 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphJsonPrinter.java
@@ -31,6 +31,8 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputN
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.UnionNode;
+import 
org.apache.iotdb.db.queryengine.execution.operator.process.copyto.tsfile.CopyToTsFileOptions;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.CopyToNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExplainAnalyzeNode;
@@ -96,6 +98,15 @@ public class PlanGraphJsonPrinter {
     } else if (node instanceof ExplainAnalyzeNode) {
       ExplainAnalyzeNode n = (ExplainAnalyzeNode) node;
       properties.add("ChildPermittedOutputs", 
toJsonArray(n.getChildPermittedOutputs()));
+    } else if (node instanceof CopyToNode n) {
+      properties.addProperty("TargetFile", n.getTargetFilePath());
+      properties.addProperty("Format", 
n.getCopyToOptions().getFormat().name());
+      if (n.getCopyToOptions() instanceof CopyToTsFileOptions options) {
+        properties.addProperty("TargetTable", options.getTargetTableName());
+        properties.addProperty("TimeColumn", options.getTargetTimeColumn());
+        properties.add("TagColumns", 
toJsonArray(options.getTargetTagColumns()));
+        properties.addProperty("MemoryThreshold", 
options.getTargetMemoryThreshold());
+      }
     } else if (node instanceof TableScanNode) {
       buildTableScanProperties(properties, (TableScanNode) node);
     } else if (node instanceof ExchangeNode) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
index 3316299ea17..557bd714d92 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanGraphPrinter.java
@@ -51,6 +51,7 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValueFi
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValuesNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.WindowNode;
 import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
+import 
org.apache.iotdb.db.queryengine.execution.operator.process.copyto.tsfile.CopyToTsFileOptions;
 import org.apache.iotdb.db.queryengine.plan.analyze.TemplatedInfo;
 import org.apache.iotdb.db.queryengine.plan.expression.Expression;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.process.AggregationMergeSortNode;
@@ -95,6 +96,7 @@ import 
org.apache.iotdb.db.queryengine.plan.planner.plan.parameter.CrossSeriesAg
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.parameter.DeviceViewIntoPathDescriptor;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.parameter.IntoPathDescriptor;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.AggregationTreeDeviceViewScanNode;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.CopyToNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.CteScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExchangeNode;
@@ -866,6 +868,40 @@ public class PlanGraphPrinter implements 
PlanVisitor<List<String>, PlanGraphPrin
     return render(node, boxValue, context);
   }
 
+  @Override
+  public List<String> visitCopyTo(CopyToNode node, GraphContext context) {
+    List<String> boxValue = new ArrayList<>();
+    boxValue.add(
+        String.format(
+            DataNodeQueryMessages.MESSAGE_COPYTO_ARG_38343199, 
node.getPlanNodeId().getId()));
+    boxValue.add(
+        String.format(
+            DataNodeQueryMessages.MESSAGE_TARGETFILE_ARG_EC484B24, 
node.getTargetFilePath()));
+    boxValue.add(
+        String.format(
+            DataNodeQueryMessages.MESSAGE_FORMAT_ARG_CE5DC4EE,
+            node.getCopyToOptions().getFormat()));
+    if (node.getCopyToOptions() instanceof CopyToTsFileOptions options) {
+      boxValue.add(
+          String.format(
+              DataNodeQueryMessages.MESSAGE_TARGETTABLE_ARG_00BE4714,
+              options.getTargetTableName()));
+      boxValue.add(
+          String.format(
+              DataNodeQueryMessages.MESSAGE_TIMECOLUMN_ARG_54F6A2A6,
+              options.getTargetTimeColumn()));
+      boxValue.add(
+          String.format(
+              DataNodeQueryMessages.MESSAGE_TAGCOLUMNS_ARG_6520A0C2,
+              options.getTargetTagColumns()));
+      boxValue.add(
+          String.format(
+              DataNodeQueryMessages.MESSAGE_MEMORYTHRESHOLD_ARG_3676AB46,
+              options.getTargetMemoryThreshold()));
+    }
+    return render(node, boxValue, context);
+  }
+
   @Override
   public List<String> visitOutput(OutputNode node, GraphContext context) {
     List<String> boxValue = new ArrayList<>();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
index e1c9db8d0f2..5214b30be99 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
@@ -1410,7 +1410,9 @@ public class StatementAnalyzer {
       queryContext.setExplainType(ExplainType.EXPLAIN);
       queryContext.setExplainOutputFormat(node.getOutputFormat());
       analysis.setFinishQueryAfterAnalyze();
-      return visitQuery((Query) node.getStatement(), context);
+      Scope scope = process(node.getStatement(), context);
+      analysis.setScope(node, scope);
+      return scope;
     }
 
     @Override
@@ -1437,7 +1439,9 @@ public class StatementAnalyzer {
       queryContext.setExplainType(ExplainType.EXPLAIN_ANALYZE);
       queryContext.setVerbose(node.isVerbose());
       queryContext.setExplainOutputFormat(node.getOutputFormat());
-      return visitQuery((Query) node.getStatement(), context);
+      Scope scope = process(node.getStatement(), context);
+      analysis.setScope(node, scope);
+      return scope;
     }
 
     @Override
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableLogicalPlanner.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableLogicalPlanner.java
index 8f9928b7ad2..c68ebb7e254 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableLogicalPlanner.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableLogicalPlanner.java
@@ -29,12 +29,15 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchem
 import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.QualifiedObjectName;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.metadata.TableSchema;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.Assignments;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.OrderingScheme;
+import org.apache.iotdb.commons.queryengine.plan.relational.planner.SortOrder;
 import org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.FilterNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.LimitNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OffsetNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Query;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Table;
@@ -48,6 +51,7 @@ import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
 import org.apache.iotdb.db.queryengine.common.QueryId;
 import org.apache.iotdb.db.queryengine.common.header.DatasetHeader;
+import 
org.apache.iotdb.db.queryengine.execution.operator.process.copyto.tsfile.CopyToTsFileOptions;
 import org.apache.iotdb.db.queryengine.execution.warnings.WarningCollector;
 import org.apache.iotdb.db.queryengine.metric.QueryPlanCostMetricSet;
 import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
@@ -98,7 +102,10 @@ import org.apache.tsfile.read.common.type.StringType;
 import org.apache.tsfile.read.common.type.TypeFactory;
 
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 
@@ -247,7 +254,7 @@ public class TableLogicalPlanner {
       return createRelationPlan(analysis, (Query) statement);
     }
     if (statement instanceof Explain) {
-      return createRelationPlan(analysis, (Query) ((Explain) 
statement).getStatement());
+      return planStatementWithoutOutput(analysis, ((Explain) 
statement).getStatement());
     }
     if (statement instanceof WrappedStatement) {
       return createRelationPlan(analysis, ((WrappedStatement) statement));
@@ -346,14 +353,14 @@ public class TableLogicalPlanner {
     int columnNumber = 0;
     // TODO perfect the logic of outputDescriptor
     if (plan.getRoot() instanceof CopyToNode) {
+      outputs.addAll(plan.getRoot().getOutputSymbols());
       for (ColumnHeader columnHeader :
           ((CopyToNode) 
plan.getRoot()).getCopyToOptions().getRespColumnHeaders()) {
-        outputs.add(new Symbol(columnHeader.getColumnName()));
         names.add(columnHeader.getColumnName());
         columnHeaders.add(columnHeader);
       }
-    } else if (queryContext.isExplainAnalyze() && 
!queryContext.isInnerTriggeredQuery()) {
-      outputs.add(new Symbol(ColumnHeaderConstant.EXPLAIN_ANALYZE));
+    } else if (plan.getRoot() instanceof ExplainAnalyzeNode) {
+      outputs.addAll(plan.getRoot().getOutputSymbols());
       names.add(ColumnHeaderConstant.EXPLAIN_ANALYZE);
       columnHeaders.add(new ColumnHeader(ColumnHeaderConstant.EXPLAIN_ANALYZE, 
TSDataType.TEXT));
     } else {
@@ -621,8 +628,7 @@ public class TableLogicalPlanner {
   }
 
   private RelationPlan planExplainAnalyze(final ExplainAnalyze statement, 
final Analysis analysis) {
-    RelationPlan originalQueryPlan =
-        createRelationPlan(analysis, (Query) (statement.getStatement()));
+    RelationPlan originalQueryPlan = planStatementWithoutOutput(analysis, 
statement.getStatement());
     Symbol symbol =
         symbolAllocator.newSymbol(ColumnHeaderConstant.EXPLAIN_ANALYZE, 
StringType.getInstance());
 
@@ -635,7 +641,9 @@ public class TableLogicalPlanner {
             queryContext.getTimeOut(),
             symbol,
             // recording permittedOutputs of ExplainAnalyzeNode's child
-            getChildPermittedOutputs(analysis, statement.getStatement(), 
originalQueryPlan),
+            statement.getStatement() instanceof CopyTo
+                ? originalQueryPlan.getRoot().getOutputSymbols()
+                : getChildPermittedOutputs(analysis, statement.getStatement(), 
originalQueryPlan),
             statement.getOutputFormat());
     return new RelationPlan(
         newRoot,
@@ -656,17 +664,50 @@ public class TableLogicalPlanner {
         .infer(analysis, innerQueryRelationPlan, 
innerQueryRespDatasetHeader.getColumnHeaders());
     
statement.getOptions().check(innerQueryRespDatasetHeader.getColumnHeaders());
 
+    PlanNode copyToSource = innerQueryRelationPlan.getRoot();
+    if (statement.getOptions() instanceof CopyToTsFileOptions tsFileOptions
+        && !tsFileOptions.isGenerateNewTimeColumn()) {
+      List<ColumnHeader> columnHeaders = 
innerQueryRespDatasetHeader.getColumnHeaders();
+      List<Symbol> outputSymbols = outputNode.getOutputSymbols();
+      Map<String, Symbol> columnSymbols = new HashMap<>();
+      for (int i = 0; i < columnHeaders.size(); i++) {
+        columnSymbols.put(columnHeaders.get(i).getColumnName(), 
outputSymbols.get(i));
+      }
+      Map<Symbol, SortOrder> orderings = new LinkedHashMap<>();
+      for (String tagColumn : tsFileOptions.getTargetTagColumns()) {
+        orderings.put(columnSymbols.get(tagColumn), SortOrder.ASC_NULLS_FIRST);
+      }
+      orderings.put(
+          columnSymbols.get(tsFileOptions.getTargetTimeColumn()), 
SortOrder.ASC_NULLS_FIRST);
+      // Sort the complete inner query result, after its LIMIT and OFFSET.
+      copyToSource =
+          new SortNode(
+              queryContext.getQueryId().genPlanNodeId(),
+              copyToSource,
+              new OrderingScheme(new ArrayList<>(orderings.keySet()), 
orderings),
+              false,
+              false);
+      analysis.setSortNode(true);
+    }
+
+    List<Symbol> outputSymbols = new ArrayList<>();
+    for (ColumnHeader columnHeader : 
statement.getOptions().getRespColumnHeaders()) {
+      outputSymbols.add(
+          symbolAllocator.newSymbol(
+              columnHeader.getColumnName(), 
TypeFactory.getType(columnHeader.getColumnType())));
+    }
     PlanNode newRoot =
         new CopyToNode(
             queryContext.getQueryId().genPlanNodeId(),
-            innerQueryRelationPlan.getRoot(),
+            copyToSource,
             statement.getTargetFileName(),
             statement.getOptions(),
             // recording permittedOutputs of CopyToNode's child
             getChildPermittedOutputs(
                 analysis, statement.getQueryStatement(), 
innerQueryRelationPlan),
             innerQueryRespDatasetHeader,
-            outputNode.getOutputSymbols());
+            outputNode.getOutputSymbols(),
+            outputSymbols);
     return new RelationPlan(
         newRoot,
         innerQueryRelationPlan.getScope(),
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableModelTypeProviderExtractor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableModelTypeProviderExtractor.java
index aaf3725ec2a..356efb97406 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableModelTypeProviderExtractor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/distribute/TableModelTypeProviderExtractor.java
@@ -107,7 +107,8 @@ public class TableModelTypeProviderExtractor {
 
     @Override
     public Void visitCopyTo(CopyToNode node, Void context) {
-      visitPlan(node.getChild(), context);
+      node.getChild().accept(this, context);
+      addOutputSymbolsToTypeProvider(node);
       return null;
     }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/CopyToNode.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/CopyToNode.java
index 2e251bd7a19..ff6899444c4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/CopyToNode.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/node/CopyToNode.java
@@ -41,6 +41,7 @@ public class CopyToNode extends SingleChildProcessNode {
   private final List<Symbol> childPermittedOutputs;
   private final List<Symbol> innerQueryOutputSymbols;
   private final DatasetHeader innerQueryDatasetHeader;
+  private final List<Symbol> outputSymbols;
 
   public CopyToNode(
       PlanNodeId id,
@@ -49,7 +50,8 @@ public class CopyToNode extends SingleChildProcessNode {
       CopyToOptions copyToOptions,
       List<Symbol> childPermittedOutputs,
       DatasetHeader innerQueryDatasetHeader,
-      List<Symbol> innerQueryOutputSymbols) {
+      List<Symbol> innerQueryOutputSymbols,
+      List<Symbol> outputSymbols) {
     super(id);
     this.child = child;
     this.targetFilePath = targetFilePath;
@@ -57,6 +59,7 @@ public class CopyToNode extends SingleChildProcessNode {
     this.childPermittedOutputs = childPermittedOutputs;
     this.innerQueryDatasetHeader = innerQueryDatasetHeader;
     this.innerQueryOutputSymbols = innerQueryOutputSymbols;
+    this.outputSymbols = outputSymbols;
   }
 
   public DatasetHeader getInnerQueryDatasetHeader() {
@@ -88,7 +91,8 @@ public class CopyToNode extends SingleChildProcessNode {
         copyToOptions,
         childPermittedOutputs,
         innerQueryDatasetHeader,
-        innerQueryOutputSymbols);
+        innerQueryOutputSymbols,
+        outputSymbols);
   }
 
   @Override
@@ -98,7 +102,7 @@ public class CopyToNode extends SingleChildProcessNode {
 
   @Override
   public List<Symbol> getOutputSymbols() {
-    return copyToOptions.getOutputSymbols();
+    return outputSymbols;
   }
 
   @Override
@@ -115,7 +119,8 @@ public class CopyToNode extends SingleChildProcessNode {
         copyToOptions,
         childPermittedOutputs,
         innerQueryDatasetHeader,
-        innerQueryOutputSymbols);
+        innerQueryOutputSymbols,
+        outputSymbols);
   }
 
   @Override
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
index 78bb5f984a1..ddf09e7f98d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/LogicalOptimizeFactory.java
@@ -211,6 +211,8 @@ public class LogicalOptimizeFactory {
     ImmutableList.Builder<PlanOptimizer> optimizerBuilder = 
ImmutableList.builder();
 
     optimizerBuilder.add(
+        // Remove superseded COPY TO ordering before scan ordering and limit 
pushdown.
+        new RemoveRedundantSortForCopyTo(),
         new IterativeOptimizer(
             plannerContext,
             ruleStats,
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/RemoveRedundantSortForCopyTo.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/RemoveRedundantSortForCopyTo.java
new file mode 100644
index 00000000000..d4295cce17e
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/RemoveRedundantSortForCopyTo.java
@@ -0,0 +1,88 @@
+/*
+ * 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.iotdb.db.queryengine.plan.relational.planner.optimizations;
+
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
+import 
org.apache.iotdb.db.queryengine.execution.operator.process.copyto.tsfile.CopyToTsFileOptions;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.PlanVisitor;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.CopyToNode;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.ExplainAnalyzeNode;
+
+import com.google.common.collect.ImmutableList;
+
+/** Removes an inner sort superseded by the TAG/TIME sort required for COPY TO 
TsFile. */
+public class RemoveRedundantSortForCopyTo implements PlanOptimizer {
+
+  @Override
+  public PlanNode optimize(PlanNode plan, Context context) {
+    if (!context.getAnalysis().hasSortNode()) {
+      return plan;
+    }
+    return plan.accept(new Rewriter(), null);
+  }
+
+  private static class Rewriter implements PlanVisitor<PlanNode, Void> {
+
+    @Override
+    public PlanNode visitPlan(PlanNode node, Void context) {
+      // COPY TO is a statement, so only its output and EXPLAIN ANALYZE 
wrappers can precede it.
+      if (node instanceof OutputNode || node instanceof ExplainAnalyzeNode) {
+        PlanNode child = node.getChildren().get(0);
+        PlanNode rewrittenChild = child.accept(this, context);
+        return child == rewrittenChild
+            ? node
+            : node.replaceChildren(ImmutableList.of(rewrittenChild));
+      }
+      return node;
+    }
+
+    @Override
+    public PlanNode visitCopyTo(CopyToNode node, Void context) {
+      if (node.getCopyToOptions() instanceof CopyToTsFileOptions options
+          && !options.isGenerateNewTimeColumn()
+          && node.getChild() instanceof SortNode sortNode) {
+        PlanNode source = removeRedundantSort(sortNode.getChild());
+        if (source == sortNode.getChild()) {
+          return node;
+        }
+        return node.replaceChildren(
+            
ImmutableList.of(sortNode.replaceChildren(ImmutableList.of(source))));
+      }
+      return node;
+    }
+
+    private PlanNode removeRedundantSort(PlanNode node) {
+      if (node instanceof SortNode sortNode) {
+        return sortNode.getChild();
+      }
+      // Stop at LIMIT, OFFSET and order-dependent computations; only identity 
projections are safe.
+      if (node instanceof ProjectNode projectNode && projectNode.isIdentity()) 
{
+        PlanNode child = removeRedundantSort(projectNode.getChild());
+        return child == projectNode.getChild()
+            ? node
+            : projectNode.replaceChildren(ImmutableList.of(child));
+      }
+      return node;
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
index a89b2d31694..90c2c91931d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/TransformSortToStreamSort.java
@@ -97,6 +97,10 @@ public class TransformSortToStreamSort implements 
PlanOptimizer {
       context.setCanTransform(false);
 
       DeviceTableScanNode deviceTableScanNode = context.getTableScanNode();
+      if (deviceTableScanNode == null) {
+        node.setChild(child);
+        return node;
+      }
       Map<Symbol, ColumnSchema> tableColumnSchema;
       if (deviceTableScanNode instanceof ExternalTsFileScanNode) {
         tableColumnSchema =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
index 131ba8af383..89e377bcffe 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/UnaliasSymbolReferences.java
@@ -484,7 +484,8 @@ public class UnaliasSymbolReferences implements 
PlanOptimizer {
               node.getCopyToOptions(),
               newChildPermittedOutputs,
               node.getInnerQueryDatasetHeader(),
-              newInnerQueryOutputSymbols),
+              newInnerQueryOutputSymbols,
+              node.getOutputSymbols()),
           mapping);
     }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
index 1d2417ea644..71686b52cf7 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
@@ -1874,6 +1874,8 @@ public class AstBuilder extends 
RelationalSqlBaseVisitor<Node> {
     Statement innerStatement;
     if (ctx.query() != null) {
       innerStatement = (Statement) visit(ctx.query());
+    } else if (ctx.copyToStatement() != null) {
+      innerStatement = (Statement) visit(ctx.copyToStatement());
     } else if (ctx.executeStatement() != null) {
       innerStatement = (Statement) visit(ctx.executeStatement());
     } else {
@@ -1905,6 +1907,8 @@ public class AstBuilder extends 
RelationalSqlBaseVisitor<Node> {
     Statement innerStatement;
     if (ctx.query() != null) {
       innerStatement = (Statement) visit(ctx.query());
+    } else if (ctx.copyToStatement() != null) {
+      innerStatement = (Statement) visit(ctx.copyToStatement());
     } else if (ctx.executeStatement() != null) {
       innerStatement = (Statement) visit(ctx.executeStatement());
     } else {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/OrderingPropertyPropagationTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/OrderingPropertyPropagationTest.java
index cf0d52bfc66..4d972991c39 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/OrderingPropertyPropagationTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/planner/optimizations/OrderingPropertyPropagationTest.java
@@ -20,31 +20,42 @@
 package org.apache.iotdb.db.queryengine.plan.relational.planner.optimizations;
 
 import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.OrderingScheme;
 import org.apache.iotdb.commons.queryengine.plan.relational.planner.Symbol;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.AggregationNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.CollectNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.FillNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.MergeSortNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OffsetNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.PatternRecognitionNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.RowNumberNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.SortNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.StreamSortNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TableFunctionProcessorNode;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.TopKRankingNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ValueFillNode;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.WindowNode;
+import org.apache.iotdb.commons.schema.column.ColumnHeader;
+import 
org.apache.iotdb.db.queryengine.execution.operator.process.copyto.tsfile.CopyToTsFileOptions;
 import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
+import org.apache.iotdb.db.queryengine.plan.relational.planner.node.CopyToNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.DeviceTableScanNode;
+import org.apache.iotdb.db.queryengine.plan.statement.component.Ordering;
 
 import org.junit.Test;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import static 
org.apache.iotdb.commons.queryengine.plan.relational.planner.SortOrder.ASC_NULLS_FIRST;
 import static 
org.apache.iotdb.commons.queryengine.plan.relational.planner.SortOrder.DESC_NULLS_LAST;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 public class OrderingPropertyPropagationTest {
@@ -437,6 +448,276 @@ public class OrderingPropertyPropagationTest {
     assertTrue(findNodes(diff, StreamSortNode.class).isEmpty());
   }
 
+  @Test
+  public void testPlainQueryPreservesOrderBy() {
+    PlanTester tester = new PlanTester();
+    PlanNode logical =
+        tester
+            .createPlan("SELECT time, tag1, tag2, tag3, s1 FROM table1 ORDER 
BY time DESC")
+            .getRootNode();
+    List<SortNode> sorts = findNodes(logical, SortNode.class);
+    assertEquals(1, sorts.size());
+    Symbol time = sorts.get(0).getOrderingScheme().getOrderBy().get(0);
+    assertEquals("time", time.getName());
+    assertEquals(DESC_NULLS_LAST, 
sorts.get(0).getOrderingScheme().getOrdering(time));
+
+    SortNode queryRoot = Mockito.spy(sorts.get(0));
+    PlanOptimizer.Context context = Mockito.mock(PlanOptimizer.Context.class);
+    Mockito.when(context.getAnalysis()).thenReturn(tester.getAnalysis());
+    assertSame(queryRoot, new 
RemoveRedundantSortForCopyTo().optimize(queryRoot, context));
+    Mockito.verify(queryRoot, Mockito.never()).getChild();
+    Mockito.verify(queryRoot, Mockito.never()).getChildren();
+    assertSame(logical, new RemoveRedundantSortForCopyTo().optimize(logical, 
context));
+
+    List<MergeSortNode> merges = findNodes(tester, MergeSortNode.class);
+    assertFalse(merges.isEmpty());
+    for (MergeSortNode merge : merges) {
+      assertEquals(List.of(time), merge.getOrderingScheme().getOrderBy());
+      assertEquals(DESC_NULLS_LAST, 
merge.getOrderingScheme().getOrdering(time));
+    }
+  }
+
+  @Test
+  public void testCopyToWithoutFromKeepsRegularSort() {
+    for (String prefix : List.of("", "EXPLAIN ", "EXPLAIN ANALYZE ")) {
+      PlanTester tester = new PlanTester();
+      PlanNode logical =
+          tester
+              .createPlan(
+                  prefix
+                      + "COPY (SELECT CAST(1 AS TIMESTAMP) AS time, 1 AS v)"
+                      + " TO 'constant.tsfile' (MEMORY_THRESHOLD 1000000)")
+              .getRootNode();
+      CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+      assertFalse(((CopyToTsFileOptions) 
copyTo.getCopyToOptions()).isGenerateNewTimeColumn());
+      List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+      assertEquals(1, sorts.size());
+      assertCopyToOrdering(copyTo, sorts.get(0).getOrderingScheme(), "time");
+      assertTrue(findNodes(logical, StreamSortNode.class).isEmpty());
+      assertTrue(findNodes(logical, DeviceTableScanNode.class).isEmpty());
+
+      CopyToNode distributedCopyTo = onlyCopyTo(findNodes(tester, 
CopyToNode.class));
+      sorts = findNodes(tester, SortNode.class);
+      assertEquals(1, sorts.size());
+      assertCopyToOrdering(distributedCopyTo, 
sorts.get(0).getOrderingScheme(), "time");
+      assertTrue(findNodes(tester, StreamSortNode.class).isEmpty());
+      assertTrue(findNodes(tester, DeviceTableScanNode.class).isEmpty());
+    }
+  }
+
+  @Test
+  public void testCopyToSortUsesStreamingScanAndDistributedMerge() {
+    for (String source :
+        List.of(
+            "table1",
+            "table1(time, tag1, tag2, tag3, s1)",
+            "(SELECT time, tag1, tag2, tag3, s1 FROM table1)",
+            "(SELECT time, tag1, tag2, tag3, s1 FROM table1 ORDER BY time 
DESC)",
+            "(SELECT time, tag1, tag2, tag3, s1 FROM table1 ORDER BY s1 
DESC)")) {
+      PlanTester tester = new PlanTester();
+      PlanNode logical = tester.createPlan("COPY " + source + " TO 
'copy.tsfile'").getRootNode();
+      assertTrue(tester.getAnalysis().isQuery());
+      assertTrue(tester.getAnalysis().hasSortNode());
+      CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+      List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+      assertEquals(1, sorts.size());
+      assertTrue(sorts.get(0) instanceof StreamSortNode);
+      assertCopyToOrdering(
+          copyTo, sorts.get(0).getOrderingScheme(), "tag1", "tag2", "tag3", 
"time");
+
+      CopyToNode distributedCopyTo = onlyCopyTo(findNodes(tester, 
CopyToNode.class));
+      List<MergeSortNode> merges = findNodes(tester, MergeSortNode.class);
+      assertFalse(merges.isEmpty());
+      for (MergeSortNode merge : merges) {
+        assertCopyToOrdering(
+            distributedCopyTo, merge.getOrderingScheme(), "tag1", "tag2", 
"tag3", "time");
+      }
+      assertTrue(findNodes(tester, SortNode.class).isEmpty());
+      List<DeviceTableScanNode> scans = findNodes(tester, 
DeviceTableScanNode.class);
+      assertFalse(scans.isEmpty());
+      scans.forEach(scan -> assertEquals(Ordering.ASC, scan.getScanOrder()));
+    }
+  }
+
+  @Test
+  public void testExplainCopyToRemovesRedundantSort() {
+    for (String prefix : List.of("EXPLAIN ", "EXPLAIN ANALYZE ")) {
+      PlanTester tester = new PlanTester();
+      PlanNode logical =
+          tester
+              .createPlan(
+                  prefix
+                      + "COPY (SELECT time, tag1, tag2, tag3, s1 FROM table1 
ORDER BY time DESC)"
+                      + " TO 'copy_explain.tsfile'")
+              .getRootNode();
+      CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+      List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+      assertEquals(1, sorts.size());
+      assertTrue(sorts.get(0) instanceof StreamSortNode);
+      assertCopyToOrdering(
+          copyTo, sorts.get(0).getOrderingScheme(), "tag1", "tag2", "tag3", 
"time");
+      onlyCopyTo(findNodes(tester, CopyToNode.class));
+      assertTrue(findNodes(tester, SortNode.class).isEmpty());
+    }
+  }
+
+  @Test
+  public void testCopyToSortPreservesInnerTopKAndOffset() {
+    for (String orderColumn : List.of("time", "s1")) {
+      PlanTester tester = new PlanTester();
+      PlanNode logical =
+          tester
+              .createPlan(
+                  "COPY (SELECT time, s1 FROM table1 ORDER BY "
+                      + orderColumn
+                      + " DESC OFFSET 2 LIMIT 3) TO 'copy_limit.tsfile'")
+              .getRootNode();
+      CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+      List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+      assertEquals(1, sorts.size());
+      assertCopyToOrdering(copyTo, sorts.get(0).getOrderingScheme(), "time");
+      List<OffsetNode> offsets = findNodes(sorts.get(0), OffsetNode.class);
+      assertEquals(1, offsets.size());
+      assertEquals(2, offsets.get(0).getCount());
+      List<TopKNode> topKs = findNodes(offsets.get(0), TopKNode.class);
+      assertEquals(1, topKs.size());
+      Symbol orderSymbol =
+          copyTo.getInnerQueryOutputSymbols().get(orderColumn.equals("time") ? 
0 : 1);
+      assertEquals(5, topKs.get(0).getCount());
+      assertEquals(List.of(orderSymbol), 
topKs.get(0).getOrderingScheme().getOrderBy());
+      assertEquals(DESC_NULLS_LAST, 
topKs.get(0).getOrderingScheme().getOrdering(orderSymbol));
+
+      CopyToNode distributedCopyTo = onlyCopyTo(findNodes(tester, 
CopyToNode.class));
+      List<SortNode> outerSorts = sortsAbove(tester, OffsetNode.class);
+      assertEquals(1, outerSorts.size());
+      assertCopyToOrdering(distributedCopyTo, 
outerSorts.get(0).getOrderingScheme(), "time");
+      offsets = findNodes(tester, OffsetNode.class);
+      assertEquals(1, offsets.size());
+      assertEquals(2, offsets.get(0).getCount());
+      topKs = findNodes(tester, TopKNode.class);
+      assertFalse(topKs.isEmpty());
+      for (TopKNode topK : topKs) {
+        assertEquals(5, topK.getCount());
+        assertEquals(List.of(orderSymbol), 
topK.getOrderingScheme().getOrderBy());
+        assertEquals(DESC_NULLS_LAST, 
topK.getOrderingScheme().getOrdering(orderSymbol));
+      }
+    }
+  }
+
+  @Test
+  public void testCopyToSortPreservesOffsetWithoutLimit() {
+    PlanTester tester = new PlanTester();
+    PlanNode logical =
+        tester
+            .createPlan(
+                "COPY (SELECT time, s1 FROM table1 ORDER BY time DESC OFFSET 
2)"
+                    + " TO 'copy_offset.tsfile'")
+            .getRootNode();
+    CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+    List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+    assertEquals(2, sorts.size());
+    assertCopyToOrdering(copyTo, sorts.get(0).getOrderingScheme(), "time");
+    List<OffsetNode> offsets = findNodes(sorts.get(0), OffsetNode.class);
+    assertEquals(1, offsets.size());
+    assertEquals(2, offsets.get(0).getCount());
+    List<SortNode> innerSorts = findNodes(offsets.get(0), SortNode.class);
+    assertEquals(1, innerSorts.size());
+    Symbol time = copyTo.getInnerQueryOutputSymbols().get(0);
+    assertEquals(DESC_NULLS_LAST, 
innerSorts.get(0).getOrderingScheme().getOrdering(time));
+
+    onlyCopyTo(findNodes(tester, CopyToNode.class));
+    assertEquals(1, sortsAbove(tester, OffsetNode.class).size());
+    offsets = findNodes(tester, OffsetNode.class);
+    assertEquals(1, offsets.size());
+    assertEquals(2, offsets.get(0).getCount());
+  }
+
+  @Test
+  public void testCopyToSortResolvesAliasesAndTargetTagOrder() {
+    PlanTester tester = new PlanTester();
+    PlanNode logical =
+        tester
+            .createPlan(
+                "COPY (SELECT time AS t, tag1 AS k1, tag2 AS k2, tag3 AS k3, 
s1 AS v"
+                    + " FROM table1) TO 'copy_alias.tsfile' WITH (TIME t, 
TAGS(k2, k1, k3))")
+            .getRootNode();
+    CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+    List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+    assertEquals(1, sorts.size());
+    assertTrue(sorts.get(0) instanceof StreamSortNode);
+    assertCopyToOrdering(copyTo, sorts.get(0).getOrderingScheme(), "k2", "k1", 
"k3", "t");
+
+    CopyToNode distributedCopyTo = onlyCopyTo(findNodes(tester, 
CopyToNode.class));
+    List<MergeSortNode> merges = findNodes(tester, MergeSortNode.class);
+    assertFalse(merges.isEmpty());
+    for (MergeSortNode merge : merges) {
+      assertCopyToOrdering(distributedCopyTo, merge.getOrderingScheme(), "k2", 
"k1", "k3", "t");
+    }
+    assertTrue(findNodes(tester, SortNode.class).isEmpty());
+  }
+
+  @Test
+  public void testCopyToGeneratedTimePreservesInnerOrderBy() {
+    PlanTester tester = new PlanTester();
+    PlanNode logical =
+        tester
+            .createPlan("COPY (SELECT s1 FROM table1 ORDER BY s1 DESC) TO 
'copy_generated.tsfile'")
+            .getRootNode();
+    CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+    assertTrue(((CopyToTsFileOptions) 
copyTo.getCopyToOptions()).isGenerateNewTimeColumn());
+    List<SortNode> sorts = findNodes(copyTo.getChild(), SortNode.class);
+    assertEquals(1, sorts.size());
+    Symbol value = copyTo.getInnerQueryOutputSymbols().get(0);
+    assertEquals(List.of(value), 
sorts.get(0).getOrderingScheme().getOrderBy());
+    assertEquals(DESC_NULLS_LAST, 
sorts.get(0).getOrderingScheme().getOrdering(value));
+
+    onlyCopyTo(findNodes(tester, CopyToNode.class));
+    sorts = findNodes(tester, SortNode.class);
+    assertFalse(sorts.isEmpty());
+    for (SortNode sort : sorts) {
+      assertEquals(List.of(value), sort.getOrderingScheme().getOrderBy());
+      assertEquals(DESC_NULLS_LAST, 
sort.getOrderingScheme().getOrdering(value));
+    }
+  }
+
+  @Test
+  public void testCopyToGeneratedTimeNeedsNoSort() {
+    PlanTester tester = new PlanTester();
+    PlanNode logical =
+        tester
+            .createPlan("COPY (SELECT tag1, tag2, tag3, s1 FROM table1) TO 
'copy_generated.tsfile'")
+            .getRootNode();
+    CopyToNode copyTo = onlyCopyTo(findNodes(logical, CopyToNode.class));
+    assertTrue(((CopyToTsFileOptions) 
copyTo.getCopyToOptions()).isGenerateNewTimeColumn());
+    assertFalse(tester.getAnalysis().hasSortNode());
+    assertTrue(findNodes(logical, SortNode.class).isEmpty());
+    onlyCopyTo(findNodes(tester, CopyToNode.class));
+    assertTrue(findNodes(tester, SortNode.class).isEmpty());
+    assertTrue(findNodes(tester, MergeSortNode.class).isEmpty());
+  }
+
+  private static CopyToNode onlyCopyTo(List<CopyToNode> nodes) {
+    assertEquals(1, nodes.size());
+    return nodes.get(0);
+  }
+
+  private static void assertCopyToOrdering(
+      CopyToNode copyTo, OrderingScheme ordering, String... columnNames) {
+    List<String> outputNames =
+        copyTo.getInnerQueryDatasetHeader().getColumnHeaders().stream()
+            .map(ColumnHeader::getColumnName)
+            .collect(Collectors.toList());
+    List<Symbol> expected = new ArrayList<>();
+    for (String columnName : columnNames) {
+      int index = outputNames.indexOf(columnName);
+      assertTrue(columnName, index >= 0);
+      expected.add(copyTo.getInnerQueryOutputSymbols().get(index));
+    }
+    assertEquals(expected, ordering.getOrderBy());
+    expected.forEach(symbol -> assertEquals(ASC_NULLS_FIRST, 
ordering.getOrdering(symbol)));
+    assertTrue(copyTo.getChild().getOutputSymbols().containsAll(expected));
+  }
+
   private static PlanTester plan(String sql) {
     PlanTester tester = new PlanTester();
     tester.createPlan(sql);
diff --git 
a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
 
b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
index f4d80cfc244..706b464f9ff 100644
--- 
a/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
+++ 
b/iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/RelationalSql.g4
@@ -968,8 +968,8 @@ copyToStatementOption
 // ------------------------------------------- Query Statement 
---------------------------------------------------------
 queryStatement
     : query                                                        
#statementDefault
-    | EXPLAIN ('(' FORMAT identifier ')')? (query | executeStatement | 
executeImmediateStatement) #explain
-    | EXPLAIN ANALYZE VERBOSE? ('(' FORMAT identifier ')')? (query | 
executeStatement | executeImmediateStatement) #explainAnalyze
+    | EXPLAIN ('(' FORMAT identifier ')')? (query | copyToStatement | 
executeStatement | executeImmediateStatement) #explain
+    | EXPLAIN ANALYZE VERBOSE? ('(' FORMAT identifier ')')? (query | 
copyToStatement | executeStatement | executeImmediateStatement) #explainAnalyze
     ;
 
 query

Reply via email to