public static Optional<PlanNode> pruneColumns(TableScanNode node, Set<Symbol> referencedOutputs) {
    if (node instanceof AggregationTableScanNode) {
      return Optional.empty();
    }
    if (node instanceof TreeDeviceViewScanNode && referencedOutputs.isEmpty()) {
      return Optional.empty();
    }
    List<Symbol> newOutputs = new ArrayList<>();
    Map<Symbol, ColumnSchema> newAssignments = new LinkedHashMap<>();
    for (Symbol symbol : node.getOutputSymbols()) {
      if (referencedOutputs.contains(symbol)) {
        newOutputs.add(symbol);
        newAssignments.put(symbol, node.getAssignments().get(symbol));
      }
    }
    if (newOutputs.size() == node.getOutputSymbols().size()) {
      return Optional.empty();
    }

    // add entry in PushDownPredicate
    if (node.getPushDownPredicate() != null) {
      SymbolsExtractor.extractUnique(node.getPushDownPredicate())
          .forEach(symbol -> newAssignments.put(symbol, node.getAssignments().get(symbol)));
    }

    if (node instanceof DeviceTableScanNode) {
      DeviceTableScanNode deviceTableScanNode = (DeviceTableScanNode) node;
      // add time entry if TimePredicate exists
      deviceTableScanNode
          .getTimePredicate()
          .ifPresent(
              timePredicate ->
                  SymbolsExtractor.extractUnique(timePredicate)
                      .forEach(
                          symbol -> newAssignments.put(symbol, node.getAssignments().get(symbol))));

      if (node instanceof TreeDeviceViewScanNode) {
        TreeDeviceViewScanNode treeDeviceViewScanNode =
            (TreeDeviceViewScanNode) deviceTableScanNode;
        return Optional.of(
            new TreeDeviceViewScanNode(
                deviceTableScanNode.getPlanNodeId(),
                deviceTableScanNode.getQualifiedObjectName(),
                newOutputs,
                newAssignments,
                deviceTableScanNode.getDeviceEntries(),
                deviceTableScanNode.getTagAndAttributeIndexMap(),
                deviceTableScanNode.getScanOrder(),
                deviceTableScanNode.getTimePredicate().orElse(null),
                deviceTableScanNode.getPushDownPredicate(),
                deviceTableScanNode.getPushDownLimit(),
                deviceTableScanNode.getPushDownOffset(),
                deviceTableScanNode.isPushLimitToEachDevice(),
                deviceTableScanNode.containsNonAlignedDevice(),
                treeDeviceViewScanNode.getTreeDBName(),
                treeDeviceViewScanNode.getMeasurementColumnNameMap()));
      } else {
        return Optional.of(
            new DeviceTableScanNode(
                deviceTableScanNode.getPlanNodeId(),
                deviceTableScanNode.getQualifiedObjectName(),
                newOutputs,
                newAssignments,
                deviceTableScanNode.getDeviceEntries(),
                deviceTableScanNode.getTagAndAttributeIndexMap(),
                deviceTableScanNode.getScanOrder(),
                deviceTableScanNode.getTimePredicate().orElse(null),
                deviceTableScanNode.getPushDownPredicate(),
                deviceTableScanNode.getPushDownLimit(),
                deviceTableScanNode.getPushDownOffset(),
                deviceTableScanNode.isPushLimitToEachDevice(),
                deviceTableScanNode.containsNonAlignedDevice()));
      }
    } else if (node instanceof InformationSchemaTableScanNode) {
      // For the convenience of process in execution stage, column-prune for
      // InformationSchemaTableScanNode is
      // not supported now.
      return Optional.empty();
    } else {
      throw new UnsupportedOperationException(
          "Unknown TableScanNode type: " + node.getClass().getSimpleName());
    }
  }
This is a special case of PushProjectionIntoTableScan that performs only column pruning.
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/iterative/rule/PruneTableScanColumns.java