public static GroupByTagNode deserialize(ByteBuffer byteBuffer) {
    // Tag keys.
    List<String> tagKeys = ReadWriteIOUtils.readStringList(byteBuffer);

    // Tag values to aggregation descriptors.
    int numOfEntries = ReadWriteIOUtils.readInt(byteBuffer);
    Map<List<String>, List<CrossSeriesAggregationDescriptor>> tagValuesToAggregationDescriptors =
        new HashMap<>();
    while (numOfEntries > 0) {
      List<String> tagValues = ReadWriteIOUtils.readStringList(byteBuffer);
      List<CrossSeriesAggregationDescriptor> aggregationDescriptors = new ArrayList<>();
      int numOfAggregationDescriptors = ReadWriteIOUtils.readInt(byteBuffer);
      while (numOfAggregationDescriptors > 0) {
        byte isNotNull = ReadWriteIOUtils.readByte(byteBuffer);
        if (isNotNull == 1) {
          aggregationDescriptors.add(CrossSeriesAggregationDescriptor.deserialize(byteBuffer));
        } else {
          aggregationDescriptors.add(null);
        }
        numOfAggregationDescriptors -= 1;
      }
      tagValuesToAggregationDescriptors.put(tagValues, aggregationDescriptors);
      numOfEntries -= 1;
    }

    // Output column names.
    List<String> outputColumnNames = ReadWriteIOUtils.readStringList(byteBuffer);

    // Group by time parameter.
    byte hasGroupByTimeParameter = ReadWriteIOUtils.readByte(byteBuffer);
    GroupByTimeParameter groupByTimeParameter = null;
    if (hasGroupByTimeParameter == 1) {
      groupByTimeParameter = GroupByTimeParameter.deserialize(byteBuffer);
    }

    // Scan order.
    Ordering scanOrder = Ordering.values()[ReadWriteIOUtils.readInt(byteBuffer)];

    PlanNodeId planNodeId = PlanNodeId.deserialize(byteBuffer);
    return new GroupByTagNode(
        planNodeId,
        groupByTimeParameter,
        scanOrder,
        tagKeys,
        tagValuesToAggregationDescriptors,
        outputColumnNames);
  }
The CrossSeriesAggregationDescriptor may be null if there exists a key containing no
timeseries.

<p>e.g. we have following timeseries:

<ul>
  <li>root.sg.d1.s1(k1=v1)
  <li>root.sg.d1.s2(k1=v1)
  <li>root.sg.d2.s1(k1=v2)
  <li>root.sg.d3.s1(k1=v2)
</ul>

Then the query <code>
SELECT avg(s1), avg(s2) FROM root.sg.** GROUP BY TAGS(k1)
</code>will generate a {@link GroupByTagNode} with the <code>TagValuesToAggregationDescriptors
</code> as below: <code>
  {
    ["v1"]: [["avg(root.sg.d1.s1)"], ["avg(root.sg.d1.s2)"]],
    ["v2"]: [["avg(root.sg.d2.s1)","avg(root.sg.d3.s1)"], null],
  }
</code>

<p>So we should use it carefully with null values.
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/process/GroupByTagNode.java